From cb93e0da44ed2d82674be290a551ffbe60e0803b Mon Sep 17 00:00:00 2001 From: Ricardo Gama Date: Tue, 18 Jul 2017 14:45:58 +0100 Subject: [PATCH 1/3] Add standard-error to dependencies --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 045bfd4..48032aa 100644 --- a/package.json +++ b/package.json @@ -66,5 +66,8 @@ }, "pre-commit": [ "lint" - ] + ], + "dependencies": { + "standard-error": "1.1.0" + } } From 71b6d7529ae6124099209c3a8d667ea289f03bdb Mon Sep 17 00:00:00 2001 From: Ricardo Gama Date: Tue, 18 Jul 2017 14:46:14 +0100 Subject: [PATCH 2/3] Add InvalidJSONError --- src/invalid-json-error.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/invalid-json-error.js diff --git a/src/invalid-json-error.js b/src/invalid-json-error.js new file mode 100644 index 0000000..5abdba3 --- /dev/null +++ b/src/invalid-json-error.js @@ -0,0 +1,12 @@ + +/** + * Module dependencies. + */ + +import StandardError from 'standard-error'; + +/** + * Export `InvalidJSONError`. + */ + +export default class InvalidJSONError extends StandardError {} From dfed5d9a3bbe5ca0c769ebebeece20841fc31b72 Mon Sep 17 00:00:00 2001 From: Ricardo Gama Date: Tue, 18 Jul 2017 14:46:39 +0100 Subject: [PATCH 3/3] Add throwJSONErrors option --- README.md | 24 +++++++++ src/index.js | 124 ++++++++++++++++++++++++------------------- test/mysql/index.js | 58 ++++++++++++++++++++ test/sqlite/index.js | 58 ++++++++++++++++++++ 4 files changed, 210 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 6e6c36a..12d0784 100644 --- a/README.md +++ b/README.md @@ -71,6 +71,30 @@ bookshelf.Model.extend({ }); ``` +### Options + +This plugin supports the following options: + +#### `throwJSONErrors` + +The `throwJSONErrors` option can be used to force the plugin to throw an [InvalidJSONError](/src/invalid-json-error.js) when an invalid JSON value is fetched: + +```js +var bookshelf = require('bookshelf')(knex); +var jsonColumns = require('bookshelf-json-columns'); + +bookshelf.plugin(jsonColumns, { + throwJSONErrors: true +}); +``` + +If you pass this option when registering the plugin, the handling of JSON errors will be done in every `fetch` call. However, you can use it without registering by passing it to the `fetch` method, or even disable it if you did: + +```js +Model.forge({ foo: 'bar' }).fetch({ throwJSONErrors: true }); +Model.forge({ foo: 'bar' }).fetch({ throwJSONErrors: false }); +``` + ## Contributing Contributions are welcome and greatly appreciated, so feel free to fork this repository and submit pull requests. diff --git a/src/index.js b/src/index.js index c496337..81e330e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,55 +1,69 @@ /** - * Stringify JSON columns. + * Module dependencies. */ -function stringify(model, attributes, options) { - // Do not stringify with `patch` option. - if (options && options.patch) { - return; - } +import InvalidJSONError from './invalid-json-error'; + +/** + * Export `bookshelf-json-columns` plugin. + */ - // Mark json columns as stringfied. - options.parseJsonColumns = true; +export default (Bookshelf, { throwJSONErrors } = {}) => { + const Model = Bookshelf.Model.prototype; + const client = Bookshelf.knex.client.config.client; + const parseOnFetch = client === 'sqlite' || client === 'sqlite3' || client === 'mysql'; - this.constructor.jsonColumns.forEach(column => { - if (this.attributes[column]) { - this.attributes[column] = JSON.stringify(this.attributes[column]); + /** + * Stringify JSON columns. + */ + + function stringify(model, attributes, options) { + // Do not stringify with `patch` option. + if (options && options.patch) { + return; } - }); -} -/** - * Parse JSON columns. - */ + // Mark json columns as stringfied. + options.parseJsonColumns = true; -function parse(model, response, options = {}) { - // Do not parse with `patch` option. - if (options.patch) { - return; + this.constructor.jsonColumns.forEach(column => { + if (this.attributes[column]) { + this.attributes[column] = JSON.stringify(this.attributes[column]); + } + }); } - // Do not parse on `fetched` event after saving. - // eslint-disable-next-line no-underscore-dangle - if (!options.parseJsonColumns && options.query && options.query._method !== 'select') { - return; - } + /** + * Parse JSON columns. + */ - this.constructor.jsonColumns.forEach(column => { - if (this.attributes[column]) { - this.attributes[column] = JSON.parse(this.attributes[column]); + function parse(model, response, options = {}) { + // Do not parse with `patch` option. + if (options.patch) { + return; } - }); -} -/** - * Export `bookshelf-json-columns` plugin. - */ + // Do not parse on `fetched` event after saving. + // eslint-disable-next-line no-underscore-dangle + if (!options.parseJsonColumns && options.query && options.query._method !== 'select') { + return; + } -export default Bookshelf => { - const Model = Bookshelf.Model.prototype; - const client = Bookshelf.knex.client.config.client; - const parseOnFetch = client === 'sqlite' || client === 'sqlite3' || client === 'mysql'; + this.constructor.jsonColumns.forEach(column => { + if (this.attributes[column]) { + try { + this.attributes[column] = JSON.parse(this.attributes[column]); + } catch (e) { + throw options.throwJSONErrors === true || throwJSONErrors && options.throwJSONErrors !== false ? new InvalidJSONError({ value: this.attributes[column] }) : e; + } + } + }); + } + + /** + * Extend Model. + */ Bookshelf.Model = Bookshelf.Model.extend({ initialize() { @@ -112,26 +126,28 @@ export default Bookshelf => { } }); - if (!parseOnFetch) { - return; - } + /** + * Extend Collection. + */ - const Collection = Bookshelf.Collection.prototype; + if (parseOnFetch) { + const Collection = Bookshelf.Collection.prototype; - Bookshelf.Collection = Bookshelf.Collection.extend({ - initialize() { - if (!this.model.jsonColumns) { - return Collection.initialize.apply(this, arguments); - } + Bookshelf.Collection = Bookshelf.Collection.extend({ + initialize() { + if (!this.model.jsonColumns) { + return Collection.initialize.apply(this, arguments); + } - // Parse JSON columns after collection is fetched. - this.on('fetched', collection => { - collection.models.forEach(model => { - parse.apply(model); + // Parse JSON columns after collection is fetched. + this.on('fetched', collection => { + collection.models.forEach(model => { + parse.apply(model); + }); }); - }); - return Collection.initialize.apply(this, arguments); - } - }); + return Collection.initialize.apply(this, arguments); + } + }); + } }; diff --git a/test/mysql/index.js b/test/mysql/index.js index 69830d6..b6fc3b4 100644 --- a/test/mysql/index.js +++ b/test/mysql/index.js @@ -4,6 +4,7 @@ */ import { dropTable, recreateTable } from '../utils'; +import InvalidJSONError from '../../src/invalid-json-error'; import bookshelf from 'bookshelf'; import jsonColumns from '../../src'; import knex from 'knex'; @@ -194,4 +195,61 @@ describe('with MySQL client', () => { sinon.restore(ModelPrototype); }); }); + + describe('when `throwJSONErrors` option is not provided', () => { + const Model = repository.Model.extend({ tableName: 'test' }, { jsonColumns: ['qux'] }); + + it('should throw an `SyntaxError` when fetching an invalid JSON value', async () => { + const [id] = await repository.knex('test').insert({ qux: 'qix' }); + + try { + await Model.forge({ id }).fetch(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(SyntaxError); + } + }); + + it('should throw an `InvalidJSONError` when fetching an invalid JSON value and the `throwJSONErrors` option is provided as `true`', async () => { + const [id] = await repository.knex('test').insert({ qux: 'qix' }); + + try { + await Model.forge({ id }).fetch({ throwJSONErrors: true }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidJSONError).with.properties({ value: 'qix' }); + } + }); + }); + + describe('when `throwJSONErrors` option is provided', () => { + const repository = bookshelf(knex(knexfile)).plugin(jsonColumns, { throwJSONErrors: true }); + const Model = repository.Model.extend({ tableName: 'test' }, { jsonColumns: ['qux'] }); + + it('should throw an `InvalidJSONError` when fetching an invalid JSON value', async () => { + const [id] = await repository.knex('test').insert({ qux: 'qix' }); + + try { + await Model.forge({ id }).fetch(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidJSONError).with.properties({ value: 'qix' }); + } + }); + + it('should throw a `SyntaxError` when fetching an invalid JSON value and the `throwJSONErrors` option is provided as `false`', async () => { + const [id] = await repository.knex('test').insert({ qux: 'qix' }); + + try { + await Model.forge({ id }).fetch({ throwJSONErrors: false }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(SyntaxError); + } + }); + }); }); diff --git a/test/sqlite/index.js b/test/sqlite/index.js index cbd1f69..e2bd9e4 100644 --- a/test/sqlite/index.js +++ b/test/sqlite/index.js @@ -4,6 +4,7 @@ */ import { clearTable, dropTable, recreateTable } from '../utils'; +import InvalidJSONError from '../../src/invalid-json-error'; import bookshelf from 'bookshelf'; import jsonColumns from '../../src'; import knex from 'knex'; @@ -231,4 +232,61 @@ describe('with SQLite client', () => { sinon.restore(CollectionPrototype); }); }); + + describe('when `throwJSONErrors` option is not provided', () => { + const Model = repository.Model.extend({ tableName: 'test' }, { jsonColumns: ['qux'] }); + + it('should throw an `SyntaxError` when fetching an invalid JSON value', async () => { + const [id] = await repository.knex('test').insert({ qux: 'qix' }); + + try { + await Model.forge({ id }).fetch(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(SyntaxError); + } + }); + + it('should throw an `InvalidJSONError` when fetching an invalid JSON value and the `throwJSONErrors` option is provided as `true`', async () => { + const [id] = await repository.knex('test').insert({ qux: 'qix' }); + + try { + await Model.forge({ id }).fetch({ throwJSONErrors: true }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidJSONError).with.properties({ value: 'qix' }); + } + }); + }); + + describe('when `throwJSONErrors` option is provided', () => { + const repository = bookshelf(knex(knexfile)).plugin(jsonColumns, { throwJSONErrors: true }); + const Model = repository.Model.extend({ tableName: 'test' }, { jsonColumns: ['qux'] }); + + it('should throw an `InvalidJSONError` when fetching an invalid JSON value', async () => { + const [id] = await repository.knex('test').insert({ qux: 'qix' }); + + try { + await Model.forge({ id }).fetch(); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(InvalidJSONError).with.properties({ value: 'qix' }); + } + }); + + it('should throw a `SyntaxError` when fetching an invalid JSON value and the `throwJSONErrors` option is provided as `false`', async () => { + const [id] = await repository.knex('test').insert({ qux: 'qix' }); + + try { + await Model.forge({ id }).fetch({ throwJSONErrors: false }); + + should.fail(); + } catch (e) { + e.should.be.an.instanceOf(SyntaxError); + } + }); + }); });