UNPKG

orm

Version:

NodeJS Object-relational mapping

762 lines (650 loc) 22.3 kB
var _ = require("lodash"); var async = require("async"); var ChainFind = require("./ChainFind"); var Instance = require("./Instance").Instance; var LazyLoad = require("./LazyLoad"); var ManyAssociation = require("./Associations/Many"); var OneAssociation = require("./Associations/One"); var ExtendAssociation = require("./Associations/Extend"); var Property = require("./Property"); var Singleton = require("./Singleton"); var Utilities = require("./Utilities"); var Validators = require("./Validators"); var ORMError = require("./Error"); var Hook = require("./Hook"); var Promise = require("bluebird"); var AvailableHooks = [ "beforeCreate", "afterCreate", "beforeSave", "afterSave", "beforeValidation", "beforeRemove", "afterRemove", "afterLoad", "afterAutoFetch" ]; exports.Model = Model; function Model(opts) { opts = _.defaults(opts || {}, { keys: [] }); opts.keys = Array.isArray(opts.keys) ? opts.keys : [opts.keys]; var one_associations = []; var many_associations = []; var extend_associations = []; var association_properties = []; var model_fields = []; var fieldToPropertyMap = {}; var allProperties = {}; var keyProperties = []; var createHookHelper = function (hook) { return function (cb) { if (typeof cb !== "function") { delete opts.hooks[hook]; } else { opts.hooks[hook] = cb; } return this; }; }; var createInstance = function (data, inst_opts, cb) { if (!inst_opts) { inst_opts = {}; } var found_assoc = false, i, k; for (k in data) { if (k === "extra_field") continue; if (opts.properties.hasOwnProperty(k)) continue; if (inst_opts.extra && inst_opts.extra.hasOwnProperty(k)) continue; if (opts.keys.indexOf(k) >= 0) continue; if (association_properties.indexOf(k) >= 0) continue; for (i = 0; i < one_associations.length; i++) { if (one_associations[i].name === k) { found_assoc = true; break; } } if (!found_assoc) { for (i = 0; i < many_associations.length; i++) { if (many_associations[i].name === k) { found_assoc = true; break; } } } if (!found_assoc) { delete data[k]; } } var assoc_opts = { autoFetch : inst_opts.autoFetch || false, autoFetchLimit : inst_opts.autoFetchLimit, cascadeRemove : inst_opts.cascadeRemove }; var setupAssociations = function (instance) { OneAssociation.extend(model, instance, opts.driver, one_associations, assoc_opts); ManyAssociation.extend(model, instance, opts.driver, many_associations, assoc_opts, createInstance); ExtendAssociation.extend(model, instance, opts.driver, extend_associations, assoc_opts); }; var pending = 2, create_err = null; var instance = new Instance(model, { uid : inst_opts.uid, // singleton unique id keys : opts.keys, is_new : inst_opts.is_new || false, isShell : inst_opts.isShell || false, data : data, autoSave : inst_opts.autoSave || false, extra : inst_opts.extra, extra_info : inst_opts.extra_info, driver : opts.driver, table : opts.table, hooks : opts.hooks, methods : opts.methods, validations : opts.validations, one_associations : one_associations, many_associations : many_associations, extend_associations : extend_associations, association_properties : association_properties, setupAssociations : setupAssociations, fieldToPropertyMap : fieldToPropertyMap, keyProperties : keyProperties }); instance.on("ready", function (err) { if (--pending > 0) { create_err = err; return; } if (typeof cb === "function") { return cb(err || create_err, instance); } }); if (model_fields !== null) { LazyLoad.extend(instance, model, opts.properties); } OneAssociation.autoFetch(instance, one_associations, assoc_opts, function () { ManyAssociation.autoFetch(instance, many_associations, assoc_opts, function () { ExtendAssociation.autoFetch(instance, extend_associations, assoc_opts, function () { Hook.wait(instance, opts.hooks.afterAutoFetch, function (err) { if (--pending > 0) { create_err = err; return; } if (typeof cb === "function") { return cb(err || create_err, instance); } }); }); }); }); return instance; }; var model = function () { var instance, i; var data = arguments.length > 1 ? arguments : arguments[0]; if (Array.isArray(opts.keys) && Array.isArray(data)) { if (data.length == opts.keys.length) { var data2 = {}; for (i = 0; i < opts.keys.length; i++) { data2[opts.keys[i]] = data[i++]; } return createInstance(data2, { isShell: true }); } else { var err = new Error('Model requires ' + opts.keys.length + ' keys, only ' + data.length + ' were provided'); err.model = opts.table; throw err; } } else if (typeof data === "number" || typeof data === "string") { var data2 = {}; data2[opts.keys[0]] = data; return createInstance(data2, { isShell: true }); } else if (typeof data === "undefined") { data = {}; } var isNew = false; for (i = 0; i < opts.keys.length; i++) { if (!data.hasOwnProperty(opts.keys[i])) { isNew = true; break; } } if (keyProperties.length != 1 || (keyProperties.length == 1 && keyProperties[0].type != 'serial')) { isNew = true; } return createInstance(data, { is_new: isNew, autoSave: opts.autoSave, cascadeRemove: opts.cascadeRemove }); }; model.allProperties = allProperties; model.properties = opts.properties; model.settings = opts.settings; model.keys = opts.keys; model.drop = function (cb) { if (arguments.length === 0) { cb = function () {}; } if (typeof opts.driver.drop === "function") { opts.driver.drop({ table : opts.table, properties : opts.properties, one_associations : one_associations, many_associations : many_associations }, cb); return this; } return cb(new ORMError("Driver does not support Model.drop()", 'NO_SUPPORT', { model: opts.table })); }; model.dropAsync = Promise.promisify(model.drop); model.sync = function (cb) { if (arguments.length === 0) { cb = function () {}; } if (typeof opts.driver.sync === "function") { try { opts.driver.sync({ extension : opts.extension, id : opts.keys, table : opts.table, properties : opts.properties, allProperties : allProperties, indexes : opts.indexes || [], customTypes : opts.db.customTypes, one_associations : one_associations, many_associations : many_associations, extend_associations : extend_associations }, cb); } catch (e) { return cb(e); } return this; } return cb(new ORMError("Driver does not support Model.sync()", 'NO_SUPPORT', { model: opts.table })); }; model.syncPromise = Promise.promisify(model.sync); model.get = function () { var conditions = {}; var options = {}; var ids = Array.prototype.slice.apply(arguments); var cb = ids.pop(); var prop; if (typeof cb !== "function") { throw new ORMError("Missing Model.get() callback", 'MISSING_CALLBACK', { model: opts.table }); } if (typeof ids[ids.length - 1] === "object" && !Array.isArray(ids[ids.length - 1])) { options = ids.pop(); } if (ids.length === 1 && Array.isArray(ids[0])) { ids = ids[0]; } if (ids.length !== opts.keys.length) { throw new ORMError("Model.get() IDs number mismatch (" + opts.keys.length + " needed, " + ids.length + " passed)", 'PARAM_MISMATCH', { model: opts.table }); } for (var i = 0; i < keyProperties.length; i++) { prop = keyProperties[i]; conditions[prop.mapsTo] = ids[i]; } if (!options.hasOwnProperty("autoFetch")) { options.autoFetch = opts.autoFetch; } if (!options.hasOwnProperty("autoFetchLimit")) { options.autoFetchLimit = opts.autoFetchLimit; } if (!options.hasOwnProperty("cascadeRemove")) { options.cascadeRemove = opts.cascadeRemove; } opts.driver.find(model_fields, opts.table, conditions, { limit: 1 }, function (err, data) { if (err) { return cb(new ORMError(err.message, 'QUERY_ERROR', { originalCode: err.code })); } if (data.length === 0) { return cb(new ORMError("Not found", 'NOT_FOUND', { model: opts.table })); } Utilities.renameDatastoreFieldsToPropertyNames(data[0], fieldToPropertyMap); var uid = opts.driver.uid + "/" + opts.table + "/" + ids.join("/"); Singleton.get(uid, { identityCache : (options.hasOwnProperty("identityCache") ? options.identityCache : opts.identityCache), saveCheck : opts.settings.get("instance.identityCacheSaveCheck") }, function (cb) { return createInstance(data[0], { uid : uid, autoSave : options.autoSave, autoFetch : (options.autoFetchLimit === 0 ? false : options.autoFetch), autoFetchLimit : options.autoFetchLimit, cascadeRemove : options.cascadeRemove }, cb); }, cb); }); return this; }; model.getAsync = Promise.promisify(model.get); model.find = function () { var options = {}; var conditions = null; var cb = null; var order = null; var merge = null; for (var i = 0; i < arguments.length; i++) { switch (typeof arguments[i]) { case "number": options.limit = arguments[i]; break; case "object": if (Array.isArray(arguments[i])) { if (arguments[i].length > 0) { order = arguments[i]; } } else { if (conditions === null) { conditions = arguments[i]; } else { if (options.hasOwnProperty("limit")) { arguments[i].limit = options.limit; } options = arguments[i]; if (options.hasOwnProperty("__merge")) { merge = options.__merge; merge.select = Object.keys(options.extra); delete options.__merge; } if (options.hasOwnProperty("order")) { order = options.order; delete options.order; } } } break; case "function": cb = arguments[i]; break; case "string": if (arguments[i][0] === "-") { order = [ arguments[i].substr(1), "Z" ]; } else { order = [ arguments[i] ]; } break; } } if (!options.hasOwnProperty("identityCache")) { options.identityCache = opts.identityCache; } if (!options.hasOwnProperty("autoFetchLimit")) { options.autoFetchLimit = opts.autoFetchLimit; } if (!options.hasOwnProperty("cascadeRemove")) { options.cascadeRemove = opts.cascadeRemove; } if (order) { order = Utilities.standardizeOrder(order); } if (conditions) { conditions = Utilities.checkConditions(conditions, one_associations); } var chain = new ChainFind(model, { only : options.only || model_fields, keys : opts.keys, table : opts.table, driver : opts.driver, conditions : conditions, associations : many_associations, limit : options.limit, order : order, merge : merge, offset : options.offset, properties : allProperties, keyProperties: keyProperties, newInstance : function (data, cb) { // We need to do the rename before we construct the UID & do the cache lookup // because the cache is loaded using propertyName rather than fieldName Utilities.renameDatastoreFieldsToPropertyNames(data, fieldToPropertyMap); // Construct UID var uid = opts.driver.uid + "/" + opts.table + (merge ? "+" + merge.from.table : ""); for (var i = 0; i < opts.keys.length; i++) { uid += "/" + data[opts.keys[i]]; } // Now we can do the cache lookup Singleton.get(uid, { identityCache : options.identityCache, saveCheck : opts.settings.get("instance.identityCacheSaveCheck") }, function (cb) { return createInstance(data, { uid : uid, autoSave : opts.autoSave, autoFetch : (options.autoFetchLimit === 0 ? false : (options.autoFetch || opts.autoFetch)), autoFetchLimit : options.autoFetchLimit, cascadeRemove : options.cascadeRemove, extra : options.extra, extra_info : options.extra_info }, cb); }, cb); } }); if (typeof cb !== "function") { return chain; } else { chain.run(cb); return this; } }; model.findAsync = Promise.promisify(model.find); model.where = model.all = model.find; model.whereAsync = model.allAsync = model.findAsync; model.one = function () { var args = Array.prototype.slice.apply(arguments); var cb = null; // extract callback for (var i = 0; i < args.length; i++) { if (typeof args[i] === "function") { cb = args.splice(i, 1)[0]; break; } } if (cb === null) { throw new ORMError("Missing Model.one() callback", 'MISSING_CALLBACK', { model: opts.table }); } // add limit 1 args.push(1); args.push(function (err, results) { if (err) { return cb(err); } return cb(null, results.length ? results[0] : null); }); return this.find.apply(this, args); }; model.oneAsync = Promise.promisify(model.one); model.count = function () { var conditions = null; var cb = null; for (var i = 0; i < arguments.length; i++) { switch (typeof arguments[i]) { case "object": conditions = arguments[i]; break; case "function": cb = arguments[i]; break; } } if (typeof cb !== "function") { throw new ORMError('MISSING_CALLBACK', "Missing Model.count() callback", { model: opts.table }); } if (conditions) { conditions = Utilities.checkConditions(conditions, one_associations); } opts.driver.count(opts.table, conditions, {}, function (err, data) { if (err || data.length === 0) { return cb(err); } return cb(null, data[0].c); }); return this; }; model.countAsync = Promise.promisify(model.count); model.aggregate = function () { var conditions = {}; var propertyList = []; for (var i = 0; i < arguments.length; i++) { if (typeof arguments[i] === "object") { if (Array.isArray(arguments[i])) { propertyList = arguments[i]; } else { conditions = arguments[i]; } } } if (conditions) { conditions = Utilities.checkConditions(conditions, one_associations); } return new require("./AggregateFunctions")({ table : opts.table, driver_name : opts.driver_name, driver : opts.driver, conditions : conditions, propertyList : propertyList, properties : allProperties }); }; model.exists = function () { var ids = Array.prototype.slice.apply(arguments); var cb = ids.pop(); if (typeof cb !== "function") { throw new ORMError("Missing Model.exists() callback", 'MISSING_CALLBACK', { model: opts.table }); } var conditions = {}, i; if (ids.length === 1 && typeof ids[0] === "object") { if (Array.isArray(ids[0])) { for (i = 0; i < opts.keys.length; i++) { conditions[opts.keys[i]] = ids[0][i]; } } else { conditions = ids[0]; } } else { for (i = 0; i < opts.keys.length; i++) { conditions[opts.keys[i]] = ids[i]; } } if (conditions) { conditions = Utilities.checkConditions(conditions, one_associations); } opts.driver.count(opts.table, conditions, {}, function (err, data) { if (err || data.length === 0) { return cb(err); } return cb(null, data[0].c > 0); }); return this; }; model.existsAsync = Promise.promisify(model.exists); model.create = function () { var itemsParams = []; var items = []; var options = {}; var done = null; var single = false; for (var i = 0; i < arguments.length; i++) { switch (typeof arguments[i]) { case "object": if ( !single && Array.isArray(arguments[i]) ) { itemsParams = itemsParams.concat(arguments[i]); } else if (i === 0) { single = true; itemsParams.push(arguments[i]); } else { options = arguments[i]; } break; case "function": done = arguments[i]; break; } } var iterator = function (params, index, cb) { createInstance(params, { is_new : true, autoSave : opts.autoSave, autoFetch : false }, function (err, item) { if (err) { err.index = index; err.instance = item; return cb(err); } item.save({}, options, function (err) { if (err) { err.index = index; err.instance = item; return cb(err); } items[index] = item; cb(); }); }); }; async.eachOfSeries(itemsParams, iterator, function (err) { if (err) return done(err); done(null, single ? items[0] : items); }); return this; }; model.createAsync = Promise.promisify(model.create); model.clear = function (cb) { opts.driver.clear(opts.table, function (err) { if (typeof cb === "function") cb(err); }); return this; }; model.clearAsync = Promise.promisify(model.clear); model.prependValidation = function (key, validation) { if(opts.validations.hasOwnProperty(key)) { opts.validations[key].splice(0, 0, validation); } else { opts.validations[key] = [validation]; } }; var currFields = {}; model.addProperty = function (propIn, options) { var cType; var prop = Property.normalize({ prop: propIn, name: (options && options.name || propIn.name), customTypes: opts.db.customTypes, settings: opts.settings }); // Maintains backwards compatibility if (opts.keys.indexOf(k) != -1) { prop.key = true; } else if (prop.key) { opts.keys.push(k); } if (options && options.klass) { prop.klass = options.klass; } switch (prop.klass) { case 'primary': opts.properties[prop.name] = prop; break; case 'hasOne': association_properties.push(prop.name) break; } allProperties[prop.name] = prop; fieldToPropertyMap[prop.mapsTo] = prop; if (prop.required) { model.prependValidation(prop.name, Validators.required()); } if (prop.key && prop.klass == 'primary') { keyProperties.push(prop); } if (prop.lazyload !== true && !currFields[prop.name]) { currFields[prop.name] = true; if ((cType = opts.db.customTypes[prop.type]) && cType.datastoreGet) { model_fields.push({ a: prop.mapsTo, sql: cType.datastoreGet(prop, opts.db.driver.query) }); } else { model_fields.push(prop.mapsTo); } } return prop; }; Object.defineProperty(model, "table", { value: opts.table, enumerable: false }); Object.defineProperty(model, "id", { value: opts.keys, enumerable: false }); Object.defineProperty(model, "uid", { value: opts.driver.uid + "/" + opts.table + "/" + opts.keys.join("/"), enumerable: false }); // Standardize validations for (var k in opts.validations) { if (!Array.isArray(opts.validations[k])) { opts.validations[k] = [ opts.validations[k] ]; } } // If no keys are defined add the default one if (opts.keys.length == 0 && !_.some(opts.properties, { key: true })) { opts.properties[opts.settings.get("properties.primary_key")] = { type: 'serial', key: true, required: false, klass: 'primary' }; } // standardize properties for (k in opts.properties) { model.addProperty(opts.properties[k], { name: k, klass: 'primary' }); } if (keyProperties.length == 0) { throw new ORMError("Model defined without any keys", 'BAD_MODEL', { model: opts.table }); } // setup hooks for (k in AvailableHooks) { model[AvailableHooks[k]] = createHookHelper(AvailableHooks[k]); } OneAssociation.prepare(model, one_associations); ManyAssociation.prepare(opts.db, model, many_associations); ExtendAssociation.prepare(opts.db, model, extend_associations); return model; }