不要怂,就是干,撸起袖子干!

Commit 1350aa91 by Sascha Depold

Merge branch 'master' of github.com:sequelize/sequelize

2 parents 30ad8c19 c5fee254
......@@ -30,7 +30,7 @@ The Sequelize library provides easy access to MySQL, SQLite or PostgreSQL databa
- Associations
- Importing definitions from single files
## Documentation, Examples and Updates ##
## Documentation and Updates ##
You can find the documentation and announcements of updates on the [project's website](http://www.sequelizejs.com).
If you want to know about latest development and releases, follow me on [Twitter](http://twitter.com/sdepold).
......@@ -42,6 +42,11 @@ Also make sure to take a look at the examples in the repository. The website wil
- [Google Groups](https://groups.google.com/forum/#!forum/sequelize)
- [XING](https://www.xing.com/net/priec1b5cx/sequelize) (pretty much inactive, but you might want to name it on your profile)
## Running Examples
Instructions for running samples are located in the [example directory](https://github.com/sequelize/sequelize/tree/master/examples). Try these samples in a live sandbox environment:
<a href="https://runnable.com/sequelize" target="_blank"><img src="https://runnable.com/external/styles/assets/runnablebtn.png"></a>
## Roadmap
A very basic roadmap. Chances aren't too bad, that not mentioned things are implemented as well. Don't panic :)
......@@ -70,8 +75,12 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl
### 2.0.0
- ~~save datetimes in UTC~~
- encapsulate attributes if a dao inside the attributes property + add getters and setters
- encapsulate attributes if a dao inside the attributes property
- ~~add getters and setters for dao~~ Implemented in [#538](https://github.com/sequelize/sequelize/pull/538), thanks to iamjochem
- add proper error message everywhere
- refactor validate() output data structure, separating field-specific errors
from general model validator errors (i.e.
`{fields: {field1: ['field1error1']}, model: ['modelError1']}` or similar)
## Collaboration 2.0 ##
......
# v1.7.0 #
- [DEPENDENCIES] Upgraded validator for IPv6 support. [#603](https://github.com/sequelize/sequelize/pull/603). thanks to durango
- [FEATURE] Validate a model before it gets saved. [#601](https://github.com/sequelize/sequelize/pull/601). thanks to durango
- [DEPENDENCIES] replaced underscore by lodash. [#954](https://github.com/sequelize/sequelize/pull/594). thanks to durango
- [BUG] Fix string escape with postgresql on raw SQL queries. [#586](https://github.com/sequelize/sequelize/pull/586). thanks to zanamixx
- [BUG] "order by" is now after "group by". [#585](https://github.com/sequelize/sequelize/pull/585). thanks to mekanics
- [BUG] Added decimal support for min/max. [#583](https://github.com/sequelize/sequelize/pull/583). thanks to durango
- [BUG] Null dates don't break SQLite anymore. [#572](https://github.com/sequelize/sequelize/pull/572). thanks to mweibel
- [BUG] Correctly handle booleans in MySQL. [#608](https://github.com/sequelize/sequelize/pull/608). Thanks to terraflubb
- [BUG] Fixed empty where conditions in MySQL. [#619](https://github.com/sequelize/sequelize/pull/619). Thanks to terraflubb
- [BUG] Allow overriding of default columns. [#635](https://github.com/sequelize/sequelize/pull/635). Thanks to sevastos
- [FEATURE] Validate a model before it gets saved. [#601](https://github.com/sequelize/sequelize/pull/601). thanks to durango
- [FEATURE] Schematics. [#564](https://github.com/sequelize/sequelize/pull/564). thanks to durango
- [FEATURE] Foreign key constraints. [#595](https://github.com/sequelize/sequelize/pull/595). thanks to optilude
- [FEATURE] Support for bulk insert (`<DAOFactory>.bulkCreate()`, update (`<DAOFactory>.update()`) and delete (`<DAOFactory>.destroy()`) [#569](https://github.com/sequelize/sequelize/pull/569). thanks to optilude
- [FEATURE] Add an extra `queryOptions` parameter to `DAOFactory.find` and `DAOFactory.findAll`. This allows a user to specify `{ raw: true }`, meaning that the raw result should be returned, instead of built DAOs. Usefull for queries returning large datasets, see [#611](https://github.com/sequelize/sequelize/pull/611) janmeier
- [FEATURE] Added convenient data types. [#616](https://github.com/sequelize/sequelize/pull/616). Thanks to Costent
- [FEATURE] Binary is more verbose now. [#612](https://github.com/sequelize/sequelize/pull/612). Thanks to terraflubb
- [FEATURE] Promises/A support. [#626](https://github.com/sequelize/sequelize/pull/626). Thanks to kevinbeaty
- [FEATURE] Added Getters/Setters method for DAO. [#538](https://github.com/sequelize/sequelize/pull/538). Thanks to iamjochem
- [FEATURE] Added model wide validations. [#640](https://github.com/sequelize/sequelize/pull/640). Thanks to tremby
# v1.6.0 #
- [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work
......
......@@ -9,7 +9,7 @@
*/
var Sequelize = require(__dirname + "/../../index")
, config = require(__dirname + "/../../test/config")
, config = require(__dirname + "/../../spec/config/config")
, sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
, Person = sequelize.define('Person', { name: Sequelize.STRING })
, Pet = sequelize.define('Pet', { name: Sequelize.STRING })
......
var Sequelize = require(__dirname + "/../../index")
, config = require(__dirname + "/../../test/config")
, config = require(__dirname + "/../../spec/config/config")
, sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
var Person = sequelize.define('Person', { name: Sequelize.STRING })
......
var Sequelize = require(__dirname + "/../../index")
, config = require(__dirname + "/../../test/config")
, config = require(__dirname + "/../../spec/config/config")
, sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
var Person = sequelize.define('Person',
......
var Sequelize = require(__dirname + "/../../index")
, config = require("../../test/config")
, config = require(__dirname + "/../../spec/config/config")
, sequelize = new Sequelize(config.database, config.username, config.password, {logging: false, host: config.host})
, QueryChainer = Sequelize.Utils.QueryChainer
, sys = require("sys")
......
var fs = require("fs")
, Sequelize = require("sequelize")
, sequelize = new Sequelize('sequelize_test', 'root', null, {logging: false})
, Image = sequelize.define('Image', { data: Sequelize.TEXT })
/*
Title: Default values
Image.sync({force: true}).on('success', function() {
console.log("reading image")
var image = fs.readFileSync(__dirname + '/source.png').toString("base64")
console.log("done\n")
This example demonstrates the use of default values for defined model fields. Instead of just specifying the datatype,
you have to pass a hash with a type and a default. You also might want to specify either an attribute can be null or not!
*/
console.log("creating database entry")
Image.create({data: image}).on('success', function(img) {
console.log("done\n")
var Sequelize = require(__dirname + "/../../index")
, config = require(__dirname + "/../../spec/config/config")
, sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
console.log("writing file")
fs.writeFileSync(__dirname + '/target.png', img.data, "base64")
console.log("done\n")
var User = sequelize.define('User', {
name: { type: Sequelize.STRING, allowNull: false},
isAdmin: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false }
})
, user = User.build({ name: 'Someone' })
sequelize.sync({force: true}).on('success', function() {
user.save().on('success', function(user) {
console.log("user.isAdmin should be the default value (false): ", user.isAdmin)
console.log("you might open the file ./target.png")
user.updateAttributes({ isAdmin: true }).on('success', function(user) {
console.log("user.isAdmin was overwritten to true: " + user.isAdmin)
})
})
}).on('failure', function(err) {
console.log(err)
})
\ No newline at end of file
......@@ -5,7 +5,7 @@
*/
var Sequelize = require(__dirname + "/../../index")
, config = require(__dirname + "/../../test/config")
, config = require(__dirname + "/../../spec/config/config")
, sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
// model definition
......
var Sequelize = require(__dirname + "/../../index")
, config = require(__dirname + "/../../test/config")
, config = require(__dirname + "/../../spec/config/config")
, sequelize = new Sequelize(config.database, config.username, config.password, {
// use other database server or port
host: 'my.srv.tld',
......
var Sequelize = require(__dirname + "/../../index")
, config = require(__dirname + "/../../test/config")
, config = require(__dirname + "/../../spec/config/config")
, sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
, Project = sequelize.import(__dirname + "/Project")
, Task = sequelize.import(__dirname + "/Task")
......
......@@ -19,7 +19,15 @@ module.exports = (function() {
whereCollection: null,
schema: null,
schemaDelimiter: ''
}, options || {})
}, options || {})
// error check options
Utils._.each(options.validate, function(validator, validatorType) {
if (Utils._.contains(Utils._.keys(attributes), validatorType))
throw new Error("A model validator function must not have the same name as a field. Model: " + name + ", field/validation name: " + validatorType)
if (!Utils._.isFunction(validator))
throw new Error("Members of the validate option must be functions. Model: " + name + ", error with validate member " + validatorType)
})
this.name = name
if (!this.options.tableName) {
......@@ -30,9 +38,6 @@ module.exports = (function() {
this.rawAttributes = attributes
this.daoFactoryManager = null // defined in init function
this.associations = {}
// extract validation
this.validate = this.options.validate || {}
}
Object.defineProperty(DAOFactory.prototype, 'attributes', {
......@@ -75,11 +80,33 @@ module.exports = (function() {
Util.inherits(this.DAO, DAO);
this.DAO.prototype.rawAttributes = this.rawAttributes;
if (this.options.instanceMethods) {
Utils._.each(this.options.instanceMethods, function(fct, name) {
self.DAO.prototype[name] = fct
})
}
Utils._.each(['Get', 'Set'], function(type) {
var prop = type.toLowerCase(),
opt = prop + 'terMethods',
meth = '__define' + type + 'ter__',
funcs = Utils._.isObject(self.options[opt]) ? self.options[opt] : {}
;
Utils._.each(self.rawAttributes, function(attr, name) {
if (attr.hasOwnProperty(prop))
funcs[name] = attr[prop]
});
Utils._.each(funcs, function(fct, name) {
if (!Utils._.isFunction(fct))
throw new Error(type + 'ter for "' + name + '" is not a function.')
self.DAO.prototype[meth](name, fct);
})
})
this.DAO.prototype.attributes = Object.keys(this.DAO.prototype.rawAttributes);
this.DAO.prototype.booleanValues = [];
......@@ -481,7 +508,9 @@ module.exports = (function() {
}
Utils._.each(defaultAttributes, function(value, attr) {
if (Utils._.isUndefined(self.rawAttributes[attr])) {
self.rawAttributes[attr] = value
}
})
}
......
......@@ -7,28 +7,15 @@ module.exports = (function() {
var DAO = function(values, options, isNewRecord) {
var self = this
this.dataValues = {}
this.__options = options
this.hasPrimaryKeys = options.hasPrimaryKeys
this.selectedValues = values
this.__eagerlyLoadedAssociations = []
initAttributes.call(this, values, isNewRecord)
if (this.hasDefaultValues) {
Utils._.each(this.defaultValues, function (value, name) {
if (typeof self[name] === 'undefined') {
self.addAttribute(name, value());
}
})
}
if (this.booleanValues.length) {
this.booleanValues.forEach(function (name) {
//transform integer 0,1 into boolean
self[name] = !!self[name];
});
}
}
Utils._.extend(DAO.prototype, Mixin.prototype)
Object.defineProperty(DAO.prototype, 'sequelize', {
......@@ -42,7 +29,7 @@ module.exports = (function() {
Object.defineProperty(DAO.prototype, 'isDeleted', {
get: function() {
var result = this.__options.timestamps && this.__options.paranoid
result = result && this[this.__options.underscored ? 'deleted_at' : 'deletedAt'] !== null
result = result && this.dataValues[this.__options.underscored ? 'deleted_at' : 'deletedAt'] !== null
return result
}
......@@ -54,7 +41,10 @@ module.exports = (function() {
, self = this
this.attributes.concat(this.__eagerlyLoadedAssociations).forEach(function(attr) {
result[attr] = self[attr]
result[attr] = self.dataValues.hasOwnProperty(attr)
? self.dataValues[attr]
: self[attr]
;
})
return result
......@@ -67,7 +57,7 @@ module.exports = (function() {
, self = this
Utils._.each(this.__factory.primaryKeys, function(_, attr) {
result[attr] = self[attr]
result[attr] = self.dataValues[attr]
})
return result
......@@ -85,13 +75,21 @@ module.exports = (function() {
}
primaryKeys.forEach(function(identifier) {
result[identifier] = self[identifier]
result[identifier] = self.dataValues[identifier]
})
return result
}
})
DAO.prototype.getDataValue = function(name) {
return this.dataValues && this.dataValues.hasOwnProperty(name) ? this.dataValues[name] : this[name]
}
DAO.prototype.setDataValue = function(name, value) {
this.dataValues[name] = value
}
// if an array with field names is passed to save()
// only those fields will be updated
DAO.prototype.save = function(fields) {
......@@ -111,9 +109,11 @@ module.exports = (function() {
}
}
var tmpVals = self.values
fields.forEach(function(field) {
if (self.values[field] !== undefined) {
values[field] = self.values[field]
if (tmpVals[field] !== undefined) {
values[field] = tmpVals[field]
}
})
}
......@@ -146,8 +146,8 @@ module.exports = (function() {
}
}
if (this.__options.timestamps && this.hasOwnProperty(updatedAtAttr)) {
this[updatedAtAttr] = values[updatedAtAttr] = Utils.now()
if (this.__options.timestamps && this.dataValues.hasOwnProperty(updatedAtAttr)) {
this.dataValues[updatedAtAttr] = values[updatedAtAttr] = Utils.now()
}
var errors = this.validate()
......@@ -269,6 +269,15 @@ module.exports = (function() {
} // if field has validator set
}) // for each field
// for each model validator for this DAO
Utils._.each(self.__options.validate, function(validator, validatorType) {
try {
validator.apply(self)
} catch (err) {
failures[validatorType] = [err.message] // TODO: data structure needs to change for 2.0
}
})
return (Utils._.isEmpty(failures) ? null : failures)
}
......@@ -301,7 +310,7 @@ module.exports = (function() {
DAO.prototype.destroy = function() {
if (this.__options.timestamps && this.__options.paranoid) {
var attr = this.__options.underscored ? 'deleted_at' : 'deletedAt'
this[attr] = new Date()
this.dataValues[attr] = new Date()
return this.save()
} else {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : this.id
......@@ -362,7 +371,37 @@ module.exports = (function() {
}
DAO.prototype.addAttribute = function(attribute, value) {
this[attribute] = value
if (typeof this.dataValues[attribute] !== 'undefined')
return;
if (this.booleanValues.length && this.booleanValues.indexOf(attribute) !== -1) // transform integer 0,1 into boolean
value = !!value;
var has = (function(o) {
var predef = Object.getOwnPropertyDescriptor(o, attribute);
if (predef && predef.hasOwnProperty('value'))
return true; // true here means 'this property exist as a simple value property, do not place setters or getters at all'
return {
get: (predef && predef.hasOwnProperty('get') ? predef.get : null) || o.__lookupGetter__(attribute),
set: (predef && predef.hasOwnProperty('set') ? predef.set : null) || o.__lookupSetter__(attribute)
};
})(this);
// @ node-v0.8.19:
// calling __defineGetter__ destroys any previously defined setters for the attribute in
// question *if* that property setter was defined on the object's prototype (which is what
// we do in dao-factory) ... therefore we need to [re]define both the setter and getter
// here with either the function that already existed OR the default/automatic definition
//
// (the same is true for __defineSetter and 'prototype' getters)
if (has !== true) {
this.__defineGetter__(attribute, has.get || function() { return this.dataValues[attribute]; });
this.__defineSetter__(attribute, has.set || function(v) { this.dataValues[attribute] = v; });
}
this[attribute] = value;
}
DAO.prototype.setValidators = function(attribute, validators) {
......@@ -376,8 +415,13 @@ module.exports = (function() {
// private
var initAttributes = function(values, isNewRecord) {
// set id to null if not passed as value, a newly created dao has no id
var defaults = this.hasPrimaryKeys ? {} : { id: null },
attrs = {},
key;
// add all passed values to the dao and store the attribute names in this.attributes
for (var key in values) {
for (key in values) {
if (values.hasOwnProperty(key)) {
if (typeof values[key] === "string" && !!this.__factory && !!this.__factory.rawAttributes[key] && !!this.__factory.rawAttributes[key].type && !!this.__factory.rawAttributes[key].type.type && this.__factory.rawAttributes[key].type.type === DataTypes.HSTORE.type) {
values[key] = this.QueryInterface.QueryGenerator.toHstore(values[key])
......@@ -387,10 +431,6 @@ module.exports = (function() {
}
}
// set id to null if not passed as value
// a newly created dao has no id
var defaults = this.hasPrimaryKeys ? {} : { id: null }
if (this.__options.timestamps && isNewRecord) {
defaults[this.__options.underscored ? 'created_at' : 'createdAt'] = Utils.now()
defaults[this.__options.underscored ? 'updated_at' : 'updatedAt'] = Utils.now()
......@@ -398,17 +438,38 @@ module.exports = (function() {
if (this.__options.paranoid) {
defaults[this.__options.underscored ? 'deleted_at' : 'deletedAt'] = null
}
if (this.hasDefaultValues) {
Utils._.each(this.defaultValues, function(valueFn, key) {
if (!defaults.hasOwnProperty(key))
defaults[key] = valueFn()
})
}
}
if (Utils._.size(defaults)) {
for (var attr in defaults) {
var value = defaults[attr]
for (key in defaults) {
attrs[key] = Utils.toDefaultValue(defaults[key])
}
}
if (!this.hasOwnProperty(attr)) {
this.addAttribute(attr, Utils.toDefaultValue(value))
Utils._.each(this.attributes, function(key) {
if (!attrs.hasOwnProperty(key)) {
attrs[key] = undefined
}
})
if (values) {
for (key in values) {
if (values.hasOwnProperty(key)) {
attrs[key] = values[key]
}
}
}
for (key in attrs) {
this.addAttribute(key, attrs[key])
}
}
return DAO
......
......@@ -19,7 +19,9 @@ module.exports = (function() {
this.poolCfg = Utils._.defaults(this.config.pool, {
maxConnections: 10,
minConnections: 0,
maxIdleTime: 1000
maxIdleTime: 1000,
handleDisconnects: false,
validate: validateConnection
});
this.pendingQueries = 0;
this.useReplicaton = !!config.replication;
......@@ -83,6 +85,7 @@ module.exports = (function() {
destroy: function(client) {
disconnect.call(self, client)
},
validate: self.poolCfg.validate,
max: self.poolCfg.maxConnections,
min: self.poolCfg.minConnections,
idleTimeoutMillis: self.poolCfg.maxIdleTime
......@@ -98,6 +101,7 @@ module.exports = (function() {
destroy: function(client) {
disconnect.call(self, client)
},
validate: self.poolCfg.validate,
max: self.poolCfg.maxConnections,
min: self.poolCfg.minConnections,
idleTimeoutMillis: self.poolCfg.maxIdleTime
......@@ -115,6 +119,7 @@ module.exports = (function() {
},
max: self.poolCfg.maxConnections,
min: self.poolCfg.minConnections,
validate: self.poolCfg.validate,
idleTimeoutMillis: self.poolCfg.maxIdleTime
})
}
......@@ -247,10 +252,25 @@ module.exports = (function() {
connection.query("SET time_zone = '+0:00'");
// client.setMaxListeners(self.maxConcurrentQueries)
this.isConnecting = false
if (config.pool.handleDisconnects) {
handleDisconnect(this.pool, connection)
}
done(null, connection)
}
var handleDisconnect = function(pool, client) {
client.on('error', function(err) {
if (err.code !== 'PROTOCOL_CONNECTION_LOST') {
throw err
}
pool.destroy(client)
})
}
var validateConnection = function(client) {
return client && client.state != 'disconnected'
}
var enqueue = function(queueItem, options) {
options = options || {}
if (this.activeQueue.length < this.maxConcurrentQueries) {
......
......@@ -186,7 +186,7 @@ module.exports = (function() {
options.attributes = options.attributes || '*'
if (options.include) {
var optAttributes = [options.table + '.*']
var optAttributes = options.attributes === '*' ? [options.table + '.*'] : [options.attributes]
options.include.forEach(function(include) {
var attributes = Object.keys(include.daoFactory.attributes).map(function(attr) {
......
......@@ -193,7 +193,7 @@ module.exports = (function() {
for (var attributeName in attributes) {
attrString.push(Utils._.template('<%= before %> TO <%= after %>')({
before: QueryGenerator.addQuotes(attrBefore),
after: QueryGenerator.addQuotes(attributeName),
after: QueryGenerator.addQuotes(attributeName)
}))
}
......@@ -233,7 +233,7 @@ module.exports = (function() {
options.attributes = options.attributes || '*'
if (options.include) {
var optAttributes = [options.table + '.*']
var optAttributes = options.attributes === '*' ? [options.table + '.*'] : [options.attributes]
options.include.forEach(function(include) {
var attributes = Object.keys(include.daoFactory.attributes).map(function(attr) {
......@@ -259,7 +259,7 @@ module.exports = (function() {
}
if(options.hasOwnProperty('where')) {
options.where = QueryGenerator.getWhereConditions(options.where)
options.where = QueryGenerator.getWhereConditions(options.where, tableName)
query += " WHERE <%= where %>"
}
......@@ -474,14 +474,16 @@ module.exports = (function() {
})
},
getWhereConditions: function(smth) {
getWhereConditions: function(smth, tableName) {
var result = null
if (Utils.isHash(smth)) {
smth = Utils.prependTableNameToHash(tableName, smth)
result = QueryGenerator.hashToWhereConditions(smth)
}
else if (typeof smth === "number") {
result = '\"id\"' + "=" + QueryGenerator.pgEscape(smth)
smth = Utils.prependTableNameToHash(tableName, { id: smth })
result = QueryGenerator.hashToWhereConditions(smth)
}
else if (typeof smth === "string") {
result = smth
......@@ -724,10 +726,16 @@ module.exports = (function() {
}
if (Utils._.includes(dataType, 'SERIAL')) {
if (Utils._.includes(dataType, 'BIGINT')) {
dataType = dataType.replace(/SERIAL/, 'BIGSERIAL')
dataType = dataType.replace(/BIGINT/, '')
tables[tableName][attr] = 'bigserial'
} else {
dataType = dataType.replace(/INTEGER/, '')
dataType = dataType.replace(/NOT NULL/, '')
tables[tableName][attr] = 'serial'
}
dataType = dataType.replace(/NOT NULL/, '')
}
if (dataType.match(/^ENUM\(/)) {
dataType = dataType.replace(/^ENUM\(.+\)/, QueryGenerator.pgEscapeAndQuote("enum_" + tableName + "_" + attr))
......
......@@ -24,8 +24,17 @@ module.exports = (function() {
var QueryGenerator = {
options: {},
addQuotes: function(s, quoteChar) {
return Utils.addTicks(s, quoteChar)
removeQuotes: function (s, quoteChar) {
quoteChar = quoteChar || '`'
return s.replace(new RegExp(quoteChar, 'g'), '')
},
addQuotes: function (s, quoteChar) {
quoteChar = quoteChar || '`'
return QueryGenerator.removeQuotes(s, quoteChar)
.split('.')
.map(function(e) { return quoteChar + String(e) + quoteChar })
.join('.')
},
addSchema: function(opts) {
......@@ -76,6 +85,10 @@ module.exports = (function() {
if (attributes.hasOwnProperty(attr)) {
var dataType = attributes[attr]
if (Utils._.includes(dataType, 'AUTOINCREMENT')) {
dataType = dataType.replace(/BIGINT/, 'INTEGER')
}
if (Utils._.includes(dataType, 'PRIMARY KEY') && needsMultiplePrimaryKeys) {
primaryKeys.push(attr)
attrStr.push(Utils.addTicks(attr) + " " + dataType.replace(/PRIMARY KEY/, 'NOT NULL'))
......@@ -145,6 +158,74 @@ module.exports = (function() {
return Utils._.template(query)(replacements)
},
selectQuery: function(tableName, options) {
var table = null,
joinQuery = ""
options = options || {}
options.table = table = Array.isArray(tableName) ? tableName.map(function(tbl){ return QueryGenerator.addQuotes(tbl) }).join(", ") : QueryGenerator.addQuotes(tableName)
options.attributes = options.attributes && options.attributes.map(function(attr){
if(Array.isArray(attr) && attr.length == 2) {
return [attr[0], QueryGenerator.addQuotes(attr[1])].join(' as ')
} else {
return attr.indexOf(Utils.TICK_CHAR) < 0 ? QueryGenerator.addQuotes(attr) : attr
}
}).join(", ")
options.attributes = options.attributes || '*'
if (options.include) {
var optAttributes = options.attributes === '*' ? [options.table + '.*'] : [options.attributes]
options.include.forEach(function(include) {
var attributes = Object.keys(include.daoFactory.attributes).map(function(attr) {
return "`" + include.as + "`.`" + attr + "` AS `" + include.as + "." + attr + "`"
})
optAttributes = optAttributes.concat(attributes)
var table = include.daoFactory.tableName
var as = include.as
var tableLeft = ((include.association.associationType === 'BelongsTo') ? include.as : tableName)
var attrLeft = 'id'
var tableRight = ((include.association.associationType === 'BelongsTo') ? tableName : include.as)
var attrRight = include.association.identifier
joinQuery += " LEFT OUTER JOIN `" + table + "` AS `" + as + "` ON `" + tableLeft + "`.`" + attrLeft + "` = `" + tableRight + "`.`" + attrRight + "`"
})
options.attributes = optAttributes.join(', ')
}
var query = "SELECT " + options.attributes + " FROM " + options.table
query += joinQuery
if (options.hasOwnProperty('where')) {
options.where = this.getWhereConditions(options.where, tableName)
query += " WHERE " + options.where
}
if (options.group) {
options.group = Array.isArray(options.group) ? options.group.map(function(grp){return QueryGenerator.addQuotes(grp)}).join(', ') : QueryGenerator.addQuotes(options.group)
query += " GROUP BY " + options.group
}
if (options.order) {
query += " ORDER BY " + options.order
}
if (options.limit && !(options.include && (options.limit === 1))) {
if (options.offset) {
query += " LIMIT " + options.offset + ", " + options.limit
} else {
query += " LIMIT " + options.limit
}
}
query += ";"
return query
},
updateQuery: function(tableName, attrValueHash, where) {
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull)
......
var util = require("util")
, EventEmitter = require("events").EventEmitter
, Promise = require("promise")
, proxyEventKeys = ['success', 'error', 'sql']
var bindToProcess = function(fct) {
if (fct) {
if(process.domain) {
return process.domain.bind(fct);
}
}
return fct;
};
module.exports = (function() {
var CustomEventEmitter = function(fct) {
this.fct = fct
this.fct = bindToProcess(fct);
}
util.inherits(CustomEventEmitter, EventEmitter)
......@@ -21,7 +33,7 @@ module.exports = (function() {
CustomEventEmitter.prototype.success =
CustomEventEmitter.prototype.ok =
function(fct) {
this.on('success', fct)
this.on('success', bindToProcess(fct))
return this
}
......@@ -29,13 +41,14 @@ module.exports = (function() {
CustomEventEmitter.prototype.fail =
CustomEventEmitter.prototype.error =
function(fct) {
this.on('error', fct)
this.on('error', bindToProcess(fct))
return this;
}
CustomEventEmitter.prototype.done =
CustomEventEmitter.prototype.complete =
function(fct) {
fct = bindToProcess(fct);
this.on('error', function(err) { fct(err, null) })
.on('success', function(result) { fct(null, result) })
return this
......@@ -49,6 +62,18 @@ module.exports = (function() {
}.bind(this))
}
CustomEventEmitter.prototype.then =
function (onFulfilled, onRejected) {
var self = this
onFulfilled = bindToProcess(onFulfilled)
onRejected = bindToProcess(onRejected)
return new Promise(function (resolve, reject) {
self.on('error', reject)
.on('success', resolve);
}).then(onFulfilled, onRejected)
}
return CustomEventEmitter;
})()
......@@ -47,7 +47,8 @@ module.exports = (function() {
var urlParts
options = options || {}
if (arguments.length === 1) {
if (arguments.length === 1 || (arguments.length === 2 && typeof username === 'object')) {
options = username || {}
urlParts = url.parse(arguments[0])
database = urlParts.path.replace(/^\//, '')
dialect = urlParts.protocol
......
......@@ -35,16 +35,17 @@
"validator": "1.1.1",
"moment": "~1.7.0",
"commander": "~0.6.0",
"generic-pool": "1.0.9",
"dottie": "0.0.6-1",
"toposort-class": "0.1.4"
"toposort-class": "0.1.4",
"generic-pool": "2.0.3",
"promise": "~3.0.0"
},
"devDependencies": {
"jasmine-node": "1.5.0",
"sqlite3": "~2.1.5",
"mysql": "~2.0.0-alpha7",
"pg": "~0.10.2",
"buster": "~0.6.0",
"buster": "~0.6.3",
"watchr": "~2.2.0",
"yuidocjs": "~0.3.36"
},
......
......@@ -121,16 +121,16 @@ describe('QueryGenerator', function() {
expectation: "SELECT \"id\", \"name\" FROM \"myTable\";"
}, {
arguments: ['myTable', {where: {id: 2}}],
expectation: "SELECT * FROM \"myTable\" WHERE \"id\"=2;"
expectation: "SELECT * FROM \"myTable\" WHERE \"myTable\".\"id\"=2;"
}, {
arguments: ['myTable', {where: {name: 'foo'}}],
expectation: "SELECT * FROM \"myTable\" WHERE \"name\"='foo';"
expectation: "SELECT * FROM \"myTable\" WHERE \"myTable\".\"name\"='foo';"
}, {
arguments: ['myTable', {where: {name: "foo';DROP TABLE myTable;"}}],
expectation: "SELECT * FROM \"myTable\" WHERE \"name\"='foo'';DROP TABLE myTable;';"
expectation: "SELECT * FROM \"myTable\" WHERE \"myTable\".\"name\"='foo'';DROP TABLE myTable;';"
}, {
arguments: ['myTable', {where: 2}],
expectation: "SELECT * FROM \"myTable\" WHERE \"id\"=2;"
expectation: "SELECT * FROM \"myTable\" WHERE \"myTable\".\"id\"=2;"
}, {
arguments: ['foo', { attributes: [['count(*)', 'count']] }],
expectation: 'SELECT count(*) as \"count\" FROM \"foo\";'
......@@ -164,7 +164,7 @@ describe('QueryGenerator', function() {
expectation: "SELECT * FROM \"mySchema\".\"myTable\";"
}, {
arguments: ['mySchema.myTable', {where: {name: "foo';DROP TABLE mySchema.myTable;"}}],
expectation: "SELECT * FROM \"mySchema\".\"myTable\" WHERE \"name\"='foo'';DROP TABLE mySchema.myTable;';"
expectation: "SELECT * FROM \"mySchema\".\"myTable\" WHERE \"mySchema\".\"myTable\".\"name\"='foo'';DROP TABLE mySchema.myTable;';"
}
],
......
......@@ -70,6 +70,34 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
})
}.bind(this), 'Invalid DAO definition. Only one autoincrement field allowed.')
})
it('throws an error if a custom model-wide validation is not a function', function() {
Helpers.assertException(function() {
this.sequelize.define('Foo', {
field: {
type: Sequelize.INTEGER
}
}, {
validate: {
notFunction: 33
}
})
}.bind(this), 'Members of the validate option must be functions. Model: Foo, error with validate member notFunction')
})
it('throws an error if a custom model-wide validation has the same name as a field', function() {
Helpers.assertException(function() {
this.sequelize.define('Foo', {
field: {
type: Sequelize.INTEGER
}
}, {
validate: {
field: function() {}
}
})
}.bind(this), 'A model validator function must not have the same name as a field. Model: Foo, field/validation name: field')
})
})
describe('build', function() {
......@@ -100,6 +128,82 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
var user = this.User.build({ username: 'John Wayne' })
expect(user.selectedValues).toEqual({ username: 'John Wayne' })
})
it("attaches getter and setter methods from attribute definition", function() {
var Product = this.sequelize.define('ProductWithSettersAndGetters1', {
price: {
type: Sequelize.INTEGER,
get : function() {
return 'answer = ' + this.getDataValue('price');
},
set : function(v) {
return this.setDataValue('price', v + 42);
}
}
},{
});
expect(Product.build({price: 42}).price).toEqual('answer = 84');
var p = Product.build({price: 1});
expect(p.price).toEqual('answer = 43');
p.price = 0;
expect(p.price).toEqual('answer = 42'); // ah finally the right answer :-)
})
it("attaches getter and setter methods from options", function() {
var Product = this.sequelize.define('ProductWithSettersAndGetters2', {
priceInCents: {
type: Sequelize.INTEGER
}
},{
setterMethods: {
price: function(value) {
this.dataValues.priceInCents = value * 100;
}
},
getterMethods: {
price: function() {
return '$' + (this.getDataValue('priceInCents') / 100);
},
priceInCents: function() {
return this.dataValues.priceInCents;
}
}
});
expect(Product.build({price: 20}).priceInCents).toEqual(20 * 100);
expect(Product.build({priceInCents: 30 * 100}).price).toEqual('$' + 30);
})
it("attaches getter and setter methods from options only if not defined in attribute", function() {
var Product = this.sequelize.define('ProductWithSettersAndGetters3', {
price1: {
type: Sequelize.INTEGER,
set : function(v) { this.setDataValue('price1', v * 10); }
},
price2: {
type: Sequelize.INTEGER,
get : function(v) { return this.getDataValue('price2') * 10; }
}
},{
setterMethods: {
price1: function(v) { this.setDataValue('price1', v * 100); }
},
getterMethods: {
price2: function() { return '$' + this.getDataValue('price2'); }
}
});
var p = Product.build({ price1: 1, price2: 2 });
expect(p.price1).toEqual(10);
expect(p.price2).toEqual(20);
})
})
describe('findOrCreate', function () {
......@@ -763,6 +867,37 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}.bind(this))
})
it('returns the selected fields and all fields of the included table as instance.selectedValues', function(done) {
this.Mission = this.sequelize.define('Mission', {
title: {type: Sequelize.STRING, defaultValue: 'a mission!!'},
foo: {type: Sequelize.INTEGER, defaultValue: 2},
})
this.Mission.belongsTo(this.User)
this.User.hasMany(this.Mission)
this.sequelize.sync({ force: true }).complete(function() {
this.Mission.create()
.success(function(mission) {
this.User.create({
username: 'John DOE'
}).success(function(user) {
mission.setUser(user)
.success(function() {
this.User.find({
where: { username: 'John DOE' },
attributes: ['username'],
include: [this.Mission]
}).success(function(user) {
expect(user.selectedValues).toEqual({ username: 'John DOE' })
done()
})
}.bind(this))
}.bind(this))
}.bind(this))
}.bind(this))
})
it('always honors ZERO as primary key', function(_done) {
var permutations = [
0,
......@@ -1532,6 +1667,10 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
expect(self.UserSpecialSync.getTableName()).toEqual('"special"."UserSpecials"');
expect(UserSpecial.indexOf('INSERT INTO "special"."UserSpecials"')).toBeGreaterThan(-1)
expect(UserPublic.indexOf('INSERT INTO "UserPublics"')).toBeGreaterThan(-1)
} else if (dialect === "sqlite") {
expect(self.UserSpecialSync.getTableName()).toEqual('`special`.`UserSpecials`');
expect(UserSpecial.indexOf('INSERT INTO `special.UserSpecials`')).toBeGreaterThan(-1)
expect(UserPublic.indexOf('INSERT INTO `UserPublics`')).toBeGreaterThan(-1)
} else {
expect(self.UserSpecialSync.getTableName()).toEqual('`special.UserSpecials`');
expect(UserSpecial.indexOf('INSERT INTO `special.UserSpecials`')).toBeGreaterThan(-1)
......
......@@ -282,5 +282,58 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() {
var successfulUser = User.build({ name : "2" })
expect(successfulUser.validate()).toBeNull()
})
it('skips other validations if allowNull is true and the value is null', function() {
var User = this.sequelize.define('User' + Math.random(), {
age: {
type: Sequelize.INTEGER,
allowNull: true,
validate: {
min: { args: 0, msg: 'must be positive' }
}
}
})
var failingUser = User.build({ age: -1 })
, errors = failingUser.validate()
expect(errors).not.toBeNull(null)
expect(errors).toEqual({ age: ['must be positive'] })
var successfulUser1 = User.build({ age: null })
expect(successfulUser1.validate()).toBeNull()
var successfulUser2 = User.build({ age: 1 })
expect(successfulUser2.validate()).toBeNull()
})
it('validates a model with custom model-wide validation methods', function() {
var Foo = this.sequelize.define('Foo' + Math.random(), {
field1: {
type: Sequelize.INTEGER,
allowNull: true
},
field2: {
type: Sequelize.INTEGER,
allowNull: true
}
}, {
validate: {
xnor: function() {
if ((this.field1 === null) === (this.field2 === null)) {
throw new Error('xnor failed');
}
}
}
})
var failingFoo = Foo.build({ field1: null, field2: null })
, errors = failingFoo.validate()
expect(errors).not.toBeNull()
expect(errors).toEqual({ 'xnor': ['xnor failed'] })
var successfulFoo = Foo.build({ field1: 33, field2: null })
expect(successfulFoo.validate()).toBeNull()
})
})
})
if (typeof require === 'function') {
const buster = require("buster")
, Helpers = require('./buster-helpers')
, dialect = Helpers.getTestDialect()
, _ = require('lodash')
}
buster.spec.expose()
describe(Helpers.getTestDialectTeaser("Promise"), function () {
before(function (done) {
var self = this
Helpers.initTests({
dialect: dialect,
beforeComplete: function (sequelize, DataTypes) {
self.sequelize = sequelize
self.User = sequelize.define('User', {
username: { type: DataTypes.STRING },
touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
aNumber: { type: DataTypes.INTEGER },
bNumber: { type: DataTypes.INTEGER },
validateTest: {
type: DataTypes.INTEGER,
allowNull: true,
validate: {isInt: true}
},
validateCustom: {
type: DataTypes.STRING,
allowNull: true,
validate: {len: {msg: 'Length failed.', args: [1, 20]}}
},
dateAllowNullTrue: {
type: DataTypes.DATE,
allowNull: true
}
})
self.HistoryLog = sequelize.define('HistoryLog', {
someText: { type: DataTypes.STRING },
aNumber: { type: DataTypes.INTEGER },
aRandomId: { type: DataTypes.INTEGER }
})
self.ParanoidUser = sequelize.define('ParanoidUser', {
username: { type: DataTypes.STRING }
}, {
paranoid: true
})
self.ParanoidUser.hasOne(self.ParanoidUser)
},
onComplete: function () {
self.User.sync({ force: true }).then(function () {
return self.HistoryLog.sync({ force: true })
}).then(function () {
return self.ParanoidUser.sync({force: true })
})
.then(function () {done()}, done)
}
})
})
describe('increment', function () {
before(function (done) {
this.User.create({ id: 1, aNumber: 0, bNumber: 0 }).done(done)
});
it('with array', function (done) {
var self = this;
// Select something
this.User.find(1).then(function (user1) {
return user1.increment(['aNumber'], 2)
}).then(function (user2) {
return self.User.find(1)
}).then(function (user3) {
expect(user3.aNumber).toBe(2);
done();
}, done);
});
it('should still work right with other concurrent updates', function (done) {
var self = this;
// Select something
this.User.find(1).then(function (user1) {
// Select the user again (simulating a concurrent query)
return self.User.find(1).then(function (user2) {
return user2.updateAttributes({
aNumber: user2.aNumber + 1
}).then(function (user3) {
return user1.increment(['aNumber'], 2)
}).then(function (user4) {
return self.User.find(1)
}).then(function (user5) {
expect(user5.aNumber).toBe(3);
done();
}, done)
});
});
});
it('with key value pair', function (done) {
var self = this;
// Select something
this.User.find(1).then(function (user1) {
return user1.increment({ 'aNumber': 1, 'bNumber': 2})
}).then(function () {
return self.User.find(1)
}).then(function (user3) {
expect(user3.aNumber).toBe(1);
expect(user3.bNumber).toBe(2);
done();
}, done);
});
});
describe('decrement', function () {
before(function (done) {
this.User.create({ id: 1, aNumber: 0, bNumber: 0 }).done(done)
});
it('with array', function (done) {
var self = this;
// Select something
this.User.find(1).then(function (user1) {
return user1.decrement(['aNumber'], 2)
}).then(function () {
return self.User.find(1);
}).then(function (user3) {
expect(user3.aNumber).toBe(-2);
done();
}, done);
});
it('with single field', function (done) {
var self = this;
// Select something
this.User.find(1).then(function (user1) {
return user1.decrement(['aNumber'], 2)
}).then(function () {
return self.User.find(1);
}).then(function (user3) {
expect(user3.aNumber).toBe(-2);
done();
}, done);
});
it('should still work right with other concurrent decrements', function (done) {
var self = this;
// Select something
this.User.find(1).then(function (user1) {
var _done = _.after(3, function () {
self.User.find(1).then(function (user2) {
expect(user2.aNumber).toEqual(-6);
done();
})
});
user1.decrement(['aNumber'], 2).done(_done);
user1.decrement(['aNumber'], 2).done(_done);
user1.decrement(['aNumber'], 2).done(_done);
});
});
});
describe('reload', function () {
it("should return a reference to the same DAO instead of creating a new one", function (done) {
this.User.create({ username: 'John Doe' }).then(function (originalUser) {
return originalUser.updateAttributes({ username: 'Doe John' }).then(function () {
return originalUser.reload()
}).then(function (updatedUser) {
expect(originalUser === updatedUser).toBeTrue()
done();
}, done)
})
})
it("should update the values on all references to the DAO", function (done) {
var self = this
this.User.create({ username: 'John Doe' }).then(function (originalUser) {
return self.User.find(originalUser.id).then(function (updater) {
return updater.updateAttributes({ username: 'Doe John' })
}).then(function () {
// We used a different reference when calling updateAttributes, so originalUser is now out of sync
expect(originalUser.username).toEqual('John Doe')
return originalUser.reload()
}).then(function (updatedUser) {
expect(originalUser.username).toEqual('Doe John')
expect(updatedUser.username).toEqual('Doe John')
done();
}, done)
})
})
it("should update the associations as well", function (done) {
var Book = this.sequelize.define('Book', { title: Helpers.Sequelize.STRING })
, Page = this.sequelize.define('Page', { content: Helpers.Sequelize.TEXT })
Book.hasMany(Page)
Page.belongsTo(Book)
this.sequelize.sync({ force: true }).then(function () {
return Book.create({ title: 'A very old book' })
}).then(function (book) {
return Page.create({ content: 'om nom nom' }).then(function (page) {
return book.setPages([ page ]).then(function () {
return Book.find({
where: (dialect === 'postgres' ? '"Books"."id"=' : '`Books`.`id`=') + book.id,
include: [Page]
}).then(function (leBook) {
return page.updateAttributes({ content: 'something totally different' }).then(function (page) {
expect(leBook.pages[0].content).toEqual('om nom nom')
expect(page.content).toEqual('something totally different')
return leBook.reload().then(function (leBook) {
expect(leBook.pages[0].content).toEqual('something totally different')
expect(page.content).toEqual('something totally different')
done()
})
})
})
})
})
}, done)
})
});
describe('complete', function () {
it("gets triggered if an error occurs", function (done) {
this.User.find({ where: "asdasdasd" }).then(null, function (err) {
expect(err).toBeDefined()
expect(err.message).toBeDefined()
done()
})
})
it("gets triggered if everything was ok", function (done) {
this.User.count().then(function (result) {
expect(result).toBeDefined()
done()
})
})
})
describe('save', function () {
it('should fail a validation upon creating', function (done) {
this.User.create({aNumber: 0, validateTest: 'hello'}).then(null, function (err) {
expect(err).toBeDefined()
expect(err).toBeObject()
expect(err.validateTest).toBeArray()
expect(err.validateTest[0]).toBeDefined()
expect(err.validateTest[0].indexOf('Invalid integer')).toBeGreaterThan(-1);
done();
});
})
it('should fail a validation upon building', function (done) {
this.User.build({aNumber: 0, validateCustom: 'aaaaaaaaaaaaaaaaaaaaaaaaaa'}).save()
.then(null, function (err) {
expect(err).toBeDefined()
expect(err).toBeObject()
expect(err.validateCustom).toBeDefined()
expect(err.validateCustom).toBeArray()
expect(err.validateCustom[0]).toBeDefined()
expect(err.validateCustom[0]).toEqual('Length failed.')
done()
})
})
it('should fail a validation when updating', function (done) {
this.User.create({aNumber: 0}).then(function (user) {
return user.updateAttributes({validateTest: 'hello'})
}).then(null, function (err) {
expect(err).toBeDefined()
expect(err).toBeObject()
expect(err.validateTest).toBeDefined()
expect(err.validateTest).toBeArray()
expect(err.validateTest[0]).toBeDefined()
expect(err.validateTest[0].indexOf('Invalid integer:')).toBeGreaterThan(-1)
done()
})
})
})
})
......@@ -194,5 +194,29 @@ describe(Helpers.getTestDialectTeaser("Sequelize"), function() {
})
})
})
describe('table', function() {
[
{ id: { type: Helpers.Sequelize.BIGINT } },
{ id: { type: Helpers.Sequelize.STRING, allowNull: true } },
{ id: { type: Helpers.Sequelize.BIGINT, allowNull: false, primaryKey: true, autoIncrement: true } }
].forEach(function(customAttributes) {
it('should be able to override options on the default attributes', function(done) {
var Picture = this.sequelize.define('picture', Helpers.Sequelize.Utils._.cloneDeep(customAttributes))
Picture.sync({ force: true }).success(function() {
Object.keys(customAttributes).forEach(function(attribute) {
Object.keys(customAttributes[attribute]).forEach(function(option) {
var optionValue = customAttributes[attribute][option];
expect(Picture.rawAttributes[attribute][option]).toBe(optionValue)
});
})
done()
})
})
})
})
})
})
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!