diff --git a/.gitignore b/.gitignore index afba1e84..a09ad17e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ test/config.js test/coverage.html lib-cov/ *~ -/.idea +npm-debug.log +.idea diff --git a/.npmignore b/.npmignore index 0c0d8ef4..7b93b7d6 100644 --- a/.npmignore +++ b/.npmignore @@ -6,3 +6,4 @@ node_modules test* lib-cov/ *~ +.idea \ No newline at end of file diff --git a/Readme.md b/Readme.md index 4a68bbfc..8ba49c65 100755 --- a/Readme.md +++ b/Readme.md @@ -95,11 +95,280 @@ orm.connect("mysql://username:password@host/database", function (err, db) { }); }); ``` +------ +## Promise -## Promises +- Read documentation about [bluebird](http://bluebirdjs.com/docs/api-reference.html) `Promise` for more advanced knowledge how to use `Promises`. -You can use the [promise enabled wrapper library](https://github.com/rafaelkaufmann/q-orm). +### Connect +The connection URL has the following syntax: `driver://username:password@hostname/database?option1=value1&option2=value2..` + +```javascript +var orm = require('orm'); + +orm.connectAsync('mysql://root:password@localhost/test') + .then(function(db) { + // connected + // ... + }) + .catch(function() { + console.error('Connection error: ' + err); + }); +``` + +Valid options are: + +- `debug` (default: **false**): prints queries to console; +- `pool` (default: **false**): manages a connection pool (only for `mysql` and `postgres`) using built-in driver pool; +- `strdates` (default: **false**): saves dates as strings (only for `sqlite`). +- `timezone` (default: **'local'**): store dates in the database using this timezone (`mysql` and `postgres` only) + +```javascript +var orm = require('orm'); + +var opts = { + host: host, + database: database, + protocol: 'mysql', + port: '3306', + query: {pool: true} + }; + +orm.connectAsync(opts) + .then(function(db) { + // connected + // ... + }) + .catch(function() { + console.error('Connection error: ' + err); + }); +``` +------- + +### Model Hooks + +If you want to listen for a type of event than occurs in instances of a Model, you can attach a function that +will be called when that event happens. For each hook above implemented Promise support, with backward capability via use next callback. +For use promise you should return `Promise`, look at example. + +Currently the following events are supported: + +- `beforeValidation` : (no parameters) Before all validations and prior to `beforeCreate` and `beforeSave`; +- `beforeCreate` : (no parameters) Right before trying to save a new instance (prior to `beforeSave`); +- `beforeSave` : (no parameters) Right before trying to save; +- `afterSave` : (bool success) Right after saving; +- `afterCreate` : (bool success) Right after saving a new instance; +- `afterLoad` : (no parameters) Right after loading and preparing an instance to be used; +- `afterAutoFetch` : (no parameters) Right after auto-fetching associations (if any), it will trigger regardless of having associations or not; +- `beforeRemove` : (no parameters) Right before trying to remove an instance; +- `afterRemove` : (bool success) Right after removing an instance; + +All hook function are called with `this` as the instance so you can access anything you want related to it. +Here's an example: + +```js +var Person = db.define("person", { + name : String, + surname : String +}, { + hooks: { + beforeCreate: function () { + return new Promise(function(resolve, reject) { + if (this.surname == "Doe") { + return reject(new Error("No Does allowed")); + } + return resolve(); + }); + } + } +}); +``` +------- +### Editing Syncing and dropping models +Syncing is an utility method that creates all the necessary tables in the database for your models and associations to work. Tables are not replaced, they are only created if they don't exist. + +There are 2 ways of syncing: + +1. Calling `Model.syncPromise()` will only synchronize the model +2. Calling `db.syncPromise()` will synchronize all models + +Dropping is a similar method but instead it drops all tables involved in your models, even if they were not created by ORM. There also 2 ways of dropping. + +```js +var orm = require("orm"); + +orm.connectAsync("....") + .then(function (db) { + var Person = db.define("person", { + name : String + }); + + return [Person, db.dropAsync()]; + }) + .spread(function(Person) { + return Person.syncPromise(); + }) + .then(function () { + // created tables for Person model + }); +``` +------- +### Finding items + +#### findAsync +Find records with matching criteria, can be chained (see below): +```javascript +Person.find({status:'active'}) + .then(function(results) { + // ... + }); +``` + +You can limit your results as well. This limits our results to 10 +```javascript +Person.find({status:'active'}, 10) + .then(function(results) { + // ... + }); +``` + +`Person.all` is an alias to `Person.find` + +#### getAsync +Find record by primary key. +```javascript +Person.getAsync(1) + .then(function(person) { + // ... + }); +``` +#### oneAsync +Find one record with similar syntax to find. +```javascript +Person.oneAsync({status:'active'}) + .then(function(person) { + // ... + }); +``` + +#### countAsync +Get the number of matching records. +```javascript +Person.countAsync({status:'active'}) + .then(function(activePeopleCount) { + // ... + }); +``` + +#### existsAsync +Test a record matching your conditions exists. +```javascript +Person.exists({id:1, status:'active'}) + .then(function(personIsActive) { + // ... + }); +``` + +#### Filtering and sorting +We accept 2 objects to perform filtering (first) and aggregate (second). The aggregate object accepts `limit`, `order`, and `groupBy`. + +```javascript +Person.findAsync({status:'active'}, {limit:10}) + .then(function(results) { + // ... + }); +``` + +#### Conditions for find/count/one etc. +All comma separated key/values are AND'd together in the query. You may prefix a set of conditions with logical operators. +```javascript +Person.findAsync({or:[{col1: 1}, {col2: 2}]}) + .then(function(res) { + // ... + }); +``` + +#### Finding with an `IN` +`sql-query` (underlying SQL engine) will automatically coerce any array to an `IN` based query. + +```javascript +Person.findAsync({id: [1, 2]}) + .then(function(persons) { + // Finds people with id's 1 and 2 (e.g. `WHERE id IN (1, 2)`) + }); +``` +------- +### Creating and Updating Items +#### createAsync +```javascript +var newRecord = {}; +newRecord.id = 1; +newRecord.name = "John"; + +Person.createAsync(newRecord) + .then(function(results) { + // ... + }); +``` + +#### saveAsync +```js + Person.findAsync({ surname: "Doe" }) + .then(function (people) { + // SQL: "SELECT * FROM person WHERE surname = 'Doe'" + + console.log("People found: %d", people.length); + console.log("First person: %s, age %d", people[0].fullName(), people[0].age); + + people[0].age = 16; + return people[0].saveAsync(); + }) + .then(function () { + // saved + }); +``` +------- +### Aggregation +If you need to get some aggregated values from a Model, you can use `Model.aggregate()`. Here's an example to better +illustrate: + +```js +Person.aggregate({ surname: "Doe" }).min("age").max("age").getAsync() + .then(function(result) { + var [min, max] = result; // you should use destructuring here + + console.log(min, max); + }); +``` + +An `Array` of properties can be passed to select only a few properties. An `Object` is also accepted to define conditions. + +Here's an example to illustrate how to use `.groupBy()`: + +```js +//The same as "select avg(weight), age from person where country='someCountry' group by age;" +Person.aggregate(["age"], { country: "someCountry" }).avg("weight").groupBy("age").getAsync() + .then(function (stats) { + // stats is an Array, each item should have 'age' and 'avg_weight' + }); +``` + +### Base `.aggregate()` methods + +- `.limit()`: you can pass a number as a limit, or two numbers as offset and limit respectively +- `.order()`: same as `Model.find().order()` + +### Additional `.aggregate()` methods + +- `min` +- `max` +- `avg` +- `sum` + +There are more aggregate functions depending on the driver (Math functions for example). + +------- ## Express diff --git a/lib/AggregateFunctions.js b/lib/AggregateFunctions.js index 6a54d880..f2842dfc 100644 --- a/lib/AggregateFunctions.js +++ b/lib/AggregateFunctions.js @@ -1,5 +1,6 @@ var ORMError = require("./Error"); var Utilities = require("./Utilities"); +var Promise = require("bluebird"); module.exports = AggregateFunctions; @@ -155,6 +156,18 @@ function AggregateFunctions(opts) { } }; + proto.getAsync = function () { + return new Promise(function(resolve, reject) { + proto.get(function () { + if (arguments[0]) { + return reject(arguments[0]); + } else { + resolve(Array.from(arguments).slice(1)); + } + }); + }); + }; + for (var i = 0; i < opts.driver.aggregate_functions.length; i++) { addAggregate(proto, opts.driver.aggregate_functions[i], appendFunction); } diff --git a/lib/Associations/Extend.js b/lib/Associations/Extend.js index 3aaed679..99a520cb 100644 --- a/lib/Associations/Extend.js +++ b/lib/Associations/Extend.js @@ -3,6 +3,9 @@ var ORMError = require("../Error"); var Settings = require("../Settings"); var Singleton = require("../Singleton"); var util = require("../Utilities"); +var Promise = require("bluebird"); + +var ACCESSOR_METHODS = ["hasAccessor", "getAccessor", "setAccessor", "delAccessor"]; exports.prepare = function (db, Model, associations) { Model.extendsTo = function (name, properties, opts) { @@ -108,10 +111,12 @@ exports.autoFetch = function (Instance, associations, opts, cb) { }; function extendInstance(Model, Instance, Driver, association, opts) { + var promiseFunctionPostfix = Model.settings.get('promiseFunctionPostfix'); + Object.defineProperty(Instance, association.hasAccessor, { value : function (cb) { if (!Instance[Model.id]) { - cb(new ORMError("Instance not saved, cannot get extension", 'NOT_DEFINED', { model: Model.table })); + cb(new ORMError("Instance not saved, cannot get extension", 'NOT_DEFINED', { model: Model.table })); } else { association.model.get(util.values(Instance, Model.id), function (err, extension) { return cb(err, !err && extension ? true : false); @@ -129,7 +134,7 @@ function extendInstance(Model, Instance, Driver, association, opts) { } if (!Instance[Model.id]) { - cb(new ORMError("Instance not saved, cannot get extension", 'NOT_DEFINED', { model: Model.table })); + cb(new ORMError("Instance not saved, cannot get extension", 'NOT_DEFINED', { model: Model.table })); } else { association.model.get(util.values(Instance, Model.id), opts, cb); } @@ -203,6 +208,16 @@ function extendInstance(Model, Instance, Driver, association, opts) { }, enumerable : false }); + + for (var i = 0; i < ACCESSOR_METHODS.length; i++) { + var name = ACCESSOR_METHODS[i]; + var asyncName = association[name] + promiseFunctionPostfix; + Object.defineProperty(Instance, asyncName, { + value: Promise.promisify(Instance[association[name]]), + enumerable: false, + writable: true + }); + } } function autoFetchInstance(Instance, association, opts, cb) { @@ -210,7 +225,7 @@ function autoFetchInstance(Instance, association, opts, cb) { return cb(); } - if (!opts.hasOwnProperty("autoFetchLimit") || typeof opts.autoFetchLimit == "undefined") { + if (!opts.hasOwnProperty("autoFetchLimit") || !opts.autoFetchLimit) { opts.autoFetchLimit = association.autoFetchLimit; } diff --git a/lib/Associations/Many.js b/lib/Associations/Many.js index cd5359c7..adb7b1ff 100644 --- a/lib/Associations/Many.js +++ b/lib/Associations/Many.js @@ -1,13 +1,16 @@ var _ = require("lodash"); -var InstanceConstructor = require("../Instance").Instance; 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; @@ -61,6 +64,7 @@ exports.prepare = function (db, Model, associations) { var assocName = opts.name || ucfirst(name); var assocTemplateName = opts.accessor || assocName; + var association = { name : name, model : OtherModel || Model, @@ -80,7 +84,13 @@ exports.prepare = function (db, Model, associations) { setAccessor : opts.setAccessor || ("set" + assocTemplateName), hasAccessor : opts.hasAccessor || ("has" + assocTemplateName), delAccessor : opts.delAccessor || ("remove" + assocTemplateName), - addAccessor : opts.addAccessor || ("add" + assocTemplateName) + addAccessor : opts.addAccessor || ("add" + assocTemplateName), + + getAccessorAsync : opts.getAccessor + promiseFunctionPostfix || ("get" + assocTemplateName + promiseFunctionPostfix), + setAccessorAsync : opts.setAccessor + promiseFunctionPostfix || ("set" + assocTemplateName + promiseFunctionPostfix), + hasAccessorAsync : opts.hasAccessor + promiseFunctionPostfix || ("has" + assocTemplateName + promiseFunctionPostfix), + delAccessorAsync : opts.delAccessor + promiseFunctionPostfix || ("remove" + assocTemplateName + promiseFunctionPostfix), + addAccessorAsync : opts.addAccessor + promiseFunctionPostfix || ("add" + assocTemplateName + promiseFunctionPostfix) }; associations.push(association); @@ -126,6 +136,8 @@ exports.autoFetch = function (Instance, associations, opts, cb) { }; 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](); @@ -204,6 +216,7 @@ function extendInstance(Model, Instance, Driver, association, opts, createInstan enumerable: false, writable: true }); + Object.defineProperty(Instance, association.getAccessor, { value: function () { var options = {}; @@ -263,7 +276,7 @@ function extendInstance(Model, Instance, Driver, association, opts, createInstan options.extra = association.props; options.extra_info = { - table: association.mergeTable, + table: association.mergeTable, id: util.values(Instance, Model.id), id_prop: Object.keys(association.mergeId), assoc_prop: Object.keys(association.mergeAssocId) @@ -282,6 +295,7 @@ function extendInstance(Model, Instance, Driver, association, opts, createInstan enumerable: false, writable: true }); + Object.defineProperty(Instance, association.setAccessor, { value: function () { var items = _.flatten(arguments); @@ -356,6 +370,7 @@ function extendInstance(Model, Instance, Driver, association, opts, createInstan enumerable: false, writable: true }); + Object.defineProperty(Instance, association.addAccessor, { value: function () { var Associations = []; @@ -476,6 +491,15 @@ function extendInstance(Model, Instance, Driver, association, opts, createInstan }, 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) { diff --git a/lib/Associations/One.js b/lib/Associations/One.js index 74098dfe..e9a3d3f8 100644 --- a/lib/Associations/One.js +++ b/lib/Associations/One.js @@ -2,6 +2,9 @@ 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 () { @@ -145,6 +148,7 @@ exports.autoFetch = function (Instance, associations, opts, cb) { }; 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") { @@ -264,6 +268,7 @@ function extendInstance(Model, Instance, Driver, association) { enumerable: false, writable: true }); + if (!association.reversed) { Object.defineProperty(Instance, association.delAccessor, { value: function (cb) { @@ -286,6 +291,18 @@ function extendInstance(Model, Instance, Driver, association) { 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) { diff --git a/lib/ChainFind.js b/lib/ChainFind.js index 7959ceed..a4df9574 100644 --- a/lib/ChainFind.js +++ b/lib/ChainFind.js @@ -1,12 +1,15 @@ -var _ = require("lodash"); -var async = require("async"); -var Utilities = require("./Utilities"); -var ChainInstance = require("./ChainInstance"); -var Promise = require("./Promise").Promise; +var _ = require("lodash"); +var async = require("async"); +var Utilities = require("./Utilities"); +var ChainInstance = require("./ChainInstance"); +var Promise = require("bluebird"); +var DeprecatedPromise = require("./DeprecatedPromise").Promise; module.exports = ChainFind; function ChainFind(Model, opts) { + var promiseFunctionPostfix = Model.settings.get('promiseFunctionPostfix'); + var prepareConditions = function () { return Utilities.transformPropertyNames( opts.conditions, opts.properties @@ -42,10 +45,8 @@ function ChainFind(Model, opts) { if (dataItems.length === 0) { return done(null, []); } - var pending = dataItems.length; var eagerLoad = function (err, items) { - var pending = opts.__eager.length; var idMap = {}; var keys = _.map(items, function (item, index) { @@ -87,8 +88,7 @@ function ChainFind(Model, opts) { return completeFn(null, items); }); }); - } - + }; var promise = null; var chain = { find: function () { @@ -148,9 +148,9 @@ function ChainFind(Model, opts) { opts.order = []; } if (property[0] === "-") { - opts.order.push([ property.substr(1), "Z" ]); + opts.order.push([property.substr(1), "Z"]); } else { - opts.order.push([ property, (order && order.toUpperCase() === "Z" ? "Z" : "A") ]); + opts.order.push([property, (order && order.toUpperCase() === "Z" ? "Z" : "A")]); } return this; }, @@ -158,7 +158,7 @@ function ChainFind(Model, opts) { if (!Array.isArray(opts.order)) { opts.order = []; } - opts.order.push([ str, args || [] ]); + opts.order.push([str, args || []]); return this; }, count: function (cb) { @@ -206,6 +206,7 @@ function ChainFind(Model, opts) { }); return this; }, + first: function (cb) { return this.run(function (err, items) { return cb(err, items && items.length > 0 ? items[0] : null); @@ -224,17 +225,19 @@ function ChainFind(Model, opts) { return this; }, success: function (cb) { + console.warn("ChainFind.success() function is deprecated & will be removed in a future version"); if (!promise) { - promise = new Promise(); + promise = new DeprecatedPromise(); promise.handle(this.all); } return promise.success(cb); }, fail: function (cb) { if (!promise) { - promise = new Promise(); + promise = new DeprecatedPromise(); promise.handle(this.all); } + console.warn("ChainFind.fail() function is deprecated & will be removed in a future version"); return promise.fail(cb); }, eager: function () { @@ -255,6 +258,12 @@ function ChainFind(Model, opts) { }; chain.all = chain.where = chain.find; + chain['find' + promiseFunctionPostfix] = Promise.promisify(chain.find); + chain['first' + promiseFunctionPostfix] = Promise.promisify(chain.first); + chain['last' + promiseFunctionPostfix] = Promise.promisify(chain.last); + chain['run' + promiseFunctionPostfix] = Promise.promisify(chain.run); + chain['remove' + promiseFunctionPostfix] = Promise.promisify(chain.remove); + if (opts.associations) { for (var i = 0; i < opts.associations.length; i++) { addChainMethod(chain, opts.associations[i], opts); diff --git a/lib/Promise.js b/lib/DeprecatedPromise.js similarity index 100% rename from lib/Promise.js rename to lib/DeprecatedPromise.js diff --git a/lib/Drivers/DML/_shared.js b/lib/Drivers/DML/_shared.js index 9875597e..e12a0055 100644 --- a/lib/Drivers/DML/_shared.js +++ b/lib/Drivers/DML/_shared.js @@ -1,30 +1,40 @@ +var Promise = require('bluebird'); -module.exports = { - execQuery: function () { - if (arguments.length == 2) { - var query = arguments[0]; - var cb = arguments[1]; - } else if (arguments.length == 3) { - var query = this.query.escape(arguments[0], arguments[1]); - var cb = arguments[2]; - } - return this.execSimpleQuery(query, cb); - }, - eagerQuery: function (association, opts, keys, cb) { - var desiredKey = Object.keys(association.field); - var assocKey = Object.keys(association.mergeAssocId); - - var where = {}; - where[desiredKey] = keys; - - var query = this.query.select() - .from(association.model.table) - .select(opts.only) - .from(association.mergeTable, assocKey, opts.keys) - .select(desiredKey).as("$p") - .where(association.mergeTable, where) - .build(); - - this.execSimpleQuery(query, cb); +var execQuery = function () { + if (arguments.length == 2) { + var query = arguments[0]; + var cb = arguments[1]; + } else if (arguments.length == 3) { + var query = this.query.escape(arguments[0], arguments[1]); + var cb = arguments[2]; } + return this.execSimpleQuery(query, cb); +}; + +var eagerQuery = function (association, opts, keys, cb) { + var desiredKey = Object.keys(association.field); + var assocKey = Object.keys(association.mergeAssocId); + + var where = {}; + where[desiredKey] = keys; + + var query = this.query.select() + .from(association.model.table) + .select(opts.only) + .from(association.mergeTable, assocKey, opts.keys) + .select(desiredKey).as("$p") + .where(association.mergeTable, where) + .build(); + + this.execSimpleQuery(query, cb); +}; + +module.exports = { + execQuery: execQuery, + + eagerQuery: eagerQuery, + + execQueryAsync: Promise.promisify(execQuery), + + eagerQueryAsync: Promise.promisify(eagerQuery) }; diff --git a/lib/Drivers/DML/mysql.js b/lib/Drivers/DML/mysql.js index ff4c801b..095b3f40 100644 --- a/lib/Drivers/DML/mysql.js +++ b/lib/Drivers/DML/mysql.js @@ -93,6 +93,7 @@ Driver.prototype.getQuery = function () { }; Driver.prototype.execSimpleQuery = function (query, cb) { + if (this.opts.debug) { require("../../Debug").sql('mysql', query); } diff --git a/lib/Drivers/DML/sqlite.js b/lib/Drivers/DML/sqlite.js index 67191439..f36220de 100644 --- a/lib/Drivers/DML/sqlite.js +++ b/lib/Drivers/DML/sqlite.js @@ -4,6 +4,7 @@ var sqlite3 = require("sqlite3"); var Query = require("sql-query").Query; var shared = require("./_shared"); var DDL = require("../DDL/SQL"); +var Promise = require("bluebird"); exports.Driver = Driver; diff --git a/lib/Hook.js b/lib/Hook.js index 1d74f4d8..22285460 100644 --- a/lib/Hook.js +++ b/lib/Hook.js @@ -11,15 +11,24 @@ exports.trigger = function () { exports.wait = function () { var args = Array.prototype.slice.apply(arguments); var self = args.shift(); - var cb = args.shift(); + var hook = args.shift(); var next = args.shift(); args.push(next); + if (typeof hook === "function") { + var hookValue = hook.apply(self, args); - if (typeof cb === "function") { - cb.apply(self, args); + var hookDoesntExpectCallback = hook.length < args.length; + var isPromise = hookValue && typeof(hookValue.then) === "function"; - if (cb.length < args.length) { + if (hookDoesntExpectCallback) { + if (isPromise) { + return hookValue + .then(function () { + next(); + }) + .catch(next); + } return next(); } } else { diff --git a/lib/Instance.js b/lib/Instance.js index 0858f34b..e0778fda 100755 --- a/lib/Instance.js +++ b/lib/Instance.js @@ -2,9 +2,12 @@ var Utilities = require("./Utilities"); var Property = require("./Property"); var Hook = require("./Hook"); var enforce = require("enforce"); +var Promise = require("bluebird"); exports.Instance = Instance; +var INSTNCE_METHOD_NAMES = ["save", "remove", "validate"]; + function Instance(Model, opts) { opts = opts || {}; opts.data = opts.data || {}; @@ -15,6 +18,7 @@ function Instance(Model, opts) { opts.associations = {}; opts.originalKeyValues = {}; + var promiseFunctionPostfix = Model.settings.get('promiseFunctionPostfix'); var instance_saving = false; var events = {}; var instance = {}; @@ -701,6 +705,15 @@ function Instance(Model, opts) { enumerable: false }); + for (var j = 0; j < INSTNCE_METHOD_NAMES.length; j++) { + var name = INSTNCE_METHOD_NAMES[j]; + Object.defineProperty(instance, name + promiseFunctionPostfix, { + value: Promise.promisify(instance[name]), + enumerable: false, + writable: true + }); + } + for (i = 0; i < opts.keyProperties.length; i++) { var prop = opts.keyProperties[i]; diff --git a/lib/LazyLoad.js b/lib/LazyLoad.js index 48af3780..d5bd78b1 100644 --- a/lib/LazyLoad.js +++ b/lib/LazyLoad.js @@ -1,4 +1,8 @@ -exports.extend = function (Instance, Model, properties) { +var Promise = require("bluebird"); + +var LAZY_METHOD_NAMES = ["get", "remove", "set"]; + +var extend = function (Instance, Model, properties) { for (var k in properties) { if (properties[k].lazyload === true) { addLazyLoadProperty(properties[k].lazyname || k, Instance, Model, k); @@ -6,14 +10,34 @@ exports.extend = function (Instance, Model, properties) { } }; +var conditionAssign = function (instance, model) { + var conditions = {}; + conditions[model.id] = instance[model.id]; + return conditions; +}; + function addLazyLoadProperty(name, Instance, Model, property) { var method = ucfirst(name); + var promiseFunctionPostfix = Model.settings.get('promiseFunctionPostfix'); + var functionNames = { + get: { + callback : "get" + method, + promise : "get" + method + promiseFunctionPostfix + }, + remove: { + callback : "remove" + method, + promise : "remove" + method + promiseFunctionPostfix + }, + set: { + callback : "set" + method, + promise : "set" + method + promiseFunctionPostfix + } + }; - Object.defineProperty(Instance, "get" + method, { + Object.defineProperty(Instance, functionNames.get.callback, { value: function (cb) { - var conditions = {}; - conditions[Model.id] = Instance[Model.id]; - + var conditions = conditionAssign(Instance, Model); + Model.find(conditions, { identityCache: false }).only(Model.id.concat(property)).first(function (err, item) { return cb(err, item ? item[property] : null); }); @@ -22,11 +46,11 @@ function addLazyLoadProperty(name, Instance, Model, property) { }, enumerable: false }); - Object.defineProperty(Instance, "remove" + method, { - value: function (cb) { - var conditions = {}; - conditions[Model.id] = Instance[Model.id]; + Object.defineProperty(Instance, functionNames.remove.callback, { + value: function (cb) { + var conditions = conditionAssign(Instance, Model); + Model.find(conditions, { identityCache: false }).only(Model.id.concat(property)).first(function (err, item) { if (err) { return cb(err); @@ -44,11 +68,11 @@ function addLazyLoadProperty(name, Instance, Model, property) { }, enumerable: false }); - Object.defineProperty(Instance, "set" + method, { - value: function (data, cb) { - var conditions = {}; - conditions[Model.id] = Instance[Model.id]; + Object.defineProperty(Instance, functionNames.set.callback, { + value: function (data, cb) { + var conditions = conditionAssign(Instance, Model); + Model.find(conditions, { identityCache: false }).first(function (err, item) { if (err) { return cb(err); @@ -66,8 +90,18 @@ function addLazyLoadProperty(name, Instance, Model, property) { }, enumerable: false }); + + for(var i = 0; i < LAZY_METHOD_NAMES.length; i++) { + var methodName = LAZY_METHOD_NAMES[i]; + Object.defineProperty(Instance, functionNames[methodName].promise, { + value: Promise.promisify(Instance[functionNames[methodName].callback]), + enumerable: false + }); + } } function ucfirst(text) { return text[0].toUpperCase() + text.substr(1).toLowerCase(); } + +exports.extend = extend; \ No newline at end of file diff --git a/lib/Model.js b/lib/Model.js index 698d93a6..6cafd884 100644 --- a/lib/Model.js +++ b/lib/Model.js @@ -12,6 +12,7 @@ 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", @@ -48,6 +49,7 @@ function Model(opts) { return this; }; }; + var createInstance = function (data, inst_opts, cb) { if (!inst_opts) { inst_opts = {}; @@ -147,6 +149,8 @@ function Model(opts) { return instance; }; + + var model = function () { var instance, i; @@ -220,6 +224,8 @@ function Model(opts) { 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 () {}; @@ -248,6 +254,8 @@ function Model(opts) { 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 = {}; @@ -315,6 +323,8 @@ function Model(opts) { return this; }; + model.getAsync = Promise.promisify(model.get); + model.find = function () { var options = {}; var conditions = null; @@ -433,7 +443,10 @@ function Model(opts) { } }; + 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); @@ -463,6 +476,8 @@ function Model(opts) { return this.find.apply(this, args); }; + model.oneAsync = Promise.promisify(model.one); + model.count = function () { var conditions = null; var cb = null; @@ -495,6 +510,8 @@ function Model(opts) { return this; }; + model.countAsync = Promise.promisify(model.count); + model.aggregate = function () { var conditions = {}; var propertyList = []; @@ -560,8 +577,10 @@ function Model(opts) { return this; }; + model.existsAsync = Promise.promisify(model.exists); + model.create = function () { - var itemsParams = [] + var itemsParams = []; var items = []; var options = {}; var done = null; @@ -619,6 +638,8 @@ function Model(opts) { 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); @@ -627,6 +648,8 @@ function Model(opts) { 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); diff --git a/lib/ORM.js b/lib/ORM.js index 4683f2ea..42641863 100644 --- a/lib/ORM.js +++ b/lib/ORM.js @@ -1,5 +1,6 @@ var _ = require("lodash"); var async = require("async"); +var Promise = require('bluebird'); var enforce = require("enforce"); var events = require("events"); var hat = require("hat"); @@ -15,6 +16,8 @@ var Settings = require("./Settings"); var Singleton = require("./Singleton"); var Utilities = require("./Utilities"); +var OPTS_TYPE_STRING = 'string'; +var OPTS_TYPE_OBJ = 'object'; // Deprecated, use enforce exports.validators = require("./Validators"); @@ -32,53 +35,38 @@ exports.Property = require("./Property"); exports.Settings = Settings; exports.ErrorCodes = ORMError.codes; -exports.Text = Query.Text; -for (var k in Query.Comparators) { - exports[Query.Comparators[k]] = Query[Query.Comparators[k]]; -} - -exports.express = function () { - return require("./Express").apply(this, arguments); +var optsChecker = function (opts) { + return [OPTS_TYPE_STRING, OPTS_TYPE_OBJ].some(function (element) { return typeof(opts) === element }) }; -exports.use = function (connection, proto, opts, cb) { - if (DriverAliases[proto]) { - proto = DriverAliases[proto]; - } - if (typeof opts === "function") { - cb = opts; - opts = {}; - } - - try { - var Driver = adapters.get(proto); - var settings = new Settings.Container(exports.settings.get('*')); - var driver = new Driver(null, connection, { - debug : (opts.query && opts.query.debug === 'true'), - settings : settings - }); +var fileLoader = function (filePaths, cb) { + var self = this; + var iterator = function (filePath, cb) { + try { + require(filePath)(self, cb); + } catch (err) { + return cb(err) + } + }; - return cb(null, new ORM(proto, driver, settings)); - } catch (ex) { - return cb(ex); - } + async.eachSeries(filePaths, iterator, cb); }; -exports.connect = function (opts, cb) { - if (arguments.length === 0 || !opts) { +var connect = function (opts, cb) { + if (arguments.length === 0 || !opts || !optsChecker(opts)) { + cb = typeof(cb) !== 'function' ? opts : cb; return ORM_Error(new ORMError("CONNECTION_URL_EMPTY", 'PARAM_MISMATCH'), cb); } - if (typeof opts == 'string') { + if (typeof opts === 'string') { if (opts.trim().length === 0) { return ORM_Error(new ORMError("CONNECTION_URL_EMPTY", 'PARAM_MISMATCH'), cb); } opts = url.parse(opts, true); - } else if (typeof opts == 'object') { + } else if (typeof opts === 'object') { opts = _.cloneDeep(opts); } opts.query = opts.query || {}; - for(var k in opts.query) { opts.query[k] = queryParamCast(opts.query[k]); opts[k] = opts.query[k]; @@ -148,6 +136,47 @@ exports.connect = function (opts, cb) { return db; }; +var use = function (connection, proto, opts, cb) { + if (DriverAliases[proto]) { + proto = DriverAliases[proto]; + } + if (typeof opts === "function") { + cb = opts; + opts = {}; + } + + try { + var Driver = adapters.get(proto); + var settings = new Settings.Container(exports.settings.get('*')); + var driver = new Driver(null, connection, { + debug : (opts.query && opts.query.debug === 'true'), + settings : settings + }); + + return cb(null, new ORM(proto, driver, settings)); + } catch (ex) { + return cb(ex); + } +} + +exports.Text = Query.Text; +for (var k in Query.Comparators) { + exports[Query.Comparators[k]] = Query[Query.Comparators[k]]; +} + +exports.express = function () { + return require("./Express").apply(this, arguments); +}; + +exports.use = use; +exports.useAsync = Promise.promisify(use); + +/** + * + * @param opts + */ +exports.connectAsync = Promise.promisify(connect); + exports.addAdapter = adapters.add; function ORM(driver_name, driver, settings) { @@ -213,7 +242,7 @@ ORM.prototype.use = function (plugin_const, opts) { return this; }; ORM.prototype.define = function (name, properties, opts) { - var i; + var i; properties = properties || {}; opts = opts || {}; @@ -252,24 +281,31 @@ ORM.prototype.define = function (name, properties, opts) { return this.models[name]; }; + ORM.prototype.defineType = function (name, opts) { this.customTypes[name] = opts; this.driver.customTypes[name] = opts; return this; }; + ORM.prototype.ping = function (cb) { this.driver.ping(cb); return this; }; + +ORM.prototype.pingAsync = Promise.promisify(ORM.prototype.ping); + ORM.prototype.close = function (cb) { this.driver.close(cb); return this; }; + +ORM.prototype.closeAsync = Promise.promisify(ORM.prototype.close); + ORM.prototype.load = function () { var files = _.flatten(Array.prototype.slice.apply(arguments)); - var self = this; var cb = function () {}; if (typeof files[files.length - 1] == "function") { @@ -286,16 +322,25 @@ ORM.prototype.load = function () { }()); } - var iterator = function (filePath, cb) { - try { - require(filePath)(self, cb); - } catch (err) { - return cb(err) - } - }; + fileLoader.call(this, filesWithPath, cb); +}; + +ORM.prototype.loadAsync = function () { + var files = _.flatten(Array.prototype.slice.apply(arguments)); + var filesWithPath = []; + // Due to intricacies of `Utilities.getRealPath` the following + // code has to look as it does. + + for(var i = 0; i < files.length; i++) { + var file = files[i]; + filesWithPath.push(function () { + return Utilities.getRealPath(file, 4) + }()); + } - async.eachSeries(filesWithPath, iterator, cb); + return Promise.promisify(fileLoader, { context: this })(filesWithPath) }; + ORM.prototype.sync = function (cb) { var modelIds = Object.keys(this.models); var syncNext = function () { @@ -324,6 +369,9 @@ ORM.prototype.sync = function (cb) { return this; }; + +ORM.prototype.syncPromise = Promise.promisify(ORM.prototype.sync); + ORM.prototype.drop = function (cb) { var modelIds = Object.keys(this.models); var dropNext = function () { @@ -352,6 +400,9 @@ ORM.prototype.drop = function (cb) { return this; }; + +ORM.prototype.dropAsync = Promise.promisify(ORM.prototype.drop); + ORM.prototype.serial = function () { var chains = Array.prototype.slice.apply(arguments); @@ -411,3 +462,5 @@ function queryParamCast (val) { } return val; } + +exports.connect = connect; \ No newline at end of file diff --git a/lib/Settings.js b/lib/Settings.js index fc5893df..500cfc5a 100644 --- a/lib/Settings.js +++ b/lib/Settings.js @@ -23,7 +23,8 @@ var default_settings = { reconnect : true, pool : false, debug : false - } + }, + promiseFunctionPostfix : 'Async' }; exports.Container = Settings; diff --git a/package-lock.json b/package-lock.json index c0832151..b1760b04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,6 +39,11 @@ "integrity": "sha1-8725mtUmihX8HwvtL7AY4mk/4jY=", "dev": true }, + "bluebird": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.0.tgz", + "integrity": "sha1-eRQg1/VR7qKJdFOop3ZT+WYG1nw=" + }, "brace-expansion": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", @@ -129,8 +134,7 @@ "diff": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/diff/-/diff-3.2.0.tgz", - "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=", - "dev": true + "integrity": "sha1-yc45Okt8vQsFinJck98pkCeGj/k=" }, "enforce": { "version": "0.1.7", @@ -143,6 +147,14 @@ "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", "dev": true }, + "formatio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.2.0.tgz", + "integrity": "sha1-87IWfZBoxGmKjVH092CjmlTYGOs=", + "requires": { + "samsam": "1.2.1" + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -221,6 +233,11 @@ "integrity": "sha1-PAQ0dD35Pi9cQq7nsZvLSDV19OE=", "dev": true }, + "just-extend": { + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-1.1.22.tgz", + "integrity": "sha1-MzCvdWyralQnAMZLLk5KoGLVL/8=" + }, "kerberos": { "version": "0.0.4", "resolved": "https://registry.npmjs.org/kerberos/-/kerberos-0.0.4.tgz", @@ -301,6 +318,11 @@ "lodash.isarray": "3.0.4" } }, + "lolex": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-2.1.2.tgz", + "integrity": "sha1-JpS5U8nqTQE+W4v7qJHJkQJbJik=" + }, "minimatch": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", @@ -435,6 +457,29 @@ "integrity": "sha1-PHa1OC6rM+RLdY0oE8qdkuk0LzQ=", "dev": true }, + "native-promise-only": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/native-promise-only/-/native-promise-only-0.8.1.tgz", + "integrity": "sha1-IKMYwwy0X3H+et+/eyHJnBRy7xE=" + }, + "nise": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/nise/-/nise-1.0.1.tgz", + "integrity": "sha1-DakrEKhU6XwPSW9sKEWjASgLPu8=", + "requires": { + "formatio": "1.2.0", + "just-extend": "1.1.22", + "lolex": "1.6.0", + "path-to-regexp": "1.7.0" + }, + "dependencies": { + "lolex": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.6.0.tgz", + "integrity": "sha1-OpoCg0UqR9dDnnJzG54H1zhuSfY=" + } + } + }, "object-assign": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.0.tgz", @@ -461,6 +506,21 @@ "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, + "path-to-regexp": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.7.0.tgz", + "integrity": "sha1-Wf3g9DW62suhA6hOnTvGTpa5k30=", + "requires": { + "isarray": "0.0.1" + }, + "dependencies": { + "isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=" + } + } + }, "pg": { "version": "6.4.1", "resolved": "https://registry.npmjs.org/pg/-/pg-6.4.1.tgz", @@ -578,6 +638,11 @@ "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", "dev": true }, + "samsam": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.2.1.tgz", + "integrity": "sha1-7dOQk6MYQ3DLhZJDsr3yVefY6mc=" + }, "semver": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-5.3.0.tgz", @@ -638,6 +703,22 @@ "integrity": "sha1-yYzaN0qmsZDfi6h8mInCtNtiAGM=", "dev": true }, + "sinon": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-3.2.1.tgz", + "integrity": "sha512-KY3OLOWpek/I4NGAMHetuutVgS2aRgMR5g5/1LSYvPJ3qo2BopIvk3esFztPxF40RWf/NNNJzdFPriSkXUVK3A==", + "requires": { + "diff": "3.2.0", + "formatio": "1.2.0", + "lolex": "2.1.2", + "native-promise-only": "0.8.1", + "nise": "1.0.1", + "path-to-regexp": "1.7.0", + "samsam": "1.2.1", + "text-encoding": "0.6.4", + "type-detect": "4.0.3" + } + }, "split": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/split/-/split-1.0.0.tgz", @@ -1779,12 +1860,22 @@ "has-flag": "2.0.0" } }, + "text-encoding": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.6.4.tgz", + "integrity": "sha1-45mpgiV6J22uQou5KEXLcb3CbRk=" + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, + "type-detect": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.3.tgz", + "integrity": "sha1-Dj8mcLRAmbC0bChNE2p+9Jx0wuo=" + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 424d140a..dfc98558 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,7 @@ "sqlite", "mongodb" ], - "version": "3.2.4", + "version": "4.0.0", "license": "MIT", "homepage": "http://dresende.github.io/node-orm2", "repository": "http://github.com/dresende/node-orm2.git", @@ -62,12 +62,14 @@ "analyse": false, "dependencies": { "async": "2.5.0", + "bluebird": "^3.5.0", "enforce": "0.1.7", "hat": "0.0.3", "lodash": "4.17.4", "path-is-absolute": "1.0.1", - "sql-query": "0.1.26", - "sql-ddl-sync": "0.3.13" + "sinon": "^3.2.1", + "sql-ddl-sync": "0.3.13", + "sql-query": "0.1.26" }, "devDependencies": { "chalk": "2.0.1", diff --git a/test/integration/association-extend-async.js b/test/integration/association-extend-async.js new file mode 100644 index 00000000..ffe1065e --- /dev/null +++ b/test/integration/association-extend-async.js @@ -0,0 +1,189 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); +var ORM = require('../../'); + +describe("Model.extendsTo()", function() { + var db = null; + var Person = null; + var PersonAddress = null; + + var setup = function () { + return function (done) { + Person = db.define("person", { + name : String + }); + PersonAddress = Person.extendsTo("address", { + street : String, + number : Number + }); + + ORM.singleton.clear(); + + return helper.dropSync([ Person, PersonAddress ], function () { + Person.create({ + name: "John Doe" + }, function (err, person) { + should.not.exist(err); + + return person.setAddress(new PersonAddress({ + street : "Liberty", + number : 123 + }), done); + }); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe("when calling hasAccessorAsync", function () { // TODO: fix Model.find to async + before(setup()); + + it("should return true if found", function () { + return Person.find() + .firstAsync() + .then(function (John) { + return John.hasAddressAsync(); + }) + .then(function (hasAddress) { + should.equal(hasAddress, true); + }); + }); + + it("should return error if instance not with an ID", function (done) { + var Jane = new Person({ + name: "Jane" + }); + + Jane.hasAddressAsync() + .catch(function (err) { + err.should.be.a.Object(); + should.equal(Array.isArray(err), false); + err.should.have.property("code", ORM.ErrorCodes.NOT_DEFINED); + done(); + }); + }); + }); + + describe("when calling getAccessorAsync", function () { // TODO: fix Model.find to async + before(setup()); + + it("should return extension if found", function () { + return Person.find() + .firstAsync() + .then(function (John) { + return John.getAddressAsync(); + }) + .then(function (Address) { + Address.should.be.a.Object(); + should.equal(Array.isArray(Address), false); + Address.should.have.property("street", "Liberty"); + }); + }); + + it("should return error if not found", function (done) { + Person.find() + .firstAsync() + .then(function (John) { + return [John, John.removeAddressAsync()]; + }) + .spread(function(John) { + return John.getAddressAsync(); + }) + .catch(function(err) { + err.should.be.a.Object(); + err.should.have.property("code", ORM.ErrorCodes.NOT_FOUND); + done(); + }); + }); + + it("should return error if instance not with an ID", function (done) { + var Jane = new Person({ + name: "Jane" + }); + Jane.getAddressAsync() + .catch(function(err) { + err.should.be.a.Object(); + err.should.have.property("code", ORM.ErrorCodes.NOT_DEFINED); + done(); + }); + }); + }); + + describe("when calling setAccessorAsync", function () { + before(setup()); + + it("should remove any previous extension", function () { // TODO: fix Model.find to async + return Person.find().firstAsync() + .then(function (John) { + return [John, PersonAddress.find({ number: 123 }).countAsync()]; + }) + .spread(function (John, count) { + count.should.equal(1); + + var addr = new PersonAddress({ + street : "4th Ave", + number : 4 + }); + + return [John, addr, John.setAddressAsync(addr)]; + }) + .spread(function (John, addr) { + return [addr, John.getAddressAsync()]; + }) + .spread(function (addr, Address) { + Address.should.be.a.Object(); + should.equal(Array.isArray(Address), false); + Address.should.have.property("street", addr.street); + return PersonAddress.findAsync({ number: 123 }); + }) + .then(function (addres) { + addres.length.should.equal(0); + }); + }); + }); + + describe("when calling delAccessor + Async", function () { // TODO: fix .find to async + before(setup()); + + it("should remove any extension", function () { + return Person.find().firstAsync() + .then(function (John) { + return [John, PersonAddress.find({ number: 123 }).countAsync()]; + }) + .spread(function (John, count) { + count.should.equal(1); + + return John.removeAddressAsync(); + }) + .then(function () { + return PersonAddress.findAsync({ number: 123 }); + }) + .then(function (addres) { + addres.length.should.equal(0); + }); + }); + + it("should return error if instance not with an ID", function (done) { + var Jane = new Person({ + name: "Jane" + }); + Jane.removeAddressAsync() + .catch(function(err) { + err.should.be.a.Object(); + err.should.have.property("code", ORM.ErrorCodes.NOT_DEFINED); + done(); + }); + }); + }); +}); diff --git a/test/integration/association-extend.js b/test/integration/association-extend.js index ea7a7e93..d5c43da6 100644 --- a/test/integration/association-extend.js +++ b/test/integration/association-extend.js @@ -215,7 +215,169 @@ describe("Model.extendsTo()", function() { }); }); - describe("findBy()", function () { + describe("when calling hasAccessor + Async", function () { + before(setup()); + + it("should return true if found", function (done) { + Person.find().first(function (err, John) { + should.equal(err, null); + + John.hasAddressAsync().then(function (hasAddress) { + should.equal(err, null); + hasAddress.should.equal(true); + + done(); + }).catch(function(err){ + done(err); + }); + }); + }); + + it("should return false if not found", function (done) { + Person.find().first(function (err, John) { + should.equal(err, null); + John.removeAddressAsync().then(function(){ + John.hasAddressAsync().then(function (hasAddress) { + }).catch(function(err) { + err.should.be.a.Object(); + done(); + }); + }); + }); + }); + + it("should return error if instance not with an ID", function (done) { + var Jane = new Person({ + name: "Jane" + }); + Jane.hasAddressAsync().catch(function(err) { + err.should.be.a.Object(); + err.should.have.property("code", ORM.ErrorCodes.NOT_DEFINED); + done(); + }); + }); + }); + + describe("when calling getAccessor + Async", function () { + before(setup()); + + it("should return extension if found", function (done) { + Person.find().first(function (err, John) { + should.equal(err, null); + + John.getAddressAsync(John).then(function (Address) { + Address.should.be.a.Object(); + Address.should.have.property("street", "Liberty"); + done(); + }).catch(function (err) { + done(err); + }); + }); + }); + + it("should return error if not found", function (done) { + Person.find().first(function (err, John) { + should.equal(err, null); + John.removeAddressAsync().then(function () { + John.getAddressAsync(John).catch(function(err){ + err.should.be.a.Object(); + err.should.have.property("code", ORM.ErrorCodes.NOT_FOUND); + done(); + }); + }); + }); + }); + + it("should return error if instance not with an ID", function (done) { + var Jane = new Person({ + name: "Jane" + }); + Jane.getAddressAsync().catch(function(err) { + err.should.be.a.Object(); + err.should.have.property("code", ORM.ErrorCodes.NOT_DEFINED); + done(); + }); + }); + }); + + describe("when calling setAccessor + Async", function () { + before(setup()); + + it("should remove any previous extension", function (done) { + Person.find().first(function (err, John) { + should.equal(err, null); + + PersonAddress.find({ number: 123 }).count(function (err, c) { + should.equal(err, null); + c.should.equal(1); + + var addr = new PersonAddress({ + street : "4th Ave", + number : 4 + }); + + John.setAddressAsync(addr).then(function () { + John.getAddressAsync(addr).then(function (Address) { + Address.should.be.a.Object(); + Address.should.have.property("street", addr.street); + PersonAddress.find({ number: 123 }).count(function (err, c) { + should.equal(err, null); + c.should.equal(0); + done(); + }); + }); + }).catch(function(err) { + done(err); + }); + }); + }); + }); + }); + + describe("when calling delAccessor + Async", function () { + before(setup()); + + it("should remove any extension", function (done) { + Person.find().first(function (err, John) { + should.equal(err, null); + + PersonAddress.find({ number: 123 }).count(function (err, c) { + should.equal(err, null); + c.should.equal(1); + + var addr = new PersonAddress({ + street : "4th Ave", + number : 4 + }); + + John.removeAddressAsync().then(function () { + + PersonAddress.find({ number: 123 }).count(function (err, c) { + should.equal(err, null); + c.should.equal(0); + + done(); + }); + }).catch(function(err) { + done(err); + }); + }); + }); + }); + + it("should return error if instance not with an ID", function (done) { + var Jane = new Person({ + name: "Jane" + }); + Jane.removeAddressAsync().catch(function(err) { + err.should.be.a.Object(); + err.should.have.property("code", ORM.ErrorCodes.NOT_DEFINED); + done(); + }); + }); + }); + + describe("findBy()", function () { // TODO: make async after Models method include async support before(setup()); it("should throw if no conditions passed", function (done) { diff --git a/test/integration/association-hasmany-async.js b/test/integration/association-hasmany-async.js new file mode 100644 index 00000000..d49a7ce7 --- /dev/null +++ b/test/integration/association-hasmany-async.js @@ -0,0 +1,575 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); +var common = require('../common'); +var protocol = common.protocol(); + +describe("hasMany", function () { + var db = null; + var Person = null; + var Pet = null; + + before(function(done) { + helper.connect(function (connection) { + db = connection; + done(); + }); + }); + + describe("normal", function () { + + var setup = function (opts) { + opts = opts || {}; + + return function (done) { + db.settings.set('instance.identityCache', false); + + Person = db.define('person', { + name : String, + surname : String, + age : Number + }); + Pet = db.define('pet', { + name : String + }); + Person.hasMany('pets', Pet, {}, { autoFetch: opts.autoFetchPets }); + + helper.dropSync([ Person, Pet], function (err) { + should.not.exist(err); + + Pet.create([{ name: "Cat" }, { name: "Dog" }], function (err) { + should.not.exist(err); + + /** + * John --+---> Deco + * '---> Mutt <----- Jane + * + * Justin + */ + Person.create([ + { + name : "Bob", + surname : "Smith", + age : 30 + }, + { + name : "John", + surname : "Doe", + age : 20, + pets : [{ + name : "Deco" + }, { + name : "Mutt" + }] + }, { + name : "Jane", + surname : "Doe", + age : 16 + }, { + name : "Justin", + surname : "Dean", + age : 18 + } + ], function (err) { + should.not.exist(err); + + Person.find({ name: "Jane" }, function (err, people) { + should.not.exist(err); + + Pet.find({ name: "Mutt" }, function (err, pets) { + should.not.exist(err); + + people[0].addPets(pets, done); + }); + }); + }); + }); + }); + }; + }; + + describe("getAccessorAsync", function () { + before(setup()); + + it("should allow to specify order as string", function () { + return Person.findAsync({ name: "John" }) + .then(function (people) { + return people[0].getPetsAsync("-name"); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(2); + pets[0].model().should.equal(Pet); + pets[0].name.should.equal("Mutt"); + pets[1].name.should.equal("Deco"); + }); + }); + + it("should return proper instance model", function(){ + return Person.findAsync({ name: "John" }) + .then(function (people) { + return people[0].getPetsAsync("-name"); + }) + .then(function (pets) { + pets[0].model().should.equal(Pet); + }); + }); + + it("should allow to specify order as Array", function () { + return Person.findAsync({ name: "John" }) + .then(function (people) { + return people[0].getPetsAsync([ "name", "Z" ]); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(2); + pets[0].name.should.equal("Mutt"); + pets[1].name.should.equal("Deco"); + }); + }); + + it("should allow to specify a limit", function () { + return Person.find({ name: "John" }) + .firstAsync() + .then(function (John) { + return John.getPetsAsync(1) + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + }); + }); + + it("should allow to specify conditions", function () { + return Person.find({ name: "John" }).firstAsync() + .then(function (John) { + return John.getPetsAsync({ name: "Mutt" }); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + pets[0].name.should.equal("Mutt"); + }); + }); + + if (common.protocol() == "mongodb") return; + + it("should allow chaining count()", function () { + return Person.findAsync({}) + .then(function (people) { + return [people[1].getPetsAsync(), people[2].getPetsAsync(), people[3].getPetsAsync()]; + }) + .spread(function (count1, count2, count3) { + should.strictEqual(count1.length, 2); + should.strictEqual(count2.length, 1); + should.strictEqual(count3.length, 0); + }); + }); + }); + + describe("hasAccessorAsync", function () { + before(setup()); + + it("should return true if instance has associated item", function () { + return Pet.findAsync({ name: "Mutt" }) + .then(function (pets) { + return [pets, Person.find({ name: "Jane" }).firstAsync()]; + }) + .spread(function (pets, Jane) { + return Jane.hasPetsAsync(pets[0]); + }) + .then(function (has_pets) { + has_pets.should.be.true(); + }); + }); + + it("should return false if any passed instances are not associated", function () { + return Pet.findAsync() + .then(function (pets) { + return [pets, Person.find({ name: "Jane" }).firstAsync()]; + }) + .spread(function (pets, Jane) { + return Jane.hasPetsAsync(pets); + }) + .then(function (has_pets) { + has_pets.should.be.false(); + }); + }); + + if (common.protocol() != "mongodb") { + it("should return true if join table has duplicate entries", function () { + return Pet.findAsync({ name: ["Mutt", "Deco"] }) + .then(function (pets) { + should.equal(pets.length, 2); + + return [pets, Person.find({ name: "John" }).firstAsync()]; + }) + .spread(function (pets, John) { + return [John, pets, John.hasPetsAsync(pets)]; + }) + .spread(function (John, pets, hasPets) { + should.equal(hasPets, true); + + return [ + John, + pets, + db.driver.execQueryAsync( + "INSERT INTO person_pets (person_id, pets_id) VALUES (?,?), (?,?)", + [John.id, pets[0].id, John.id, pets[1].id] + ) + ]; + }) + .spread(function (John, pets) { + return John.hasPetsAsync(pets); + }) + .then(function (hasPets) { + should.equal(hasPets, true); + }); + }); + + it("should return true if join table has duplicate entries (promise-based)", function () { + return Pet.findAsync({ name: ["Mutt", "Deco"] }) + .then(function (pets) { + should.equal(pets.length, 2); + + return [pets, Person.find({ name: "John" }).firstAsync()]; + }) + .spread(function (pets, John) { + return [ John, pets, John.hasPetsAsync(pets)]; + }) + .spread(function (John, pets, hasPets) { + should.equal(hasPets, true); + + return [ + John, + pets, + db.driver.execQueryAsync( + "INSERT INTO person_pets (person_id, pets_id) VALUES (?,?), (?,?)", + [John.id, pets[0].id, John.id, pets[1].id] + ) + ]; + }) + .spread(function (John, pets) { + return John.hasPetsAsync(pets); + }) + .then(function (hasPets) { + should.equal(hasPets, true); + }); + }); + } + }); + + describe("delAccessorAsync", function () { + before(setup()); + + it("should accept arguments in different orders", function () { + return Pet.findAsync({ name: "Mutt" }) + .then(function (pets) { + return [pets, Person.findAsync({ name: "John" })]; + }) + .spread(function (pets, people) { + return [people, people[0].removePetsAsync(pets[0])]; + }) + .spread(function (people) { + return people[0].getPetsAsync(); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + pets[0].name.should.equal("Deco"); + }); + }); + + it("should remove specific associations if passed", function () { + return Pet.findAsync({ name: "Mutt" }) + .then(function (pets) { + return [pets, Person.findAsync({ name: "John" })]; + }) + .spread(function (pets, people) { + return [people, people[0].removePetsAsync(pets[0])]; + }) + .spread(function (people) { + return people[0].getPetsAsync(); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + pets[0].name.should.equal("Deco"); + }); + }); + + it("should remove all associations if none passed", function () { + return Person.find({ name: "John" }).firstAsync() + .then(function (John) { + return [John, John.removePetsAsync()]; + }) + .spread(function (John) { + return John.getPetsAsync(); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(0); + }); + }); + }); + + describe("addAccessorAsync", function () { + before(setup()); + + if (common.protocol() != "mongodb") { + + it("might add duplicates (promise-based)", function () { + return Pet.findAsync({ name: "Mutt" }) + .then(function (pets) { + return [pets, Person.findAsync({ name: "Jane" })]; + }) + .spread(function (pets, people) { + return [people, people[0].addPetsAsync(pets[0])]; + }) + .spread(function (people) { + return people[0].getPetsAsync("name"); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(2); + pets[0].name.should.equal("Mutt"); + pets[1].name.should.equal("Mutt"); + }); + }); + } + + it("should keep associations and add new ones", function () { + return Pet.find({ name: "Deco" }).firstAsync() + .then(function (Deco) { + return [Deco, Person.find({ name: "Jane" }).firstAsync()]; + }) + .spread(function (Deco, Jane) { + return [Jane, Deco, Jane.getPetsAsync()] + }) + .spread(function (Jane, Deco, janesPets) { + var petsAtStart = janesPets.length; + + return [petsAtStart, Jane, Jane.addPetsAsync(Deco)]; + }) + .spread(function (petsAtStart, Jane) { + return [petsAtStart, Jane.getPetsAsync("name")]; + }) + .spread(function (petsAtStart, pets) { + should(Array.isArray(pets)); + pets.length.should.equal(petsAtStart + 1); + pets[0].name.should.equal("Deco"); + pets[1].name.should.equal("Mutt"); + }); + }); + + it("should accept several arguments as associations (promise-based)", function () { + return Pet.findAsync() + .then(function (pets) { + return [pets, Person.find({ name: "Justin" }).firstAsync()]; + }) + .spread(function (pets, Justin) { + return [Justin, Justin.addPetsAsync(pets[0], pets[1])]; + }) + .spread(function (Justin) { + return Justin.getPetsAsync(); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(2); + }); + }); + + it("should accept array as list of associations (promise-based)", function () { + return Pet.createAsync([{ name: 'Ruff' }, { name: 'Spotty' }]) + .then(function (pets) { + return [pets, Person.find({ name: "Justin" }).firstAsync()]; + }) + .spread(function (pets, Justin) { + return [pets, Justin, Justin.getPetsAsync()]; + }) + .spread(function (pets, Justin, justinsPets) { + var petCount = justinsPets.length; + + return [Justin, petCount, Justin.addPetsAsync(pets)]; + }) + .spread(function (Justin, petCount) { + return [petCount, Justin.getPetsAsync()]; + }) + .spread(function (petCount, justinsPets) { + should(Array.isArray(justinsPets)); + // Mongo doesn't like adding duplicates here, so we add new ones. + should.equal(justinsPets.length, petCount + 2); + }); + }); + }); + + describe("setAccessorAsync", function () { + before(setup()); + + it("should accept several arguments as associations", function () { + return Pet.findAsync() + .then(function (pets) { + return [pets, Person.find({ name: "Justin" }).firstAsync()]; + }) + .spread(function (pets, Justin) { + return [Justin, Justin.setPetsAsync(pets[0], pets[1])] + }) + .spread(function (Justin) { + return Justin.getPetsAsync(); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(2); + }); + }); + + it("should accept an array of associations", function () { + return Pet.findAsync() + .then(function (pets) { + return [pets, Person.find({ name: "Justin" }).firstAsync()]; + }) + .spread(function (pets, Justin) { + return [pets, Justin, Justin.setPetsAsync(pets)]; + }) + .spread(function (pets, Justin) { + return [pets, Justin.getPetsAsync()]; + }) + .spread(function (pets, all_pets) { + should(Array.isArray(all_pets)); + all_pets.length.should.equal(pets.length); + }); + }); + + it("should remove all associations if an empty array is passed", function () { + return Person.find({ name: "Justin" }).firstAsync() + .then(function (Justin) { + return [Justin, Justin.getPetsAsync()]; + }) + .spread(function (Justin, pets) { + should.equal(pets.length, 4); + + return [Justin, Justin.setPetsAsync([])]; + }) + .spread(function (Justin) { + return Justin.getPetsAsync(); + }) + .then(function (pets) { + should.equal(pets.length, 0); + }); + }); + + it("clears current associations", function () { + return Pet.findAsync({ name: "Deco" }) + .then(function (pets) { + var Deco = pets[0]; + + return [Deco, Person.find({ name: "Jane" }).firstAsync()]; + }) + .spread(function (Deco, Jane) { + return [Jane, Deco, Jane.getPetsAsync()]; + }) + .spread(function (Jane, Deco, pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + pets[0].name.should.equal("Mutt"); + + return [Jane, Deco, Jane.setPetsAsync(Deco)]; + }) + .spread(function (Jane, Deco) { + return [Deco, Jane.getPetsAsync()]; + }) + .spread(function (Deco, pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + pets[0].name.should.equal(Deco.name); + }); + }); + }); + + describe("with autoFetch turned on", function () { + before(setup({ + autoFetchPets : true + })); + + it("should fetch associations", function () { + return Person.find({ name: "John" }).firstAsync() + .then(function (John) { + John.should.have.property("pets"); + should(Array.isArray(John.pets)); + John.pets.length.should.equal(2); + }); + }); + + it("should save existing", function () { + return Person.createAsync({ name: 'Bishan' }) + .then(function () { + return Person.oneAsync({ name: 'Bishan' }); + }) + .then(function (person) { + person.surname = 'Dominar'; + + return person.saveAsync(); + }) + .then(function (person) { + should.equal(person.surname, 'Dominar'); + }); + }); + + it("should not auto save associations which were autofetched", function () { + return Pet.allAsync() + .then(function (pets) { + should.equal(pets.length, 4); + + return [pets, Person.createAsync({ name: 'Paul' })]; + }) + .spread(function (pets) { + return [pets, Person.oneAsync({ name: 'Paul' })]; + }) + .spread(function (pets, paul) { + should.equal(paul.pets.length, 0); + + return paul.setPetsAsync(pets); + }) + .then(function () { + // reload paul to make sure we have 2 pets + return Person.oneAsync({ name: 'Paul' }); + }) + .then(function (paul) { + should.equal(paul.pets.length, 4); + + // Saving paul2 should NOT auto save associations and hence delete + // the associations we just created. + return paul.saveAsync(); + }) + .then(function () { + // let's check paul - pets should still be associated + return Person.oneAsync({ name: 'Paul' }); + }) + .then(function (paul) { + should.equal(paul.pets.length, 4); + }); + }); + + it("should save associations set by the user", function () { + return Person.oneAsync({ name: 'John' }) + .then(function (john) { + should.equal(john.pets.length, 2); + + john.pets = []; + + return john.saveAsync(); + }) + .then(function () { + // reload john to make sure pets were deleted + return Person.oneAsync({ name: 'John' }); + }) + .then(function (john) { + should.equal(john.pets.length, 0); + }); + }); + + }); + }); + + if (protocol == "mongodb") return; + +}); diff --git a/test/integration/association-hasmany-extra.js b/test/integration/association-hasmany-extra.js index 5b8606d9..2b07f164 100644 --- a/test/integration/association-hasmany-extra.js +++ b/test/integration/association-hasmany-extra.js @@ -1,6 +1,5 @@ var should = require('should'); var helper = require('../support/spec_helper'); -var ORM = require('../../'); describe("hasMany extra properties", function() { var db = null; @@ -75,4 +74,44 @@ describe("hasMany extra properties", function() { }); }); }); + + describe("if passed to addAccessorAsync", function () { + before(setup()); + + it("should be added to association", function () { + return Person.createAsync([{ + name : "John" + }]) + .then(function (people) { + return [people, Pet.createAsync([{ + name : "Deco" + }, { + name : "Mutt" + }])]; + }) + .spread(function (people, pets) { + var data = { adopted: true }; + + return [pets, data, people[0].addPetsAsync(pets, { since : new Date(), data: data })]; + }) + .spread(function (pets, data) { + return [pets, data, Person.find({ name: "John" }, { autoFetch : true }).firstAsync()]; + }) + .spread(function (pets, data, John) { + John.should.have.property("pets"); + should(Array.isArray(pets)); + + John.pets.length.should.equal(2); + + John.pets[0].should.have.property("name"); + John.pets[0].should.have.property("extra"); + John.pets[0].extra.should.be.a.Object(); + John.pets[0].extra.should.have.property("since"); + should(John.pets[0].extra.since instanceof Date); + + should.equal(typeof John.pets[0].extra.data, 'object'); + should.equal(JSON.stringify(data), JSON.stringify(John.pets[0].extra.data)); + }); + }); + }); }); diff --git a/test/integration/association-hasmany-hooks.js b/test/integration/association-hasmany-hooks.js index 9f640f04..2eeb9607 100644 --- a/test/integration/association-hasmany-hooks.js +++ b/test/integration/association-hasmany-hooks.js @@ -1,6 +1,5 @@ var should = require('should'); var helper = require('../support/spec_helper'); -var ORM = require('../../'); describe("hasMany hooks", function() { var db = null; @@ -64,8 +63,6 @@ describe("hasMany hooks", function() { }); describe("beforeSave", function () { - var had_extra = false; - before(setup({}, { hooks : { beforeSave: function (next) { @@ -93,8 +90,6 @@ describe("hasMany hooks", function() { }); describe("beforeSave", function () { - var had_extra = false; - before(setup({}, { hooks : { beforeSave: function (next) { @@ -122,4 +117,72 @@ describe("hasMany hooks", function() { }); }); }); + + describe("beforeSaveAsync", function () { + var had_extra = false; + + before(setup({ + born : Date + }, { + hooks : { + beforeSave: function (extra) { + return new Promise(function (resolve) { + setTimeout(function () { + had_extra = (typeof extra == "object"); + resolve() + }, 3000); + }); + } + } + })); + + it("should pass extra data to hook if extra defined", function () { + return Person.createAsync({ + name : "John" + }) + .then(function (John) { + return [John, Pet.createAsync({ + name : "Deco" + })]; + }) + .spread(function (John, Deco) { + return John.addPetsAsync(Deco); + }) + .then(function () { + had_extra.should.equal(true); + }); + }); + }); + + describe("beforeSaveAsync", function () { + before(setup({}, { + hooks : { + beforeSave: function () { + return new Promise(function (resolve, reject) { + setTimeout(function () { + return reject(new Error('blocked')); + }, 1000); + }); + } + } + })); + + it("should block if error returned", function () { + return Person.createAsync({ + name : "John" + }) + .then(function (John) { + return [John, Pet.createAsync({ + name : "Deco" + })]; + }) + .spread(function (John, Deco) { + return John.addPetsAsync(Deco); + }) + .catch(function(err) { + should.exist(err); + err.message.should.equal('blocked'); + }); + }); + }); }); diff --git a/test/integration/association-hasmany-mapsto-async.js b/test/integration/association-hasmany-mapsto-async.js new file mode 100644 index 00000000..1088bdfe --- /dev/null +++ b/test/integration/association-hasmany-mapsto-async.js @@ -0,0 +1,474 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); +var common = require('../common'); + +if (common.protocol() == "mongodb") return; // Can't do mapsTo testing on mongoDB () + +describe("hasMany with MapsTo Async", function () { + var db = null; + var Person = null; + var Pet = null; + + before(function(done) { + helper.connect(function (connection) { + db = connection; + done(); + }); + }); + + var setup = function (opts) { + opts = opts || {}; + + return function (done) { + db.settings.set('instance.identityCache', false); + + Person = db.define('person', { + id : {type : "serial", size:"8", mapsTo: "personID", key:true}, + firstName : {type : "text", size:"255", mapsTo: "name"}, + lastName : {type : "text", size:"255", mapsTo: "surname"}, + ageYears : {type : "number", size:"8", mapsTo: "age"} + }); + + Pet = db.define('pet', { + id : {type : "serial", size:"8", mapsTo:"petID", key:true}, + petName : {type : "text", size:"255", mapsTo: "name"} + }); + + Person.hasMany('pets', Pet, {}, + { autoFetch: opts.autoFetchPets, + mergeTable: 'person_pet', + mergeId: 'person_id', + mergeAssocId: 'pet_id'}); + + helper.dropSync([ Person, Pet ], function (err) { + if (err) return done(err); + // + // John --+---> Deco + // '---> Mutt <----- Jane + // + // Justin + // + Person.create([{ + firstName : "John", + lastName : "Doe", + ageYears : 20, + pets : [{ + petName : "Deco" + }, { + petName : "Mutt" + }] + }, { + firstName : "Jane", + lastName : "Doe", + ageYears : 16 + }, { + firstName : "Justin", + lastName : "Dean", + ageYears : 18 + }], function () { + Person.find({ firstName: "Jane" }, function (err, people) { + Pet.find({ petName: "Mutt" }, function (err, pets) { + people[0].addPets(pets, done); + }); + }); + }); + }); + }; + }; + + describe("getAccessorAsync", function () { + before(setup()); + + it("should allow to specify order as string", function () { + return Person.findAsync({ firstName: "John" }) + .then(function (people) { + return people[0].getPetsAsync("-petName"); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(2); + pets[0].model().should.equal(Pet); + pets[0].petName.should.equal("Mutt"); + pets[1].petName.should.equal("Deco"); + }); + }); + + it("should return proper instance model", function(){ + return Person.findAsync({ firstName: "John" }) + .then(function (people) { + return people[0].getPetsAsync("-petName"); + }) + .then(function (pets) { + pets[0].model().should.equal(Pet); + }); + }); + + it("should allow to specify order as Array", function () { + return Person.findAsync({ firstName: "John" }) + .then(function (people) { + return people[0].getPetsAsync([ "petName", "Z" ]); + }) + .then(function (pets) { + + should(Array.isArray(pets)); + pets.length.should.equal(2); + pets[0].petName.should.equal("Mutt"); + pets[1].petName.should.equal("Deco"); + }); + }); + + it("should allow to specify a limit", function () { + return Person.find({ firstName: "John" }).firstAsync() + .then(function (John) { + return John.getPetsAsync(1); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + }); + }); + + it("should allow to specify conditions", function () { + return Person.find({ firstName: "John" }).firstAsync() + .then(function (John) { + return John.getPetsAsync({ petName: "Mutt" }); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + pets[0].petName.should.equal("Mutt"); + }); + }); + + if (common.protocol() == "mongodb") return; + + it("should allow chaining count()", function () { + return Person.findAsync({}) + .then(function (people) { + return [people, people[0].getPetsAsync()]; + }) + .spread(function (people, count) { + should.strictEqual(count.length, 2); + return [people, people[1].getPetsAsync()]; + }) + .spread(function (people, count) { + should.strictEqual(count.length, 1); + return people[2].getPetsAsync(); + }) + .then(function (count) { + should.strictEqual(count.length, 0); + }); + }); + }); + + describe("hasAccessorAsync", function () { + before(setup()); + + it("should return true if instance has associated item", function () { + Pet.findAsync({ petName: "Mutt" }) + .then(function (pets) { + return [pets, Person.find({ firstName: "Jane" }).firstAsync()]; + }) + .spread(function (pets, Jane) { + return Jane.hasPetsAsync(pets[0]); + }) + .then(function (has_pets) { + has_pets.should.equal(true); + }); + }); + + it("should return false if any passed instances are not associated", function () { + Pet.findAsync() + .then(function (pets) { + return [pets, Person.find({ firstName: "Jane" }).firstAsync()]; + }) + .spread(function (pets, Jane) { + return Jane.hasPetsAsync(pets); + }) + .then(function (has_pets) { + has_pets.should.be.false(); + }); + }); + }); + + describe("delAccessorAsync", function () { + before(setup()); + + it("should accept arguments in different orders", function () { + return Pet.findAsync({ petName: "Mutt" }) + .then(function (pets) { + return [pets, Person.findAsync({ firstName: "John" })]; + }) + .spread(function (pets, people) { + return [people, people[0].removePetsAsync(pets[0])]; + }) + .spread(function (people) { + return people[0].getPetsAsync(); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + pets[0].petName.should.equal("Deco"); + + }); + }); + }); + + describe("delAccessorAsync", function () { + before(setup()); + + it("should remove specific associations if passed", function () { + return Pet.findAsync({ petName: "Mutt" }) + .then(function (pets) { + return [pets, Person.findAsync({ firstName: "John" })]; + }) + .spread(function (pets, people) { + return [people, people[0].removePetsAsync(pets[0])]; + }) + .spread(function (people) { + return people[0].getPetsAsync(); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + pets[0].petName.should.equal("Deco"); + }); + }); + + it("should remove all associations if none passed", function () { + return Person.find({ firstName: "John" }).firstAsync() + .then(function (John) { + return [John, John.removePetsAsync()]; + }) + .spread(function (John) { + return John.getPetsAsync(); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(0); + }); + }); + }); + + describe("addAccessorAsync", function () { + before(setup()); + + if (common.protocol() != "mongodb") { + it("might add duplicates", function () { + return Pet.findAsync({ petName: "Mutt" }) + .then(function (pets) { + return [pets, Person.findAsync({ firstName: "Jane" })]; + }) + .spread(function (pets, people) { + return [people, people[0].addPetsAsync(pets[0])]; + }) + .spread(function (people) { + return people[0].getPetsAsync("petName"); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(2); + pets[0].petName.should.equal("Mutt"); + pets[1].petName.should.equal("Mutt"); + }); + }); + } + + it("should keep associations and add new ones", function () { + return Pet.find({ petName: "Deco" }).firstAsync() + .then(function (Deco) { + return [Deco, Person.find({ firstName: "Jane" }).firstAsync()]; + }) + .spread(function (Deco, Jane) { + return [Deco, Jane, Jane.getPetsAsync()]; + }) + .spread(function (Deco, Jane, janesPets) { + var petsAtStart = janesPets.length; + return [petsAtStart, Jane, Jane.addPetsAsync(Deco)]; + }) + .spread(function (petsAtStart, Jane) { + return [petsAtStart, Jane.getPetsAsync("petName")]; + }) + .spread(function (petsAtStart, pets) { + should(Array.isArray(pets)); + pets.length.should.equal(petsAtStart + 1); + pets[0].petName.should.equal("Deco"); + pets[1].petName.should.equal("Mutt"); + }); + }); + + it("should accept several arguments as associations", function () { + return Pet.findAsync() + .then(function (pets) { + return [pets, Person.find({ firstName: "Justin" }).firstAsync()]; + }) + .spread(function (pets, Justin) { + return [Justin, Justin.addPetsAsync(pets[0], pets[1])]; + }) + .spread(function (Justin) { + return Justin.getPetsAsync(); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(2); + }); + }); + + it("should accept array as list of associations", function () { + return Pet.createAsync([{ petName: 'Ruff' }, { petName: 'Spotty' }]) + .then(function (pets) { + return [pets, Person.find({ firstName: "Justin" }).firstAsync()]; + }) + .spread(function (pets, Justin) { + return [Justin, pets, Justin.getPetsAsync()]; + }) + .spread(function (Justin, pets, justinsPets) { + var petCount = justinsPets.length; + return [petCount, Justin, Justin.addPetsAsync(pets)]; + }) + .spread(function (petCount, Justin) { + return [petCount, Justin.getPetsAsync()]; + }) + .spread(function (petCount, justinsPets) { + should(Array.isArray(justinsPets)); + // Mongo doesn't like adding duplicates here, so we add new ones. + should.equal(justinsPets.length, petCount + 2); + }); + }); + + it("should throw if no items passed", function () { + return Person.oneAsync() + .then(function (person) { + return person.addPetsAsync() + }) + .catch(function(err) { + should.exists(err); + }); + }); + }); + + describe("setAccessorAsync", function () { + before(setup()); + + it("should accept several arguments as associations", function () { + return Pet.findAsync() + .then(function (pets) { + return [pets, Person.find({ firstName: "Justin" }).firstAsync()]; + }) + .spread(function (pets, Justin) { + return [Justin, Justin.setPetsAsync(pets[0], pets[1])]; + }) + .spread(function (Justin) { + return Justin.getPetsAsync(); + }) + .then(function (pets) { + should(Array.isArray(pets)); + pets.length.should.equal(2); + }); + }); + + it("should accept an array of associations", function () { + return Pet.findAsync() + .then(function (pets) { + return [pets, Person.find({ firstName: "Justin" }).firstAsync()]; + }) + .spread(function (pets, Justin) { + return [Justin, pets, Justin.setPetsAsync(pets)]; + }) + .spread(function (Justin, pets) { + return [Justin.getPetsAsync(), pets]; + }) + .spread(function (all_pets, pets) { + should(Array.isArray(all_pets)); + all_pets.length.should.equal(pets.length); + }); + }); + + it("should remove all associations if an empty array is passed", function () { + return Person.find({ firstName: "Justin" }).firstAsync() + .then(function (Justin) { + return [Justin, Justin.getPetsAsync()]; + }) + .spread(function (Justin, pets) { + should.equal(pets.length, 2); + + return [Justin, Justin.setPetsAsync([])]; + }) + .spread(function (Justin) { + return Justin.getPetsAsync(); + }) + .then(function (pets) { + should.equal(pets.length, 0); + }); + }); + + it("clears current associations", function () { + return Pet.findAsync({ petName: "Deco" }) + .then(function (pets) { + var Deco = pets[0]; + + return [Deco, Person.find({ firstName: "Jane" }).firstAsync()]; + }) + .spread(function (Deco, Jane) { + return [Deco, Jane, Jane.getPetsAsync()]; + }) + .spread(function (Deco, Jane, pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + pets[0].petName.should.equal("Mutt"); + + return [pets, Jane, Deco, Jane.setPetsAsync(Deco)] + }) + .spread(function (pets, Jane, Deco) { + return [Deco, Jane.getPetsAsync()]; + }) + .spread(function (Deco, pets) { + should(Array.isArray(pets)); + pets.length.should.equal(1); + pets[0].petName.should.equal(Deco.petName); + }); + }); + }); + + describe("with autoFetch turned on (promised-based test)", function () { + before(setup({ + autoFetchPets : true + })); + + it("should not auto save associations which were autofetched", function () { + return Pet.allAsync() + .then(function (pets) { + should.equal(pets.length, 2); + + return [pets, Person.createAsync({ firstName: 'Paul' })]; + }) + .spread(function (pets, paul) { + return [pets, paul, Person.oneAsync({ firstName: 'Paul' })]; + }) + .spread(function (pets, paul, paul2) { + should.equal(paul2.pets.length, 0); + + return [pets, paul, paul2, paul.setPetsAsync(pets)]; + }) + .spread(function (pets, paul2) { + + // reload paul to make sure we have 2 pets + return [pets, Person.oneAsync({ firstName: 'Paul' }), paul2]; + }) + .spread(function (pets, paul, paul2) { + should.equal(paul.pets.length, 2); + + // Saving paul2 should NOT auto save associations and hence delete + // the associations we just created. + return paul2.saveAsync(); + }) + .then(function () { + // let's check paul - pets should still be associated + return Person.oneAsync({ firstName: 'Paul' }); + }) + .then(function (paul) { + should.equal(paul.pets.length, 2); + }); + }); + }); +}); diff --git a/test/integration/association-hasmany-mapsto.js b/test/integration/association-hasmany-mapsto.js index 1502d32b..05a3b1a8 100644 --- a/test/integration/association-hasmany-mapsto.js +++ b/test/integration/association-hasmany-mapsto.js @@ -1,9 +1,6 @@ -var _ = require('lodash'); var should = require('should'); var helper = require('../support/spec_helper'); -var ORM = require('../../'); var common = require('../common'); -var protocol = common.protocol(); if (common.protocol() == "mongodb") return; // Can't do mapsTo testing on mongoDB () @@ -661,7 +658,6 @@ describe("hasMany with mapsTo", function () { }); }); }); - }); }); }); diff --git a/test/integration/association-hasmany.js b/test/integration/association-hasmany.js index f4bc3fad..3170b07e 100644 --- a/test/integration/association-hasmany.js +++ b/test/integration/association-hasmany.js @@ -306,6 +306,37 @@ describe("hasMany", function () { }); }); }); + it("should return true if join table has duplicate entries (promise-based)", function (done) { + Pet.find({ name: ["Mutt", "Deco"] }, function (err, pets) { + should.not.exist(err); + should.equal(pets.length, 2); + + Person.find({ name: "John" }).first(function (err, John) { + should.not.exist(err); + + John.hasPetsAsync(pets).then(function (hasPets) { + should.equal(hasPets, true); + + db.driver.execQueryAsync( + "INSERT INTO person_pets (person_id, pets_id) VALUES (?,?), (?,?)", + [John.id, pets[0].id, John.id, pets[1].id]).then( + function () { + John.hasPetsAsync(pets).then(function (hasPets) { + should.equal(hasPets, true); + done(); + }).catch(function(err){ + done(err); + }); + } + ).catch(function(err) { + done(err); + }); + }).catch(function(err){ + done(err); + }); + }); + }); + }); } }); @@ -333,10 +364,6 @@ describe("hasMany", function () { }); }); }); - }); - - describe("delAccessor", function () { - before(setup()); it("should remove specific associations if passed", function (done) { Pet.find({ name: "Mutt" }, function (err, pets) { diff --git a/test/integration/association-hasone-async.js b/test/integration/association-hasone-async.js new file mode 100644 index 00000000..81b165d6 --- /dev/null +++ b/test/integration/association-hasone-async.js @@ -0,0 +1,216 @@ +var ORM = require('../../'); +var helper = require('../support/spec_helper'); +var should = require('should'); +var async = require('async'); +var _ = require('lodash'); +var common = require('../common'); +var protocol = common.protocol(); + +describe("hasOne", function() { + var db = null; + var Tree = null; + var Stalk = null; + var Leaf = null; + var leafId = null; + var treeId = null; + var stalkId = null; + var holeId = null; + + var setup = function (opts) { + opts = opts || {}; + return function (done) { + db.settings.set('instance.identityCache', false); + db.settings.set('instance.returnAllErrors', true); + Tree = db.define("tree", { type: { type: 'text' } }); + Stalk = db.define("stalk", { length: { type: 'integer' } }); + Hole = db.define("hole", { width: { type: 'integer' } }); + Leaf = db.define("leaf", { + size: { type: 'integer' }, + holeId: { type: 'integer', mapsTo: 'hole_id' } + }, { + validations: opts.validations + }); + Leaf.hasOne('tree', Tree, { field: 'treeId', autoFetch: !!opts.autoFetch }); + Leaf.hasOne('stalk', Stalk, { field: 'stalkId', mapsTo: 'stalk_id' }); + Leaf.hasOne('hole', Hole, { field: 'holeId' }); + + return helper.dropSync([Tree, Stalk, Hole, Leaf], function() { + Tree.create({ type: 'pine' }, function (err, tree) { + should.not.exist(err); + treeId = tree[Tree.id]; + Leaf.create({ size: 14 }, function (err, leaf) { + should.not.exist(err); + leafId = leaf[Leaf.id]; + leaf.setTree(tree, function (err) { + should.not.exist(err); + Stalk.create({ length: 20 }, function (err, stalk) { + should.not.exist(err); + should.exist(stalk); + stalkId = stalk[Stalk.id]; + Hole.create({ width: 3 }, function (err, hole) { + should.not.exist(err); + holeId = hole.id; + done(); + }); + }); + }); + }); + }); + }); + }; + }; + + before(function(done) { + helper.connect(function (connection) { + db = connection; + done(); + }); + }); + + describe("accessors Async", function () { + before(setup()); + + it("get should get the association", function () { + return Leaf + .oneAsync({ size: 14 }) + .then(function (leaf) { + should.exist(leaf); + return leaf.getTreeAsync(); + }) + .then(function (tree) { + should.exist(tree); + }); + }); + + it("should return proper instance model", function () { + return Leaf + .oneAsync({ size: 14 }) + .then(function (leaf) { + return leaf.getTreeAsync(); + }) + .then(function (tree) { + tree.model().should.equal(Tree); + }); + }); + + it("get should get the association with a shell model", function () { + return Leaf(leafId) + .getTreeAsync() + .then(function (tree) { + should.exist(tree); + should.equal(tree[Tree.id], treeId); + }); + }); + + it("has should indicate if there is an association present", function () { + return Leaf.oneAsync({ size: 14 }) + .then(function (leaf) { + should.exist(leaf); + return [leaf, leaf.hasTreeAsync()]; + }) + .spread(function (leaf, has) { + should.equal(has, true); + return leaf.hasStalkAsync(); + }) + .then(function (has) { + should.equal(has, false); + }); + }); + + it("set should associate another instance", function () { + return Stalk + .oneAsync({ length: 20 }) + .then(function (stalk) { + should.exist(stalk); + return [stalk, Leaf.oneAsync({ size: 14 })]; + }) + .spread(function (stalk, leaf) { + should.exist(leaf); + should.not.exist(leaf.stalkId); + return [stalk, leaf.setStalkAsync(stalk)]; + }) + .then(function (stalk) { + return [stalk, Leaf.oneAsync({ size: 14 })]; + }) + .spread(function (stalk, leafOne) { + should.equal(leafOne.stalkId, stalk[0][Stalk.id]); + }); + }); + + it("remove should unassociation another instance", function () { + return Stalk + .oneAsync({ length: 20 }) + .then(function (stalk) { + should.exist(stalk); + return Leaf.oneAsync({size: 14}); + }) + .then(function (leaf) { + should.exist(leaf); + should.exist(leaf.stalkId); + return leaf.removeStalkAsync(); + }) + .then(function () { + return Leaf.oneAsync({ size: 14 }); + }) + .then(function (leaf) { + should.equal(leaf.stalkId, null); + }); + }); + }); + + if (protocol != "mongodb") { + describe("mapsTo Async", function () { + describe("with `mapsTo` get via `getOneAsync`", function () { + var leaf = null; + + before(setup()); + + before(function (done) { + Leaf.createAsync({ size: 444, stalkId: stalkId, holeId: holeId }) + .then(function (lf) { + leaf = lf; + done(); + }).catch(function(err) { + done(err); + }); + }); + + it("should get parent", function () { + return leaf + .getStalkAsync() + .then(function (stalk) { + should.exist(stalk); + should.equal(stalk.id, stalkId); + should.equal(stalk.length, 20); + }); + }); + }); + + describe("with `mapsTo` set via property definition", function () { + var leaf = null; + + before(setup()); + + before(function (done) { + Leaf.createAsync({ size: 444, stalkId: stalkId, holeId: holeId }) + .then(function (lf) { + leaf = lf; + done(); + }).catch(function(err) { + done(err); + }); + }); + + it("should get parent", function () { + return leaf + .getHoleAsync() + .then(function (hole) { + should.exist(hole); + should.equal(hole.id, stalkId); + should.equal(hole.width, 3); + }); + }); + }); + }); + }; +}); diff --git a/test/integration/association-hasone-required-async.js b/test/integration/association-hasone-required-async.js new file mode 100644 index 00000000..7bddbd71 --- /dev/null +++ b/test/integration/association-hasone-required-async.js @@ -0,0 +1,81 @@ +var ORM = require('../../'); +var helper = require('../support/spec_helper'); +var should = require('should'); +var async = require('async'); +var _ = require('lodash'); + +describe("hasOne Async", function() { + var db = null; + var Person = null; + + var setup = function (required) { + return function (done) { + db.settings.set('instance.identityCache', false); + db.settings.set('instance.returnAllErrors', true); + + Person = db.define('person', { + name : String + }); + Person.hasOne('parent', Person, { + required : required, + field : 'parentId' + }); + + return helper.dropSync(Person, done); + }; + }; + + before(function(done) { + helper.connect(function (connection) { + db = connection; + done(); + }); + }); + + describe("required", function () { + before(setup(true)); + + it("should not accept empty association", function (done) { + var John = new Person({ + name : "John", + parentId : null + }); + John.saveAsync() + .catch(function(err) { + should.exist(err); + should.equal(err.length, 1); + should.equal(err[0].type, 'validation'); + should.equal(err[0].msg, 'required'); + should.equal(err[0].property, 'parentId'); + done(); + }); + }); + + it("should accept association", function () { + var John = new Person({ + name : "John", + parentId : 1 + }); + return John.saveAsync(); + }); + }); + + describe("not required", function () { + before(setup(false)); + + it("should accept empty association", function () { + var John = new Person({ + name : "John" + }); + return John.saveAsync(); + }); + + it("should accept null association", function () { + var John = new Person({ + name : "John", + parent_id : null + }); + return John.saveAsync(); + }); + }); +}); \ No newline at end of file diff --git a/test/integration/association-hasone-required.js b/test/integration/association-hasone-required.js index cde0160b..137bd7d2 100644 --- a/test/integration/association-hasone-required.js +++ b/test/integration/association-hasone-required.js @@ -86,4 +86,4 @@ describe("hasOne", function() { }); }); }); -}); +}); \ No newline at end of file diff --git a/test/integration/association-hasone-reverse-async.js b/test/integration/association-hasone-reverse-async.js new file mode 100644 index 00000000..a15bd675 --- /dev/null +++ b/test/integration/association-hasone-reverse-async.js @@ -0,0 +1,219 @@ +var ORM = require('../../'); +var helper = require('../support/spec_helper'); +var should = require('should'); +var async = require('async'); +var common = require('../common'); +var _ = require('lodash'); + +describe("hasOne Async", function () { + var db = null; + var Person = null; + var Pet = null; + + var setup = function () { + return function (done) { + Person = db.define('person', { + name: String + }); + Pet = db.define('pet', { + name: String + }); + Person.hasOne('pet', Pet, { + reverse: 'owners', + field: 'pet_id' + }); + + return helper.dropSync([Person, Pet], function () { + // Running in series because in-memory sqlite encounters problems + async.series([ + Person.create.bind(Person, { name: "John Doe" }), + Person.create.bind(Person, { name: "Jane Doe" }), + Pet.create.bind(Pet, { name: "Deco" }), + Pet.create.bind(Pet, { name: "Fido" }) + ], done); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + done(); + }); + }); + + describe("reverse", function () { + removeHookRun = false; + + before(setup({ + hooks: { + beforeRemove: function () { + removeHookRun = true; + } + } + })); + + it("should create methods in both models", function (done) { + var person = Person(1); + var pet = Pet(1); + + person.getPet.should.be.a.Function(); + person.setPet.should.be.a.Function(); + person.removePet.should.be.a.Function(); + person.hasPet.should.be.a.Function(); + + pet.getOwners.should.be.a.Function(); + pet.setOwners.should.be.a.Function(); + pet.hasOwners.should.be.a.Function(); + + person.getPetAsync.should.be.a.Function(); + person.setPetAsync.should.be.a.Function(); + person.removePetAsync.should.be.a.Function(); + person.hasPetAsync.should.be.a.Function(); + + pet.getOwnersAsync.should.be.a.Function(); + pet.setOwnersAsync.should.be.a.Function(); + pet.hasOwnersAsync.should.be.a.Function(); + + return done(); + }); + + describe(".getAccessorAsync()", function () { + + it("compare if model updated", function () { + return Person + .findAsync({ name: "John Doe" }) + .then(function (John) { + return [John, Pet.findAsync({ name: "Deco" })]; + }) + .spread(function (John, deco) { + return [John[0], deco[0], deco[0].hasOwnersAsync()]; + }) + .spread(function (John, deco, has_owner) { + has_owner.should.equal(false); + return [deco.setOwnersAsync(John), deco]; + }) + .spread(function (John, deco) { + return [John, deco.getOwnersAsync()]; + }) + .spread(function (John, JohnCopy) { + should(Array.isArray(JohnCopy)); + John.should.eql(JohnCopy[0]); + }); + }); + + }); + + it("should be able to set an array of people as the owner", function () { + return Person + .findAsync({ name: ["John Doe", "Jane Doe"] }) + .then(function (owners) { + return [owners, Pet.findAsync({ name: "Fido" })]; + }) + .spread(function (owners, Fido) { + return [Fido[0], owners, Fido[0].hasOwnersAsync()]; + }) + .spread(function (Fido, owners, has_owner) { + has_owner.should.equal(false); + return [Fido, owners, Fido.setOwnersAsync(owners)]; + }) + .spread(function (Fido, owners) { + return [owners, Fido.getOwnersAsync()]; + }) + .spread(function (owners, ownersCopy) { + should(Array.isArray(owners)); + owners.length.should.equal(2); + // Don't know which order they'll be in. + var idProp = common.protocol() == 'mongodb' ? '_id' : 'id' + + if (owners[0][idProp] == ownersCopy[0][idProp]) { + owners[0].should.eql(ownersCopy[0]); + owners[1].should.eql(ownersCopy[1]); + } else { + owners[0].should.eql(ownersCopy[1]); + owners[1].should.eql(ownersCopy[0]); + } + }); + }); + }); + + describe("reverse find", function () { + before(setup()); + it("should be able to find given an association id", function () { + return Person + .findAsync({ name: "John Doe" }) + .then(function (persons) { + var John = persons[0]; + should.exist(John); + return [John, Pet.findAsync({ name: "Deco" })]; + }) + .spread(function (John, Deco) { + var Deco = Deco[0]; + should.exist(Deco); + return [John, Deco, Deco.hasOwnersAsync()]; + }) + .spread(function (John, Deco, has_owner) { + has_owner.should.equal(false); + return [John, Deco, Deco.setOwnersAsync(John)]; + }) + .spread(function (John, Deco) { + return [John, Person.findAsync({ pet_id: Deco[Pet.id] })]; + }) + .spread(function (John, owner) { + should.exist(owner); + should.equal(owner[0].name, John.name); + }); + }); + + it("should be able to find given an association instance", function () { + return Person + .findAsync({ name: "John Doe" }) + .then(function (John) { + var John = John[0]; + should.exist(John); + return [John, Pet.findAsync({ name: "Deco" })]; + }) + .spread(function (John, Deco) { + var Deco = Deco[0]; + should.exist(Deco); + return [John, Deco, Deco.hasOwnersAsync()]; + }) + .spread(function (John, Deco, has_owner) { + has_owner.should.equal(false); + return [John, Deco, Deco.setOwnersAsync(John)]; + }) + .spread(function (John, Deco) { + return [John, Person.findAsync({ pet: Deco })]; + }) + .spread(function(John, owner){ + should.exist(owner[0]); + should.equal(owner[0].name, John.name); + }); + }); + + it("should be able to find given a number of association instances with a single primary key", function () { + return Person.findAsync({ name: "John Doe" }) + .then(function (John) { + should.exist(John); + return [John, Pet.allAsync()]; + }) + .spread(function (John, pets) { + should.exist(pets); + should.equal(pets.length, 2); + return [John[0], pets, pets[0].hasOwnersAsync()]; + }) + .spread(function (John, pets, has_owner) { + has_owner.should.equal(false); + return [John, pets, pets[0].setOwnersAsync(John)]; + }) + .spread(function (John, pets) { + return [John, Person.findAsync({ pet: pets })]; + }) + .spread(function (John, owners) { + should.exist(owners[0]); + owners.length.should.equal(1); + should.equal(owners[0].name, John.name); + }); + }); + }); +}); diff --git a/test/integration/association-hasone-reverse.js b/test/integration/association-hasone-reverse.js index 8c637924..bb215182 100644 --- a/test/integration/association-hasone-reverse.js +++ b/test/integration/association-hasone-reverse.js @@ -92,6 +92,7 @@ describe("hasOne", function () { }); }); }); + describe("Chain", function () { before(function (done) { @@ -161,7 +162,7 @@ describe("hasOne", function () { Fido.hasOwners(function (err, has_owner) { should.not.exist(err); has_owner.should.be.false; - + Fido.setOwners(owners, function (err) { should.not.exist(err); @@ -188,7 +189,7 @@ describe("hasOne", function () { }); }); }); - + // broken in mongo if (common.protocol() != "mongodb") { describe("findBy()", function () { diff --git a/test/integration/association-hasone-zeroid-async.js b/test/integration/association-hasone-zeroid-async.js new file mode 100644 index 00000000..29f3777e --- /dev/null +++ b/test/integration/association-hasone-zeroid-async.js @@ -0,0 +1,155 @@ +var _ = require('lodash'); +var should = require('should'); +var async = require('async'); +var helper = require('../support/spec_helper'); +var ORM = require('../../'); + +describe("hasOne promise-based methods", function() { + var db = null; + var Person = null; + var Pet = null; + + var setup = function (autoFetch) { + return function (done) { + db.settings.set('instance.identityCache', false); + db.settings.set('instance.returnAllErrors', true); + + Person = db.define('person', { + id : { type : "integer", mapsTo: "personID", key: true }, + firstName : { type : "text", size: "255" }, + lastName : { type : "text", size: "255" } + }); + + Pet = db.define('pet', { + id : { type : "integer", mapsTo: "petID", key: true }, + petName : { type : "text", size: "255" }, + ownerID : { type : "integer", size: "4" } + }); + + Pet.hasOne('owner', Person, { field: 'ownerID', autoFetch: autoFetch }); + + helper.dropSync([Person, Pet], function(err) { + if (err) return done(err); + + Pet.create([ + { + id: 10, + petName: 'Muttley', + owner: { + id: 12, + firstName: 'Stuey', + lastName: 'McG' + } + }, + { + id: 11, + petName: 'Snagglepuss', + owner: { + id: 0, + firstName: 'John', + lastName: 'Doe' + } + } + ], done); + }); + }; + }; + + before(function(done) { + helper.connect(function (connection) { + db = connection; + done(); + }); + }); + + describe("auto fetch", function () { + before(setup(true)); + + it("should work for non-zero ownerID ", function () { + return Pet + .findAsync({petName: "Muttley"}) + .then(function(pets) { + pets[0].petName.should.equal("Muttley"); + pets[0].should.have.property("id"); + pets[0].id.should.equal(10); + pets[0].ownerID.should.equal(12); + pets[0].should.have.property("owner"); + pets[0].owner.firstName.should.equal("Stuey"); + }); + }); + + it("should work for zero ownerID ", function () { + return Pet + .findAsync({petName: "Snagglepuss"}) + .then(function(pets) { + var pet = pets[0]; + pet.petName.should.equal("Snagglepuss"); + pet.should.have.property("id"); + pet.id.should.equal(11); + + return Person.allAsync(); + }) + .then(function (people) { + should.equal(typeof people[0], 'object'); + should.equal(Array.isArray(people), true); + }); + }); + }); + + describe("no auto fetch", function () { + before(setup(false)); + + it("should work for non-zero ownerID (promise-based)", function () { + return Pet + .findAsync({petName: "Muttley"}) + .then(function(pets) { + var pets = pets[0]; + + pets.petName.should.equal("Muttley"); + pets.should.have.property("id"); + pets.id.should.equal(10); + pets.ownerID.should.equal(12); + + pets.should.not.have.property("owner"); + + // But we should be able to see if its there + return [pets, pets.hasOwnerAsync()]; + }) + .spread(function(pets, hasOwner) { + should.equal(hasOwner, true); + // ...and then get it + return pets.getOwnerAsync(); + }) + .then(function(petOwner) { + petOwner.firstName.should.equal("Stuey"); + }); + }); + + it("should work for zero ownerID", function () { + return Pet + .findAsync({petName: "Snagglepuss"}) + .then(function(pets) { + var pets = pets[0]; + + pets.petName.should.equal("Snagglepuss"); + pets.should.have.property("id"); + pets.id.should.equal(11); + pets.ownerID.should.equal(0); + + pets.should.not.have.property("owner"); + + // But we should be able to see if its there + return [pets, pets.hasOwnerAsync()]; + }) + .spread(function(pets, hasOwner) { + should.equal(hasOwner, true); + + // ...and then get it + return pets.getOwnerAsync(); + }) + .then(function(petOwner) { + petOwner.firstName.should.equal("John"); + }); + }); + }); +}); diff --git a/test/integration/association-hasone-zeroid.js b/test/integration/association-hasone-zeroid.js index 6248e5f0..8fd2bd17 100644 --- a/test/integration/association-hasone-zeroid.js +++ b/test/integration/association-hasone-zeroid.js @@ -125,32 +125,5 @@ describe("hasOne", function() { }); }); }); - - it("should work for zero ownerID ", function (done) { - Pet.find({petName: "Snagglepuss"}, function(err, pets) { - should.not.exist(err); - - pets[0].petName.should.equal("Snagglepuss"); - pets[0].should.have.property("id"); - pets[0].id.should.equal(11); - pets[0].ownerID.should.equal(0); - - pets[0].should.not.have.property("owner"); - - // But we should be able to see if its there - pets[0].hasOwner(function(err, result) { - should.not.exist(err); - should.equal(result, true); - - // ...and then get it - pets[0].getOwner(function(err, result) { - should.not.exist(err); - result.firstName.should.equal("John"); - - return done() - }); - }); - }); - }); }); }); diff --git a/test/integration/association-hasone.js b/test/integration/association-hasone.js index bc8a5a91..fa68dffb 100644 --- a/test/integration/association-hasone.js +++ b/test/integration/association-hasone.js @@ -468,4 +468,5 @@ describe("hasOne", function() { }); }); }; + }); diff --git a/test/integration/db.js b/test/integration/db.js index d0f4c793..016824b1 100644 --- a/test/integration/db.js +++ b/test/integration/db.js @@ -1,16 +1,31 @@ var should = require('should'); +var path = require('path'); var helper = require('../support/spec_helper'); -var ORM = require('../../'); +var sinon = require('sinon'); var common = require('../common'); -describe("db.use()", function () { +describe("db.driver", function () { var db = null; before(function (done) { helper.connect(function (connection) { db = connection; - return done(); + var Log = db.define('log', { + what: { type: 'text' }, + when: { type: 'date', time: true }, + who: { type: 'text' } + }); + + helper.dropSync(Log, function (err) { + if (err) return done(err); + + Log.create([ + { what: "password reset", when: new Date('2013/04/07 12:33:05'), who: "jane" }, + { what: "user login", when: new Date('2013/04/07 13:01:44'), who: "jane" }, + { what: "user logout", when: new Date('2013/05/12 04:09:31'), who: "john" } + ], done); + }); }); }); @@ -18,250 +33,399 @@ describe("db.use()", function () { return db.close(); }); - it("should be able to register a plugin", function (done) { - var MyPlugin = require("../support/my_plugin"); - var opts = { - option : true, - calledDefine : false - }; + it("should be available", function () { + should.exist(db.driver); + }); - db.use(MyPlugin, opts); + if (common.protocol() === "mongodb") return; - var MyModel = db.define("my_model", { // db.define should call plugin.define method - property: String + describe("query", function () { + it("should be available", function () { + should.exist(db.driver.query); }); - opts.calledDefine.should.be.true; - - return done(); - }); + describe('#execQueryAsync', function () { + it('should execute sql queries', function () { + return db.driver.execQueryAsync('SELECT id FROM log') + .then(function (data) { + should(JSON.stringify(data) === JSON.stringify([{ id: 1 }, { id: 2 }, { id: 3 }])); + }) + }); - it("a plugin should be able to catch models before defining them", function (done) { - var MyPlugin = require("../support/my_plugin"); - var opts = { - option : true, - calledDefine : false, - beforeDefine : function (name, props, opts) { - props.otherprop = Number; - } - }; + it("should escape sql queries", function () { + var query = "SELECT log.?? FROM log WHERE log.?? LIKE ? AND log.?? > ?"; + var args = ['what', 'who', 'jane', 'when', new Date('2013/04/07 12:40:00')]; + return db.driver.execQueryAsync(query, args) + .then(function (data) { + should(JSON.stringify(data) === JSON.stringify([{ "what": "user login" }])); + }); + }); + }); - db.use(MyPlugin, opts); + describe('#eagerQuery', function () { + if (common.protocol() === "redshift") return; + var fixture = { + association: { + model: { + table: 'dog' + }, + field: { + dog_id: { + type: 'serial', + key: true, + required: false, + klass: 'primary', + enumerable: true, + mapsTo: 'dog_id', + name: 'dog_id' + } + }, + mergeAssocId: { + family_id: { + type: 'integer', + required: true, + klass: 'primary', + enumerable: true, + mapsTo: 'family_id', + name: 'family_id' + } + }, + mergeTable: 'dog_family', + }, + opts: { + only: ['name', 'id'], + keys: ['id'], + }, + expectedQuery: { + postgres: 'SELECT "t1"."name", "t1"."id", "t2"."dog_id" AS "$p" FROM "dog" "t1" JOIN "dog_family" "t2" ON "t2"."family_id" = "t1"."id" WHERE "t2"."dog_id" IN (1, 5)', + mysql: 'SELECT `t1`.`name`, `t1`.`id`, `t2`.`dog_id` AS `$p` FROM `dog` `t1` JOIN `dog_family` `t2` ON `t2`.`family_id` = `t1`.`id` WHERE `t2`.`dog_id` IN (1, 5)', + sqlite: 'SELECT `t1`.`name`, `t1`.`id`, `t2`.`dog_id` AS `$p` FROM `dog` `t1` JOIN `dog_family` `t2` ON `t2`.`family_id` = `t1`.`id` WHERE `t2`.`dog_id` IN (1, 5)' + } + }; + + describe('cb', function () { + it('should build correct query', function (done) { + var execSimpleQueryStub = sinon.stub(db.driver, 'execSimpleQuery') + .callsFake(function (q, cb) { + cb(); + }); + + db.driver.eagerQuery(fixture.association, fixture.opts, [1, 5], function (err, data) { + if (err) { + execSimpleQueryStub.restore(); + done(err); + } + should.equal(execSimpleQueryStub.calledOnce, true); + should.equal(execSimpleQueryStub.lastCall.args[0], fixture.expectedQuery[common.protocol()]); + execSimpleQueryStub.restore(); + done(); + }); + }); + }); - var MyModel = db.define("my_model", { // db.define should call plugin.define method - property: String + describe('async', function () { + it('should build correct query', function () { + var execSimpleQueryStub = sinon.stub(db.driver, 'execSimpleQuery') + .callsFake(function (q, cb) { + cb(); + }); + return db.driver.eagerQueryAsync(fixture.association, fixture.opts, [1, 5]) + .then(function () { + should.equal(execSimpleQueryStub.calledOnce, true); + should.equal(execSimpleQueryStub.lastCall.args[0], fixture.expectedQuery[common.protocol()]); + execSimpleQueryStub.restore(); + }) + .catch(function () { + execSimpleQueryStub.restore(); + }); + }); + }); }); - opts.calledDefine.should.be.true; - MyModel.properties.should.have.property("otherprop"); - - return done(); - }); + describe("#execQuery", function () { + it("should execute sql queries", function (done) { + db.driver.execQuery("SELECT id FROM log", function (err, data) { + should.not.exist(err); - it("should be able to register a plugin as string", function (done) { - var opts = { - option : true, - calledDefine : false - }; + should(JSON.stringify(data) === JSON.stringify([{ id: 1 }, { id: 2 }, { id: 3 }])); + done(); + }); + }); - db.use("../support/my_plugin", opts); + it("should escape sql queries", function (done) { + var query = "SELECT log.?? FROM log WHERE log.?? LIKE ? AND log.?? > ?"; + var args = ['what', 'who', 'jane', 'when', new Date('2013/04/07 12:40:00')]; + db.driver.execQuery(query, args, function (err, data) { + should.not.exist(err); - var MyModel = db.define("my_model", { // db.define should call plugin.define method - property: String + should(JSON.stringify(data) === JSON.stringify([{ "what": "user login" }])); + done(); + }); + }); }); - - opts.calledDefine.should.be.true; - - return done(); }); -}); -describe("db.define()", function() { - var db = null; + describe('DB', function () { + var db = null; + beforeEach(function (done) { + helper.connect(function (connection) { + db = connection; - before(function (done) { - helper.connect(function (connection) { - db = connection; + return done(); + }); + }); - return done(); + afterEach(function () { + return db.close(); }); - }); - after(function () { - return db.close(); - }); + describe('db.syncPromise()', function () { + it('should call sync for each model', function () { + db.define("my_model", { + property: String + }); + db.define("my_model2", { + property: String + }); + var syncStub = sinon.stub(db.models['my_model'], 'sync').callsFake(function (cb) { + cb(null, {}) + }); + var syncStub2 = sinon.stub(db.models['my_model2'], 'sync').callsFake(function (cb) { + cb(null, {}) + }); + return db.syncPromise() + .then(function () { + should.equal(syncStub.calledOnce, true); + should.equal(syncStub2.calledOnce, true); + }); + }); + }); + + describe("db.dropAsync()", function () { + it('should should call drop for each model', function () { + db.define("my_model", { + property: String + }); + + db.define("my_model2", { + property: String + }); - it("should use setting model.namePrefix as table prefix if defined", function (done) { - db.settings.set("model.namePrefix", "orm_"); + var dropStub = sinon.stub(db.models['my_model'], 'drop').callsFake(function (cb) { + cb(null, {}) + }); + var dropStub2 = sinon.stub(db.models['my_model2'], 'drop').callsFake(function (cb) { + cb(null, {}) + }); - var Person = db.define("person", { - name: String + return db.dropAsync() + .then(function () { + should.equal(dropStub.calledOnce, true); + should.equal(dropStub2.calledOnce, true); + }); + }); }); - Person.table.should.equal("orm_person"); + describe("db.use()", function () { + it("should be able to register a plugin", function (done) { + var MyPlugin = require("../support/my_plugin"); + var opts = { + option: true, + calledDefine: false + }; - return done(); - }); -}); + db.use(MyPlugin, opts); -describe("db.load()", function () { - var db = null; + db.define("my_model", { // db.define should call plugin.define method + property: String + }); - beforeEach(function (done) { - helper.connect(function (connection) { - db = connection; + opts.calledDefine.should.be.true(); - done(); - }); - }); + return done(); + }); - afterEach(function () { - db.close(); - }); + it("a plugin should be able to catch models before defining them", function (done) { + var MyPlugin = require("../support/my_plugin"); + var opts = { + option: true, + calledDefine: false, + beforeDefine: function (name, props, opts) { + props.otherprop = Number; + } + }; - it("should require a file based on relative path", function (done) { - db.load("../support/spec_load", function (err) { - should.not.exist(err); + db.use(MyPlugin, opts); - db.models.should.have.property("person"); - db.models.should.have.property("pet"); + var MyModel = db.define("my_model", { // db.define should call plugin.define method + property: String + }); - done(); - }); - }); + opts.calledDefine.should.be.true(); + MyModel.properties.should.have.property("otherprop"); - it("should be able to load more than one file", function (done) { - db.load("../support/spec_load_second", "../support/spec_load_third", function (err) { - should.not.exist(err); + done(); + }); - db.models.should.have.property("person"); - db.models.should.have.property("pet"); + it("should be able to register a plugin as string", function (done) { + var opts = { + option: true, + calledDefine: false + }; - done(); - }); - }); + db.use("../support/my_plugin", opts); - it("should be able to load more than one file passed as Array", function (done) { - db.load([ "../support/spec_load_second", "../support/spec_load_third" ], function (err) { - should.not.exist(err); + db.define("my_model", { // db.define should call plugin.define method + property: String + }); - db.models.should.have.property("person"); - db.models.should.have.property("pet"); + opts.calledDefine.should.be.true(); - done(); + return done(); + }); }); - }); -}); -describe("db.serial()", function () { - var db = null; + describe("db.define()", function () { + it("should use setting model.namePrefix as table prefix if defined", function (done) { + db.settings.set("model.namePrefix", "orm_"); - before(function (done) { - helper.connect(function (connection) { - db = connection; + var Person = db.define("person", { + name: String + }); - return done(); - }); - }); + Person.table.should.equal("orm_person"); - after(function () { - return db.close(); + return done(); + }); + }); }); - it("should be able to execute chains in serial", function (done) { - var Person = db.define("person", { - name : String, - surname : String + describe("db.loadAsync()", function () { + it("should require a file if array", function () { + var filePath = "../support/spec_load"; + return db.loadAsync([filePath]) + .then(function () { + db.models.should.have.property("person"); + db.models.should.have.property("pet"); + }); }); - helper.dropSync(Person, function () { - Person.create([ - { name : "John", surname : "Doe" }, - { name : "Jane", surname : "Doe" } - ], function () { - db.serial( - Person.find({ surname : "Doe" }), - Person.find({ name : "John" }) - ).get(function (err, DoeFamily, JohnDoe) { - should.equal(err, null); - should(Array.isArray(DoeFamily)); - should(Array.isArray(JohnDoe)); - - DoeFamily.length.should.equal(2); - JohnDoe.length.should.equal(1); + it("should require a file if single file path string", function () { + var filePath = "../support/spec_load"; + return db.loadAsync(filePath) + .then(function () { + db.models.should.have.property("person"); + db.models.should.have.property("pet"); + }); + }); - DoeFamily[0].surname.should.equal("Doe"); - DoeFamily[1].surname.should.equal("Doe"); + it("should be able to load more than one file", function () { + var filePaths = ["../support/spec_load_second", "../support/spec_load_third"]; - JohnDoe[0].name.should.equal("John"); + return db.loadAsync(filePaths) + .then(function () { + db.models.should.have.property("person"); + db.models.should.have.property("pet"); + }); + }); - return done(); + it("should throw error if files passed like arguments", function () { + return db.loadAsync("../support/spec_load_second", "../support/spec_load_third") + .then(function () { + db.models.should.have.property("person"); + db.models.should.have.property("pet"); }); - }); }); }); -}); -describe("db.driver", function () { - var db = null; + describe("db.load()", function () { + it("should require a file based on relative path", function (done) { + db.load("../support/spec_load", function (err) { + should.not.exist(err); - before(function (done) { - helper.connect(function (connection) { - db = connection; + db.models.should.have.property("person"); + db.models.should.have.property("pet"); - var Log = db.define('log', { - what : { type: 'text' }, - when : { type: 'date', time: true }, - who : { type: 'text' } + return done(); }); + }); - helper.dropSync(Log, function (err) { - if (err) return done(err); + it("should be able to load more than one file", function (done) { + db.load("../support/spec_load_second", "../support/spec_load_third", function (err) { + should.not.exist(err); - Log.create([ - { what: "password reset", when: new Date('2013/04/07 12:33:05'), who: "jane" }, - { what: "user login", when: new Date('2013/04/07 13:01:44'), who: "jane" }, - { what: "user logout", when: new Date('2013/05/12 04:09:31'), who: "john" } - ], done); + db.models.should.have.property("person"); + db.models.should.have.property("pet"); + return done(); }); }); }); - after(function () { - return db.close(); - }); + describe("db.load()", function () { + it("should be able to load more than one file", function (done) { + db.load("../support/spec_load_second", "../support/spec_load_third", function () { + db.models.should.have.property("person"); + db.models.should.have.property("pet"); - it("should be available", function () { - should.exist(db.driver); - }); + return done(); + }); + }); - if (common.protocol() == "mongodb") return; + it("should be able to load more than one file passed as Array", function (done) { + db.load(["../support/spec_load_second", "../support/spec_load_third"], function (err) { + should.not.exist(err); - describe("query", function () { - it("should be available", function () { - should.exist(db.driver.query); + db.models.should.have.property("person"); + db.models.should.have.property("pet"); + return done(); + }); }); + }); - describe("#execQuery", function () { - it("should execute sql queries", function (done) { - db.driver.execQuery("SELECT id FROM log", function (err, data) { - should.not.exist(err); + describe("db.load()", function () { + it("should be able to load more than one file passed as Array", function (done) { + db.load(["../support/spec_load_second", "../support/spec_load_third"], function () { + db.models.should.have.property("person"); + db.models.should.have.property("pet"); - should(JSON.stringify(data) == JSON.stringify([{ id: 1 }, { id: 2 }, { id: 3 }])); - done(); - }); + return done(); }); + }); + }); - it("should escape sql queries", function (done) { - var query = "SELECT log.?? FROM log WHERE log.?? LIKE ? AND log.?? > ?"; - var args = ['what', 'who', 'jane', 'when', new Date('2013/04/07 12:40:00')]; - db.driver.execQuery(query, args, function (err, data) { - should.not.exist(err); - - should(JSON.stringify(data) == JSON.stringify([{ "what": "user login" }])); - done(); + describe("db.serial()", function () { + it("should be able to execute chains in serial", function (done) { + var Person = db.define("person", { + name: String, + surname: String + }); + helper.dropSync(Person, function () { + Person.create([ + { name: "John", surname: "Doe" }, + { name: "Jane", surname: "Doe" } + ], function () { + db.serial( + Person.find({ surname: "Doe" }), + Person.find({ name: "John" }) + ).get(function (err, DoeFamily, JohnDoe) { + should.equal(err, null); + + should(Array.isArray(DoeFamily)); + should(Array.isArray(JohnDoe)); + + DoeFamily.length.should.equal(2); + JohnDoe.length.should.equal(1); + + DoeFamily[0].surname.should.equal("Doe"); + DoeFamily[1].surname.should.equal("Doe"); + + JohnDoe[0].name.should.equal("John"); + + return done(); + }); }); }); }); }); -}); + +}); \ No newline at end of file diff --git a/test/integration/hook-promise.js b/test/integration/hook-promise.js new file mode 100644 index 00000000..5969917b --- /dev/null +++ b/test/integration/hook-promise.js @@ -0,0 +1,353 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); +var async = require('async'); +var Promise = require('bluebird'); + +describe("HookPromise", function() { + var db = null; + var Person = null; + var triggeredHooks = {}; + var getTimestamp; // Calling it 'getTime' causes strangeness. + + getTimestamp = function () { return Date.now(); }; + + var checkHook = function (hook) { + triggeredHooks[hook] = false; + + return function () { + triggeredHooks[hook] = getTimestamp(); + }; + }; + + var setup = function (hooks) { + if (typeof hooks == "undefined") { + hooks = { + afterCreate : checkHook("afterCreate"), + beforeCreate : checkHook("beforeCreate"), + afterSave : checkHook("afterSave"), + beforeSave : checkHook("beforeSave"), + beforeValidation : checkHook("beforeValidation"), + beforeRemove : checkHook("beforeRemove"), + afterRemove : checkHook("afterRemove") + }; + } + + return function (done) { + Person = db.define("person", { + name : String + }, { + hooks : hooks + }); + + Person.settings.set("instance.returnAllErrors", false); + + return helper.dropSync(Person, done); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe("beforeCreate", function () { + beforeEach(setup()); + it("should allow modification of instance", function () { + Person.beforeCreate(function () { + var self = this; + return new Promise(function (resolve) { + setTimeout(function () { + self.name = "Hook Worked"; + resolve(); + }, 200); + }); + }); + + return Person.createAsync([{ }]) + .then(function (people) { + should.exist(people); + should.equal(people.length, 1); + should.equal(people[0].name, "Hook Worked"); + // garantee it was correctly saved on database + Person.one({ name: "Hook Worked" }, function (err, person) { + should.not.exist(err); + should.exist(person); + }); + }); + }); + + it("should trigger error", function () { + Person.beforeCreate(function () { + return new Promise(function (resolve, reject) { + setTimeout(function () { + reject(new Error('beforeCreate-error')); + }, 200); + }); + }); + + return Person.createAsync([{ name: "John Doe" }]) + .catch(function (err) { + err.should.be.a.Object(); + should.equal(err.message, "beforeCreate-error"); + }); + }); + }); + + describe("beforeSave", function () { + beforeEach(setup()); + it("should trigger and wait before save hook", function () { + Person.beforeSave(function () { + return new Promise(function (_, reject) { + setTimeout(function () { + reject(new Error('beforeSave-error')); + }, 400); + }); + }); + + return Person.createAsync([{ name: "Jane Doe" }]) + .catch(function (err) { + err.should.be.a.Object(); + should.equal(err.message, "beforeSave-error"); + }); + }); + + it("should trigger error", function () { + Person.beforeSave(function () { + return new Promise(function (_, reject) { + setTimeout(function () { + reject(new Error('beforeSave-error')); + }, 400); + }); + }); + + return Person.createAsync([{ name: "Jane Doe" }]) + .catch(function (err) { + err.should.be.a.Object(); + should.equal("beforeSave-error", err.message); + }); + }); + + it("should trigger and wait", function () { + Person.beforeSave(function () { + var self = this; + return new Promise(function (resolve) { + setTimeout(function () { + self.name = 'John Doe'; + resolve(); + }, 400); + }); + }); + + return Person.createAsync([{ name: "Jane Doe" }]) + .then(function (John) { + return John[0].saveAsync(); + }) + .then(function (John) { + should.equal(John.name, 'John Doe'); + }); + }); + }); + + describe("beforeValidation", function () { + beforeEach(setup()); + it("should trigger and wait", function () { + Person.beforeValidation(function () { + var self = this; + return new Promise(function (resolve) { + setTimeout(function () { + self.name = "John Snow"; + resolve(); + }, 500); + }); + }); + + return Person.createAsync([{ name: "John Doe" }]) + .then(function (Jhon) { + should.equal(Jhon[0].name, "John Snow"); + }); + }); + + it("should throw error", function () { + Person.beforeValidation(function () { + var self = this; + return new Promise(function (_, reject) { + setTimeout(function () { + self.name = "John Snow"; + reject(new Error("beforeValidation-error")); + }, 500); + }); + }); + + return Person.createAsync([{ name: "John Doe" }]) + .catch(function (err) { + should.equal(err.message, "beforeValidation-error"); + }); + }) + }); + + describe("afterLoad", function () { + beforeEach(setup()); + it("should trigger and wait", function () { + var afterLoad = false; + Person.afterLoad(function () { + return new Promise(function (resolve) { + setTimeout(function () { + afterLoad = true; + resolve(); + }, 500); + }); + }); + + return Person.createAsync([{ name: "John Doe" }]) + .then(function () { + should.equal(afterLoad, true); + }); + }); + + it("should throw error", function () { + var afterLoad = false; + Person.afterLoad(function () { + return new Promise(function (_, reject) { + setTimeout(function () { + afterLoad = true; + reject(new Error("afterLoad-error")); + }, 500); + }); + }); + + return Person.createAsync([{ name: "John Doe" }]) + .catch(function (err) { + err.should.exist; + should.equal("afterLoad-error", err.message); + }); + }); + }); + + describe("afterAutoFetch", function () { + beforeEach(setup()); + it("should trigger and wait", function () { + var afterAutoFetch = false; + Person.afterAutoFetch(function () { + return new Promise(function (resolve) { + setTimeout(function () { + afterAutoFetch = true; + resolve(); + }, 1000); + }); + }); + + return Person.createAsync({ name: "John" }) + .then(function () { + should.equal(afterAutoFetch, true); + }); + }); + + it("should throw error", function () { + var afterAutoFetch = false; + Person.afterAutoFetch(function () { + return new Promise(function (_, reject) { + setTimeout(function () { + afterAutoFetch = true; + reject(new Error("afterAutoFetch-error")); + }, 500); + }); + }); + + return Person.createAsync({ name: "John" }) + .catch(function (err) { + should.equal(err.message, "afterAutoFetch-error"); + }); + }); + }); + + describe("beforeRemove", function () { + before(setup()); + + it("should trigger and wait", function () { + var beforeRemove = false; + Person.beforeRemove(function () { + return new Promise(function (resolve) { + beforeRemove = true; + resolve(); + }); + }); + + return Person.createAsync([{ name: "John Doe" }]) + .then(function (items) { + return items[0].removeAsync(); + }) + .then(function () { + should.equal(beforeRemove, true); + }); + }); + + it("should throw error", function () { + Person.beforeRemove(function () { + return new Promise(function (_, reject) { + setTimeout(function () { + reject(new Error('beforeRemove-error')); + }, 600); + }); + }); + + return Person.createAsync([{ name: "John Doe" }]) + .then(function (items) { + return items[0].removeAsync(); + }) + .catch(function (err) { + should.equal(err.message, 'beforeRemove-error'); + }); + }); + }); + + describe("instance modifications", function () { + before(setup({ + beforeValidation: function () { + var self = this; + return new Promise(function (resolve) { + setTimeout(function () { + should.equal(self.name, "John Doe"); + self.name = "beforeValidation"; + resolve(); + }, 800); + }); + }, + beforeCreate: function () { + var self = this; + return new Promise(function (resolve) { + setTimeout(function () { + should.equal(self.name, "beforeValidation"); + self.name = "beforeCreate"; + resolve(); + }, 700); + }); + }, + beforeSave: function () { + var self = this; + return new Promise(function (resolve) { + setTimeout(function () { + should.equal(self.name, "beforeCreate"); + self.name = "beforeSave"; + resolve(); + }, 500); + }); + } + })); + + it("should propagate down hooks", function () { + return Person.createAsync([{ name: "John Doe" }]) + .then(function (people) { + should.exist(people); + should.equal(people.length, 1); + should.equal(people[0].name, "beforeSave"); + }); + }); + }); +}); diff --git a/test/integration/hook.js b/test/integration/hook.js index 596dce3f..77dfe346 100644 --- a/test/integration/hook.js +++ b/test/integration/hook.js @@ -1,16 +1,16 @@ -var _ = require('lodash'); -var should = require('should'); -var helper = require('../support/spec_helper'); -var async = require('async'); -var ORM = require('../../'); +var should = require('should'); +var helper = require('../support/spec_helper'); +var async = require('async'); -describe("Hook", function() { +describe("Hook", function () { var db = null; var Person = null; var triggeredHooks = {}; var getTimestamp; // Calling it 'getTime' causes strangeness. - getTimestamp = function () { return Date.now(); }; + getTimestamp = function () { + return Date.now(); + }; // this next lines are failing... // if (process.hrtime) { // getTimestamp = function () { return parseFloat(process.hrtime().join('.')); }; @@ -29,21 +29,21 @@ describe("Hook", function() { var setup = function (hooks) { if (typeof hooks == "undefined") { hooks = { - afterCreate : checkHook("afterCreate"), - beforeCreate : checkHook("beforeCreate"), - afterSave : checkHook("afterSave"), - beforeSave : checkHook("beforeSave"), - beforeValidation : checkHook("beforeValidation"), - beforeRemove : checkHook("beforeRemove"), - afterRemove : checkHook("afterRemove") + afterCreate: checkHook("afterCreate"), + beforeCreate: checkHook("beforeCreate"), + afterSave: checkHook("afterSave"), + beforeSave: checkHook("beforeSave"), + beforeValidation: checkHook("beforeValidation"), + beforeRemove: checkHook("beforeRemove"), + afterRemove: checkHook("afterRemove") }; } return function (done) { Person = db.define("person", { - name : String + name: String }, { - hooks : hooks + hooks: hooks }); Person.settings.set("instance.returnAllErrors", false); @@ -65,7 +65,7 @@ describe("Hook", function() { }); describe("after Model creation", function () { - before(setup({})); + before(setup()); it("can be changed", function (done) { var triggered = false; @@ -73,9 +73,9 @@ describe("Hook", function() { Person.afterCreate(function () { triggered = true; }); + Person.create([{ name: "John Doe" }], function () { triggered.should.be.true; - return done(); }); }); @@ -116,36 +116,36 @@ describe("Hook", function() { }); it("should allow modification of instance", function (done) { - Person.beforeCreate(function (next) { - this.name = "Hook Worked"; - next(); - }); + Person.beforeCreate(function (next) { + this.name = "Hook Worked"; + next(); + }); - Person.create([{ }], function (err, people) { - should.not.exist(err); - should.exist(people); - should.equal(people.length, 1); - should.equal(people[0].name, "Hook Worked"); + Person.create([{}], function (err, people) { + should.not.exist(err); + should.exist(people); + should.equal(people.length, 1); + should.equal(people[0].name, "Hook Worked"); - // garantee it was correctly saved on database - Person.one({ name: "Hook Worked" }, function (err, person) { - should.not.exist(err); - should.exist(person); + // garantee it was correctly saved on database + Person.one({ name: "Hook Worked" }, function (err, person) { + should.not.exist(err); + should.exist(person); - return done(); - }); + return done(); }); + }); }); describe("when setting properties", function () { before(setup({ - beforeCreate : function () { + beforeCreate: function () { this.name = "Jane Doe"; } })); it("should not be discarded", function (done) { - Person.create([{ }], function (err, items) { + Person.create([{}], function (err, items) { should.equal(err, null); items.should.be.a.Object(); @@ -167,7 +167,7 @@ describe("Hook", function() { var beforeCreate = false; before(setup({ - beforeCreate : function (next) { + beforeCreate: function (next) { setTimeout(function () { beforeCreate = true; @@ -186,7 +186,7 @@ describe("Hook", function() { describe("if hook triggers error", function () { before(setup({ - beforeCreate : function (next) { + beforeCreate: function (next) { setTimeout(function () { return next(new Error('beforeCreate-error')); }, 200); @@ -233,15 +233,15 @@ describe("Hook", function() { }); it("should allow modification of instance", function (done) { - Person.beforeSave(function () { - this.name = "Hook Worked"; - }); + Person.beforeSave(function () { + this.name = "Hook Worked"; + }); - Person.create([{ name: "John Doe" }], function (err, people) { - should.not.exist(err); - should.exist(people); - should.equal(people.length, 1); - should.equal(people[0].name, "Hook Worked"); + Person.create([{ name: "John Doe" }], function (err, people) { + should.not.exist(err); + should.exist(people); + should.equal(people.length, 1); + should.equal(people[0].name, "Hook Worked"); // garantee it was correctly saved on database Person.find({ name: "Hook Worked" }, { identityCache: false }, 1, function (err, people) { @@ -250,18 +250,18 @@ describe("Hook", function() { return done(); }); - }); + }); }); describe("when setting properties", function () { before(setup({ - beforeSave : function () { + beforeSave: function () { this.name = "Jane Doe"; } })); it("should not be discarded", function (done) { - Person.create([{ }], function (err, items) { + Person.create([{}], function (err, items) { should.equal(err, null); items.should.be.a.Object(); @@ -283,7 +283,7 @@ describe("Hook", function() { var beforeSave = false; before(setup({ - beforeSave : function (next) { + beforeSave: function (next) { setTimeout(function () { beforeSave = true; @@ -303,7 +303,7 @@ describe("Hook", function() { describe("if hook triggers error", function () { before(setup({ - beforeSave : function (next) { + beforeSave: function (next) { if (this.name == "John Doe") { return next(); } @@ -400,24 +400,24 @@ describe("Hook", function() { it("should allow modification of instance", function (done) { - Person.beforeValidation(function () { - this.name = "Hook Worked"; - }); + Person.beforeValidation(function () { + this.name = "Hook Worked"; + }); - Person.create([{ name: "John Doe" }], function (err, people) { - should.not.exist(err); - should.exist(people); - should.equal(people.length, 1); - should.equal(people[0].name, "Hook Worked"); - done(); - }); + Person.create([{ name: "John Doe" }], function (err, people) { + should.not.exist(err); + should.exist(people); + should.equal(people.length, 1); + should.equal(people[0].name, "Hook Worked"); + done(); + }); }); describe("if hook method has 1 argument", function () { var beforeValidation = false; before(setup({ - beforeValidation : function (next) { + beforeValidation: function (next) { setTimeout(function () { beforeValidation = true; @@ -483,7 +483,7 @@ describe("Hook", function() { var afterLoad = false; before(setup({ - afterLoad : function (next) { + afterLoad: function (next) { setTimeout(function () { afterLoad = true; @@ -502,7 +502,7 @@ describe("Hook", function() { describe("if hook returns an error", function () { before(setup({ - afterLoad : function (next) { + afterLoad: function (next) { return next(new Error("AFTERLOAD_FAIL")); } })); @@ -540,7 +540,7 @@ describe("Hook", function() { var afterAutoFetch = false; before(setup({ - afterAutoFetch : function (next) { + afterAutoFetch: function (next) { setTimeout(function () { afterAutoFetch = true; @@ -559,7 +559,7 @@ describe("Hook", function() { describe("if hook returns an error", function () { before(setup({ - afterAutoFetch : function (next) { + afterAutoFetch: function (next) { return next(new Error("AFTERAUTOFETCH_FAIL")); } })); @@ -595,7 +595,7 @@ describe("Hook", function() { var beforeRemove = false; before(setup({ - beforeRemove : function (next) { + beforeRemove: function (next) { setTimeout(function () { beforeRemove = true; @@ -617,7 +617,7 @@ describe("Hook", function() { describe("if hook triggers error", function () { before(setup({ - beforeRemove : function (next) { + beforeRemove: function (next) { setTimeout(function () { return next(new Error('beforeRemove-error')); }, 200); @@ -657,12 +657,12 @@ describe("Hook", function() { describe("if model has autoSave", function () { before(function (done) { Person = db.define("person", { - name : String, - surname : String + name: String, + surname: String }, { - autoSave : true, - hooks : { - afterSave : checkHook("afterSave") + autoSave: true, + hooks: { + afterSave: checkHook("afterSave") } }); @@ -672,7 +672,7 @@ describe("Hook", function() { }); it("should trigger for single property changes", function (done) { - Person.create({ name : "John", surname : "Doe" }, function (err, John) { + Person.create({ name: "John", surname: "Doe" }, function (err, John) { should.equal(err, null); triggeredHooks.afterSave.should.be.a.Number(); @@ -690,29 +690,29 @@ describe("Hook", function() { }); describe("instance modifications", function () { - before(setup({ - beforeValidation: function () { - should.equal(this.name, "John Doe"); - this.name = "beforeValidation"; - }, - beforeCreate: function () { - should.equal(this.name, "beforeValidation"); - this.name = "beforeCreate"; - }, - beforeSave: function () { - should.equal(this.name, "beforeCreate"); - this.name = "beforeSave"; - } - })); + before(setup({ + beforeValidation: function () { + should.equal(this.name, "John Doe"); + this.name = "beforeValidation"; + }, + beforeCreate: function () { + should.equal(this.name, "beforeValidation"); + this.name = "beforeCreate"; + }, + beforeSave: function () { + should.equal(this.name, "beforeCreate"); + this.name = "beforeSave"; + } + })); - it("should propagate down hooks", function (done) { - Person.create([{ name: "John Doe" }], function (err, people) { - should.not.exist(err); - should.exist(people); - should.equal(people.length, 1); - should.equal(people[0].name, "beforeSave"); - done(); - }); + it("should propagate down hooks", function (done) { + Person.create([{ name: "John Doe" }], function (err, people) { + should.not.exist(err); + should.exist(people); + should.equal(people.length, 1); + should.equal(people[0].name, "beforeSave"); + done(); }); + }); }); }); diff --git a/test/integration/instance.js b/test/integration/instance.js index ae9cacf1..91e6f9c4 100644 --- a/test/integration/instance.js +++ b/test/integration/instance.js @@ -96,6 +96,13 @@ describe("Model instance", function() { }); }); }); + + it("should have a saving state to avoid loops (promise-based)", function () { + return main_item.find({ name : "new name" }).firstAsync() + .then(function (mainItem) { + return mainItem.saveAsync({ name : "new name test" }); + }); + }); }); describe("#isInstance", function () { @@ -336,6 +343,25 @@ describe("Model instance", function() { return done(); }); }); + + it("should return validation errors if invalid (promise-based)", function () { + var person = new Person({ age: -1 }); + + return person.validateAsync() + .then(function (validationErrors) { + should.equal(Array.isArray(validationErrors), true); + }); + }); + + it("should return false if valid (promise-based)", function () { + var person = new Person({ name: 'Janette' }); + + return person.validateAsync() + .then(function (validationErrors) { + should.equal(validationErrors, false); + + }); + }); }); describe("properties", function () { @@ -363,6 +389,24 @@ describe("Model instance", function() { }); }); + it("should be saved for valid numbers, using both save & create (promise-based)", function () { + var person1 = new Person({ height: 190 }); + + return person1.saveAsync().then(function () { + return Person.createAsync({ height: 170 }) + .then(function (person2) { + return [person2, Person.getAsync(person1[Person.id])]; + }) + .spread(function (person2, item) { + should.equal(item.height, 190); + return Person.getAsync(person2[Person.id]); + }) + .then(function (item) { + should.equal(item.height, 170); + }); + }); + }); + if (protocol == 'postgres') { // Only postgres raises propper errors. // Sqlite & Mysql fail silently and insert nulls. @@ -415,6 +459,52 @@ describe("Model instance", function() { }); }); }); + + it("should raise an error for NaN integers (promise-based)", function () { + var person = new Person({ height: NaN }); + + return person.saveAsync() + .catch(function(err) { + var msg = { + postgres : 'invalid input syntax for integer: "NaN"' + }[protocol]; + + should.equal(err.message, msg); + }); + }); + + it("should raise an error for Infinity integers (promise-based)", function () { + var person = new Person({ height: Infinity }); + + return person.saveAsync() + .catch(function (err) { + should.exist(err); + var msg = { + postgres : 'invalid input syntax for integer: "Infinity"' + }[protocol]; + + should.equal(err.message, msg); + }); + }); + + it("should raise an error for nonsensical integers, for both save & create (promise-based)", function () { + var person = new Person({ height: 'bugz' }); + var msg = { + postgres : 'invalid input syntax for integer: "bugz"' + }[protocol]; + + return person.saveAsync() + .catch(function (err) { + should.exist(err); + + should.equal(err.message, msg); + return Person.createAsync({ height: 'bugz' }); + }) + .catch(function(err) { + should.exist(err); + should.equal(err.message, msg); + }); + }); } if (protocol != 'mysql') { @@ -442,6 +532,26 @@ describe("Model instance", function() { }); }); }); + + it("should store NaN & Infinite floats (promise-based)", function () { + var person = new Person({ weight: NaN }); + + return person.saveAsync() + .then(function () { + return Person.getAsync(person[Person.id]); + }) + .then(function (person) { + should(isNaN(person.weight)); + + return person.saveAsync({ weight: Infinity, name: 'black hole' }); + }) + .then(function () { + return Person.getAsync(person[Person.id]); + }) + .then(function (person) { + should.strictEqual(person.weight, Infinity); + }); + }); } }); @@ -460,5 +570,55 @@ describe("Model instance", function() { }); }); }); + + describe("#removeAsync", function () { + var main_item, item; + + before(function (done) { + main_item = db.define("main_item", { + name : String + }, { + auteFetch : true + }); + item = db.define("item", { + name : String + }, { + identityCache : false + }); + item.hasOne("main_item", main_item, { + reverse : "items", + autoFetch : true + }); + + return helper.dropSync([ main_item, item ], function () { + main_item.create({ + name : "Main Item" + }, function (err, mainItem) { + item.create({ + name : "Item" + }, function (err, Item) { + mainItem.setItems(Item, function (err) { + should.not.exist(err); + + return done(); + }); + }); + }); + }); + }); + + it("should delete an item and send an error", function () { + return main_item.find({ name : "Main Item" }).firstAsync() + .then(function (mainItem) { + return mainItem.removeAsync(); + }) + .then(function () { + return main_item.find({ name : "Main Item" }).firstAsync(); + }) + .then(function (item) { + should.equal(item, null) + }); + }); + }); }); }); diff --git a/test/integration/model-aggregate.js b/test/integration/model-aggregate.js index 2f45d912..2302e52a 100644 --- a/test/integration/model-aggregate.js +++ b/test/integration/model-aggregate.js @@ -55,6 +55,24 @@ describe("Model.aggregate()", function() { }); }); + + + describe("with multiple methods using getAsync", function () { + before(setup()); + + it("should return value for everyone of them with getAsync", function (done) { + Person.aggregate().count('id').min('id').max('id').getAsync() + .then(function(results) { + + results[0].should.equal(3); + results[1].should.equal(1); + results[2].should.equal(3); + + return done(); + }); + }); + }); + describe("with call()", function () { before(setup()); @@ -95,6 +113,52 @@ describe("Model.aggregate()", function() { }); }); + describe("with call() (using getAsync)", function () { + before(setup()); + + it("should accept a function", function (done) { + Person.aggregate().call('COUNT').getAsync() + .then(function (results) { + var count = results[0]; + count.should.equal(3); + return done(); + }) + .catch(function(err) { + done(err); + }) + }); + + it("should accept arguments to the function as an Array", function (done) { + Person.aggregate().call('COUNT', [ 'id' ]).getAsync() + .then(function (results) { + var count = results[0]; + count.should.equal(3); + return done(); + }) + .catch(function(err) { + done(err); + }); + }); + + describe("if function is DISTINCT", function () { + it("should work as calling .distinct() directly", function (done) { + Person.aggregate().call('DISTINCT', [ 'name' ]).as('name').order('name').getAsync() + .then(function (results) { + var rows = results[0]; + should(Array.isArray(rows)); + rows.length.should.equal(2); + + rows[0].should.equal('Jane Doe'); + rows[1].should.equal('John Doe'); + + return done(); + }).catch(function(err) { + done(err); + }); + }); + }); + }); + describe("with as() without previous aggregates", function () { before(setup()); @@ -149,6 +213,46 @@ describe("Model.aggregate()", function() { }); }); + describe("with select() with arguments (using getAsync)", function () { + before(setup()); + + it("should use them as properties if 1st argument is Array", function (done) { + Person.aggregate().select([ 'id' ]).count('id').groupBy('id').getAsync() + .then(function (results) { + var people = results[0]; + should(Array.isArray(people)); + people.length.should.be.above(0); + + people[0].should.be.a.Object(); + people[0].should.have.property("id"); + people[0].should.not.have.property("name"); + + return done(); + }) + .catch(function(err) { + done(err); + }); + }); + + it("should use them as properties", function (done) { + Person.aggregate().select('id').count().groupBy('id').getAsync() + .then(function (results) { + var people = results[0]; + should(Array.isArray(people)); + people.length.should.be.above(0); + + people[0].should.be.a.Object(); + people[0].should.have.property("id"); + people[0].should.not.have.property("name"); + + return done(); + }) + .catch(function(err) { + done(err); + }); + }); + }); + describe("with get() without callback", function () { before(setup()); @@ -171,6 +275,17 @@ describe("Model.aggregate()", function() { }); }); + describe("with getAsync() without aggregates", function () { + before(setup()); + + it("should reject", function (done) { + Person.aggregate().getAsync().catch(function(err) { + should.ok(err); + done(); + }); + }); + }); + describe("with distinct()", function () { before(setup()); @@ -214,6 +329,58 @@ describe("Model.aggregate()", function() { }); }); + describe("with distinct() (using getAsync)", function () { + before(setup()); + + it("should return a list of distinct properties", function (done) { + Person.aggregate().distinct('name').getAsync() + .then(function (results) { + var names = results[0]; + names.should.be.a.Object(); + names.should.have.property("length", 2); + + return done(); + }) + .catch(function(err) { + done(err); + }); + }); + + describe("with limit(1)", function () { + it("should return only one value", function (done) { + Person.aggregate().distinct('name').limit(1).order("name").getAsync() + .then(function (results) { + var names = results[0]; + names.should.be.a.Object(); + names.should.have.property("length", 1); + names[0].should.equal("Jane Doe"); + + return done(); + }) + .catch(function(err) { + done(err); + }); + }); + }); + + describe("with limit(1, 1)", function () { + it("should return only one value", function (done) { + Person.aggregate().distinct('name').limit(1, 1).order("name").getAsync() + .then(function (results) { + var names = results[0]; + names.should.be.a.Object(); + names.should.have.property("length", 1); + names[0].should.equal("John Doe"); + + return done(); + }) + .catch(function(err) { + done(err); + }); + }); + }); + }); + describe("with groupBy()", function () { before(setup()); @@ -249,6 +416,47 @@ describe("Model.aggregate()", function() { }); }); + describe("with groupBy() (using getAsync)", function () { + before(setup()); + + it("should return items grouped by property", function (done) { + Person.aggregate().count().groupBy('name').getAsync() + .then(function (results) { + var rows = results[0]; + rows.should.be.a.Object(); + rows.should.have.property("length", 2); + + (rows[0].count + rows[1].count).should.equal(3); // 1 + 2 + + return done(); + }) + .catch(function(err) { + done(err); + }); + }); + + describe("with order()", function () { + before(setup()); + + it("should order items", function (done) { + Person.aggregate().count().groupBy('name').order('-count').getAsync() + .then(function (results) { + var rows = results[0]; + rows.should.be.a.Object(); + rows.should.have.property("length", 2); + + rows[0].count.should.equal(2); + rows[1].count.should.equal(1); + + return done(); + }) + .catch(function(err) { + done(err); + }); + }); + }); + }); + describe("using as()", function () { before(setup()); @@ -274,4 +482,27 @@ describe("Model.aggregate()", function() { return done(); }); }); + + describe("using as() (with getAsync)", function () { + before(setup()); + + it("should use as an alias", function (done) { + Person.aggregate().count().as('total').groupBy('name').getAsync() + .then(function (results) { + var people = results[0]; + should(Array.isArray(people)); + people.length.should.be.above(0); + + people[0].should.be.a.Object(); + people[0].should.have.property("total"); + + return done(); + }) + .catch(function (err) { + done(err); + }); + }); + + }); + }); diff --git a/test/integration/model-clearAsync.js b/test/integration/model-clearAsync.js new file mode 100644 index 00000000..c82aa057 --- /dev/null +++ b/test/integration/model-clearAsync.js @@ -0,0 +1,62 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); +var ORM = require('../../'); + +describe("Model.clearAsync()", function() { + var db = null; + var Person = null; + + var setup = function () { + return function (done) { + Person = db.define("person", { + name : String + }); + + ORM.singleton.clear(); + + return helper.dropSync(Person, function () { + Person.create([{ + name: "John Doe" + }, { + name: "Jane Doe" + }], done); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe("with callback", function () { + before(setup()); + + it("should call when done", function () { + return Person.clearAsync() + .then(Person.countAsync) + .then(function (count) { + should.equal(count, 0); + }); + }); + }); + + describe("without callback", function () { + before(setup()); + + it("should still remove", function () { + return Person.clearAsync() + .then(Person.countAsync) + .then(function (count) { + should.equal(count, 0); + }); + }); + }); +}); diff --git a/test/integration/model-countAsync.js b/test/integration/model-countAsync.js new file mode 100644 index 00000000..a9708ab0 --- /dev/null +++ b/test/integration/model-countAsync.js @@ -0,0 +1,62 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); + +describe("Model.countAsync()", function() { + var db = null; + var Person = null; + + var setup = function () { + return function (done) { + Person = db.define("person", { + name : String + }); + + return helper.dropSync(Person, function () { + Person.create([{ + id : 1, + name: "John Doe" + }, { + id : 2, + name: "Jane Doe" + }, { + id : 3, + name: "John Doe" + }], done); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe("without conditions", function () { + before(setup()); + + it("should return all items in model", function () { + return Person.countAsync() + .then(function (count) { + should.equal(count, 3); + }); + }); + }); + + describe("with conditions", function () { + before(setup()); + + it("should return only matching items", function () { + return Person.countAsync({ name: "John Doe" }) + .then(function (count) { + should.equal(count, 2); + }); + }); + }); +}); diff --git a/test/integration/model-create.js b/test/integration/model-create.js index 4e2b7c37..9217c957 100644 --- a/test/integration/model-create.js +++ b/test/integration/model-create.js @@ -1,6 +1,5 @@ var should = require('should'); var helper = require('../support/spec_helper'); -var ORM = require('../../'); describe("Model.create()", function() { var db = null; diff --git a/test/integration/model-createAsync.js b/test/integration/model-createAsync.js new file mode 100644 index 00000000..cb95d53c --- /dev/null +++ b/test/integration/model-createAsync.js @@ -0,0 +1,113 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); + +describe("Model.createAsync()", function() { + var db = null; + var Pet = null; + var Person = null; + + var setup = function () { + return function (done) { + Person = db.define("person", { + name : String + }); + Pet = db.define("pet", { + name : { type: "text", defaultValue: "Mutt" } + }); + Person.hasMany("pets", Pet); + + return helper.dropSync([ Person, Pet ], done); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe("if passing an object", function () { + before(setup()); + + it("should accept it as the only item to create", function () { + return Person.createAsync({ + name : "John Doe" + }) + .then(function (John) { + John.should.have.property("name", "John Doe"); + }) + }); + }); + + describe("if passing an array", function () { + before(setup()); + + it("should accept it as a list of items to create", function () { + return Person.createAsync([{ + name : "John Doe" + }, { + name : "Jane Doe" + }]) + .then(function (people) { + should(Array.isArray(people)); + + people.should.have.property("length", 2); + people[0].should.have.property("name", "John Doe"); + people[1].should.have.property("name", "Jane Doe"); + }) + }); + }); + + describe("if element has an association", function () { + before(setup()); + + it("should also create it or save it", function () { + return Person.createAsync({ + name : "John Doe", + pets : [ new Pet({ name: "Deco" }) ] + }) + .then(function (John) { + John.should.have.property("name", "John Doe"); + + should(Array.isArray(John.pets)); + + John.pets[0].should.have.property("name", "Deco"); + John.pets[0].should.have.property(Pet.id); + should.equal(John.pets[0].saved(), true); + }); + }); + + it("should also create it or save it even if it's an object and not an instance", function () { + return Person.createAsync({ + name : "John Doe", + pets : [ { name: "Deco" } ] + }) + .then(function (John) { + John.should.have.property("name", "John Doe"); + + should(Array.isArray(John.pets)); + + John.pets[0].should.have.property("name", "Deco"); + John.pets[0].should.have.property(Pet.id); + should.equal(John.pets[0].saved(), true); + }); + }); + }); + + describe("when not passing a property", function () { + before(setup()); + + it("should use defaultValue if defined", function () { + return Pet.createAsync({}) + .then(function (Mutt) { + Mutt.should.have.property("name", "Mutt"); + }); + }); + }); +}); diff --git a/test/integration/model-existsAsync.js b/test/integration/model-existsAsync.js new file mode 100644 index 00000000..b01f6222 --- /dev/null +++ b/test/integration/model-existsAsync.js @@ -0,0 +1,104 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); + +describe("Model.existsAsync()", function() { + var db = null; + var Person = null; + var good_id, bad_id; + + var setup = function () { + return function (done) { + Person = db.define("person", { + name : String + }); + + return helper.dropSync(Person, function () { + Person.create([{ + name: "Jeremy Doe" + }, { + name: "John Doe" + }, { + name: "Jane Doe" + }], function (err, people) { + good_id = people[0][Person.id]; + + if (typeof good_id == "number") { + // numeric ID + bad_id = good_id * 100; + } else { + // string ID, keep same length.. + bad_id = good_id.split('').reverse().join(''); + } + + return done(); + }); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe("with an id", function () { + before(setup()); + + it("should return true if found", function () { + return Person.existsAsync(good_id) + .then(function (exists) { + exists.should.be.true(); + }); + }); + + it("should return false if not found", function () { + return Person.existsAsync(bad_id) + .then(function (exists) { + exists.should.be.false(); + }); + }); + }); + + describe("with a list of ids", function () { + before(setup()); + + it("should return true if found", function () { + return Person.existsAsync([ good_id ]) + .then(function (exists) { + exists.should.be.true(); + }); + }); + + it("should return false if not found", function () { + return Person.existsAsync([ bad_id ]) + .then(function (exists) { + exists.should.be.false(); + }); + }); + }); + + describe("with a conditions object", function () { + before(setup()); + + it("should return true if found", function () { + return Person.existsAsync({ name: "John Doe" }) + .then(function (exists) { + exists.should.be.true(); + }); + }); + + it("should return false if not found", function () { + return Person.existsAsync({ name: "Jack Doe" }) + .then(function (exists) { + exists.should.be.false(); + }); + }); + }); +}); diff --git a/test/integration/model-find-chain-async.js b/test/integration/model-find-chain-async.js new file mode 100644 index 00000000..6c6d5b53 --- /dev/null +++ b/test/integration/model-find-chain-async.js @@ -0,0 +1,185 @@ +var async = require('async'); +var should = require('should'); +var helper = require('../support/spec_helper'); +var ORM = require('../../'); +var common = require('../common'); + +describe("Model.find() chaining", function() { + var db = null; + var Person = null; + var Dog = null; + + var setup = function (extraOpts) { + if (!extraOpts) extraOpts = {}; + + return function (done) { + Person = db.define("person", { + name : String, + surname : String, + age : Number + }, extraOpts); + Person.hasMany("parents"); + Person.hasOne("friend"); + + ORM.singleton.clear(); // clear identityCache cache + + return helper.dropSync(Person, function () { + Person.create([{ + name : "John", + surname : "Doe", + age : 18, + friend_id : 1 + }, { + name : "Jane", + surname : "Doe", + age : 20, + friend_id : 1 + }, { + name : "Jane", + surname : "Dean", + age : 18, + friend_id : 1 + }], done); + }); + }; + }; + + var setup2 = function () { + return function (done) { + Dog = db.define("dog", { + name: String, + }); + Dog.hasMany("friends"); + Dog.hasMany("family"); + + ORM.singleton.clear(); // clear identityCache cache + + return helper.dropSync(Dog, function () { + Dog.create([{ + name : "Fido", + friends : [{ name: "Gunner" }, { name: "Chainsaw" }], + family : [{ name: "Chester" }] + }, { + name : "Thumper", + friends : [{ name: "Bambi" }], + family : [{ name: "Princess" }, { name: "Butch" }] + }], done); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe(".firstAsync()", function () { + before(setup()); + + it("should return only the first element", function () { + return Person.find() + .order("-age") + .firstAsync() + .then(function (JaneDoe) { + JaneDoe.name.should.equal("Jane"); + JaneDoe.surname.should.equal("Doe"); + JaneDoe.age.should.equal(20); + }); + }); + + it("should return null if not found", function () { + return Person.find({ name: "Jack" }) + .firstAsync() + .then(function (Jack) { + should.equal(Jack, null); + }); + }); + }); + + describe(".lastAsync()", function () { + before(setup()); + + it("should return only the last element", function () { + return Person.find() + .order("age") + .lastAsync() + .then(function (JaneDoe) { + JaneDoe.name.should.equal("Jane"); + JaneDoe.surname.should.equal("Doe"); + JaneDoe.age.should.equal(20); + }); + }); + + it("should return null if not found", function () { + return Person.find({ name: "Jack" }) + .lastAsync() + .then(function (Jack) { + should.equal(Jack, null); + }); + }); + }); + + describe(".findAsync()", function () { + before(setup()); + + it("should not change find if no arguments", function () { + return Person.find() + .findAsync() + .then(function(person) { + should.equal(person.length, 3); + }); + }); + + it("should restrict conditions if passed", function () { + return Person.find() + .findAsync({ age: 18 }) + .then(function(person) { + should.equal(person.length, 2); + }); + }); + + if (common.protocol() == "mongodb") return; + + }); + + describe(".removeAsync()", function () { + var hookFired = false; + + before(setup({ + hooks: { + beforeRemove: function () { + hookFired = true; + } + } + })); + + it("should have no problems if no results found", function () { + return Person.find({ age: 22 }) + .removeAsync() + .then(function () { + return Person.find().findAsync(); + }).then(function(person) { + should.equal(person.length, 3); + }); + }); + + it("should remove results without calling hooks", function () { + return Person.find({ age: 20 }) + .removeAsync() + .then(function () { + should.equal(hookFired, false); + return Person.find().findAsync(); + }) + .then(function(person) { + should.equal(person.length, 2); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/integration/model-find-chain.js b/test/integration/model-find-chain.js index 438d1d3a..75d0f321 100644 --- a/test/integration/model-find-chain.js +++ b/test/integration/model-find-chain.js @@ -91,6 +91,15 @@ describe("Model.find() chaining", function() { return done(); }); }); + it("should limit results to N items (promise-based)", function (done) { + Person.find().limit(2).runAsync().then(function (instances) { + instances.should.have.property("length", 2); + + done(); + }).catch(function(err) { + done(err); + }); + }); }); describe(".skip(N)", function () { @@ -105,6 +114,16 @@ describe("Model.find() chaining", function() { return done(); }); }); + it("should skip the first N results (promise-based)", function (done) { + Person.find().skip(2).order("age").runAsync().then(function (instances) { + instances.should.have.property("length", 1); + instances[0].age.should.equal(20); + + done(); + }).catch(function(err) { + done(err); + }); + }); }); describe(".offset(N)", function () { @@ -119,6 +138,16 @@ describe("Model.find() chaining", function() { return done(); }); }); + it("should skip the first N results (promise-based)", function (done) { + Person.find().offset(2).order("age").runAsync().then(function (instances) { + instances.should.have.property("length", 1); + instances[0].age.should.equal(20); + + done(); + }).catch(function(err) { + done(err); + }); + }); }); describe("order", function () { @@ -135,6 +164,18 @@ describe("Model.find() chaining", function() { }); }); + it("('property') should order by that property ascending (promise-based)", function (done) { + Person.find().order("age").runAsync().then(function (instances) { + instances.should.have.property("length", 3); + instances[0].age.should.equal(18); + instances[2].age.should.equal(20); + + done(); + }).catch(function(err) { + done(err); + }); + }); + it("('-property') should order by that property descending", function (done) { Person.find().order("-age").run(function (err, instances) { should.equal(err, null); @@ -146,6 +187,18 @@ describe("Model.find() chaining", function() { }); }); + it("('-property') should order by that property descending (promise-based)", function (done) { + Person.find().order("-age").runAsync().then(function (instances) { + instances.should.have.property("length", 3); + instances[0].age.should.equal(20); + instances[2].age.should.equal(18); + + done(); + }).catch(function(err) { + done(err); + }); + }); + it("('property', 'Z') should order by that property descending", function (done) { Person.find().order("age", "Z").run(function (err, instances) { should.equal(err, null); @@ -156,6 +209,18 @@ describe("Model.find() chaining", function() { return done(); }); }); + + it("('property', 'Z') should order by that property descending (promise-based)", function (done) { + Person.find().order("age", "Z").runAsync().then(function (instances) { + instances.should.have.property("length", 3); + instances[0].age.should.equal(20); + instances[2].age.should.equal(18); + + done(); + }).catch(function(err) { + done(err); + }); + }); }); describe("orderRaw", function () { @@ -174,6 +239,18 @@ describe("Model.find() chaining", function() { }); }); + it("should allow ordering by SQL (promise-based)", function (done) { + Person.find().orderRaw("age DESC").runAsync().then(function (instances) { + instances.should.have.property("length", 3); + instances[0].age.should.equal(20); + instances[2].age.should.equal(18); + + done(); + }).catch(function(err) { + done(err); + }); + }); + it("should allow ordering by SQL with escaping", function (done) { Person.find().orderRaw("?? DESC", ['age']).run(function (err, instances) { should.equal(err, null); @@ -184,6 +261,18 @@ describe("Model.find() chaining", function() { return done(); }); }); + + it("should allow ordering by SQL with escaping (promise-based)", function (done) { + Person.find().orderRaw("?? DESC", ['age']).runAsync().then(function (instances) { + instances.should.have.property("length", 3); + instances[0].age.should.equal(20); + instances[2].age.should.equal(18); + + done(); + }).catch(function(err) { + done(err); + }); + }); }); describe("only", function () { @@ -201,8 +290,21 @@ describe("Model.find() chaining", function() { }); }); + it("('property', ...) should return only those properties, others null (promise-based)", function (done) { + Person.find().only("age", "surname").order("-age").runAsync().then(function (instances) { + instances.should.have.property("length", 3); + instances[0].should.have.property("age"); + instances[0].should.have.property("surname", "Doe"); + instances[0].should.have.property("name", null); + + done(); + }).catch(function(err){ + done(err); + }); + }); + // This works if cache is disabled. I suspect a cache bug. - xit("(['property', ...]) should return only those properties, others null", function (done) { + it("(['property', ...]) should return only those properties, others null", function (done) { Person.find().only([ "age", "surname" ]).order("-age").run(function (err, instances) { should.equal(err, null); instances.should.have.property("length", 3); @@ -213,6 +315,18 @@ describe("Model.find() chaining", function() { return done(); }); }); + it("(['property', ...]) should return only those properties, others null (promise-based)", function (done) { + Person.find().only([ "age", "surname" ]).order("-age").runAsync().then(function (instances) { + instances.should.have.property("length", 3); + instances[0].should.have.property("age"); + instances[0].should.have.property("surname", "Doe"); + instances[0].should.have.property("name", null); + + done(); + }).catch(function(err) { + done(err); + }); + }); }); describe("omit", function () { @@ -234,6 +348,23 @@ describe("Model.find() chaining", function() { }); }); + it("('property', ...) should not get these properties (promise-based)", function (done) { + Person.find().omit("age", "surname").order("-age").runAsync().then(function (instances) { + instances.should.have.property("length", 3); + if (common.protocol() != "mongodb") { + should.exist(instances[0].id); + } + should.exist(instances[0].friend_id); + instances[0].should.have.property("age", null); + instances[0].should.have.property("surname", null); + instances[0].should.have.property("name", "Jane"); + + done(); + }).catch(function(err) { + done(err); + }); + }); + it("(['property', ...]) should not get these properties", function (done) { Person.find().omit(["age", "surname"]).order("-age").run(function (err, instances) { should.equal(err, null); @@ -245,6 +376,19 @@ describe("Model.find() chaining", function() { return done(); }); }); + + it("(['property', ...]) should not get these properties (promise-based)", function (done) { + Person.find().omit(["age", "surname"]).order("-age").runAsync().then(function (instances) { + instances.should.have.property("length", 3); + instances[0].should.have.property("age", null); + instances[0].should.have.property("surname", null); + instances[0].should.have.property("name", "Jane"); + + done(); + }).catch(function(err) { + done(err); + }); + }); }); describe(".count()", function () { @@ -678,32 +822,4 @@ describe("Model.find() chaining", function() { }); }); }); - - describe(".success()", function () { - before(setup()); - - it("should return a Promise with .fail() method", function (done) { - Person.find().success(function (people) { - should(Array.isArray(people)); - - return done(); - }).fail(function (err) { - // never called.. - }); - }); - }); - - describe(".fail()", function () { - before(setup()); - - it("should return a Promise with .success() method", function (done) { - Person.find().fail(function (err) { - // never called.. - }).success(function (people) { - should(Array.isArray(people)); - - return done(); - }); - }); - }); }); diff --git a/test/integration/model-find-mapsto.js b/test/integration/model-find-mapsto.js index 1986d690..5b2877d5 100644 --- a/test/integration/model-find-mapsto.js +++ b/test/integration/model-find-mapsto.js @@ -1,6 +1,5 @@ var should = require('should'); var helper = require('../support/spec_helper'); -var ORM = require('../../'); describe("Model.pkMapTo.find()", function() { var db = null; @@ -95,4 +94,4 @@ describe("Model.pkMapTo.find()", function() { }); }); }); -}); +}); \ No newline at end of file diff --git a/test/integration/model-findAsync-mapsto.js b/test/integration/model-findAsync-mapsto.js new file mode 100644 index 00000000..efec9e94 --- /dev/null +++ b/test/integration/model-findAsync-mapsto.js @@ -0,0 +1,94 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); + +describe("Model.pkMapTo.findAsync()", function() { + var db = null; + var Person = null; + + var setup = function () { + return function (done) { + + // The fact that we've applied mapsTo to the key + // property of the model - will break the cache. + + // Without Stuart's little bugfix, 2nd (and subsequent) calls to find() + // will return the repeats of the first obect retrieved and placed in the cache. + Person = db.define("person", { + personId : {type : "integer", key: true, mapsTo: "id"}, + name : String, + surname : String, + age : Number, + male : Boolean + }); + + return helper.dropSync(Person, function () { + Person.create([{ + personId: 1001, + name : "John", + surname : "Doe", + age : 18, + male : true + }, { + personId: 1002, + name : "Jane", + surname : "Doe", + age : 16, + male : false + }, { + personId: 1003, + name : "Jeremy", + surname : "Dean", + age : 18, + male : true + }, { + personId: 1004, + name : "Jack", + surname : "Dean", + age : 20, + male : true + }, { + personId: 1005, + name : "Jasmine", + surname : "Doe", + age : 20, + male : false + }], done); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + + describe("Cache should work with mapped key field", function () { + before(setup()); + + it("1st find should work", function () { + return Person.findAsync({ surname: "Dean" }) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 2); + people[0].surname.should.equal("Dean"); + }); + }); + it("2nd find should should also work", function () { + return Person.findAsync({ surname: "Doe" }) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 3); + people[0].surname.should.equal("Doe"); + }); + }); + }); +}); + diff --git a/test/integration/model-findAsync.js b/test/integration/model-findAsync.js new file mode 100644 index 00000000..21b3e33a --- /dev/null +++ b/test/integration/model-findAsync.js @@ -0,0 +1,305 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); +var ORM = require('../../'); + +describe("Model.findAsync()", function() { + var db = null; + var Person = null; + + var setup = function () { + return function (done) { + Person = db.define("person", { + name : String, + surname : String, + age : Number, + male : Boolean + }); + + return helper.dropSync(Person, function () { + Person.create([{ + name : "John", + surname : "Doe", + age : 18, + male : true + }, { + name : "Jane", + surname : "Doe", + age : 16, + male : false + }, { + name : "Jeremy", + surname : "Dean", + age : 18, + male : true + }, { + name : "Jack", + surname : "Dean", + age : 20, + male : true + }, { + name : "Jasmine", + surname : "Doe", + age : 20, + male : false + }], done); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe("without arguments", function () { + before(setup()); + + it("should return all items", function () { + return Person.findAsync() + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 5); + }); + }); + }); + + describe("with a number as argument", function () { + before(setup()); + + it("should use it as limit", function () { + return Person.findAsync(2) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 2); + }); + }); + }); + + describe("with a string argument", function () { + before(setup()); + + it("should use it as property ascending order", function () { + return Person.findAsync("age") + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 5); + people[0].age.should.equal(16); + people[4].age.should.equal(20); + }); + }); + + it("should use it as property descending order if starting with '-'", function () { + return Person.findAsync("-age") + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 5); + people[0].age.should.equal(20); + people[4].age.should.equal(16); + }); + }); + }); + + describe("with an Array as argument", function () { + before(setup()); + + it("should use it as property ascending order", function () { + return Person.findAsync([ "age" ]) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 5); + people[0].age.should.equal(16); + people[4].age.should.equal(20); + }); + }); + + it("should use it as property descending order if starting with '-'", function () { + return Person.findAsync([ "-age" ]) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 5); + people[0].age.should.equal(20); + people[4].age.should.equal(16); + }); + }); + + it("should use it as property descending order if element is 'Z'", function () { + return Person.findAsync([ "age", "Z" ]) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 5); + people[0].age.should.equal(20); + people[4].age.should.equal(16); + }); + }); + + it("should accept multiple ordering", function () { + return Person.findAsync([ "age", "name", "Z" ]) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 5); + people[0].age.should.equal(16); + people[4].age.should.equal(20); + }); + }); + + it("should accept multiple ordering using '-' instead of 'Z'", function () { + return Person.findAsync([ "age", "-name" ]) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 5); + people[0].age.should.equal(16); + people[4].age.should.equal(20); + }); + }); + }); + + describe("with an Object as argument", function () { + before(setup()); + + it("should use it as conditions", function () { + return Person.findAsync({ age: 16 }) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 1); + people[0].age.should.equal(16); + }); + }); + + it("should accept comparison objects", function () { + return Person.findAsync({ age: ORM.gt(18) }) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 2); + people[0].age.should.equal(20); + people[1].age.should.equal(20); + }); + }); + + describe("with another Object as argument", function () { + before(setup()); + + it("should use it as options", function () { + return Person.findAsync({ age: 18 }, 1, { cache: false }) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 1); + people[0].age.should.equal(18); + + + }); + }); + + describe("if a limit is passed", function () { + before(setup()); + + it("should use it", function () { + return Person.findAsync({ age: 18 }, { limit: 1 }) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 1); + people[0].age.should.equal(18); + + + }); + }); + }); + + describe("if an offset is passed", function () { + before(setup()); + + it("should use it", function () { + return Person.findAsync({}, { offset: 1 }, "age") + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 4); + people[0].age.should.equal(18); + + + }); + }); + }); + + describe("if an order is passed", function () { + before(setup()); + + it("should use it", function () { + return Person.findAsync({ surname: "Doe" }, { order: "age" }) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 3); + people[0].age.should.equal(16); + + + }); + }); + + it("should use it and ignore previously defined order", function () { + return Person.findAsync({ surname: "Doe" }, "-age", { order: "age" }) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 3); + people[0].age.should.equal(16); + + + }); + }); + }); + }); + }); + + describe("with identityCache disabled", function () { + before(setup()); + it("should not return singletons", function () { + return Person.findAsync({ name: "Jasmine" }, { identityCache: false }) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 1); + people[0].name.should.equal("Jasmine"); + people[0].surname.should.equal("Doe"); + + people[0].surname = "Dux"; + + return Person.findAsync({ name: "Jasmine" }, { identityCache: false }); + }) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 1); + people[0].name.should.equal("Jasmine"); + people[0].surname.should.equal("Doe"); + }); + }); + }); + + describe("when using Model.allAsync()", function () { + before(setup()); + it("should work exactly the same", function () { + return Person.allAsync({ surname: "Doe" }, "-age", 1) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 1); + people[0].name.should.equal("Jasmine"); + people[0].surname.should.equal("Doe"); + }); + }); + }); + + describe("when using Model.whereAsync()", function () { + before(setup()); + it("should work exactly the same", function () { + return Person.whereAsync({ surname: "Doe" }, "-age", 1) + .then(function (people) { + people.should.be.a.Object(); + people.should.have.property("length", 1); + people[0].name.should.equal("Jasmine"); + people[0].surname.should.equal("Doe"); + }); + }); + }); +}); diff --git a/test/integration/model-get.js b/test/integration/model-get.js index 3bcffc78..1759cf64 100644 --- a/test/integration/model-get.js +++ b/test/integration/model-get.js @@ -124,23 +124,23 @@ describe("Model.get()", function() { }); describe("changing instance.identityCacheSaveCheck = false", function () { - before(function (done) { + before(function () { Person.settings.set("instance.identityCacheSaveCheck", false); + }); - it("should return the same object with the changed name", function (done) { - Person.get(John[Person.id], function (err, John1) { - should.equal(err, null); + it("should return the same object with the changed name", function (done) { + Person.get(John[Person.id], function (err, John1) { + should.equal(err, null); - John1.name = "James"; + John1.name = "James"; - Person.get(John[Person.id], function (err, John2) { - should.equal(err, null); + Person.get(John[Person.id], function (err, John2) { + should.equal(err, null); - John1[Person.id].should.equal(John2[Person.id]); - John2.name.should.equal("James"); + John1[Person.id].should.equal(John2[Person.id]); + John2.name.should.equal("James"); - return done(); - }); + return done(); }); }); }); diff --git a/test/integration/model-getAsync.js b/test/integration/model-getAsync.js new file mode 100644 index 00000000..7f8b223a --- /dev/null +++ b/test/integration/model-getAsync.js @@ -0,0 +1,218 @@ +var should = require('should'); +var Promise = require('bluebird'); +var helper = require('../support/spec_helper'); +var common = require('../common'); +var ORM = require('../../'); + +describe("Model.getAsync()", function () { + var db = null; + var Person = null; + var John; + + var setup = function (identityCache) { + return function (done) { + Person = db.define("person", { + name : { type: 'text', mapsTo: 'fullname' } + }, { + identityCache : identityCache, + methods : { + UID: function () { + return this[Person.id]; + } + } + }); + + ORM.singleton.clear(); // clear identityCache cache + + return helper.dropSync(Person, function () { + Person.createAsync([{ + name: "John Doe" + }, { + name: "Jane Doe" + }]).then(function (people) { + John = people[0]; + done(); + }).catch(function(err) { + done(err); + }); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe('with identityCache cache', function () { + before(setup(true)); + + it("should throw if passed a wrong number of ids", function () { + return Person.getAsync(1, 2) + .catch(function (err) { + err.should.be.an.Object(); + }); + }); + + it("should accept and try to fetch if passed an Array with ids", function () { + return Person.getAsync([ John[Person.id] ]) + .then(function (John) { + John.should.be.a.Object(); + John.should.have.property(Person.id, John[Person.id]); + John.should.have.property("name", "John Doe"); + }) + }); + + it("should throw err", function () { + return Person.getAsync(999) + .catch(function (err) { + err.should.be.a.Object(); + err.message.should.equal("Not found"); + }); + }); + + it("should return item with id 1", function () { + return Person.getAsync(John[Person.id]) + .then(function (John) { + John.should.be.a.Object(); + John.should.have.property(Person.id, John[Person.id]); + John.should.have.property("name", "John Doe"); + }) + }); + + it("should have an UID method", function () { + return Person.getAsync(John[Person.id]) + .then(function (John) { + John.UID.should.be.a.Function(); + John.UID().should.equal(John[Person.id]); + }) + }); + + it("should return the original object with unchanged name", function () { + return Person.getAsync(John[Person.id]) + .then(function (John1) { + John1.name = "James"; + return Person.getAsync(John[Person.id]); + }) + .then(function (John2) { + should.equal(John2.name, "John Doe"); + }); + }); + + describe("changing instance.identityCacheSaveCheck = false", function () { + before(function () { + Person.settings.set("instance.identityCacheSaveCheck", false); + }); + + it("should return the same object with the changed name", function () { + return Person.getAsync(John[Person.id]) + .then(function (John1) { + John1.name = "James"; + return [John1, Person.getAsync(John[Person.id])]; + }) + .spread(function (John1, John2) { + John1[Person.id].should.equal(John2[Person.id]); + John2.name.should.equal("James"); + }); + }); + }); + }); + + describe("with no identityCache cache", function () { + before(setup(false)); + + it("should return different objects", function () { + return Person.getAsync(John[Person.id]) + .then(function (John1) { + return [John1, Person.getAsync(John[Person.id])]; + }) + .spread(function (John1, John2) { + John1[Person.id].should.equal(John2[Person.id]); + John1.should.not.equal(John2); + }); + }); + }); + + describe("with identityCache cache = 0.5 secs", function () { + before(setup(0.5)); + + it("should return same objects after 0.2 sec", function () { + return Person.getAsync(John[Person.id]) + .then(function (John1) { + return [John1, Promise.delay(200)]; + }) + .spread(function (John1) { + return [John1, Person.getAsync(John[Person.id])]; + }) + .spread(function (John1, John2) { + John1[Person.id].should.equal(John2[Person.id]); + John1.should.equal(John2); + }); + }); + + it("should return different objects after 0.7 sec", function () { + return Person.getAsync(John[Person.id]) + .then(function (John1) { + return [John1, Promise.delay(700)]; + }) + .spread(function (John1) { + return [John1, Person.getAsync(John[Person.id])]; + }) + .spread(function (John1, John2) { + John1.should.not.equal(John2); + }); + }); + }); + + describe("if primary key name is changed", function () { + before(function (done) { + Person = db.define("person", { + name : String + }); + + ORM.singleton.clear(); + + return helper.dropSync(Person, function () { + Person.create([{ + name : "John Doe" + }, { + name : "Jane Doe" + }], done); + }); + }); + + it("should search by key name and not 'id'", function () { + db.settings.set('properties.primary_key', 'name'); + + var OtherPerson = db.define("person", { + id : Number + }); + + return OtherPerson.getAsync("Jane Doe") + .then(function (person) { + person.name.should.equal("Jane Doe"); + db.settings.set('properties.primary_key', 'id'); + }); + }); + }); + + describe("with empty object as options", function () { + before(setup()); + + it("should return item with id 1 like previously", function () { + return Person.getAsync(John[Person.id], {}) + .then(function (John) { + John.should.be.a.Object(); + John.should.have.property(Person.id, John[Person.id]); + John.should.have.property("name", "John Doe"); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/integration/model-one.js b/test/integration/model-one.js index 0ed6e153..dfe62d3b 100644 --- a/test/integration/model-one.js +++ b/test/integration/model-one.js @@ -1,6 +1,5 @@ var should = require('should'); var helper = require('../support/spec_helper'); -var ORM = require('../../'); describe("Model.one()", function() { var db = null; diff --git a/test/integration/model-oneAsync.js b/test/integration/model-oneAsync.js new file mode 100644 index 00000000..2e77a0cf --- /dev/null +++ b/test/integration/model-oneAsync.js @@ -0,0 +1,84 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); + +describe("Model.oneAsync()", function() { + var db = null; + var Person = null; + + var setup = function () { + return function (done) { + Person = db.define("person", { + name : String + }); + + return helper.dropSync(Person, function () { + Person.create([{ + id : 1, + name: "Jeremy Doe" + }, { + id : 2, + name: "John Doe" + }, { + id : 3, + name: "Jane Doe" + }], done); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe("without arguments", function () { + before(setup()); + + it("should return first item in model", function () { + return Person.oneAsync() + .then(function (person) { + person.name.should.equal("Jeremy Doe"); + }); + }); + }); + + describe("with order", function () { + before(setup()); + + it("should return first item in model based on order", function () { + return Person.oneAsync("-name") + .then(function (person) { + person.name.should.equal("John Doe"); + }); + }); + }); + + describe("with conditions", function () { + before(setup()); + + it("should return first item in model based on conditions", function () { + return Person.oneAsync({ name: "Jane Doe" }) + .then(function (person) { + person.name.should.equal("Jane Doe"); + }); + }); + + describe("if no match", function () { + before(setup()); + + it("should return null", function () { + return Person.oneAsync({ name: "Jack Doe" }) + .then(function (person) { + should.equal(person, null); + }); + }); + }); + }); +}); diff --git a/test/integration/model-save.js b/test/integration/model-save.js index 30d48cf6..9d3f4bce 100644 --- a/test/integration/model-save.js +++ b/test/integration/model-save.js @@ -1,7 +1,6 @@ var should = require('should'); var helper = require('../support/spec_helper'); var common = require('../common'); -var ORM = require('../../'); describe("Model.save()", function() { var db = null; diff --git a/test/integration/model-saveAsync.js b/test/integration/model-saveAsync.js new file mode 100644 index 00000000..df78332f --- /dev/null +++ b/test/integration/model-saveAsync.js @@ -0,0 +1,415 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); +var common = require('../common'); + +describe("Model.saveAsync()", function() { + var db = null; + var Person = null; + + var setup = function (nameDefinition, opts) { + opts = opts || {}; + + return function (done) { + Person = db.define("person", { + name : nameDefinition || String + }, opts || {}); + + Person.hasOne("parent", Person, opts.hasOneOpts); + if ('saveAssociationsByDefault' in opts) { + Person.settings.set( + 'instance.saveAssociationsByDefault', opts.saveAssociationsByDefault + ); + } + + return helper.dropSync(Person, done); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe("if properties have default values", function () { + before(setup({ type: "text", defaultValue: "John" })); + + it("should use it if not defined", function () { + var John = new Person(); + + return John.saveAsync() + .then(function () { + John.name.should.equal("John"); + }); + }); + }); + + describe("with callback", function () { + before(setup()); + + it("should save item and return id", function () { + var John = new Person({ + name: "John" + }); + + return John.saveAsync() + .then(function () { + should.exist(John[Person.id]); + return Person.getAsync(John[Person.id]); + }) + .then(function (JohnCopy) { + JohnCopy[Person.id].should.equal(John[Person.id]); + JohnCopy.name.should.equal(John.name); + }); + }); + }); + + describe("without callback", function () { + before(setup()); + + it("should still save item and return id", function (done) { + var John = new Person({ + name: "John" + }); + John.saveAsync(); + John.on("save", function (err) { + should.equal(err, null); + should.exist(John[Person.id]); + + Person.getAsync(John[Person.id]) + .then(function (JohnCopy) { + JohnCopy[Person.id].should.equal(John[Person.id]); + JohnCopy.name.should.equal(John.name); + + return done(); + }); + }); + }); + }); + + describe("with properties object", function () { + before(setup()); + + it("should update properties, save item and return id", function () { + var John = new Person({ + name: "Jane" + }); + return John.saveAsync({ name: "John" }) + .then(function () { + should.exist(John[Person.id]); + John.name.should.equal("John"); + + return Person.getAsync(John[Person.id]) + }) + .then(function (JohnCopy) { + JohnCopy[Person.id].should.equal(John[Person.id]); + JohnCopy.name.should.equal(John.name); + }); + }); + }); + + describe("with unknown argument type", function () { + before(setup()); + + it("should should throw", function () { + var John = new Person({ + name: "Jane" + }); + return John.saveAsync("will-fail") + .catch(function (err) { + err.should.be.an.Object(); + }); + }); + }); + + describe("if passed an association instance", function () { + before(setup()); + + it("should save association first and then save item and return id", function () { + var Jane = new Person({ + name : "Jane" + }); + var John = new Person({ + name : "John", + parent: Jane + }); + + return John.saveAsync() + .then(function () { + John.saved().should.be.true(); + Jane.saved().should.be.true(); + + should.exist(John[Person.id]); + should.exist(Jane[Person.id]); + }); + }); + }); + + describe("if passed an association object", function () { + before(setup()); + + it("should save association first and then save item and return id", function () { + var John = new Person({ + name : "John", + parent: { + name : "Jane" + } + }); + + return John.saveAsync() + .then(function () { + John.saved().should.be.true(); + John.parent.saved().should.be.true(); + + should.exist(John[Person.id]); + should.exist(John.parent[Person.id]); + should.equal(John.parent.name, "Jane"); + }); + }); + }); + + describe("if autoSave is on", function () { + before(setup(null, { autoSave: true })); + + it("should save the instance as soon as a property is changed", function (done) { + var John = new Person({ + name : "Jhon" + }); + + John.saveAsync() + .then(function () { + John.on("save", function () { + return done(); + }); + + John.name = "John"; + }); + }); + }); + + describe("with saveAssociations", function () { + var afterSaveCalled = false; + + if (common.protocol() == 'mongodb') return; + + describe("default on in settings", function () { + beforeEach(function (done) { + function afterSave () { + afterSaveCalled = true; + } + var hooks = { afterSave: afterSave }; + + setup(null, { hooks: hooks, cache: false, hasOneOpts: { autoFetch: true } })(function (err) { + should.not.exist(err); + + Person.create({ name: 'Olga' }, function (err, olga) { + should.not.exist(err); + + should.exist(olga); + Person.create({ name: 'Hagar', parent_id: olga.id }, function (err, hagar) { + should.not.exist(err); + should.exist(hagar); + afterSaveCalled = false; + done(); + }); + }); + }); + }); + + it("should be on", function () { + should.equal(Person.settings.get('instance.saveAssociationsByDefault'), true); + }); + + it("off should not save associations but save itself", function () { + return Person.oneAsync({ name: 'Hagar' }) + .then(function (hagar) { + should.exist(hagar.parent); + + hagar.parent.name = 'Olga2'; + return [hagar, hagar.saveAsync({name: 'Hagar2'}, { saveAssociations: false })]; + }) + .spread(function (hagar) { + should.equal(afterSaveCalled, true); + + return Person.getAsync(hagar.parent.id); + }) + .then(function (olga) { + should.equal(olga.name, 'Olga'); + }); + }); + + it("off should not save associations or itself if there are no changes", function () { + return Person.oneAsync({ name: 'Hagar' }) + .then(function (hagar) { + return [hagar, hagar.saveAsync({}, { saveAssociations: false })]; + }) + .spread(function (hagar) { + should.equal(afterSaveCalled, false); + return Person.getAsync(hagar.parent.id); + }) + .then(function (olga) { + should.equal(olga.name, 'Olga'); + }); + }); + + it("unspecified should save associations and itself", function () { + return Person.oneAsync({ name: 'Hagar' }) + .then(function (hagar) { + should.exist(hagar.parent); + + hagar.parent.name = 'Olga2'; + return [hagar, hagar.saveAsync({name: 'Hagar2'})]; + }) + .spread(function (hagar) { + return [hagar, Person.getAsync(hagar.parent.id)]; + }) + .spread(function (hagar, olga) { + should.equal(olga.name, 'Olga2'); + + return Person.getAsync(hagar.id); + }) + .then(function (person) { + should.equal(person.name, 'Hagar2'); + }); + }); + + it("on should save associations and itself", function () { + return Person.oneAsync({ name: 'Hagar' }) + .then(function (hagar) { + should.exist(hagar.parent); + + hagar.parent.name = 'Olga2'; + return [hagar, hagar.saveAsync({name: 'Hagar2'}, { saveAssociations: true })]; + }) + .spread(function (hagar) { + return [hagar, Person.getAsync(hagar.parent.id)]; + }) + .spread(function (hagar, olga) { + should.equal(olga.name, 'Olga2'); + + return Person.getAsync(hagar.id); + }) + .then(function (person) { + should.equal(person.name, 'Hagar2'); + }); + }); + }); + + describe("turned off in settings", function () { + beforeEach(function (done) { + function afterSave () { + afterSaveCalled = true; + } + var hooks = { afterSave: afterSave }; + + setup(null, { + hooks: hooks, cache: false, hasOneOpts: { autoFetch: true }, + saveAssociationsByDefault: false + })(function (err) { + should.not.exist(err); + + Person.create({ name: 'Olga' }, function (err, olga) { + should.not.exist(err); + + should.exist(olga); + Person.create({ name: 'Hagar', parent_id: olga.id }, function (err, hagar) { + should.not.exist(err); + should.exist(hagar); + afterSaveCalled = false; + done(); + }); + }); + }); + }); + + it("should be off", function () { + should.equal(Person.settings.get('instance.saveAssociationsByDefault'), false); + }); + + it("unspecified should not save associations but save itself", function () { + return Person.oneAsync({ name: 'Hagar' }) + .then(function (hagar) { + should.exist(hagar.parent); + + hagar.parent.name = 'Olga2'; + return [hagar, hagar.saveAsync({ name: 'Hagar2' })]; + }) + .spread(function (hagar) { + return [hagar, Person.getAsync(hagar.parent.id)]; + }) + .spread(function (hagar, olga) { + should.equal(olga.name, 'Olga'); + + return Person.getAsync(hagar.id); + }) + .then(function (person) { + should.equal(person.name, 'Hagar2'); + }); + }); + + it("off should not save associations but save itself", function () { + return Person.oneAsync({ name: 'Hagar' }) + .then(function (hagar) { + should.exist(hagar.parent); + + hagar.parent.name = 'Olga2'; + return [hagar, hagar.saveAsync({ name: 'Hagar2' }, { saveAssociations: false })]; + }) + .spread(function (hagar) { + should.equal(afterSaveCalled, true); + + return Person.getAsync(hagar.parent.id); + }) + .then(function (olga) { + should.equal(olga.name, 'Olga'); + }); + }); + + it("on should save associations and itself", function () { + return Person.oneAsync({ name: 'Hagar' }) + .then(function (hagar) { + should.exist(hagar.parent); + + hagar.parent.name = 'Olga2'; + return [hagar, hagar.saveAsync({ name: 'Hagar2' }, { saveAssociations: true })]; + }) + .spread(function (hagar) { + return [hagar, Person.getAsync(hagar.parent.id)]; + }) + .spread(function (hagar, olga) { + should.equal(olga.name, 'Olga2'); + + return Person.getAsync(hagar.id); + }) + .then(function (person) { + should.equal(person.name, 'Hagar2'); + }); + }); + }); + }); + + describe("with a point property", function () { + if (common.protocol() == 'sqlite' || common.protocol() == 'mongodb') return; + before(function (done) { + setup({ type: "point" })(done); + }); + + it("should save the instance as a geospatial point", function () { + var John = new Person({ + name: { x: 51.5177, y: -0.0968 } + }); + return John.saveAsync() + .then(function () { + John.name.should.be.an.instanceOf(Object); + John.name.should.have.property('x', 51.5177); + John.name.should.have.property('y', -0.0968); + }); + }); + }); +}); diff --git a/test/integration/model-sync.js b/test/integration/model-sync.js index bf4199a3..585f6606 100644 --- a/test/integration/model-sync.js +++ b/test/integration/model-sync.js @@ -1,7 +1,5 @@ -var _ = require('lodash'); var should = require('should'); var helper = require('../support/spec_helper'); -var ORM = require('../../'); var common = require('../common'); describe("Model.sync", function () { diff --git a/test/integration/orm-exports.js b/test/integration/orm-exports.js index 9b66b1c1..9fa4871e 100644 --- a/test/integration/orm-exports.js +++ b/test/integration/orm-exports.js @@ -12,7 +12,9 @@ describe("ORM", function() { it("should expose .express(), .use() and .connect()", function (done) { ORM.express.should.be.a.Function(); ORM.use.should.be.a.Function(); + ORM.useAsync.should.be.a.Function(); ORM.connect.should.be.a.Function(); + ORM.connectAsync.should.be.a.Function(); return done(); }); @@ -48,302 +50,516 @@ describe("ORM", function() { return done(); }); }); -}); + describe('ORM.connectAsync()', function () { + it('should be a function', function () { + return ORM.connectAsync.should.be.a.Function() + }); -describe("ORM.connect()", function () { - it("should expose .use(), .define(), .sync() and .load()", function (done) { - var db = ORM.connect(); + it('should throw error with correct message when protocol not supported', function () { + return ORM.connectAsync("bd://127.0.0.6") + .catch(function (err) { + should.exist(err); + err.message.should.not.equal("CONNECTION_PROTOCOL_NOT_SUPPORTED"); + }); + }); - db.use.should.be.a.Function(); - db.define.should.be.a.Function(); - db.sync.should.be.a.Function(); - db.load.should.be.a.Function(); + it('should throw error with correct message when connection URL doesn\'t exist', function () { + ORM.connectAsync() + .catch(function (err) { + err.message.should.equal("CONNECTION_URL_EMPTY"); + }); + }); - return done(); - }); + it("should throw error when passed empty string like connection URL", function () { + return ORM.connectAsync("") + .catch(function (err) { + err.message.should.equal("CONNECTION_URL_EMPTY"); + }); + }); - it("should emit an error if no url is passed", function (done) { - var db = ORM.connect(); + it("should throw error when passed string with spaces only", function () { + return ORM.connectAsync(" ") + .catch(function (err) { + err.message.should.equal("CONNECTION_URL_EMPTY"); + }); + }); - db.on("connect", function (err) { - err.message.should.equal("CONNECTION_URL_EMPTY"); + it("should throw error when passed invalid protocol", function () { + return ORM.connectAsync("user@db") + .catch(function (err) { + err.message.should.equal("CONNECTION_URL_NO_PROTOCOL"); + }); + }); - return done(); + it("should throw error when passed unknown protocol", function () { + return ORM.connectAsync("unknown://db") + .catch(function (err) { + should.equal(err.literalCode, 'NO_SUPPORT'); + should.equal( + err.message, + "Connection protocol not supported - have you installed the database driver for unknown?" + ); + }); }); - }); - it("should allow protocol alias", function (done) { - var db = ORM.connect("pg://127.0.0.2"); + it("should throw error when passed invalid connection db link", function () { + return ORM.connectAsync("mysql://fakeuser:nopassword@127.0.0.1/unknowndb") + .catch(function (err) { + should.exist(err); + should.equal(err.message.indexOf("Connection protocol not supported"), -1); + err.message.should.not.equal("CONNECTION_URL_NO_PROTOCOL"); + err.message.should.not.equal("CONNECTION_URL_EMPTY"); + }); + }); - db.once("connect", function (err) { - should.exist(err); - err.message.should.not.equal("CONNECTION_PROTOCOL_NOT_SUPPORTED"); + it("should do not mutate opts", function () { + var opts = { + protocol : 'mysql', + user : 'notauser', + password : "wrong password", + query : { pool: true, debug: true } + }; + + var expected = JSON.stringify(opts); + + return ORM.connectAsync(opts) + .catch(function () { + should.equal( + JSON.stringify(opts), + expected + ); + }); + }); - return done(); + it("should pass successful when opts is OK!", function () { + return ORM.connectAsync(common.getConnectionString()) + .then(function (db) { + should.exist(db); + + db.use.should.be.a.Function(); + db.define.should.be.a.Function(); + db.sync.should.be.a.Function(); + db.load.should.be.a.Function(); + }) }); - }); - it("should emit an error if empty url is passed", function (done) { - var db = ORM.connect(""); + describe('POOL via connectAsync', function () { + var connStr = null; - db.on("connect", function (err) { - err.message.should.equal("CONNECTION_URL_EMPTY"); + beforeEach(function () { + connStr = common.getConnectionString(); + }); - return done(); - }); - }); + afterEach(function () { + connStr = null + }); - it("should emit an error if empty url (with only spaces) is passed", function (done) { - var db = ORM.connect(" "); + if (protocol !== 'mongodb') { + it("should understand pool `'false'` from query string", function () { + var connString = connStr + "debug=false&pool=false"; + return ORM.connectAsync(connString) + .then(function (db) { + should.strictEqual(db.driver.opts.pool, false); + should.strictEqual(db.driver.opts.debug, false); + }) + }); - db.on("connect", function (err) { - err.message.should.equal("CONNECTION_URL_EMPTY"); + it("should understand pool `'0'` from query string", function () { + var connString = connStr + "debug=0&pool=0"; + return ORM.connectAsync(connString) + .then(function (db) { + should.strictEqual(db.driver.opts.pool, false); + should.strictEqual(db.driver.opts.debug, false); + }); + }); - return done(); - }); - }); + it("should understand pool `'true'` from query string", function () { + var connString = connStr + "debug=true&pool=true"; + return ORM.connectAsync(connString) + .then(function (db) { + should.strictEqual(db.driver.opts.pool, true); + should.strictEqual(db.driver.opts.debug, true); + }); + }); - it("should emit an error if no protocol is passed", function (done) { - var db = ORM.connect("user@db"); + it("should understand pool `'true'` from query string", function () { + var connString = connStr + "debug=1&pool=1"; + return ORM.connectAsync(connString) + .then(function (db) { + should.strictEqual(db.driver.opts.pool, true); + should.strictEqual(db.driver.opts.debug, true); + }); + }); - db.on("connect", function (err) { - err.message.should.equal("CONNECTION_URL_NO_PROTOCOL"); + it("should understand pool `'true'` from query string", function () { + var connCopy = _.cloneDeep(common.getConfig()); + var connOpts = _.extend(connCopy, { + protocol: common.protocol(), + query: { + pool: true, debug: true + } + }); + + return ORM.connectAsync(connOpts) + .then(function (db) { + should.strictEqual(db.driver.opts.pool, true); + should.strictEqual(db.driver.opts.debug, true); + }); + }); - return done(); + it("should understand pool `false` from query options", function () { + var connCopy = _.cloneDeep(common.getConfig()); + var connOpts = _.extend(connCopy, { + protocol: common.protocol(), + query: { + pool: false, debug: false + } + }); + + return ORM.connectAsync(connOpts) + .then(function (db) { + should.strictEqual(db.driver.opts.pool, false); + should.strictEqual(db.driver.opts.debug, false); + }); + }); + } }); }); - it("should emit an error if unknown protocol is passed", function (done) { - var db = ORM.connect("unknown://db"); + describe("ORM.connect()", function () { + it("should expose .use(), .define(), .sync() and .load()", function (done) { + var db = ORM.connect(); - db.on("connect", function (err) { - should.equal(err.literalCode, 'NO_SUPPORT'); - should.equal( - err.message, - "Connection protocol not supported - have you installed the database driver for unknown?" - ); + db.use.should.be.a.Function(); + db.define.should.be.a.Function(); + db.sync.should.be.a.Function(); + db.load.should.be.a.Function(); return done(); }); - }); - it("should emit an error if cannot connect", function (done) { - var db = ORM.connect("mysql://fakeuser:nopassword@127.0.0.1/unknowndb"); + it("should emit an error if no url is passed", function (done) { + var db = ORM.connect(); - db.on("connect", function (err) { - should.exist(err); - should.equal(err.message.indexOf("Connection protocol not supported"), -1); - err.message.should.not.equal("CONNECTION_URL_NO_PROTOCOL"); - err.message.should.not.equal("CONNECTION_URL_EMPTY"); + db.on("connect", function (err) { + err.message.should.equal("CONNECTION_URL_EMPTY"); - return done(); + return done(); + }); }); - }); - it("should emit valid error if exception being thrown during connection try", function (done) { - var testConfig = { - protocol : 'mongodb', - href : 'unknownhost', - database : 'unknowndb', - user : '', - password : '' - }, - db = ORM.connect(testConfig); - - db.on("connect", function (err) { - should.exist(err); - should.equal(err.message.indexOf("Connection protocol not supported"), -1); - err.message.should.not.equal("CONNECTION_URL_NO_PROTOCOL"); - err.message.should.not.equal("CONNECTION_URL_EMPTY"); + it.skip("should allow protocol alias", function (done) { + this.timeout(60000); + var db = ORM.connect("pg://127.0.0.6"); - return done(); + db.once("connect", function (err) { + should.exist(err); + err.message.should.not.equal("CONNECTION_PROTOCOL_NOT_SUPPORTED"); + + return done(); + }); }); - }); - it("should not modify connection opts", function (done) { - var opts = { - protocol : 'mysql', - user : 'notauser', - password : "wrong password", - query : { pool: true, debug: true } - }; + it("should emit an error if empty url is passed", function (done) { + var db = ORM.connect(""); - var expected = JSON.stringify(opts); + db.on("connect", function (err) { + err.message.should.equal("CONNECTION_URL_EMPTY"); - ORM.connect(opts, function (err, db) { - should.equal( - JSON.stringify(opts), - expected - ); - done(); + return done(); + }); }); - }); - it("should emit no error if ok", function (done) { - var db = ORM.connect(common.getConnectionString()); + it("should emit an error if empty url (with only spaces) is passed", function (done) { + var db = ORM.connect(" "); - db.on("connect", function (err) { - should.not.exist(err); + db.on("connect", function (err) { + err.message.should.equal("CONNECTION_URL_EMPTY"); - return done(); + return done(); + }); }); - }); - describe("if no connection error", function () { - var db = null; + it("should emit an error if no protocol is passed", function (done) { + var db = ORM.connect("user@db"); - before(function (done) { - helper.connect(function (connection) { - db = connection; + db.on("connect", function (err) { + err.message.should.equal("CONNECTION_URL_NO_PROTOCOL"); return done(); }); }); - after(function () { - return db.close(); - }); + it("should emit an error if unknown protocol is passed", function (done) { + var db = ORM.connect("unknown://db"); + + db.on("connect", function (err) { + should.equal(err.literalCode, 'NO_SUPPORT'); + should.equal( + err.message, + "Connection protocol not supported - have you installed the database driver for unknown?" + ); - it("should be able to ping the server", function (done) { - db.ping(function () { return done(); }); }); - }); - describe("if callback is passed", function (done) { - it("should return an error if empty url is passed", function (done) { - ORM.connect("", function (err) { - err.message.should.equal("CONNECTION_URL_EMPTY"); + it("should emit an error if cannot connect", function (done) { + var db = ORM.connect("mysql://fakeuser:nopassword@127.0.0.1/unknowndb"); + + db.on("connect", function (err) { + should.exist(err); + should.equal(err.message.indexOf("Connection protocol not supported"), -1); + err.message.should.not.equal("CONNECTION_URL_NO_PROTOCOL"); + err.message.should.not.equal("CONNECTION_URL_EMPTY"); return done(); }); }); - it("should return an error if no protocol is passed", function (done) { - ORM.connect("user@db", function (err) { - err.message.should.equal("CONNECTION_URL_NO_PROTOCOL"); + it("should emit valid error if exception being thrown during connection try", function (done) { + var testConfig = { + protocol : 'mongodb', + href : 'unknownhost', + database : 'unknowndb', + user : '', + password : '' + }, + db = ORM.connect(testConfig); + + db.on("connect", function (err) { + should.exist(err); + should.equal(err.message.indexOf("Connection protocol not supported"), -1); + err.message.should.not.equal("CONNECTION_URL_NO_PROTOCOL"); + err.message.should.not.equal("CONNECTION_URL_EMPTY"); return done(); }); }); - it("should return an error if unknown protocol is passed", function (done) { - ORM.connect("unknown://db", function (err) { - should.equal(err.literalCode, 'NO_SUPPORT'); + it("should not modify connection opts", function (done) { + var opts = { + protocol : 'mysql', + user : 'notauser', + password : "wrong password", + query : { pool: true, debug: true } + }; + + var expected = JSON.stringify(opts); + + ORM.connect(opts, function (err, db) { should.equal( - err.message, - "Connection protocol not supported - have you installed the database driver for unknown?" + JSON.stringify(opts), + expected ); + done(); + }); + }); + + it("should emit no error if ok", function (done) { + var db = ORM.connect(common.getConnectionString()); + + db.on("connect", function (err) { + should.not.exist(err); return done(); }); }); - }); - if (protocol != 'mongodb') { - describe("query options", function () { - it("should understand pool `'false'` from query string", function (done) { - var connString = common.getConnectionString() + "debug=false&pool=false"; - ORM.connect(connString, function (err, db) { - should.not.exist(err); - should.strictEqual(db.driver.opts.pool, false); - should.strictEqual(db.driver.opts.debug, false); - done(); + describe("if no connection error", function () { + var db = null; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); }); }); - it("should understand pool `'0'` from query string", function (done) { - var connString = common.getConnectionString() + "debug=0&pool=0"; - ORM.connect(connString, function (err, db) { - should.not.exist(err); - should.strictEqual(db.driver.opts.pool, false); - should.strictEqual(db.driver.opts.debug, false); - done(); - }); + after(function () { + return db.close(); }); - it("should understand pool `'true'` from query string", function (done) { - var connString = common.getConnectionString() + "debug=true&pool=true"; - ORM.connect(connString, function (err, db) { - should.not.exist(err); - should.strictEqual(db.driver.opts.pool, true); - should.strictEqual(db.driver.opts.debug, true); - done(); + it("should be able to ping the server", function (done) { + db.ping(function () { + return done(); }); }); - it("should understand pool `'1'` from query string", function (done) { - var connString = common.getConnectionString() + "debug=1&pool=1"; - ORM.connect(connString, function (err, db) { - should.not.exist(err); - should.strictEqual(db.driver.opts.pool, true); - should.strictEqual(db.driver.opts.debug, true); - done(); + it("should be able to pingAsync the server", function () { + return db.pingAsync(); + }); + }); + + describe("if callback is passed", function () { + it("should return an error if empty url is passed", function (done) { + ORM.connect("", function (err) { + err.message.should.equal("CONNECTION_URL_EMPTY"); + + return done(); }); }); - it("should understand pool `true` from query options", function (done) { - var connOpts = _.extend(common.getConfig(), { - protocol: common.protocol(), - query: { - pool: true, debug: true - } + it("should return an error if no protocol is passed", function (done) { + ORM.connect("user@db", function (err) { + err.message.should.equal("CONNECTION_URL_NO_PROTOCOL"); + + return done(); }); - ORM.connect(connOpts, function (err, db) { - should.not.exist(err); - should.strictEqual(db.driver.opts.pool, true); - should.strictEqual(db.driver.opts.debug, true); - done(); + }); + + it("should return an error if unknown protocol is passed", function (done) { + ORM.connect("unknown://db", function (err) { + should.equal(err.literalCode, 'NO_SUPPORT'); + should.equal( + err.message, + "Connection protocol not supported - have you installed the database driver for unknown?" + ); + + return done(); }); }); + }); + + if (protocol !== 'mongodb') { + describe("query options", function () { + var connStr = null; + + beforeEach(function () { + connStr = common.getConnectionString(); + }); + + afterEach(function () { + connStr = null + }); + it("should understand pool `'false'` from query string", function (done) { + var connString = connStr + "debug=false&pool=false"; + ORM.connect(connString, function (err, db) { + should.not.exist(err); + should.strictEqual(db.driver.opts.pool, false); + should.strictEqual(db.driver.opts.debug, false); + done(); + }); + }); + + it("should understand pool `'0'` from query string", function (done) { + var connString = connStr + "debug=0&pool=0"; + ORM.connect(connString, function (err, db) { + should.not.exist(err); + should.strictEqual(db.driver.opts.pool, false); + should.strictEqual(db.driver.opts.debug, false); + done(); + }); + }); + + it("should understand pool `'true'` from query string", function (done) { + var connString = connStr + "debug=true&pool=true"; + ORM.connect(connString, function (err, db) { + should.not.exist(err); + should.strictEqual(db.driver.opts.pool, true); + should.strictEqual(db.driver.opts.debug, true); + done(); + }); + }); - it("should understand pool `false` from query options", function (done) { - var connOpts = _.extend(common.getConfig(), { - protocol: common.protocol(), - query: { - pool: false, debug: false - } + it("should understand pool `'1'` from query string", function (done) { + var connString = connStr + "debug=1&pool=1"; + ORM.connect(connString, function (err, db) { + should.not.exist(err); + should.strictEqual(db.driver.opts.pool, true); + should.strictEqual(db.driver.opts.debug, true); + done(); + }); }); - ORM.connect(connOpts, function (err, db) { - should.not.exist(err); - should.strictEqual(db.driver.opts.pool, false); - should.strictEqual(db.driver.opts.debug, false); - done(); + + it("should understand pool `true` from query options", function (done) { + var connCopy = _.cloneDeep(common.getConfig()); + var connOpts = _.extend(connCopy, { + protocol: common.protocol(), + query: { + pool: true, debug: true + } + }); + ORM.connect(connOpts, function (err, db) { + should.not.exist(err); + should.strictEqual(db.driver.opts.pool, true); + should.strictEqual(db.driver.opts.debug, true); + done(); + }); + }); + + it("should understand pool `false` from query options", function (done) { + var connCopy = _.cloneDeep(common.getConfig()); + var connOpts = _.extend(connCopy, { + protocol: common.protocol(), + query: { + pool: false, debug: false + } + }); + ORM.connect(connOpts, function (err, db) { + should.not.exist(err); + should.strictEqual(db.driver.opts.pool, false); + should.strictEqual(db.driver.opts.debug, false); + done(); + }); }); }); + } + }); + + describe("ORM.use()", function () { + it("should be able to use an established connection", function (done) { + var db = new sqlite.Database(':memory:'); + + ORM.use(db, "sqlite", function (err) { + should.not.exist(err); + + return done(); + }); }); - } -}); -describe("ORM.use()", function () { - it("should be able to use an established connection", function (done) { - var db = new sqlite.Database(':memory:'); + it("should be accept protocol alias", function (done) { + var db = new pg.Client(); - ORM.use(db, "sqlite", function (err) { - should.not.exist(err); + ORM.use(db, "pg", function (err) { + should.equal(err, null); - return done(); + return done(); + }); }); - }); - it("should be accept protocol alias", function (done) { - var db = new pg.Client(); + it("should return an error in callback if protocol not supported", function (done) { + var db = new pg.Client(); - ORM.use(db, "pg", function (err) { - should.equal(err, null); + ORM.use(db, "unknowndriver", function (err) { + should.exist(err); - return done(); + return done(); + }); }); }); - it("should return an error in callback if protocol not supported", function (done) { - var db = new pg.Client(); + describe("ORM.useAsync()", function () { + it("should be able to use an established connection", function () { + var db = new sqlite.Database(':memory:'); - ORM.use(db, "unknowndriver", function (err) { - should.exist(err); + return ORM.useAsync(db, "sqlite"); + }); - return done(); + it("should be accept protocol alias", function () { + var db = new pg.Client(); + + return ORM.useAsync(db, "pg") + }); + + it("should throw an error in callback if protocol not supported", function () { + var db = new pg.Client(); + + return ORM.useAsync(db, "unknowndriver") + .catch(function (err) { + should.exist(err); + }); }); }); -}); +}); \ No newline at end of file diff --git a/test/integration/property-lazyload-async.js b/test/integration/property-lazyload-async.js new file mode 100644 index 00000000..bc963f8d --- /dev/null +++ b/test/integration/property-lazyload-async.js @@ -0,0 +1,133 @@ +var should = require('should'); +var helper = require('../support/spec_helper'); +var ORM = require('../../'); + +describe("LazyLoad Async properties", function() { + var db = null; + var Person = null; + var PersonPhoto = new Buffer(1024); // fake photo + var OtherPersonPhoto = new Buffer(1024); // other fake photo + + var setup = function () { + return function (done) { + Person = db.define("person", { + name : String, + photo : { type: "binary", lazyload: true } + }); + + ORM.singleton.clear(); + + return helper.dropSync(Person, function () { + Person.create({ + name : "John Doe", + photo : PersonPhoto + }, done); + }); + }; + }; + + before(function (done) { + helper.connect(function (connection) { + db = connection; + + return done(); + }); + }); + + after(function () { + return db.close(); + }); + + describe("when defined Async methods", function () { + before(setup()); + + it("should not be available when fetching an instance", function () { + return Person + .findAsync() + .then(function (John) { + var john = John[0]; + + should.equal(typeof john, 'object'); + should.equal(Array.isArray(john), false); + john.should.have.property("name", "John Doe"); + john.should.have.property("photo", null); + }); + }); + + it("should have apropriate accessors", function () { + return Person + .findAsync() + .then(function (persons) { + var John = persons[0]; + should.equal(typeof John, 'object'); + should.equal(Array.isArray(John), false); + + John.getPhotoAsync.should.be.a.Function(); + John.setPhotoAsync.should.be.a.Function(); + John.removePhotoAsync.should.be.a.Function(); + }); + }); + + it("getAccessorAsync should return property", function () { + return Person + .findAsync() + .then(function (persons) { + var John = persons[0]; + + should.equal(typeof John, 'object'); + should.equal(Array.isArray(John), false); + return John.getPhotoAsync(); + }) + .then(function (photo) { + photo.toString().should.equal(PersonPhoto.toString()); + }); + }); + + it("setAccessorAsync should change property", function () { + return Person + .findAsync() + .then(function (persons) { + var John = persons[0]; + should.equal(typeof John, 'object'); + return John.setPhotoAsync(OtherPersonPhoto); + }) + .then(function (johnPhotoUpdated) { + should.equal(typeof johnPhotoUpdated, 'object'); + return Person.findAsync(); + }) + .then(function (persons) { + var John = persons[0]; + + should.equal(typeof John, 'object'); + should.equal(Array.isArray(John), false); + return John.getPhotoAsync(); + }) + .then(function (photo) { + photo.toString().should.equal(OtherPersonPhoto.toString()); + }); + }); + + it("removeAccessorAsync should change property", function () { + return Person + .findAsync() + .then(function (persons) { + var John = persons[0]; + + should.equal(typeof John, 'object'); + should.equal(Array.isArray(John), false); + return John.removePhotoAsync(); + }) + .then(function (John) { + return Person.getAsync(John[Person.id]); + }) + .then(function (John) { + should.equal(typeof John, 'object'); + should.equal(Array.isArray(John), false); + return John.getPhotoAsync(); + }) + .then(function (photo) { + should.equal(photo, null); + }); + }); + }); +}); diff --git a/test/integration/property-lazyload.js b/test/integration/property-lazyload.js index 3149eaa0..0b83f00c 100644 --- a/test/integration/property-lazyload.js +++ b/test/integration/property-lazyload.js @@ -59,6 +59,7 @@ describe("LazyLoad properties", function() { should.equal(err, null); John.should.be.a.Object(); + John.getPhoto.should.be.a.Function(); John.setPhoto.should.be.a.Function(); John.removePhoto.should.be.a.Function(); @@ -85,7 +86,6 @@ describe("LazyLoad properties", function() { it("setAccessor should change property", function (done) { Person.find().first(function (err, John) { should.equal(err, null); - John.should.be.a.Object(); John.setPhoto(OtherPersonPhoto, function (err) { @@ -93,13 +93,11 @@ describe("LazyLoad properties", function() { Person.find().first(function (err, John) { should.equal(err, null); - John.should.be.a.Object(); John.getPhoto(function (err, photo) { should.equal(err, null); photo.toString().should.equal(OtherPersonPhoto.toString()); - return done(); }); }); @@ -110,26 +108,24 @@ describe("LazyLoad properties", function() { it("removeAccessor should change property", function (done) { Person.find().first(function (err, John) { should.equal(err, null); + John.should.be.a.Object(); - John.should.be.a.Object(); - - John.removePhoto(function (err) { - should.equal(err, null); - - Person.get(John[Person.id], function (err, John) { + John.removePhoto(function (err) { should.equal(err, null); - John.should.be.a.Object(); - - John.getPhoto(function (err, photo) { + Person.get(John[Person.id], function (err, John) { should.equal(err, null); - should.equal(photo, null); + John.should.be.a.Object(); - return done(); + John.getPhoto(function (err, photo) { + should.equal(err, null); + should.equal(photo, null); + + return done(); + }); }); }); }); - }); }); }); }); diff --git a/test/integration/property-number-size.js b/test/integration/property-number-size.js index b4b3e2dd..bf175518 100644 --- a/test/integration/property-number-size.js +++ b/test/integration/property-number-size.js @@ -1,7 +1,6 @@ var should = require('should'); var common = require('../common'); var helper = require('../support/spec_helper'); -var ORM = require('../../'); var protocol = common.protocol().toLowerCase(); // Round because different systems store floats in different