diff --git a/.gitignore b/.gitignore index 6a03a6e..813f0f9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ /node_modules /test.sqlite3 /test/coverage +/test/mysql/knexfile.js /test/postgres/knexfile.js /test/sqlite/knexfile.js diff --git a/.travis.yml b/.travis.yml index a45d109..ab0fc70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,16 @@ language: node_js +services: + - mysql + addons: postgresql: "9.2" before_script: + - cp test/mysql/knexfile.js.dist test/mysql/knexfile.js - cp test/postgres/knexfile.js.dist test/postgres/knexfile.js - cp test/sqlite/knexfile.js.dist test/sqlite/knexfile.js + - mysql -e "create database \`bookshelf-json-columns\`;" -uroot - psql -U postgres -c 'create database "bookshelf-json-columns";' node_js: diff --git a/README.md b/README.md index f0ba7c6..6e6c36a 100644 --- a/README.md +++ b/README.md @@ -75,12 +75,12 @@ bookshelf.Model.extend({ Contributions are welcome and greatly appreciated, so feel free to fork this repository and submit pull requests. -**bookshelf-json-columns** supports PostgreSQL and SQLite3. You can find test suites for each of these database engines in the *test/postgres* and *test/sqlite* folders. +**bookshelf-json-columns** supports PostgreSQL, SQLite3 and MySQL. You can find test suites for all these database engines in the *test* folder. ### Setting up - Fork and clone the **bookshelf-json-columns** repository. -- Duplicate *test/postgres/knexfile.js.dist* and *test/sqlite/knexfile.js.dist* files and update them to your needs. +- Duplicate all *.dist* knexfiles and update them to your needs. - Make sure all the tests pass: ```sh diff --git a/package.json b/package.json index 9805c62..9d08d9c 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "eslint-config-seegno": "8.0.0", "knex": "0.12.6", "mocha": "3.1.2", + "mysql": "2.12.0", "nyc": "8.3.2", "pg": "6.1.0", "pre-commit": "1.1.3", diff --git a/src/index.js b/src/index.js index d1e8f1a..9aeabb0 100644 --- a/src/index.js +++ b/src/index.js @@ -49,7 +49,7 @@ function parse(model, response, options = {}) { export default Bookshelf => { const Model = Bookshelf.Model.prototype; const client = Bookshelf.knex.client.config.client; - const parseOnFetch = client === 'sqlite' || client === 'sqlite3'; + const parseOnFetch = client === 'sqlite' || client === 'sqlite3' || client === 'mysql'; Bookshelf.Model = Bookshelf.Model.extend({ initialize() { diff --git a/test/index.js b/test/index.js index eaeb1f1..eab1f27 100644 --- a/test/index.js +++ b/test/index.js @@ -3,5 +3,6 @@ * Test suite. */ +import './mysql'; import './postgres'; import './sqlite'; diff --git a/test/mysql/index.js b/test/mysql/index.js new file mode 100644 index 0000000..4d8040f --- /dev/null +++ b/test/mysql/index.js @@ -0,0 +1,187 @@ + +/** + * Module dependencies. + */ + +import { dropTable, recreateTable } from '../utils'; +import bookshelf from 'bookshelf'; +import jsonColumns from '../../src'; +import knex from 'knex'; +import knexfile from './knexfile'; +import should from 'should'; +import sinon from 'sinon'; + +/** + * Test `bookshelf-json-columns` plugin with MySQL client. + */ + +describe('with MySQL client', () => { + const repository = bookshelf(knex(knexfile)); + const ModelPrototype = repository.Model.prototype; + + repository.plugin(jsonColumns); + + before(async () => { + await recreateTable(repository); + }); + + after(async () => { + await dropTable(repository); + }); + + describe('when a JSON column is not registered', () => { + const Model = repository.Model.extend({ tableName: 'test' }); + + it('should throw an error on create', async () => { + try { + await Model.forge().save({ foo: { bar: 'baz' } }); + + should.fail(); + } catch (e) { + e.should.be.instanceOf(Error); + e.code.should.equal('ER_BAD_FIELD_ERROR'); + } + }); + + it('should throw an error creating through a collection', async () => { + const Collection = repository.Collection.extend({ model: Model }); + const collection = Collection.forge(); + + try { + await collection.create(Model.forge({ foo: { bar: 'baz' } })); + + should.fail(); + } catch (e) { + e.should.be.instanceOf(Error); + e.code.should.equal('ER_BAD_FIELD_ERROR'); + } + }); + + it('should throw an error on update', async () => { + const model = await Model.forge().save(); + + try { + await model.save({ foo: { bar: 'baz' } }); + + should.fail(); + } catch (e) { + e.should.be.instanceOf(Error); + e.code.should.equal('ER_BAD_FIELD_ERROR'); + } + }); + + it('should not override model prototype initialize method', async () => { + sinon.spy(ModelPrototype, 'initialize'); + + Model.forge(); + + ModelPrototype.initialize.callCount.should.equal(1); + + sinon.restore(ModelPrototype); + }); + }); + + describe('when a JSON column is registered', () => { + const Model = repository.Model.extend({ tableName: 'test' }, { jsonColumns: ['foo'] }); + + it('should keep a JSON value on create', async () => { + const model = await Model.forge().save({ foo: { bar: 'baz' } }); + + model.get('foo').should.eql({ bar: 'baz' }); + }); + + it('should keep a JSON value using save with a key and value', async () => { + const model = await Model.forge().save('foo', { bar: 'baz' }, { method: 'insert' }); + + model.get('foo').should.eql({ bar: 'baz' }); + }); + + it('should keep a JSON value when creating through a collection', async () => { + const Collection = repository.Collection.extend({ model: Model }); + const collection = Collection.forge(); + + await collection.create(Model.forge({ foo: { bar: 'baz' } })); + + collection.at(0).get('foo').should.eql({ bar: 'baz' }); + }); + + it('should keep a null value on create', async () => { + const model = await Model.forge().save(); + + should(model.get('foo')).be.undefined(); + }); + + it('should keep a JSON value on update', async () => { + const model = await Model.forge().save(); + + await model.save({ foo: { bar: 'baz' } }); + + model.get('foo').should.eql({ bar: 'baz' }); + }); + + it('should keep a null value on update', async () => { + const model = await Model.forge().save(); + + await model.save(); + + should(model.get('foo')).be.undefined(); + }); + + it('should not stringify null values on update with `patch` option', async () => { + sinon.spy(ModelPrototype, 'save'); + + const model = await Model.forge().save(); + + await model.save({ foo: null }, { patch: true }); + + ModelPrototype.save.callCount.should.equal(2); + ModelPrototype.save.secondCall.args[0].should.eql({ foo: null }); + + sinon.restore(ModelPrototype); + }); + + it('should keep a JSON value when updating with `patch` option', async () => { + const model = await Model.forge().save(); + + await model.save({ foo: { bar: 'baz' } }, { patch: true }); + + model.get('foo').should.eql({ bar: 'baz' }); + }); + + it('should keep a JSON value when updating other columns', async () => { + const model = await Model.forge().save({ foo: { bar: 'baz' } }); + + await model.save({ qux: 'qix' }, { patch: true }); + + model.get('foo').should.eql({ bar: 'baz' }); + }); + + it('should keep a JSON value on fetch', async () => { + await Model.forge().save({ foo: { bar: 'baz' }, qux: 'qix' }); + + const model = await Model.forge({ qux: 'qix' }).fetch(); + + model.get('foo').should.eql({ bar: 'baz' }); + }); + + it('should keep a JSON value when updating through query', async () => { + const model = await Model.forge().save({ foo: { bar: 'baz' } }); + + model.query().update({ qux: 'qix' }); + + await model.refresh(); + + model.get('foo').should.eql({ bar: 'baz' }); + }); + + it('should not override model initialize method', async () => { + sinon.spy(ModelPrototype, 'initialize'); + + Model.forge(); + + ModelPrototype.initialize.callCount.should.equal(1); + + sinon.restore(ModelPrototype); + }); + }); +}); diff --git a/test/mysql/knexfile.js.dist b/test/mysql/knexfile.js.dist new file mode 100644 index 0000000..f10c7c7 --- /dev/null +++ b/test/mysql/knexfile.js.dist @@ -0,0 +1,14 @@ + +/** + * Export MySQL knexfile. + */ + +export default { + client: 'mysql', + connection: { + charset: 'utf8', + database: 'bookshelf-json-columns', + host: 'localhost', + user: 'root' + } +};