├── .babelrc ├── .eslintrc.yml ├── .gitignore ├── .npmignore ├── .nycrc ├── .travis.yml ├── CHANGELOG.md ├── README.md ├── dist └── index.js ├── docker-compose.yml ├── package-lock.json ├── package.json ├── src └── index.js └── test ├── index.js ├── mysql ├── index.js └── knexfile.js.dist ├── postgres ├── index.js └── knexfile.js.dist ├── sqlite ├── index.js └── knexfile.js.dist └── utils └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "test": { 4 | "plugins": ["istanbul"] 5 | } 6 | }, 7 | "presets": ["es2015-node", "stage-3"], 8 | "plugins": ["add-module-exports", "array-includes"] 9 | } 10 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | extends: seegno 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.nyc_output 2 | /node_modules 3 | /test.sqlite3 4 | /test/coverage 5 | /test/mysql/knexfile.js 6 | /test/postgres/knexfile.js 7 | /test/sqlite/knexfile.js 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.babelrc 2 | /.eslintrc.yml 3 | /.nycrc 4 | /.travis.yml 5 | /src 6 | /test 7 | /test.sqlite3 8 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "instrument": false, 3 | "report-dir": "test/coverage", 4 | "reporter": [ 5 | "html", 6 | "lcov", 7 | "text-summary" 8 | ], 9 | "sourceMap": false 10 | } 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: bionic 2 | 3 | language: node_js 4 | 5 | before_script: 6 | - docker-compose up -d 7 | - cp test/mysql/knexfile.js.dist test/mysql/knexfile.js 8 | - cp test/postgres/knexfile.js.dist test/postgres/knexfile.js 9 | - cp test/sqlite/knexfile.js.dist test/sqlite/knexfile.js 10 | - docker exec mysql mysql -e "create database \`bookshelf-json-columns\`;" -uroot 11 | - docker exec postgres psql -U postgres -c 'create database "bookshelf-json-columns";' 12 | 13 | node_js: 14 | - "6" 15 | - "7" 16 | 17 | after_success: 18 | - npm run coveralls 19 | 20 | sudo: false 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [3.0.0](https://github.com/seegno/bookshelf-json-columns/tree/) (2019-12-28) 4 | 5 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/2.1.1...3.0.0) 6 | 7 | **Closed issues:** 8 | 9 | - Not compatible with Bookshelf new version [#55](https://github.com/seegno/bookshelf-json-columns/issues/55) 10 | 11 | **Merged pull requests:** 12 | 13 | - Add support for Bookshelf 1.0.0 [#56](https://github.com/seegno/bookshelf-json-columns/pull/56) ([ricardogama](https://github.com/ricardogama)) 14 | 15 | ## [2.1.1](https://github.com/seegno/bookshelf-json-columns/tree/2.1.1) (2017-07-18) 16 | 17 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/2.1.0...2.1.1) 18 | 19 | **Merged pull requests:** 20 | 21 | - Fix parsing empty strings [#50](https://github.com/seegno/bookshelf-json-columns/pull/50) ([zhongzhi107](https://github.com/zhongzhi107)) 22 | 23 | ## [2.1.0](https://github.com/seegno/bookshelf-json-columns/tree/2.1.0) (2017-03-01) 24 | 25 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/2.0.1...2.1.0) 26 | 27 | **Closed issues:** 28 | 29 | - Why is parse on fetch limited to sqlite? [#46](https://github.com/seegno/bookshelf-json-columns/issues/46) 30 | 31 | **Merged pull requests:** 32 | 33 | - Add support for MySQL [#47](https://github.com/seegno/bookshelf-json-columns/pull/47) ([ricardogama](https://github.com/ricardogama)) 34 | - Add lcov reporter to .nycrc [#45](https://github.com/seegno/bookshelf-json-columns/pull/45) ([abelsoares](https://github.com/abelsoares)) 35 | 36 | ## [2.0.1](https://github.com/seegno/bookshelf-json-columns/tree/2.0.1) (2016-11-11) 37 | 38 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/2.0.0...2.0.1) 39 | 40 | **Closed issues:** 41 | 42 | - model.refresh is not working properly | bug [#42](https://github.com/seegno/bookshelf-json-columns/issues/42) 43 | - Malformed array literal [#41](https://github.com/seegno/bookshelf-json-columns/issues/41) 44 | 45 | **Merged pull requests:** 46 | 47 | - Add user to postgres knexfile [#44](https://github.com/seegno/bookshelf-json-columns/pull/44) ([ricardogama](https://github.com/ricardogama)) 48 | - Fix parsing on saving through query [#43](https://github.com/seegno/bookshelf-json-columns/pull/43) ([ricardogama](https://github.com/ricardogama)) 49 | 50 | ## [2.0.0](https://github.com/seegno/bookshelf-json-columns/tree/2.0.0) (2016-10-25) 51 | 52 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/1.2.2...2.0.0) 53 | 54 | **Closed issues:** 55 | 56 | - Idea: automatic string to Date conversion [#34](https://github.com/seegno/bookshelf-json-columns/issues/34) 57 | 58 | **Merged pull requests:** 59 | 60 | - Add .nycrc to .npmignore [#40](https://github.com/seegno/bookshelf-json-columns/pull/40) ([ricardogama](https://github.com/ricardogama)) 61 | - Remove knex client duplicate condition [#39](https://github.com/seegno/bookshelf-json-columns/pull/39) ([ricardogama](https://github.com/ricardogama)) 62 | - Add node version badge [#38](https://github.com/seegno/bookshelf-json-columns/pull/38) ([ricardogama](https://github.com/ricardogama)) 63 | - Add .npmignore [#37](https://github.com/seegno/bookshelf-json-columns/pull/37) ([abelsoares](https://github.com/abelsoares)) 64 | - Update dependencies versions [#36](https://github.com/seegno/bookshelf-json-columns/pull/36) ([ricardogama](https://github.com/ricardogama)) 65 | - Update jsonColumns option to be a class property [#35](https://github.com/seegno/bookshelf-json-columns/pull/35) ([ricardogama](https://github.com/ricardogama)) 66 | - Test against Node.js 6 [#33](https://github.com/seegno/bookshelf-json-columns/pull/33) ([MarkHerhold](https://github.com/MarkHerhold)) 67 | - Add release script [#24](https://github.com/seegno/bookshelf-json-columns/pull/24) ([ricardogama](https://github.com/ricardogama)) 68 | 69 | ## [1.2.2](https://github.com/seegno/bookshelf-json-columns/tree/1.2.2) (2016-09-20) 70 | 71 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/1.2.1...1.2.2) 72 | 73 | **Closed issues:** 74 | 75 | - issue when saving a null value [#31](https://github.com/seegno/bookshelf-json-columns/issues/31) 76 | 77 | **Merged pull requests:** 78 | 79 | - Fix stringifying null values with patch option [#32](https://github.com/seegno/bookshelf-json-columns/pull/32) ([ricardogama](https://github.com/ricardogama)) 80 | 81 | ## [1.2.1](https://github.com/seegno/bookshelf-json-columns/tree/1.2.1) (2016-08-29) 82 | 83 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/1.2.0...1.2.1) 84 | 85 | **Closed issues:** 86 | 87 | - `array.includes` is undefined in non-ES2015 environments [#28](https://github.com/seegno/bookshelf-json-columns/issues/28) 88 | 89 | **Merged pull requests:** 90 | 91 | - Update eslint version [#30](https://github.com/seegno/bookshelf-json-columns/pull/30) ([ricardogama](https://github.com/ricardogama)) 92 | - Add babel-plugin-array-includes [#29](https://github.com/seegno/bookshelf-json-columns/pull/29) ([ricardogama](https://github.com/ricardogama)) 93 | 94 | ## [1.2.0](https://github.com/seegno/bookshelf-json-columns/tree/1.2.0) (2016-08-29) 95 | 96 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/1.1.1...1.2.0) 97 | 98 | **Closed issues:** 99 | 100 | - Working for updates? [#25](https://github.com/seegno/bookshelf-json-columns/issues/25) 101 | 102 | **Merged pull requests:** 103 | 104 | - Add support for update with patch option [#27](https://github.com/seegno/bookshelf-json-columns/pull/27) ([ricardogama](https://github.com/ricardogama)) 105 | 106 | ## [1.1.1](https://github.com/seegno/bookshelf-json-columns/tree/1.1.1) (2016-08-23) 107 | 108 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/1.1.0...1.1.1) 109 | 110 | **Closed issues:** 111 | 112 | - Add Bookshelf as peer dependency [#21](https://github.com/seegno/bookshelf-json-columns/issues/21) 113 | - Improve documentation contributing section [#19](https://github.com/seegno/bookshelf-json-columns/issues/19) 114 | 115 | **Merged pull requests:** 116 | 117 | - adding support for sqlite3 [#26](https://github.com/seegno/bookshelf-json-columns/pull/26) ([csepulv](https://github.com/csepulv)) 118 | - Add bookshelf as peer dependency [#23](https://github.com/seegno/bookshelf-json-columns/pull/23) ([ricardogama](https://github.com/ricardogama)) 119 | - Fix typo and files notation on README.md [#22](https://github.com/seegno/bookshelf-json-columns/pull/22) ([ricardogama](https://github.com/ricardogama)) 120 | - Improve contributing section [#20](https://github.com/seegno/bookshelf-json-columns/pull/20) ([abelsoares](https://github.com/abelsoares)) 121 | 122 | ## [1.1.0](https://github.com/seegno/bookshelf-json-columns/tree/1.1.0) (2016-06-07) 123 | 124 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/1.0.1...1.1.0) 125 | 126 | **Closed issues:** 127 | 128 | - What is this plugin for [#15](https://github.com/seegno/bookshelf-json-columns/issues/15) 129 | 130 | **Merged pull requests:** 131 | 132 | - Add master branch to travis image url [#18](https://github.com/seegno/bookshelf-json-columns/pull/18) ([ricardogama](https://github.com/ricardogama)) 133 | - Add JSON columns parsing with SQLite client [#17](https://github.com/seegno/bookshelf-json-columns/pull/17) ([ricardogama](https://github.com/ricardogama)) 134 | - Update coveralls image badge to square [#14](https://github.com/seegno/bookshelf-json-columns/pull/14) ([ruimarinho](https://github.com/ruimarinho)) 135 | 136 | ## [1.0.1](https://github.com/seegno/bookshelf-json-columns/tree/1.0.1) (2016-04-21) 137 | 138 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/1.0.0...1.0.1) 139 | 140 | **Merged pull requests:** 141 | 142 | - Update lint script to only check staged files [#13](https://github.com/seegno/bookshelf-json-columns/pull/13) ([ricardogama](https://github.com/ricardogama)) 143 | - Add isparta options [#12](https://github.com/seegno/bookshelf-json-columns/pull/12) ([ricardogama](https://github.com/ricardogama)) 144 | 145 | ## [1.0.0](https://github.com/seegno/bookshelf-json-columns/tree/1.0.0) (2016-04-21) 146 | 147 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/0.1.0...1.0.0) 148 | 149 | **Closed issues:** 150 | 151 | - Supporting JSON Arrays [#7](https://github.com/seegno/bookshelf-json-columns/issues/7) 152 | - Unable to get plugin working [#6](https://github.com/seegno/bookshelf-json-columns/issues/6) 153 | 154 | **Merged pull requests:** 155 | 156 | - Add initialize method extension note to README.md [#11](https://github.com/seegno/bookshelf-json-columns/pull/11) ([ricardogama](https://github.com/ricardogama)) 157 | - Add coveralls [#10](https://github.com/seegno/bookshelf-json-columns/pull/10) ([ricardogama](https://github.com/ricardogama)) 158 | - Update changelog script [#9](https://github.com/seegno/bookshelf-json-columns/pull/9) ([ricardogama](https://github.com/ricardogama)) 159 | - Update eslint modules versions and configuration [#8](https://github.com/seegno/bookshelf-json-columns/pull/8) ([ricardogama](https://github.com/ricardogama)) 160 | 161 | ## [0.1.0](https://github.com/seegno/bookshelf-json-columns/tree/0.1.0) (2015-12-16) 162 | 163 | [Full Changelog](https://github.com/seegno/bookshelf-json-columns/compare/2146ca3ac6ef42faa30e84bc5029220634187f92...0.1.0) 164 | 165 | **Closed issues:** 166 | 167 | - Travis configuration [#2](https://github.com/seegno/bookshelf-json-columns/issues/2) 168 | 169 | **Merged pull requests:** 170 | 171 | - Fix travis configuration [#5](https://github.com/seegno/bookshelf-json-columns/pull/5) ([ricardogama](https://github.com/ricardogama)) 172 | - Fix `changelog` script [#4](https://github.com/seegno/bookshelf-json-columns/pull/4) ([ricardogama](https://github.com/ricardogama)) 173 | - Add travis configuration file [#3](https://github.com/seegno/bookshelf-json-columns/pull/3) ([ricardogama](https://github.com/ricardogama)) 174 | - Add initial code [#1](https://github.com/seegno/bookshelf-json-columns/pull/1) ([ricardogama](https://github.com/ricardogama)) 175 | 176 | - _This Changelog was automatically generated by [github_changelog_generator](https://github.com/github-changelog-generator/github-changelog-generator)_ 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bookshelf-json-columns 2 | 3 | This [Bookshelf.js](https://github.com/tgriesser/bookshelf) plugin enables you to define which model columns have JSON format, preventing manual hook definition for each model with JSON columns. 4 | 5 | ## Status 6 | 7 | [![npm version][npm-image]][npm-url] ![node version][node-image] [![build status][travis-image]][travis-url] [![coverage status][coveralls-image]][coveralls-url] 8 | 9 | ## Installation 10 | 11 | Install the package via `npm`: 12 | 13 | ```sh 14 | $ npm install --save bookshelf-json-columns 15 | ``` 16 | 17 | ## Usage 18 | 19 | Require and register the **bookshelf-json-columns** plugin: 20 | 21 | ```js 22 | var bookshelf = require('bookshelf')(knex); 23 | var jsonColumns = require('bookshelf-json-columns'); 24 | 25 | bookshelf.plugin(jsonColumns); 26 | ``` 27 | 28 | Define which columns have JSON format with the `jsonColumns` class property: 29 | 30 | ```js 31 | bookshelf.Model.extend({ 32 | tableName: 'foo' 33 | }, { 34 | jsonColumns: ['bar', 'biz'] 35 | }); 36 | ``` 37 | 38 | If you're using ES6 class syntax, define `jsonColumns` as static property: 39 | 40 | ```js 41 | class Model extends bookshelf.Model { 42 | get tableName() { 43 | return 'foo'; 44 | } 45 | 46 | static jsonColumns = ['bar', 'biz']; 47 | } 48 | ``` 49 | 50 | This plugin extends the `initialize` and `save` methods of Bookshelf's `Model`, so if you are also extending or overriding them on your models make sure to call their prototype after your work is done: 51 | 52 | ```js 53 | bookshelf.Model.extend({ 54 | initialize: function() { 55 | // Do some stuff. 56 | store.addModel(this); 57 | 58 | // Call the `initialize` prototype method. 59 | bookshelf.Model.prototype.initialize.apply(this, arguments); 60 | }, 61 | save: function() { 62 | // Do some stuff. 63 | store.validateModel(this); 64 | 65 | // Call the `save` prototype method. 66 | bookshelf.Model.prototype.save.apply(this, arguments); 67 | }, 68 | tableName: 'foo' 69 | }, { 70 | jsonColumns: ['bar', 'biz'] 71 | }); 72 | ``` 73 | 74 | ## Contributing 75 | 76 | Contributions are welcome and greatly appreciated, so feel free to fork this repository and submit pull requests. 77 | 78 | **bookshelf-json-columns** supports PostgreSQL, SQLite3 and MySQL. You can find test suites for all these database engines in the *test* folder. 79 | 80 | ### Setting up 81 | 82 | - Fork and clone the **bookshelf-json-columns** repository. 83 | - Duplicate all *.dist* knexfiles and update them to your needs. 84 | - Make sure all the tests pass: 85 | 86 | ```sh 87 | $ npm test 88 | ``` 89 | 90 | ### Linting 91 | 92 | **bookshelf-json-columns** enforces linting using [ESLint](http://eslint.org/) with the [Seegno-flavored ESLint config](https://github.com/seegno/eslint-config-seegno). We recommend you to install an eslint plugin in your editor of choice, although you can run the linter anytime with: 93 | 94 | ```sh 95 | $ eslint src test 96 | ``` 97 | 98 | ### Pull Request 99 | 100 | Please follow these advices to simplify the pull request workflow: 101 | 102 | - If you add or enhance functionality, an update of *README.md* usage section should be part of the PR. 103 | - If your PR fixes a bug you should include tests that at least fail before your code changes and pass after. 104 | - Keep your branch rebased and fix all conflicts before submitting. 105 | - Make sure Travis build status is ok. 106 | 107 | ## License 108 | 109 | [MIT](https://opensource.org/licenses/MIT) 110 | 111 | [coveralls-image]: https://img.shields.io/coveralls/seegno/bookshelf-json-columns/master.svg?style=flat-square 112 | [coveralls-url]: https://coveralls.io/github/seegno/bookshelf-json-columns?branch=master 113 | [node-image]: https://img.shields.io/node/v/bookshelf-json-columns.svg?style=flat-square 114 | [npm-image]: https://img.shields.io/npm/v/bookshelf-json-columns.svg?style=flat-square 115 | [npm-url]: https://npmjs.org/package/bookshelf-json-columns 116 | [travis-image]: https://img.shields.io/travis/seegno/bookshelf-json-columns/master.svg?style=flat-square 117 | [travis-url]: https://travis-ci.org/seegno/bookshelf-json-columns 118 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 8 | 9 | /** 10 | * Stringify JSON columns. 11 | */ 12 | 13 | function stringify(model, attributes, options) { 14 | // Mark json columns as stringfied. 15 | options.parseJsonColumns = true; 16 | 17 | this.constructor.jsonColumns.forEach(column => { 18 | if (this.attributes[column]) { 19 | this.attributes[column] = JSON.stringify(this.attributes[column]); 20 | } 21 | }); 22 | } 23 | 24 | /** 25 | * Parse JSON columns. 26 | */ 27 | 28 | function parse(model, response) { 29 | let options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {}; 30 | 31 | // Do not parse on `fetched` event after saving. 32 | // eslint-disable-next-line no-underscore-dangle 33 | if (!options.parseJsonColumns && options.query && options.query._method !== 'select') { 34 | return; 35 | } 36 | 37 | this.constructor.jsonColumns.forEach(column => { 38 | const value = this.attributes[column]; 39 | 40 | if (value && typeof value === 'string') { 41 | this.attributes[column] = JSON.parse(value); 42 | } 43 | }); 44 | } 45 | 46 | /** 47 | * Export `bookshelf-json-columns` plugin. 48 | */ 49 | 50 | exports.default = Bookshelf => { 51 | const Model = Bookshelf.Model.prototype; 52 | const client = Bookshelf.knex.client.config.client; 53 | const parseOnFetch = client === 'sqlite' || client === 'sqlite3' || client === 'mysql'; 54 | 55 | Bookshelf.Model = Bookshelf.Model.extend({ 56 | initialize: function initialize() { 57 | if (!this.constructor.jsonColumns) { 58 | return Model.initialize.apply(this, arguments); 59 | } 60 | 61 | // Stringify JSON columns before model is saved. 62 | this.on('saving', stringify.bind(this)); 63 | 64 | // Parse JSON columns after model is saved. 65 | this.on('saved', parse.bind(this)); 66 | 67 | if (parseOnFetch) { 68 | // Parse JSON columns after model is fetched. 69 | this.on('fetched', parse.bind(this)); 70 | } 71 | 72 | return Model.initialize.apply(this, arguments); 73 | }, 74 | save: function save(key, value, options) { 75 | if (!this.constructor.jsonColumns) { 76 | return Model.save.apply(this, arguments); 77 | } 78 | 79 | // Handle arguments as Bookshelf. 80 | let attributes; 81 | 82 | if (key === null || typeof key === 'object') { 83 | attributes = key || {}; 84 | options = value ? _extends({}, value) : {}; 85 | } else { 86 | (attributes = {})[key] = value; 87 | options = options ? _extends({}, options) : {}; 88 | } 89 | 90 | // Only handle arguments with `patch` option. 91 | if (!options.patch) { 92 | return Model.save.apply(this, arguments); 93 | } 94 | 95 | // Stringify JSON columns. 96 | Object.keys(attributes).forEach(attribute => { 97 | if (this.constructor.jsonColumns.indexOf(attribute) !== -1 && attributes[attribute]) { 98 | attributes[attribute] = JSON.stringify(attributes[attribute]); 99 | } 100 | }); 101 | 102 | return Model.save.call(this, attributes, options); 103 | } 104 | }); 105 | 106 | if (!parseOnFetch) { 107 | return; 108 | } 109 | 110 | const Collection = Bookshelf.Collection.prototype; 111 | 112 | Bookshelf.Collection = Bookshelf.Collection.extend({ 113 | initialize: function initialize() { 114 | if (!this.model.jsonColumns) { 115 | return Collection.initialize.apply(this, arguments); 116 | } 117 | 118 | // Parse JSON columns after collection is fetched. 119 | this.on('fetched', collection => { 120 | collection.models.forEach(model => { 121 | parse.apply(model); 122 | }); 123 | }); 124 | 125 | return Collection.initialize.apply(this, arguments); 126 | } 127 | }); 128 | }; 129 | 130 | module.exports = exports['default']; -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.3' 2 | 3 | services: 4 | postgres: 5 | image: postgres 6 | container_name: postgres 7 | healthcheck: 8 | test: ["CMD-SHELL", "pg_isready -U postgres"] 9 | ports: 10 | - 5432:5432 11 | 12 | mysql: 13 | image: mariadb:10.3.13 14 | container_name: mysql 15 | environment: 16 | MYSQL_ALLOW_EMPTY_PASSWORD: 'true' 17 | healthcheck: 18 | test: "/usr/bin/mysql --user=root --execute \"SHOW DATABASES;\"" 19 | ports: 20 | - 3307:3306 21 | 22 | wait: 23 | image: alpine 24 | depends_on: 25 | mysql: 26 | condition: service_healthy 27 | postgres: 28 | condition: service_healthy 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bookshelf-json-columns", 3 | "version": "3.0.0", 4 | "description": "Parse JSON columns with Bookshelf.js", 5 | "license": "MIT", 6 | "author": { 7 | "name": "Ricardo Gama", 8 | "email": "ricardo@seegno.com", 9 | "url": "https://github.com/ricardogama" 10 | }, 11 | "homepage": "https://github.com/seegno/bookshelf-json-columns", 12 | "bugs": { 13 | "url": "https://github.com/seegno/bookshelf-json-columns/issues" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/seegno/bookshelf-json-columns.git" 18 | }, 19 | "main": "./dist/index.js", 20 | "keywords": [ 21 | "bookshelf", 22 | "parse", 23 | "json", 24 | "columns" 25 | ], 26 | "options": { 27 | "mocha": "--compilers js:babel-register --bail test" 28 | }, 29 | "scripts": { 30 | "build": "rm -rf dist/* && ./node_modules/.bin/babel src/ --out-dir dist/", 31 | "changelog": "github_changelog_generator --user seegno --project bookshelf-json-columns --bug-labels --enhancement-labels --future-release=$npm_config_release --header-label='# Changelog'", 32 | "coveralls": "npm run cover && cat ./test/coverage/lcov.info | coveralls", 33 | "cover": "NODE_ENV=test nyc mocha $npm_package_options_mocha", 34 | "lint": "git diff --cached --name-only --diff-filter=ACMRTUXB | grep -E '\\.(js)(\\..+)?$' | xargs eslint", 35 | "release": "npm run changelog && npm run version && npm run build && git add -A && git commit -n -m \"Release $npm_config_release\"", 36 | "test": "mocha $npm_package_options_mocha", 37 | "version": "sed -i '' 's/\\(\"version\": \"\\)'\"$npm_package_version\"'\\(\"\\)/\\1'\"$npm_config_release\"'\\2/' package.json" 38 | }, 39 | "peerDependencies": { 40 | "bookshelf": ">= 1" 41 | }, 42 | "devDependencies": { 43 | "babel-cli": "6.18.0", 44 | "babel-plugin-add-module-exports": "0.2.1", 45 | "babel-plugin-array-includes": "2.0.3", 46 | "babel-plugin-istanbul": "2.0.3", 47 | "babel-preset-es2015-node": "4.0.2", 48 | "babel-preset-stage-3": "6.17.0", 49 | "babel-register": "6.18.0", 50 | "bookshelf": "1.0.1", 51 | "coveralls": "2.11.14", 52 | "eslint": "3.8.1", 53 | "eslint-config-seegno": "8.0.0", 54 | "knex": "0.15.0", 55 | "mocha": "3.1.2", 56 | "mysql": "2.12.0", 57 | "nyc": "8.3.2", 58 | "pg": "6.1.0", 59 | "pre-commit": "1.1.3", 60 | "should": "11.1.1", 61 | "sinon": "1.17.6", 62 | "sqlite3": "3.1.7" 63 | }, 64 | "engines": { 65 | "node": ">= 6" 66 | }, 67 | "pre-commit": [ 68 | "lint" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Stringify JSON columns. 4 | */ 5 | 6 | function stringify(model, attributes, options) { 7 | // Mark json columns as stringfied. 8 | options.parseJsonColumns = true; 9 | 10 | this.constructor.jsonColumns.forEach(column => { 11 | if (this.attributes[column]) { 12 | this.attributes[column] = JSON.stringify(this.attributes[column]); 13 | } 14 | }); 15 | } 16 | 17 | /** 18 | * Parse JSON columns. 19 | */ 20 | 21 | function parse(model, response, options = {}) { 22 | // Do not parse on `fetched` event after saving. 23 | // eslint-disable-next-line no-underscore-dangle 24 | if (!options.parseJsonColumns && options.query && options.query._method !== 'select') { 25 | return; 26 | } 27 | 28 | this.constructor.jsonColumns.forEach(column => { 29 | const value = this.attributes[column]; 30 | 31 | if (value && typeof value === 'string') { 32 | this.attributes[column] = JSON.parse(value); 33 | } 34 | }); 35 | } 36 | 37 | /** 38 | * Export `bookshelf-json-columns` plugin. 39 | */ 40 | 41 | export default Bookshelf => { 42 | const Model = Bookshelf.Model.prototype; 43 | const client = Bookshelf.knex.client.config.client; 44 | const parseOnFetch = client === 'sqlite' || client === 'sqlite3' || client === 'mysql'; 45 | 46 | Bookshelf.Model = Bookshelf.Model.extend({ 47 | initialize() { 48 | if (!this.constructor.jsonColumns) { 49 | return Model.initialize.apply(this, arguments); 50 | } 51 | 52 | // Stringify JSON columns before model is saved. 53 | this.on('saving', stringify.bind(this)); 54 | 55 | // Parse JSON columns after model is saved. 56 | this.on('saved', parse.bind(this)); 57 | 58 | if (parseOnFetch) { 59 | // Parse JSON columns after model is fetched. 60 | this.on('fetched', parse.bind(this)); 61 | } 62 | 63 | return Model.initialize.apply(this, arguments); 64 | }, 65 | save(key, value, options) { 66 | if (!this.constructor.jsonColumns) { 67 | return Model.save.apply(this, arguments); 68 | } 69 | 70 | // Handle arguments as Bookshelf. 71 | let attributes; 72 | 73 | if (key === null || typeof key === 'object') { 74 | attributes = key || {}; 75 | options = value ? { ...value } : {}; 76 | } else { 77 | (attributes = {})[key] = value; 78 | options = options ? { ...options } : {}; 79 | } 80 | 81 | // Only handle arguments with `patch` option. 82 | if (!options.patch) { 83 | return Model.save.apply(this, arguments); 84 | } 85 | 86 | // Stringify JSON columns. 87 | Object.keys(attributes).forEach(attribute => { 88 | if (this.constructor.jsonColumns.includes(attribute) && attributes[attribute]) { 89 | attributes[attribute] = JSON.stringify(attributes[attribute]); 90 | } 91 | }); 92 | 93 | return Model.save.call(this, attributes, options); 94 | } 95 | }); 96 | 97 | if (!parseOnFetch) { 98 | return; 99 | } 100 | 101 | const Collection = Bookshelf.Collection.prototype; 102 | 103 | Bookshelf.Collection = Bookshelf.Collection.extend({ 104 | initialize() { 105 | if (!this.model.jsonColumns) { 106 | return Collection.initialize.apply(this, arguments); 107 | } 108 | 109 | // Parse JSON columns after collection is fetched. 110 | this.on('fetched', collection => { 111 | collection.models.forEach(model => { 112 | parse.apply(model); 113 | }); 114 | }); 115 | 116 | return Collection.initialize.apply(this, arguments); 117 | } 118 | }); 119 | }; 120 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Test suite. 4 | */ 5 | 6 | import './mysql'; 7 | import './postgres'; 8 | import './sqlite'; 9 | -------------------------------------------------------------------------------- /test/mysql/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | import { dropTable, recreateTable } from '../utils'; 7 | import bookshelf from 'bookshelf'; 8 | import jsonColumns from '../../src'; 9 | import knex from 'knex'; 10 | import knexfile from './knexfile'; 11 | import should from 'should'; 12 | import sinon from 'sinon'; 13 | 14 | /** 15 | * Test `bookshelf-json-columns` plugin with MySQL client. 16 | */ 17 | 18 | describe('with MySQL client', () => { 19 | const repository = bookshelf(knex(knexfile)); 20 | const ModelPrototype = repository.Model.prototype; 21 | 22 | repository.plugin(jsonColumns); 23 | 24 | before(async () => { 25 | await recreateTable(repository); 26 | }); 27 | 28 | after(async () => { 29 | await dropTable(repository); 30 | }); 31 | 32 | describe('when a JSON column is not registered', () => { 33 | const Model = repository.Model.extend({ tableName: 'test' }); 34 | 35 | it('should throw an error on create', async () => { 36 | try { 37 | await Model.forge().save({ foo: { bar: 'baz' } }); 38 | 39 | should.fail(); 40 | } catch (e) { 41 | e.should.be.instanceOf(Error); 42 | e.code.should.equal('ER_BAD_FIELD_ERROR'); 43 | } 44 | }); 45 | 46 | it('should throw an error creating through a collection', async () => { 47 | const Collection = repository.Collection.extend({ model: Model }); 48 | const collection = Collection.forge(); 49 | 50 | try { 51 | await collection.create(Model.forge({ foo: { bar: 'baz' } })); 52 | 53 | should.fail(); 54 | } catch (e) { 55 | e.should.be.instanceOf(Error); 56 | e.code.should.equal('ER_BAD_FIELD_ERROR'); 57 | } 58 | }); 59 | 60 | it('should throw an error on update', async () => { 61 | const model = await Model.forge().save(); 62 | 63 | try { 64 | await model.save({ foo: { bar: 'baz' } }); 65 | 66 | should.fail(); 67 | } catch (e) { 68 | e.should.be.instanceOf(Error); 69 | e.code.should.equal('ER_BAD_FIELD_ERROR'); 70 | } 71 | }); 72 | 73 | it('should not override model prototype initialize method', async () => { 74 | sinon.spy(ModelPrototype, 'initialize'); 75 | 76 | Model.forge(); 77 | 78 | ModelPrototype.initialize.callCount.should.equal(1); 79 | 80 | sinon.restore(ModelPrototype); 81 | }); 82 | }); 83 | 84 | describe('when a JSON column is registered', () => { 85 | const Model = repository.Model.extend({ tableName: 'test' }, { jsonColumns: ['foo'] }); 86 | 87 | it('should keep a JSON value on create', async () => { 88 | const model = await Model.forge().save({ foo: { bar: 'baz' } }); 89 | 90 | model.get('foo').should.eql({ bar: 'baz' }); 91 | }); 92 | 93 | it('should keep a JSON value using save with a key and value', async () => { 94 | const model = await Model.forge().save('foo', { bar: 'baz' }, { method: 'insert' }); 95 | 96 | model.get('foo').should.eql({ bar: 'baz' }); 97 | }); 98 | 99 | it('should keep a JSON value when creating through a collection', async () => { 100 | const Collection = repository.Collection.extend({ model: Model }); 101 | const collection = Collection.forge(); 102 | 103 | await collection.create(Model.forge({ foo: { bar: 'baz' } })); 104 | 105 | collection.at(0).get('foo').should.eql({ bar: 'baz' }); 106 | }); 107 | 108 | it('should keep a null value on create', async () => { 109 | const model = await Model.forge().save(); 110 | 111 | should(model.get('foo')).be.null(); 112 | }); 113 | 114 | it('should keep a JSON value on update', async () => { 115 | const model = await Model.forge().save(); 116 | 117 | await model.save({ foo: { bar: 'baz' } }); 118 | 119 | model.get('foo').should.eql({ bar: 'baz' }); 120 | }); 121 | 122 | it('should keep a null value on update', async () => { 123 | const model = await Model.forge().save(); 124 | 125 | await model.save(); 126 | 127 | should(model.get('foo')).be.null(); 128 | }); 129 | 130 | it('should not stringify null values on update with `patch` option', async () => { 131 | sinon.spy(ModelPrototype, 'save'); 132 | 133 | const model = await Model.forge().save(); 134 | 135 | await model.save({ foo: null }, { patch: true }); 136 | 137 | ModelPrototype.save.callCount.should.equal(2); 138 | ModelPrototype.save.secondCall.args[0].should.eql({ foo: null }); 139 | 140 | sinon.restore(ModelPrototype); 141 | }); 142 | 143 | it('should keep an empty string on update with `patch` option', async () => { 144 | sinon.spy(ModelPrototype, 'save'); 145 | 146 | const model = await Model.forge().save(); 147 | 148 | await model.save({ foo: '' }, { patch: true }); 149 | 150 | model.get('foo').should.equal(''); 151 | }); 152 | 153 | it('should keep a JSON value when updating with `patch` option', async () => { 154 | const model = await Model.forge().save(); 155 | 156 | await model.save({ foo: { bar: 'baz' } }, { patch: true }); 157 | 158 | model.get('foo').should.eql({ bar: 'baz' }); 159 | }); 160 | 161 | it('should keep a JSON value when updating other columns', async () => { 162 | const model = await Model.forge().save({ foo: { bar: 'baz' } }); 163 | 164 | await model.save({ qux: 'qix' }, { patch: true }); 165 | 166 | model.get('foo').should.eql({ bar: 'baz' }); 167 | }); 168 | 169 | it('should keep a JSON value on fetch', async () => { 170 | await Model.forge().save({ foo: { bar: 'baz' }, qux: 'qix' }); 171 | 172 | const model = await Model.forge({ qux: 'qix' }).fetch(); 173 | 174 | model.get('foo').should.eql({ bar: 'baz' }); 175 | }); 176 | 177 | it('should keep a JSON value when updating through query', async () => { 178 | const model = await Model.forge().save({ foo: { bar: 'baz' } }); 179 | 180 | model.query().update({ qux: 'qix' }); 181 | 182 | await model.refresh(); 183 | 184 | model.get('foo').should.eql({ bar: 'baz' }); 185 | }); 186 | 187 | it('should not override model initialize method', async () => { 188 | sinon.spy(ModelPrototype, 'initialize'); 189 | 190 | Model.forge(); 191 | 192 | ModelPrototype.initialize.callCount.should.equal(1); 193 | 194 | sinon.restore(ModelPrototype); 195 | }); 196 | }); 197 | }); 198 | -------------------------------------------------------------------------------- /test/mysql/knexfile.js.dist: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Export MySQL knexfile. 4 | */ 5 | 6 | export default { 7 | client: 'mysql', 8 | connection: { 9 | charset: 'utf8', 10 | database: 'bookshelf-json-columns', 11 | host: 'localhost', 12 | port: 3307, 13 | user: 'root' 14 | } 15 | }; 16 | -------------------------------------------------------------------------------- /test/postgres/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | import { dropTable, recreateTable } from '../utils'; 7 | import bookshelf from 'bookshelf'; 8 | import jsonColumns from '../../src'; 9 | import knex from 'knex'; 10 | import knexfile from './knexfile'; 11 | import should from 'should'; 12 | import sinon from 'sinon'; 13 | 14 | /** 15 | * Test `bookshelf-json-columns` plugin with PostgreSQL client. 16 | */ 17 | 18 | describe('with PostgreSQL client', () => { 19 | const repository = bookshelf(knex(knexfile)); 20 | const ModelPrototype = repository.Model.prototype; 21 | 22 | repository.plugin(jsonColumns); 23 | 24 | before(async () => { 25 | await recreateTable(repository); 26 | }); 27 | 28 | after(async () => { 29 | await dropTable(repository); 30 | }); 31 | 32 | describe('when a JSON column is not registered', () => { 33 | const Model = repository.Model.extend({ tableName: 'test' }); 34 | 35 | it('should throw an error on create', async () => { 36 | try { 37 | await Model.forge().save({ foo: ['bar'] }); 38 | 39 | should.fail(); 40 | } catch (e) { 41 | e.should.be.instanceOf(Error); 42 | e.code.should.equal('22P02'); 43 | e.routine.should.equal('report_parse_error'); 44 | } 45 | }); 46 | 47 | it('should throw an error creating through a collection', async () => { 48 | const Collection = repository.Collection.extend({ model: Model }); 49 | const collection = Collection.forge(); 50 | 51 | try { 52 | await collection.create(Model.forge({ foo: ['bar'] })); 53 | 54 | should.fail(); 55 | } catch (e) { 56 | e.should.be.instanceOf(Error); 57 | e.code.should.equal('22P02'); 58 | e.routine.should.equal('report_parse_error'); 59 | } 60 | }); 61 | 62 | it('should throw an error on update', async () => { 63 | const model = await Model.forge().save(); 64 | 65 | try { 66 | await model.save({ foo: ['bar'] }); 67 | 68 | should.fail(); 69 | } catch (e) { 70 | e.should.be.instanceOf(Error); 71 | e.code.should.equal('22P02'); 72 | e.routine.should.equal('report_parse_error'); 73 | } 74 | }); 75 | 76 | it('should not override model prototype initialize method', async () => { 77 | sinon.spy(ModelPrototype, 'initialize'); 78 | 79 | Model.forge(); 80 | 81 | ModelPrototype.initialize.callCount.should.equal(1); 82 | 83 | sinon.restore(ModelPrototype); 84 | }); 85 | }); 86 | 87 | describe('when a JSON column is registered', () => { 88 | const Model = repository.Model.extend({ tableName: 'test' }, { jsonColumns: ['foo'] }); 89 | 90 | it('should keep the JSON value on create', async () => { 91 | const model = await Model.forge().save({ foo: ['bar'] }); 92 | 93 | model.get('foo').should.eql(['bar']); 94 | }); 95 | 96 | it('should keep the JSON value using save with a key and value', async () => { 97 | const model = await Model.forge().save('foo', ['bar'], { method: 'insert' }); 98 | 99 | model.get('foo').should.eql(['bar']); 100 | }); 101 | 102 | it('should keep a JSON value when creating through a collection', async () => { 103 | const Collection = repository.Collection.extend({ model: Model }); 104 | const collection = Collection.forge(); 105 | 106 | await collection.create(Model.forge({ foo: ['bar'] })); 107 | 108 | collection.at(0).get('foo').should.eql(['bar']); 109 | }); 110 | 111 | it('should keep a null value on create', async () => { 112 | const model = await Model.forge().save(); 113 | 114 | should(model.get('foo')).be.null(); 115 | }); 116 | 117 | it('should keep a JSON value on update', async () => { 118 | const model = await Model.forge().save(); 119 | 120 | await model.save({ foo: ['bar'] }); 121 | 122 | model.get('foo').should.eql(['bar']); 123 | }); 124 | 125 | it('should keep a null value on update', async () => { 126 | const model = await Model.forge().save(); 127 | 128 | await model.save(); 129 | 130 | should(model.get('foo')).be.null(); 131 | }); 132 | 133 | it('should not stringify null values on update with `patch` option', async () => { 134 | sinon.spy(ModelPrototype, 'save'); 135 | 136 | const model = await Model.forge().save(); 137 | 138 | await model.save({ foo: null }, { patch: true }); 139 | 140 | ModelPrototype.save.callCount.should.equal(2); 141 | ModelPrototype.save.secondCall.args[0].should.eql({ foo: null }); 142 | 143 | sinon.restore(ModelPrototype); 144 | }); 145 | 146 | it('should keep a JSON value when updating with `patch` option', async () => { 147 | const model = await Model.forge().save(); 148 | 149 | await model.save({ foo: ['bar'] }, { patch: true }); 150 | 151 | model.get('foo').should.eql(['bar']); 152 | }); 153 | 154 | it('should keep a JSON value when updating other columns', async () => { 155 | const model = await Model.forge().save({ foo: ['bar'] }); 156 | 157 | await model.save({ qux: 'qix' }, { patch: true }); 158 | 159 | model.get('foo').should.eql(['bar']); 160 | }); 161 | 162 | it('should keep a JSON value on fetch', async () => { 163 | await Model.forge().save({ foo: ['bar'], qux: 'qix' }); 164 | 165 | const model = await Model.forge({ qux: 'qix' }).fetch(); 166 | 167 | model.get('foo').should.eql(['bar']); 168 | }); 169 | 170 | it('should keep a JSON value when updating through query', async () => { 171 | const model = await Model.forge().save({ foo: ['bar'] }); 172 | 173 | model.query().update({ qux: 'qix' }); 174 | 175 | await model.refresh(); 176 | 177 | model.get('foo').should.eql(['bar']); 178 | }); 179 | 180 | it('should not override model initialize method', async () => { 181 | sinon.spy(ModelPrototype, 'initialize'); 182 | 183 | Model.forge(); 184 | 185 | ModelPrototype.initialize.callCount.should.equal(1); 186 | 187 | sinon.restore(ModelPrototype); 188 | }); 189 | }); 190 | }); 191 | -------------------------------------------------------------------------------- /test/postgres/knexfile.js.dist: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Export PostgreSQL knexfile. 4 | */ 5 | 6 | export default { 7 | client: 'postgres', 8 | connection: { 9 | charset: 'utf8', 10 | database: 'bookshelf-json-columns', 11 | host: 'localhost', 12 | user: 'postgres' 13 | } 14 | }; 15 | -------------------------------------------------------------------------------- /test/sqlite/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | import { clearTable, dropTable, recreateTable } from '../utils'; 7 | import bookshelf from 'bookshelf'; 8 | import jsonColumns from '../../src'; 9 | import knex from 'knex'; 10 | import knexfile from './knexfile'; 11 | import should from 'should'; 12 | import sinon from 'sinon'; 13 | 14 | /** 15 | * Test `bookshelf-json-columns` plugin with SQLite client. 16 | */ 17 | 18 | describe('with SQLite client', () => { 19 | const repository = bookshelf(knex(knexfile)); 20 | const CollectionPrototype = repository.Collection.prototype; 21 | const ModelPrototype = repository.Model.prototype; 22 | 23 | repository.plugin(jsonColumns); 24 | 25 | before(async () => { 26 | await recreateTable(repository); 27 | }); 28 | 29 | afterEach(async () => { 30 | await clearTable(repository); 31 | }); 32 | 33 | after(async () => { 34 | await dropTable(repository); 35 | }); 36 | 37 | describe('when a JSON column is not registered', () => { 38 | const Model = repository.Model.extend({ tableName: 'test' }); 39 | 40 | it('should not save a valid JSON value on create', async () => { 41 | const model = await Model.forge().save({ foo: ['bar'] }); 42 | const fetched = await Model.forge({ id: model.get('id') }).fetch(); 43 | 44 | should(fetched.get('foo')).be.null(); 45 | }); 46 | 47 | it('should not save a valid JSON value creating through a collection', async () => { 48 | const Collection = repository.Collection.extend({ model: Model }); 49 | const collection = Collection.forge(); 50 | 51 | await collection.create(Model.forge({ foo: ['bar'] })); 52 | 53 | const fetched = await Collection.forge().fetch(); 54 | 55 | should(fetched.at(0).get('foo')).be.null(); 56 | }); 57 | 58 | it('should not save a valid JSON value on update', async () => { 59 | const model = await Model.forge().save(); 60 | 61 | await model.save({ foo: ['bar'] }); 62 | 63 | const fetched = await Model.forge({ id: model.get('id') }).fetch(); 64 | 65 | should(fetched.get('foo')).be.null(); 66 | }); 67 | 68 | it('should not parse a JSON value on fetch', async () => { 69 | const model = await Model.forge().save({ foo: JSON.stringify(['bar']) }); 70 | 71 | await model.refresh(); 72 | 73 | model.get('foo').should.equal(JSON.stringify(['bar'])); 74 | }); 75 | 76 | it('should not override model prototype initialize method', async () => { 77 | sinon.spy(ModelPrototype, 'initialize'); 78 | 79 | Model.forge(); 80 | 81 | ModelPrototype.initialize.callCount.should.equal(1); 82 | 83 | sinon.restore(ModelPrototype); 84 | }); 85 | 86 | it('should not override collection prototype initialize method', async () => { 87 | const Collection = repository.Collection.extend({ model: Model }); 88 | 89 | sinon.spy(CollectionPrototype, 'initialize'); 90 | 91 | Collection.forge({}); 92 | 93 | CollectionPrototype.initialize.callCount.should.equal(1); 94 | 95 | sinon.restore(CollectionPrototype); 96 | }); 97 | }); 98 | 99 | describe('when a JSON column is registered', () => { 100 | const Model = repository.Model.extend({ tableName: 'test' }, { jsonColumns: ['foo'] }); 101 | 102 | it('should keep a JSON value on create', async () => { 103 | const model = await Model.forge().save({ foo: ['bar'] }); 104 | const fetched = await Model.forge({ id: model.get('id') }).fetch(); 105 | 106 | fetched.get('foo').should.eql(['bar']); 107 | }); 108 | 109 | it('should keep the JSON value using save with a key and value', async () => { 110 | const model = await Model.forge().save('foo', ['bar'], { method: 'insert' }); 111 | 112 | model.get('foo').should.eql(['bar']); 113 | }); 114 | 115 | it('should keep a JSON value when creating through a collection', async () => { 116 | const Collection = repository.Collection.extend({ model: Model }); 117 | const collection = Collection.forge(); 118 | 119 | await collection.create(Model.forge({ foo: ['bar'] })); 120 | 121 | const fetched = await Collection.forge().fetch(); 122 | 123 | fetched.at(0).get('foo').should.eql(['bar']); 124 | }); 125 | 126 | it('should keep a null value on create', async () => { 127 | const model = await Model.forge().save(); 128 | const fetched = await Model.forge({ id: model.get('id') }).fetch(); 129 | 130 | should(fetched.get('foo')).be.null(); 131 | }); 132 | 133 | it('should keep a JSON value on update', async () => { 134 | const model = await Model.forge().save(); 135 | 136 | await model.save({ foo: ['bar'] }); 137 | 138 | const fetched = await Model.forge({ id: model.get('id') }).fetch(); 139 | 140 | fetched.get('foo').should.eql(['bar']); 141 | }); 142 | 143 | it('should keep a null value on update', async () => { 144 | const model = await Model.forge().save(); 145 | 146 | await model.save(); 147 | 148 | const fetched = await Model.forge({ id: model.get('id') }).fetch(); 149 | 150 | should(fetched.get('foo')).be.null(); 151 | }); 152 | 153 | it('should not stringify null values on update with `patch` option', async () => { 154 | sinon.spy(ModelPrototype, 'save'); 155 | 156 | const model = await Model.forge().save(); 157 | 158 | await model.save({ foo: null }, { patch: true }); 159 | 160 | ModelPrototype.save.callCount.should.equal(2); 161 | ModelPrototype.save.secondCall.args[0].should.eql({ foo: null }); 162 | 163 | sinon.restore(ModelPrototype); 164 | }); 165 | 166 | it('should keep an empty string on update with `patch` option', async () => { 167 | sinon.spy(ModelPrototype, 'save'); 168 | 169 | const model = await Model.forge().save(); 170 | 171 | await model.save({ foo: '' }, { patch: true }); 172 | 173 | model.get('foo').should.equal(''); 174 | }); 175 | 176 | it('should keep a JSON value when updating with `patch` option', async () => { 177 | const model = await Model.forge().save(); 178 | 179 | await model.save({ foo: ['bar'] }, { patch: true }); 180 | 181 | model.get('foo').should.eql(['bar']); 182 | }); 183 | 184 | it('should keep a JSON value when updating other columns', async () => { 185 | const model = await Model.forge().save({ foo: ['bar'] }); 186 | 187 | await model.save({ qux: 'qix' }, { patch: true }); 188 | 189 | const fetched = await Model.forge({ id: model.get('id') }).fetch(); 190 | 191 | fetched.get('foo').should.eql(['bar']); 192 | }); 193 | 194 | it('should keep a JSON value on fetch', async () => { 195 | const model = await Model.forge().save({ foo: ['bar'] }); 196 | 197 | await model.refresh(); 198 | 199 | model.get('foo').should.eql(['bar']); 200 | }); 201 | 202 | it('should keep a JSON value when updating through query', async () => { 203 | const model = await Model.forge().save({ foo: ['bar'] }); 204 | 205 | model.query().update({ qux: 'qix' }); 206 | 207 | await model.refresh(); 208 | 209 | model.get('foo').should.eql(['bar']); 210 | }); 211 | 212 | it('should not override model prototype initialize method', async () => { 213 | sinon.spy(ModelPrototype, 'initialize'); 214 | 215 | Model.forge(); 216 | 217 | ModelPrototype.initialize.callCount.should.equal(1); 218 | 219 | sinon.restore(ModelPrototype); 220 | }); 221 | 222 | it('should not override collection prototype initialize method', async () => { 223 | const Collection = repository.Collection.extend({ model: Model }); 224 | 225 | sinon.spy(CollectionPrototype, 'initialize'); 226 | 227 | Collection.forge(); 228 | 229 | CollectionPrototype.initialize.callCount.should.equal(1); 230 | 231 | sinon.restore(CollectionPrototype); 232 | }); 233 | }); 234 | }); 235 | -------------------------------------------------------------------------------- /test/sqlite/knexfile.js.dist: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Export SQLite knexfile. 4 | */ 5 | 6 | export default { 7 | client: 'sqlite', 8 | connection: { 9 | filename: './test.sqlite3' 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /test/utils/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Export `clearTable`. 4 | */ 5 | 6 | export function clearTable(repository) { 7 | return repository.knex('test').del(); 8 | } 9 | 10 | /** 11 | * Export `dropTable`. 12 | */ 13 | 14 | export function dropTable(repository) { 15 | return repository.knex.schema.dropTable('test'); 16 | } 17 | 18 | /** 19 | * Export `recreateTable`. 20 | */ 21 | 22 | export function recreateTable(repository) { 23 | return repository.knex.schema 24 | .dropTableIfExists('test') 25 | .createTable('test', table => { 26 | table.increments('id').primary(); 27 | table.json('foo'); 28 | table.string('qux'); 29 | }); 30 | } 31 | --------------------------------------------------------------------------------