Skip to content

Commit

Permalink
Add throwJSONErrors option
Browse files Browse the repository at this point in the history
  • Loading branch information
Ricardo Gama committed Jul 18, 2017
1 parent 71b6d75 commit c1c9d61
Show file tree
Hide file tree
Showing 4 changed files with 210 additions and 54 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 it with the plugin by passing the option 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.
Expand Down
124 changes: 70 additions & 54 deletions src/index.js
Original file line number Diff line number Diff line change
@@ -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() {
Expand Down Expand Up @@ -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);
}
});
}
};
58 changes: 58 additions & 0 deletions test/mysql/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
});
});
});
58 changes: 58 additions & 0 deletions test/sqlite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
}
});
});
});

0 comments on commit c1c9d61

Please sign in to comment.