Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add throwJSONErrors option #51

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 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.
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,8 @@
},
"pre-commit": [
"lint"
]
],
"dependencies": {
"standard-error": "1.1.0"
}
}
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);
}
});
}
};
12 changes: 12 additions & 0 deletions src/invalid-json-error.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@

/**
* Module dependencies.
*/

import StandardError from 'standard-error';

/**
* Export `InvalidJSONError`.
*/

export default class InvalidJSONError extends StandardError {}
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);
}
});
});
});