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

Commit 70f3cded by Jochem Maas

Merge remote-tracking branch 'upstream/master' into feature/findAndCountSugar

2 parents 3ef54244 eaefe44c
Showing with 4920 additions and 689 deletions
...@@ -4,4 +4,4 @@ test*.js ...@@ -4,4 +4,4 @@ test*.js
.DS_STORE .DS_STORE
node_modules node_modules
npm-debug.log npm-debug.log
*~ *~
\ No newline at end of file
{
"globals": {
"jasmine": false,
"spyOn": false,
"it": false,
"console": false,
"describe": false,
"expect": false,
"beforeEach": false,
"waits": false,
"waitsFor": false,
"runs": false
},
"camelcase": true,
"curly": true,
"forin": true,
"indent": 2,
"unused": true,
"asi": true,
"evil": false,
"laxcomma": true
}
\ No newline at end of file
...@@ -19,5 +19,4 @@ env: ...@@ -19,5 +19,4 @@ env:
language: node_js language: node_js
node_js: node_js:
- 0.8 - 0.8
\ No newline at end of file
...@@ -2,6 +2,9 @@ ...@@ -2,6 +2,9 @@
The Sequelize library provides easy access to MySQL, SQLite or PostgreSQL databases by mapping database entries to objects and vice versa. To put it in a nutshell... it's an ORM (Object-Relational-Mapper). The library is written entirely in JavaScript and can be used in the Node.JS environment. The Sequelize library provides easy access to MySQL, SQLite or PostgreSQL databases by mapping database entries to objects and vice versa. To put it in a nutshell... it's an ORM (Object-Relational-Mapper). The library is written entirely in JavaScript and can be used in the Node.JS environment.
<a href="http://flattr.com/thing/1259407/Sequelize" target="_blank">
<img src="http://api.flattr.com/button/flattr-badge-large.png" alt="Flattr this" title="Flattr this" border="0" /></a>
## Important Notes ## ## Important Notes ##
### 1.6.0 ### ### 1.6.0 ###
...@@ -27,7 +30,7 @@ The Sequelize library provides easy access to MySQL, SQLite or PostgreSQL databa ...@@ -27,7 +30,7 @@ The Sequelize library provides easy access to MySQL, SQLite or PostgreSQL databa
- Associations - Associations
- Importing definitions from single files - 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). 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). If you want to know about latest development and releases, follow me on [Twitter](http://twitter.com/sdepold).
...@@ -35,29 +38,31 @@ Also make sure to take a look at the examples in the repository. The website wil ...@@ -35,29 +38,31 @@ Also make sure to take a look at the examples in the repository. The website wil
- [Documentation](http://www.sequelizejs.com) - [Documentation](http://www.sequelizejs.com)
- [Twitter](http://twitter.com/sdepold) - [Twitter](http://twitter.com/sdepold)
- [IRC](irc://irc.freenode.net/sequelizejs) - [IRC](http://webchat.freenode.net?channels=sequelizejs)
- [Google Groups](https://groups.google.com/forum/#!forum/sequelize) - [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) - [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 ## Roadmap
A very basic roadmap. Chances aren't too bad, that not mentioned things are implemented as well. Don't panic :) A very basic roadmap. Chances aren't too bad, that not mentioned things are implemented as well. Don't panic :)
### 1.6.0 (ToDo)
- ~~Fix last issues with eager loading of associated data~~
- ~~Find out why Person.belongsTo(House) would add person_id to house. It should add house_id to person~~
### 1.7.0 ### 1.7.0
- Check if lodash is a proper alternative to current underscore usage. - ~~Check if lodash is a proper alternative to current underscore usage.~~
- Transactions - Transactions
- Support for update of tables without primary key - Support for update of tables without primary key
- MariaDB support - MariaDB support
- Support for update and delete calls for whole tables without previous loading of instances - ~~Support for update and delete calls for whole tables without previous loading of instances~~ Implemented in [#569](https://github.com/sequelize/sequelize/pull/569) thanks to @optiltude
- Eager loading of nested associations [#388](https://github.com/sdepold/sequelize/issues/388#issuecomment-12019099) - Eager loading of nested associations [#388](https://github.com/sdepold/sequelize/issues/388#issuecomment-12019099)
- Model#delete - Model#delete
- Validate a model before it gets saved. (Move validation of enum attribute value to validate method) - ~~Validate a model before it gets saved.~~ Implemented in [#601](https://github.com/sequelize/sequelize/pull/601), thanks to @durango
- BLOB [#99](https://github.com/sdepold/sequelize/issues/99) - Move validation of enum attribute value to validate method
- Support for foreign keys - BLOB [#99](https://github.com/sequelize/sequelize/issues/99)
- ~~Support for foreign keys~~ Implemented in [#595](https://github.com/sequelize/sequelize/pull/595), thanks to @optilude
### 1.7.x ### 1.7.x
- Complete support for non-id primary keys - Complete support for non-id primary keys
...@@ -70,7 +75,13 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl ...@@ -70,7 +75,13 @@ A very basic roadmap. Chances aren't too bad, that not mentioned things are impl
### 2.0.0 ### 2.0.0
- ~~save datetimes in UTC~~ - ~~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 ## ## Collaboration 2.0 ##
...@@ -234,4 +245,4 @@ for (var key in obj) { ...@@ -234,4 +245,4 @@ for (var key in obj) {
The automated tests we talk about just so much are running on The automated tests we talk about just so much are running on
[Travis public CI](http://travis-ci.org), here is its status: [Travis public CI](http://travis-ci.org), here is its status:
[![Build Status](https://secure.travis-ci.org/sdepold/sequelize.png)](http://travis-ci.org/sdepold/sequelize) [![Build Status](https://secure.travis-ci.org/sequelize/sequelize.png)](http://travis-ci.org/sequelize/sequelize)
...@@ -8,6 +8,7 @@ const path = require("path") ...@@ -8,6 +8,7 @@ const path = require("path")
, _ = Sequelize.Utils._ , _ = Sequelize.Utils._
var configPath = process.cwd() + '/config' var configPath = process.cwd() + '/config'
, environment = process.env.NODE_ENV || 'development'
, migrationsPath = process.cwd() + '/migrations' , migrationsPath = process.cwd() + '/migrations'
, packageJsonPath = __dirname + '/../package.json' , packageJsonPath = __dirname + '/../package.json'
, packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString()) , packageJson = JSON.parse(fs.readFileSync(packageJsonPath).toString())
...@@ -50,34 +51,52 @@ var createMigrationsFolder = function(force) { ...@@ -50,34 +51,52 @@ var createMigrationsFolder = function(force) {
} }
var readConfig = function() { var readConfig = function() {
var config
try { try {
var config = JSON.parse(fs.readFileSync(configFile)) config = fs.readFileSync(configFile)
, env = process.env.NODE_ENV || 'development' } catch(e) {
throw new Error('Error reading "config/config.json".')
}
if (config[env]) { try {
config = config[env] config = JSON.parse(config)
} } catch (e) {
throw new Error('Error parsing "config/config.json" as JSON.')
}
return config if (config[environment]) {
} catch(e) { config = config[environment]
throw new Error('The config.json is not available or contains invalid JSON.')
} }
return config
} }
program program
.version(packageJson.version) .version(packageJson.version)
.option('-i, --init', 'Initializes the project. Creates a config/config.json') .option('-i, --init', 'Initializes the project.')
.option('-m, --migrate', 'Runs undone migrations') .option('-e, --env <environment>', 'Specify the environment.')
.option('-m, --migrate', 'Run pending migrations.')
.option('-u, --undo', 'Undo the last migration.') .option('-u, --undo', 'Undo the last migration.')
.option('-f, --force', 'Forces the action to be done.') .option('-f, --force', 'Forces the action to be done.')
.option('-c, --create-migration [migration-name]', 'Create a new migration skeleton file.') .option('-c, --create-migration [migration-name]', 'Creates a new migration.')
.parse(process.argv) .parse(process.argv)
if(typeof program.env === 'string') {
environment = program.env
}
console.log("Using environment '" + environment + "'.")
if(program.migrate) { if(program.migrate) {
if(configFileExists) { if(configFileExists) {
var config = readConfig() var config
, options = {} , options = {}
try {
config = readConfig()
} catch(e) {
console.log(e.message)
process.exit(1)
}
_.each(config, function(value, key) { _.each(config, function(value, key) {
if(['database', 'username', 'password'].indexOf(key) == -1) { if(['database', 'username', 'password'].indexOf(key) == -1) {
options[key] = value options[key] = value
...@@ -104,7 +123,8 @@ if(program.migrate) { ...@@ -104,7 +123,8 @@ if(program.migrate) {
sequelize.migrate() sequelize.migrate()
} }
} else { } else {
throw new Error('Please add a configuration file under config/config.json. You might run "sequelize --init".') console.log('Cannot find "config/config.json". Have you run "sequelize --init"?')
process.exit(1)
} }
} else if(program.init) { } else if(program.init) {
if(!configFileExists || !!program.force) { if(!configFileExists || !!program.force) {
...@@ -129,9 +149,10 @@ if(program.migrate) { ...@@ -129,9 +149,10 @@ if(program.migrate) {
} }
}) })
console.log('Successfully created config.json') console.log('Created "config/config.json"')
} else { } else {
console.log('A config.json already exists. Run "sequelize --init --force" to overwrite it.') console.log('"config/config.json" already exists. Run "sequelize --init --force" to overwrite.')
process.exit(1)
} }
createMigrationsFolder(program.force) createMigrationsFolder(program.force)
...@@ -140,21 +161,23 @@ if(program.migrate) { ...@@ -140,21 +161,23 @@ if(program.migrate) {
var migrationName = [ var migrationName = [
moment().format('YYYYMMDDHHmmss'), moment().format('YYYYMMDDHHmmss'),
(typeof program.createMigration == 'string') ? program.createMigration : 'unnamed-migration' (typeof program.createMigration === 'string') ? program.createMigration : 'unnamed-migration'
].join('-') + '.js' ].join('-') + '.js'
var migrationContent = [ var migrationContent = [
"module.exports = {", "module.exports = {",
" up: function(migration, DataTypes) {", " up: function(migration, DataTypes, done) {",
" // add altering commands here", " // add altering commands here, calling 'done' when finished",
" done()",
" },", " },",
" down: function(migration) {", " down: function(migration, DataTypes, done) {",
" // add reverting commands here", " // add reverting commands here, calling 'done' when finished",
" done()",
" }", " }",
"}" "}"
].join('\n') ].join('\n')
fs.writeFileSync(migrationsPath + '/' + migrationName, migrationContent) fs.writeFileSync(migrationsPath + '/' + migrationName, migrationContent)
} else { } else {
console.log('Please define any params!') console.log('Try "sequelize --help" for usage information.')
} }
# v1.7.0 #
- [DEPENDENCIES] Upgraded validator for IPv6 support. [#603](https://github.com/sequelize/sequelize/pull/603). 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
- [BUG] Fix where params for belongsTo [#658](https://github.com/sequelize/sequelize/pull/658). Thanks to mweibel
- [BUG] Default ports are now declared in the connector manager, which means the default port for PG correctly becomes 5432. [#633](https://github.com/sequelize/sequelize/issues/633)
- [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
- [FEATURE] `findOrCreate` now returns an additional flag (`created`), that is true if a model was created, and false if it was found [#648](https://github.com/sequelize/sequelize/pull/648). janmeier
- [FEATURE] Field and table comments for MySQL and PG. [#523](https://github.com/sequelize/sequelize/pull/523). MySQL by iamjochen. PG by janmeier
- [FEATURE] BigInts can now be used for autoincrement/serial columns. [#673](https://github.com/sequelize/sequelize/pull/673). thanks to sevastos
# v1.6.0 # # v1.6.0 #
- [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work - [DEPENDENCIES] upgrade mysql to alpha7. You *MUST* use this version or newer for DATETIMEs to work
- [DEPENDENCIES] upgraded most dependencies. most important: mysql was upgraded to 2.0.0-alpha-3 - [DEPENDENCIES] upgraded most dependencies. most important: mysql was upgraded to 2.0.0-alpha-3
......
...@@ -5,11 +5,11 @@ ...@@ -5,11 +5,11 @@
First of all, Person is getting associated via many-to-many with other Person objects (e.g. Person.hasMany('brothers')). First of all, Person is getting associated via many-to-many with other Person objects (e.g. Person.hasMany('brothers')).
Afterwards a Person becomes associated with a 'father' and a mother using a one-to-one association created by hasOneAndBelongsTo. Afterwards a Person becomes associated with a 'father' and a mother using a one-to-one association created by hasOneAndBelongsTo.
The last association has the type many-to-one and is defined by the function hasManyAndBelongsTo. The last association has the type many-to-one and is defined by the function hasManyAndBelongsTo.
The rest of the example is about setting and getting the associated data. The rest of the example is about setting and getting the associated data.
*/ */
var Sequelize = require(__dirname + "/../../index") 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}) , sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
, Person = sequelize.define('Person', { name: Sequelize.STRING }) , Person = sequelize.define('Person', { name: Sequelize.STRING })
, Pet = sequelize.define('Pet', { name: Sequelize.STRING }) , Pet = sequelize.define('Pet', { name: Sequelize.STRING })
...@@ -36,13 +36,13 @@ sequelize.sync({force:true}).on('success', function() { ...@@ -36,13 +36,13 @@ sequelize.sync({force:true}).on('success', function() {
.add(brother.save()) .add(brother.save())
.add(sister.save()) .add(sister.save())
.add(pet.save()) .add(pet.save())
chainer.run().on('success', function() { chainer.run().on('success', function() {
person.setMother(mother).on('success', function() { person.getMother().on('success', function(mom) { person.setMother(mother).on('success', function() { person.getMother().on('success', function(mom) {
console.log('my mom: ', mom.name) console.log('my mom: ', mom.name)
})}) })})
person.setFather(father).on('success', function() { person.getFather().on('success', function(dad) { person.setFather(father).on('success', function() { person.getFather().on('success', function(dad) {
console.log('my dad: ', dad.name) console.log('my dad: ', dad.name)
})}) })})
person.setBrothers([brother]).on('success', function() { person.getBrothers().on('success', function(brothers) { person.setBrothers([brother]).on('success', function() { person.getBrothers().on('success', function(brothers) {
console.log("my brothers: " + brothers.map(function(b) { return b.name })) console.log("my brothers: " + brothers.map(function(b) { return b.name }))
......
var Sequelize = require(__dirname + "/../../index") 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}) , sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
var Person = sequelize.define('Person', { name: Sequelize.STRING }) var Person = sequelize.define('Person', { name: Sequelize.STRING })
...@@ -8,12 +8,12 @@ var Person = sequelize.define('Person', { name: Sequelize.STRING }) ...@@ -8,12 +8,12 @@ var Person = sequelize.define('Person', { name: Sequelize.STRING })
sequelize.sync({force: true}).on('success', function() { sequelize.sync({force: true}).on('success', function() {
var count = 10, var count = 10,
queries = [] queries = []
for(var i = 0; i < count; i++) for(var i = 0; i < count; i++)
chainer.add(Person.create({name: 'someone' + (i % 3)})) chainer.add(Person.create({name: 'someone' + (i % 3)}))
console.log("Begin to save " + count + " items!") console.log("Begin to save " + count + " items!")
chainer.run().on('success', function() { chainer.run().on('success', function() {
console.log("finished") console.log("finished")
Person.count().on('success', function(count) { Person.count().on('success', function(count) {
......
var Sequelize = require(__dirname + "/../../index") 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}) , sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
var Person = sequelize.define('Person', var Person = sequelize.define('Person',
{ name: Sequelize.STRING, { name: Sequelize.STRING,
age : Sequelize.INTEGER age : Sequelize.INTEGER
...@@ -12,12 +12,12 @@ var Person = sequelize.define('Person', ...@@ -12,12 +12,12 @@ var Person = sequelize.define('Person',
sequelize.sync({force: true}).on('success', function() { sequelize.sync({force: true}).on('success', function() {
var count = 10, var count = 10,
queries = [] queries = []
for(var i = 0; i < count; i++) for(var i = 0; i < count; i++)
chainer.add(Person.create({name: 'someone' + (i % 3), age : i+5})) chainer.add(Person.create({name: 'someone' + (i % 3), age : i+5}))
console.log("Begin to save " + count + " items!") console.log("Begin to save " + count + " items!")
chainer.run().on('success', function() { chainer.run().on('success', function() {
console.log("finished") console.log("finished")
Person.max('age').on('success', function(max) { Person.max('age').on('success', function(max) {
......
var Sequelize = require(__dirname + "/../../index") 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}) , sequelize = new Sequelize(config.database, config.username, config.password, {logging: false, host: config.host})
, QueryChainer = Sequelize.Utils.QueryChainer , QueryChainer = Sequelize.Utils.QueryChainer
, sys = require("sys") , sys = require("sys")
...@@ -10,16 +10,16 @@ Person.sync({force: true}).on('success', function() { ...@@ -10,16 +10,16 @@ Person.sync({force: true}).on('success', function() {
var start = Date.now() var start = Date.now()
, count = 10000 , count = 10000
, done = 0 , done = 0
var createPerson = function() { var createPerson = function() {
Person.create({name: 'someone'}).on('success', function() { Person.create({name: 'someone'}).on('success', function() {
if(++done == count) { if(++done == count) {
var duration = (Date.now() - start) var duration = (Date.now() - start)
console.log("\nFinished creation of " + count + " people. Took: " + duration + "ms (avg: " + (duration/count) + "ms)") console.log("\nFinished creation of " + count + " people. Took: " + duration + "ms (avg: " + (duration/count) + "ms)")
start = Date.now() start = Date.now()
console.log("Will now read them from the database:") console.log("Will now read them from the database:")
Person.findAll().on('success', function(people) { Person.findAll().on('success', function(people) {
console.log("Reading " + people.length + " items took: " + (Date.now() - start) + "ms") console.log("Reading " + people.length + " items took: " + (Date.now() - start) + "ms")
}) })
...@@ -35,7 +35,7 @@ Person.sync({force: true}).on('success', function() { ...@@ -35,7 +35,7 @@ Person.sync({force: true}).on('success', function() {
for(var i = 0; i < count; i++) { for(var i = 0; i < count; i++) {
createPerson() createPerson()
} }
}).on('failure', function(err) { }).on('failure', function(err) {
console.log(err) console.log(err)
}) })
\ No newline at end of file
var fs = require("fs") /*
, Sequelize = require("sequelize") Title: Default values
, sequelize = new Sequelize('sequelize_test', 'root', null, {logging: false})
, Image = sequelize.define('Image', { data: Sequelize.TEXT })
Image.sync({force: true}).on('success', function() { This example demonstrates the use of default values for defined model fields. Instead of just specifying the datatype,
console.log("reading image") 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!
var image = fs.readFileSync(__dirname + '/source.png').toString("base64") */
console.log("done\n")
var Sequelize = require(__dirname + "/../../index")
console.log("creating database entry") , config = require(__dirname + "/../../spec/config/config")
Image.create({data: image}).on('success', function(img) { , sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
console.log("done\n")
var User = sequelize.define('User', {
console.log("writing file") name: { type: Sequelize.STRING, allowNull: false},
fs.writeFileSync(__dirname + '/target.png', img.data, "base64") isAdmin: { type: Sequelize.BOOLEAN, allowNull: false, defaultValue: false }
console.log("done\n") })
, user = User.build({ name: 'Someone' })
console.log("you might open the file ./target.png")
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)
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
/* /*
Title: Defining class and instance methods Title: Defining class and instance methods
This example shows the usage of the classMethods and instanceMethods option for Models. This example shows the usage of the classMethods and instanceMethods option for Models.
*/ */
var Sequelize = require(__dirname + "/../../index") 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}) , sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
// model definition // model definition
var Task = sequelize.define("Task", { var Task = sequelize.define("Task", {
name: Sequelize.STRING, name: Sequelize.STRING,
deadline: Sequelize.DATE, deadline: Sequelize.DATE,
...@@ -51,7 +51,7 @@ Task.sync({force: true}).on('success', function() { ...@@ -51,7 +51,7 @@ Task.sync({force: true}).on('success', function() {
console.log("should be false: " + task1.passedDeadline()) console.log("should be false: " + task1.passedDeadline())
console.log("should be true: " + task2.passedDeadline()) console.log("should be true: " + task2.passedDeadline())
console.log("should be 10: " + task1.importance) console.log("should be 10: " + task1.importance)
Task.setImportance(30, function() { Task.setImportance(30, function() {
Task.findAll().on('success', function(tasks) { Task.findAll().on('success', function(tasks) {
tasks.forEach(function(task) { tasks.forEach(function(task) {
......
var Sequelize = require(__dirname + "/../../index") 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, { , sequelize = new Sequelize(config.database, config.username, config.password, {
// use other database server or port // use other database server or port
host: 'my.srv.tld', host: 'my.srv.tld',
port: 12345, port: 12345,
// disable logging // disable logging
logging: false logging: false
}) })
......
var Sequelize = require(__dirname + "/../../index") 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}) , sequelize = new Sequelize(config.database, config.username, config.password, {logging: false})
, Project = sequelize.import(__dirname + "/Project") , Project = sequelize.import(__dirname + "/Project")
, Task = sequelize.import(__dirname + "/Task") , Task = sequelize.import(__dirname + "/Task")
Project.hasMany(Task) Project.hasMany(Task)
Task.belongsTo(Project) Task.belongsTo(Project)
sequelize.sync({force: true}).on('success', function() { sequelize.sync({force: true}).on('success', function() {
Project Project
.create({ name: 'Sequelize', description: 'A nice MySQL ORM for NodeJS' }) .create({ name: 'Sequelize', description: 'A nice MySQL ORM for NodeJS' })
......
var Utils = require("./../utils") var Utils = require("./../utils")
, DataTypes = require('./../data-types') , DataTypes = require('./../data-types')
, Helpers = require('./helpers')
module.exports = (function() { module.exports = (function() {
var BelongsTo = function(srcDAO, targetDAO, options) { var BelongsTo = function(srcDAO, targetDAO, options) {
...@@ -23,7 +24,8 @@ module.exports = (function() { ...@@ -23,7 +24,8 @@ module.exports = (function() {
var newAttributes = {} var newAttributes = {}
this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.target.tableName) + "Id", this.source.options.underscored) this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.target.tableName) + "Id", this.source.options.underscored)
newAttributes[this.identifier] = { type: DataTypes.INTEGER } newAttributes[this.identifier] = { type: this.options.keyType || DataTypes.INTEGER }
Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.target, this.source, this.options)
Utils._.defaults(this.source.rawAttributes, newAttributes) Utils._.defaults(this.source.rawAttributes, newAttributes)
// Sync attributes to DAO proto each time a new assoc is added // Sync attributes to DAO proto each time a new assoc is added
...@@ -40,8 +42,10 @@ module.exports = (function() { ...@@ -40,8 +42,10 @@ module.exports = (function() {
var id = this[self.identifier] var id = this[self.identifier]
if (!Utils._.isUndefined(params)) { if (!Utils._.isUndefined(params)) {
if (!Utils._.isUndefined(params.attributes)) { if (!Utils._.isUndefined(params.where)) {
params = Utils._.extend({where: {id:id}}, params) params.where = Utils._.extend({id:id}, params.where)
} else {
params.where = {id: id}
} }
} else { } else {
params = id params = id
......
var Utils = require("./../utils") var Utils = require("./../utils")
, DataTypes = require('./../data-types') , DataTypes = require('./../data-types')
, Helpers = require('./helpers')
var HasManySingleLinked = require("./has-many-single-linked") var HasManySingleLinked = require("./has-many-single-linked")
, HasManyMultiLinked = require("./has-many-double-linked") , HasManyMultiLinked = require("./has-many-double-linked")
...@@ -50,8 +51,9 @@ module.exports = (function() { ...@@ -50,8 +51,9 @@ module.exports = (function() {
// define a new model, which connects the models // define a new model, which connects the models
var combinedTableAttributes = {} var combinedTableAttributes = {}
combinedTableAttributes[this.identifier] = {type:DataTypes.INTEGER, primaryKey: true} var keyType = this.options.keyType || DataTypes.INTEGER
combinedTableAttributes[this.foreignIdentifier] = {type:DataTypes.INTEGER, primaryKey: true} combinedTableAttributes[this.identifier] = {type: keyType, primaryKey: true}
combinedTableAttributes[this.foreignIdentifier] = {type: keyType, primaryKey: true}
this.connectorDAO = this.source.daoFactoryManager.sequelize.define(this.combinedName, combinedTableAttributes, this.options) this.connectorDAO = this.source.daoFactoryManager.sequelize.define(this.combinedName, combinedTableAttributes, this.options)
...@@ -64,7 +66,8 @@ module.exports = (function() { ...@@ -64,7 +66,8 @@ module.exports = (function() {
} }
} else { } else {
var newAttributes = {} var newAttributes = {}
newAttributes[this.identifier] = { type: DataTypes.INTEGER } newAttributes[this.identifier] = { type: this.options.keyType || DataTypes.INTEGER }
Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, this.options)
Utils._.defaults(this.target.rawAttributes, newAttributes) Utils._.defaults(this.target.rawAttributes, newAttributes)
} }
......
var Utils = require("./../utils") var Utils = require("./../utils")
, DataTypes = require('./../data-types') , DataTypes = require('./../data-types')
, Helpers = require("./helpers")
module.exports = (function() { module.exports = (function() {
var HasOne = function(srcDAO, targetDAO, options) { var HasOne = function(srcDAO, targetDAO, options) {
...@@ -28,7 +29,8 @@ module.exports = (function() { ...@@ -28,7 +29,8 @@ module.exports = (function() {
var newAttributes = {} var newAttributes = {}
this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName) + "Id", this.options.underscored) this.identifier = this.options.foreignKey || Utils._.underscoredIf(Utils.singularize(this.source.tableName) + "Id", this.options.underscored)
newAttributes[this.identifier] = { type: DataTypes.INTEGER } newAttributes[this.identifier] = { type: this.options.keyType || DataTypes.INTEGER }
Helpers.addForeignKeyConstraints(newAttributes[this.identifier], this.source, this.target, this.options)
Utils._.defaults(this.target.rawAttributes, newAttributes) Utils._.defaults(this.target.rawAttributes, newAttributes)
// Sync attributes to DAO proto each time a new assoc is added // Sync attributes to DAO proto each time a new assoc is added
......
var Utils = require("./../utils")
module.exports = {
addForeignKeyConstraints: function(newAttribute, source, target, options) {
// FK constraints are opt-in: users must either rset `foreignKeyConstraints`
// on the association, or request an `onDelete` or `onUpdate` behaviour
if(options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
// Find primary keys: composite keys not supported with this approach
var primaryKeys = Utils._.filter(Utils._.keys(source.rawAttributes), function(key) {
return source.rawAttributes[key].primaryKey
})
if(primaryKeys.length == 1) {
newAttribute.references = source.tableName,
newAttribute.referencesKey = primaryKeys[0]
newAttribute.onDelete = options.onDelete,
newAttribute.onUpdate = options.onUpdate
}
}
}
}
var Toposort = require('toposort-class')
module.exports = (function() { module.exports = (function() {
var DAOFactoryManager = function(sequelize) { var DAOFactoryManager = function(sequelize) {
this.daos = [] this.daos = []
...@@ -31,5 +33,34 @@ module.exports = (function() { ...@@ -31,5 +33,34 @@ module.exports = (function() {
return this.daos return this.daos
}) })
/**
* Iterate over DAOs in an order suitable for e.g. creating tables. Will
* take foreign key constraints into account so that dependencies are visited
* before dependents.
*/
DAOFactoryManager.prototype.forEachDAO = function(iterator) {
var daos = {}
, sorter = new Toposort()
this.daos.forEach(function(dao) {
daos[dao.tableName] = dao
var deps = []
for(var attrName in dao.rawAttributes) {
if(dao.rawAttributes.hasOwnProperty(attrName)) {
if(dao.rawAttributes[attrName].references) {
deps.push(dao.rawAttributes[attrName].references)
}
}
}
sorter.add(dao.tableName, deps)
})
sorter.sort().reverse().forEach(function(name) {
iterator(daos[name])
})
}
return DAOFactoryManager return DAOFactoryManager
})() })()
...@@ -16,8 +16,18 @@ module.exports = (function() { ...@@ -16,8 +16,18 @@ module.exports = (function() {
underscored: false, underscored: false,
syncOnAssociation: true, syncOnAssociation: true,
paranoid: false, paranoid: false,
whereCollection: null whereCollection: null,
}, options || {}) schema: null,
schemaDelimiter: ''
}, 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 this.name = name
if (!this.options.tableName) { if (!this.options.tableName) {
...@@ -28,9 +38,6 @@ module.exports = (function() { ...@@ -28,9 +38,6 @@ module.exports = (function() {
this.rawAttributes = attributes this.rawAttributes = attributes
this.daoFactoryManager = null // defined in init function this.daoFactoryManager = null // defined in init function
this.associations = {} this.associations = {}
// extract validation
this.validate = this.options.validate || {}
} }
Object.defineProperty(DAOFactory.prototype, 'attributes', { Object.defineProperty(DAOFactory.prototype, 'attributes', {
...@@ -54,11 +61,6 @@ module.exports = (function() { ...@@ -54,11 +61,6 @@ module.exports = (function() {
this.primaryKeys = {}; this.primaryKeys = {};
Utils._.each(this.attributes, function(dataTypeString, attributeName) { Utils._.each(this.attributes, function(dataTypeString, attributeName) {
// If you don't specify a valid data type lets help you debug it
if (dataTypeString === undefined) {
throw new Error("Unrecognized data type for field " + attributeName );
}
if ((attributeName !== 'id') && (dataTypeString.indexOf('PRIMARY KEY') !== -1)) { if ((attributeName !== 'id') && (dataTypeString.indexOf('PRIMARY KEY') !== -1)) {
self.primaryKeys[attributeName] = dataTypeString self.primaryKeys[attributeName] = dataTypeString
} }
...@@ -78,11 +80,33 @@ module.exports = (function() { ...@@ -78,11 +80,33 @@ module.exports = (function() {
Util.inherits(this.DAO, DAO); Util.inherits(this.DAO, DAO);
this.DAO.prototype.rawAttributes = this.rawAttributes; this.DAO.prototype.rawAttributes = this.rawAttributes;
if (this.options.instanceMethods) { if (this.options.instanceMethods) {
Utils._.each(this.options.instanceMethods, function(fct, name) { Utils._.each(this.options.instanceMethods, function(fct, name) {
self.DAO.prototype[name] = fct 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.attributes = Object.keys(this.DAO.prototype.rawAttributes);
this.DAO.prototype.booleanValues = []; this.DAO.prototype.booleanValues = [];
...@@ -116,7 +140,7 @@ module.exports = (function() { ...@@ -116,7 +140,7 @@ module.exports = (function() {
return new Utils.CustomEventEmitter(function(emitter) { return new Utils.CustomEventEmitter(function(emitter) {
var doQuery = function() { var doQuery = function() {
self.QueryInterface self.QueryInterface
.createTable(self.tableName, self.attributes, options) .createTable(self.getTableName(), self.attributes, options)
.success(function() { emitter.emit('success', self) }) .success(function() { emitter.emit('success', self) })
.error(function(err) { emitter.emit('error', err) }) .error(function(err) { emitter.emit('error', err) })
.on('sql', function(sql) { emitter.emit('sql', sql) }) .on('sql', function(sql) { emitter.emit('sql', sql) })
...@@ -134,18 +158,40 @@ module.exports = (function() { ...@@ -134,18 +158,40 @@ module.exports = (function() {
return this.QueryInterface.dropTable(this.tableName) return this.QueryInterface.dropTable(this.tableName)
} }
DAOFactory.prototype.dropSchema = function(schema) {
return this.QueryInterface.dropSchema(schema)
}
DAOFactory.prototype.schema = function(schema, options) {
this.options.schema = schema
if (!!options) {
if (typeof options === "string") {
this.options.schemaDelimiter = options
} else {
if (!!options.schemaDelimiter) {
this.options.schemaDelimiter = options.schemaDelimiter
}
}
}
return this
}
DAOFactory.prototype.getTableName = function() {
return this.QueryGenerator.addSchema(this)
}
// alias for findAll // alias for findAll
DAOFactory.prototype.all = function(options) { DAOFactory.prototype.all = function(options, queryOptions) {
return this.findAll(options) return this.findAll(options, queryOptions)
} }
DAOFactory.prototype.findAll = function(options) { DAOFactory.prototype.findAll = function(options, queryOptions) {
var hasJoin = false var hasJoin = false
var options = Utils._.clone(options) var options = Utils._.clone(options)
if (typeof options === 'object') { if (typeof options === 'object') {
hasJoin = true
if (options.hasOwnProperty('include')) { if (options.hasOwnProperty('include')) {
hasJoin = true hasJoin = true
...@@ -158,21 +204,21 @@ module.exports = (function() { ...@@ -158,21 +204,21 @@ module.exports = (function() {
this.options.whereCollection = options.where || null this.options.whereCollection = options.where || null
} }
return this.QueryInterface.select(this, this.tableName, options, { return this.QueryInterface.select(this, this.tableName, options, Utils._.defaults({
type: 'SELECT', type: 'SELECT',
hasJoin: hasJoin hasJoin: hasJoin
}) }, queryOptions))
} }
//right now, the caller (has-many-double-linked) is in charge of the where clause //right now, the caller (has-many-double-linked) is in charge of the where clause
DAOFactory.prototype.findAllJoin = function(joinTableName, options) { DAOFactory.prototype.findAllJoin = function(joinTableName, options) {
var optcpy = Utils._.clone(options) var optcpy = Utils._.clone(options)
optcpy.attributes = optcpy.attributes || [Utils.addTicks(this.tableName)+".*"] optcpy.attributes = optcpy.attributes || [this.QueryInterface.quoteIdentifier(this.tableName)+".*"]
// whereCollection is used for non-primary key updates // whereCollection is used for non-primary key updates
this.options.whereCollection = optcpy.where || null; this.options.whereCollection = optcpy.where || null;
return this.QueryInterface.select(this, [this.tableName, joinTableName], optcpy, { type: 'SELECT' }) return this.QueryInterface.select(this, [this.getTableName(), joinTableName], optcpy, { type: 'SELECT' })
} }
/** /**
...@@ -180,9 +226,10 @@ module.exports = (function() { ...@@ -180,9 +226,10 @@ module.exports = (function() {
* *
* @param {Object} options Options to describe the scope of the search. * @param {Object} options Options to describe the scope of the search.
* @param {Array} include A list of associations which shall get eagerly loaded. Supported is either { include: [ DaoFactory1, DaoFactory2, ...] } or { include: [ { daoFactory: DaoFactory1, as: 'Alias' } ] }. * @param {Array} include A list of associations which shall get eagerly loaded. Supported is either { include: [ DaoFactory1, DaoFactory2, ...] } or { include: [ { daoFactory: DaoFactory1, as: 'Alias' } ] }.
* @param {Object} set the query options, e.g. raw, specifying that you want raw data instead of built DAOs
* @return {Object} A promise which fires `success`, `error`, `complete` and `sql`. * @return {Object} A promise which fires `success`, `error`, `complete` and `sql`.
*/ */
DAOFactory.prototype.find = function(options) { DAOFactory.prototype.find = function(options, queryOptions) {
var hasJoin = false var hasJoin = false
// no options defined? // no options defined?
...@@ -234,11 +281,11 @@ module.exports = (function() { ...@@ -234,11 +281,11 @@ module.exports = (function() {
options.limit = 1 options.limit = 1
return this.QueryInterface.select(this, this.tableName, options, { return this.QueryInterface.select(this, this.getTableName(), options, Utils._.defaults({
plain: true, plain: true,
type: 'SELECT', type: 'SELECT',
hasJoin: hasJoin hasJoin: hasJoin
}) }, queryOptions))
} }
DAOFactory.prototype.count = function(options) { DAOFactory.prototype.count = function(options) {
...@@ -246,7 +293,7 @@ module.exports = (function() { ...@@ -246,7 +293,7 @@ module.exports = (function() {
options.attributes.push(['count(*)', 'count']) options.attributes.push(['count(*)', 'count'])
options.parseInt = true options.parseInt = true
return this.QueryInterface.rawSelect(this.tableName, options, 'count') return this.QueryInterface.rawSelect(this.getTableName(), options, 'count')
} }
DAOFactory.prototype.findAndCountAll = function(options) { DAOFactory.prototype.findAndCountAll = function(options) {
...@@ -316,16 +363,16 @@ module.exports = (function() { ...@@ -316,16 +363,16 @@ module.exports = (function() {
DAOFactory.prototype.max = function(field, options) { DAOFactory.prototype.max = function(field, options) {
options = Utils._.extend({ attributes: [] }, options || {}) options = Utils._.extend({ attributes: [] }, options || {})
options.attributes.push(['max(' + field + ')', 'max']) options.attributes.push(['max(' + field + ')', 'max'])
options.parseInt = true options.parseFloat = true
return this.QueryInterface.rawSelect(this.tableName, options, 'max') return this.QueryInterface.rawSelect(this.getTableName(), options, 'max')
} }
DAOFactory.prototype.min = function(field, options) { DAOFactory.prototype.min = function(field, options) {
options = Utils._.extend({ attributes: [] }, options || {}) options = Utils._.extend({ attributes: [] }, options || {})
options.attributes.push(['min(' + field + ')', 'min']) options.attributes.push(['min(' + field + ')', 'min'])
options.parseInt = true options.parseFloat = true
return this.QueryInterface.rawSelect(this.tableName, options, 'min') return this.QueryInterface.rawSelect(this.getTableName(), options, 'min')
} }
DAOFactory.prototype.build = function(values, options) { DAOFactory.prototype.build = function(values, options) {
...@@ -359,13 +406,13 @@ module.exports = (function() { ...@@ -359,13 +406,13 @@ module.exports = (function() {
self.create(params) self.create(params)
.success(function (instance) { .success(function (instance) {
emitter.emit('success', instance) emitter.emit('success', instance, true)
}) })
.error( function (error) { .error( function (error) {
emitter.emit('error', error) emitter.emit('error', error)
}) })
} else { } else {
emitter.emit('success', instance) emitter.emit('success', instance, false)
} }
}).error(function (error) { }).error(function (error) {
emitter.emit('error', error) emitter.emit('error', error)
...@@ -373,6 +420,113 @@ module.exports = (function() { ...@@ -373,6 +420,113 @@ module.exports = (function() {
}).run() }).run()
} }
/**
* Create and insert multiple instances
*
* @param {Array} records List of objects (key/value pairs) to create instances from
* @param {Array} fields Fields to insert (defaults to all fields)
* @return {Object} A promise which fires `success`, `error`, `complete` and `sql`.
*
* Note: the `success` handler is not passed any arguments. To obtain DAOs for
* the newly created values, you will need to query for them again. This is
* because MySQL and SQLite do not make it easy to obtain back automatically
* generated IDs and other default values in a way that can be mapped to
* multiple records
*/
DAOFactory.prototype.bulkCreate = function(records, fields) {
var self = this
, daos = records.map(function(v) { return self.build(v) })
, updatedAtAttr = self.options.underscored ? 'updated_at' : 'updatedAt'
, createdAtAttr = self.options.underscored ? 'created_at' : 'createdAt'
// we will re-create from DAOs, which may have set up default attributes
records = []
if (fields) {
// Always insert updated and created time stamps
if (self.options.timestamps) {
if (fields.indexOf(updatedAtAttr) === -1) {
fields.push(updatedAtAttr)
}
if (fields.indexOf(createdAtAttr) === -1) {
fields.push(createdAtAttr)
}
}
// Build records for the fields we know about
daos.forEach(function(dao) {
var values = {};
fields.forEach(function(field) {
values[field] = dao.values[field]
})
if (self.options.timestamps) {
values[updatedAtAttr] = Utils.now()
}
records.push(values);
})
} else {
daos.forEach(function(dao) {
records.push(dao.values)
})
}
// Validate enums
records.forEach(function(values) {
for (var attrName in self.rawAttributes) {
if (self.rawAttributes.hasOwnProperty(attrName)) {
var definition = self.rawAttributes[attrName]
, isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString()))
, hasValue = (typeof values[attrName] !== 'undefined')
, valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
if (isEnum && hasValue && valueOutOfScope) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
}
}
}
})
return self.QueryInterface.bulkInsert(self.tableName, records)
}
/**
* Delete multiple instances
*
* @param {Object} where Options to describe the scope of the search.
* @param {Object} options Possible options are:
- limit: How many rows to delete
- truncate: If set to true, dialects that support it will use TRUNCATE instead of DELETE FROM. If a table is truncated the where and limit options are ignored
* @return {Object} A promise which fires `success`, `error`, `complete` and `sql`.
*/
DAOFactory.prototype.destroy = function(where, options) {
if (this.options.timestamps && this.options.paranoid) {
var attr = this.options.underscored ? 'deleted_at' : 'deletedAt'
var attrValueHash = {}
attrValueHash[attr] = Utils.now()
return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where)
} else {
return this.QueryInterface.bulkDelete(this.tableName, where, options)
}
}
/**
* Update multiple instances
*
* @param {Object} attrValueHash A hash of fields to change and their new values
* @param {Object} where Options to describe the scope of the search.
* @return {Object} A promise which fires `success`, `error`, `complete` and `sql`.
*/
DAOFactory.prototype.update = function(attrValueHash, where) {
if(this.options.timestamps) {
var attr = this.options.underscored ? 'updated_at' : 'updatedAt'
attrValueHash[attr] = Utils.now()
}
return this.QueryInterface.bulkUpdate(this.tableName, attrValueHash, where)
}
// private // private
var query = function() { var query = function() {
...@@ -421,7 +575,9 @@ module.exports = (function() { ...@@ -421,7 +575,9 @@ module.exports = (function() {
} }
Utils._.each(defaultAttributes, function(value, attr) { Utils._.each(defaultAttributes, function(value, attr) {
self.rawAttributes[attr] = value if (Utils._.isUndefined(self.rawAttributes[attr])) {
self.rawAttributes[attr] = value
}
}) })
} }
......
var Validator = require("validator")
, Utils = require("./utils")
var DaoValidator = module.exports = function(model) {
this.model = model
}
DaoValidator.prototype.validate = function() {
var errors = {}
errors = Utils._.extend(errors, validateAttributes.call(this))
errors = Utils._.extend(errors, validateModel.call(this))
return errors
}
// private
var validateModel = function() {
var errors = {}
// for each model validator for this DAO
Utils._.each(this.model.__options.validate, function(validator, validatorType) {
try {
validator.apply(this.model)
} catch (err) {
errors[validatorType] = [err.message] // TODO: data structure needs to change for 2.0
}
}.bind(this))
return errors
}
var validateAttributes = function() {
var errors = {}
// for each field and value
Utils._.each(this.model.values, function(value, field) {
var rawAttribute = this.model.rawAttributes[field]
, hasAllowedNull = ((rawAttribute.allowNull === true) && ((value === null) || (value === undefined)))
if (this.model.validators.hasOwnProperty(field) && !hasAllowedNull) {
errors = Utils._.merge(errors, validateAttribute.call(this, value, field))
}
}.bind(this)) // for each field
return errors
}
var validateAttribute = function(value, field) {
var errors = {}
// for each validator
Utils._.each(this.model.validators[field], function(details, validatorType) {
var validator = prepareValidationOfAttribute.call(this, value, details, validatorType)
try {
validator.fn.apply(null, validator.args)
} catch (err) {
var msg = err.message
// if we didn't provide a custom error message then augment the default one returned by the validator
if (!validator.msg && !validator.isCustom) {
msg += ": " + field
}
// each field can have multiple validation errors stored against it
errors[field] = errors[field] || []
errors[field].push(msg)
}
}.bind(this)) // for each validator for this field
return errors
}
var prepareValidationOfAttribute = function(value, details, validatorType) {
var isCustomValidator = false // if true then it's a custom validation method
, validatorFunction = null // the validation function to call
, validatorArgs = [] // extra arguments to pass to validation function
, errorMessage = "" // the error message to return if validation fails
if (typeof details === 'function') {
// it is a custom validator function?
isCustomValidator = true
validatorFunction = Utils._.bind(details, this.model, value)
} else {
// it is a validator module function?
// extract extra arguments for the validator
validatorArgs = details.hasOwnProperty("args") ? details.args : details
if (!Array.isArray(validatorArgs)) {
validatorArgs = [validatorArgs]
}
// extract the error msg
errorMessage = details.hasOwnProperty("msg") ? details.msg : false
// check method exists
var validator = Validator.check(value, errorMessage)
// check if Validator knows that kind of validation test
if (!Utils._.isFunction(validator[validatorType])) {
throw new Error("Invalid validator function: " + validatorType)
}
// bind to validator obj
validatorFunction = Utils._.bind(validator[validatorType], validator)
}
return {
fn: validatorFunction,
msg: errorMessage,
args: validatorArgs,
isCustom: isCustomValidator
}
}
var Utils = require("./utils") var Utils = require("./utils")
, Mixin = require("./associations/mixin") , Mixin = require("./associations/mixin")
, Validator = require("validator") , DaoValidator = require("./dao-validator")
, DataTypes = require("./data-types") , DataTypes = require("./data-types")
module.exports = (function() { module.exports = (function() {
var DAO = function(values, options, isNewRecord) { var DAO = function(values, options, isNewRecord) {
var self = this this.dataValues = {}
this.__options = options this.__options = options
this.hasPrimaryKeys = options.hasPrimaryKeys this.hasPrimaryKeys = options.hasPrimaryKeys
this.selectedValues = values this.selectedValues = values
this.__eagerlyLoadedAssociations = [] this.__eagerlyLoadedAssociations = []
initAttributes.call(this, values, isNewRecord) 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) Utils._.extend(DAO.prototype, Mixin.prototype)
Object.defineProperty(DAO.prototype, 'sequelize', { Object.defineProperty(DAO.prototype, 'sequelize', {
...@@ -42,7 +27,7 @@ module.exports = (function() { ...@@ -42,7 +27,7 @@ module.exports = (function() {
Object.defineProperty(DAO.prototype, 'isDeleted', { Object.defineProperty(DAO.prototype, 'isDeleted', {
get: function() { get: function() {
var result = this.__options.timestamps && this.__options.paranoid 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 return result
} }
...@@ -54,7 +39,10 @@ module.exports = (function() { ...@@ -54,7 +39,10 @@ module.exports = (function() {
, self = this , self = this
this.attributes.concat(this.__eagerlyLoadedAssociations).forEach(function(attr) { 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 return result
...@@ -67,7 +55,7 @@ module.exports = (function() { ...@@ -67,7 +55,7 @@ module.exports = (function() {
, self = this , self = this
Utils._.each(this.__factory.primaryKeys, function(_, attr) { Utils._.each(this.__factory.primaryKeys, function(_, attr) {
result[attr] = self[attr] result[attr] = self.dataValues[attr]
}) })
return result return result
...@@ -85,13 +73,21 @@ module.exports = (function() { ...@@ -85,13 +73,21 @@ module.exports = (function() {
} }
primaryKeys.forEach(function(identifier) { primaryKeys.forEach(function(identifier) {
result[identifier] = self[identifier] result[identifier] = self.dataValues[identifier]
}) })
return result 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() // if an array with field names is passed to save()
// only those fields will be updated // only those fields will be updated
DAO.prototype.save = function(fields) { DAO.prototype.save = function(fields) {
...@@ -111,9 +107,11 @@ module.exports = (function() { ...@@ -111,9 +107,11 @@ module.exports = (function() {
} }
} }
var tmpVals = self.values
fields.forEach(function(field) { fields.forEach(function(field) {
if (self.values[field] !== undefined) { if (tmpVals[field] !== undefined) {
values[field] = self.values[field] values[field] = tmpVals[field]
} }
}) })
} }
...@@ -122,29 +120,51 @@ module.exports = (function() { ...@@ -122,29 +120,51 @@ module.exports = (function() {
if (this.daoFactory.rawAttributes.hasOwnProperty(attrName)) { if (this.daoFactory.rawAttributes.hasOwnProperty(attrName)) {
var definition = this.daoFactory.rawAttributes[attrName] var definition = this.daoFactory.rawAttributes[attrName]
, isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString())) , isEnum = (definition.type && (definition.type.toString() === DataTypes.ENUM.toString()))
, isHstore = (!!definition.type && !!definition.type.type && definition.type.type === DataTypes.HSTORE.type)
, hasValue = (typeof values[attrName] !== 'undefined') , hasValue = (typeof values[attrName] !== 'undefined')
, valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1) , valueOutOfScope = ((definition.values || []).indexOf(values[attrName]) === -1)
if (isEnum && hasValue && valueOutOfScope) { if (isEnum && hasValue && valueOutOfScope) {
throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', ')) throw new Error('Value "' + values[attrName] + '" for ENUM ' + attrName + ' is out of allowed scope. Allowed values: ' + definition.values.join(', '))
} }
if (isHstore) {
if (typeof values[attrName] === "object") {
var text = []
Utils._.each(values[attrName], function(value, key){
if (typeof value !== "string" && typeof value !== "number") {
throw new Error('Value for HSTORE must be a string or number.')
}
text.push(this.QueryInterface.quoteIdentifier(key) + '=>' + (typeof value === "string" ? this.QueryInterface.quoteIdentifier(value) : value))
}.bind(this))
values[attrName] = text.join(',')
}
}
} }
} }
if (this.__options.timestamps && this.hasOwnProperty(updatedAtAttr)) { if (this.__options.timestamps && this.dataValues.hasOwnProperty(updatedAtAttr)) {
this[updatedAtAttr] = values[updatedAtAttr] = Utils.now() this.dataValues[updatedAtAttr] = values[updatedAtAttr] = Utils.now()
} }
if (this.isNewRecord) { var errors = this.validate()
return this.QueryInterface.insert(this, this.__factory.tableName, values)
if (!!errors) {
return new Utils.CustomEventEmitter(function(emitter) {
emitter.emit('error', errors)
}).run()
}
else if (this.isNewRecord) {
return this.QueryInterface.insert(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory), values)
} else { } else {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : this.id; var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id };
if (identifier === null && this.__options.whereCollection !== null) { if (identifier === null && this.__options.whereCollection !== null) {
identifier = this.__options.whereCollection; identifier = this.__options.whereCollection;
} }
var tableName = this.__factory.tableName var tableName = this.QueryInterface.QueryGenerator.addSchema(this.__factory)
, query = this.QueryInterface.update(this, tableName, values, identifier) , query = this.QueryInterface.update(this, tableName, values, identifier)
return query return query
...@@ -160,7 +180,7 @@ module.exports = (function() { ...@@ -160,7 +180,7 @@ module.exports = (function() {
*/ */
DAO.prototype.reload = function() { DAO.prototype.reload = function() {
var where = [ var where = [
this.QueryInterface.QueryGenerator.addQuotes(this.__factory.tableName) + '.' + this.QueryInterface.QueryGenerator.addQuotes('id')+'=?', this.QueryInterface.quoteIdentifier(this.__factory.tableName) + '.' + this.QueryInterface.quoteIdentifier('id')+'=?',
this.id this.id
] ]
...@@ -189,63 +209,10 @@ module.exports = (function() { ...@@ -189,63 +209,10 @@ module.exports = (function() {
* @return null if and only if validation successful; otherwise an object containing { field name : [error msgs] } entries. * @return null if and only if validation successful; otherwise an object containing { field name : [error msgs] } entries.
*/ */
DAO.prototype.validate = function() { DAO.prototype.validate = function() {
var self = this var validator = new DaoValidator(this)
var failures = {} , errors = validator.validate()
// for each field and value
Utils._.each(self.values, function(value, field) {
// if field has validators
if (self.validators.hasOwnProperty(field)) {
// for each validator
Utils._.each(self.validators[field], function(details, validatorType) {
var is_custom_fn = false // if true then it's a custom validation method return (Utils._.isEmpty(errors) ? null : errors)
var fn_method = null // the validation function to call
var fn_args = [] // extra arguments to pass to validation function
var fn_msg = "" // the error message to return if validation fails
// is it a custom validator function?
if (Utils._.isFunction(details)) {
is_custom_fn = true
fn_method = Utils._.bind(details, self, value)
}
// is it a validator module function?
else {
// extra args
fn_args = details.hasOwnProperty("args") ? details.args : details
if (!Array.isArray(fn_args))
fn_args = [fn_args]
// error msg
fn_msg = details.hasOwnProperty("msg") ? details.msg : false
// check method exists
var v = Validator.check(value, fn_msg)
if (!Utils._.isFunction(v[validatorType]))
throw new Error("Invalid validator function: " + validatorType)
// bind to validator obj
fn_method = Utils._.bind(v[validatorType], v)
}
try {
fn_method.apply(null, fn_args)
} catch (err) {
err = err.message
// if we didn't provide a custom error message then augment the default one returned by the validator
if (!fn_msg && !is_custom_fn)
err += ": " + field
// each field can have multiple validation failures stored against it
if (failures.hasOwnProperty(field)) {
failures[field].push(err)
} else {
failures[field] = [err]
}
}
}) // for each validator for this field
} // if field has validator set
}) // for each field
return (Utils._.isEmpty(failures) ? null : failures)
} }
...@@ -277,16 +244,16 @@ module.exports = (function() { ...@@ -277,16 +244,16 @@ module.exports = (function() {
DAO.prototype.destroy = function() { DAO.prototype.destroy = function() {
if (this.__options.timestamps && this.__options.paranoid) { if (this.__options.timestamps && this.__options.paranoid) {
var attr = this.__options.underscored ? 'deleted_at' : 'deletedAt' var attr = this.__options.underscored ? 'deleted_at' : 'deletedAt'
this[attr] = new Date() this.dataValues[attr] = new Date()
return this.save() return this.save()
} else { } else {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : this.id var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id };
return this.QueryInterface.delete(this, this.__factory.tableName, identifier) return this.QueryInterface.delete(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory.tableName, this.__factory.options.schema), identifier)
} }
} }
DAO.prototype.increment = function(fields, count) { DAO.prototype.increment = function(fields, count) {
var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : this.id, var identifier = this.__options.hasPrimaryKeys ? this.primaryKeyValues : { id: this.id },
values = {} values = {}
if (count === undefined) count = 1; if (count === undefined) count = 1;
...@@ -301,7 +268,7 @@ module.exports = (function() { ...@@ -301,7 +268,7 @@ module.exports = (function() {
values = fields; values = fields;
} }
return this.QueryInterface.increment(this, this.__factory.tableName, values, identifier) return this.QueryInterface.increment(this, this.QueryInterface.QueryGenerator.addSchema(this.__factory.tableName, this.__factory.options.schema), values, identifier)
} }
DAO.prototype.decrement = function (fields, count) { DAO.prototype.decrement = function (fields, count) {
...@@ -338,7 +305,37 @@ module.exports = (function() { ...@@ -338,7 +305,37 @@ module.exports = (function() {
} }
DAO.prototype.addAttribute = function(attribute, value) { 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) { DAO.prototype.setValidators = function(attribute, validators) {
...@@ -352,17 +349,22 @@ module.exports = (function() { ...@@ -352,17 +349,22 @@ module.exports = (function() {
// private // private
var initAttributes = function(values, isNewRecord) { 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 // 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 (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])
}
this.addAttribute(key, values[key]) this.addAttribute(key, values[key])
} }
} }
// 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) { if (this.__options.timestamps && isNewRecord) {
defaults[this.__options.underscored ? 'created_at' : 'createdAt'] = Utils.now() defaults[this.__options.underscored ? 'created_at' : 'createdAt'] = Utils.now()
defaults[this.__options.underscored ? 'updated_at' : 'updatedAt'] = Utils.now() defaults[this.__options.underscored ? 'updated_at' : 'updatedAt'] = Utils.now()
...@@ -370,17 +372,38 @@ module.exports = (function() { ...@@ -370,17 +372,38 @@ module.exports = (function() {
if (this.__options.paranoid) { if (this.__options.paranoid) {
defaults[this.__options.underscored ? 'deleted_at' : 'deletedAt'] = null 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)) { if (Utils._.size(defaults)) {
for (var attr in defaults) { for (key in defaults) {
var value = defaults[attr] attrs[key] = Utils.toDefaultValue(defaults[key])
}
}
if (!this.hasOwnProperty(attr)) { Utils._.each(this.attributes, function(key) {
this.addAttribute(attr, Utils.toDefaultValue(value)) 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 return DAO
......
var STRING = function(length, binary) {
if (this instanceof STRING) {
this._binary = !!binary;
if (typeof length === 'number') {
this._length = length;
} else {
this._length = 255;
}
} else {
return new STRING(length, binary);
}
};
STRING.prototype = {
get BINARY() {
this._binary = true;
return this;
},
get type() {
return this.toString();
},
toString: function() {
return 'VARCHAR(' + this._length + ')' + ((this._binary) ? ' BINARY' : '');
}
};
Object.defineProperty(STRING, 'BINARY', {
get: function() {
return new STRING(undefined, true);
}
});
var INTEGER = function() {
return INTEGER.prototype.construct.apply(this, [INTEGER].concat(Array.prototype.slice.apply(arguments)));
};
var BIGINT = function() {
return BIGINT.prototype.construct.apply(this, [BIGINT].concat(Array.prototype.slice.apply(arguments)));
};
var FLOAT = function() {
return FLOAT.prototype.construct.apply(this, [FLOAT].concat(Array.prototype.slice.apply(arguments)));
};
FLOAT._type = FLOAT;
FLOAT._typeName = 'FLOAT';
INTEGER._type = INTEGER;
INTEGER._typeName = 'INTEGER';
BIGINT._type = BIGINT;
BIGINT._typeName = 'BIGINT';
STRING._type = STRING;
STRING._typeName = 'VARCHAR';
STRING.toString = INTEGER.toString = FLOAT.toString = BIGINT.toString = function() {
return new this._type().toString();
};
FLOAT.prototype = BIGINT.prototype = INTEGER.prototype = {
construct: function(RealType, length, decimals, unsigned, zerofill) {
if (this instanceof RealType) {
this._typeName = RealType._typeName;
this._unsigned = !!unsigned;
this._zerofill = !!zerofill;
if (typeof length === 'number') {
this._length = length;
}
if (typeof decimals === 'number') {
this._decimals = decimals;
}
} else {
return new RealType(length, decimals, unsigned, zerofill);
}
},
get type() {
return this.toString();
},
get UNSIGNED() {
this._unsigned = true;
return this;
},
get ZEROFILL() {
this._zerofill = true;
return this;
},
toString: function() {
var result = this._typeName;
if (this._length) {
result += '(' + this._length;
if (typeof this._decimals === 'number') {
result += ',' + this._decimals;
}
result += ')';
}
if (this._unsigned) {
result += ' UNSIGNED';
}
if (this._zerofill) {
result += ' ZEROFILL';
}
return result;
}
};
var unsignedDesc = {
get: function() {
return new this._type(undefined, undefined, true);
}
};
var zerofillDesc = {
get: function() {
return new this._type(undefined, undefined, undefined, true);
}
};
var typeDesc = {
get: function() {
return new this._type().toString();
}
};
Object.defineProperty(STRING, 'type', typeDesc);
Object.defineProperty(INTEGER, 'type', typeDesc);
Object.defineProperty(BIGINT, 'type', typeDesc);
Object.defineProperty(FLOAT, 'type', typeDesc);
Object.defineProperty(INTEGER, 'UNSIGNED', unsignedDesc);
Object.defineProperty(BIGINT, 'UNSIGNED', unsignedDesc);
Object.defineProperty(FLOAT, 'UNSIGNED', unsignedDesc);
Object.defineProperty(INTEGER, 'ZEROFILL', zerofillDesc);
Object.defineProperty(BIGINT, 'ZEROFILL', zerofillDesc);
Object.defineProperty(FLOAT, 'ZEROFILL', zerofillDesc);
module.exports = { module.exports = {
STRING: 'VARCHAR(255)', STRING: STRING,
TEXT: 'TEXT', TEXT: 'TEXT',
INTEGER: 'INTEGER', INTEGER: INTEGER,
BIGINT: 'BIGINT', BIGINT: BIGINT,
DATE: 'DATETIME', DATE: 'DATETIME',
BOOLEAN: 'TINYINT(1)', BOOLEAN: 'TINYINT(1)',
FLOAT: 'FLOAT', FLOAT: FLOAT,
NOW: 'NOW', NOW: 'NOW',
get ENUM() { get ENUM() {
...@@ -32,5 +172,19 @@ module.exports = { ...@@ -32,5 +172,19 @@ module.exports = {
return result return result
}, },
ARRAY: function(type) { return type + '[]' }
ARRAY: function(type) { return type + '[]' },
get HSTORE() {
var result = function() {
return {
type: 'HSTORE'
}
}
result.type = 'HSTORE'
result.toString = result.valueOf = function() { return 'TEXT' }
return result
}
} }
...@@ -12,6 +12,7 @@ module.exports = (function() { ...@@ -12,6 +12,7 @@ module.exports = (function() {
this.sequelize = sequelize this.sequelize = sequelize
this.client = null this.client = null
this.config = config || {} this.config = config || {}
this.config.port = this.config.port || 3306
this.disconnectTimeoutId = null this.disconnectTimeoutId = null
this.queue = [] this.queue = []
this.activeQueue = [] this.activeQueue = []
...@@ -19,7 +20,9 @@ module.exports = (function() { ...@@ -19,7 +20,9 @@ module.exports = (function() {
this.poolCfg = Utils._.defaults(this.config.pool, { this.poolCfg = Utils._.defaults(this.config.pool, {
maxConnections: 10, maxConnections: 10,
minConnections: 0, minConnections: 0,
maxIdleTime: 1000 maxIdleTime: 1000,
handleDisconnects: false,
validate: validateConnection
}); });
this.pendingQueries = 0; this.pendingQueries = 0;
this.useReplicaton = !!config.replication; this.useReplicaton = !!config.replication;
...@@ -83,6 +86,7 @@ module.exports = (function() { ...@@ -83,6 +86,7 @@ module.exports = (function() {
destroy: function(client) { destroy: function(client) {
disconnect.call(self, client) disconnect.call(self, client)
}, },
validate: self.poolCfg.validate,
max: self.poolCfg.maxConnections, max: self.poolCfg.maxConnections,
min: self.poolCfg.minConnections, min: self.poolCfg.minConnections,
idleTimeoutMillis: self.poolCfg.maxIdleTime idleTimeoutMillis: self.poolCfg.maxIdleTime
...@@ -98,6 +102,7 @@ module.exports = (function() { ...@@ -98,6 +102,7 @@ module.exports = (function() {
destroy: function(client) { destroy: function(client) {
disconnect.call(self, client) disconnect.call(self, client)
}, },
validate: self.poolCfg.validate,
max: self.poolCfg.maxConnections, max: self.poolCfg.maxConnections,
min: self.poolCfg.minConnections, min: self.poolCfg.minConnections,
idleTimeoutMillis: self.poolCfg.maxIdleTime idleTimeoutMillis: self.poolCfg.maxIdleTime
...@@ -115,6 +120,7 @@ module.exports = (function() { ...@@ -115,6 +120,7 @@ module.exports = (function() {
}, },
max: self.poolCfg.maxConnections, max: self.poolCfg.maxConnections,
min: self.poolCfg.minConnections, min: self.poolCfg.minConnections,
validate: self.poolCfg.validate,
idleTimeoutMillis: self.poolCfg.maxIdleTime idleTimeoutMillis: self.poolCfg.maxIdleTime
}) })
} }
...@@ -247,10 +253,25 @@ module.exports = (function() { ...@@ -247,10 +253,25 @@ module.exports = (function() {
connection.query("SET time_zone = '+0:00'"); connection.query("SET time_zone = '+0:00'");
// client.setMaxListeners(self.maxConcurrentQueries) // client.setMaxListeners(self.maxConcurrentQueries)
this.isConnecting = false this.isConnecting = false
if (config.pool.handleDisconnects) {
handleDisconnect(this.pool, connection)
}
done(null, 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) { var enqueue = function(queueItem, options) {
options = options || {} options = options || {}
if (this.activeQueue.length < this.maxConcurrentQueries) { if (this.activeQueue.length < this.maxConcurrentQueries) {
...@@ -336,4 +357,4 @@ module.exports = (function() { ...@@ -336,4 +357,4 @@ module.exports = (function() {
} }
return ConnectorManager return ConnectorManager
})() })()
\ No newline at end of file
...@@ -4,14 +4,48 @@ var Utils = require("../../utils") ...@@ -4,14 +4,48 @@ var Utils = require("../../utils")
module.exports = (function() { module.exports = (function() {
var QueryGenerator = { var QueryGenerator = {
addSchema: function(opts) {
var tableName
var schema = (!!opts && !!opts.options && !!opts.options.schema ? opts.options.schema : undefined)
var schemaDelimiter = (!!opts && !!opts.options && !!opts.options.schemaDelimiter ? opts.options.schemaDelimiter : undefined)
if (!!opts && !!opts.tableName) {
tableName = opts.tableName
}
else if (typeof opts === "string") {
tableName = opts
}
if (!schema || schema.toString().trim() === "") {
return tableName
}
return this.quoteIdentifier(schema + (!schemaDelimiter ? '.' : schemaDelimiter) + tableName, false)
},
createSchema: function() {
var query = "SHOW TABLES"
return Utils._.template(query)({})
},
dropSchema: function() {
var query = "SHOW TABLES"
return Utils._.template(query)({})
},
showSchemasQuery: function() {
return "SHOW TABLES"
},
createTableQuery: function(tableName, attributes, options) { createTableQuery: function(tableName, attributes, options) {
options = Utils._.extend({ options = Utils._.extend({
engine: 'InnoDB', engine: 'InnoDB',
charset: null charset: null
}, options || {}) }, options || {})
var query = "CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>) ENGINE=<%= engine %> <%= charset %>" var query = "CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>)<%= comment %> ENGINE=<%= engine %> <%= charset %>"
, primaryKeys = [] , primaryKeys = []
, foreignKeys = {}
, attrStr = [] , attrStr = []
for (var attr in attributes) { for (var attr in attributes) {
...@@ -20,25 +54,37 @@ module.exports = (function() { ...@@ -20,25 +54,37 @@ module.exports = (function() {
if (Utils._.includes(dataType, 'PRIMARY KEY')) { if (Utils._.includes(dataType, 'PRIMARY KEY')) {
primaryKeys.push(attr) primaryKeys.push(attr)
attrStr.push(QueryGenerator.addQuotes(attr) + " " + dataType.replace(/PRIMARY KEY/, '')) attrStr.push(this.quoteIdentifier(attr) + " " + dataType.replace(/PRIMARY KEY/, ''))
} else if (Utils._.includes(dataType, 'REFERENCES')) {
// MySQL doesn't support inline REFERENCES declarations: move to the end
var m = dataType.match(/^(.+) (REFERENCES.*)$/)
attrStr.push(this.quoteIdentifier(attr) + " " + m[1])
foreignKeys[attr] = m[2]
} else { } else {
attrStr.push(QueryGenerator.addQuotes(attr) + " " + dataType) attrStr.push(this.quoteIdentifier(attr) + " " + dataType)
} }
} }
} }
var values = { var values = {
table: QueryGenerator.addQuotes(tableName), table: this.quoteIdentifier(tableName),
attributes: attrStr.join(", "), attributes: attrStr.join(", "),
comment: options.comment && Utils._.isString(options.comment) ? " COMMENT " + this.escape(options.comment) : "",
engine: options.engine, engine: options.engine,
charset: (options.charset ? "DEFAULT CHARSET=" + options.charset : "") charset: (options.charset ? "DEFAULT CHARSET=" + options.charset : "")
} }
, pkString = primaryKeys.map(function(pk) { return QueryGenerator.addQuotes(pk) }).join(", ") , pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk) }.bind(this)).join(", ")
if (pkString.length > 0) { if (pkString.length > 0) {
values.attributes += ", PRIMARY KEY (" + pkString + ")" values.attributes += ", PRIMARY KEY (" + pkString + ")"
} }
for (var fkey in foreignKeys) {
if(foreignKeys.hasOwnProperty(fkey)) {
values.attributes += ", FOREIGN KEY (" + this.quoteIdentifier(fkey) + ") " + foreignKeys[fkey]
}
}
return Utils._.template(query)(values).trim() + ";" return Utils._.template(query)(values).trim() + ";"
}, },
...@@ -48,7 +94,7 @@ module.exports = (function() { ...@@ -48,7 +94,7 @@ module.exports = (function() {
var query = "DROP TABLE IF EXISTS <%= table %>;" var query = "DROP TABLE IF EXISTS <%= table %>;"
return Utils._.template(query)({ return Utils._.template(query)({
table: QueryGenerator.addQuotes(tableName) table: this.quoteIdentifier(tableName)
}) })
}, },
...@@ -86,7 +132,7 @@ module.exports = (function() { ...@@ -86,7 +132,7 @@ module.exports = (function() {
var query = "ALTER TABLE `<%= tableName %>` CHANGE <%= attributes %>;" var query = "ALTER TABLE `<%= tableName %>` CHANGE <%= attributes %>;"
var attrString = [] var attrString = []
for (attrName in attributes) { for (var attrName in attributes) {
var definition = attributes[attrName] var definition = attributes[attrName]
attrString.push(Utils._.template('`<%= attrName %>` `<%= attrName %>` <%= definition %>')({ attrString.push(Utils._.template('`<%= attrName %>` `<%= attrName %>` <%= definition %>')({
...@@ -116,145 +162,175 @@ module.exports = (function() { ...@@ -116,145 +162,175 @@ module.exports = (function() {
}, },
selectQuery: function(tableName, options) { selectQuery: function(tableName, options) {
var query = "SELECT <%= attributes %> FROM <%= table %>" var table = null,
, table = null joinQuery = ""
options = options || {} options = options || {}
options.table = table = Array.isArray(tableName) ? tableName.map(function(tbl){ return QueryGenerator.addQuotes(tbl) }).join(", ") : QueryGenerator.addQuotes(tableName) options.table = table = Array.isArray(tableName) ? tableName.map(function(t) { return this.quoteIdentifier(t)}.bind(this)).join(", ") : this.quoteIdentifier(tableName)
options.attributes = options.attributes && options.attributes.map(function(attr){ options.attributes = options.attributes && options.attributes.map(function(attr){
if(Array.isArray(attr) && attr.length == 2) { if(Array.isArray(attr) && attr.length == 2) {
return [attr[0], QueryGenerator.addQuotes(attr[1])].join(' as ') return [attr[0], this.quoteIdentifier(attr[1])].join(' as ')
} else { } else {
return attr.indexOf(Utils.TICK_CHAR) < 0 ? QueryGenerator.addQuotes(attr) : attr return attr.indexOf(Utils.TICK_CHAR) < 0 ? this.quoteIdentifiers(attr) : attr
} }
}).join(", ") }.bind(this)).join(", ")
options.attributes = options.attributes || '*' options.attributes = options.attributes || '*'
if (options.include) { if (options.include) {
var optAttributes = [options.table + '.*'] var optAttributes = options.attributes === '*' ? [options.table + '.*'] : [options.attributes]
options.include.forEach(function(include) { options.include.forEach(function(include) {
var attributes = Object.keys(include.daoFactory.attributes).map(function(attr) { var attributes = Object.keys(include.daoFactory.attributes).map(function(attr) {
var template = Utils._.template("`<%= as %>`.`<%= attr %>` AS `<%= as %>.<%= attr %>`") return this.quoteIdentifier(include.as) + "." + this.quoteIdentifier(attr) + " AS " + this.quoteIdentifier(include.as + "." + attr)
return template({ as: include.as, attr: attr }) }.bind(this))
})
optAttributes = optAttributes.concat(attributes) optAttributes = optAttributes.concat(attributes)
var joinQuery = " LEFT OUTER JOIN `<%= table %>` AS `<%= as %>` ON `<%= tableLeft %>`.`<%= attrLeft %>` = `<%= tableRight %>`.`<%= attrRight %>`" var table = include.daoFactory.tableName
query += Utils._.template(joinQuery)({ var as = include.as
table: include.daoFactory.tableName, var tableLeft = ((include.association.associationType === 'BelongsTo') ? include.as : tableName)
as: include.as, var attrLeft = 'id'
tableLeft: ((include.association.associationType === 'BelongsTo') ? include.as : tableName), var tableRight = ((include.association.associationType === 'BelongsTo') ? tableName : include.as)
attrLeft: 'id', var attrRight = include.association.identifier
tableRight: ((include.association.associationType === 'BelongsTo') ? tableName : include.as), joinQuery += " LEFT OUTER JOIN " + this.quoteIdentifier(table) + " AS " + this.quoteIdentifier(as) + " ON " + this.quoteIdentifier(tableLeft) + "." + this.quoteIdentifier(attrLeft) + " = " + this.quoteIdentifier(tableRight) + "." + this.quoteIdentifier(attrRight)
attrRight: include.association.identifier
}) }.bind(this))
})
options.attributes = optAttributes.join(', ') options.attributes = optAttributes.join(', ')
} }
if (options.where) { var query = "SELECT " + options.attributes + " FROM " + options.table
query += joinQuery
if (options.hasOwnProperty('where')) {
options.where = this.getWhereConditions(options.where, tableName) options.where = this.getWhereConditions(options.where, tableName)
query += " WHERE <%= where %>" query += " WHERE " + options.where
} }
if (options.group) { if (options.group) {
options.group = Array.isArray(options.group) ? options.group.map(function(grp){return QueryGenerator.addQuotes(grp)}).join(', ') : QueryGenerator.addQuotes(options.group) options.group = Array.isArray(options.group) ? options.group.map(function(t) { return this.quoteIdentifiers(t)}.bind(this)).join(', ') : this.quoteIdentifiers(options.group)
query += " GROUP BY <%= group %>" query += " GROUP BY " + options.group
} }
if (options.order) { if (options.order) {
query += " ORDER BY <%= order %>" query += " ORDER BY " + options.order
} }
if (options.limit && !(options.include && (options.limit === 1))) { if (options.limit && !(options.include && (options.limit === 1))) {
if (options.offset) { if (options.offset) {
query += " LIMIT <%= offset %>, <%= limit %>" query += " LIMIT " + options.offset + ", " + options.limit
} else { } else {
query += " LIMIT <%= limit %>" query += " LIMIT " + options.limit
} }
} }
query += ";" query += ";"
return Utils._.template(query)(options) return query
}, },
insertQuery: function(tableName, attrValueHash) { insertQuery: function(tableName, attrValueHash) {
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull)
var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES (<%= values %>);" var table = this.quoteIdentifier(tableName)
var attributes = Object.keys(attrValueHash).map(function(attr){return this.quoteIdentifier(attr)}.bind(this)).join(",")
var values = Utils._.values(attrValueHash).map(function(v) { return this.escape(v) }.bind(this)).join(",")
var replacements = { var query = "INSERT INTO " + table + " (" + attributes + ") VALUES (" + values + ");"
table: QueryGenerator.addQuotes(tableName),
attributes: Object.keys(attrValueHash).map(function(attr){return QueryGenerator.addQuotes(attr)}).join(","), return query
values: Utils._.values(attrValueHash).map(function(value){ },
return Utils.escape((value instanceof Date) ? Utils.toSqlDate(value) : value)
}).join(",") bulkInsertQuery: function(tableName, attrValueHashes) {
} var tuples = []
Utils._.forEach(attrValueHashes, function(attrValueHash) {
tuples.push("(" +
Utils._.values(attrValueHash).map(function(v) { return this.escape(v) }.bind(this)).join(",") +
")")
}.bind(this))
return Utils._.template(query)(replacements) var table = this.quoteIdentifier(tableName)
var attributes = Object.keys(attrValueHashes[0]).map(function(attr){return this.quoteIdentifier(attr)}.bind(this)).join(",")
var query = "INSERT INTO " + table + " (" + attributes + ") VALUES " + tuples.join(",") + ";"
return query
}, },
updateQuery: function(tableName, attrValueHash, where) { updateQuery: function(tableName, attrValueHash, where) {
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull)
var query = "UPDATE <%= table %> SET <%= values %> WHERE <%= where %>" var values = []
, values = []
for (var key in attrValueHash) { for (var key in attrValueHash) {
var value = attrValueHash[key] var value = attrValueHash[key]
, _value = (value instanceof Date) ? Utils.toSqlDate(value) : value , _value = this.escape(value)
values.push(QueryGenerator.addQuotes(key) + "=" + Utils.escape(_value)) values.push(this.quoteIdentifier(key) + "=" + _value)
} }
var replacements = { var query = "UPDATE " + this.quoteIdentifier(tableName) +
table: QueryGenerator.addQuotes(tableName), " SET " + values.join(",") +
values: values.join(","), " WHERE " + this.getWhereConditions(where)
where: QueryGenerator.getWhereConditions(where)
}
return Utils._.template(query)(replacements) return query
}, },
deleteQuery: function(tableName, where, options) { deleteQuery: function(tableName, where, options) {
options = options || {} options = options || {}
options.limit = options.limit || 1
var query = "DELETE FROM <%= table %> WHERE <%= where %> LIMIT <%= limit %>" var table = this.quoteIdentifier(tableName)
var replacements = { if (options.truncate === true) {
table: QueryGenerator.addQuotes(tableName), // Truncate does not allow LIMIT and WHERE
where: QueryGenerator.getWhereConditions(where), return "TRUNCATE " + table
limit: Utils.escape(options.limit) }
where = this.getWhereConditions(where)
var limit = ""
if(Utils._.isUndefined(options.limit)) {
options.limit = 1;
}
if(!!options.limit) {
limit = " LIMIT " + this.escape(options.limit)
} }
return Utils._.template(query)(replacements) return "DELETE FROM " + table + " WHERE " + where + limit
},
bulkDeleteQuery: function(tableName, where, options) {
options = options || {}
var table = this.quoteIdentifier(tableName)
where = this.getWhereConditions(where)
var query = "DELETE FROM " + table + " WHERE " + where
return query
}, },
incrementQuery: function (tableName, attrValueHash, where) { incrementQuery: function (tableName, attrValueHash, where) {
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull)
var query = "UPDATE <%= table %> SET <%= values %> WHERE <%= where %> " var values = []
, values = []
for (var key in attrValueHash) { for (var key in attrValueHash) {
var value = attrValueHash[key] var value = attrValueHash[key]
, _value = (value instanceof Date) ? Utils.toSqlDate(value) : value , _value = this.escape(value)
values.push(QueryGenerator.addQuotes(key) + "=" + QueryGenerator.addQuotes(key) + " + " +Utils.escape(_value)) values.push(this.quoteIdentifier(key) + "=" + this.quoteIdentifier(key) + " + " + _value)
} }
var replacements = { var table = this.quoteIdentifier(tableName)
table: QueryGenerator.addQuotes(tableName), values = values.join(",")
values: values.join(","), where = this.getWhereConditions(where)
where: QueryGenerator.getWhereConditions(where)
}
return Utils._.template(query)(replacements) var query = "UPDATE " + table + " SET " + values + " WHERE " + where
return query
}, },
addIndexQuery: function(tableName, attributes, options) { addIndexQuery: function(tableName, attributes, options) {
...@@ -280,11 +356,11 @@ module.exports = (function() { ...@@ -280,11 +356,11 @@ module.exports = (function() {
return result return result
} }
}) }.bind(this))
var onlyAttributeNames = attributes.map(function(attribute) { var onlyAttributeNames = attributes.map(function(attribute) {
return (typeof attribute === 'string') ? attribute : attribute.attribute return (typeof attribute === 'string') ? attribute : attribute.attribute
}) }.bind(this))
options = Utils._.extend({ options = Utils._.extend({
indicesType: null, indicesType: null,
...@@ -334,7 +410,7 @@ module.exports = (function() { ...@@ -334,7 +410,7 @@ module.exports = (function() {
result = Utils.format(smth) result = Utils.format(smth)
} }
return result return result ? result : '1=1'
}, },
hashToWhereConditions: function(hash) { hashToWhereConditions: function(hash) {
...@@ -344,25 +420,23 @@ module.exports = (function() { ...@@ -344,25 +420,23 @@ module.exports = (function() {
var value = hash[key] var value = hash[key]
//handle qualified key names //handle qualified key names
var _key = key.split('.').map(function(col){return QueryGenerator.addQuotes(col)}).join(".") var _key = this.quoteIdentifiers(key)
, _value = null , _value = null
if (Array.isArray(value)) { if (Array.isArray(value)) {
// is value an array? // is value an array?
if (value.length == 0) { value = [null] } if (value.length === 0) { value = [null] }
_value = "(" + value.map(function(subValue) { _value = "(" + value.map(function(v) { return this.escape(v) }.bind(this)).join(',') + ")"
return Utils.escape(subValue);
}).join(',') + ")"
result.push([_key, _value].join(" IN ")) result.push([_key, _value].join(" IN "))
} else if ((value) && (typeof value == 'object')) { } else if ((value) && (typeof value == 'object') && !(value instanceof Date)) {
// is value an object? // is value an object?
//using as sentinel for join column => value //using as sentinel for join column => value
_value = value.join.split('.').map(function(col){ return QueryGenerator.addQuotes(col) }).join(".") _value = this.quoteIdentifiers(value.join)
result.push([_key, _value].join("=")) result.push([_key, _value].join("="))
} else { } else {
_value = Utils.escape(value) _value = this.escape(value)
result.push((_value == 'NULL') ? _key + " IS NULL" : [_key, _value].join("=")) result.push((_value == 'NULL') ? _key + " IS NULL" : [_key, _value].join("="))
} }
} }
...@@ -377,17 +451,18 @@ module.exports = (function() { ...@@ -377,17 +451,18 @@ module.exports = (function() {
var dataType = attributes[name] var dataType = attributes[name]
if (Utils.isHash(dataType)) { if (Utils.isHash(dataType)) {
var template = "<%= type %>" var template
, replacements = { type: dataType.type }
if (dataType.type.toString() === DataTypes.ENUM.toString()) { if (dataType.type.toString() === DataTypes.ENUM.toString()) {
if (Array.isArray(dataType.values) && (dataType.values.length > 0)) { if (Array.isArray(dataType.values) && (dataType.values.length > 0)) {
replacements.type = "ENUM(" + Utils._.map(dataType.values, function(value) { template = "ENUM(" + Utils._.map(dataType.values, function(value) {
return Utils.escape(value) return this.escape(value)
}).join(", ") + ")" }.bind(this)).join(", ") + ")"
} else { } else {
throw new Error('Values for ENUM haven\'t been defined.') throw new Error('Values for ENUM haven\'t been defined.')
} }
} else {
template = dataType.type.toString();
} }
if (dataType.hasOwnProperty('allowNull') && (!dataType.allowNull)) { if (dataType.hasOwnProperty('allowNull') && (!dataType.allowNull)) {
...@@ -398,9 +473,8 @@ module.exports = (function() { ...@@ -398,9 +473,8 @@ module.exports = (function() {
template += " auto_increment" template += " auto_increment"
} }
if ((dataType.defaultValue != undefined) && (dataType.defaultValue != DataTypes.NOW)) { if ((dataType.defaultValue !== undefined) && (dataType.defaultValue != DataTypes.NOW)) {
template += " DEFAULT <%= defaultValue %>" template += " DEFAULT " + this.escape(dataType.defaultValue)
replacements.defaultValue = Utils.escape(dataType.defaultValue)
} }
if (dataType.unique) { if (dataType.unique) {
...@@ -411,7 +485,31 @@ module.exports = (function() { ...@@ -411,7 +485,31 @@ module.exports = (function() {
template += " PRIMARY KEY" template += " PRIMARY KEY"
} }
result[name] = Utils._.template(template)(replacements) if(dataType.references) {
template += " REFERENCES " + this.quoteIdentifier(dataType.references)
if(dataType.referencesKey) {
template += " (" + this.quoteIdentifier(dataType.referencesKey) + ")"
} else {
template += " (" + this.quoteIdentifier('id') + ")"
}
if(dataType.onDelete) {
template += " ON DELETE " + dataType.onDelete.toUpperCase()
}
if(dataType.onUpdate) {
template += " ON UPDATE " + dataType.onUpdate.toUpperCase()
}
}
if (dataType.comment && Utils._.isString(dataType.comment) && dataType.comment.length) {
template += " COMMENT " + Utils.escape(dataType.comment)
}
result[name] = template
} else { } else {
result[name] = dataType result[name] = dataType
} }
...@@ -436,13 +534,33 @@ module.exports = (function() { ...@@ -436,13 +534,33 @@ module.exports = (function() {
return fields return fields
}, },
addQuotes: function(s, quoteChar) { enableForeignKeyConstraintsQuery: function() {
return Utils.addTicks(s, quoteChar) var sql = "SET FOREIGN_KEY_CHECKS = 1;"
return Utils._.template(sql, {})
}, },
removeQuotes: function(s, quoteChar) { disableForeignKeyConstraintsQuery: function() {
return Utils.removeTicks(s, quoteChar) var sql = "SET FOREIGN_KEY_CHECKS = 0;"
return Utils._.template(sql, {})
},
quoteIdentifier: function(identifier, force) {
return Utils.addTicks(identifier, "`")
},
quoteIdentifiers: function(identifiers, force) {
return identifiers.split('.').map(function(v) { return this.quoteIdentifier(v, force) }.bind(this)).join('.')
},
escape: function(value) {
if (value instanceof Date) {
value = Utils.toSqlDate(value)
} else if (typeof value === 'boolean') {
value = value ? 1 : 0
}
return Utils.escape(value)
} }
} }
return Utils._.extend(Utils._.clone(require("../query-generator")), QueryGenerator) return Utils._.extend(Utils._.clone(require("../query-generator")), QueryGenerator)
......
...@@ -4,10 +4,11 @@ var Query = require("./query") ...@@ -4,10 +4,11 @@ var Query = require("./query")
module.exports = (function() { module.exports = (function() {
var ConnectorManager = function(sequelize, config) { var ConnectorManager = function(sequelize, config) {
this.sequelize = sequelize this.sequelize = sequelize
this.client = null this.client = null
this.config = config || {} this.config = config || {}
this.pooling = (!!this.config.poolCfg && (this.config.poolCfg.maxConnections > 0)) this.config.port = this.config.port || 5432
this.pg = this.config.native ? require('pg').native : require('pg') this.pooling = (!!this.config.poolCfg && (this.config.poolCfg.maxConnections > 0))
this.pg = this.config.native ? require('pg').native : require('pg')
// set pooling parameters if specified // set pooling parameters if specified
if (this.pooling) { if (this.pooling) {
......
...@@ -8,6 +8,39 @@ module.exports = (function() { ...@@ -8,6 +8,39 @@ module.exports = (function() {
var QueryGenerator = { var QueryGenerator = {
options: {}, options: {},
addSchema: function(opts) {
var tableName = undefined
var schema = (!!opts.options && !!opts.options.schema ? opts.options.schema : undefined)
var schemaDelimiter = (!!opts.options && !!opts.options.schemaDelimiter ? opts.options.schemaDelimiter : undefined)
if (!!opts.tableName) {
tableName = opts.tableName
}
else if (typeof opts === "string") {
tableName = opts
}
if (!schema || schema.toString().trim() === "") {
return tableName
}
return this.quoteIdentifier(schema) + '.' + this.quoteIdentifier(tableName)
},
createSchema: function(schema) {
var query = "CREATE SCHEMA <%= schema%>;"
return Utils._.template(query)({schema: schema})
},
dropSchema: function(schema) {
var query = "DROP SCHEMA <%= schema%> CASCADE;"
return Utils._.template(query)({schema: schema})
},
showSchemasQuery: function() {
return "SELECT schema_name FROM information_schema.schemata WHERE schema_name <> 'information_schema' AND schema_name != 'public' AND schema_name !~ E'^pg_';"
},
createTableQuery: function(tableName, attributes, options) { createTableQuery: function(tableName, attributes, options) {
options = Utils._.extend({ options = Utils._.extend({
}, options || {}) }, options || {})
...@@ -15,26 +48,39 @@ module.exports = (function() { ...@@ -15,26 +48,39 @@ module.exports = (function() {
primaryKeys[tableName] = [] primaryKeys[tableName] = []
tables[tableName] = {} tables[tableName] = {}
var query = "CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>)" var query = "CREATE TABLE IF NOT EXISTS <%= table %> (<%= attributes%>)<%= comments %>"
, comments = ""
, attrStr = [] , attrStr = []
, i
if (options.comment && Utils._.isString(options.comment)) {
comments += "; COMMENT ON TABLE <%= table %> IS " + this.escape(options.comment)
}
for (var attr in attributes) { for (var attr in attributes) {
var dataType = QueryGenerator.pgDataTypeMapping(tableName, attr, attributes[attr]) if ((i = attributes[attr].indexOf('COMMENT')) !== -1) {
attrStr.push(QueryGenerator.addQuotes(attr) + " " + dataType) // Move comment to a seperate query
comments += "; " + attributes[attr].substring(i)
attributes[attr] = attributes[attr].substring(0, i)
}
var dataType = this.pgDataTypeMapping(tableName, attr, attributes[attr])
attrStr.push(this.quoteIdentifier(attr) + " " + dataType)
if (attributes[attr].match(/^ENUM\(/)) { if (attributes[attr].match(/^ENUM\(/)) {
query = QueryGenerator.pgEnum(tableName, attr, attributes[attr]) + query query = this.pgEnum(tableName, attr, attributes[attr]) + query
} }
} }
var values = { var values = {
table: QueryGenerator.addQuotes(tableName), table: this.quoteIdentifiers(tableName),
attributes: attrStr.join(", "), attributes: attrStr.join(", "),
comments: Utils._.template(comments, { table: this.quoteIdentifiers(tableName)})
} }
var pks = primaryKeys[tableName].map(function(pk){ var pks = primaryKeys[tableName].map(function(pk){
return QueryGenerator.addQuotes(pk) return this.quoteIdentifier(pk)
}).join(",") }.bind(this)).join(",")
if (pks.length > 0) { if (pks.length > 0) {
values.attributes += ", PRIMARY KEY (" + pks + ")" values.attributes += ", PRIMARY KEY (" + pks + ")"
...@@ -45,17 +91,18 @@ module.exports = (function() { ...@@ -45,17 +91,18 @@ module.exports = (function() {
dropTableQuery: function(tableName, options) { dropTableQuery: function(tableName, options) {
options = options || {} options = options || {}
var query = "DROP TABLE IF EXISTS <%= table %>;" var query = "DROP TABLE IF EXISTS <%= table %><%= cascade %>;"
return Utils._.template(query)({ return Utils._.template(query)({
table: QueryGenerator.addQuotes(tableName) table: this.quoteIdentifiers(tableName),
cascade: options.cascade? " CASCADE" : ""
}) })
}, },
renameTableQuery: function(before, after) { renameTableQuery: function(before, after) {
var query = "ALTER TABLE <%= before %> RENAME TO <%= after %>;" var query = "ALTER TABLE <%= before %> RENAME TO <%= after %>;"
return Utils._.template(query)({ return Utils._.template(query)({
before: QueryGenerator.addQuotes(before), before: this.quoteIdentifier(before),
after: QueryGenerator.addQuotes(after) after: this.quoteIdentifier(after)
}) })
}, },
...@@ -64,9 +111,9 @@ module.exports = (function() { ...@@ -64,9 +111,9 @@ module.exports = (function() {
}, },
describeTableQuery: function(tableName) { describeTableQuery: function(tableName) {
var query = 'SELECT column_name as "Field", column_default as "Default", is_nullable as "Null", data_type as "Type" FROM information_schema.columns WHERE table_name = <%= table %>;' var query = 'SELECT c.column_name as "Field", c.column_default as "Default", c.is_nullable as "Null", c.data_type as "Type", (SELECT array_agg(e.enumlabel) FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON t.oid=e.enumtypid WHERE t.typname=c.udt_name) AS special FROM information_schema.columns c WHERE table_name = <%= table %>;'
return Utils._.template(query)({ return Utils._.template(query)({
table: QueryGenerator.addQuotes(tableName, "'") table: this.escape(tableName)
}) })
}, },
...@@ -78,25 +125,25 @@ module.exports = (function() { ...@@ -78,25 +125,25 @@ module.exports = (function() {
var definition = attributes[attrName] var definition = attributes[attrName]
attrString.push(Utils._.template('<%= attrName %> <%= definition %>')({ attrString.push(Utils._.template('<%= attrName %> <%= definition %>')({
attrName: QueryGenerator.addQuotes(attrName), attrName: this.quoteIdentifier(attrName),
definition: QueryGenerator.pgDataTypeMapping(tableName, attrName, definition) definition: this.pgDataTypeMapping(tableName, attrName, definition)
})) }))
if (definition.match(/^ENUM\(/)) { if (definition.match(/^ENUM\(/)) {
query = QueryGenerator.pgEnum(tableName, attrName, definition) + query query = this.pgEnum(tableName, attrName, definition) + query
} }
} }
return Utils._.template(query)({ return Utils._.template(query)({
tableName: QueryGenerator.addQuotes(tableName), tableName: this.quoteIdentifiers(tableName),
attributes: attrString.join(', ') }) attributes: attrString.join(', ') })
}, },
removeColumnQuery: function(tableName, attributeName) { removeColumnQuery: function(tableName, attributeName) {
var query = "ALTER TABLE <%= tableName %> DROP COLUMN <%= attributeName %>;" var query = "ALTER TABLE <%= tableName %> DROP COLUMN <%= attributeName %>;"
return Utils._.template(query)({ return Utils._.template(query)({
tableName: QueryGenerator.addQuotes(tableName), tableName: this.quoteIdentifiers(tableName),
attributeName: QueryGenerator.addQuotes(attributeName) attributeName: this.quoteIdentifier(attributeName)
}) })
}, },
...@@ -110,40 +157,40 @@ module.exports = (function() { ...@@ -110,40 +157,40 @@ module.exports = (function() {
if (definition.indexOf('NOT NULL') > 0) { if (definition.indexOf('NOT NULL') > 0) {
attrSql += Utils._.template(query)({ attrSql += Utils._.template(query)({
tableName: QueryGenerator.addQuotes(tableName), tableName: this.quoteIdentifiers(tableName),
query: QueryGenerator.addQuotes(attributeName) + ' SET NOT NULL' query: this.quoteIdentifier(attributeName) + ' SET NOT NULL'
}) })
definition = definition.replace('NOT NULL', '').trim() definition = definition.replace('NOT NULL', '').trim()
} else { } else {
attrSql += Utils._.template(query)({ attrSql += Utils._.template(query)({
tableName: QueryGenerator.addQuotes(tableName), tableName: this.quoteIdentifiers(tableName),
query: QueryGenerator.addQuotes(attributeName) + ' DROP NOT NULL' query: this.quoteIdentifier(attributeName) + ' DROP NOT NULL'
}) })
} }
if (definition.indexOf('DEFAULT') > 0) { if (definition.indexOf('DEFAULT') > 0) {
attrSql += Utils._.template(query)({ attrSql += Utils._.template(query)({
tableName: QueryGenerator.addQuotes(tableName), tableName: this.quoteIdentifiers(tableName),
query: QueryGenerator.addQuotes(attributeName) + ' SET DEFAULT' + definition.match(/DEFAULT ([^;]+)/)[1] query: this.quoteIdentifier(attributeName) + ' SET DEFAULT' + definition.match(/DEFAULT ([^;]+)/)[1]
}) })
definition = definition.replace(/(DEFAULT[^;]+)/, '').trim() definition = definition.replace(/(DEFAULT[^;]+)/, '').trim()
} else { } else {
attrSql += Utils._.template(query)({ attrSql += Utils._.template(query)({
tableName: QueryGenerator.addQuotes(tableName), tableName: this.quoteIdentifiers(tableName),
query: QueryGenerator.addQuotes(attributeName) + ' DROP DEFAULT' query: this.quoteIdentifier(attributeName) + ' DROP DEFAULT'
}) })
} }
if (definition.match(/^ENUM\(/)) { if (definition.match(/^ENUM\(/)) {
query = QueryGenerator.pgEnum(tableName, attributeName, definition) + query query = this.pgEnum(tableName, attributeName, definition) + query
definition = definition.replace(/^ENUM\(.+\)/, Utils.escape("enum_" + tableName + "_" + attributeName)) definition = definition.replace(/^ENUM\(.+\)/, this.quoteIdentifier("enum_" + tableName + "_" + attributeName))
} }
attrSql += Utils._.template(query)({ attrSql += Utils._.template(query)({
tableName: QueryGenerator.addQuotes(tableName), tableName: this.quoteIdentifiers(tableName),
query: QueryGenerator.addQuotes(attributeName) + ' TYPE ' + definition query: this.quoteIdentifier(attributeName) + ' TYPE ' + definition
}) })
sql.push(attrSql) sql.push(attrSql)
...@@ -158,96 +205,74 @@ module.exports = (function() { ...@@ -158,96 +205,74 @@ module.exports = (function() {
for (var attributeName in attributes) { for (var attributeName in attributes) {
attrString.push(Utils._.template('<%= before %> TO <%= after %>')({ attrString.push(Utils._.template('<%= before %> TO <%= after %>')({
before: QueryGenerator.addQuotes(attrBefore), before: this.quoteIdentifier(attrBefore),
after: QueryGenerator.addQuotes(attributeName), after: this.quoteIdentifier(attributeName)
})) }))
} }
return Utils._.template(query)({ return Utils._.template(query)({
tableName: QueryGenerator.addQuotes(tableName), tableName: this.quoteIdentifiers(tableName),
attributes: attrString.join(', ') attributes: attrString.join(', ')
}) })
}, },
selectQuery: function(tableName, options) { selectQuery: function(tableName, options) {
var query = "SELECT <%= attributes %> FROM <%= table %>" var query = "SELECT <%= attributes %> FROM <%= table %>",
, table = null table = null
options = options || {}
if (Array.isArray(tableName)) {
options.table = table = tableName.map(function(t){
return QueryGenerator.addQuotes(t)
}).join(", ")
} else {
options.table = table = QueryGenerator.addQuotes(tableName)
}
options = options || {}
options.table = table = Array.isArray(tableName) ? tableName.map(function(t) { return this.quoteIdentifiers(t) }.bind(this)).join(", ") : this.quoteIdentifiers(tableName)
options.attributes = options.attributes && options.attributes.map(function(attr) { options.attributes = options.attributes && options.attributes.map(function(attr) {
if (Array.isArray(attr) && attr.length === 2) { if (Array.isArray(attr) && attr.length === 2) {
return [ return [attr[0], this.quoteIdentifier(attr[1])].join(' as ')
attr[0],
QueryGenerator.addQuotes(QueryGenerator.removeQuotes(attr[1], '`'))
].join(' as ')
} else if (attr.indexOf('`') >= 0) {
return attr.replace(/`/g, '"')
} else { } else {
return QueryGenerator.addQuotes(attr) return attr.indexOf('"') < 0 ? this.quoteIdentifiers(attr) : attr
} }
}).join(", ") }.bind(this)).join(", ")
options.attributes = options.attributes || '*' options.attributes = options.attributes || '*'
if (options.include) { if (options.include) {
var optAttributes = [options.table + '.*'] var optAttributes = options.attributes === '*' ? [options.table + '.*'] : [options.attributes]
options.include.forEach(function(include) { options.include.forEach(function(include) {
var attributes = Object.keys(include.daoFactory.attributes).map(function(attr) { var attributes = Object.keys(include.daoFactory.attributes).map(function(attr) {
var template = Utils._.template('"<%= as %>"."<%= attr %>" AS "<%= as %>.<%= attr %>"') return this.quoteIdentifier(include.as) + "." + this.quoteIdentifier(attr) + " AS " + this.quoteIdentifier(include.as + "." + attr, true)
return template({ as: include.as, attr: attr }) }.bind(this))
})
optAttributes = optAttributes.concat(attributes) optAttributes = optAttributes.concat(attributes)
var joinQuery = ' LEFT OUTER JOIN "<%= table %>" AS "<%= as %>" ON "<%= tableLeft %>"."<%= attrLeft %>" = "<%= tableRight %>"."<%= attrRight %>"' var joinQuery = ' LEFT OUTER JOIN <%= table %> AS <%= as %> ON <%= tableLeft %>.<%= attrLeft %> = <%= tableRight %>.<%= attrRight %>'
query += Utils._.template(joinQuery)({ query += Utils._.template(joinQuery)({
table: include.daoFactory.tableName, table: this.quoteIdentifiers(include.daoFactory.tableName),
as: include.as, as: this.quoteIdentifier(include.as),
tableLeft: ((include.association.associationType === 'BelongsTo') ? include.as : tableName), tableLeft: this.quoteIdentifiers((include.association.associationType === 'BelongsTo') ? include.as : tableName),
attrLeft: 'id', attrLeft: this.quoteIdentifier('id'),
tableRight: ((include.association.associationType === 'BelongsTo') ? tableName : include.as), tableRight: this.quoteIdentifiers((include.association.associationType === 'BelongsTo') ? tableName : include.as),
attrRight: include.association.identifier attrRight: this.quoteIdentifier(include.association.identifier)
}) })
}) }.bind(this))
options.attributes = optAttributes.join(', ') options.attributes = optAttributes.join(', ')
} }
if(options.where) { if(options.hasOwnProperty('where')) {
options.where = QueryGenerator.getWhereConditions(options.where) options.where = this.getWhereConditions(options.where, tableName)
query += " WHERE <%= where %>" query += " WHERE <%= where %>"
} }
if(options.group) {
options.group = Array.isArray(options.group) ? options.group.map(function(t) { return this.quoteIdentifiers(t) }.bind(this)).join(', ') : this.quoteIdentifiers(options.group)
query += " GROUP BY <%= group %>"
}
if(options.order) { if(options.order) {
options.order = options.order.replace(/([^ ]+)(.*)/, function(m, g1, g2) { options.order = options.order.replace(/([^ ]+)(.*)/, function(m, g1, g2) {
return QueryGenerator.addQuotes(g1) + g2 return this.quoteIdentifiers(g1) + g2
}) }.bind(this))
query += " ORDER BY <%= order %>" query += " ORDER BY <%= order %>"
} }
if(options.group) {
if (Array.isArray(options.group)) {
options.group = options.group.map(function(grp){
return QueryGenerator.addQuotes(grp)
}).join(', ')
} else {
options.group = QueryGenerator.addQuotes(options.group)
}
query += " GROUP BY <%= group %>"
}
if (!(options.include && (options.limit === 1))) { if (!(options.include && (options.limit === 1))) {
if (options.limit) { if (options.limit) {
query += " LIMIT <%= limit %>" query += " LIMIT <%= limit %>"
...@@ -267,27 +292,40 @@ module.exports = (function() { ...@@ -267,27 +292,40 @@ module.exports = (function() {
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull)
var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES (<%= values %>) RETURNING *;" var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES (<%= values %>) RETURNING *;"
, returning = [] , returning = removeSerialsFromHash(tableName, attrValueHash)
Utils._.forEach(attrValueHash, function(value, key, hash) {
if (tables[tableName] && tables[tableName][key]) {
switch (tables[tableName][key]) {
case 'serial':
delete hash[key]
returning.push(key)
break
}
}
});
var replacements = { var replacements = {
table: QueryGenerator.addQuotes(tableName) table: this.quoteIdentifiers(tableName)
, attributes: Object.keys(attrValueHash).map(function(attr){ , attributes: Object.keys(attrValueHash).map(function(attr){
return QueryGenerator.addQuotes(attr) return this.quoteIdentifier(attr)
}).join(",") }.bind(this)).join(",")
, values: Utils._.values(attrValueHash).map(function(value){ , values: Utils._.values(attrValueHash).map(function(value){
return QueryGenerator.pgEscape(value) return this.escape(value)
}).join(",") }.bind(this)).join(",")
}
return Utils._.template(query)(replacements)
},
bulkInsertQuery: function(tableName, attrValueHashes) {
var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %> RETURNING *;"
, tuples = []
Utils._.forEach(attrValueHashes, function(attrValueHash) {
removeSerialsFromHash(tableName, attrValueHash)
tuples.push("(" +
Utils._.values(attrValueHash).map(function(value){
return this.escape(value)
}.bind(this)).join(",") +
")")
}.bind(this))
var replacements = {
table: this.quoteIdentifiers(tableName)
, attributes: Object.keys(attrValueHashes[0]).map(function(attr){
return this.quoteIdentifier(attr)
}.bind(this)).join(",")
, tuples: tuples.join(",")
} }
return Utils._.template(query)(replacements) return Utils._.template(query)(replacements)
...@@ -301,13 +339,13 @@ module.exports = (function() { ...@@ -301,13 +339,13 @@ module.exports = (function() {
for (var key in attrValueHash) { for (var key in attrValueHash) {
var value = attrValueHash[key] var value = attrValueHash[key]
values.push(QueryGenerator.addQuotes(key) + "=" + QueryGenerator.pgEscape(value)) values.push(this.quoteIdentifier(key) + "=" + this.escape(value))
} }
var replacements = { var replacements = {
table: QueryGenerator.addQuotes(tableName), table: this.quoteIdentifiers(tableName),
values: values.join(","), values: values.join(","),
where: QueryGenerator.getWhereConditions(where) where: this.getWhereConditions(where)
} }
return Utils._.template(query)(replacements) return Utils._.template(query)(replacements)
...@@ -315,25 +353,32 @@ module.exports = (function() { ...@@ -315,25 +353,32 @@ module.exports = (function() {
deleteQuery: function(tableName, where, options) { deleteQuery: function(tableName, where, options) {
options = options || {} options = options || {}
options.limit = options.limit || 1
if (options.truncate === true) {
return "TRUNCATE " + QueryGenerator.quoteIdentifier(tableName)
}
if(Utils._.isUndefined(options.limit)) {
options.limit = 1;
}
primaryKeys[tableName] = primaryKeys[tableName] || []; primaryKeys[tableName] = primaryKeys[tableName] || [];
var query = "DELETE FROM <%= table %> WHERE <%= primaryKeys %> IN (SELECT <%= primaryKeysSelection %> FROM <%= table %> WHERE <%= where %> LIMIT <%= limit %>)" var query = "DELETE FROM <%= table %> WHERE <%= primaryKeys %> IN (SELECT <%= primaryKeysSelection %> FROM <%= table %> WHERE <%= where %><%= limit %>)"
var pks; var pks;
if (primaryKeys[tableName] && primaryKeys[tableName].length > 0) { if (primaryKeys[tableName] && primaryKeys[tableName].length > 0) {
pks = primaryKeys[tableName].map(function(pk) { pks = primaryKeys[tableName].map(function(pk) {
return QueryGenerator.addQuotes(pk) return this.quoteIdentifier(pk)
}).join(',') }.bind(this)).join(',')
} else { } else {
pks = QueryGenerator.addQuotes('id') pks = this.quoteIdentifier('id')
} }
var replacements = { var replacements = {
table: QueryGenerator.addQuotes(tableName), table: this.quoteIdentifiers(tableName),
where: QueryGenerator.getWhereConditions(where), where: this.getWhereConditions(where),
limit: QueryGenerator.pgEscape(options.limit), limit: !!options.limit? " LIMIT " + this.escape(options.limit) : "",
primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks, primaryKeys: primaryKeys[tableName].length > 1 ? '(' + pks + ')' : pks,
primaryKeysSelection: pks primaryKeysSelection: pks
} }
...@@ -349,13 +394,13 @@ module.exports = (function() { ...@@ -349,13 +394,13 @@ module.exports = (function() {
for (var key in attrValueHash) { for (var key in attrValueHash) {
var value = attrValueHash[key] var value = attrValueHash[key]
values.push(QueryGenerator.addQuotes(key) + "=" + QueryGenerator.addQuotes(key) + " + " + QueryGenerator.pgEscape(value)) values.push(this.quoteIdentifier(key) + "=" + this.quoteIdentifier(key) + " + " + this.escape(value))
} }
var replacements = { var replacements = {
table: QueryGenerator.addQuotes(tableName), table: this.quoteIdentifiers(tableName),
values: values.join(","), values: values.join(","),
where: QueryGenerator.getWhereConditions(where) where: this.getWhereConditions(where)
} }
return Utils._.template(query)(replacements) return Utils._.template(query)(replacements)
...@@ -365,7 +410,7 @@ module.exports = (function() { ...@@ -365,7 +410,7 @@ module.exports = (function() {
addIndexQuery: function(tableName, attributes, options) { addIndexQuery: function(tableName, attributes, options) {
var transformedAttributes = attributes.map(function(attribute) { var transformedAttributes = attributes.map(function(attribute) {
if (typeof attribute === 'string') { if (typeof attribute === 'string') {
return QueryGenerator.addQuotes(attribute) return this.quoteIdentifier(attribute)
} else { } else {
var result = "" var result = ""
...@@ -373,7 +418,7 @@ module.exports = (function() { ...@@ -373,7 +418,7 @@ module.exports = (function() {
throw new Error('The following index attribute has no attribute: ' + util.inspect(attribute)) throw new Error('The following index attribute has no attribute: ' + util.inspect(attribute))
} }
result += QueryGenerator.addQuotes(attribute.attribute) result += this.quoteIdentifier(attribute.attribute)
if (attribute.length) { if (attribute.length) {
result += '(' + attribute.length + ')' result += '(' + attribute.length + ')'
...@@ -385,11 +430,11 @@ module.exports = (function() { ...@@ -385,11 +430,11 @@ module.exports = (function() {
return result return result
} }
}) }.bind(this))
var onlyAttributeNames = attributes.map(function(attribute) { var onlyAttributeNames = attributes.map(function(attribute) {
return (typeof attribute === "string") ? attribute : attribute.attribute return (typeof attribute === "string") ? attribute : attribute.attribute
}) }.bind(this))
var indexTable = tableName.split('.') var indexTable = tableName.split('.')
options = Utils._.extend({ options = Utils._.extend({
...@@ -399,9 +444,9 @@ module.exports = (function() { ...@@ -399,9 +444,9 @@ module.exports = (function() {
}, options || {}) }, options || {})
return Utils._.compact([ return Utils._.compact([
"CREATE", options.indicesType, "INDEX", QueryGenerator.addQuotes(options.indexName), "CREATE", options.indicesType, "INDEX", this.quoteIdentifiers(options.indexName),
(options.indexType ? ('USING ' + options.indexType) : undefined), (options.indexType ? ('USING ' + options.indexType) : undefined),
"ON", QueryGenerator.addQuotes(tableName), '(' + transformedAttributes.join(', ') + ')' "ON", this.quoteIdentifiers(tableName), '(' + transformedAttributes.join(', ') + ')'
]).join(' ') ]).join(' ')
}, },
...@@ -419,19 +464,21 @@ module.exports = (function() { ...@@ -419,19 +464,21 @@ module.exports = (function() {
} }
return Utils._.template(sql)({ return Utils._.template(sql)({
tableName: QueryGenerator.addQuotes(tableName), tableName: this.quoteIdentifiers(tableName),
indexName: QueryGenerator.addQuotes(indexName) indexName: this.quoteIdentifiers(indexName)
}) })
}, },
getWhereConditions: function(smth) { getWhereConditions: function(smth, tableName) {
var result = null var result = null
if (Utils.isHash(smth)) { if (Utils.isHash(smth)) {
result = QueryGenerator.hashToWhereConditions(smth) smth = Utils.prependTableNameToHash(tableName, smth)
result = this.hashToWhereConditions(smth)
} }
else if (typeof smth === "number") { else if (typeof smth === "number") {
result = '\"id\"' + "=" + QueryGenerator.pgEscape(smth) smth = Utils.prependTableNameToHash(tableName, { id: smth })
result = this.hashToWhereConditions(smth)
} }
else if (typeof smth === "string") { else if (typeof smth === "string") {
result = smth result = smth
...@@ -450,23 +497,21 @@ module.exports = (function() { ...@@ -450,23 +497,21 @@ module.exports = (function() {
var value = hash[key] var value = hash[key]
//handle qualified key names //handle qualified key names
var _key = key.split('.').map(function(col){return QueryGenerator.addQuotes(col)}).join(".") var _key = this.quoteIdentifiers(key)
, _value = null , _value = null
if (Array.isArray(value)) { if (Array.isArray(value)) {
if (value.length == 0) { value = [null] } if (value.length === 0) { value = [null] }
_value = "(" + value.map(function(subValue) { _value = "(" + value.map(this.escape).join(',') + ")"
return QueryGenerator.pgEscape(subValue);
}).join(',') + ")"
result.push([_key, _value].join(" IN ")) result.push([_key, _value].join(" IN "))
} }
else if ((value) && (typeof value === "object")) { else if ((value) && (typeof value === "object")) {
//using as sentinel for join column => value //using as sentinel for join column => value
_value = value.join.split('.').map(function(col){return QueryGenerator.addQuotes(col)}).join(".") _value = this.quoteIdentifiers(value.join)
result.push([_key, _value].join("=")) result.push([_key, _value].join("="))
} else { } else {
_value = QueryGenerator.pgEscape(value) _value = this.escape(value)
result.push((_value == 'NULL') ? _key + " IS NULL" : [_key, _value].join("=")) result.push((_value == 'NULL') ? _key + " IS NULL" : [_key, _value].join("="))
} }
} }
...@@ -487,8 +532,8 @@ module.exports = (function() { ...@@ -487,8 +532,8 @@ module.exports = (function() {
if (dataType.type.toString() === DataTypes.ENUM.toString()) { if (dataType.type.toString() === DataTypes.ENUM.toString()) {
if (Array.isArray(dataType.values) && (dataType.values.length > 0)) { if (Array.isArray(dataType.values) && (dataType.values.length > 0)) {
replacements.type = "ENUM(" + Utils._.map(dataType.values, function(value) { replacements.type = "ENUM(" + Utils._.map(dataType.values, function(value) {
return Utils.escape(value) return this.escape(value)
}).join(", ") + ")" }.bind(this)).join(", ") + ")"
} else { } else {
throw new Error('Values for ENUM haven\'t been defined.') throw new Error('Values for ENUM haven\'t been defined.')
} }
...@@ -507,12 +552,12 @@ module.exports = (function() { ...@@ -507,12 +552,12 @@ module.exports = (function() {
} }
if (dataType.autoIncrement) { if (dataType.autoIncrement) {
template +=" SERIAL" template += " SERIAL"
} }
if (dataType.defaultValue !== undefined) { if (dataType.defaultValue !== undefined) {
template += " DEFAULT <%= defaultValue %>" template += " DEFAULT <%= defaultValue %>"
replacements.defaultValue = QueryGenerator.pgEscape(dataType.defaultValue) replacements.defaultValue = this.escape(dataType.defaultValue)
} }
if (dataType.unique) { if (dataType.unique) {
...@@ -523,6 +568,34 @@ module.exports = (function() { ...@@ -523,6 +568,34 @@ module.exports = (function() {
template += " PRIMARY KEY" template += " PRIMARY KEY"
} }
if(dataType.references) {
template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)"
replacements.referencesTable = this.quoteIdentifier(dataType.references)
if(dataType.referencesKey) {
replacements.referencesKey = this.quoteIdentifier(dataType.referencesKey)
} else {
replacements.referencesKey = this.quoteIdentifier('id')
}
if(dataType.onDelete) {
template += " ON DELETE <%= onDeleteAction %>"
replacements.onDeleteAction = dataType.onDelete.toUpperCase()
}
if(dataType.onUpdate) {
template += " ON UPDATE <%= onUpdateAction %>"
replacements.onUpdateAction = dataType.onUpdate.toUpperCase()
}
}
if (dataType.comment && Utils._.isString(dataType.comment)) {
template += " COMMENT ON COLUMN <%= tableName %>.<%= columnName %> IS <%= comment %>"
replacements.columnName = this.quoteIdentifier(name)
replacements.tableName = '<%= table %>' // Hacky, table name will be inserted by create table
replacements.comment = this.escape(dataType.comment)
}
result[name] = Utils._.template(template)(replacements) result[name] = Utils._.template(template)(replacements)
} else { } else {
result[name] = dataType result[name] = dataType
...@@ -546,8 +619,16 @@ module.exports = (function() { ...@@ -546,8 +619,16 @@ module.exports = (function() {
return fields return fields
}, },
enableForeignKeyConstraintsQuery: function() {
return false // not supported by dialect
},
disableForeignKeyConstraintsQuery: function() {
return false // not supported by dialect
},
databaseConnectionUri: function(config) { databaseConnectionUri: function(config) {
var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %>'; var template = '<%= protocol %>://<%= user %>:<%= password %>@<%= host %><% if(port) { %>:<%= port %><% } %>/<%= database %>'
return Utils._.template(template)({ return Utils._.template(template)({
user: encodeURIComponent(config.username), user: encodeURIComponent(config.username),
...@@ -559,49 +640,43 @@ module.exports = (function() { ...@@ -559,49 +640,43 @@ module.exports = (function() {
}) })
}, },
removeQuotes: function (s, quoteChar) { pgEscapeAndQuote: function (val) {
quoteChar = quoteChar || '"' return this.quoteIdentifier(Utils.removeTicks(this.escape(val), "'"))
return s.replace(new RegExp(quoteChar, 'g'), '')
}, },
addQuotes: function (s, quoteChar) { pgEnum: function (tableName, attr, dataType) {
quoteChar = quoteChar || '"' var enumName = this.pgEscapeAndQuote("enum_" + tableName + "_" + attr)
return QueryGenerator.removeQuotes(s, quoteChar) return "DROP TYPE IF EXISTS " + enumName + "; CREATE TYPE " + enumName + " AS " + dataType.match(/^ENUM\(.+\)/)[0] + "; "
.split('.')
.map(function(e) { return quoteChar + String(e) + quoteChar })
.join('.')
}, },
pgEscape: function (val) { fromArray: function(text) {
if (val === undefined || val === null) { text = text.replace(/^{/, '').replace(/}$/, '')
return 'NULL'; var matches = text.match(/("(?:\\.|[^"\\\\])*"|[^,]*)(?:\s*,\s*|\s*$)/ig)
}
switch (typeof val) { if (matches.length < 1) {
case 'boolean': return (val) ? 'true' : 'false'; return []
case 'number': return val+'';
case 'object':
if (Array.isArray(val)) {
return 'ARRAY['+ val.map(function(it) { return QueryGenerator.pgEscape(it) }).join(',') +']';
}
} }
if (val instanceof Date) { matches = matches.map(function(m){
val = QueryGenerator.pgSqlDate(val); return m.replace(/",$/, '').replace(/,$/, '').replace(/(^"|"$)/, '')
} })
// http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS return matches.slice(0, -1)
val = val.replace(/'/g, "''");
return "'"+val+"'";
}, },
pgEscapeAndQuote: function (val) { toHstore: function(text) {
return QueryGenerator.addQuotes(QueryGenerator.removeQuotes(QueryGenerator.pgEscape(val), "'")) var obj = {}
}, , pattern = '("\\\\.|[^"\\\\]*"\s*=|[^=]*)\s*=\s*>\s*("(?:\\.|[^"\\\\])*"|[^,]*)(?:\s*,\s*|$)'
, rex = new RegExp(pattern,'g')
, r = null
pgEnum: function (tableName, attr, dataType) { while ((r = rex.exec(text)) !== null) {
var enumName = QueryGenerator.pgEscapeAndQuote("enum_" + tableName + "_" + attr) if (!!r[1] && !!r[2]) {
return "DROP TYPE IF EXISTS " + enumName + "; CREATE TYPE " + enumName + " AS " + dataType.match(/^ENUM\(.+\)/)[0] + "; " obj[r[1].replace(/^"/, '').replace(/"$/, '')] = r[2].replace(/^"/, '').replace(/"$/, '')
}
}
return obj
}, },
padInt: function (i) { padInt: function (i) {
...@@ -609,8 +684,8 @@ module.exports = (function() { ...@@ -609,8 +684,8 @@ module.exports = (function() {
}, },
pgSqlDate: function (dt) { pgSqlDate: function (dt) {
var date = [ dt.getUTCFullYear(), QueryGenerator.padInt(dt.getUTCMonth()+1), QueryGenerator.padInt(dt.getUTCDate()) ].join('-') var date = [ dt.getUTCFullYear(), this.padInt(dt.getUTCMonth()+1), this.padInt(dt.getUTCDate()) ].join('-')
var time = [ dt.getUTCHours(), QueryGenerator.padInt(dt.getUTCMinutes()), QueryGenerator.padInt(dt.getUTCSeconds())].join(':') var time = [ dt.getUTCHours(), this.padInt(dt.getUTCMinutes()), this.padInt(dt.getUTCSeconds())].join(':')
return date + ' ' + time + '.' + ((dt.getTime() % 1000) * 1000) + 'Z' return date + ' ' + time + '.' + ((dt.getTime() % 1000) * 1000) + 'Z'
}, },
...@@ -629,17 +704,84 @@ module.exports = (function() { ...@@ -629,17 +704,84 @@ module.exports = (function() {
} }
if (Utils._.includes(dataType, 'SERIAL')) { if (Utils._.includes(dataType, 'SERIAL')) {
dataType = dataType.replace(/INTEGER/, '') if (Utils._.includes(dataType, 'BIGINT')) {
dataType = dataType.replace(/SERIAL/, 'BIGSERIAL')
dataType = dataType.replace(/BIGINT/, '')
tables[tableName][attr] = 'bigserial'
} else {
dataType = dataType.replace(/INTEGER/, '')
tables[tableName][attr] = 'serial'
}
dataType = dataType.replace(/NOT NULL/, '') dataType = dataType.replace(/NOT NULL/, '')
tables[tableName][attr] = 'serial'
} }
if (dataType.match(/^ENUM\(/)) { if (dataType.match(/^ENUM\(/)) {
dataType = dataType.replace(/^ENUM\(.+\)/, QueryGenerator.pgEscapeAndQuote("enum_" + tableName + "_" + attr)) dataType = dataType.replace(/^ENUM\(.+\)/, this.pgEscapeAndQuote("enum_" + tableName + "_" + attr))
} }
return dataType return dataType
},
quoteIdentifier: function(identifier, force) {
if(!force && this.options && this.options.quoteIdentifiers === false) { // default is `true`
// In Postgres, if tables or attributes are created double-quoted,
// they are also case sensitive. If they contain any uppercase
// characters, they must always be double-quoted. This makes it
// impossible to write queries in portable SQL if tables are created in
// this way. Hence, we strip quotes if we don't want case sensitivity.
return Utils.removeTicks(identifier, '"')
} else {
return Utils.addTicks(identifier, '"')
}
},
quoteIdentifiers: function(identifiers, force) {
return identifiers.split('.').map(function(t) { return this.quoteIdentifier(t, force) }.bind(this)).join('.')
},
escape: function (val) {
if (val === undefined || val === null) {
return 'NULL';
}
switch (typeof val) {
case 'boolean':
return (val) ? 'true' : 'false';
case 'number':
return val + '';
case 'object':
if (Array.isArray(val)) {
return 'ARRAY['+ val.map(function(it) { return this.escape(it) }.bind(this)).join(',') + ']';
}
}
if (val instanceof Date) {
val = this.pgSqlDate(val);
}
// http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS
val = val.replace(/'/g, "''");
return "'" + val + "'";
} }
}
// Private
var removeSerialsFromHash = function(tableName, attrValueHash) {
var returning = [];
Utils._.forEach(attrValueHash, function(value, key, hash) {
if (tables[tableName] && tables[tableName][key]) {
switch (tables[tableName][key]) {
case 'bigserial':
case 'serial':
delete hash[key]
returning.push(key)
break
}
}
});
return returning;
} }
return Utils._.extend(Utils._.clone(require("../query-generator")), QueryGenerator) return Utils._.extend(Utils._.clone(require("../query-generator")), QueryGenerator)
......
var Utils = require("../../utils") var Utils = require("../../utils")
, AbstractQuery = require('../abstract/query') , AbstractQuery = require('../abstract/query')
, DataTypes = require('../../data-types')
module.exports = (function() { module.exports = (function() {
var Query = function(client, sequelize, callee, options) { var Query = function(client, sequelize, callee, options) {
...@@ -55,6 +56,7 @@ module.exports = (function() { ...@@ -55,6 +56,7 @@ module.exports = (function() {
var onSuccess = function(rows) { var onSuccess = function(rows) {
var results = [] var results = []
, self = this
, isTableNameQuery = (this.sql.indexOf('SELECT table_name FROM information_schema.tables') === 0) , isTableNameQuery = (this.sql.indexOf('SELECT table_name FROM information_schema.tables') === 0)
, isRelNameQuery = (this.sql.indexOf('SELECT relname FROM pg_class WHERE oid IN') === 0) , isRelNameQuery = (this.sql.indexOf('SELECT relname FROM pg_class WHERE oid IN') === 0)
...@@ -73,14 +75,15 @@ module.exports = (function() { ...@@ -73,14 +75,15 @@ module.exports = (function() {
} }
if (this.send('isSelectQuery')) { if (this.send('isSelectQuery')) {
if (this.sql.toLowerCase().indexOf('select column_name') === 0) { if (this.sql.toLowerCase().indexOf('select c.column_name') === 0) {
var result = {} var result = {}
rows.forEach(function(_result) { rows.forEach(function(_result) {
result[_result.Field] = { result[_result.Field] = {
type: _result.Type.toUpperCase(), type: _result.Type.toUpperCase(),
allowNull: (_result.Null === 'YES'), allowNull: (_result.Null === 'YES'),
defaultValue: _result.Default defaultValue: _result.Default,
special: (!!_result.special ? self.sequelize.queryInterface.QueryGenerator.fromArray(_result.special) : [])
} }
if (result[_result.Field].type === 'BOOLEAN') { if (result[_result.Field].type === 'BOOLEAN') {
...@@ -95,29 +98,58 @@ module.exports = (function() { ...@@ -95,29 +98,58 @@ module.exports = (function() {
result[_result.Field].defaultValue = result[_result.Field].defaultValue.replace(/'/g, "") result[_result.Field].defaultValue = result[_result.Field].defaultValue.replace(/'/g, "")
if (result[_result.Field].defaultValue.indexOf('::') > -1) { if (result[_result.Field].defaultValue.indexOf('::') > -1) {
result[_result.Field].defaultValue = result[_result.Field].defaultValue.split('::')[0] var split = result[_result.Field].defaultValue.split('::');
if (split[1].toLowerCase() !== "regclass)") {
result[_result.Field].defaultValue = split[0]
}
} }
} }
}) })
this.emit('success', result) this.emit('success', result)
} else { } else {
// Postgres will treat tables as case-insensitive, so fix the case
// of the returned values to match attributes
if(this.sequelize.options.quoteIdentifiers == false) {
var attrsMap = Utils._.reduce(this.callee.attributes, function(m, v, k) { m[k.toLowerCase()] = k; return m}, {})
rows.forEach(function(row) {
Utils._.keys(row).forEach(function(key) {
var targetAttr = attrsMap[key]
if(targetAttr != key) {
row[targetAttr] = row[key]
delete row[key]
}
})
})
}
this.emit('success', this.send('handleSelectQuery', rows)) this.emit('success', this.send('handleSelectQuery', rows))
} }
} else if (this.send('isShowOrDescribeQuery')) { } else if (this.send('isShowOrDescribeQuery')) {
this.emit('success', results) this.emit('success', results)
} else if (this.send('isInsertQuery')) { } else if (this.send('isInsertQuery')) {
for (var key in rows[0]) { if(this.callee !== null) { // may happen for bulk inserts
if (rows[0].hasOwnProperty(key)) { for (var key in rows[0]) {
this.callee[key] = rows[0][key] if (rows[0].hasOwnProperty(key)) {
var record = rows[0][key]
if (!!this.callee.daoFactory && !!this.callee.daoFactory.rawAttributes && !!this.callee.daoFactory.rawAttributes[key] && !!this.callee.daoFactory.rawAttributes[key].type && !!this.callee.daoFactory.rawAttributes[key].type.type && this.callee.daoFactory.rawAttributes[key].type.type === DataTypes.HSTORE.type) {
record = this.callee.daoFactory.daoFactoryManager.sequelize.queryInterface.QueryGenerator.toHstore(record)
}
this.callee[key] = record
}
} }
} }
this.emit('success', this.callee) this.emit('success', this.callee)
} else if (this.send('isUpdateQuery')) { } else if (this.send('isUpdateQuery')) {
for (var key in rows[0]) { if(this.callee !== null) { // may happen for bulk updates
if (rows[0].hasOwnProperty(key)) { for (var key in rows[0]) {
this.callee[key] = rows[0][key] if (rows[0].hasOwnProperty(key)) {
var record = rows[0][key]
if (!!this.callee.daoFactory && !!this.callee.daoFactory.rawAttributes && !!this.callee.daoFactory.rawAttributes[key] && !!this.callee.daoFactory.rawAttributes[key].type && !!this.callee.daoFactory.rawAttributes[key].type.type && this.callee.daoFactory.rawAttributes[key].type.type === DataTypes.HSTORE.type) {
record = this.callee.daoFactory.daoFactoryManager.sequelize.queryInterface.QueryGenerator.toHstore(record)
}
this.callee[key] = record
}
} }
} }
......
module.exports = (function() { module.exports = (function() {
var QueryGenerator = { var QueryGenerator = {
addSchema: function(opts) {
throwMethodUndefined('addSchema')
},
/* /*
Returns a query for creating a table. Returns a query for creating a table.
Parameters: Parameters:
...@@ -115,6 +119,14 @@ module.exports = (function() { ...@@ -115,6 +119,14 @@ module.exports = (function() {
}, },
/* /*
Returns an insert into command for multiple values.
Parameters: table name + list of hashes of attribute-value-pairs.
*/
bulkInsertQuery: function(tableName, attrValueHashes) {
throwMethodUndefined('bulkInsertQuery')
},
/*
Returns an update query. Returns an update query.
Parameters: Parameters:
- tableName -> Name of the table - tableName -> Name of the table
...@@ -138,12 +150,30 @@ module.exports = (function() { ...@@ -138,12 +150,30 @@ module.exports = (function() {
If you use a string, you have to escape it on your own. If you use a string, you have to escape it on your own.
Options: Options:
- limit -> Maximaum count of lines to delete - limit -> Maximaum count of lines to delete
- truncate -> boolean - whether to use an 'optimized' mechanism (i.e. TRUNCATE) if available,
note that this should not be the default behaviour because TRUNCATE does not
always play nicely (e.g. InnoDB tables with FK constraints)
(@see http://dev.mysql.com/doc/refman/5.6/en/truncate-table.html).
Note that truncate must ignore limit and where
*/ */
deleteQuery: function(tableName, where, options) { deleteQuery: function(tableName, where, options) {
throwMethodUndefined('deleteQuery') throwMethodUndefined('deleteQuery')
}, },
/* /*
Returns a bulk deletion query.
Parameters:
- tableName -> Name of the table
- where -> A hash with conditions (e.g. {name: 'foo'})
OR an ID as integer
OR a string with conditions (e.g. 'name="foo"').
If you use a string, you have to escape it on your own.
*/
bulkDeleteQuery: function(tableName, where, options) {
throwMethodUndefined('bulkDeleteQuery')
},
/*
Returns an update query. Returns an update query.
Parameters: Parameters:
- tableName -> Name of the table - tableName -> Name of the table
...@@ -225,7 +255,43 @@ module.exports = (function() { ...@@ -225,7 +255,43 @@ module.exports = (function() {
*/ */
findAutoIncrementField: function(factory) { findAutoIncrementField: function(factory) {
throwMethodUndefined('findAutoIncrementField') throwMethodUndefined('findAutoIncrementField')
},
/*
Globally enable foreign key constraints
*/
enableForeignKeyConstraintsQuery: function() {
throwMethodUndefined('enableForeignKeyConstraintsQuery')
},
/*
Globally disable foreign key constraints
*/
disableForeignKeyConstraintsQuery: function() {
throwMethodUndefined('disableForeignKeyConstraintsQuery')
},
/*
Escape an identifier (e.g. a table or attribute name)
*/
quoteIdentifier: function(identifier, force) {
throwMethodUndefined('quoteIdentifier')
},
/*
Split an identifier into .-separated tokens and quote each part
*/
quoteIdentifiers: function(identifiers, force) {
throwMethodUndefined('quoteIdentifiers')
},
/*
Escape a value (e.g. a string, number or date)
*/
escape: function(value) {
throwMethodUndefined('quoteIdentifier')
} }
} }
var throwMethodUndefined = function(methodName) { var throwMethodUndefined = function(methodName) {
......
...@@ -5,7 +5,13 @@ var Utils = require("../../utils") ...@@ -5,7 +5,13 @@ var Utils = require("../../utils")
module.exports = (function() { module.exports = (function() {
var ConnectorManager = function(sequelize) { var ConnectorManager = function(sequelize) {
this.sequelize = sequelize this.sequelize = sequelize
this.database = new sqlite3.Database(sequelize.options.storage || ':memory:') this.database = db = new sqlite3.Database(sequelize.options.storage || ':memory:', function(err) {
if(!err && sequelize.options.foreignKeys !== false) {
// Make it possible to define and use foreign key constraints unless
// explicitly disallowed. It's still opt-in per relation
db.run('PRAGMA FOREIGN_KEYS=ON')
}
})
} }
Utils._.extend(ConnectorManager.prototype, require("../connector-manager").prototype) Utils._.extend(ConnectorManager.prototype, require("../connector-manager").prototype)
......
...@@ -8,22 +8,43 @@ var MySqlQueryGenerator = Utils._.extend( ...@@ -8,22 +8,43 @@ var MySqlQueryGenerator = Utils._.extend(
var hashToWhereConditions = MySqlQueryGenerator.hashToWhereConditions var hashToWhereConditions = MySqlQueryGenerator.hashToWhereConditions
var escape = function(str) {
if (typeof str === 'string') {
return "'" + str.replace(/'/g, "''") + "'";
} else if (typeof str === 'boolean') {
return str ? 1 : 0; // SQLite has no type boolean
} else if (str === null || str === undefined) {
return 'NULL';
} else {
return str;
}
};
module.exports = (function() { module.exports = (function() {
var QueryGenerator = { var QueryGenerator = {
options: {}, options: {},
addSchema: function(opts) {
var tableName = undefined
var schema = (!!opts && !!opts.options && !!opts.options.schema ? opts.options.schema : undefined)
var schemaPrefix = (!!opts && !!opts.options && !!opts.options.schemaPrefix ? opts.options.schemaPrefix : undefined)
if (!!opts && !!opts.tableName) {
tableName = opts.tableName
}
else if (typeof opts === "string") {
tableName = opts
}
if (!schema || schema.toString().trim() === "") {
return tableName
}
return this.quoteIdentifier(schema) + (!schemaPrefix ? '.' : schemaPrefix) + this.quoteIdentifier(tableName)
},
createSchema: function() {
var query = "SELECT name FROM sqlite_master WHERE type='table' and name!='sqlite_sequence';"
return Utils._.template(query)({})
},
dropSchema: function() {
var query = "SELECT name FROM sqlite_master WHERE type='table' and name!='sqlite_sequence';"
return Utils._.template(query)({})
},
showSchemasQuery: function() {
return "SELECT name FROM sqlite_master WHERE type='table' and name!='sqlite_sequence';"
},
createTableQuery: function(tableName, attributes, options) { createTableQuery: function(tableName, attributes, options) {
options = options || {} options = options || {}
...@@ -39,33 +60,37 @@ module.exports = (function() { ...@@ -39,33 +60,37 @@ module.exports = (function() {
if (attributes.hasOwnProperty(attr)) { if (attributes.hasOwnProperty(attr)) {
var dataType = attributes[attr] var dataType = attributes[attr]
if (Utils._.includes(dataType, 'AUTOINCREMENT')) {
dataType = dataType.replace(/BIGINT/, 'INTEGER')
}
if (Utils._.includes(dataType, 'PRIMARY KEY') && needsMultiplePrimaryKeys) { if (Utils._.includes(dataType, 'PRIMARY KEY') && needsMultiplePrimaryKeys) {
primaryKeys.push(attr) primaryKeys.push(attr)
attrStr.push(Utils.addTicks(attr) + " " + dataType.replace(/PRIMARY KEY/, 'NOT NULL')) attrStr.push(this.quoteIdentifier(attr) + " " + dataType.replace(/PRIMARY KEY/, 'NOT NULL'))
} else { } else {
attrStr.push(Utils.addTicks(attr) + " " + dataType) attrStr.push(this.quoteIdentifier(attr) + " " + dataType)
} }
} }
} }
var values = { var values = {
table: Utils.addTicks(tableName), table: this.quoteIdentifier(tableName),
attributes: attrStr.join(", "), attributes: attrStr.join(", "),
charset: (options.charset ? "DEFAULT CHARSET=" + options.charset : "") charset: (options.charset ? "DEFAULT CHARSET=" + options.charset : "")
} }
, pkString = primaryKeys.map(function(pk) { return Utils.addTicks(pk) }).join(", ") , pkString = primaryKeys.map(function(pk) { return this.quoteIdentifier(pk) }.bind(this)).join(", ")
if (pkString.length > 0) { if (pkString.length > 0) {
values.attributes += ", PRIMARY KEY (" + pkString + ")" values.attributes += ", PRIMARY KEY (" + pkString + ")"
} }
var sql = Utils._.template(query, values).trim() + ";" var sql = Utils._.template(query, values).trim() + ";"
return QueryGenerator.replaceBooleanDefaults(sql) return this.replaceBooleanDefaults(sql)
}, },
addColumnQuery: function() { addColumnQuery: function() {
var sql = MySqlQueryGenerator.addColumnQuery.apply(null, arguments) var sql = MySqlQueryGenerator.addColumnQuery.apply(this, arguments)
return QueryGenerator.replaceBooleanDefaults(sql) return this.replaceBooleanDefaults(sql)
}, },
showTablesQuery: function() { showTablesQuery: function() {
...@@ -78,16 +103,105 @@ module.exports = (function() { ...@@ -78,16 +103,105 @@ module.exports = (function() {
var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES (<%= values %>);"; var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES (<%= values %>);";
var replacements = { var replacements = {
table: Utils.addTicks(tableName), table: this.quoteIdentifier(tableName),
attributes: Object.keys(attrValueHash).map(function(attr){return Utils.addTicks(attr)}).join(","), attributes: Object.keys(attrValueHash).map(function(attr){return this.quoteIdentifier(attr)}.bind(this)).join(","),
values: Utils._.values(attrValueHash).map(function(value){ values: Utils._.values(attrValueHash).map(function(value){
return escape((value instanceof Date) ? Utils.toSqlDate(value) : value) return this.escape(value)
}).join(",") }.bind(this)).join(",")
} }
return Utils._.template(query)(replacements) return Utils._.template(query)(replacements)
}, },
bulkInsertQuery: function(tableName, attrValueHashes) {
var query = "INSERT INTO <%= table %> (<%= attributes %>) VALUES <%= tuples %>;"
, tuples = []
Utils._.forEach(attrValueHashes, function(attrValueHash) {
tuples.push("(" +
Utils._.values(attrValueHash).map(function(value){
return this.escape(value)
}.bind(this)).join(",") +
")")
}.bind(this))
var replacements = {
table: this.quoteIdentifier(tableName),
attributes: Object.keys(attrValueHashes[0]).map(function(attr){return this.quoteIdentifier(attr)}.bind(this)).join(","),
tuples: tuples
}
return Utils._.template(query)(replacements)
},
selectQuery: function(tableName, options) {
var table = null,
joinQuery = ""
options = options || {}
options.table = table = Array.isArray(tableName) ? tableName.map(function(t) { return this.quoteIdentifier(t)}.bind(this)).join(", ") : this.quoteIdentifier(tableName)
options.attributes = options.attributes && options.attributes.map(function(attr){
if(Array.isArray(attr) && attr.length == 2) {
return [attr[0], this.quoteIdentifier(attr[1])].join(' as ')
} else {
return attr.indexOf(Utils.TICK_CHAR) < 0 ? this.quoteIdentifiers(attr) : attr
}
}.bind(this)).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 this.quoteIdentifier(include.as) + "." + this.quoteIdentifier(attr) + " AS " + this.quoteIdentifier(include.as + "." + attr)
}.bind(this))
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 " + this.quoteIdentifier(table) + " AS " + this.quoteIdentifier(as) + " ON " + this.quoteIdentifier(tableLeft) + "." + this.quoteIdentifier(attrLeft) + " = " + this.quoteIdentifier(tableRight) + "." + this.quoteIdentifier(attrRight) + ""
}.bind(this))
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(t) { return this.quoteIdentifiers(t)}.bind(this)).join(', ') : qa(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) { updateQuery: function(tableName, attrValueHash, where) {
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull) attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, this.options.omitNull)
...@@ -96,13 +210,13 @@ module.exports = (function() { ...@@ -96,13 +210,13 @@ module.exports = (function() {
for (var key in attrValueHash) { for (var key in attrValueHash) {
var value = attrValueHash[key] var value = attrValueHash[key]
values.push(Utils.addTicks(key) + "=" + escape((value instanceof Date) ? Utils.toSqlDate(value) : value)) values.push(this.quoteIdentifier(key) + "=" + this.escape(value))
} }
var replacements = { var replacements = {
table: Utils.addTicks(tableName), table: this.quoteIdentifier(tableName),
values: values.join(","), values: values.join(","),
where: MySqlQueryGenerator.getWhereConditions(where) where: this.getWhereConditions(where)
} }
return Utils._.template(query)(replacements) return Utils._.template(query)(replacements)
...@@ -113,9 +227,8 @@ module.exports = (function() { ...@@ -113,9 +227,8 @@ module.exports = (function() {
var query = "DELETE FROM <%= table %> WHERE <%= where %>" var query = "DELETE FROM <%= table %> WHERE <%= where %>"
var replacements = { var replacements = {
table: Utils.addTicks(tableName), table: this.quoteIdentifier(tableName),
where: this.getWhereConditions(where), where: this.getWhereConditions(where)
limit: Utils.escape(options.limit)
} }
return Utils._.template(query)(replacements) return Utils._.template(query)(replacements)
...@@ -129,13 +242,13 @@ module.exports = (function() { ...@@ -129,13 +242,13 @@ module.exports = (function() {
for (var key in attrValueHash) { for (var key in attrValueHash) {
var value = attrValueHash[key] var value = attrValueHash[key]
values.push(Utils.addTicks(key) + "=" + Utils.addTicks(key) + "+ " + escape((value instanceof Date) ? Utils.toSqlDate(value) : value)) values.push(this.quoteIdentifier(key) + "=" + this.quoteIdentifier(key) + "+ " + this.escape(value))
} }
var replacements = { var replacements = {
table: Utils.addTicks(tableName), table: this.quoteIdentifier(tableName),
values: values.join(","), values: values.join(","),
where: MySqlQueryGenerator.getWhereConditions(where) where: this.getWhereConditions(where)
} }
return Utils._.template(query)(replacements) return Utils._.template(query)(replacements)
...@@ -165,7 +278,7 @@ module.exports = (function() { ...@@ -165,7 +278,7 @@ module.exports = (function() {
if (dataType.defaultValue !== undefined) { if (dataType.defaultValue !== undefined) {
template += " DEFAULT <%= defaultValue %>" template += " DEFAULT <%= defaultValue %>"
replacements.defaultValue = Utils.escape(dataType.defaultValue) replacements.defaultValue = this.escape(dataType.defaultValue)
} }
if (dataType.unique) { if (dataType.unique) {
...@@ -180,6 +293,28 @@ module.exports = (function() { ...@@ -180,6 +293,28 @@ module.exports = (function() {
} }
} }
if(dataType.references) {
template += " REFERENCES <%= referencesTable %> (<%= referencesKey %>)"
replacements.referencesTable = this.quoteIdentifier(dataType.references)
if(dataType.referencesKey) {
replacements.referencesKey = this.quoteIdentifier(dataType.referencesKey)
} else {
replacements.referencesKey = this.quoteIdentifier('id')
}
if(dataType.onDelete) {
template += " ON DELETE <%= onDeleteAction %>"
replacements.onDeleteAction = dataType.onDelete.toUpperCase()
}
if(dataType.onUpdate) {
template += " ON UPDATE <%= onUpdateAction %>"
replacements.onUpdateAction = dataType.onUpdate.toUpperCase()
}
}
result[name] = Utils._.template(template)(replacements) result[name] = Utils._.template(template)(replacements)
} else { } else {
result[name] = dataType result[name] = dataType
...@@ -205,6 +340,16 @@ module.exports = (function() { ...@@ -205,6 +340,16 @@ module.exports = (function() {
return fields return fields
}, },
enableForeignKeyConstraintsQuery: function() {
var sql = "PRAGMA foreign_keys = ON;"
return Utils._.template(sql, {})
},
disableForeignKeyConstraintsQuery: function() {
var sql = "PRAGMA foreign_keys = OFF;"
return Utils._.template(sql, {})
},
hashToWhereConditions: function(hash) { hashToWhereConditions: function(hash) {
for (var key in hash) { for (var key in hash) {
if (hash.hasOwnProperty(key)) { if (hash.hasOwnProperty(key)) {
...@@ -218,7 +363,7 @@ module.exports = (function() { ...@@ -218,7 +363,7 @@ module.exports = (function() {
} }
} }
return hashToWhereConditions(hash).replace(/\\'/g, "''"); return hashToWhereConditions.call(this, hash).replace(/\\'/g, "''");
}, },
showIndexQuery: function(tableName) { showIndexQuery: function(tableName) {
...@@ -248,14 +393,14 @@ module.exports = (function() { ...@@ -248,14 +393,14 @@ module.exports = (function() {
}, },
removeColumnQuery: function(tableName, attributes) { removeColumnQuery: function(tableName, attributes) {
attributes = QueryGenerator.attributesToSQL(attributes) attributes = this.attributesToSQL(attributes)
var backupTableName = tableName + "_backup" var backupTableName = tableName + "_backup"
var query = [ var query = [
QueryGenerator.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE'), this.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE'),
"INSERT INTO <%= tableName %>_backup SELECT <%= attributeNames %> FROM <%= tableName %>;", "INSERT INTO <%= tableName %>_backup SELECT <%= attributeNames %> FROM <%= tableName %>;",
"DROP TABLE <%= tableName %>;", "DROP TABLE <%= tableName %>;",
QueryGenerator.createTableQuery(tableName, attributes), this.createTableQuery(tableName, attributes),
"INSERT INTO <%= tableName %> SELECT <%= attributeNames %> FROM <%= tableName %>_backup;", "INSERT INTO <%= tableName %> SELECT <%= attributeNames %> FROM <%= tableName %>_backup;",
"DROP TABLE <%= tableName %>_backup;" "DROP TABLE <%= tableName %>_backup;"
].join("") ].join("")
...@@ -267,14 +412,14 @@ module.exports = (function() { ...@@ -267,14 +412,14 @@ module.exports = (function() {
}, },
renameColumnQuery: function(tableName, attrNameBefore, attrNameAfter, attributes) { renameColumnQuery: function(tableName, attrNameBefore, attrNameAfter, attributes) {
attributes = QueryGenerator.attributesToSQL(attributes) attributes = this.attributesToSQL(attributes)
var backupTableName = tableName + "_backup" var backupTableName = tableName + "_backup"
var query = [ var query = [
QueryGenerator.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE'), this.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE'),
"INSERT INTO <%= tableName %>_backup SELECT <%= attributeNamesImport %> FROM <%= tableName %>;", "INSERT INTO <%= tableName %>_backup SELECT <%= attributeNamesImport %> FROM <%= tableName %>;",
"DROP TABLE <%= tableName %>;", "DROP TABLE <%= tableName %>;",
QueryGenerator.createTableQuery(tableName, attributes), this.createTableQuery(tableName, attributes),
"INSERT INTO <%= tableName %> SELECT <%= attributeNamesExport %> FROM <%= tableName %>_backup;", "INSERT INTO <%= tableName %> SELECT <%= attributeNamesExport %> FROM <%= tableName %>_backup;",
"DROP TABLE <%= tableName %>_backup;" "DROP TABLE <%= tableName %>_backup;"
].join("") ].join("")
...@@ -283,16 +428,41 @@ module.exports = (function() { ...@@ -283,16 +428,41 @@ module.exports = (function() {
tableName: tableName, tableName: tableName,
attributeNamesImport: Utils._.keys(attributes).map(function(attr) { attributeNamesImport: Utils._.keys(attributes).map(function(attr) {
return (attrNameAfter === attr) ? attrNameBefore + ' AS ' + attr : attr return (attrNameAfter === attr) ? attrNameBefore + ' AS ' + attr : attr
}).join(', '), }.bind(this)).join(', '),
attributeNamesExport: Utils._.keys(attributes).map(function(attr) { attributeNamesExport: Utils._.keys(attributes).map(function(attr) {
return attr return attr
}).join(', ') }.bind(this)).join(', ')
}) })
}, },
replaceBooleanDefaults: function(sql) { replaceBooleanDefaults: function(sql) {
return sql.replace(/DEFAULT '?false'?/g, "DEFAULT 0").replace(/DEFAULT '?true'?/g, "DEFAULT 1") return sql.replace(/DEFAULT '?false'?/g, "DEFAULT 0").replace(/DEFAULT '?true'?/g, "DEFAULT 1")
},
quoteIdentifier: function(identifier, force) {
return Utils.addTicks(identifier, "`")
},
quoteIdentifiers: function(identifiers, force) {
return identifiers.split('.').map(function(v) { return this.quoteIdentifier(v, force) }.bind(this)).join('.')
},
escape: function(value) {
if (value instanceof Date) {
value = Utils.toSqlDate(value)
}
if (typeof value === 'string') {
return "'" + value.replace(/'/g, "''") + "'";
} else if (typeof value === 'boolean') {
return value ? 1 : 0; // SQLite has no type boolean
} else if (value === null || value === undefined) {
return 'NULL';
} else {
return value;
}
} }
} }
return Utils._.extend({}, MySqlQueryGenerator, QueryGenerator) return Utils._.extend({}, MySqlQueryGenerator, QueryGenerator)
......
...@@ -91,7 +91,10 @@ module.exports = (function() { ...@@ -91,7 +91,10 @@ module.exports = (function() {
results = results.map(function(result) { results = results.map(function(result) {
for (var name in result) { for (var name in result) {
if (result.hasOwnProperty(name) && (metaData.columnTypes[name] === 'DATETIME')) { if (result.hasOwnProperty(name) && (metaData.columnTypes[name] === 'DATETIME')) {
result[name] = new Date(result[name]+'Z'); // Z means UTC var val = result[name];
if(val !== null) {
result[name] = new Date(val+'Z'); // Z means UTC
}
} }
} }
return result return result
......
var util = require("util") var util = require("util")
, EventEmitter = require("events").EventEmitter , 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() { module.exports = (function() {
var CustomEventEmitter = function(fct) { var CustomEventEmitter = function(fct) {
this.fct = fct this.fct = bindToProcess(fct);
} }
util.inherits(CustomEventEmitter, EventEmitter) util.inherits(CustomEventEmitter, EventEmitter)
...@@ -13,14 +26,14 @@ module.exports = (function() { ...@@ -13,14 +26,14 @@ module.exports = (function() {
this.fct.call(this, this) this.fct.call(this, this)
} }
}.bind(this)) }.bind(this))
return this return this
} }
CustomEventEmitter.prototype.success = CustomEventEmitter.prototype.success =
CustomEventEmitter.prototype.ok = CustomEventEmitter.prototype.ok =
function(fct) { function(fct) {
this.on('success', fct) this.on('success', bindToProcess(fct))
return this return this
} }
...@@ -28,18 +41,39 @@ module.exports = (function() { ...@@ -28,18 +41,39 @@ module.exports = (function() {
CustomEventEmitter.prototype.fail = CustomEventEmitter.prototype.fail =
CustomEventEmitter.prototype.error = CustomEventEmitter.prototype.error =
function(fct) { function(fct) {
this.on('error', fct) this.on('error', bindToProcess(fct))
return this; return this;
} }
CustomEventEmitter.prototype.done = CustomEventEmitter.prototype.done =
CustomEventEmitter.prototype.complete = CustomEventEmitter.prototype.complete =
function(fct) { function(fct) {
fct = bindToProcess(fct);
this.on('error', function(err) { fct(err, null) }) this.on('error', function(err) { fct(err, null) })
.on('success', function(result) { fct(null, result) }) .on('success', function(result) { fct(null, result) })
return this return this
} }
CustomEventEmitter.prototype.proxy = function(emitter) {
proxyEventKeys.forEach(function (eventKey) {
this.on(eventKey, function (result) {
emitter.emit(eventKey, result)
})
}.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; return CustomEventEmitter;
})() })()
...@@ -12,7 +12,8 @@ module.exports = (function() { ...@@ -12,7 +12,8 @@ module.exports = (function() {
path: __dirname + '/../migrations', path: __dirname + '/../migrations',
from: null, from: null,
to: null, to: null,
logging: console.log logging: console.log,
filesFilter: /\.js$/
}, options || {}) }, options || {})
if (this.options.logging === true) { if (this.options.logging === true) {
...@@ -52,16 +53,26 @@ module.exports = (function() { ...@@ -52,16 +53,26 @@ module.exports = (function() {
migrations.reverse() migrations.reverse()
} }
if (migrations.length === 0) {
self.options.logging("There are no pending migrations.")
} else {
self.options.logging("Running migrations...")
}
migrations.forEach(function(migration) { migrations.forEach(function(migration) {
var migrationTime
chainer.add(migration, 'execute', [options], { chainer.add(migration, 'execute', [options], {
before: function(migration) { before: function(migration) {
if (self.options.logging !== false) { if (self.options.logging !== false) {
self.options.logging('Executing migration: ' + migration.filename) self.options.logging(migration.filename)
} }
migrationTime = process.hrtime()
}, },
after: function(migration) { after: function(migration) {
migrationTime = process.hrtime(migrationTime)
migrationTime = Math.round( (migrationTime[0] * 1000) + (migrationTime[1] / 1000000));
if (self.options.logging !== false) { if (self.options.logging !== false) {
self.options.logging('Executed migration: ' + migration.filename) self.options.logging('Completed in ' + migrationTime + 'ms')
} }
}, },
success: function(migration, callback) { success: function(migration, callback) {
...@@ -96,7 +107,7 @@ module.exports = (function() { ...@@ -96,7 +107,7 @@ module.exports = (function() {
} }
var migrationFiles = fs.readdirSync(this.options.path).filter(function(file) { var migrationFiles = fs.readdirSync(this.options.path).filter(function(file) {
return /\.js$/.test(file) return self.options.filesFilter.test(file)
}) })
var migrations = migrationFiles.map(function(file) { var migrations = migrationFiles.map(function(file) {
......
...@@ -10,6 +10,58 @@ module.exports = (function() { ...@@ -10,6 +10,58 @@ module.exports = (function() {
} }
Utils.addEventEmitter(QueryInterface) Utils.addEventEmitter(QueryInterface)
QueryInterface.prototype.createSchema = function(schema) {
var sql = this.QueryGenerator.createSchema(schema)
return queryAndEmit.call(this, sql, 'createSchema')
}
QueryInterface.prototype.dropSchema = function(schema) {
var sql = this.QueryGenerator.dropSchema(schema)
return queryAndEmit.call(this, sql, 'dropSchema')
}
QueryInterface.prototype.dropAllSchemas = function() {
var self = this
return new Utils.CustomEventEmitter(function(emitter) {
var chainer = new Utils.QueryChainer()
self.showAllSchemas().success(function(schemaNames) {
schemaNames.forEach(function(schemaName) {
chainer.add(self.dropSchema(schemaName))
})
chainer
.run()
.success(function() {
self.emit('dropAllSchemas', null)
emitter.emit('success', null)
})
.error(function(err) {
self.emit('dropAllSchemas', err)
emitter.emit('error', err)
})
}).error(function(err) {
self.emit('dropAllSchemas', err)
emitter.emit('error', err)
})
}).run()
}
QueryInterface.prototype.showAllSchemas = function() {
var self = this
return new Utils.CustomEventEmitter(function(emitter) {
var showSchemasSql = self.QueryGenerator.showSchemasQuery()
self.sequelize.query(showSchemasSql, null, { raw: true }).success(function(schemaNames) {
self.emit('showAllSchemas', null)
emitter.emit('success', Utils._.flatten(Utils._.map(schemaNames, function(value){ return value.schema_name })))
}).error(function(err) {
self.emit('showAllSchemas', err)
emitter.emit('error', err)
})
}).run()
}
QueryInterface.prototype.createTable = function(tableName, attributes, options) { QueryInterface.prototype.createTable = function(tableName, attributes, options) {
var attributeHashes = {} var attributeHashes = {}
...@@ -27,8 +79,8 @@ module.exports = (function() { ...@@ -27,8 +79,8 @@ module.exports = (function() {
return queryAndEmit.call(this, sql, 'createTable') return queryAndEmit.call(this, sql, 'createTable')
} }
QueryInterface.prototype.dropTable = function(tableName) { QueryInterface.prototype.dropTable = function(tableName, options) {
var sql = this.QueryGenerator.dropTableQuery(tableName) var sql = this.QueryGenerator.dropTableQuery(tableName, options)
return queryAndEmit.call(this, sql, 'dropTable') return queryAndEmit.call(this, sql, 'dropTable')
} }
...@@ -39,11 +91,17 @@ module.exports = (function() { ...@@ -39,11 +91,17 @@ module.exports = (function() {
var chainer = new Utils.QueryChainer() var chainer = new Utils.QueryChainer()
self.showAllTables().success(function(tableNames) { self.showAllTables().success(function(tableNames) {
chainer.add(self, 'disableForeignKeyConstraints', [])
tableNames.forEach(function(tableName) { tableNames.forEach(function(tableName) {
chainer.add(self.dropTable(tableName)) chainer.add(self, 'dropTable', [tableName, {cascade: true}])
}) })
chainer.add(self, 'enableForeignKeyConstraints', [])
chainer chainer
.run() .runSerially()
.success(function() { .success(function() {
self.emit('dropAllTables', null) self.emit('dropAllTables', null)
emitter.emit('success', null) emitter.emit('success', null)
...@@ -202,16 +260,31 @@ module.exports = (function() { ...@@ -202,16 +260,31 @@ module.exports = (function() {
}) })
} }
QueryInterface.prototype.bulkInsert = function(tableName, records) {
var sql = this.QueryGenerator.bulkInsertQuery(tableName, records)
return queryAndEmit.call(this, sql, 'bulkInsert')
}
QueryInterface.prototype.update = function(dao, tableName, values, identifier) { QueryInterface.prototype.update = function(dao, tableName, values, identifier) {
var sql = this.QueryGenerator.updateQuery(tableName, values, identifier) var sql = this.QueryGenerator.updateQuery(tableName, values, identifier)
return queryAndEmit.call(this, [sql, dao], 'update') return queryAndEmit.call(this, [sql, dao], 'update')
} }
QueryInterface.prototype.bulkUpdate = function(tableName, values, identifier) {
var sql = this.QueryGenerator.updateQuery(tableName, values, identifier)
return queryAndEmit.call(this, sql, 'bulkUpdate')
}
QueryInterface.prototype.delete = function(dao, tableName, identifier) { QueryInterface.prototype.delete = function(dao, tableName, identifier) {
var sql = this.QueryGenerator.deleteQuery(tableName, identifier) var sql = this.QueryGenerator.deleteQuery(tableName, identifier)
return queryAndEmit.call(this, [sql, dao], 'delete') return queryAndEmit.call(this, [sql, dao], 'delete')
} }
QueryInterface.prototype.bulkDelete = function(tableName, identifier, options) {
var sql = this.QueryGenerator.deleteQuery(tableName, identifier, Utils._.defaults(options || {}, {limit: null}))
return queryAndEmit.call(this, sql, 'bulkDelete')
}
QueryInterface.prototype.select = function(factory, tableName, options, queryOptions) { QueryInterface.prototype.select = function(factory, tableName, options, queryOptions) {
options = options || {} options = options || {}
...@@ -244,6 +317,10 @@ module.exports = (function() { ...@@ -244,6 +317,10 @@ module.exports = (function() {
result = parseInt(result) result = parseInt(result)
} }
if (options && options.parseFloat) {
result = parseFloat(result)
}
self.emit('rawSelect', null) self.emit('rawSelect', null)
emitter.emit('success', result) emitter.emit('success', result)
}) })
...@@ -257,6 +334,57 @@ module.exports = (function() { ...@@ -257,6 +334,57 @@ module.exports = (function() {
}).run() }).run()
} }
QueryInterface.prototype.enableForeignKeyConstraints = function() {
var sql = this.QueryGenerator.enableForeignKeyConstraintsQuery()
if(sql) {
return queryAndEmit.call(this, sql, 'enableForeignKeyConstraints')
} else {
return new Utils.CustomEventEmitter(function(emitter) {
this.emit('enableForeignKeyConstraints', null)
emitter.emit('success')
}).run()
}
}
QueryInterface.prototype.disableForeignKeyConstraints = function() {
var sql = this.QueryGenerator.disableForeignKeyConstraintsQuery()
if(sql){
return queryAndEmit.call(this, sql, 'disableForeignKeyConstraints')
} else {
return new Utils.CustomEventEmitter(function(emitter) {
this.emit('disableForeignKeyConstraints', null)
emitter.emit('success')
}).run()
}
}
// Helper methods useful for querying
/**
* Escape an identifier (e.g. a table or attribute name). If force is true,
* the identifier will be quoted even if the `quoteIdentifiers` option is
* false.
*/
QueryInterface.prototype.quoteIdentifier = function(identifier, force) {
return this.QueryGenerator.quoteIdentifier(identifier, force)
}
/**
* Split an identifier into .-separated tokens and quote each part.
* If force is true, the identifier will be quoted even if the
* `quoteIdentifiers` option is false.
*/
QueryInterface.prototype.quoteIdentifiers = function(identifiers, force) {
return this.QueryGenerator.quoteIdentifiers(identifiers, force)
}
/**
* Escape a value (e.g. a string, number or date)
*/
QueryInterface.prototype.escape = function(value) {
return this.QueryGenerator.escape(value)
}
// private // private
var queryAndEmit = function(sqlOrQueryParams, methodName, options, emitter) { var queryAndEmit = function(sqlOrQueryParams, methodName, options, emitter) {
......
var Utils = require("./utils") var url = require("url")
, Utils = require("./utils")
, DAOFactory = require("./dao-factory") , DAOFactory = require("./dao-factory")
, DataTypes = require('./data-types') , DataTypes = require('./data-types')
, DAOFactoryManager = require("./dao-factory-manager") , DAOFactoryManager = require("./dao-factory-manager")
...@@ -25,6 +26,7 @@ module.exports = (function() { ...@@ -25,6 +26,7 @@ module.exports = (function() {
@param {Boolean} [options.native=false] A flag that defines if native library shall be used or not. @param {Boolean} [options.native=false] A flag that defines if native library shall be used or not.
@param {Boolean} [options.replication=false] I have absolutely no idea. @param {Boolean} [options.replication=false] I have absolutely no idea.
@param {Object} [options.pool={}] Something. @param {Object} [options.pool={}] Something.
@param {Boolean} [options.quoteIdentifiers=true] Set to `false` to make table names and attributes case-insensitive on Postgres and skip double quoting of them.
@example @example
// without password and options // without password and options
...@@ -43,10 +45,30 @@ module.exports = (function() { ...@@ -43,10 +45,30 @@ module.exports = (function() {
@constructor @constructor
*/ */
var Sequelize = function(database, username, password, options) { var Sequelize = function(database, username, password, options) {
var urlParts
options = options || {}
if (arguments.length === 1 || (arguments.length === 2 && typeof username === 'object')) {
options = username || {}
urlParts = url.parse(arguments[0])
database = urlParts.path.replace(/^\//, '')
dialect = urlParts.protocol
options.dialect = urlParts.protocol.replace(/:$/, '')
options.host = urlParts.hostname
if (urlParts.port) {
options.port = urlParts.port
}
if (urlParts.auth) {
username = urlParts.auth.split(':')[0]
password = urlParts.auth.split(':')[1]
}
}
this.options = Utils._.extend({ this.options = Utils._.extend({
dialect: 'mysql', dialect: 'mysql',
host: 'localhost', host: 'localhost',
port: 3306,
protocol: 'tcp', protocol: 'tcp',
define: {}, define: {},
query: {}, query: {},
...@@ -56,7 +78,8 @@ module.exports = (function() { ...@@ -56,7 +78,8 @@ module.exports = (function() {
queue: true, queue: true,
native: false, native: false,
replication: false, replication: false,
pool: {} pool: {},
quoteIdentifiers: true
}, options || {}) }, options || {})
if (this.options.logging === true) { if (this.options.logging === true) {
...@@ -130,6 +153,16 @@ module.exports = (function() { ...@@ -130,6 +153,16 @@ module.exports = (function() {
options = options || {} options = options || {}
var globalOptions = this.options var globalOptions = this.options
// If you don't specify a valid data type lets help you debug it
Utils._.each(attributes, function(dataType, name){
if (Utils.isHash(dataType)) {
dataType = dataType.type
}
if (dataType === undefined) {
throw new Error('Unrecognized data type for field '+ name)
}
})
if (globalOptions.define) { if (globalOptions.define) {
options = Utils._.extend({}, globalOptions.define, options) options = Utils._.extend({}, globalOptions.define, options)
Utils._(['classMethods', 'instanceMethods']).each(function(key) { Utils._(['classMethods', 'instanceMethods']).each(function(key) {
...@@ -171,7 +204,7 @@ module.exports = (function() { ...@@ -171,7 +204,7 @@ module.exports = (function() {
Sequelize.prototype.query = function(sql, callee, options, replacements) { Sequelize.prototype.query = function(sql, callee, options, replacements) {
if (arguments.length === 4) { if (arguments.length === 4) {
sql = Utils.format([sql].concat(replacements)) sql = Utils.format([sql].concat(replacements), this.options.dialect)
} else if (arguments.length === 3) { } else if (arguments.length === 3) {
options = options options = options
} else if (arguments.length === 2) { } else if (arguments.length === 2) {
...@@ -189,6 +222,38 @@ module.exports = (function() { ...@@ -189,6 +222,38 @@ module.exports = (function() {
return this.connectorManager.query(sql, callee, options) return this.connectorManager.query(sql, callee, options)
} }
Sequelize.prototype.createSchema = function(schema) {
var chainer = new Utils.QueryChainer()
chainer.add(this.getQueryInterface().createSchema(schema))
return chainer.run()
}
Sequelize.prototype.showAllSchemas = function() {
var chainer = new Utils.QueryChainer()
chainer.add(this.getQueryInterface().showAllSchemas())
return chainer.run()
}
Sequelize.prototype.dropSchema = function(schema) {
var chainer = new Utils.QueryChainer()
chainer.add(this.getQueryInterface().dropSchema(schema))
return chainer.run()
}
Sequelize.prototype.dropAllSchemas = function() {
var self = this
var chainer = new Utils.QueryChainer()
chainer.add(self.getQueryInterface().dropAllSchemas())
return chainer.run()
}
Sequelize.prototype.sync = function(options) { Sequelize.prototype.sync = function(options) {
options = options || {} options = options || {}
...@@ -198,11 +263,14 @@ module.exports = (function() { ...@@ -198,11 +263,14 @@ module.exports = (function() {
var chainer = new Utils.QueryChainer() var chainer = new Utils.QueryChainer()
this.daoFactoryManager.daos.forEach(function(dao) { // Topologically sort by foreign key constraints to give us an appropriate
chainer.add(dao.sync(options)) // creation order
this.daoFactoryManager.forEachDAO(function(dao) {
chainer.add(dao, 'sync', [options])
}) })
return chainer.run() return chainer.runSerially()
} }
Sequelize.prototype.drop = function() { Sequelize.prototype.drop = function() {
......
...@@ -7,7 +7,7 @@ SqlString.escapeId = function (val, forbidQualified) { ...@@ -7,7 +7,7 @@ SqlString.escapeId = function (val, forbidQualified) {
return '`' + val.replace(/`/g, '``').replace(/\./g, '`.`') + '`'; return '`' + val.replace(/`/g, '``').replace(/\./g, '`.`') + '`';
}; };
SqlString.escape = function(val, stringifyObjects, timeZone) { SqlString.escape = function(val, stringifyObjects, timeZone, dialect) {
if (val === undefined || val === null) { if (val === undefined || val === null) {
return 'NULL'; return 'NULL';
} }
...@@ -37,17 +37,22 @@ SqlString.escape = function(val, stringifyObjects, timeZone) { ...@@ -37,17 +37,22 @@ SqlString.escape = function(val, stringifyObjects, timeZone) {
} }
} }
val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) { if (dialect == "postgres") {
switch(s) { // http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS
case "\0": return "\\0"; val = val.replace(/'/g, "''");
case "\n": return "\\n"; } else {
case "\r": return "\\r"; val = val.replace(/[\0\n\r\b\t\\\'\"\x1a]/g, function(s) {
case "\b": return "\\b"; switch(s) {
case "\t": return "\\t"; case "\0": return "\\0";
case "\x1a": return "\\Z"; case "\n": return "\\n";
default: return "\\"+s; case "\r": return "\\r";
} case "\b": return "\\b";
}); case "\t": return "\\t";
case "\x1a": return "\\Z";
default: return "\\"+s;
}
});
}
return "'"+val+"'"; return "'"+val+"'";
}; };
...@@ -58,7 +63,7 @@ SqlString.arrayToList = function(array, timeZone) { ...@@ -58,7 +63,7 @@ SqlString.arrayToList = function(array, timeZone) {
}).join(', '); }).join(', ');
}; };
SqlString.format = function(sql, values, timeZone) { SqlString.format = function(sql, values, timeZone, dialect) {
values = [].concat(values); values = [].concat(values);
return sql.replace(/\?/g, function(match) { return sql.replace(/\?/g, function(match) {
...@@ -66,7 +71,7 @@ SqlString.format = function(sql, values, timeZone) { ...@@ -66,7 +71,7 @@ SqlString.format = function(sql, values, timeZone) {
return match; return match;
} }
return SqlString.escape(values.shift(), false, timeZone); return SqlString.escape(values.shift(), false, timeZone, dialect);
}); });
}; };
......
...@@ -4,7 +4,7 @@ var util = require("util") ...@@ -4,7 +4,7 @@ var util = require("util")
var Utils = module.exports = { var Utils = module.exports = {
_: (function() { _: (function() {
var _ = require("underscore") var _ = require("lodash")
, _s = require('underscore.string') , _s = require('underscore.string')
_.mixin(_s.exports()) _.mixin(_s.exports())
...@@ -35,20 +35,9 @@ var Utils = module.exports = { ...@@ -35,20 +35,9 @@ var Utils = module.exports = {
addEventEmitter: function(_class) { addEventEmitter: function(_class) {
util.inherits(_class, require('events').EventEmitter) util.inherits(_class, require('events').EventEmitter)
}, },
TICK_CHAR: '`', format: function(arr, dialect) {
addTicks: function(s, tickChar) { var timeZone = null;
tickChar = tickChar || Utils.TICK_CHAR return SqlString.format(arr.shift(), arr, timeZone, dialect)
return tickChar + Utils.removeTicks(s, tickChar) + tickChar
},
removeTicks: function(s, tickChar) {
tickChar = tickChar || Utils.TICK_CHAR
return s.replace(new RegExp(tickChar, 'g'), "")
},
escape: function(s) {
return SqlString.escape(s, true, "local").replace(/\\"/g, '"')
},
format: function(arr) {
return SqlString.format(arr.shift(), arr)
}, },
isHash: function(obj) { isHash: function(obj) {
return Utils._.isObject(obj) && !Array.isArray(obj); return Utils._.isObject(obj) && !Array.isArray(obj);
...@@ -179,9 +168,25 @@ var Utils = module.exports = { ...@@ -179,9 +168,25 @@ var Utils = module.exports = {
var now = new Date() var now = new Date()
now.setMilliseconds(0) now.setMilliseconds(0)
return now return now
},
// Note: Use the `quoteIdentifier()` and `escape()` methods on the
// `QueryInterface` instead for more portable code.
TICK_CHAR: '`',
addTicks: function(s, tickChar) {
tickChar = tickChar || Utils.TICK_CHAR
return tickChar + Utils.removeTicks(s, tickChar) + tickChar
},
removeTicks: function(s, tickChar) {
tickChar = tickChar || Utils.TICK_CHAR
return s.replace(new RegExp(tickChar, 'g'), "")
},
escape: function(s) {
return SqlString.escape(s, true, "local").replace(/\\"/g, '"')
} }
} }
Utils.CustomEventEmitter = require("./emitters/custom-event-emitter") Utils.CustomEventEmitter = require("./emitters/custom-event-emitter")
Utils.QueryChainer = require("./query-chainer") Utils.QueryChainer = require("./query-chainer")
Utils.Lingo = require("lingo") Utils.Lingo = require("lingo")
\ No newline at end of file
{ {
"name": "sequelize", "name": "sequelize",
"description": "Multi dialect ORM for Node.JS", "description": "Multi dialect ORM for Node.JS",
"version": "1.6.0", "version": "1.7.0-alpha2",
"author": "Sascha Depold <sascha@depold.com>", "author": "Sascha Depold <sascha@depold.com>",
"contributors": [ "contributors": [
{ {
...@@ -23,27 +23,29 @@ ...@@ -23,27 +23,29 @@
], ],
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/sdepold/sequelize.git" "url": "https://github.com/sequelize/sequelize.git"
}, },
"bugs": { "bugs": {
"url": "https://github.com/sdepold/sequelize/issues" "url": "https://github.com/sequelize/sequelize/issues"
}, },
"dependencies": { "dependencies": {
"underscore": "~1.4.0", "lodash": "~1.2.1",
"underscore.string": "~2.3.0", "underscore.string": "~2.3.0",
"lingo": "~0.0.5", "lingo": "~0.0.5",
"validator": "0.4.x", "validator": "1.1.1",
"moment": "~1.7.0", "moment": "~1.7.0",
"commander": "~0.6.0", "commander": "~0.6.0",
"generic-pool": "1.0.9", "dottie": "0.0.6-1",
"dottie": "0.0.6-1" "toposort-class": "0.1.4",
"generic-pool": "2.0.3",
"promise": "~3.0.0"
}, },
"devDependencies": { "devDependencies": {
"jasmine-node": "1.0.17", "jasmine-node": "1.5.0",
"sqlite3": "~2.1.5", "sqlite3": "~2.1.5",
"mysql": "~2.0.0-alpha7", "mysql": "~2.0.0-alpha7",
"pg": "~0.10.2", "pg": "~0.10.2",
"buster": "~0.6.0", "buster": "~0.6.3",
"watchr": "~2.2.0", "watchr": "~2.2.0",
"yuidocjs": "~0.3.36" "yuidocjs": "~0.3.36"
}, },
...@@ -56,13 +58,13 @@ ...@@ -56,13 +58,13 @@
"main": "index", "main": "index",
"scripts": { "scripts": {
"test": "npm run test-jasmine && npm run test-buster", "test": "npm run test-jasmine && npm run test-buster",
"test-jasmine": "./node_modules/.bin/jasmine-node spec-jasmine/", "test-jasmine": "jasmine-node spec-jasmine/",
"test-buster": "npm run test-buster-mysql && npm run test-buster-postgres && npm run test-buster-postgres-native && npm run test-buster-sqlite", "test-buster": "npm run test-buster-mysql && npm run test-buster-postgres && npm run test-buster-postgres-native && npm run test-buster-sqlite",
"test-buster-travis": "./node_modules/.bin/buster-test", "test-buster-travis": "buster-test",
"test-buster-mysql": "DIALECT=mysql ./node_modules/.bin/buster-test", "test-buster-mysql": "DIALECT=mysql buster-test",
"test-buster-postgres": "DIALECT=postgres ./node_modules/.bin/buster-test", "test-buster-postgres": "DIALECT=postgres buster-test",
"test-buster-postgres-native": "DIALECT=postgres-native ./node_modules/.bin/buster-test", "test-buster-postgres-native": "DIALECT=postgres-native buster-test",
"test-buster-sqlite": "DIALECT=sqlite ./node_modules/.bin/buster-test", "test-buster-sqlite": "DIALECT=sqlite buster-test",
"docs": "node_modules/.bin/yuidoc . -o docs" "docs": "node_modules/.bin/yuidoc . -o docs"
}, },
"bin": { "bin": {
......
var config = require("../config/config") var config = require("../config/config")
, Sequelize = require("../../index") , Sequelize = require("../../index")
, sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { logging: false }) , sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false, host: config.mysql.host, port: config.mysql.port })
, Helpers = new (require("../config/helpers"))(sequelize) , Helpers = new (require("../config/helpers"))(sequelize)
describe('BelongsTo', function() { describe('BelongsTo', function() {
...@@ -8,7 +8,10 @@ describe('BelongsTo', function() { ...@@ -8,7 +8,10 @@ describe('BelongsTo', function() {
, Task = null , Task = null
var setup = function() { var setup = function() {
User = sequelize.define('User', { username: Sequelize.STRING }) User = sequelize.define('User', { username: Sequelize.STRING, enabled: {
type: Sequelize.BOOLEAN,
defaultValue: true
}})
Task = sequelize.define('Task', { title: Sequelize.STRING }) Task = sequelize.define('Task', { title: Sequelize.STRING })
} }
...@@ -88,6 +91,29 @@ describe('BelongsTo', function() { ...@@ -88,6 +91,29 @@ describe('BelongsTo', function() {
}) })
}) })
it('extends the id where param with the supplied where params', function() {
Task.belongsTo(User, {as: 'User'})
Helpers.async(function(done) {
User.sync({force: true}).success(function() {
Task.sync({force: true}).success(done)
})
})
Helpers.async(function(done) {
User.create({username: 'asd', enabled: false}).success(function(u) {
Task.create({title: 'a task'}).success(function(t) {
t.setUser(u).success(function() {
t.getUser({where: {enabled: true}}).success(function(user) {
expect(user).toEqual(null)
done()
})
})
})
})
})
})
it("handles self associations", function() { it("handles self associations", function() {
Helpers.async(function(done) { Helpers.async(function(done) {
var Person = sequelize.define('Person', { name: Sequelize.STRING }) var Person = sequelize.define('Person', { name: Sequelize.STRING })
......
var config = require("../config/config") var config = require("../config/config")
, Sequelize = require("../../index") , Sequelize = require("../../index")
, sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { logging: false }) , sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false, host: config.mysql.host, port: config.mysql.port })
, Helpers = new (require("../config/helpers"))(sequelize) , Helpers = new (require("../config/helpers"))(sequelize)
describe('HasMany', function() { describe('HasMany', function() {
...@@ -10,7 +10,7 @@ describe('HasMany', function() { ...@@ -10,7 +10,7 @@ describe('HasMany', function() {
, Helpers = null , Helpers = null
var setup = function() { var setup = function() {
sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { logging: false }) sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false, host: config.mysql.host, port: config.mysql.port })
Helpers = new (require("../config/helpers"))(sequelize) Helpers = new (require("../config/helpers"))(sequelize)
Helpers.dropAllTables() Helpers.dropAllTables()
......
var config = require("../config/config") var config = require("../config/config")
, Sequelize = require("../../index") , Sequelize = require("../../index")
, sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { logging: false }) , sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false, host: config.mysql.host, port: config.mysql.port })
, Helpers = new (require("../config/helpers"))(sequelize) , Helpers = new (require("../config/helpers"))(sequelize)
describe('HasOne', function() { describe('HasOne', function() {
......
...@@ -90,25 +90,6 @@ describe('DAOFactory', function() { ...@@ -90,25 +90,6 @@ describe('DAOFactory', function() {
}) })
}) })
it('marks the database entry as deleted if dao is paranoid', function() {
Helpers.async(function(done) {
User = sequelize.define('User', {
name: Sequelize.STRING, bio: Sequelize.TEXT
}, { paranoid:true })
User.sync({ force: true }).success(done)
})
Helpers.async(function(done) {
User.create({ name: 'asd', bio: 'asd' }).success(function(u) {
expect(u.deletedAt).toBeNull()
u.destroy().success(function(u) {
expect(u.deletedAt).toBeTruthy()
done()
})
})
})
})
it('allows sql logging of update statements', function() { it('allows sql logging of update statements', function() {
Helpers.async(function(done) { Helpers.async(function(done) {
User = sequelize.define('User', { User = sequelize.define('User', {
......
...@@ -13,7 +13,8 @@ describe('DAO', function() { ...@@ -13,7 +13,8 @@ describe('DAO', function() {
{ {
logging: false, logging: false,
dialect: dialect, dialect: dialect,
port: config[dialect].port port: config[dialect].port,
host: config[dialect].host
} }
) )
, Helpers = new (require("./config/helpers"))(sequelize) , Helpers = new (require("./config/helpers"))(sequelize)
......
var config = require("../config/config") var config = require("../config/config")
, Sequelize = require("../../index") , Sequelize = require("../../index")
, sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false }) , sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false, host: config.mysql.host, port: config.mysql.port })
, Helpers = new (require("../config/helpers"))(sequelize) , Helpers = new (require("../config/helpers"))(sequelize)
describe('HasMany', function() { describe('HasMany', function() {
......
var config = require("../config/config") var config = require("../config/config")
, Sequelize = require("../../index") , Sequelize = require("../../index")
, sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false }) , sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false, host: config.mysql.host, port: config.mysql.port })
, Helpers = new (require("../config/helpers"))(sequelize) , Helpers = new (require("../config/helpers"))(sequelize)
describe('Associations', function() { describe('Associations', function() {
......
var config = require("../config/config") var config = require("../config/config")
, Sequelize = require("../../index") , Sequelize = require("../../index")
, sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false }) , sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false, host: config.mysql.host, port: config.mysql.port })
, Helpers = new (require("../config/helpers"))(sequelize) , Helpers = new (require("../config/helpers"))(sequelize)
describe('ConnectorManager', function() { describe('ConnectorManager', function() {
......
var config = require("../config/config") var config = require("../config/config")
, Sequelize = require("../../index") , Sequelize = require("../../index")
, sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false }) , sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false, host: config.mysql.host, port: config.mysql.port })
, Helpers = new (require("../config/helpers"))(sequelize) , Helpers = new (require("../config/helpers"))(sequelize)
describe('DAOFactory', function() { describe('DAOFactory', function() {
...@@ -31,6 +31,13 @@ describe('DAOFactory', function() { ...@@ -31,6 +31,13 @@ describe('DAOFactory', function() {
expect(User.attributes).toEqual({username:"VARCHAR(255) NOT NULL",id:"INTEGER NOT NULL auto_increment PRIMARY KEY"}) expect(User.attributes).toEqual({username:"VARCHAR(255) NOT NULL",id:"INTEGER NOT NULL auto_increment PRIMARY KEY"})
}) })
it("handles extended attributes (comment)", function() {
var User = sequelize.define('User' + config.rand(), {
username: {type: Sequelize.STRING, comment: 'This be\'s a comment'}
}, { timestamps: false })
expect(User.attributes).toEqual({username:"VARCHAR(255) COMMENT 'This be\\'s a comment'",id:"INTEGER NOT NULL auto_increment PRIMARY KEY"})
})
it("handles extended attributes (primaryKey)", function() { it("handles extended attributes (primaryKey)", function() {
var User = sequelize.define('User' + config.rand(), { var User = sequelize.define('User' + config.rand(), {
username: {type: Sequelize.STRING, primaryKey: true} username: {type: Sequelize.STRING, primaryKey: true}
......
var config = require("../config/config") var config = require("../config/config")
, Sequelize = require("../../index") , Sequelize = require("../../index")
, sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false }) , sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, { pool: config.mysql.pool, logging: false, host: config.mysql.host, port: config.mysql.port })
, Helpers = new (require("../config/helpers"))(sequelize) , Helpers = new (require("../config/helpers"))(sequelize)
, QueryGenerator = require("../../lib/dialects/mysql/query-generator") , QueryGenerator = require("../../lib/dialects/mysql/query-generator")
, util = require("util") , util = require("util")
...@@ -10,12 +10,80 @@ describe('QueryGenerator', function() { ...@@ -10,12 +10,80 @@ describe('QueryGenerator', function() {
afterEach(function() { Helpers.drop() }) afterEach(function() { Helpers.drop() })
var suites = { var suites = {
attributesToSQL: [
{
arguments: [{id: 'INTEGER'}],
expectation: {id: 'INTEGER'}
},
{
arguments: [{id: 'INTEGER', foo: 'VARCHAR(255)'}],
expectation: {id: 'INTEGER', foo: 'VARCHAR(255)'}
},
{
arguments: [{id: {type: 'INTEGER'}}],
expectation: {id: 'INTEGER'}
},
{
arguments: [{id: {type: 'INTEGER', allowNull: false}}],
expectation: {id: 'INTEGER NOT NULL'}
},
{
arguments: [{id: {type: 'INTEGER', allowNull: true}}],
expectation: {id: 'INTEGER'}
},
{
arguments: [{id: {type: 'INTEGER', primaryKey: true, autoIncrement: true}}],
expectation: {id: 'INTEGER auto_increment PRIMARY KEY'}
},
{
arguments: [{id: {type: 'INTEGER', defaultValue: 0}}],
expectation: {id: 'INTEGER DEFAULT 0'}
},
{
arguments: [{id: {type: 'INTEGER', unique: true}}],
expectation: {id: 'INTEGER UNIQUE'}
},
{
arguments: [{id: {type: 'INTEGER', comment: "I'm a comment!" }}],
expectation: {id: "INTEGER COMMENT 'I\\'m a comment!'" }
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar'}}],
expectation: {id: 'INTEGER REFERENCES `Bar` (`id`)'}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}],
expectation: {id: 'INTEGER REFERENCES `Bar` (`pk`)'}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', onDelete: 'CASCADE'}}],
expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON DELETE CASCADE'}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', onUpdate: 'RESTRICT'}}],
expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON UPDATE RESTRICT'}
},
{
arguments: [{id: {type: 'INTEGER', allowNull: false, autoIncrement: true, defaultValue: 1, references: 'Bar', onDelete: 'CASCADE', onUpdate: 'RESTRICT'}}],
expectation: {id: 'INTEGER NOT NULL auto_increment DEFAULT 1 REFERENCES `Bar` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT'}
},
],
createTableQuery: [ createTableQuery: [
{ {
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}], arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255)) ENGINE=InnoDB;" expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255)) ENGINE=InnoDB;"
}, },
{ {
arguments: ['myTable', {title: "INTEGER COMMENT 'I\\'m a comment!'"}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` INTEGER COMMENT 'I\\'m a comment!') ENGINE=InnoDB;",
},
{
arguments: ['myTable', {title: "INTEGER"}, {comment: "I'm a comment!"}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` INTEGER) COMMENT 'I\\'m a comment!' ENGINE=InnoDB;",
},
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}, {engine: 'MyISAM'}], arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}, {engine: 'MyISAM'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255)) ENGINE=MyISAM;" expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255)) ENGINE=MyISAM;"
}, },
...@@ -26,6 +94,14 @@ describe('QueryGenerator', function() { ...@@ -26,6 +94,14 @@ describe('QueryGenerator', function() {
{ {
arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}, {charset: 'latin1'}], arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}, {charset: 'latin1'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` ENUM(\"A\", \"B\", \"C\"), `name` VARCHAR(255)) ENGINE=InnoDB DEFAULT CHARSET=latin1;" expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` ENUM(\"A\", \"B\", \"C\"), `name` VARCHAR(255)) ENGINE=InnoDB DEFAULT CHARSET=latin1;"
},
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `id` INTEGER , PRIMARY KEY (`id`)) ENGINE=InnoDB;"
},
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `otherId` INTEGER, FOREIGN KEY (`otherId`) REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION) ENGINE=InnoDB;"
} }
], ],
...@@ -102,6 +178,26 @@ describe('QueryGenerator', function() { ...@@ -102,6 +178,26 @@ describe('QueryGenerator', function() {
arguments: ['myTable', {offset: 2}], arguments: ['myTable', {offset: 2}],
expectation: "SELECT * FROM `myTable`;", expectation: "SELECT * FROM `myTable`;",
context: QueryGenerator context: QueryGenerator
}, {
title: 'multiple where arguments',
arguments: ['myTable', {where: {boat: 'canoe', weather: 'cold'}}],
expectation: "SELECT * FROM `myTable` WHERE `myTable`.`boat`='canoe' AND `myTable`.`weather`='cold';",
context: QueryGenerator
}, {
title: 'no where arguments (object)',
arguments: ['myTable', {where: {}}],
expectation: "SELECT * FROM `myTable` WHERE 1=1;",
context: QueryGenerator
}, {
title: 'no where arguments (string)',
arguments: ['myTable', {where: ''}],
expectation: "SELECT * FROM `myTable` WHERE 1=1;",
context: QueryGenerator
}, {
title: 'no where arguments (null)',
arguments: ['myTable', {where: null}],
expectation: "SELECT * FROM `myTable` WHERE 1=1;",
context: QueryGenerator
} }
], ],
...@@ -133,6 +229,46 @@ describe('QueryGenerator', function() { ...@@ -133,6 +229,46 @@ describe('QueryGenerator', function() {
arguments: ['myTable', {name: 'foo', foo: 1, nullValue: undefined}], arguments: ['myTable', {name: 'foo', foo: 1, nullValue: undefined}],
expectation: "INSERT INTO `myTable` (`name`,`foo`) VALUES ('foo',1);", expectation: "INSERT INTO `myTable` (`name`,`foo`) VALUES ('foo',1);",
context: {options: {omitNull: true}} context: {options: {omitNull: true}}
}, {
arguments: ['myTable', {foo: false}],
expectation: "INSERT INTO `myTable` (`foo`) VALUES (0);"
}, {
arguments: ['myTable', {foo: true}],
expectation: "INSERT INTO `myTable` (`foo`) VALUES (1);"
}
],
bulkInsertQuery: [
{
arguments: ['myTable', [{name: 'foo'}, {name: 'bar'}]],
expectation: "INSERT INTO `myTable` (`name`) VALUES ('foo'),('bar');"
}, {
arguments: ['myTable', [{name: "foo';DROP TABLE myTable;"}, {name: 'bar'}]],
expectation: "INSERT INTO `myTable` (`name`) VALUES ('foo\\';DROP TABLE myTable;'),('bar');"
}, {
arguments: ['myTable', [{name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}, {name: 'bar', birthday: new Date(Date.UTC(2012, 2, 27, 10, 1, 55))}]],
expectation: "INSERT INTO `myTable` (`name`,`birthday`) VALUES ('foo','2011-03-27 10:01:55'),('bar','2012-03-27 10:01:55');"
}, {
arguments: ['myTable', [{name: 'foo', foo: 1}, {name: 'bar', foo: 2}]],
expectation: "INSERT INTO `myTable` (`name`,`foo`) VALUES ('foo',1),('bar',2);"
}, {
arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', nullValue: null}]],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',NULL);"
}, {
arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);",
context: {options: {omitNull: false}}
}, {
arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);",
context: {options: {omitNull: true}} // Note: We don't honour this because it makes little sense when some rows may have nulls and others not
}, {
arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: undefined}, {name: 'bar', foo: 2, undefinedValue: undefined}]],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);",
context: {options: {omitNull: true}} // Note: As above
}, {
arguments: ['myTable', [{name: "foo", value: true}, {name: 'bar', value: false}]],
expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('foo',1),('bar',0);"
} }
], ],
...@@ -160,6 +296,12 @@ describe('QueryGenerator', function() { ...@@ -160,6 +296,12 @@ describe('QueryGenerator', function() {
arguments: ['myTable', {bar: 2, nullValue: null}, {name: 'foo'}], arguments: ['myTable', {bar: 2, nullValue: null}, {name: 'foo'}],
expectation: "UPDATE `myTable` SET `bar`=2 WHERE `name`='foo'", expectation: "UPDATE `myTable` SET `bar`=2 WHERE `name`='foo'",
context: {options: {omitNull: true}} context: {options: {omitNull: true}}
}, {
arguments: ['myTable', {bar: false}, {name: 'foo'}],
expectation: "UPDATE `myTable` SET `bar`=0 WHERE `name`='foo'"
}, {
arguments: ['myTable', {bar: true}, {name: 'foo'}],
expectation: "UPDATE `myTable` SET `bar`=1 WHERE `name`='foo'"
} }
], ],
...@@ -170,12 +312,21 @@ describe('QueryGenerator', function() { ...@@ -170,12 +312,21 @@ describe('QueryGenerator', function() {
}, { }, {
arguments: ['myTable', 1], arguments: ['myTable', 1],
expectation: "DELETE FROM `myTable` WHERE `id`=1 LIMIT 1" expectation: "DELETE FROM `myTable` WHERE `id`=1 LIMIT 1"
},{
arguments: ['myTable', undefined, {truncate: true}],
expectation: "TRUNCATE `myTable`"
},{
arguments: ['myTable', 1, {limit: 10, truncate: true}],
expectation: "TRUNCATE `myTable`"
}, { }, {
arguments: ['myTable', 1, {limit: 10}], arguments: ['myTable', 1, {limit: 10}],
expectation: "DELETE FROM `myTable` WHERE `id`=1 LIMIT 10" expectation: "DELETE FROM `myTable` WHERE `id`=1 LIMIT 10"
}, { }, {
arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}, {limit: 10}], arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}, {limit: 10}],
expectation: "DELETE FROM `myTable` WHERE `name`='foo\\';DROP TABLE myTable;' LIMIT 10" expectation: "DELETE FROM `myTable` WHERE `name`='foo\\';DROP TABLE myTable;' LIMIT 10"
}, {
arguments: ['myTable', {name: 'foo'}, {limit: null}],
expectation: "DELETE FROM `myTable` WHERE `name`='foo'"
} }
], ],
...@@ -227,6 +378,27 @@ describe('QueryGenerator', function() { ...@@ -227,6 +378,27 @@ describe('QueryGenerator', function() {
{ {
arguments: [{ id: [] }], arguments: [{ id: [] }],
expectation: "`id` IN (NULL)" expectation: "`id` IN (NULL)"
},
{
arguments: [{ maple: false, bacon: true }],
expectation: "`maple`=0 AND `bacon`=1"
},
{
arguments: [{ beaver: [false, true] }],
expectation: "`beaver` IN (0,1)"
},
{
arguments: [{birthday: new Date(Date.UTC(2011, 6, 1, 10, 1, 55))}],
expectation: "`birthday`='2011-07-01 10:01:55'"
},
{
arguments: [{ birthday: new Date(Date.UTC(2011, 6, 1, 10, 1, 55)),
otherday: new Date(Date.UTC(2013, 6, 2, 10, 1, 22)) }],
expectation: "`birthday`='2011-07-01 10:01:55' AND `otherday`='2013-07-02 10:01:22'"
},
{
arguments: [{ birthday: [new Date(Date.UTC(2011, 6, 1, 10, 1, 55)), new Date(Date.UTC(2013, 6, 2, 10, 1, 22))] }],
expectation: "`birthday` IN ('2011-07-01 10:01:55','2013-07-02 10:01:22')"
} }
] ]
} }
...@@ -238,7 +410,8 @@ describe('QueryGenerator', function() { ...@@ -238,7 +410,8 @@ describe('QueryGenerator', function() {
it(title, function() { it(title, function() {
// Options would normally be set by the query interface that instantiates the query-generator, but here we specify it explicitly // Options would normally be set by the query interface that instantiates the query-generator, but here we specify it explicitly
var context = test.context || {options: {}}; var context = test.context || {options: {}};
var conditions = QueryGenerator[suiteTitle].apply(context, test.arguments) QueryGenerator.options = context.options
var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments)
expect(conditions).toEqual(test.expectation) expect(conditions).toEqual(test.expectation)
}) })
......
...@@ -14,10 +14,106 @@ describe('QueryGenerator', function() { ...@@ -14,10 +14,106 @@ describe('QueryGenerator', function() {
afterEach(function() { Helpers.drop() }) afterEach(function() { Helpers.drop() })
var suites = { var suites = {
attributesToSQL: [
{
arguments: [{id: 'INTEGER'}],
expectation: {id: 'INTEGER'}
},
{
arguments: [{id: 'INTEGER', foo: 'VARCHAR(255)'}],
expectation: {id: 'INTEGER', foo: 'VARCHAR(255)'}
},
{
arguments: [{id: {type: 'INTEGER'}}],
expectation: {id: 'INTEGER'}
},
{
arguments: [{id: {type: 'INTEGER', allowNull: false}}],
expectation: {id: 'INTEGER NOT NULL'}
},
{
arguments: [{id: {type: 'INTEGER', allowNull: true}}],
expectation: {id: 'INTEGER'}
},
{
arguments: [{id: {type: 'INTEGER', primaryKey: true, autoIncrement: true}}],
expectation: {id: 'INTEGER SERIAL PRIMARY KEY'}
},
{
arguments: [{id: {type: 'INTEGER', defaultValue: 0}}],
expectation: {id: 'INTEGER DEFAULT 0'}
},
{
arguments: [{id: {type: 'INTEGER', unique: true}}],
expectation: {id: 'INTEGER UNIQUE'}
},
{
arguments: [{id: {type: 'INTEGER', comment: "I'm a comment!" }}],
expectation: {id: "INTEGER COMMENT ON COLUMN <%= table %>.\"id\" IS 'I''m a comment!'" }
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar'}}],
expectation: {id: 'INTEGER REFERENCES "Bar" ("id")'}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}],
expectation: {id: 'INTEGER REFERENCES "Bar" ("pk")'}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', onDelete: 'CASCADE'}}],
expectation: {id: 'INTEGER REFERENCES "Bar" ("id") ON DELETE CASCADE'}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', onUpdate: 'RESTRICT'}}],
expectation: {id: 'INTEGER REFERENCES "Bar" ("id") ON UPDATE RESTRICT'}
},
{
arguments: [{id: {type: 'INTEGER', allowNull: false, defaultValue: 1, references: 'Bar', onDelete: 'CASCADE', onUpdate: 'RESTRICT'}}],
expectation: {id: 'INTEGER NOT NULL DEFAULT 1 REFERENCES "Bar" ("id") ON DELETE CASCADE ON UPDATE RESTRICT'}
},
// Variants when quoteIdentifiers is false
{
arguments: [{id: {type: 'INTEGER', references: 'Bar'}}],
expectation: {id: 'INTEGER REFERENCES Bar (id)'},
context: {options: {quoteIdentifiers: false}}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}],
expectation: {id: 'INTEGER REFERENCES Bar (pk)'},
context: {options: {quoteIdentifiers: false}}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', onDelete: 'CASCADE'}}],
expectation: {id: 'INTEGER REFERENCES Bar (id) ON DELETE CASCADE'},
context: {options: {quoteIdentifiers: false}}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', onUpdate: 'RESTRICT'}}],
expectation: {id: 'INTEGER REFERENCES Bar (id) ON UPDATE RESTRICT'},
context: {options: {quoteIdentifiers: false}}
},
{
arguments: [{id: {type: 'INTEGER', allowNull: false, defaultValue: 1, references: 'Bar', onDelete: 'CASCADE', onUpdate: 'RESTRICT'}}],
expectation: {id: 'INTEGER NOT NULL DEFAULT 1 REFERENCES Bar (id) ON DELETE CASCADE ON UPDATE RESTRICT'},
context: {options: {quoteIdentifiers: false}}
},
],
createTableQuery: [ createTableQuery: [
{ {
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}], arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" VARCHAR(255), \"name\" VARCHAR(255));" expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" VARCHAR(255), \"name\" VARCHAR(255));",
},
{
arguments: ['myTable', {title: "INTEGER COMMENT ON COLUMN <%= table %>.\"title\" IS 'I''m a comment!'"}],
expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" INTEGER ); COMMENT ON COLUMN \"myTable\".\"title\" IS 'I''m a comment!';",
},
{
arguments: ['myTable', {title: "INTEGER"}, {comment: "I'm a comment!"}],
expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" INTEGER); COMMENT ON TABLE \"myTable\" IS 'I''m a comment!';",
}, },
{ {
arguments: ['mySchema.myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}], arguments: ['mySchema.myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
...@@ -26,6 +122,41 @@ describe('QueryGenerator', function() { ...@@ -26,6 +122,41 @@ describe('QueryGenerator', function() {
{ {
arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}], arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}],
expectation: "DROP TYPE IF EXISTS \"enum_myTable_title\"; CREATE TYPE \"enum_myTable_title\" AS ENUM(\"A\", \"B\", \"C\"); CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" \"enum_myTable_title\", \"name\" VARCHAR(255));" expectation: "DROP TYPE IF EXISTS \"enum_myTable_title\"; CREATE TYPE \"enum_myTable_title\" AS ENUM(\"A\", \"B\", \"C\"); CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" \"enum_myTable_title\", \"name\" VARCHAR(255));"
},
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}],
expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" VARCHAR(255), \"name\" VARCHAR(255), \"id\" INTEGER , PRIMARY KEY (\"id\"));"
},
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES "otherTable" ("id") ON DELETE CASCADE ON UPDATE NO ACTION'}],
expectation: "CREATE TABLE IF NOT EXISTS \"myTable\" (\"title\" VARCHAR(255), \"name\" VARCHAR(255), \"otherId\" INTEGER REFERENCES \"otherTable\" (\"id\") ON DELETE CASCADE ON UPDATE NO ACTION);"
},
// Variants when quoteIdentifiers is false
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
expectation: "CREATE TABLE IF NOT EXISTS myTable (title VARCHAR(255), name VARCHAR(255));",
context: {options: {quoteIdentifiers: false}}
},
{
arguments: ['mySchema.myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
expectation: "CREATE TABLE IF NOT EXISTS mySchema.myTable (title VARCHAR(255), name VARCHAR(255));",
context: {options: {quoteIdentifiers: false}}
},
{
arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}],
expectation: "DROP TYPE IF EXISTS enum_myTable_title; CREATE TYPE enum_myTable_title AS ENUM(\"A\", \"B\", \"C\"); CREATE TABLE IF NOT EXISTS myTable (title enum_myTable_title, name VARCHAR(255));",
context: {options: {quoteIdentifiers: false}}
},
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}],
expectation: "CREATE TABLE IF NOT EXISTS myTable (title VARCHAR(255), name VARCHAR(255), id INTEGER , PRIMARY KEY (id));",
context: {options: {quoteIdentifiers: false}}
},
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES otherTable (id) ON DELETE CASCADE ON UPDATE NO ACTION'}],
expectation: "CREATE TABLE IF NOT EXISTS myTable (title VARCHAR(255), name VARCHAR(255), otherId INTEGER REFERENCES otherTable (id) ON DELETE CASCADE ON UPDATE NO ACTION);",
context: {options: {quoteIdentifiers: false}}
} }
], ],
...@@ -37,6 +168,36 @@ describe('QueryGenerator', function() { ...@@ -37,6 +168,36 @@ describe('QueryGenerator', function() {
{ {
arguments: ['mySchema.myTable'], arguments: ['mySchema.myTable'],
expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\";" expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\";"
},
{
arguments: ['myTable', {cascade: true}],
expectation: "DROP TABLE IF EXISTS \"myTable\" CASCADE;"
},
{
arguments: ['mySchema.myTable', {cascade: true}],
expectation: "DROP TABLE IF EXISTS \"mySchema\".\"myTable\" CASCADE;"
},
// Variants when quoteIdentifiers is false
{
arguments: ['myTable'],
expectation: "DROP TABLE IF EXISTS myTable;",
context: {options: {quoteIdentifiers: false}}
},
{
arguments: ['mySchema.myTable'],
expectation: "DROP TABLE IF EXISTS mySchema.myTable;",
context: {options: {quoteIdentifiers: false}}
},
{
arguments: ['myTable', {cascade: true}],
expectation: "DROP TABLE IF EXISTS myTable CASCADE;",
context: {options: {quoteIdentifiers: false}}
},
{
arguments: ['mySchema.myTable', {cascade: true}],
expectation: "DROP TABLE IF EXISTS mySchema.myTable CASCADE;",
context: {options: {quoteIdentifiers: false}}
} }
], ],
...@@ -49,16 +210,16 @@ describe('QueryGenerator', function() { ...@@ -49,16 +210,16 @@ describe('QueryGenerator', function() {
expectation: "SELECT \"id\", \"name\" FROM \"myTable\";" expectation: "SELECT \"id\", \"name\" FROM \"myTable\";"
}, { }, {
arguments: ['myTable', {where: {id: 2}}], 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'}}], 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;"}}], 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}], arguments: ['myTable', {where: 2}],
expectation: "SELECT * FROM \"myTable\" WHERE \"id\"=2;" expectation: "SELECT * FROM \"myTable\" WHERE \"myTable\".\"id\"=2;"
}, { }, {
arguments: ['foo', { attributes: [['count(*)', 'count']] }], arguments: ['foo', { attributes: [['count(*)', 'count']] }],
expectation: 'SELECT count(*) as \"count\" FROM \"foo\";' expectation: 'SELECT count(*) as \"count\" FROM \"foo\";'
...@@ -92,7 +253,79 @@ describe('QueryGenerator', function() { ...@@ -92,7 +253,79 @@ describe('QueryGenerator', function() {
expectation: "SELECT * FROM \"mySchema\".\"myTable\";" expectation: "SELECT * FROM \"mySchema\".\"myTable\";"
}, { }, {
arguments: ['mySchema.myTable', {where: {name: "foo';DROP TABLE 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;';"
},
// Variants when quoteIdentifiers is false
{
arguments: ['myTable'],
expectation: "SELECT * FROM myTable;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {attributes: ['id', 'name']}],
expectation: "SELECT id, name FROM myTable;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {where: {id: 2}}],
expectation: "SELECT * FROM myTable WHERE myTable.id=2;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {where: {name: 'foo'}}],
expectation: "SELECT * FROM myTable WHERE myTable.name='foo';",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {where: {name: "foo';DROP TABLE myTable;"}}],
expectation: "SELECT * FROM myTable WHERE myTable.name='foo'';DROP TABLE myTable;';",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {where: 2}],
expectation: "SELECT * FROM myTable WHERE myTable.id=2;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['foo', { attributes: [['count(*)', 'count']] }],
expectation: 'SELECT count(*) as count FROM foo;',
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {where: "foo='bar'"}],
expectation: "SELECT * FROM myTable WHERE foo='bar';",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {order: "id DESC"}],
expectation: "SELECT * FROM myTable ORDER BY id DESC;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {group: "name"}],
expectation: "SELECT * FROM myTable GROUP BY name;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {group: ["name"]}],
expectation: "SELECT * FROM myTable GROUP BY name;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {group: ["name","title"]}],
expectation: "SELECT * FROM myTable GROUP BY name, title;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {limit: 10}],
expectation: "SELECT * FROM myTable LIMIT 10;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {limit: 10, offset: 2}],
expectation: "SELECT * FROM myTable LIMIT 10 OFFSET 2;",
context: {options: {quoteIdentifiers: false}}
}, {
title: 'uses offset even if no limit was passed',
arguments: ['myTable', {offset: 2}],
expectation: "SELECT * FROM myTable OFFSET 2;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['mySchema.myTable'],
expectation: "SELECT * FROM mySchema.myTable;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['mySchema.myTable', {where: {name: "foo';DROP TABLE mySchema.myTable;"}}],
expectation: "SELECT * FROM mySchema.myTable WHERE mySchema.myTable.name='foo'';DROP TABLE mySchema.myTable;';",
context: {options: {quoteIdentifiers: false}}
} }
], ],
...@@ -133,6 +366,141 @@ describe('QueryGenerator', function() { ...@@ -133,6 +366,141 @@ describe('QueryGenerator', function() {
}, { }, {
arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}], arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}],
expectation: "INSERT INTO \"mySchema\".\"myTable\" (\"name\") VALUES ('foo'';DROP TABLE mySchema.myTable;') RETURNING *;" expectation: "INSERT INTO \"mySchema\".\"myTable\" (\"name\") VALUES ('foo'';DROP TABLE mySchema.myTable;') RETURNING *;"
},
// Variants when quoteIdentifiers is false
{
arguments: ['myTable', {name: 'foo'}],
expectation: "INSERT INTO myTable (name) VALUES ('foo') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}],
expectation: "INSERT INTO myTable (name) VALUES ('foo'';DROP TABLE myTable;') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}],
expectation: "INSERT INTO myTable (name,birthday) VALUES ('foo','2011-03-27 10:01:55.0Z') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: 'foo', foo: 1}],
expectation: "INSERT INTO myTable (name,foo) VALUES ('foo',1) RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: 'foo', nullValue: null}],
expectation: "INSERT INTO myTable (name,nullValue) VALUES ('foo',NULL) RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: 'foo', nullValue: null}],
expectation: "INSERT INTO myTable (name,nullValue) VALUES ('foo',NULL) RETURNING *;",
context: {options: {omitNull: false, quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: 'foo', nullValue: null}],
expectation: "INSERT INTO myTable (name) VALUES ('foo') RETURNING *;",
context: {options: {omitNull: true, quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: 'foo', nullValue: undefined}],
expectation: "INSERT INTO myTable (name) VALUES ('foo') RETURNING *;",
context: {options: {omitNull: true, quoteIdentifiers: false}}
}, {
arguments: ['mySchema.myTable', {name: 'foo'}],
expectation: "INSERT INTO mySchema.myTable (name) VALUES ('foo') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['mySchema.myTable', {name: JSON.stringify({info: 'Look ma a " quote'})}],
expectation: "INSERT INTO mySchema.myTable (name) VALUES ('{\"info\":\"Look ma a \\\" quote\"}') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}],
expectation: "INSERT INTO mySchema.myTable (name) VALUES ('foo'';DROP TABLE mySchema.myTable;') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}
],
bulkInsertQuery: [
{
arguments: ['myTable', [{name: 'foo'}, {name: 'bar'}]],
expectation: "INSERT INTO \"myTable\" (\"name\") VALUES ('foo'),('bar') RETURNING *;"
}, {
arguments: ['myTable', [{name: "foo';DROP TABLE myTable;"}, {name: 'bar'}]],
expectation: "INSERT INTO \"myTable\" (\"name\") VALUES ('foo'';DROP TABLE myTable;'),('bar') RETURNING *;"
}, {
arguments: ['myTable', [{name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}, {name: 'bar', birthday: new Date(Date.UTC(2012, 2, 27, 10, 1, 55))}]],
expectation: "INSERT INTO \"myTable\" (\"name\",\"birthday\") VALUES ('foo','2011-03-27 10:01:55.0Z'),('bar','2012-03-27 10:01:55.0Z') RETURNING *;"
}, {
arguments: ['myTable', [{name: 'foo', foo: 1}, {name: 'bar', foo: 2}]],
expectation: "INSERT INTO \"myTable\" (\"name\",\"foo\") VALUES ('foo',1),('bar',2) RETURNING *;"
}, {
arguments: ['myTable', [{name: 'foo', nullValue: null}, {name: 'bar', nullValue: null}]],
expectation: "INSERT INTO \"myTable\" (\"name\",\"nullValue\") VALUES ('foo',NULL),('bar',NULL) RETURNING *;"
}, {
arguments: ['myTable', [{name: 'foo', nullValue: null}, {name: 'bar', nullValue: null}]],
expectation: "INSERT INTO \"myTable\" (\"name\",\"nullValue\") VALUES ('foo',NULL),('bar',NULL) RETURNING *;",
context: {options: {omitNull: false}}
}, {
arguments: ['myTable', [{name: 'foo', nullValue: null}, {name: 'bar', nullValue: null}]],
expectation: "INSERT INTO \"myTable\" (\"name\",\"nullValue\") VALUES ('foo',NULL),('bar',NULL) RETURNING *;",
context: {options: {omitNull: true}} // Note: We don't honour this because it makes little sense when some rows may have nulls and others not
}, {
arguments: ['myTable', [{name: 'foo', nullValue: undefined}, {name: 'bar', nullValue: undefined}]],
expectation: "INSERT INTO \"myTable\" (\"name\",\"nullValue\") VALUES ('foo',NULL),('bar',NULL) RETURNING *;",
context: {options: {omitNull: true}} // Note: As above
}, {
arguments: ['mySchema.myTable', [{name: 'foo'}, {name: 'bar'}]],
expectation: "INSERT INTO \"mySchema\".\"myTable\" (\"name\") VALUES ('foo'),('bar') RETURNING *;"
}, {
arguments: ['mySchema.myTable', [{name: JSON.stringify({info: 'Look ma a " quote'})}, {name: JSON.stringify({info: 'Look ma another " quote'})}]],
expectation: "INSERT INTO \"mySchema\".\"myTable\" (\"name\") VALUES ('{\"info\":\"Look ma a \\\" quote\"}'),('{\"info\":\"Look ma another \\\" quote\"}') RETURNING *;"
}, {
arguments: ['mySchema.myTable', [{name: "foo';DROP TABLE mySchema.myTable;"}, {name: 'bar'}]],
expectation: "INSERT INTO \"mySchema\".\"myTable\" (\"name\") VALUES ('foo'';DROP TABLE mySchema.myTable;'),('bar') RETURNING *;"
},
// Variants when quoteIdentifiers is false
{
arguments: ['myTable', [{name: 'foo'}, {name: 'bar'}]],
expectation: "INSERT INTO myTable (name) VALUES ('foo'),('bar') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', [{name: "foo';DROP TABLE myTable;"}, {name: 'bar'}]],
expectation: "INSERT INTO myTable (name) VALUES ('foo'';DROP TABLE myTable;'),('bar') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', [{name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}, {name: 'bar', birthday: new Date(Date.UTC(2012, 2, 27, 10, 1, 55))}]],
expectation: "INSERT INTO myTable (name,birthday) VALUES ('foo','2011-03-27 10:01:55.0Z'),('bar','2012-03-27 10:01:55.0Z') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', [{name: 'foo', foo: 1}, {name: 'bar', foo: 2}]],
expectation: "INSERT INTO myTable (name,foo) VALUES ('foo',1),('bar',2) RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', [{name: 'foo', nullValue: null}, {name: 'bar', nullValue: null}]],
expectation: "INSERT INTO myTable (name,nullValue) VALUES ('foo',NULL),('bar',NULL) RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', [{name: 'foo', nullValue: null}, {name: 'bar', nullValue: null}]],
expectation: "INSERT INTO myTable (name,nullValue) VALUES ('foo',NULL),('bar',NULL) RETURNING *;",
context: {options: {quoteIdentifiers: false, omitNull: false}},
}, {
arguments: ['myTable', [{name: 'foo', nullValue: null}, {name: 'bar', nullValue: null}]],
expectation: "INSERT INTO myTable (name,nullValue) VALUES ('foo',NULL),('bar',NULL) RETURNING *;",
context: {options: {omitNull: true, quoteIdentifiers: false}} // Note: We don't honour this because it makes little sense when some rows may have nulls and others not
}, {
arguments: ['myTable', [{name: 'foo', nullValue: undefined}, {name: 'bar', nullValue: undefined}]],
expectation: "INSERT INTO myTable (name,nullValue) VALUES ('foo',NULL),('bar',NULL) RETURNING *;",
context: {options: {omitNull: true, quoteIdentifiers: false}} // Note: As above
}, {
arguments: ['mySchema.myTable', [{name: 'foo'}, {name: 'bar'}]],
expectation: "INSERT INTO mySchema.myTable (name) VALUES ('foo'),('bar') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['mySchema.myTable', [{name: JSON.stringify({info: 'Look ma a " quote'})}, {name: JSON.stringify({info: 'Look ma another " quote'})}]],
expectation: "INSERT INTO mySchema.myTable (name) VALUES ('{\"info\":\"Look ma a \\\" quote\"}'),('{\"info\":\"Look ma another \\\" quote\"}') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['mySchema.myTable', [{name: "foo';DROP TABLE mySchema.myTable;"}, {name: 'bar'}]],
expectation: "INSERT INTO mySchema.myTable (name) VALUES ('foo'';DROP TABLE mySchema.myTable;'),('bar') RETURNING *;",
context: {options: {quoteIdentifiers: false}}
} }
], ],
...@@ -170,6 +538,49 @@ describe('QueryGenerator', function() { ...@@ -170,6 +538,49 @@ describe('QueryGenerator', function() {
}, { }, {
arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}, {name: 'foo'}], arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}, {name: 'foo'}],
expectation: "UPDATE \"mySchema\".\"myTable\" SET \"name\"='foo'';DROP TABLE mySchema.myTable;' WHERE \"name\"='foo' RETURNING *" expectation: "UPDATE \"mySchema\".\"myTable\" SET \"name\"='foo'';DROP TABLE mySchema.myTable;' WHERE \"name\"='foo' RETURNING *"
},
// Variants when quoteIdentifiers is false
{
arguments: ['myTable', {name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}, {id: 2}],
expectation: "UPDATE myTable SET name='foo',birthday='2011-03-27 10:01:55.0Z' WHERE id=2 RETURNING *",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}, 2],
expectation: "UPDATE myTable SET name='foo',birthday='2011-03-27 10:01:55.0Z' WHERE id=2 RETURNING *",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {bar: 2}, {name: 'foo'}],
expectation: "UPDATE myTable SET bar=2 WHERE name='foo' RETURNING *",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}, {name: 'foo'}],
expectation: "UPDATE myTable SET name='foo'';DROP TABLE myTable;' WHERE name='foo' RETURNING *",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {bar: 2, nullValue: null}, {name: 'foo'}],
expectation: "UPDATE myTable SET bar=2,nullValue=NULL WHERE name='foo' RETURNING *",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {bar: 2, nullValue: null}, {name: 'foo'}],
expectation: "UPDATE myTable SET bar=2,nullValue=NULL WHERE name='foo' RETURNING *",
context: {options: {omitNull: false, quoteIdentifiers: false}},
}, {
arguments: ['myTable', {bar: 2, nullValue: null}, {name: 'foo'}],
expectation: "UPDATE myTable SET bar=2 WHERE name='foo' RETURNING *",
context: {options: {omitNull: true, quoteIdentifiers: false}},
}, {
arguments: ['myTable', {bar: 2, nullValue: undefined}, {name: 'foo'}],
expectation: "UPDATE myTable SET bar=2 WHERE name='foo' RETURNING *",
context: {options: {omitNull: true, quoteIdentifiers: false}},
}, {
arguments: ['mySchema.myTable', {name: 'foo', birthday: new Date(Date.UTC(2011, 2, 27, 10, 1, 55))}, {id: 2}],
expectation: "UPDATE mySchema.myTable SET name='foo',birthday='2011-03-27 10:01:55.0Z' WHERE id=2 RETURNING *",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}, {name: 'foo'}],
expectation: "UPDATE mySchema.myTable SET name='foo'';DROP TABLE mySchema.myTable;' WHERE name='foo' RETURNING *",
context: {options: {quoteIdentifiers: false}}
} }
], ],
...@@ -181,6 +592,12 @@ describe('QueryGenerator', function() { ...@@ -181,6 +592,12 @@ describe('QueryGenerator', function() {
arguments: ['myTable', 1], arguments: ['myTable', 1],
expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"id\"=1 LIMIT 1)" expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"id\"=1 LIMIT 1)"
}, { }, {
arguments: ['myTable', undefined, {truncate: true}],
expectation: "TRUNCATE \"myTable\""
}, {
arguments: ['myTable', 1, {limit: 10, truncate: true}],
expectation: "TRUNCATE \"myTable\""
}, {
arguments: ['myTable', 1, {limit: 10}], arguments: ['myTable', 1, {limit: 10}],
expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"id\"=1 LIMIT 10)" expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"id\"=1 LIMIT 10)"
}, { }, {
...@@ -192,6 +609,40 @@ describe('QueryGenerator', function() { ...@@ -192,6 +609,40 @@ describe('QueryGenerator', function() {
}, { }, {
arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}, {limit: 10}], arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}, {limit: 10}],
expectation: "DELETE FROM \"mySchema\".\"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"mySchema\".\"myTable\" WHERE \"name\"='foo'';DROP TABLE mySchema.myTable;' LIMIT 10)" expectation: "DELETE FROM \"mySchema\".\"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"mySchema\".\"myTable\" WHERE \"name\"='foo'';DROP TABLE mySchema.myTable;' LIMIT 10)"
}, {
arguments: ['myTable', {name: 'foo'}, {limit: null}],
expectation: "DELETE FROM \"myTable\" WHERE \"id\" IN (SELECT \"id\" FROM \"myTable\" WHERE \"name\"='foo')"
},
// Variants when quoteIdentifiers is false
{
arguments: ['myTable', {name: 'foo'}],
expectation: "DELETE FROM myTable WHERE id IN (SELECT id FROM myTable WHERE name='foo' LIMIT 1)",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', 1],
expectation: "DELETE FROM myTable WHERE id IN (SELECT id FROM myTable WHERE id=1 LIMIT 1)",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', 1, {limit: 10}],
expectation: "DELETE FROM myTable WHERE id IN (SELECT id FROM myTable WHERE id=1 LIMIT 10)",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}, {limit: 10}],
expectation: "DELETE FROM myTable WHERE id IN (SELECT id FROM myTable WHERE name='foo'';DROP TABLE myTable;' LIMIT 10)",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['mySchema.myTable', {name: 'foo'}],
expectation: "DELETE FROM mySchema.myTable WHERE id IN (SELECT id FROM mySchema.myTable WHERE name='foo' LIMIT 1)",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['mySchema.myTable', {name: "foo';DROP TABLE mySchema.myTable;"}, {limit: 10}],
expectation: "DELETE FROM mySchema.myTable WHERE id IN (SELECT id FROM mySchema.myTable WHERE name='foo'';DROP TABLE mySchema.myTable;' LIMIT 10)",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['myTable', {name: 'foo'}, {limit: null}],
expectation: "DELETE FROM myTable WHERE id IN (SELECT id FROM myTable WHERE name='foo')",
context: {options: {quoteIdentifiers: false}}
} }
], ],
...@@ -215,6 +666,32 @@ describe('QueryGenerator', function() { ...@@ -215,6 +666,32 @@ describe('QueryGenerator', function() {
}, { }, {
arguments: ['mySchema.User', ['username', 'isAdmin']], arguments: ['mySchema.User', ['username', 'isAdmin']],
expectation: 'CREATE INDEX \"user_username_is_admin\" ON \"mySchema\".\"User\" (\"username\", \"isAdmin\")' expectation: 'CREATE INDEX \"user_username_is_admin\" ON \"mySchema\".\"User\" (\"username\", \"isAdmin\")'
},
// Variants when quoteIdentifiers is false
{
arguments: ['User', ['username', 'isAdmin']],
expectation: 'CREATE INDEX user_username_is_admin ON User (username, isAdmin)',
context: {options: {quoteIdentifiers: false}}
}, {
arguments: [
'User', [
{ attribute: 'username', length: 10, order: 'ASC'},
'isAdmin'
]
],
expectation: "CREATE INDEX user_username_is_admin ON User (username(10) ASC, isAdmin)",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: [
'User', ['username', 'isAdmin'], { indicesType: 'FULLTEXT', indexName: 'bar'}
],
expectation: "CREATE FULLTEXT INDEX bar ON User (username, isAdmin)",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['mySchema.User', ['username', 'isAdmin']],
expectation: 'CREATE INDEX user_username_is_admin ON mySchema.User (username, isAdmin)',
context: {options: {quoteIdentifiers: false}}
} }
], ],
...@@ -239,6 +716,21 @@ describe('QueryGenerator', function() { ...@@ -239,6 +716,21 @@ describe('QueryGenerator', function() {
}, { }, {
arguments: ['User', 'mySchema.user_foo_bar'], arguments: ['User', 'mySchema.user_foo_bar'],
expectation: "DROP INDEX IF EXISTS \"mySchema\".\"user_foo_bar\"" expectation: "DROP INDEX IF EXISTS \"mySchema\".\"user_foo_bar\""
},
// Variants when quoteIdentifiers is false
{
arguments: ['User', 'user_foo_bar'],
expectation: "DROP INDEX IF EXISTS user_foo_bar",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['User', ['foo', 'bar']],
expectation: "DROP INDEX IF EXISTS user_foo_bar",
context: {options: {quoteIdentifiers: false}}
}, {
arguments: ['User', 'mySchema.user_foo_bar'],
expectation: "DROP INDEX IF EXISTS mySchema.user_foo_bar",
context: {options: {quoteIdentifiers: false}}
} }
], ],
...@@ -250,7 +742,19 @@ describe('QueryGenerator', function() { ...@@ -250,7 +742,19 @@ describe('QueryGenerator', function() {
{ {
arguments: [{ id: [] }], arguments: [{ id: [] }],
expectation: "\"id\" IN (NULL)" expectation: "\"id\" IN (NULL)"
} },
// Variants when quoteIdentifiers is false
{
arguments: [{ id: [1,2,3] }],
expectation: "id IN (1,2,3)",
context: {options: {quoteIdentifiers: false}}
},
{
arguments: [{ id: [] }],
expectation: "id IN (NULL)",
context: {options: {quoteIdentifiers: false}}
},
] ]
} }
...@@ -261,7 +765,9 @@ describe('QueryGenerator', function() { ...@@ -261,7 +765,9 @@ describe('QueryGenerator', function() {
it(title, function() { it(title, function() {
// Options would normally be set by the query interface that instantiates the query-generator, but here we specify it explicitly // Options would normally be set by the query interface that instantiates the query-generator, but here we specify it explicitly
var context = test.context || {options: {}}; var context = test.context || {options: {}};
var conditions = QueryGenerator[suiteTitle].apply(context, test.arguments) QueryGenerator.options = context.options
var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments)
expect(conditions).toEqual(test.expectation) expect(conditions).toEqual(test.expectation)
}) })
}) })
......
...@@ -8,7 +8,17 @@ describe('Sequelize', function() { ...@@ -8,7 +8,17 @@ describe('Sequelize', function() {
var setup = function(options) { var setup = function(options) {
options = options || {logging: false} options = options || {}
if (!options.hasOwnProperty('pool'))
options.pool = config.mysql.pool
if (!options.hasOwnProperty('logging'))
options.logging = false
if (!options.hasOwnProperty('host'))
options.host = config.mysql.host
if (!options.hasOwnProperty('port'))
options.port = config.mysql.port
sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, options) sequelize = new Sequelize(config.mysql.database, config.mysql.username, config.mysql.password, options)
Helpers = new (require("./config/helpers"))(sequelize) Helpers = new (require("./config/helpers"))(sequelize)
......
...@@ -9,6 +9,81 @@ describe('QueryGenerator', function() { ...@@ -9,6 +9,81 @@ describe('QueryGenerator', function() {
afterEach(function() { Helpers.drop() }) afterEach(function() { Helpers.drop() })
var suites = { var suites = {
attributesToSQL: [
{
arguments: [{id: 'INTEGER'}],
expectation: {id: 'INTEGER'}
},
{
arguments: [{id: 'INTEGER', foo: 'VARCHAR(255)'}],
expectation: {id: 'INTEGER', foo: 'VARCHAR(255)'}
},
{
arguments: [{id: {type: 'INTEGER'}}],
expectation: {id: 'INTEGER'}
},
{
arguments: [{id: {type: 'INTEGER', allowNull: false}}],
expectation: {id: 'INTEGER NOT NULL'}
},
{
arguments: [{id: {type: 'INTEGER', allowNull: true}}],
expectation: {id: 'INTEGER'}
},
{
arguments: [{id: {type: 'INTEGER', primaryKey: true, autoIncrement: true}}],
expectation: {id: 'INTEGER PRIMARY KEY AUTOINCREMENT'}
},
{
arguments: [{id: {type: 'INTEGER', defaultValue: 0}}],
expectation: {id: 'INTEGER DEFAULT 0'}
},
{
arguments: [{id: {type: 'INTEGER', unique: true}}],
expectation: {id: 'INTEGER UNIQUE'}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar'}}],
expectation: {id: 'INTEGER REFERENCES `Bar` (`id`)'}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', referencesKey: 'pk'}}],
expectation: {id: 'INTEGER REFERENCES `Bar` (`pk`)'}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', onDelete: 'CASCADE'}}],
expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON DELETE CASCADE'}
},
{
arguments: [{id: {type: 'INTEGER', references: 'Bar', onUpdate: 'RESTRICT'}}],
expectation: {id: 'INTEGER REFERENCES `Bar` (`id`) ON UPDATE RESTRICT'}
},
{
arguments: [{id: {type: 'INTEGER', allowNull: false, defaultValue: 1, references: 'Bar', onDelete: 'CASCADE', onUpdate: 'RESTRICT'}}],
expectation: {id: 'INTEGER NOT NULL DEFAULT 1 REFERENCES `Bar` (`id`) ON DELETE CASCADE ON UPDATE RESTRICT'}
},
],
createTableQuery: [
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255));"
},
{
arguments: ['myTable', {title: 'ENUM("A", "B", "C")', name: 'VARCHAR(255)'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` ENUM(\"A\", \"B\", \"C\"), `name` VARCHAR(255));"
},
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', id: 'INTEGER PRIMARY KEY'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `id` INTEGER PRIMARY KEY);"
},
{
arguments: ['myTable', {title: 'VARCHAR(255)', name: 'VARCHAR(255)', otherId: 'INTEGER REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION'}],
expectation: "CREATE TABLE IF NOT EXISTS `myTable` (`title` VARCHAR(255), `name` VARCHAR(255), `otherId` INTEGER REFERENCES `otherTable` (`id`) ON DELETE CASCADE ON UPDATE NO ACTION);"
}
],
insertQuery: [ insertQuery: [
{ {
arguments: ['myTable', { name: 'foo' }], arguments: ['myTable', { name: 'foo' }],
...@@ -46,6 +121,43 @@ describe('QueryGenerator', function() { ...@@ -46,6 +121,43 @@ describe('QueryGenerator', function() {
} }
], ],
bulkInsertQuery: [
{
arguments: ['myTable', [{name: 'foo'}, {name: 'bar'}]],
expectation: "INSERT INTO `myTable` (`name`) VALUES ('foo'),('bar');"
}, {
arguments: ['myTable', [{name: "'bar'"}, {name: 'foo'}]],
expectation: "INSERT INTO `myTable` (`name`) VALUES ('''bar'''),('foo');"
}, {
arguments: ['myTable', [{name: "bar", value: null}, {name: 'foo', value: 1}]],
expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('bar',NULL),('foo',1);"
}, {
arguments: ['myTable', [{name: "bar", value: undefined}, {name: 'bar', value: 2}]],
expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('bar',NULL),('bar',2);"
}, {
arguments: ['myTable', [{name: "foo", value: true}, {name: 'bar', value: false}]],
expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('foo',1),('bar',0);"
}, {
arguments: ['myTable', [{name: "foo", value: false}, {name: 'bar', value: false}]],
expectation: "INSERT INTO `myTable` (`name`,`value`) VALUES ('foo',0),('bar',0);"
}, {
arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);"
}, {
arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);",
context: {options: {omitNull: false}}
}, {
arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);",
context: {options: {omitNull: true}} // Note: We don't honour this because it makes little sense when some rows may have nulls and others not
}, {
arguments: ['myTable', [{name: 'foo', foo: 1, nullValue: null}, {name: 'bar', foo: 2, nullValue: null}]],
expectation: "INSERT INTO `myTable` (`name`,`foo`,`nullValue`) VALUES ('foo',1,NULL),('bar',2,NULL);",
context: {options: {omitNull: true}} // Note: As above
}
],
updateQuery: [ updateQuery: [
{ {
arguments: ['myTable', { name: 'foo' }, { id: 2 }], arguments: ['myTable', { name: 'foo' }, { id: 2 }],
...@@ -77,6 +189,28 @@ describe('QueryGenerator', function() { ...@@ -77,6 +189,28 @@ describe('QueryGenerator', function() {
expectation: "UPDATE `myTable` SET `bar`=2 WHERE `name`='foo'", expectation: "UPDATE `myTable` SET `bar`=2 WHERE `name`='foo'",
context: {options: {omitNull: true}} context: {options: {omitNull: true}}
} }
],
deleteQuery: [
{
arguments: ['myTable', {name: 'foo'}],
expectation: "DELETE FROM `myTable` WHERE `name`='foo'"
}, {
arguments: ['myTable', 1],
expectation: "DELETE FROM `myTable` WHERE `id`=1"
}, {
arguments: ['myTable', 1, {truncate: true}],
expectation: "DELETE FROM `myTable` WHERE `id`=1"
}, {
arguments: ['myTable', 1, {limit: 10}],
expectation: "DELETE FROM `myTable` WHERE `id`=1"
}, {
arguments: ['myTable', {name: "foo';DROP TABLE myTable;"}, {limit: 10}],
expectation: "DELETE FROM `myTable` WHERE `name`='foo'';DROP TABLE myTable;'"
}, {
arguments: ['myTable', {name: 'foo'}, {limit: null}],
expectation: "DELETE FROM `myTable` WHERE `name`='foo'"
}
] ]
}; };
...@@ -87,8 +221,8 @@ describe('QueryGenerator', function() { ...@@ -87,8 +221,8 @@ describe('QueryGenerator', function() {
it(title, function() { it(title, function() {
// Options would normally be set by the query interface that instantiates the query-generator, but here we specify it explicitly // Options would normally be set by the query interface that instantiates the query-generator, but here we specify it explicitly
var context = test.context || {options: {}}; var context = test.context || {options: {}};
QueryGenerator.options = context.options
var conditions = QueryGenerator[suiteTitle].apply(context, test.arguments) var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments)
expect(conditions).toEqual(test.expectation) expect(conditions).toEqual(test.expectation)
}) })
}) })
......
...@@ -47,4 +47,161 @@ describe(Helpers.getTestDialectTeaser("BelongsTo"), function() { ...@@ -47,4 +47,161 @@ describe(Helpers.getTestDialectTeaser("BelongsTo"), function() {
}) })
}) })
}) })
describe("Foreign key constraints", function() {
it("are not enabled by default", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
Task.belongsTo(User)
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
task.setUser(user).success(function() {
user.destroy().success(function() {
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
done()
})
})
})
})
})
})
})
it("can cascade deletes", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
Task.belongsTo(User, {onDelete: 'cascade'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
task.setUser(user).success(function() {
user.destroy().success(function() {
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(0)
done()
})
})
})
})
})
})
})
it("can restrict deletes", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
Task.belongsTo(User, {onDelete: 'restrict'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
task.setUser(user).success(function() {
user.destroy().error(function() {
// Should fail due to FK restriction
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
done()
})
})
})
})
})
})
})
it("can cascade updates", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
Task.belongsTo(User, {onUpdate: 'cascade'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
task.setUser(user).success(function() {
// Changing the id of a DAO requires a little dance since
// the `UPDATE` query generated by `save()` uses `id` in the
// `WHERE` clause
var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
user.QueryInterface.update(user, tableName, {id: 999}, user.id)
.success(function() {
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
expect(tasks[0].UserId).toEqual(999)
done()
})
})
})
})
})
})
})
it("can restrict updates", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
Task.belongsTo(User, {onUpdate: 'restrict'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
task.setUser(user).success(function() {
// Changing the id of a DAO requires a little dance since
// the `UPDATE` query generated by `save()` uses `id` in the
// `WHERE` clause
var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
user.QueryInterface.update(user, tableName, {id: 999}, user.id)
.error(function() {
// Should fail due to FK restriction
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
done()
})
})
})
})
})
})
})
})
describe("Association options", function() {
it('can specify data type for autogenerated relational keys', function(done) {
var User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING })
, dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT, Sequelize.STRING]
, self = this
dataTypes.forEach(function(dataType) {
var tableName = 'TaskXYZ_' + dataType.toString()
, Task = self.sequelize.define(tableName, { title: Sequelize.STRING })
Task.belongsTo(User, { foreignKey: 'userId', keyType: dataType })
self.sequelize.sync({ force: true }).success(function() {
expect(Task.rawAttributes.userId.type.toString())
.toEqual(dataType.toString())
dataTypes.splice(dataTypes.indexOf(dataType), 1)
if (!dataTypes.length) {
done()
}
})
})
})
})
}) })
...@@ -9,6 +9,7 @@ buster.spec.expose() ...@@ -9,6 +9,7 @@ buster.spec.expose()
buster.testRunner.timeout = 500 buster.testRunner.timeout = 500
describe(Helpers.getTestDialectTeaser("HasMany"), function() { describe(Helpers.getTestDialectTeaser("HasMany"), function() {
before(function(done) { before(function(done) {
var self = this var self = this
...@@ -288,7 +289,7 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() { ...@@ -288,7 +289,7 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
var add = this.spy() var add = this.spy()
this.stub(Sequelize.Utils, 'QueryChainer').returns({ add: add, run: function(){} }) this.stub(Sequelize.Utils, 'QueryChainer').returns({ add: add, runSerially: function(){} })
this.sequelize.sync({ force: true }) this.sequelize.sync({ force: true })
expect(add).toHaveBeenCalledThrice() expect(add).toHaveBeenCalledThrice()
...@@ -323,4 +324,161 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() { ...@@ -323,4 +324,161 @@ describe(Helpers.getTestDialectTeaser("HasMany"), function() {
}) })
}) })
}) })
describe("Foreign key constraints", function() {
it("are not enabled by default", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
User.hasMany(Task)
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
user.setTasks([task]).success(function() {
user.destroy().success(function() {
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
done()
})
})
})
})
})
})
})
it("can cascade deletes", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
User.hasMany(Task, {onDelete: 'cascade'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
user.setTasks([task]).success(function() {
user.destroy().success(function() {
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(0)
done()
})
})
})
})
})
})
})
it("can restrict deletes", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
User.hasMany(Task, {onDelete: 'restrict'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
user.setTasks([task]).success(function() {
user.destroy().error(function() {
// Should fail due to FK restriction
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
done()
})
})
})
})
})
})
})
it("can cascade updates", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
User.hasMany(Task, {onUpdate: 'cascade'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
user.setTasks([task]).success(function() {
// Changing the id of a DAO requires a little dance since
// the `UPDATE` query generated by `save()` uses `id` in the
// `WHERE` clause
var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
user.QueryInterface.update(user, tableName, {id: 999}, user.id)
.success(function() {
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
expect(tasks[0].UserId).toEqual(999)
done()
})
})
})
})
})
})
})
it("can restrict updates", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
User.hasMany(Task, {onUpdate: 'restrict'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
user.setTasks([task]).success(function() {
// Changing the id of a DAO requires a little dance since
// the `UPDATE` query generated by `save()` uses `id` in the
// `WHERE` clause
var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
user.QueryInterface.update(user, tableName, {id: 999}, user.id)
.error(function() {
// Should fail due to FK restriction
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
done()
})
})
})
})
})
})
})
})
describe("Association options", function() {
it('can specify data type for autogenerated relational keys', function(done) {
var User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING })
, dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT, Sequelize.STRING]
, self = this
dataTypes.forEach(function(dataType) {
var tableName = 'TaskXYZ_' + dataType.toString()
, Task = self.sequelize.define(tableName, { title: Sequelize.STRING })
User.hasMany(Task, { foreignKey: 'userId', keyType: dataType })
self.sequelize.sync({ force: true }).success(function() {
expect(Task.rawAttributes.userId.type.toString())
.toEqual(dataType.toString())
dataTypes.splice(dataTypes.indexOf(dataType), 1)
if (!dataTypes.length) {
done()
}
})
})
})
})
}) })
...@@ -47,4 +47,161 @@ describe(Helpers.getTestDialectTeaser("HasOne"), function() { ...@@ -47,4 +47,161 @@ describe(Helpers.getTestDialectTeaser("HasOne"), function() {
}) })
}) })
}) })
describe("Foreign key constraints", function() {
it("are not enabled by default", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
User.hasOne(Task)
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
user.setTask(task).success(function() {
user.destroy().success(function() {
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
done()
})
})
})
})
})
})
})
it("can cascade deletes", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
User.hasOne(Task, {onDelete: 'cascade'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
user.setTask(task).success(function() {
user.destroy().success(function() {
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(0)
done()
})
})
})
})
})
})
})
it("can restrict deletes", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
User.hasOne(Task, {onDelete: 'restrict'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
user.setTask(task).success(function() {
user.destroy().error(function() {
// Should fail due to FK restriction
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
done()
})
})
})
})
})
})
})
it("can cascade updates", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
User.hasOne(Task, {onUpdate: 'cascade'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
user.setTask(task).success(function() {
// Changing the id of a DAO requires a little dance since
// the `UPDATE` query generated by `save()` uses `id` in the
// `WHERE` clause
var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
user.QueryInterface.update(user, tableName, {id: 999}, user.id)
.success(function() {
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
expect(tasks[0].UserId).toEqual(999)
done()
})
})
})
})
})
})
})
it("can restrict updates", function(done) {
var Task = this.sequelize.define('Task', { title: Sequelize.STRING })
, User = this.sequelize.define('User', { username: Sequelize.STRING })
User.hasOne(Task, {onUpdate: 'restrict'})
this.sequelize.sync({ force: true }).success(function() {
User.create({ username: 'foo' }).success(function(user) {
Task.create({ title: 'task' }).success(function(task) {
user.setTask(task).success(function() {
// Changing the id of a DAO requires a little dance since
// the `UPDATE` query generated by `save()` uses `id` in the
// `WHERE` clause
var tableName = user.QueryInterface.QueryGenerator.addSchema(user.__factory)
user.QueryInterface.update(user, tableName, {id: 999}, user.id)
.error(function() {
// Should fail due to FK restriction
Task.findAll().success(function(tasks) {
expect(tasks.length).toEqual(1)
done()
})
})
})
})
})
})
})
})
describe("Association options", function() {
it('can specify data type for autogenerated relational keys', function(done) {
var User = this.sequelize.define('UserXYZ', { username: Sequelize.STRING })
, dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT, Sequelize.STRING]
, self = this
dataTypes.forEach(function(dataType) {
var tableName = 'TaskXYZ_' + dataType.toString()
, Task = self.sequelize.define(tableName, { title: Sequelize.STRING })
User.hasOne(Task, { foreignKey: 'userId', keyType: dataType })
self.sequelize.sync({ force: true }).success(function() {
expect(Task.rawAttributes.userId.type.toString())
.toEqual(dataType.toString())
dataTypes.splice(dataTypes.indexOf(dataType), 1)
if (!dataTypes.length) {
done()
}
})
})
})
})
}) })
if(typeof require === 'function') {
const buster = require("buster")
, CustomEventEmitter = require("../lib/emitters/custom-event-emitter")
, Helpers = require('./buster-helpers')
, dialect = Helpers.getTestDialect()
}
buster.spec.expose()
buster.testRunner.timeout = 1000
var Sequelize = require(__dirname + '/../index')
describe(Helpers.getTestDialectTeaser("Configuration"), function() {
describe('Instantiation with a URL string', function() {
it('should accept username, password, host, port, and database', function() {
var sequelize = new Sequelize('mysql://user:pass@example.com:9821/dbname')
var config = sequelize.config
var options = sequelize.options
expect(options.dialect).toEqual('mysql')
expect(config.database).toEqual('dbname')
expect(config.host).toEqual('example.com')
expect(config.username).toEqual('user')
expect(config.password).toEqual('pass')
expect(config.port).toEqual(9821)
})
it('should work with no authentication options', function() {
var sequelize = new Sequelize('mysql://example.com:9821/dbname')
var config = sequelize.config
expect(config.username).toEqual(undefined)
expect(config.password).toEqual(null)
})
it('should use the default port when no other is specified', function() {
var sequelize = new Sequelize('mysql://example.com/dbname')
var config = sequelize.config
// The default port should be set
expect(config.port).toEqual(3306)
})
})
describe('Intantiation with arguments', function() {
it('should accept two parameters (database, username)', function() {
var sequelize = new Sequelize('dbname', 'root')
var config = sequelize.config
expect(config.database).toEqual('dbname')
expect(config.username).toEqual('root')
})
it('should accept three parameters (database, username, password)', function() {
var sequelize = new Sequelize('dbname', 'root', 'pass')
var config = sequelize.config
expect(config.database).toEqual('dbname')
expect(config.username).toEqual('root')
expect(config.password).toEqual('pass')
})
it('should accept four parameters (database, username, password, options)', function() {
var sequelize = new Sequelize('dbname', 'root', 'pass', { port: 999 })
var config = sequelize.config
expect(config.database).toEqual('dbname')
expect(config.username).toEqual('root')
expect(config.password).toEqual('pass')
expect(config.port).toEqual(999)
})
})
})
...@@ -2,6 +2,7 @@ if(typeof require === 'function') { ...@@ -2,6 +2,7 @@ if(typeof require === 'function') {
const buster = require("buster") const buster = require("buster")
, Sequelize = require("../index") , Sequelize = require("../index")
, Helpers = require('./buster-helpers') , Helpers = require('./buster-helpers')
, _ = require('lodash')
, dialect = Helpers.getTestDialect() , dialect = Helpers.getTestDialect()
} }
...@@ -40,7 +41,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -40,7 +41,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
var User = this.sequelize.define('SuperUser', {}, { freezeTableName: false }) var User = this.sequelize.define('SuperUser', {}, { freezeTableName: false })
var factorySize = this.sequelize.daoFactoryManager.all.length var factorySize = this.sequelize.daoFactoryManager.all.length
var User2 = this.sequelize.define('SuperUser', {}, { freezeTableName: false }) var User2 = this.sequelize.define('SuperUser', {}, { freezeTableName: false })
var factorySize2 = this.sequelize.daoFactoryManager.all.length var factorySize2 = this.sequelize.daoFactoryManager.all.length
expect(factorySize).toEqual(factorySize2) expect(factorySize).toEqual(factorySize2)
...@@ -69,6 +70,34 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -69,6 +70,34 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}) })
}.bind(this), 'Invalid DAO definition. Only one autoincrement field allowed.') }.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() { describe('build', function() {
...@@ -99,6 +128,82 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -99,6 +128,82 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
var user = this.User.build({ username: 'John Wayne' }) var user = this.User.build({ username: 'John Wayne' })
expect(user.selectedValues).toEqual({ 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 () { describe('findOrCreate', function () {
...@@ -111,9 +216,11 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -111,9 +216,11 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
this.User.create(data).success(function (user) { this.User.create(data).success(function (user) {
self.User.findOrCreate({ self.User.findOrCreate({
username: user.username username: user.username
}).success(function (_user) { }).success(function (_user, created) {
expect(_user.id).toEqual(user.id) expect(_user.id).toEqual(user.id)
expect(_user.username).toEqual('Username') expect(_user.username).toEqual('Username')
expect(created).toBeFalse()
done() done()
}) })
}) })
...@@ -127,10 +234,12 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -127,10 +234,12 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}; };
this.User.create(data).success(function (user) { this.User.create(data).success(function (user) {
self.User.findOrCreate(data).success(function (_user) { self.User.findOrCreate(data).success(function (_user, created) {
expect(_user.id).toEqual(user.id) expect(_user.id).toEqual(user.id)
expect(_user.username).toEqual('Username') expect(_user.username).toEqual('Username')
expect(_user.data).toEqual('ThisIsData') expect(_user.data).toEqual('ThisIsData')
expect(created).toBeFalse()
done() done()
}) })
}) })
...@@ -145,9 +254,11 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -145,9 +254,11 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
data: 'ThisIsData' data: 'ThisIsData'
}; };
this.User.findOrCreate(data, default_values).success(function (user) { this.User.findOrCreate(data, default_values).success(function (user, created) {
expect(user.username).toEqual('Username') expect(user.username).toEqual('Username')
expect(user.data).toEqual('ThisIsData') expect(user.data).toEqual('ThisIsData')
expect(created).toBeTrue()
done() done()
}) })
}) })
...@@ -215,6 +326,12 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -215,6 +326,12 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
activity_date: Sequelize.DATe activity_date: Sequelize.DATe
}) })
}.bind(this), 'Unrecognized data type for field activity_date') }.bind(this), 'Unrecognized data type for field activity_date')
Helpers.assertException(function() {
this.sequelize.define('UserBadDataType', {
activity_date: {type: Sequelize.DATe}
})
}.bind(this), 'Unrecognized data type for field activity_date')
}) })
it('sets a 64 bit int in bigint', function(done) { it('sets a 64 bit int in bigint', function(done) {
...@@ -282,7 +399,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -282,7 +399,7 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}) })
}) })
it('should only store the values passed in the witelist', function(done) { it('should only store the values passed in the whitelist', function(done) {
var self = this var self = this
, data = { username: 'Peter', secretValue: '42' } , data = { username: 'Peter', secretValue: '42' }
...@@ -309,6 +426,34 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -309,6 +426,34 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}) })
}) })
it('can omitt autoincremental columns', function(done) {
var self = this
, data = { title: 'Iliad' }
, dataTypes = [Sequelize.INTEGER, Sequelize.BIGINT]
dataTypes.forEach(function(dataType, index) {
var Book = self.sequelize.define('Book'+index, {
id: { type: dataType, primaryKey: true, autoIncrement: true },
title: Sequelize.TEXT
})
Book.sync({ force: true }).success(function() {
Book
.create(data)
.success(function(book) {
expect(book.title).toEqual(data.title)
expect(book.author).toEqual(data.author)
expect(Book.rawAttributes.id.type.toString())
.toEqual(dataTypes[index].toString())
Book.drop()
if (index >= dataTypes.length - 1) {
done()
}
})
})
})
})
it('saves data with single quote', function(done) { it('saves data with single quote', function(done) {
var quote = "single'quote" var quote = "single'quote"
, self = this , self = this
...@@ -380,6 +525,276 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -380,6 +525,276 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}) })
}) })
describe('bulkCreate', function() {
it('inserts multiple values respecting the white list', function(done) {
var self = this
, data = [{ username: 'Peter', secretValue: '42' },
{ username: 'Paul', secretValue: '23'}]
this.User.bulkCreate(data, ['username']).success(function() {
self.User.findAll({order: 'id'}).success(function(users) {
expect(users.length).toEqual(2)
expect(users[0].username).toEqual("Peter")
expect(users[0].secretValue).toBeNull();
expect(users[1].username).toEqual("Paul")
expect(users[1].secretValue).toBeNull();
done()
})
})
})
it('should store all values if no whitelist is specified', function(done) {
var self = this
, data = [{ username: 'Peter', secretValue: '42' },
{ username: 'Paul', secretValue: '23'}]
this.User.bulkCreate(data).success(function() {
self.User.findAll({order: 'id'}).success(function(users) {
expect(users.length).toEqual(2)
expect(users[0].username).toEqual("Peter")
expect(users[0].secretValue).toEqual('42')
expect(users[1].username).toEqual("Paul")
expect(users[1].secretValue).toEqual('23')
done()
})
})
})
it('saves data with single quote', function(done) {
var self = this
, quote = "Single'Quote"
, data = [{ username: 'Peter', data: quote},
{ username: 'Paul', data: quote}]
this.User.bulkCreate(data).success(function() {
self.User.findAll({order: 'id'}).success(function(users) {
expect(users.length).toEqual(2)
expect(users[0].username).toEqual("Peter")
expect(users[0].data).toEqual(quote)
expect(users[1].username).toEqual("Paul")
expect(users[1].data).toEqual(quote)
done()
})
})
})
it('saves data with double quote', function(done) {
var self = this
, quote = 'Double"Quote'
, data = [{ username: 'Peter', data: quote},
{ username: 'Paul', data: quote}]
this.User.bulkCreate(data).success(function() {
self.User.findAll({order: 'id'}).success(function(users) {
expect(users.length).toEqual(2)
expect(users[0].username).toEqual("Peter")
expect(users[0].data).toEqual(quote)
expect(users[1].username).toEqual("Paul")
expect(users[1].data).toEqual(quote)
done()
})
})
})
it('saves stringified JSON data', function(done) {
var self = this
, json = JSON.stringify({ key: 'value' })
, data = [{ username: 'Peter', data: json},
{ username: 'Paul', data: json}]
this.User.bulkCreate(data).success(function() {
self.User.findAll({order: 'id'}).success(function(users) {
expect(users.length).toEqual(2)
expect(users[0].username).toEqual("Peter")
expect(users[0].data).toEqual(json)
expect(users[1].username).toEqual("Paul")
expect(users[1].data).toEqual(json)
done()
})
})
})
it('stores the current date in createdAt', function(done) {
var self = this
, data = [{ username: 'Peter'},
{ username: 'Paul'}]
this.User.bulkCreate(data).success(function() {
self.User.findAll({order: 'id'}).success(function(users) {
expect(users.length).toEqual(2)
expect(users[0].username).toEqual("Peter")
expect(parseInt(+users[0].createdAt/5000)).toEqual(parseInt(+new Date()/5000))
expect(users[1].username).toEqual("Paul")
expect(parseInt(+users[1].createdAt/5000)).toEqual(parseInt(+new Date()/5000))
done()
})
})
})
describe('enums', function() {
before(function(done) {
this.Item = this.sequelize.define('Item', {
state: { type: Helpers.Sequelize.ENUM, values: ['available', 'in_cart', 'shipped'] },
name: Sequelize.STRING
})
this.sequelize.sync({ force: true }).success(function() {
this.Item.bulkCreate([{state: 'in_cart', name: 'A'}, { state: 'available', name: 'B'}]).success(function() {
done()
}.bind(this))
}.bind(this))
})
it('correctly restores enum values', function(done) {
this.Item.find({ where: { state: 'available' }}).success(function(item) {
expect(item.name).toEqual('B')
done()
}.bind(this))
})
})
}) // - bulkCreate
describe('update', function() {
it('updates only values that match filter', function(done) {
var self = this
, data = [{ username: 'Peter', secretValue: '42' },
{ username: 'Paul', secretValue: '42' },
{ username: 'Bob', secretValue: '43' }]
this.User.bulkCreate(data).success(function() {
self.User.update({username: 'Bill'}, {secretValue: '42'})
.success(function() {
self.User.findAll({order: 'id'}).success(function(users) {
expect(users.length).toEqual(3)
users.forEach(function (user) {
if (user.secretValue == '42') {
expect(user.username).toEqual("Bill")
} else {
expect(user.username).toEqual("Bob")
}
})
done()
})
})
})
})
it('sets updatedAt to the current timestamp', function(done) {
var self = this
, data = [{ username: 'Peter', secretValue: '42' },
{ username: 'Paul', secretValue: '42' },
{ username: 'Bob', secretValue: '43' }]
this.User.bulkCreate(data).success(function() {
self.User.update({username: 'Bill'}, {secretValue: '42'})
.success(function() {
self.User.findAll({order: 'id'}).success(function(users) {
expect(users.length).toEqual(3)
expect(users[0].username).toEqual("Bill")
expect(users[1].username).toEqual("Bill")
expect(users[2].username).toEqual("Bob")
expect(parseInt(+users[0].updatedAt/5000)).toEqual(parseInt(+new Date()/5000))
expect(parseInt(+users[1].updatedAt/5000)).toEqual(parseInt(+new Date()/5000))
done()
})
})
})
})
}) // - update
describe('destroy', function() {
it('deletes values that match filter', function(done) {
var self = this
, data = [{ username: 'Peter', secretValue: '42' },
{ username: 'Paul', secretValue: '42' },
{ username: 'Bob', secretValue: '43' }]
this.User.bulkCreate(data).success(function() {
self.User.destroy({secretValue: '42'})
.success(function() {
self.User.findAll({order: 'id'}).success(function(users) {
expect(users.length).toEqual(1)
expect(users[0].username).toEqual("Bob")
done()
})
})
})
})
it('sets deletedAt to the current timestamp if paranoid is true', function(done) {
var self = this
, User = this.sequelize.define('ParanoidUser', {
username: Sequelize.STRING,
secretValue: Sequelize.STRING,
data: Sequelize.STRING
}, {
paranoid: true
})
, data = [{ username: 'Peter', secretValue: '42' },
{ username: 'Paul', secretValue: '42' },
{ username: 'Bob', secretValue: '43' }]
User.sync({ force: true }).success(function() {
User.bulkCreate(data).success(function() {
User.destroy({secretValue: '42'})
.success(function() {
User.findAll({order: 'id'}).success(function(users) {
expect(users.length).toEqual(3)
expect(users[0].username).toEqual("Peter")
expect(users[1].username).toEqual("Paul")
expect(users[2].username).toEqual("Bob")
expect(parseInt(+users[0].deletedAt/5000)).toEqual(parseInt(+new Date()/5000))
expect(parseInt(+users[1].deletedAt/5000)).toEqual(parseInt(+new Date()/5000))
done()
})
})
})
})
})
}) // - destroy
describe('find', function find() { describe('find', function find() {
before(function(done) { before(function(done) {
this.User.create({ this.User.create({
...@@ -486,6 +901,62 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -486,6 +901,62 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}.bind(this)) }.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,
'0',
{where: {id: 0}},
{where: {id: '0'}}
]
, done = _.after(2 * permutations.length, _done);
this.User.create({name: 'jack'}).success(function (jack) {
this.User.create({name: 'jill'}).success(function (jill) {
permutations.forEach(function(perm) {
this.User.find(perm).done(function(err, user) {
expect(err).toBeNull();
expect(user).toBeNull();
done();
}).on('sql', function(s) {
expect(s.indexOf(0)).not.toEqual(-1);
done();
})
}.bind(this))
}.bind(this))
}.bind(this))
})
describe('eager loading', function() { describe('eager loading', function() {
before(function() { before(function() {
this.Task = this.sequelize.define('Task', { title: Sequelize.STRING }) this.Task = this.sequelize.define('Task', { title: Sequelize.STRING })
...@@ -758,6 +1229,42 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -758,6 +1229,42 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}) })
}) })
}) })
describe('queryOptions', function() {
before(function(done) {
this.User.create({
username: 'barfooz'
}).success(function(user) {
this.user = user
done()
}.bind(this))
})
it("should return a DAO when queryOptions are not set", function (done) {
this.User.find({ where: { username: 'barfooz'}}).done(function (err, user) {
expect(user).toHavePrototype(this.User.DAO.prototype)
done();
}.bind(this))
})
it("should return a DAO when raw is false", function (done) {
this.User.find({ where: { username: 'barfooz'}}, { raw: false }).done(function (err, user) {
expect(user).toHavePrototype(this.User.DAO.prototype)
done();
}.bind(this))
})
it("should return raw data when raw is true", function (done) {
this.User.find({ where: { username: 'barfooz'}}, { raw: true }).done(function (err, user) {
expect(user).not.toHavePrototype(this.User.DAO.prototype)
expect(user).toBeObject()
done();
}.bind(this))
})
}) // - describe: queryOptions
}) //- describe: find }) //- describe: find
describe('findAll', function findAll() { describe('findAll', function findAll() {
...@@ -990,6 +1497,49 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -990,6 +1497,49 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}.bind(this)) }.bind(this))
}) })
}) })
describe('queryOptions', function() {
before(function(done) {
this.User.create({
username: 'barfooz'
}).success(function(user) {
this.user = user
done()
}.bind(this))
})
it("should return a DAO when queryOptions are not set", function (done) {
this.User.findAll({ where: { username: 'barfooz'}}).done(function (err, users) {
users.forEach(function (user) {
expect(user).toHavePrototype(this.User.DAO.prototype)
}, this)
done();
}.bind(this))
})
it("should return a DAO when raw is false", function (done) {
this.User.findAll({ where: { username: 'barfooz'}}, { raw: false }).done(function (err, users) {
users.forEach(function (user) {
expect(user).toHavePrototype(this.User.DAO.prototype)
}, this)
done();
}.bind(this))
})
it("should return raw data when raw is true", function (done) {
this.User.findAll({ where: { username: 'barfooz'}}, { raw: true }).done(function (err, users) {
users.forEach(function (user) {
expect(user).not.toHavePrototype(this.User.DAO.prototype)
expect(users[0]).toBeObject()
}, this)
done();
}.bind(this))
})
}) // - describe: queryOptions
}) })
}) //- describe: findAll }) //- describe: findAll
...@@ -999,7 +1549,13 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -999,7 +1549,13 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
age: Sequelize.INTEGER age: Sequelize.INTEGER
}) })
this.UserWithAge.sync({ force: true }).success(done) this.UserWithDec = this.sequelize.define('UserWithDec', {
value: Sequelize.DECIMAL(10, 3)
})
this.UserWithAge.sync({ force: true }).success(function(){
this.UserWithDec.sync({ force: true }).success(done)
}.bind(this))
}) })
it("should return the min value", function(done) { it("should return the min value", function(done) {
...@@ -1020,6 +1576,17 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -1020,6 +1576,17 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
done() done()
}) })
}) })
it("should allow decimals in min", function(done){
this.UserWithDec.create({value: 3.5}).success(function(){
this.UserWithDec.create({ value: 5.5 }).success(function(){
this.UserWithDec.min('value').success(function(min){
expect(min).toEqual(3.5)
done()
})
}.bind(this))
}.bind(this))
})
}) //- describe: min }) //- describe: min
describe('max', function() { describe('max', function() {
...@@ -1028,7 +1595,13 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -1028,7 +1595,13 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
age: Sequelize.INTEGER age: Sequelize.INTEGER
}) })
this.UserWithAge.sync({ force: true }).success(done) this.UserWithDec = this.sequelize.define('UserWithDec', {
value: Sequelize.DECIMAL(10, 3)
})
this.UserWithAge.sync({ force: true }).success(function(){
this.UserWithDec.sync({ force: true }).success(done)
}.bind(this))
}) })
it("should return the max value", function(done) { it("should return the max value", function(done) {
...@@ -1042,6 +1615,17 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -1042,6 +1615,17 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}.bind(this)) }.bind(this))
}) })
it("should allow decimals in max", function(done){
this.UserWithDec.create({value: 3.5}).success(function(){
this.UserWithDec.create({ value: 5.5 }).success(function(){
this.UserWithDec.max('value').success(function(max){
expect(max).toEqual(5.5)
done()
})
}.bind(this))
}.bind(this))
})
it('allows sql logging', function(done) { it('allows sql logging', function(done) {
this.UserWithAge.max('age').on('sql', function(sql) { this.UserWithAge.max('age').on('sql', function(sql) {
expect(sql).toBeDefined() expect(sql).toBeDefined()
...@@ -1050,4 +1634,97 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() { ...@@ -1050,4 +1634,97 @@ describe(Helpers.getTestDialectTeaser("DAOFactory"), function() {
}) })
}) })
}) //- describe: max }) //- describe: max
describe('schematic support', function() {
before(function(done){
var self = this;
this.UserPublic = this.sequelize.define('UserPublic', {
age: Sequelize.INTEGER
})
this.UserSpecial = this.sequelize.define('UserSpecial', {
age: Sequelize.INTEGER
})
self.sequelize.dropAllSchemas().success(function(){
self.sequelize.createSchema('schema_test').success(function(){
self.sequelize.createSchema('special').success(function(){
self.UserSpecial.schema('special').sync({force: true}).success(function(UserSpecialSync){
self.UserSpecialSync = UserSpecialSync;
done()
})
})
})
})
})
it("should be able to list schemas", function(done){
this.sequelize.showAllSchemas().success(function(schemas){
expect(schemas).toBeDefined()
expect(schemas[0]).toBeArray()
expect(schemas[0].length).toEqual(2)
done()
})
})
if (dialect === "mysql") {
it("should take schemaDelimiter into account if applicable", function(done){
var UserSpecialUnderscore = this.sequelize.define('UserSpecialUnderscore', {age: Sequelize.INTEGER}, {schema: 'hello', schemaDelimiter: '_'})
var UserSpecialDblUnderscore = this.sequelize.define('UserSpecialDblUnderscore', {age: Sequelize.INTEGER})
UserSpecialUnderscore.sync({force: true}).success(function(User){
UserSpecialDblUnderscore.schema('hello', '__').sync({force: true}).success(function(DblUser){
DblUser.create({age: 3}).on('sql', function(dblSql){
User.create({age: 3}).on('sql', function(sql){
expect(dblSql).toBeDefined()
expect(dblSql.indexOf('INSERT INTO `hello__UserSpecialDblUnderscores`')).toBeGreaterThan(-1)
expect(sql).toBeDefined()
expect(sql.indexOf('INSERT INTO `hello_UserSpecialUnderscores`')).toBeGreaterThan(-1)
done()
})
})
})
})
})
}
it("should be able to create and update records under any valid schematic", function(done){
var self = this
self.UserPublic.sync({ force: true }).success(function(UserPublicSync){
UserPublicSync.create({age: 3}).on('sql', function(UserPublic){
self.UserSpecialSync.schema('special').create({age: 3})
.on('sql', function(UserSpecial){
expect(UserSpecial).toBeDefined()
expect(UserPublic).toBeDefined()
if (dialect === "postgres") {
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)
expect(UserPublic.indexOf('INSERT INTO `UserPublics`')).toBeGreaterThan(-1)
}
})
.success(function(UserSpecial){
UserSpecial.updateAttributes({age: 5})
.on('sql', function(user){
expect(user).toBeDefined()
if (dialect === "postgres") {
expect(user.indexOf('UPDATE "special"."UserSpecials"')).toBeGreaterThan(-1)
} else {
expect(user.indexOf('UPDATE `special.UserSpecials`')).toBeGreaterThan(-1)
}
done()
})
}.bind(this))
}.bind(this))
}.bind(this))
})
})
}) })
...@@ -2,7 +2,7 @@ if (typeof require === 'function') { ...@@ -2,7 +2,7 @@ if (typeof require === 'function') {
const buster = require("buster") const buster = require("buster")
, Helpers = require('./buster-helpers') , Helpers = require('./buster-helpers')
, dialect = Helpers.getTestDialect() , dialect = Helpers.getTestDialect()
, _ = require('underscore') , _ = require('lodash')
} }
buster.spec.expose() buster.spec.expose()
...@@ -19,7 +19,23 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { ...@@ -19,7 +19,23 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() {
username: { type: DataTypes.STRING }, username: { type: DataTypes.STRING },
touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW }, touchedAt: { type: DataTypes.DATE, defaultValue: DataTypes.NOW },
aNumber: { type: DataTypes.INTEGER }, aNumber: { type: DataTypes.INTEGER },
bNumber: { 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', { self.HistoryLog = sequelize.define('HistoryLog', {
...@@ -27,10 +43,20 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { ...@@ -27,10 +43,20 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() {
aNumber: { type: DataTypes.INTEGER }, aNumber: { type: DataTypes.INTEGER },
aRandomId: { 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() { onComplete: function() {
self.User.sync({ force: true }).success(function(){ self.User.sync({ force: true }).success(function(){
self.HistoryLog.sync({ force: true }).success(done) self.HistoryLog.sync({ force: true }).success(function(){
self.ParanoidUser.sync({force: true }).success(done)
})
}) })
} }
}) })
...@@ -320,6 +346,29 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { ...@@ -320,6 +346,29 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() {
expect(+user.touchedAt).toBe(5000) expect(+user.touchedAt).toBe(5000)
}) })
}) })
describe('allowNull date', function() {
it('should be just "null" and not Date with Invalid Date', function(done) {
var self = this;
this.User.build({ username: 'a user'}).save().success(function() {
self.User.find({where: {username: 'a user'}}).success(function(user) {
expect(user.dateAllowNullTrue).toBe(null)
done()
})
})
})
it('should be the same valid date when saving the date', function(done) {
var self = this;
var date = new Date();
this.User.build({ username: 'a user', dateAllowNullTrue: date}).save().success(function() {
self.User.find({where: {username: 'a user'}}).success(function(user) {
expect(user.dateAllowNullTrue.toString()).toEqual(date.toString())
done()
})
})
})
})
}) })
describe('complete', function() { describe('complete', function() {
...@@ -341,6 +390,44 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { ...@@ -341,6 +390,44 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() {
}) })
describe('save', function() { describe('save', function() {
it('should fail a validation upon creating', function(done){
this.User.create({aNumber: 0, validateTest: 'hello'}).error(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()
.error(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}).success(function(user){
user.updateAttributes({validateTest: 'hello'}).error(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()
})
})
})
it('takes zero into account', function(done) { it('takes zero into account', function(done) {
this.User.build({ aNumber: 0 }).save([ 'aNumber' ]).success(function(user) { this.User.build({ aNumber: 0 }).save([ 'aNumber' ]).success(function(user) {
expect(user.aNumber).toEqual(0) expect(user.aNumber).toEqual(0)
...@@ -467,6 +554,51 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { ...@@ -467,6 +554,51 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() {
}.bind(this)) }.bind(this))
}) })
it("creates the deletedAt property, when defining paranoid as true", function(done) {
this.ParanoidUser.create({ username: 'fnord' }).success(function() {
this.ParanoidUser.findAll().success(function(users) {
expect(users[0].deletedAt).toBeDefined()
expect(users[0].deletedAt).toBe(null)
done()
}.bind(this))
}.bind(this))
})
it("sets deletedAt property to a specific date when deleting an instance", function(done) {
this.ParanoidUser.create({ username: 'fnord' }).success(function() {
this.ParanoidUser.findAll().success(function(users) {
users[0].destroy().success(function(user) {
expect(user.deletedAt.getMonth).toBeDefined()
done()
}.bind(this))
}.bind(this))
}.bind(this))
})
it("keeps the deletedAt-attribute with value null, when running updateAttributes", function(done) {
this.ParanoidUser.create({ username: 'fnord' }).success(function() {
this.ParanoidUser.findAll().success(function(users) {
users[0].updateAttributes({username: 'newFnord'}).success(function(user) {
expect(user.deletedAt).toBe(null)
done()
}.bind(this))
}.bind(this))
}.bind(this))
})
it("keeps the deletedAt-attribute with value null, when updating associations", function(done) {
this.ParanoidUser.create({ username: 'fnord' }).success(function() {
this.ParanoidUser.findAll().success(function(users) {
this.ParanoidUser.create({ username: 'linkedFnord' }).success(function( linkedUser ) {
users[0].setParanoidUser( linkedUser ).success(function(user) {
expect(user.deletedAt).toBe(null)
done()
}.bind(this))
}.bind(this))
}.bind(this))
}.bind(this))
})
it("can reuse query option objects", function(done) { it("can reuse query option objects", function(done) {
this.User.create({ username: 'fnord' }).success(function() { this.User.create({ username: 'fnord' }).success(function() {
var query = { where: { username: 'fnord' }} var query = { where: { username: 'fnord' }}
...@@ -512,4 +644,44 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { ...@@ -512,4 +644,44 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() {
}.bind(this)) }.bind(this))
}) })
}) })
describe('updateAttributes', function() {
it('stores and restores null values', function(done) {
var Download = this.sequelize.define('download', {
startedAt: Helpers.Sequelize.DATE,
canceledAt: Helpers.Sequelize.DATE,
finishedAt: Helpers.Sequelize.DATE
})
Download.sync({ force: true }).success(function() {
Download.create({
startedAt: new Date()
}).success(function(download) {
expect(download.startedAt instanceof Date).toBeTrue()
expect(download.canceledAt).toBeFalsy()
expect(download.finishedAt).toBeFalsy()
download.updateAttributes({
canceledAt: new Date()
}).success(function(download) {
expect(download.startedAt instanceof Date).toBeTrue()
expect(download.canceledAt instanceof Date).toBeTrue()
expect(download.finishedAt).toBeFalsy()
Download.all({
where: (dialect === 'postgres' ? '"finishedAt" IS NULL' : "`finishedAt` IS NULL")
}).success(function(downloads) {
downloads.forEach(function(download) {
expect(download.startedAt instanceof Date).toBeTrue()
expect(download.canceledAt instanceof Date).toBeTrue()
expect(download.finishedAt).toBeFalsy()
})
done()
})
})
})
})
})
})
}) })
...@@ -7,7 +7,7 @@ if(typeof require === 'function') { ...@@ -7,7 +7,7 @@ if(typeof require === 'function') {
buster.spec.expose() buster.spec.expose()
describe(Helpers.getTestDialectTeaser("DAO"), function() { describe(Helpers.getTestDialectTeaser("DaoValidator"), function() {
describe('validations', function() { describe('validations', function() {
before(function(done) { before(function(done) {
Helpers.initTests({ Helpers.initTests({
...@@ -42,6 +42,10 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { ...@@ -42,6 +42,10 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() {
fail: "abc", fail: "abc",
pass: "129.89.23.1" pass: "129.89.23.1"
} }
, isIPv6 : {
fail: '1111:2222:3333::5555:',
pass: 'fe80:0000:0000:0000:0204:61ff:fe9d:f156'
}
, isAlpha : { , isAlpha : {
fail: "012", fail: "012",
pass: "abc" pass: "abc"
...@@ -278,5 +282,59 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() { ...@@ -278,5 +282,59 @@ describe(Helpers.getTestDialectTeaser("DAO"), function() {
var successfulUser = User.build({ name : "2" }) var successfulUser = User.build({ name : "2" })
expect(successfulUser.validate()).toBeNull() 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()
})
}) })
}) })
...@@ -7,7 +7,7 @@ if(typeof require === 'function') { ...@@ -7,7 +7,7 @@ if(typeof require === 'function') {
buster.spec.expose() buster.spec.expose()
describe(Helpers.getTestDialectTeaser('Data types'), function() { describe(Helpers.getTestDialectTeaser('DataTypes'), function() {
it('should return DECIMAL for the default decimal type', function() { it('should return DECIMAL for the default decimal type', function() {
expect(Sequelize.DECIMAL).toEqual('DECIMAL'); expect(Sequelize.DECIMAL).toEqual('DECIMAL');
}); });
...@@ -15,4 +15,52 @@ describe(Helpers.getTestDialectTeaser('Data types'), function() { ...@@ -15,4 +15,52 @@ describe(Helpers.getTestDialectTeaser('Data types'), function() {
it('should return DECIMAL(10,2) for the default decimal type with arguments', function() { it('should return DECIMAL(10,2) for the default decimal type with arguments', function() {
expect(Sequelize.DECIMAL(10, 2)).toEqual('DECIMAL(10,2)'); expect(Sequelize.DECIMAL(10, 2)).toEqual('DECIMAL(10,2)');
}); });
});
var tests = [
[Sequelize.STRING, 'STRING', 'VARCHAR(255)'],
[Sequelize.STRING(1234), 'STRING(1234)', 'VARCHAR(1234)'],
[Sequelize.STRING(1234).BINARY, 'STRING(1234).BINARY', 'VARCHAR(1234) BINARY'],
[Sequelize.STRING.BINARY, 'STRING.BINARY', 'VARCHAR(255) BINARY'],
[Sequelize.TEXT, 'TEXT', 'TEXT'],
[Sequelize.DATE, 'DATE', 'DATETIME'],
[Sequelize.NOW, 'NOW', 'NOW'],
[Sequelize.BOOLEAN, 'BOOLEAN', 'TINYINT(1)'],
[Sequelize.INTEGER, 'INTEGER', 'INTEGER'],
[Sequelize.INTEGER.UNSIGNED, 'INTEGER.UNSIGNED', 'INTEGER UNSIGNED'],
[Sequelize.INTEGER(11), 'INTEGER(11)','INTEGER(11)'],
[Sequelize.INTEGER(11).UNSIGNED, 'INTEGER(11).UNSIGNED', 'INTEGER(11) UNSIGNED'],
[Sequelize.INTEGER(11).UNSIGNED.ZEROFILL,'INTEGER(11).UNSIGNED.ZEROFILL','INTEGER(11) UNSIGNED ZEROFILL'],
[Sequelize.INTEGER(11).ZEROFILL,'INTEGER(11).ZEROFILL', 'INTEGER(11) ZEROFILL'],
[Sequelize.INTEGER(11).ZEROFILL.UNSIGNED,'INTEGER(11).ZEROFILL.UNSIGNED', 'INTEGER(11) UNSIGNED ZEROFILL'],
[Sequelize.BIGINT, 'BIGINT', 'BIGINT'],
[Sequelize.BIGINT.UNSIGNED, 'BIGINT.UNSIGNED', 'BIGINT UNSIGNED'],
[Sequelize.BIGINT(11), 'BIGINT(11)','BIGINT(11)'],
[Sequelize.BIGINT(11).UNSIGNED, 'BIGINT(11).UNSIGNED', 'BIGINT(11) UNSIGNED'],
[Sequelize.BIGINT(11).UNSIGNED.ZEROFILL, 'BIGINT(11).UNSIGNED.ZEROFILL','BIGINT(11) UNSIGNED ZEROFILL'],
[Sequelize.BIGINT(11).ZEROFILL, 'BIGINT(11).ZEROFILL', 'BIGINT(11) ZEROFILL'],
[Sequelize.BIGINT(11).ZEROFILL.UNSIGNED, 'BIGINT(11).ZEROFILL.UNSIGNED', 'BIGINT(11) UNSIGNED ZEROFILL'],
[Sequelize.FLOAT, 'FLOAT', 'FLOAT'],
[Sequelize.FLOAT.UNSIGNED, 'FLOAT.UNSIGNED', 'FLOAT UNSIGNED'],
[Sequelize.FLOAT(11), 'FLOAT(11)','FLOAT(11)'],
[Sequelize.FLOAT(11).UNSIGNED, 'FLOAT(11).UNSIGNED', 'FLOAT(11) UNSIGNED'],
[Sequelize.FLOAT(11).UNSIGNED.ZEROFILL,'FLOAT(11).UNSIGNED.ZEROFILL','FLOAT(11) UNSIGNED ZEROFILL'],
[Sequelize.FLOAT(11).ZEROFILL,'FLOAT(11).ZEROFILL', 'FLOAT(11) ZEROFILL'],
[Sequelize.FLOAT(11).ZEROFILL.UNSIGNED,'FLOAT(11).ZEROFILL.UNSIGNED', 'FLOAT(11) UNSIGNED ZEROFILL'],
[Sequelize.FLOAT(11, 12), 'FLOAT(11,12)','FLOAT(11,12)'],
[Sequelize.FLOAT(11, 12).UNSIGNED, 'FLOAT(11,12).UNSIGNED', 'FLOAT(11,12) UNSIGNED'],
[Sequelize.FLOAT(11, 12).UNSIGNED.ZEROFILL,'FLOAT(11,12).UNSIGNED.ZEROFILL','FLOAT(11,12) UNSIGNED ZEROFILL'],
[Sequelize.FLOAT(11, 12).ZEROFILL,'FLOAT(11,12).ZEROFILL', 'FLOAT(11,12) ZEROFILL'],
[Sequelize.FLOAT(11, 12).ZEROFILL.UNSIGNED,'FLOAT(11,12).ZEROFILL.UNSIGNED', 'FLOAT(11,12) UNSIGNED ZEROFILL']
]
tests.forEach(function(test) {
it('transforms "' + test[1] + '" to "' + test[2] + '"', function() {
expect(test[0]).toEqual(test[2])
})
})
})
if(typeof require === 'function') {
const buster = require("buster")
, CustomEventEmitter = require("../../lib/emitters/custom-event-emitter")
, Helpers = require('../buster-helpers')
, dialect = Helpers.getTestDialect()
}
buster.spec.expose()
buster.testRunner.timeout = 1000
describe(Helpers.getTestDialectTeaser("CustomEventEmitter"), function() {
describe("proxy", function () {
/* Tests could _probably_ be run synchronously, but for future proofing we're basing it on the events */
it("should correctly work with success listeners", function (done) {
var emitter = new CustomEventEmitter()
, proxy = new CustomEventEmitter()
, success = this.spy()
emitter.success(success)
proxy.success(function () {
process.nextTick(function () {
expect(success.called).toEqual(true)
done();
})
})
proxy.proxy(emitter)
proxy.emit('success')
})
it("should correctly work with error listeners", function (done) {
var emitter = new CustomEventEmitter()
, proxy = new CustomEventEmitter()
, error = this.spy()
emitter.error(error)
proxy.error(function () {
process.nextTick(function () {
expect(error.called).toEqual(true)
done();
})
})
proxy.proxy(emitter)
proxy.emit('error')
})
it("should correctly work with complete/done listeners", function (done) {
var emitter = new CustomEventEmitter()
, proxy = new CustomEventEmitter()
, complete = this.spy()
emitter.complete(complete)
proxy.complete(function () {
process.nextTick(function () {
expect(complete.called).toEqual(true)
done();
})
})
proxy.proxy(emitter)
proxy.emit('success')
})
});
});
\ No newline at end of file
...@@ -14,8 +14,8 @@ describe(Helpers.getTestDialectTeaser("Migrator"), function() { ...@@ -14,8 +14,8 @@ describe(Helpers.getTestDialectTeaser("Migrator"), function() {
before(function(done) { before(function(done) {
this.init = function(options, callback) { this.init = function(options, callback) {
options = Helpers.Sequelize.Utils._.extend({ options = Helpers.Sequelize.Utils._.extend({
path: __dirname + '/assets/migrations', path: __dirname + '/assets/migrations',
logging: false logging: function(){}
}, options || {}) }, options || {})
var migrator = new Migrator(this.sequelize, options) var migrator = new Migrator(this.sequelize, options)
......
...@@ -18,7 +18,8 @@ if (dialect.match(/^postgres/)) { ...@@ -18,7 +18,8 @@ if (dialect.match(/^postgres/)) {
self.User = sequelize.define('User', { self.User = sequelize.define('User', {
username: DataTypes.STRING, username: DataTypes.STRING,
email: {type: DataTypes.ARRAY(DataTypes.TEXT)} email: {type: DataTypes.ARRAY(DataTypes.TEXT)},
document: {type: DataTypes.HSTORE, defaultValue: 'default=>value'}
}) })
}, },
onComplete: function() { onComplete: function() {
...@@ -34,13 +35,90 @@ if (dialect.match(/^postgres/)) { ...@@ -34,13 +35,90 @@ if (dialect.match(/^postgres/)) {
this.User this.User
.create({ username: 'user', email: ['foo@bar.com', 'bar@baz.com'] }) .create({ username: 'user', email: ['foo@bar.com', 'bar@baz.com'] })
.success(function(oldUser) { .success(function(oldUser) {
expect(oldUser.email).toEqual(['foo@bar.com', 'bar@baz.com']); expect(oldUser.email).toEqual(['foo@bar.com', 'bar@baz.com'])
done(); done()
}) })
.error(function(err) { .error(function(err) {
console.log(err) console.log(err)
}) })
}) })
it("should handle hstore correctly", function(done) {
var self = this
this.User
.create({ username: 'user', email: ['foo@bar.com'], document: {hello: 'world'}})
.success(function(newUser) {
expect(newUser.document).toEqual({hello: 'world'})
// Check to see if updating an hstore field works
newUser.updateAttributes({document: {should: 'update', to: 'this', first: 'place'}}).success(function(oldUser){
// Postgres always returns keys in alphabetical order (ascending)
expect(oldUser.document).toEqual({first: 'place', should: 'update', to: 'this'})
// Check to see if the default value for an hstore field works
self.User.create({ username: 'user2', email: ['bar@baz.com']}).success(function(defaultUser){
expect(defaultUser.document).toEqual({default: 'value'})
done()
})
})
})
.error(console.log)
})
})
})
describe('[POSTGRES] Unquoted identifiers', function() {
before(function(done) {
var self = this
Helpers.initTests({
dialect: dialect,
beforeComplete: function(sequelize, DataTypes) {
self.sequelize = sequelize
self.sequelize.options.quoteIdentifiers = false
self.User = sequelize.define('User', {
username: DataTypes.STRING,
fullName: DataTypes.STRING // Note mixed case
})
},
onComplete: function() {
// We can create a table with non-quoted identifiers
self.User.sync({ force: true }).success(done)
}
})
})
it("can insert and select", function(done) {
var self = this
self.User
.create({ username: 'user', fullName: "John Smith" })
.success(function(user) {
// We can insert into a table with non-quoted identifiers
expect(user.id).toBeDefined()
expect(user.id).not.toBeNull()
expect(user.username).toEqual('user')
expect(user.fullName).toEqual('John Smith')
// We can query by non-quoted identifiers
self.User.find({
where: {fullName: "John Smith"}
})
.success(function(user2) {
// We can map values back to non-quoted identifiers
expect(user2.id).toEqual(user.id)
expect(user2.username).toEqual('user')
expect(user2.fullName).toEqual('John Smith')
done();
})
.error(function(err) {
console.log(err)
})
})
.error(function(err) {
console.log(err)
})
}) })
}) })
} }
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()
})
})
})
})
...@@ -93,7 +93,8 @@ describe(Helpers.getTestDialectTeaser("QueryInterface"), function() { ...@@ -93,7 +93,8 @@ describe(Helpers.getTestDialectTeaser("QueryInterface"), function() {
before(function(done) { before(function(done) {
this.interface.createTable('User', { this.interface.createTable('User', {
username: Helpers.Sequelize.STRING, username: Helpers.Sequelize.STRING,
isAdmin: Helpers.Sequelize.BOOLEAN isAdmin: Helpers.Sequelize.BOOLEAN,
enumVals: Helpers.Sequelize.ENUM('hello', 'world')
}).success(done) }).success(done)
}) })
...@@ -103,6 +104,7 @@ describe(Helpers.getTestDialectTeaser("QueryInterface"), function() { ...@@ -103,6 +104,7 @@ describe(Helpers.getTestDialectTeaser("QueryInterface"), function() {
var username = metadata.username var username = metadata.username
var isAdmin = metadata.isAdmin var isAdmin = metadata.isAdmin
var enumVals = metadata.enumVals
expect(username.type).toEqual(dialect === 'postgres' ? 'CHARACTER VARYING' : 'VARCHAR(255)') expect(username.type).toEqual(dialect === 'postgres' ? 'CHARACTER VARYING' : 'VARCHAR(255)')
expect(username.allowNull).toBeTrue() expect(username.allowNull).toBeTrue()
...@@ -112,6 +114,11 @@ describe(Helpers.getTestDialectTeaser("QueryInterface"), function() { ...@@ -112,6 +114,11 @@ describe(Helpers.getTestDialectTeaser("QueryInterface"), function() {
expect(isAdmin.allowNull).toBeTrue() expect(isAdmin.allowNull).toBeTrue()
expect(isAdmin.defaultValue).toBeNull() expect(isAdmin.defaultValue).toBeNull()
if (dialect === 'postgres') {
expect(enumVals.special).toBeArray();
expect(enumVals.special.length).toEqual(2);
}
done() done()
}) })
}) })
......
...@@ -194,5 +194,29 @@ describe(Helpers.getTestDialectTeaser("Sequelize"), function() { ...@@ -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!