orm
Version:
NodeJS Object-relational mapping
530 lines (447 loc) • 16.4 kB
JavaScript
var _ = require("lodash");
var Hook = require("../Hook");
var Settings = require("../Settings");
var Property = require("../Property");
var ORMError = require("../Error");
var util = require("../Utilities");
var Promise = require("bluebird");
var ACCESSOR_METHODS = ["hasAccessor", "getAccessor", "setAccessor", "delAccessor", "addAccessor"];
exports.prepare = function (db, Model, associations) {
Model.hasMany = function () {
var promiseFunctionPostfix = Model.settings.get('promiseFunctionPostfix');
var name, makeKey, mergeId, mergeAssocId;
var OtherModel = Model;
var props = null;
var opts = {};
for (var i = 0; i < arguments.length; i++) {
switch (typeof arguments[i]) {
case "string":
name = arguments[i];
break;
case "function":
OtherModel = arguments[i];
break;
case "object":
if (props === null) {
props = arguments[i];
} else {
opts = arguments[i];
}
break;
}
}
if (props === null) {
props = {};
} else {
for (var k in props) {
props[k] = Property.normalize({
prop: props[k], name: k, customTypes: db.customTypes, settings: Model.settings
});
}
}
makeKey = opts.key || Settings.defaults().hasMany.key;
mergeId = util.convertPropToJoinKeyProp(
util.wrapFieldObject({
field: opts.mergeId, model: Model, altName: Model.table
}) ||
util.formatField(Model, Model.table, true, opts.reversed),
{ makeKey: makeKey, required: true }
);
mergeAssocId = util.convertPropToJoinKeyProp(
util.wrapFieldObject({
field: opts.mergeAssocId, model: OtherModel, altName: name
}) ||
util.formatField(OtherModel, name, true, opts.reversed),
{ makeKey: makeKey, required: true }
);
var assocName = opts.name || ucfirst(name);
var assocTemplateName = opts.accessor || assocName;
var association = {
name : name,
model : OtherModel || Model,
props : props,
hooks : opts.hooks || {},
autoFetch : opts.autoFetch || false,
autoFetchLimit : opts.autoFetchLimit || 2,
// I'm not sure the next key is used..
field : util.wrapFieldObject({
field: opts.field, model: OtherModel, altName: Model.table
}) ||
util.formatField(Model, name, true, opts.reversed),
mergeTable : opts.mergeTable || (Model.table + "_" + name),
mergeId : mergeId,
mergeAssocId : mergeAssocId,
getAccessor : opts.getAccessor || ("get" + assocTemplateName),
setAccessor : opts.setAccessor || ("set" + assocTemplateName),
hasAccessor : opts.hasAccessor || ("has" + assocTemplateName),
delAccessor : opts.delAccessor || ("remove" + assocTemplateName),
addAccessor : opts.addAccessor || ("add" + assocTemplateName)
};
associations.push(association);
if (opts.reverse) {
OtherModel.hasMany(opts.reverse, Model, association.props, {
reversed : true,
association : opts.reverseAssociation,
mergeTable : association.mergeTable,
mergeId : association.mergeAssocId,
mergeAssocId : association.mergeId,
field : association.field,
autoFetch : association.autoFetch,
autoFetchLimit : association.autoFetchLimit
});
}
return this;
};
};
exports.extend = function (Model, Instance, Driver, associations, opts, createInstance) {
for (var i = 0; i < associations.length; i++) {
extendInstance(Model, Instance, Driver, associations[i], opts, createInstance);
}
};
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, opts, createInstance) {
var promiseFunctionPostfix = Model.settings.get('promiseFunctionPostfix');
if (Model.settings.get("instance.cascadeRemove")) {
Instance.on("beforeRemove", function () {
Instance[association.delAccessor]();
});
}
function adjustForMapsTo(options) {
// Loop through the (cloned) association model id fields ... some of them may've been mapped to different
// names in the actual database - if so update to the mapped database column name
for(var i=0; i<options.__merge.to.field.length; i++) {
var idProp = association.model.properties[options.__merge.to.field[i]];
if(idProp && idProp.mapsTo) {
options.__merge.to.field[i] = idProp.mapsTo;
}
}
}
Object.defineProperty(Instance, association.hasAccessor, {
value: function () {
var Instances = Array.prototype.slice.apply(arguments);
var cb = Instances.pop();
var conditions = {}, options = {};
if (Instances.length) {
if (Array.isArray(Instances[0])) {
Instances = Instances[0];
}
}
if (Driver.hasMany) {
return Driver.hasMany(Model, association).has(Instance, Instances, conditions, cb);
}
options.autoFetchLimit = 0;
options.__merge = {
from: { table: association.mergeTable, field: Object.keys(association.mergeAssocId) },
to: { table: association.model.table, field: association.model.id.slice(0) }, // clone model id
where: [ association.mergeTable, {} ]
};
adjustForMapsTo(options);
options.extra = association.props;
options.extra_info = {
table: association.mergeTable,
id: util.values(Instance, Model.id),
id_prop: Object.keys(association.mergeId),
assoc_prop: Object.keys(association.mergeAssocId)
};
util.populateConditions(Model, Object.keys(association.mergeId), Instance, options.__merge.where[1]);
for (var i = 0; i < Instances.length; i++) {
util.populateConditions(association.model, Object.keys(association.mergeAssocId), Instances[i], options.__merge.where[1], false);
}
association.model.find(conditions, options, function (err, foundItems) {
if (err) return cb(err);
if (_.isEmpty(Instances)) return cb(null, false);
var mapKeysToString = function (item) {
return _.map(association.model.keys, function (k) {
return item[k];
}).join(',')
}
var foundItemsIDs = _(foundItems).map(mapKeysToString).uniq().value();
var InstancesIDs = _(Instances ).map(mapKeysToString).uniq().value();
var sameLength = foundItemsIDs.length == InstancesIDs.length;
var sameContents = sameLength && _.isEmpty(_.difference(foundItemsIDs, InstancesIDs));
return cb(null, sameContents);
});
return this;
},
enumerable: false,
writable: true
});
Object.defineProperty(Instance, association.getAccessor, {
value: function () {
var options = {};
var conditions = null;
var order = null;
var cb = null;
for (var i = 0; i < arguments.length; i++) {
switch (typeof arguments[i]) {
case "function":
cb = arguments[i];
break;
case "object":
if (Array.isArray(arguments[i])) {
order = arguments[i];
order[0] = [ association.model.table, order[0] ];
} else {
if (conditions === null) {
conditions = arguments[i];
} else {
options = arguments[i];
}
}
break;
case "string":
if (arguments[i][0] == "-") {
order = [ [ association.model.table, arguments[i].substr(1) ], "Z" ];
} else {
order = [ [ association.model.table, arguments[i] ] ];
}
break;
case "number":
options.limit = arguments[i];
break;
}
}
if (order !== null) {
options.order = order;
}
if (conditions === null) {
conditions = {};
}
if (Driver.hasMany) {
return Driver.hasMany(Model, association).get(Instance, conditions, options, createInstance, cb);
}
options.__merge = {
from : { table: association.mergeTable, field: Object.keys(association.mergeAssocId) },
to : { table: association.model.table, field: association.model.id.slice(0) }, // clone model id
where : [ association.mergeTable, {} ]
};
adjustForMapsTo(options);
options.extra = association.props;
options.extra_info = {
table: association.mergeTable,
id: util.values(Instance, Model.id),
id_prop: Object.keys(association.mergeId),
assoc_prop: Object.keys(association.mergeAssocId)
};
util.populateConditions(Model, Object.keys(association.mergeId), Instance, options.__merge.where[1]);
if (cb === null) {
return association.model.find(conditions, options);
}
association.model.find(conditions, options, cb);
return this;
},
enumerable: false,
writable: true
});
Object.defineProperty(Instance, association.setAccessor, {
value: function () {
var items = _.flatten(arguments);
var cb = _.last(items) instanceof Function ? items.pop() : noOperation;
Instance[association.delAccessor](function (err) {
if (err) return cb(err);
if (items.length) {
Instance[association.addAccessor](items, cb);
} else {
cb(null);
}
});
return this;
},
enumerable: false,
writable: true
});
Object.defineProperty(Instance, association.delAccessor, {
value: function () {
var Associations = [];
var cb = noOperation;
for (var i = 0; i < arguments.length; i++) {
switch (typeof arguments[i]) {
case "function":
cb = arguments[i];
break;
case "object":
if (Array.isArray(arguments[i])) {
Associations = Associations.concat(arguments[i]);
} else if (arguments[i].isInstance) {
Associations.push(arguments[i]);
}
break;
}
}
var conditions = {};
var run = function () {
if (Driver.hasMany) {
return Driver.hasMany(Model, association).del(Instance, Associations, cb);
}
if (Associations.length === 0) {
return Driver.remove(association.mergeTable, conditions, cb);
}
for (var i = 0; i < Associations.length; i++) {
util.populateConditions(association.model, Object.keys(association.mergeAssocId), Associations[i], conditions, false);
}
Driver.remove(association.mergeTable, conditions, cb);
};
util.populateConditions(Model, Object.keys(association.mergeId), Instance, conditions);
if (this.saved()) {
run();
} else {
this.save(function (err) {
if (err) {
return cb(err);
}
return run();
});
}
return this;
},
enumerable: false,
writable: true
});
Object.defineProperty(Instance, association.addAccessor, {
value: function () {
var Associations = [];
var opts = {};
var cb = noOperation;
var run = function () {
var savedAssociations = [];
var saveNextAssociation = function () {
if (Associations.length === 0) {
return cb(null, savedAssociations);
}
var Association = Associations.pop();
var saveAssociation = function (err) {
if (err) {
return cb(err);
}
Association.save(function (err) {
if (err) {
return cb(err);
}
var data = {};
for (var k in opts) {
if (k in association.props && Driver.propertyToValue) {
data[k] = Driver.propertyToValue(opts[k], association.props[k]);
} else {
data[k] = opts[k];
}
}
if (Driver.hasMany) {
return Driver.hasMany(Model, association).add(Instance, Association, data, function (err) {
if (err) {
return cb(err);
}
savedAssociations.push(Association);
return saveNextAssociation();
});
}
util.populateConditions(Model, Object.keys(association.mergeId), Instance, data);
util.populateConditions(association.model, Object.keys(association.mergeAssocId), Association, data);
Driver.insert(association.mergeTable, data, null, function (err) {
if (err) {
return cb(err);
}
savedAssociations.push(Association);
return saveNextAssociation();
});
});
};
if (Object.keys(association.props).length) {
Hook.wait(Association, association.hooks.beforeSave, saveAssociation, opts);
} else {
Hook.wait(Association, association.hooks.beforeSave, saveAssociation);
}
};
return saveNextAssociation();
};
for (var i = 0; i < arguments.length; i++) {
switch (typeof arguments[i]) {
case "function":
cb = arguments[i];
break;
case "object":
if (Array.isArray(arguments[i])) {
Associations = Associations.concat(arguments[i]);
} else if (arguments[i].isInstance) {
Associations.push(arguments[i]);
} else {
opts = arguments[i];
}
break;
}
}
if (Associations.length === 0) {
throw new ORMError("No associations defined", 'PARAM_MISMATCH', { model: Model.name });
}
if (this.saved()) {
run();
} else {
this.save(function (err) {
if (err) {
return cb(err);
}
return run();
});
}
return this;
},
enumerable: false,
writable: true
});
Object.defineProperty(Instance, association.name, {
get: function () {
return Instance.__opts.associations[association.name].value;
},
set: function (val) {
Instance.__opts.associations[association.name].changed = true;
Instance.__opts.associations[association.name].value = val;
},
enumerable: true
});
for (var y = 0; y < ACCESSOR_METHODS.length; y++) {
var accessorMethodName = ACCESSOR_METHODS[y];
Object.defineProperty(Instance, association[accessorMethodName] + promiseFunctionPostfix, {
value: Promise.promisify(Instance[association[accessorMethodName]]),
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();
}
Instance[association.getAccessor]({}, { autoFetchLimit: opts.autoFetchLimit - 1 }, function (err, Assoc) {
if (!err) {
// Set this way to prevent setting 'changed' status
Instance.__opts.associations[association.name].value = Assoc;
}
return cb();
});
}
function ucfirst(text) {
return text[0].toUpperCase() + text.substr(1).replace(/_([a-z])/, function (m, l) {
return l.toUpperCase();
});
}
function noOperation() {
}