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

Commit 797f4f12 by Victor Pontis

Merge branch 'master' into associating-sections-when-deletions

* master:
  Allow removing has-many associations by id
  Update README.md
  #2338 - Associations remove[AS][plural]
  Use the new beta build env on Travis
  Text fix
  More promises
  Promisify tests
  Remove unnecssarily asynchronous tests
  Update changelog with BC breaking hstore change
  Update json test expectation
  Merge fix
  Update pg-hstore
  Removed no longer supported nested hstore objects from tests. Improved hstore array handling.
  Use pg-hstore for hstore parsing
  Hstore empty string tests
2 parents b17e71d7 de05498b
before_script: language: node_js
- "mysql -e 'create database sequelize_test;'"
- "psql -c 'create database sequelize_test;' -U postgres"
script: node_js:
- "make test" - "0.10"
notifications: sudo: false
hipchat:
- 40e8850aaba9854ac4c9963bd33f8b@253477 cache:
irc: directories:
- "chat.freenode.net#sequelizejs" - node_modules
env: env:
- DB=mysql DIALECT=mysql - DB=mysql DIALECT=mysql
...@@ -18,30 +16,32 @@ env: ...@@ -18,30 +16,32 @@ env:
- DB=mysql DIALECT=sqlite - DB=mysql DIALECT=sqlite
- DB=mysql DIALECT=mariadb - DB=mysql DIALECT=mariadb
language: node_js addons:
postgresql: "9.3"
node_js: before_script:
- "0.10" - "mysql -e 'create database sequelize_test;'"
- "psql -c 'create database sequelize_test;' -U postgres"
script:
- "make test"
branches: branches:
only: only:
- master - master
- 1.7.0 - 1.7.0
cache:
directories:
- node_modules
matrix: matrix:
fast_finish: true fast_finish: true
include: include:
- node_js: "0.10" - node_js: "0.10"
env: COVERAGE=true env: COVERAGE=true
allow_failures: allow_failures:
- node_js: "0.10" - node_js: "0.10"
env: COVERAGE=true env: COVERAGE=true
addons: notifications:
postgresql: "9.3" hipchat:
\ No newline at end of file - 40e8850aaba9854ac4c9963bd33f8b@253477
irc:
- "chat.freenode.net#sequelizejs"
...@@ -36,7 +36,7 @@ To install 2.x.x branch - which has a unstable API and will break backwards comp ...@@ -36,7 +36,7 @@ To install 2.x.x branch - which has a unstable API and will break backwards comp
- [Getting Started](http://sequelizejs.com/articles/getting-started) - [Getting Started](http://sequelizejs.com/articles/getting-started)
- [Documentation](http://sequelizejs.com/docs) - [Documentation](http://sequelizejs.com/docs)
- [API Reference](https://github.com/sequelize/sequelize/wiki/API-Reference) *Work in progress* - [API Reference](https://github.com/sequelize/sequelize/wiki/API-Reference) *Work in progress*
- [Collaboration and pull requests](https://github.com/sequelize/sequelize/wiki/Collaboration) - [Collaboration and pull requests](https://github.com/sequelize/sequelize/blob/master/CONTRIBUTING.md)
- [Roadmap](https://github.com/sequelize/sequelize/wiki/Roadmap) - [Roadmap](https://github.com/sequelize/sequelize/wiki/Roadmap)
- [Meetups](https://github.com/sequelize/sequelize/wiki/Meetups) - [Meetups](https://github.com/sequelize/sequelize/wiki/Meetups)
- [Twitter](http://twitter.com/sdepold) - [Twitter](http://twitter.com/sdepold)
......
# Next # Next
- [FEATURE] Added the possibility of removing multiple associations in 1 call [#2338](https://github.com/sequelize/sequelize/issues/2338)
- [BUG] Add support for `field` named the same as the attribute in `reload`, `bulkCreate` and `save` [#2348](https://github.com/sequelize/sequelize/issues/2348) - [BUG] Add support for `field` named the same as the attribute in `reload`, `bulkCreate` and `save` [#2348](https://github.com/sequelize/sequelize/issues/2348)
- [BUG] Copy the options object in association getters. [#2311](https://github.com/sequelize/sequelize/issues/2311) - [BUG] Copy the options object in association getters. [#2311](https://github.com/sequelize/sequelize/issues/2311)
- [BUG] `Model#destroy()` now supports `field`, this also fixes an issue with `N:M#removeAssociation` and `field` - [BUG] `Model#destroy()` now supports `field`, this also fixes an issue with `N:M#removeAssociation` and `field`
...@@ -6,6 +7,7 @@ ...@@ -6,6 +7,7 @@
#### Backwards compatability changes #### Backwards compatability changes
- When eager-loading a many-to-many association, the attributes of the through table are now accessible through an attribute named after the through model rather than the through table name singularized. i.e. `Task.find({include: Worker})` where the table name for through model `TaskWorker` is `TableTaskWorkers` used to produce `{ Worker: { ..., TableTaskWorker: {...} } }`. It now produces `{ Worker: { ..., TaskWorker: {...} } }`. Does not affect models where table name is auto-defined by Sequelize, or where table name is model name pluralized. - When eager-loading a many-to-many association, the attributes of the through table are now accessible through an attribute named after the through model rather than the through table name singularized. i.e. `Task.find({include: Worker})` where the table name for through model `TaskWorker` is `TableTaskWorkers` used to produce `{ Worker: { ..., TableTaskWorker: {...} } }`. It now produces `{ Worker: { ..., TaskWorker: {...} } }`. Does not affect models where table name is auto-defined by Sequelize, or where table name is model name pluralized.
- When using `Model#find()` with an `order` clause, the table name is prepended to the `ORDER BY` SQL. e.g. `ORDER BY Task.id` rather than `ORDER BY id`. The change is to avoid ambiguous column names where there are eager-loaded associations with the same column names. A side effect is that code like `Task.findAll( { include: [ User ], order: [ [ 'Users.id', 'ASC' ] ] } )` will now throw an error. This should be achieved with `Task.findAll( { include: [ User ], order: [ [ User, 'id', 'ASC' ] ] } )` instead. - When using `Model#find()` with an `order` clause, the table name is prepended to the `ORDER BY` SQL. e.g. `ORDER BY Task.id` rather than `ORDER BY id`. The change is to avoid ambiguous column names where there are eager-loaded associations with the same column names. A side effect is that code like `Task.findAll( { include: [ User ], order: [ [ 'Users.id', 'ASC' ] ] } )` will now throw an error. This should be achieved with `Task.findAll( { include: [ User ], order: [ [ User, 'id', 'ASC' ] ] } )` instead.
- Nested HSTORE objects are no longer supported. Use DataTypes.JSON instead.
# 2.0.0-rc2 # 2.0.0-rc2
- [FEATURE] Added to posibility of using a sequelize object as key in `sequelize.where`. Also added the option of specifying a comparator - [FEATURE] Added to posibility of using a sequelize object as key in `sequelize.where`. Also added the option of specifying a comparator
......
...@@ -176,6 +176,7 @@ module.exports = (function() { ...@@ -176,6 +176,7 @@ module.exports = (function() {
add: 'add' + singular, add: 'add' + singular,
create: 'create' + singular, create: 'create' + singular,
remove: 'remove' + singular, remove: 'remove' + singular,
removeMultiple: 'remove' + plural,
hasSingle: 'has' + singular, hasSingle: 'has' + singular,
hasAll: 'has' + plural hasAll: 'has' + plural
}; };
...@@ -473,6 +474,14 @@ module.exports = (function() { ...@@ -473,6 +474,14 @@ module.exports = (function() {
return instance[association.accessors.get]({}, options).then(function(currentAssociatedObjects) { return instance[association.accessors.get]({}, options).then(function(currentAssociatedObjects) {
var newAssociations = []; var newAssociations = [];
if (!(oldAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = oldAssociatedObject;
oldAssociatedObject = association.target.build(tmpInstance, {
isNewRecord: false
});
}
currentAssociatedObjects.forEach(function(association) { currentAssociatedObjects.forEach(function(association) {
if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) { if (!Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers)) {
newAssociations.push(association); newAssociations.push(association);
...@@ -483,6 +492,41 @@ module.exports = (function() { ...@@ -483,6 +492,41 @@ module.exports = (function() {
}); });
}; };
obj[this.accessors.removeMultiple] = function(oldAssociatedObjects, options) {
var instance = this;
return instance[association.accessors.get]({}, options).then(function(currentAssociatedObjects) {
var newAssociations = [];
// Ensure the oldAssociatedObjects array is an array of target instances
oldAssociatedObjects = oldAssociatedObjects.map(function(oldAssociatedObject) {
if (!(oldAssociatedObject instanceof association.target.Instance)) {
var tmpInstance = {};
tmpInstance[primaryKeyAttribute] = oldAssociatedObject;
oldAssociatedObject = association.target.build(tmpInstance, {
isNewRecord: false
});
}
return oldAssociatedObject;
});
currentAssociatedObjects.forEach(function(association) {
// Determine is this is an association we want to remove
var obj = Utils._.find(oldAssociatedObjects, function(oldAssociatedObject) {
return Utils._.isEqual(oldAssociatedObject.identifiers, association.identifiers);
});
// This is not an association we want to remove. Add it back
// to the set of associations we will associate our instance with
if (!obj) {
newAssociations.push(association);
}
});
return instance[association.accessors.set](newAssociations, options);
});
};
return this; return this;
}; };
......
...@@ -187,7 +187,8 @@ Mixin.belongsTo = singleLinked(BelongsTo); ...@@ -187,7 +187,8 @@ Mixin.belongsTo = singleLinked(BelongsTo);
* * add[AS] - for example addPicture(instance, defaultAttributes|options). Add another associated object. * * add[AS] - for example addPicture(instance, defaultAttributes|options). Add another associated object.
* * add[AS] [plural] - for example addPictures([instance1, instance2], defaultAttributes|options). Add some more associated objects. * * add[AS] [plural] - for example addPictures([instance1, instance2], defaultAttributes|options). Add some more associated objects.
* * create[AS] - for example createPicture(values, options). Build and save a new association. * * create[AS] - for example createPicture(values, options). Build and save a new association.
* * remove[AS] - for example removePicture(instance). Remove a single association * * remove[AS] - for example removePicture(instance). Remove a single association.
* * remove[AS] [plural] - for example removePictures(instance). Remove multiple association.
* * has[AS] - for example hasPicture(instance). Is source associated to this target? * * has[AS] - for example hasPicture(instance). Is source associated to this target?
* * has[AS] [plural] - for example hasPictures(instances). Is source associated to all these targets? * * has[AS] [plural] - for example hasPictures(instances). Is source associated to all these targets?
* *
......
'use strict'; 'use strict';
module.exports = { var hstore = require("pg-hstore")({sanitize : true});
stringifyPart: function(part) {
switch (typeof part) {
case 'boolean':
case 'number':
return String(part);
case 'string':
return '"' + part.replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
case 'undefined':
return 'NULL';
default:
if (part === null)
return 'NULL';
else
return '"' + JSON.stringify(part).replace(/\\/g, '\\\\').replace(/"/g, '\\"') + '"';
}
},
stringifyObject: function(data) {
var self = this;
return Object.keys(data).map(function(key) { module.exports = {
return self.stringifyPart(key) + '=>' + self.stringifyPart(data[key]);
}).join(',');
},
stringifyArray: function(data) {
return data.map(this.stringifyObject, this);
},
stringify: function(data) { stringify: function(data) {
if (Array.isArray(data)) { if(data === null) return null;
return this.stringifyArray(data);
}
return this.stringifyObject(data);
},
parsePart: function(part) {
part = part.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
switch (part[0]) { return hstore.stringify(data);
case '{':
case '[':
return JSON.parse(part);
default:
return part;
}
},
parseObject: function(string) {
var self = this,
object = { };
if (0 === string.length) {
return object;
}
var rx = /\"((?:\\\"|[^"])*)\"\s*\=\>\s*((?:true|false|NULL|\d+|\d+\.\d+|\"((?:\\\"|[^"])*)\"))/g;
string = string || '';
string.replace(rx, function(match, key, value, innerValue) {
switch (value) {
case 'true':
object[self.parsePart(key)] = true;
break;
case 'false':
object[self.parsePart(key)] = false;
break;
case 'NULL':
object[self.parsePart(key)] = null;
break;
default:
object[self.parsePart(key)] = self.parsePart(innerValue || value);
break;
}
});
return object;
},
parseArray: function(string) {
var matches = string.match(/{(.*)}/);
var array = JSON.parse('['+ matches[1] +']');
return array.map(this.parseObject, this);
}, },
parse: function(value) { parse: function(value) {
if ('string' !== typeof value) { if(value === null) return null;
return value;
}
if ('{' === value[0] && '}' === value[value.length - 1]) {
return this.parseArray(value);
}
return this.parseObject(value); return hstore.parse(value);
} }
}; };
...@@ -852,7 +852,11 @@ module.exports = (function() { ...@@ -852,7 +852,11 @@ module.exports = (function() {
} }
if (Utils._.isObject(value) && field && (field.type === DataTypes.HSTORE || field.type === DataTypes.ARRAY(DataTypes.HSTORE))) { if (Utils._.isObject(value) && field && (field.type === DataTypes.HSTORE || field.type === DataTypes.ARRAY(DataTypes.HSTORE))) {
value = hstore.stringify(value); if(field.type === DataTypes.HSTORE){
return "'" + hstore.stringify(value) + "'";
}else if (field.type === DataTypes.ARRAY(DataTypes.HSTORE)){
return "ARRAY[" + Utils._.map(value, function(v){return "'" + hstore.stringify(v) + "'::hstore";}).join(",") + "]::HSTORE[]";
}
} else if (Utils._.isObject(value) && field && (field.type === DataTypes.JSON)) { } else if (Utils._.isObject(value) && field && (field.type === DataTypes.JSON)) {
value = JSON.stringify(value); value = JSON.stringify(value);
} }
......
...@@ -12,13 +12,16 @@ var Utils = require('../../utils') ...@@ -12,13 +12,16 @@ var Utils = require('../../utils')
// This cannot be done in the 'pg' lib because hstore is a UDT. // This cannot be done in the 'pg' lib because hstore is a UDT.
var parseHstoreFields = function(model, row) { var parseHstoreFields = function(model, row) {
Utils._.forEach(row, function(value, key) { Utils._.forEach(row, function(value, key) {
if (model._isHstoreAttribute(key) || (model.attributes[key] && model.attributes[key].type === DataTypes.ARRAY(DataTypes.HSTORE))) { if(value === null) return row[key] = null;
row[key] = hstore.parse(value);
return;
}
if (model._isHstoreAttribute(key)) {
row[key] = hstore.parse(value);
}else if(model.attributes[key] && model.attributes[key].type === DataTypes.ARRAY(DataTypes.HSTORE)) {
var array = JSON.parse('[' + value.slice(1).slice(0,-1) + ']');
row[key] = Utils._.map(array, function(v){return hstore.parse(v);});
}else{
row[key] = value; row[key] = value;
}
}); });
}; };
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
"node-uuid": "~1.4.1", "node-uuid": "~1.4.1",
"bluebird": "~2.3.2", "bluebird": "~2.3.2",
"sql": "~0.40.0", "sql": "~0.40.0",
"pg-hstore": "^2.2.0",
"toposort-class": "~0.3.0", "toposort-class": "~0.3.0",
"validator": "~3.22.0" "validator": "~3.22.0"
}, },
......
...@@ -2016,14 +2016,46 @@ describe(Support.getTestDialectTeaser("HasMany"), function() { ...@@ -2016,14 +2016,46 @@ describe(Support.getTestDialectTeaser("HasMany"), function() {
return this.sequelize.sync().then(function() { return this.sequelize.sync().then(function() {
return Sequelize.Promise.all([ return Sequelize.Promise.all([
Worker.create({}), Worker.create({}),
Task.bulkCreate([{}, {}]).then(function () { Task.bulkCreate([{}, {}, {}]).then(function () {
return Task.findAll(); return Task.findAll();
}) })
]); ]);
}).spread(function (worker, tasks) { }).spread(function (worker, tasks) {
// Set all tasks, then remove one tasks, then return all tasks // Set all tasks, then remove one task by instance, then remove one task by id, then return all tasks
return worker.setTasks(tasks).then(function () { return worker.setTasks(tasks).then(function () {
return worker.removeTask(tasks[0]); return worker.removeTask(tasks[0]);
}).then(function() {
return worker.removeTask(tasks[1].id);
}).then(function () {
return worker.getTasks();
});
}).then(function (tasks) {
expect(tasks.length).to.equal(1);
});
});
it('should remove multiple entries without any attributes (and timestamps off) on the through model', function () {
var Worker = this.sequelize.define('Worker', {}, {timestamps: false})
, Task = this.sequelize.define('Task', {}, {timestamps: false})
, WorkerTasks = this.sequelize.define('WorkerTasks', {}, {timestamps: false});
Worker.hasMany(Task, { through: WorkerTasks });
Task.hasMany(Worker, { through: WorkerTasks });
// Test setup
return this.sequelize.sync().then(function() {
return Sequelize.Promise.all([
Worker.create({}),
Task.bulkCreate([{}, {}, {}, {}, {}]).then(function () {
return Task.findAll();
})
]);
}).spread(function (worker, tasks) {
// Set all tasks, then remove two tasks by instance, then remove two tasks by id, then return all tasks
return worker.setTasks(tasks).then(function () {
return worker.removeTasks([tasks[0], tasks[1]]);
}).then(function () {
return worker.removeTasks([tasks[2].id, tasks[3].id]);
}).then(function () { }).then(function () {
return worker.getTasks(); return worker.getTasks();
}); });
......
...@@ -137,6 +137,9 @@ if (Support.dialectIsMySQL()) { ...@@ -137,6 +137,9 @@ if (Support.dialectIsMySQL()) {
self.user.removeTask(self.tasks[0]).on('success', function() { self.user.removeTask(self.tasks[0]).on('success', function() {
self.user.getTasks().on('success', function(_tasks) { self.user.getTasks().on('success', function(_tasks) {
expect(_tasks.length).to.equal(self.tasks.length - 1) expect(_tasks.length).to.equal(self.tasks.length - 1)
self.user.removeTasks([self.tasks[1], self.tasks[2]]).on('success', function() {
self.user.getTasks().on('success', function(_tasks) {
expect(_tasks).to.have.length(self.tasks.length - 3)
done() done()
}) })
}) })
...@@ -147,4 +150,6 @@ if (Support.dialectIsMySQL()) { ...@@ -147,4 +150,6 @@ if (Support.dialectIsMySQL()) {
}) })
}) })
}) })
})
})
} }
...@@ -151,6 +151,9 @@ if (dialect.match(/^postgres/)) { ...@@ -151,6 +151,9 @@ if (dialect.match(/^postgres/)) {
self.user.removeTask(self.tasks[0]).on('success', function() { self.user.removeTask(self.tasks[0]).on('success', function() {
self.user.getTasks().on('success', function(_tasks) { self.user.getTasks().on('success', function(_tasks) {
expect(_tasks).to.have.length(self.tasks.length - 1) expect(_tasks).to.have.length(self.tasks.length - 1)
self.user.removeTasks([self.tasks[1], self.tasks[2]]).on('success', function() {
self.user.getTasks().on('success', function(_tasks) {
expect(_tasks).to.have.length(self.tasks.length - 3)
done() done()
}) })
}) })
...@@ -166,4 +169,6 @@ if (dialect.match(/^postgres/)) { ...@@ -166,4 +169,6 @@ if (dialect.match(/^postgres/)) {
}) })
}) })
}) })
})
})
} }
...@@ -16,7 +16,7 @@ if (dialect.match(/^postgres/)) { ...@@ -16,7 +16,7 @@ if (dialect.match(/^postgres/)) {
username: DataTypes.STRING, username: DataTypes.STRING,
email: { type: DataTypes.ARRAY(DataTypes.TEXT) }, email: { type: DataTypes.ARRAY(DataTypes.TEXT) },
settings: DataTypes.HSTORE, settings: DataTypes.HSTORE,
document: { type: DataTypes.HSTORE, defaultValue: { default: 'value' } }, document: { type: DataTypes.HSTORE, defaultValue: { default: "'value'" } },
phones: DataTypes.ARRAY(DataTypes.HSTORE), phones: DataTypes.ARRAY(DataTypes.HSTORE),
emergency_contact: DataTypes.JSON emergency_contact: DataTypes.JSON
}) })
...@@ -66,7 +66,7 @@ if (dialect.match(/^postgres/)) { ...@@ -66,7 +66,7 @@ if (dialect.match(/^postgres/)) {
username: 'bob', username: 'bob',
emergency_contact: { name: 'joe', phones: [1337, 42] } emergency_contact: { name: 'joe', phones: [1337, 42] }
}).on('sql', function (sql) { }).on('sql', function (sql) {
var expected = 'INSERT INTO "Users" ("id","username","document","emergency_contact","createdAt","updatedAt") VALUES (DEFAULT,\'bob\',\'"default"=>"value"\',\'{"name":"joe","phones":[1337,42]}\'' var expected = 'INSERT INTO "Users" ("id","username","document","emergency_contact","createdAt","updatedAt") VALUES (DEFAULT,\'bob\',\'"default"=>"\'\'value\'\'"\',\'{"name":"joe","phones":[1337,42]}\''
expect(sql.indexOf(expected)).to.equal(0); expect(sql.indexOf(expected)).to.equal(0);
}); });
}); });
...@@ -179,25 +179,24 @@ if (dialect.match(/^postgres/)) { ...@@ -179,25 +179,24 @@ if (dialect.match(/^postgres/)) {
}); });
describe('hstore', function() { describe('hstore', function() {
it('should tell me that a column is hstore and not USER-DEFINED', function(done) { it('should tell me that a column is hstore and not USER-DEFINED', function() {
this.sequelize.queryInterface.describeTable('Users').success(function(table) { return this.sequelize.queryInterface.describeTable('Users').then(function(table) {
expect(table.settings.type).to.equal('HSTORE') expect(table.settings.type).to.equal('HSTORE')
expect(table.document.type).to.equal('HSTORE') expect(table.document.type).to.equal('HSTORE')
done()
}) })
}) })
it('should stringify hstore with insert', function(done) { it('should stringify hstore with insert', function() {
this.User.create({ return this.User.create({
username: 'bob', username: 'bob',
email: ['myemail@email.com'], email: ['myemail@email.com'],
settings: {mailing: false, push: 'facebook', frequency: 3} settings: {mailing: false, push: 'facebook', frequency: 3}
}).on('sql', function(sql) { }).on('sql', function(sql) {
var expected = 'INSERT INTO "Users" ("id","username","email","settings","document","createdAt","updatedAt") VALUES (DEFAULT,\'bob\',ARRAY[\'myemail@email.com\']::TEXT[],\'"mailing"=>false,"push"=>"facebook","frequency"=>3\',\'"default"=>"value"\'' var expected = 'INSERT INTO "Users" ("id","username","email","settings","document","createdAt","updatedAt") VALUES (DEFAULT,\'bob\',ARRAY[\'myemail@email.com\']::TEXT[],\'"mailing"=>"false","push"=>"facebook","frequency"=>"3"\',\'"default"=>"\'\'value\'\'"\''
expect(sql.indexOf(expected)).to.equal(0) expect(sql.indexOf(expected)).to.equal(0)
done()
}) })
}) })
}) })
describe('enums', function() { describe('enums', function() {
...@@ -403,106 +402,105 @@ if (dialect.match(/^postgres/)) { ...@@ -403,106 +402,105 @@ if (dialect.match(/^postgres/)) {
}) })
}) })
it("should save hstore correctly", function(done) { it("should save hstore correctly", function() {
this.User return this.User.create({ username: 'user', email: ['foo@bar.com'], settings: { created: '"value"' }}).then(function(newUser) {
.create({ username: 'user', email: ['foo@bar.com'], settings: { created: { test: '"value"' }}})
.success(function(newUser) {
// Check to see if the default value for an hstore field works // Check to see if the default value for an hstore field works
expect(newUser.document).to.deep.equal({ default: 'value' }) expect(newUser.document).to.deep.equal({ default: "'value'" })
expect(newUser.settings).to.deep.equal({ created: { test: '"value"' }}) expect(newUser.settings).to.deep.equal({ created: '"value"' })
// Check to see if updating an hstore field works // Check to see if updating an hstore field works
newUser.updateAttributes({settings: {should: 'update', to: 'this', first: 'place'}}).success(function(oldUser){ return newUser.updateAttributes({settings: {should: 'update', to: 'this', first: 'place'}}).then(function(oldUser){
// Postgres always returns keys in alphabetical order (ascending) // Postgres always returns keys in alphabetical order (ascending)
expect(oldUser.settings).to.deep.equal({first: 'place', should: 'update', to: 'this'}) expect(oldUser.settings).to.deep.equal({first: 'place', should: 'update', to: 'this'})
done()
}) })
}) })
.error(console.log)
}) })
it('should save hstore array correctly', function(done) { it('should save hstore array correctly', function() {
this.User.create({ var User = this.User;
return this.User.create({
username: 'bob', username: 'bob',
email: ['myemail@email.com'], email: ['myemail@email.com'],
phones: [{ number: '123456789', type: 'mobile' }, { number: '987654321', type: 'landline' }] phones: [{ number: '123456789', type: 'mobile' }, { number: '987654321', type: 'landline' }, { number : '8675309', type : "Jenny's"}, {number : '5555554321', type : '"home"' }]
}).on('sql', function(sql) { }).then(function(){
var expected = 'INSERT INTO "Users" ("id","username","email","document","phones","createdAt","updatedAt") VALUES (DEFAULT,\'bob\',ARRAY[\'myemail@email.com\']::TEXT[],\'"default"=>"value"\',ARRAY[\'"number"=>"123456789","type"=>"mobile"\',\'"number"=>"987654321","type"=>"landline"\']::HSTORE[]' return User.find(1).then(function(user){
expect(sql).to.contain(expected) expect(user.phones.length).to.equal(4);
done() expect(user.phones[1].number).to.equal('987654321');
expect(user.phones[2].type).to.equal("Jenny's");
expect(user.phones[3].type).to.equal('"home"');
})
}) })
}) })
it("should update hstore correctly", function(done) { it('should bulkCreate with hstore property', function() {
var self = this var User = this.User;
this.User return this.User.bulkCreate([{
.create({ username: 'user', email: ['foo@bar.com'], settings: { created: { test: '"value"' }}}) username: 'bob',
.success(function(newUser) { email: ['myemail@email.com'],
settings: {mailing: true, push: 'facebook', frequency: 3}
}]).then(function(){
return User.find(1).then(function(user){
expect(user.settings.mailing).to.equal("true")
})
})
})
it("should update hstore correctly", function() {
var self = this;
return this.User.create({ username: 'user', email: ['foo@bar.com'], settings: { test: '"value"' }}).then(function(newUser) {
// Check to see if the default value for an hstore field works // Check to see if the default value for an hstore field works
expect(newUser.document).to.deep.equal({default: 'value'}) expect(newUser.document).to.deep.equal({default: "'value'"})
expect(newUser.settings).to.deep.equal({ created: { test: '"value"' }}) expect(newUser.settings).to.deep.equal({ test: '"value"' })
// Check to see if updating an hstore field works // Check to see if updating an hstore field works
self.User.update({settings: {should: 'update', to: 'this', first: 'place'}}, {where: newUser.identifiers}).success(function() { return self.User.update({settings: {should: 'update', to: 'this', first: 'place'}}, {where: newUser.identifiers}).then(function() {
newUser.reload().success(function() { return newUser.reload().success(function() {
// Postgres always returns keys in alphabetical order (ascending) // Postgres always returns keys in alphabetical order (ascending)
expect(newUser.settings).to.deep.equal({first: 'place', should: 'update', to: 'this'}) expect(newUser.settings).to.deep.equal({first: 'place', should: 'update', to: 'this'})
done()
});
}) })
}) })
.error(console.log) })
}) })
it("should update hstore correctly and return the affected rows", function(done) { it("should update hstore correctly and return the affected rows", function() {
var self = this var self = this
this.User return this.User.create({ username: 'user', email: ['foo@bar.com'], settings: { test: '"value"' }}).then(function(oldUser) {
.create({ username: 'user', email: ['foo@bar.com'], settings: { created: { test: '"value"' }}})
.success(function(oldUser) {
// Update the user and check that the returned object's fields have been parsed by the hstore library // Update the user and check that the returned object's fields have been parsed by the hstore library
self.User.update({settings: {should: 'update', to: 'this', first: 'place'}}, {where: oldUser.identifiers, returning: true }).spread(function(count, users) { return self.User.update({settings: {should: 'update', to: 'this', first: 'place'}}, {where: oldUser.identifiers, returning: true }).spread(function(count, users) {
expect(count).to.equal(1); expect(count).to.equal(1);
expect(users[0].settings).to.deep.equal({should: 'update', to: 'this', first: 'place'}) expect(users[0].settings).to.deep.equal({should: 'update', to: 'this', first: 'place'})
done()
}) })
}) })
.error(console.log)
}) })
it("should read hstore correctly", function(done) { it("should read hstore correctly", function() {
var self = this var self = this
var data = { username: 'user', email: ['foo@bar.com'], settings: { created: { test: '"value"' }}} var data = { username: 'user', email: ['foo@bar.com'], settings: { test: '"value"' }}
this.User return this.User.create(data)
.create(data) .then(function() {
.success(function() { return self.User.find({ where: { username: 'user' }})
})
.then(function(user){
// Check that the hstore fields are the same when retrieving the user // Check that the hstore fields are the same when retrieving the user
self.User.find({ where: { username: 'user' }})
.success(function(user) {
expect(user.settings).to.deep.equal(data.settings) expect(user.settings).to.deep.equal(data.settings)
done()
}) })
}) })
.error(console.log)
})
it('should read an hstore array correctly', function(done) { it('should read an hstore array correctly', function() {
var self = this var self = this
var data = { username: 'user', email: ['foo@bar.com'], phones: [{ number: '123456789', type: 'mobile' }, { number: '987654321', type: 'landline' }] } var data = { username: 'user', email: ['foo@bar.com'], phones: [{ number: '123456789', type: 'mobile' }, { number: '987654321', type: 'landline' }] }
this.User return this.User.create(data)
.create(data) .then(function() {
.success(function() {
// Check that the hstore fields are the same when retrieving the user // Check that the hstore fields are the same when retrieving the user
self.User.find({ where: { username: 'user' }}) return self.User.find({ where: { username: 'user' }});
.success(function(user) { }).then(function(user) {
expect(user.phones).to.deep.equal(data.phones) expect(user.phones).to.deep.equal(data.phones)
done()
})
}) })
}) })
...@@ -510,17 +508,17 @@ if (dialect.match(/^postgres/)) { ...@@ -510,17 +508,17 @@ if (dialect.match(/^postgres/)) {
var self = this var self = this
self.User self.User
.create({ username: 'user1', email: ['foo@bar.com'], settings: { created: { test: '"value"' }}}) .create({ username: 'user1', email: ['foo@bar.com'], settings: { test: '"value"' }})
.then(function() { .then(function() {
return self.User.create({ username: 'user2', email: ['foo2@bar.com'], settings: { updated: { another: '"example"' }}}) return self.User.create({ username: 'user2', email: ['foo2@bar.com'], settings: { another : '"example"' }})
}) })
.then(function() { .then(function() {
// Check that the hstore fields are the same when retrieving the user // Check that the hstore fields are the same when retrieving the user
return self.User.findAll({ order: 'username' }) return self.User.findAll({ order: 'username' })
}) })
.then(function(users) { .then(function(users) {
expect(users[0].settings).to.deep.equal({ created: { test: '"value"' }}) expect(users[0].settings).to.deep.equal({ test: '"value"' })
expect(users[1].settings).to.deep.equal({ updated: { another: '"example"' }}) expect(users[1].settings).to.deep.equal({ another: '"example"' })
done() done()
}) })
......
...@@ -3,131 +3,83 @@ var chai = require('chai') ...@@ -3,131 +3,83 @@ var chai = require('chai')
, expect = chai.expect , expect = chai.expect
, Support = require(__dirname + '/../support') , Support = require(__dirname + '/../support')
, dialect = Support.getTestDialect() , dialect = Support.getTestDialect()
, hstore = require(__dirname + '/../../lib/dialects/postgres/hstore') , hstore = require("../../lib/dialects/postgres/hstore")
chai.config.includeStack = true chai.config.includeStack = true
if (dialect.match(/^postgres/)) { if (dialect.match(/^postgres/)) {
describe('[POSTGRES Specific] hstore', function() { describe('[POSTGRES Specific] hstore', function() {
describe('stringifyPart', function() { describe('stringify', function() {
it("handles undefined values correctly", function(done) { it('should handle empty objects correctly', function() {
expect(hstore.stringifyPart(undefined)).to.equal('NULL') expect(hstore.stringify({ })).to.equal('')
done()
})
it("handles null values correctly", function(done) {
expect(hstore.stringifyPart(null)).to.equal('NULL')
done()
})
it("handles boolean values correctly", function(done) {
expect(hstore.stringifyPart(false)).to.equal('false')
expect(hstore.stringifyPart(true)).to.equal('true')
done()
}) })
it("handles strings correctly", function(done) { it('should handle null values correctly', function() {
expect(hstore.stringifyPart('foo')).to.equal('"foo"') expect(hstore.stringify({ null: null })).to.equal('"null"=>NULL')
done()
}) })
it("handles strings with backslashes correctly", function(done) { it('should handle null values correctly', function() {
expect(hstore.stringifyPart("\\'literally\\'")).to.equal('"\\\\\'literally\\\\\'"') expect(hstore.stringify({ foo: null })).to.equal('"foo"=>NULL')
done()
}) })
it("handles arrays correctly", function(done) { it('should handle empty string correctly', function(done) {
expect(hstore.stringifyPart([1,['2'],'"3"'])).to.equal('"[1,[\\"2\\"],\\"\\\\\\"3\\\\\\"\\"]"') expect(hstore.stringify({foo : ""})).to.equal('"foo"=>\"\"')
done() done()
}) })
it("handles simple objects correctly", function(done) { it('should handle a string with backslashes correctly', function() {
expect(hstore.stringifyPart({ test: 'value' })).to.equal('"{\\"test\\":\\"value\\"}"') expect(hstore.stringify({foo : "\\"})).to.equal('"foo"=>"\\\\"')
done()
}) })
it("handles nested objects correctly", function(done) { it('should handle a string with double quotes correctly', function() {
expect(hstore.stringifyPart({ test: { nested: 'value' } })).to.equal('"{\\"test\\":{\\"nested\\":\\"value\\"}}"') expect(hstore.stringify({foo : '""a"'})).to.equal('"foo"=>"\\"\\"a\\""')
done()
}) })
it("handles objects correctly", function(done) { it('should handle a string with single quotes correctly', function() {
expect(hstore.stringifyPart({test: {nested: {value: {including: '"string"'}}}})).to.equal('"{\\"test\\":{\\"nested\\":{\\"value\\":{\\"including\\":\\"\\\\\\"string\\\\\\"\\"}}}}"') expect(hstore.stringify({foo : "''a'"})).to.equal('"foo"=>"\'\'\'\'a\'\'"')
done()
})
}) })
describe('stringify', function() { it('should handle simple objects correctly', function() {
it('should handle empty objects correctly', function(done) { expect(hstore.stringify({ test: 'value' })).to.equal('"test"=>"value"')
expect(hstore.stringify({ })).to.equal('')
done()
}) })
it('should handle null values correctly', function(done) {
expect(hstore.stringify({ null: null })).to.equal('"null"=>NULL')
done()
}) })
it('should handle simple objects correctly', function(done) { describe('parse', function() {
expect(hstore.stringify({ test: 'value' })).to.equal('"test"=>"value"') it('should handle a null object correctly', function() {
done() expect(hstore.parse(null)).to.deep.equal(null)
}) })
it('should handle arrays correctly', function(done) { it('should handle empty string correctly', function() {
expect(hstore.stringify([{ test: 'value' }, { another: 'val' }])).to.deep.equal(['\"test\"=>\"value\"','\"another\"=>\"val\"']) expect(hstore.parse('"foo"=>\"\"')).to.deep.equal({foo : ""})
done()
});
it('should handle nested objects correctly', function(done) {
expect(hstore.stringify({ test: { nested: 'value' } })).to.equal('"test"=>"{\\"nested\\":\\"value\\"}"')
done()
}) })
it('should handle nested arrays correctly', function(done) { it('should handle a string with double quotes correctly', function() {
expect(hstore.stringify({ test: [ 1, '2', [ '"3"' ] ] })).to.equal('"test"=>"[1,\\"2\\",[\\"\\\\\\"3\\\\\\"\\"]]"') expect(hstore.parse('"foo"=>"\\\"\\\"a\\\""')).to.deep.equal({foo : "\"\"a\""})
done()
}) })
it('should handle multiple keys with different types of values', function(done) { it('should handle a string with single quotes correctly', function() {
expect(hstore.stringify({ true: true, false: false, null: null, undefined: undefined, integer: 1, array: [1,'2'], object: { object: 'value' }})).to.equal('"true"=>true,"false"=>false,"null"=>NULL,"undefined"=>NULL,"integer"=>1,"array"=>"[1,\\"2\\"]","object"=>"{\\"object\\":\\"value\\"}"') expect(hstore.parse('"foo"=>"\'\'\'\'a\'\'"')).to.deep.equal({foo : "''a'"})
done()
})
}) })
describe('parse', function() { it('should handle a string with backslashes correctly', function() {
it('should handle null objects correctly', function(done) { expect(hstore.parse('"foo"=>"\\\\"')).to.deep.equal({foo : "\\"})
expect(hstore.parse(null)).to.equal(null)
done()
}) })
it('should handle empty objects correctly', function(done) { it('should handle empty objects correctly', function() {
expect(hstore.parse('')).to.deep.equal({ }) expect(hstore.parse('')).to.deep.equal({ })
done()
}) })
it('should handle simple objects correctly', function(done) { it('should handle simple objects correctly', function() {
expect(hstore.parse('"test"=>"value"')).to.deep.equal({ test: 'value' }) expect(hstore.parse('"test"=>"value"')).to.deep.equal({ test: 'value' })
done()
})
it('should handle arrays correctly', function(done) {
expect(hstore.parse('{"\\"test\\"=>\\"value\\"","\\"another\\"=>\\"val\\""}')).to.deep.equal([{ test: 'value' }, { another: 'val' }])
done()
}) })
it('should handle nested objects correctly', function(done) {
expect(hstore.parse('"test"=>"{\\"nested\\":\\"value\\"}"')).to.deep.equal({ test: { nested: 'value' } })
done()
}) })
describe('stringify and parse', function() {
it('should handle nested arrays correctly', function(done) { it('should stringify then parse back the same structure', function(){
expect(hstore.parse('"test"=>"[1,\\"2\\",[\\"\\\\\\"3\\\\\\"\\"]]"')).to.deep.equal({ test: [ 1, '2', [ '"3"' ] ] }) var testObj = {foo : "bar", count : "1", emptyString : "", quotyString : '""', extraQuotyString : '"""a"""""', backslashes : '\\f023', moreBackslashes : '\\f\\0\\2\\1', backslashesAndQuotes : '\\"\\"uhoh"\\"', nully : null};
done() expect(hstore.parse(hstore.stringify(testObj))).to.deep.equal(testObj);
}) expect(hstore.parse(hstore.stringify(hstore.parse(hstore.stringify(testObj))))).to.deep.equal(testObj);
it('should handle multiple keys with different types of values', function(done) {
expect(hstore.parse('"true"=>true,"false"=>false,"null"=>NULL,"undefined"=>NULL,"integer"=>1,"array"=>"[1,\\"2\\"]","object"=>"{\\"object\\":\\"value\\"}"')).to.deep.equal({ true: true, false: false, null: null, undefined: null, integer: "1", array: [1,'2'], object: { object: 'value' }})
done()
}) })
}) })
}) })
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!