├── .editorconfig ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── spec ├── bookshelf-spec.ts └── support │ └── jasmine.json ├── src ├── bookshelf │ ├── extras.ts │ ├── index.ts │ ├── links.ts │ └── utils.ts ├── index.ts ├── interfaces │ ├── common.ts │ ├── index.ts │ ├── links.ts │ └── relations.ts └── serializer │ ├── index.ts │ └── jsonapi-serializer.skel.d.ts ├── tsconfig.base.json ├── tsconfig.publish.json ├── tsconfig.test.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig: http://editorconfig.org 2 | 3 | # Top-most EditorConfig file 4 | root = true 5 | 6 | # For all files 7 | [*] 8 | charset = utf-8 9 | # Unix-style newlines 10 | end_of_line = lf 11 | # Whitespace handling 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | # TypeScript 16 | [*.ts] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | # JSON 21 | [*.json] 22 | indent_style = space 23 | indent_size = 2 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Build files 2 | es5 3 | 4 | # Typescript Definitions 5 | typings 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | 12 | # node modules 13 | node_modules/ 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "5.0" 5 | 6 | # Libraries install and database migration 7 | install: 8 | - npm install 9 | 10 | script: npm test 11 | 12 | # Branches white list 13 | branches: 14 | - only: 15 | - master 16 | - develop 17 | - testing 18 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to JSONAPI Mapper 2 | 3 | We would love for you to contribute and help make the Mapper a better tool for your JSONAPI needs. You can [open an issue](https://github.com/scoutforpets/jsonapi-mapper/issues/new) to make questions and requests, also find us on [gitter](https://gitter.im/scoutforpets/jsonapi-mapper) to chat with us (we don't bite!). 4 | 5 | If you wan't to hack in the code and get your hands dirty keep reading... 6 | 7 | ## Setup for development 8 | 9 | As easy as it gets: 10 | 11 | * Have `git`, `node`, and `npm` ready 12 | * Clone this repo: `git clone git@github.com:scoutforpets/jsonapi-mapper` 13 | * Install dependencies: `npm install`. This will also run the `prepublish` script running TypeScript-specific tasks 14 | * Profit! 15 | 16 | ## Running tests 17 | 18 | Just run `npm test`. The tests are also in TypeScript made under the Jasmine framework. 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JSON API Mapper 2 | [![Build Status](https://travis-ci.org/scoutforpets/jsonapi-mapper.svg?branch=master)](https://travis-ci.org/scoutforpets/jsonapi-mapper) 3 | [![npm version](https://badge.fury.io/js/jsonapi-mapper.svg)](https://badge.fury.io/js/jsonapi-mapper) 4 | [![david dm](https://david-dm.org/scoutforpets/jsonapi-mapper.svg)](https://david-dm.org/scoutforpets/jsonapi-mapper) 5 | 6 | JSON API Mapper (_formerly Oh My JSON API_) is a wrapper around [@Seyz](https://github.com/SeyZ/)'s excellent [JSON API](http://jsonapi.org/)-compliant serializer, [jsonapi-serializer](https://github.com/SeyZ/jsonapi-serializer), that removes the pain of generating the necessary options needed to serialize each of your ORM models. 7 | 8 | [![Join the chat at https://gitter.im/scoutforpets/oh-my-jsonapi](https://badges.gitter.im/scoutforpets/jsonapi-mapper.svg)](https://gitter.im/scoutforpets/jsonapi-mapper?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 9 | 10 | ## _Breaking Changes_ 11 | This project has recently been renamed and rewritten using Typescript. While most functionality is generally the same, there have been a few deprecations and changes to how the Mapper is initialized. Please see the [migration guide](#migrating-from-ohmyjsonapi) below. 12 | 13 | ## How does it work? 14 | A serializer requires some sort of 'template' to understand how to convert what you're passing in to whatever you want to come out. When you're dealing with an ORM, such as [Bookshelf](https://github.com/tgriesser/bookshelf), it would be a real pain to have to generate the 'template' for every one of your Bookshelf models in order to convert them to JSON API. JSON API Mapper handles this by dynamically analyzing your models and automatically generating the necessary 'template' to pass to the serializer. 15 | 16 | ## What ORMs do you support? 17 | Initially, we only provide a mapper for [Bookshelf](https://github.com/tgriesser/bookshelf). However, the library can be easily extended with new mappers to support other ORMs. PR's are more than welcome! 18 | 19 | ## How do I install it? 20 | `npm install jsonapi-mapper --save` 21 | 22 | ## How do I use it? 23 | It's pretty simple. You only need to configure the mapper and then use it as many times you need. 24 | 25 | ```javascript 26 | import Mapper = require('jsonapi-mapper'); 27 | 28 | // Create the mapper 29 | var mapper = new Mapper.Bookshelf('https://api.hotapp.com'); 30 | 31 | // Use the mapper to output JSON API-compliant using your ORM-provided data 32 | return mapper.map(myData, 'appointment'); 33 | ``` 34 | 35 | ## How can I contribute? 36 | God bless you if you're reading this! Check out our [Contributing page](https://github.com/scoutforpets/jsonapi-mapper/blob/master/CONTRIBUTING.md) 37 | 38 | ## Migrating from OhMyJSONAPI 39 | The migration process is painless: 40 | - Remove `oh-my-jsonapi` from your project. 41 | - Run `npm install jsonapi-mapper --save` 42 | - Convert any instances of the constructor: 43 | 44 | ```javascript 45 | new OhMyJSONAPI('bookshelf', 'https://api.hotapp.com'); 46 | ``` 47 | 48 | to 49 | 50 | ```javascript 51 | new Mapper.Bookshelf('https://api.hotapp.com'); 52 | ``` 53 | - Convert any instances of: 54 | 55 | ```javascript 56 | jsonApi.toJSONAPI(myData, 'appointment'); 57 | ``` 58 | 59 | to 60 | 61 | ```javascript 62 | mapper.map(myData, 'appointment'); 63 | ``` 64 | 65 | ## API 66 | ```javascript 67 | new Mapper.Bookshelf(baseUrl, serializerOptions) 68 | ``` 69 | - _(optional)_ `baseUrl` _(string)_: the base URL to be used in all `links` objects returned. 70 | - _(optional)_ `serializerOptions` _(string)_: options to be passed to the serializer. These options will override any options generated by the mapper. For more information on the raw serializer, please [see the documentation here](https://github.com/SeyZ/jsonapi-serializer#documentation). 71 | 72 | ```javascript 73 | mapper#map(data, type, mapperOptions) 74 | ``` 75 | - `data` _(object)_: The data object from Bookshelf (either a Model or a Collection) to be serialized. 76 | - `type` _(string)_: The type of the resource being returned. For example, if you passed in an `Appointment` model, your `type` might be `appointment`. 77 | - _(optional)_ `mapperOptions` _(object)_: 78 | - _(optional)_ `attributes` (object): 79 | - _(optional)_ `omit` _(RegExp | string)[]_: List of model attributes to omit from the resulting payload. For example, you may wish to exclude any foreign keys (as recommended by the JSON API-spec). Note: the model's `idAttribute` is automatically excluded by default. 80 | - _(optional)_ `include` _(RegExp | string)[]_: List of model attributes to explicitly include from the resulting payload. 81 | - _(optional)_ `keyForAttr` _(function (string => string))_: Function to customize the attributes keys. The function is passed as input the attribute key (`string`) and output the new attribute key (`string`). 82 | - _(optional)_ `relations` _(boolean | object)_: Flag to enable (`true`) or disable (`false`) serializing of related models on the response. Alternatively, you can provide an object containing the following options: 83 | - `included` _(boolean | string[])_ (default: `true`) - includes data for all relations in the response. You may optionally specify an array containing the names of specific relations to be included. 84 | - `fields` _string[]_ - an array of relation names that should be included in the response. 85 | - _(optional)_ `typeForModel` _(object | function)_: To customize the type of a relation. If the value returned is _falsy_, then it's automatically pluralized. This function **also affects** the type passed as the second parameter of the `map` function. 86 | - _object_: The object should have the structure `{"": ""}` (e.g. `{"best-friend": "people"}`). 87 | - _function (string => string)_: The function is passed as input the relation name (`string`) and output the type for that relation (`string`) (e.g. `(x) => x + '-resources'`). 88 | - _(optional)_ `enableLinks` _(boolean)_: Flag to enable (`true`) or disable (`false`) the generation of links in the payload. This may be useful if the consuming system doesn't take advantage of links and you want to save on payload size and maybe a bit of performance. Defaults to `true`. 89 | - _(optional)_ `query` _(object)_: An object containing the original query parameters. These will be appended to `self` and pagination links. Developer Note: This is not fully implemented yet, but following releases will fix that. 90 | - _(optional)_ `pagination` _(object)_: Pagination-related parameters for building pagination links for collections. 91 | - _(required)_ `offset` _(integer)_ 92 | - _(required)_ `limit` _(integer)_ 93 | - _(optional)_ `total` or `rowCount` _(integer)_ 94 | - _(optional)_ `meta` _(object)_: Top-level meta object to include in the document 95 | - _(optional)_ `outputVirtuals` _(boolean)_: Flag to include/omit the virtuals if the `virtuals` plugin is used, overrides the default `outputVirtuals` value. 96 | 97 | # How can I contribute? 98 | The project is very open to collaboration from the public, especially on providing the groundwork for other ORM's (like [Sequelize](http://docs.sequelizejs.com/) or [Mongoose](http://mongoosejs.com/)). Just open a PR! 99 | 100 | The project source has been recently rewritten using [Typescript](http://www.typescriptlang.org/), which has been proven useful for static checks and overall development. 101 | 102 | # Credits 103 | - Thanks to [@Seyz](https://github.com/SeyZ/). Without his work, the project would not be possible. 104 | - Thanks to the [zomoz team](https://github.com/zomoz) (especially [@ShadowManu](https://github.com/shadowmanu)) for their hard work in migrating the codebase to Typescript and writing a comprehensive test suite. 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jsonapi-mapper", 3 | "version": "1.0.0-beta.16", 4 | "description": "JSON API-Compliant Serialization for your ORM", 5 | "main": "es5/index.js", 6 | "types": "es5/index.d.ts", 7 | "scripts": { 8 | "prebuild": "rm -rf es5 || true", 9 | "build": "tsc -p tsconfig.publish.json", 10 | "prepublish": "npm run build", 11 | "pretest": "npm run build -- -p tsconfig.test.json", 12 | "test": "jasmine" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/scoutforpets/jsonapi-mapper" 17 | }, 18 | "keywords": [ 19 | "json", 20 | "api", 21 | "json-api", 22 | "jsonapi", 23 | "orm", 24 | "mapper", 25 | "bookshelf", 26 | "serializer", 27 | "serialization" 28 | ], 29 | "author": "James Dixon ", 30 | "contributors": [ 31 | { 32 | "name": "Manuel Pacheco", 33 | "email": "manuelalejandropm@gmail.com" 34 | }, 35 | { 36 | "name": "Matteo Ferrando", 37 | "email": "matteo.ferrando2@gmail.com" 38 | } 39 | ], 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/scoutforpets/jsonapi-mapper/issues" 43 | }, 44 | "homepage": "https://github.com/scoutforpets/jsonapi-mapper", 45 | "files": [ 46 | "es5" 47 | ], 48 | "dependencies": { 49 | "@types/bookshelf": "0.9.7", 50 | "inflection": "1.12.0", 51 | "jsonapi-serializer": "3.5.6", 52 | "lodash": "4.17.4", 53 | "qs": "6.5.1", 54 | "type-check": "0.3.2" 55 | }, 56 | "devDependencies": { 57 | "@types/inflection": "1.5.28", 58 | "@types/jasmine": "2.8.4", 59 | "@types/knex": "0.0.61", 60 | "@types/lodash": "4.14.93", 61 | "@types/qs": "6.5.1", 62 | "bookshelf": "0.10.4", 63 | "jasmine": "2.9.0", 64 | "knex": "0.13.0", 65 | "typescript": "2.6.2" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /spec/bookshelf-spec.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import * as _ from 'lodash'; 4 | import * as bs from 'bookshelf'; 5 | import * as knex from 'knex'; 6 | 7 | import * as Mapper from '../src'; 8 | 9 | type Model = bs.Model; 10 | type Collection = bs.Collection; 11 | 12 | describe('Bookshelf Adapter', () => { 13 | let bookshelf: bs; 14 | let mapper: Mapper.Bookshelf; 15 | let domain: string = 'https://domain.com'; 16 | 17 | beforeAll(() => { 18 | bookshelf = bs(knex(({ client: 'sqlite3', useNullAsDefault: true } as knex.Config))); 19 | mapper = new Mapper.Bookshelf(domain); 20 | }); 21 | 22 | afterAll((done: Function) => { 23 | bookshelf.knex.destroy(done); 24 | }); 25 | 26 | it('should serialize a basic model', () => { 27 | let model: Model = bookshelf.Model.forge({ 28 | id: '5', 29 | name: 'A test model', 30 | description: 'something to use as a test' 31 | }); 32 | 33 | let result: any = mapper.map(model, 'models'); 34 | 35 | let expected: any = { 36 | data: { 37 | id: '5', 38 | type: 'models', 39 | attributes: { 40 | name: 'A test model', 41 | description: 'something to use as a test' 42 | } 43 | } 44 | }; 45 | 46 | expect(_.matches(expected)(result)).toBe(true); 47 | }); 48 | 49 | it('should serialize a basic model with custom id attribute', () => { 50 | let customModel: any = bookshelf.Model.extend({ 51 | idAttribute : 'email' 52 | }); 53 | 54 | let model: any = customModel.forge({ 55 | email : 'foo@example.com', 56 | name: 'A test model', 57 | description: 'something to use as a test' 58 | }); 59 | 60 | let result: any = mapper.map(model, 'models'); 61 | 62 | let expected: any = { 63 | data: { 64 | id : 'foo@example.com', 65 | type: 'models', 66 | attributes: { 67 | name: 'A test model', 68 | description: 'something to use as a test' 69 | } 70 | } 71 | }; 72 | 73 | expect(_.matches(expected)(result)).toBe(true); 74 | }); 75 | 76 | it('should serialize a basic model with custom array of id attributes', () => { 77 | let customModel: any = bookshelf.Model.extend({ 78 | idAttribute : ['email', 'name'] 79 | }); 80 | 81 | let model: any = customModel.forge({ 82 | email : 'foo@example.com', 83 | name: 'A test model', 84 | description: 'something to use as a test' 85 | }); 86 | 87 | let result: any = mapper.map(model, 'models'); 88 | 89 | let expected: any = { 90 | data: { 91 | id: 'foo@example.com,A test model', 92 | type: 'models', 93 | attributes: { 94 | description: 'something to use as a test' 95 | } 96 | } 97 | }; 98 | expect(_.matches(expected)(result)).toBe(true); 99 | }); 100 | 101 | it('should serialize related model with custom id attribute in relationships object', () => { 102 | 103 | let customModel: any = bookshelf.Model.extend({ 104 | idAttribute : 'email' 105 | }); 106 | 107 | let model: any = bookshelf.Model.forge({ 108 | id : 5, 109 | name: 'A test model', 110 | description: 'something to use as a test' 111 | }); 112 | 113 | (model as any).relations['related-model'] = customModel.forge({ 114 | email: 'foo@example.com', 115 | attr2: 'value2' 116 | }); 117 | 118 | let result: any = mapper.map(model, 'models'); 119 | 120 | let expected: any = { 121 | data: { 122 | relationships: { 123 | 'related-model': { 124 | data: { 125 | id: 'foo@example.com', 126 | type: 'related-models' // TODO check correct casing 127 | }, 128 | links: { 129 | self: domain + '/models/' + '5' + '/relationships/' + 'related-model', 130 | related: domain + '/models/' + '5' + '/related-model' 131 | } 132 | } 133 | } 134 | } 135 | }; 136 | 137 | expect(_.matches(expected)(result)).toBe(true); 138 | }); 139 | 140 | it('should include a repeated model only once in the included array', () => { 141 | let model: any = bookshelf.Model.forge({ 142 | id : 5, 143 | name: 'A test model', 144 | description: 'something to use as a test' 145 | }); 146 | 147 | let related: any = bookshelf.Model.forge({ 148 | id: 4, 149 | attr: 'first value' 150 | }); 151 | 152 | (model as any).relations.relateds = bookshelf.Collection.forge([related]); 153 | (model as any).relations.related = related; 154 | 155 | let result: any = mapper.map(model, 'models'); 156 | 157 | let expected: any = { 158 | data: { 159 | relationships: { 160 | 'related': { 161 | data: { 162 | id: '4', 163 | type: 'relateds' 164 | } 165 | }, 166 | 'relateds': { 167 | data: [ 168 | { 169 | id: '4', 170 | type: 'relateds' 171 | } 172 | ] 173 | } 174 | } 175 | }, 176 | included: [ 177 | { 178 | id: '4', 179 | type: 'relateds', 180 | attributes: { 181 | attr: 'first value' 182 | } 183 | } 184 | ] 185 | }; 186 | 187 | expect(_.matches(expected)(result)).toBe(true); 188 | expect(result.included.length).toBe(1); 189 | }); 190 | 191 | it('should serialize related model with custom id attribute in included array', () => { 192 | 193 | let customModel: any = bookshelf.Model.extend({ 194 | idAttribute : 'email' 195 | }); 196 | 197 | let model: any = bookshelf.Model.forge({ 198 | id : 5, 199 | name: 'A test model', 200 | description: 'something to use as a test' 201 | }); 202 | 203 | (model as any).relations['related-model'] = customModel.forge({ 204 | email: 'foo@example.com', 205 | attr2: 'value2' 206 | }); 207 | 208 | let result: any = mapper.map(model, 'models'); 209 | 210 | let expected: any = { 211 | included: [ 212 | { 213 | id: 'foo@example.com', 214 | type: 'related-models', 215 | attributes: { 216 | attr2: 'value2' 217 | } 218 | } 219 | ] 220 | }; 221 | 222 | expect(_.matches(expected)(result)).toBe(true); 223 | }); 224 | 225 | it('should serialize a collection with custom id attribute', () => { 226 | let customModel: any = bookshelf.Model.extend({ 227 | idAttribute : 'email' 228 | }); 229 | 230 | let model1: any = customModel.forge({ 231 | email : 'foo@example.com', 232 | name: 'A test model1', 233 | description: 'something to use as a test' 234 | }); 235 | 236 | let collection: Collection = bookshelf.Collection.forge([model1]); 237 | 238 | let result: any = mapper.map(collection, 'models'); 239 | 240 | let expected: any = { 241 | data: [ 242 | { 243 | id : 'foo@example.com', 244 | type: 'models', 245 | attributes: { 246 | name: 'A test model1', 247 | description: 'something to use as a test' 248 | } 249 | } 250 | ] 251 | }; 252 | 253 | expect(_.matches(expected)(result)).toBe(true); 254 | }); 255 | 256 | it('should serialize a collection with custom id attribute within a related model on relationships object', () => { 257 | let customModel: any = bookshelf.Model.extend({ 258 | idAttribute : 'email' 259 | }); 260 | 261 | let model: any = bookshelf.Model.forge({ 262 | id : 5, 263 | name: 'A test model', 264 | description: 'something to use as a test' 265 | }); 266 | 267 | (model as any).relations['related-model'] = customModel.forge({ 268 | email: 'foo@example.com', 269 | attr2: 'value2' 270 | }); 271 | 272 | let collection: Collection = bookshelf.Collection.forge([model]); 273 | 274 | let result: any = mapper.map(collection, 'models'); 275 | 276 | let expected: any = { 277 | data: [ 278 | { 279 | type: 'models', 280 | id : '5', 281 | attributes: { 282 | name: 'A test model', 283 | description: 'something to use as a test' 284 | }, 285 | links : { self : domain + '/models/5' }, 286 | relationships : { 287 | 'related-model' : { 288 | data: { id : 'foo@example.com', type : 'related-models' }, 289 | links : { 290 | self : domain + '/models/5/relationships/related-model', 291 | related : domain + '/models/5/related-model' 292 | } 293 | } 294 | } 295 | } 296 | ] 297 | }; 298 | 299 | expect(_.matches(expected)(result)).toBe(true); 300 | }); 301 | 302 | it('should serialize a collection with custom id attribute within a related model on included array', () => { 303 | let customModel: any = bookshelf.Model.extend({ 304 | idAttribute : 'email' 305 | }); 306 | 307 | let model: any = bookshelf.Model.forge({ 308 | id : 5, 309 | name: 'A test model', 310 | description: 'something to use as a test' 311 | }); 312 | 313 | (model as any).relations['related-model'] = customModel.forge({ 314 | email: 'foo@example.com', 315 | attr2: 'value2' 316 | }); 317 | 318 | let collection: Collection = bookshelf.Collection.forge([model]); 319 | 320 | let result: any = mapper.map(collection, 'models'); 321 | 322 | let expected: any = { 323 | included: [ 324 | { 325 | type: 'related-models', 326 | id : 'foo@example.com', 327 | attributes: { 328 | attr2: 'value2' 329 | }, 330 | links : { self : domain + '/related-models/foo@example.com' } 331 | } 332 | ] 333 | }; 334 | 335 | expect(_.matches(expected)(result)).toBe(true); 336 | }); 337 | 338 | it('should serialize null or undefined data', () => { 339 | let result1: any = mapper.map(undefined, 'models'); 340 | let result2: any = mapper.map(null, 'models'); 341 | 342 | let expected: any = { 343 | data: null 344 | }; 345 | 346 | expect(_.matches(expected)(result1)).toBe(true); 347 | expect(_.matches(expected)(result2)).toBe(true); 348 | }); 349 | 350 | it('should omit the model idAttribute from the attributes by default', () => { 351 | let customModel: any = bookshelf.Model.extend({ 352 | idAttribute : 'email' 353 | }); 354 | 355 | let model: any = customModel.forge({ 356 | email : 'foo@example.com', 357 | name: 'A test model', 358 | description: 'something to use as a test' 359 | }); 360 | 361 | let result: any = mapper.map(model, 'models'); 362 | 363 | let expected: any = { 364 | data: { 365 | id : 'foo@example.com', 366 | type: 'models', 367 | attributes: { 368 | name: 'A test model', 369 | description: 'something to use as a test' 370 | } 371 | } 372 | }; 373 | 374 | expect(_.matches(expected)(result)).toBe(true); 375 | expect(_.has(result.data.attributes, 'email')).toBe(false); 376 | }); 377 | 378 | it('should omit the model idAttribute from the attributes by default with array idAttribute', () => { 379 | let customModel: any = bookshelf.Model.extend({ 380 | idAttribute : ['email', 'name'] 381 | }); 382 | 383 | let model: any = customModel.forge({ 384 | email : 'foo@example.com', 385 | name : 'A test model', 386 | description: 'something to use as a test' 387 | }); 388 | 389 | let result: any = mapper.map(model, 'models'); 390 | 391 | let expected: any = { 392 | data: { 393 | attributes: { 394 | description: 'something to use as a test' 395 | } 396 | } 397 | }; 398 | 399 | expect(_.matches(expected)(result)).toBe(true); 400 | expect(_.has(result.data.attributes, 'email')).toBe(false); 401 | expect(_.has(result.data.attributes, 'name')).toBe(false); 402 | }); 403 | 404 | it('should omit attributes that match regexes passed by the user', () => { 405 | let model: Model = bookshelf.Model.forge({ 406 | id: '4', 407 | attr: 'value', 408 | paid: true, 409 | 'related-id': 123, 410 | 'another_id': '456', 411 | 'someId': '890' 412 | }); 413 | 414 | let result: any = mapper.map(model, 'models', { attributes: { omit: [/^id$/, /[_-]id$/, /Id$/] }}); 415 | 416 | let expected: any = { 417 | data: { 418 | id: '4', 419 | type: 'models', 420 | attributes: { 421 | attr: 'value', 422 | paid: true 423 | } 424 | } 425 | }; 426 | 427 | expect(_.matches(expected)(result)).toBe(true); 428 | expect(_.isEqual(result.data.attributes, expected.data.attributes)).toBe(true); 429 | }); 430 | 431 | it('should omit attributes that exactly equal strings passed by the user', () => { 432 | let model: Model = bookshelf.Model.forge({ 433 | id: '4', 434 | attr: 'value', 435 | 'to-omit': true, 436 | 'not-to-omit': false, 437 | ids : [4, 5, 6] 438 | }); 439 | 440 | let result: any = mapper.map(model, 'models', { attributes: { omit: ['id', 'to-omit'] } }); 441 | 442 | let expected: any = { 443 | data: { 444 | id: '4', 445 | type: 'models', 446 | attributes: { 447 | attr: 'value', 448 | 'not-to-omit': false, 449 | ids : [4, 5, 6] 450 | } 451 | } 452 | }; 453 | 454 | expect(_.matches(expected)(result)).toBe(true); 455 | expect(_.isEqual(result.data.attributes, expected.data.attributes)).toBe(true); 456 | }); 457 | 458 | it('should stop omitting attributes that would be omitted', () => { 459 | let customModel: any = bookshelf.Model.extend({ 460 | idAttribute : 'email' 461 | }); 462 | 463 | let model: Model = customModel.forge({ 464 | email: 'email@example.com' 465 | }); 466 | 467 | let result1: any = mapper.map(model, 'models', { attributes: { omit: [] } }); 468 | 469 | let expected: any = { 470 | data: { 471 | id: 'email@example.com', 472 | type: 'models', 473 | attributes: { 474 | email: 'email@example.com' 475 | } 476 | } 477 | }; 478 | 479 | expect(_.isMatch(result1, expected)).toBe(true); 480 | expect(_.isEqual(result1.data.attributes, expected.data.attributes)).toBe(true); 481 | }); 482 | 483 | it('should only include attributes that match regexes passed by the user', () => { 484 | let model: Model = bookshelf.Model.forge({ 485 | id: '4', 486 | Attr: 'value', 487 | paid: true, 488 | 'related-id': 123, 489 | 'another_id': '456', 490 | 'someId': '890' 491 | }); 492 | 493 | let result: any = mapper.map(model, 'models', { attributes: { include: [ /.*at.*/i ] } }); 494 | 495 | let expected: any = { 496 | data: { 497 | id: '4', 498 | type: 'models', 499 | attributes: { 500 | Attr: 'value', 501 | 'related-id': 123 502 | } 503 | } 504 | }; 505 | 506 | expect(_.matches(expected)(result)).toBe(true); 507 | expect(_.isEqual(result.data.attributes, expected.data.attributes)).toBe(true); 508 | }); 509 | 510 | it('should give more precedence to omit than include option for attributes', () => { 511 | let model: Model = bookshelf.Model.forge({ 512 | id: '4', 513 | Attr: 'value', 514 | 'related-id': 123, 515 | 'another_id': '456', 516 | 'someId': '890' 517 | }); 518 | 519 | let result: any = mapper.map(model, 'models', { attributes: { omit: [ 'related-id' ], include: [ /id/i ] } }); 520 | 521 | let expected: any = { 522 | data: { 523 | id: '4', 524 | type: 'models', 525 | attributes: { 526 | id: '4', 527 | 'another_id': '456', 528 | 'someId': '890' 529 | } 530 | } 531 | }; 532 | 533 | expect(_.matches(expected)(result)).toBe(true); 534 | expect(_.isEqual(result.data.attributes, expected.data.attributes)).toBe(true); 535 | }); 536 | 537 | it('passing attributes an array should be used as include the property', () => { 538 | let model: Model = bookshelf.Model.forge({ 539 | id: '4', 540 | Attr: 'value', 541 | 'related-id': 123, 542 | 'another_id': '456', 543 | 'someId': '890' 544 | }); 545 | 546 | let result: any = mapper.map(model, 'models', { attributes: [ /id/i ] }); 547 | 548 | let expected: any = { 549 | data: { 550 | id: '4', 551 | type: 'models', 552 | attributes: { 553 | id: '4', 554 | 'related-id': 123, 555 | 'another_id': '456', 556 | 'someId': '890' 557 | } 558 | } 559 | }; 560 | 561 | expect(_.matches(expected)(result)).toBe(true); 562 | expect(_.isEqual(result.data.attributes, expected.data.attributes)).toBe(true); 563 | }); 564 | 565 | it('should only include attributes that exactly equal strings passed by the user', () => { 566 | let model: Model = bookshelf.Model.forge({ 567 | id: '4', 568 | attr: 'value', 569 | paid: true, 570 | 'related-id': 123, 571 | 'another_id': '456', 572 | 'someId': '890' 573 | }); 574 | 575 | let result: any = mapper.map(model, 'models', { attributes: [ 'attr', 'paid' ] }); 576 | 577 | let expected: any = { 578 | data: { 579 | id: '4', 580 | type: 'models', 581 | attributes: { 582 | attr: 'value', 583 | paid: true 584 | } 585 | } 586 | }; 587 | 588 | expect(_.matches(expected)(result)).toBe(true); 589 | expect(_.isEqual(result.data.attributes, expected.data.attributes)).toBe(true); 590 | }); 591 | 592 | it('should serialize an empty collection', () => { 593 | let collection: Collection = bookshelf.Collection.forge(); 594 | 595 | let result: any = mapper.map(collection, 'models'); 596 | 597 | let expected: any = { 598 | data: [] 599 | }; 600 | 601 | expect(_.matches(expected)(result)).toBe(true); 602 | }); 603 | 604 | it('should serialize a collection', () => { 605 | let elements: Model[] = _.range(5).map((num: number) => { 606 | return bookshelf.Model.forge({id: num, attr: 'value' + num}); 607 | }); 608 | 609 | let collection: Collection = bookshelf.Collection.forge(elements); 610 | 611 | let result: any = mapper.map(collection, 'models'); 612 | 613 | let expected: any = { 614 | data: _.range(5).map((num: number) => { 615 | return { 616 | id: num.toString(), 617 | type: 'models', 618 | attributes: { 619 | attr: 'value' + num 620 | } 621 | }; 622 | }) 623 | }; 624 | 625 | expect(_.matches(expected)(result)).toBe(true); 626 | }); 627 | 628 | it('should serialize a basic model with a top-level meta object', () => { 629 | let model: Model = bookshelf.Model.forge({ 630 | id: '5', 631 | name: 'A test model', 632 | description: 'something to use as a test' 633 | }); 634 | 635 | let result: any = mapper.map(model, 'models', {meta: {key: 'value'}}); 636 | 637 | let expected: any = { 638 | meta: { key: 'value' }, 639 | data: { 640 | id: '5', 641 | type: 'models', 642 | attributes: { 643 | name: 'A test model', 644 | description: 'something to use as a test' 645 | } 646 | } 647 | }; 648 | 649 | expect(_.matches(expected)(result)).toBe(true); 650 | }); 651 | 652 | it('should serialize a basic model without a top-level meta object', () => { 653 | let model: Model = bookshelf.Model.forge({ 654 | id: '5', 655 | name: 'A test model', 656 | description: 'something to use as a test' 657 | }); 658 | 659 | let result: any = mapper.map(model, 'models'); 660 | 661 | let expected: any = { 662 | data: { 663 | id: '5', 664 | type: 'models', 665 | attributes: { 666 | name: 'A test model', 667 | description: 'something to use as a test' 668 | } 669 | } 670 | }; 671 | 672 | expect(_.has(result, 'meta')).toBe(false); 673 | }); 674 | 675 | it('should serialize a collection with a top-level meta object', () => { 676 | let elements: Model[] = _.range(5).map((num: number) => { 677 | return bookshelf.Model.forge({id: num, attr: 'value' + num}); 678 | }); 679 | 680 | let collection: Collection = bookshelf.Collection.forge(elements); 681 | 682 | let result: any = mapper.map(collection, 'models', {meta: {key: 'value'}}); 683 | 684 | let expected: any = { 685 | meta: { key: 'value' }, 686 | data: _.range(5).map((num: number) => { 687 | return { 688 | id: num.toString(), 689 | type: 'models', 690 | attributes: { 691 | attr: 'value' + num 692 | } 693 | }; 694 | }) 695 | }; 696 | 697 | expect(_.matches(expected)(result)).toBe(true); 698 | }); 699 | 700 | describe('should serialize id as string', () => { 701 | 702 | it('for basic model', () => { 703 | let model: Model = bookshelf.Model.forge({ id: 5 }); 704 | 705 | let result: any = mapper.map(model, 'models'); 706 | 707 | expect(result.data.id).toBe('5'); 708 | expect(typeof result.data.id).toBe('string'); 709 | }); 710 | 711 | it('for collection', () => { 712 | let elements: Model[] = _.times(5, (num) => { 713 | return bookshelf.Model.forge({id: num, attr: 'value' + num}); 714 | }); 715 | 716 | let collection: Collection = bookshelf.Collection.forge(elements); 717 | 718 | let result: any = mapper.map(collection, 'models'); 719 | 720 | _.times(5, (num) => { 721 | expect(result.data[num].id).toBe(num.toString()); 722 | expect(typeof result.data[num].id).toBe('string'); 723 | }); 724 | }); 725 | 726 | it('for relationships', () => { 727 | let model: Model = bookshelf.Model.forge({id: 5, attr: 'value'}); 728 | (model as any).relations['related-model'] = bookshelf.Model.forge({id: 10, attr2: 'value2'}); 729 | 730 | let result: any = mapper.map(model, 'model'); 731 | 732 | let related = result.data.relationships['related-model']; 733 | 734 | expect(related.data.id).toBe('10'); 735 | expect(typeof related.data.id).toBe('string'); 736 | 737 | }); 738 | 739 | }); 740 | }); 741 | 742 | describe('Bookshelf links', () => { 743 | let bookshelf: bs; 744 | let mapper: Mapper.Bookshelf; 745 | let domain: string = 'https://domain.com'; 746 | 747 | beforeAll(() => { 748 | bookshelf = bs(knex(({ client: 'sqlite3', useNullAsDefault: true } as knex.Config))); 749 | mapper = new Mapper.Bookshelf(domain); 750 | }); 751 | 752 | afterAll((done: Function) => { 753 | bookshelf.knex.destroy(done); 754 | }); 755 | 756 | it('should add top level links', () => { 757 | let model: Model = bookshelf.Model.forge({id: '10'}); 758 | 759 | let result: any = mapper.map(model, 'models'); 760 | 761 | let expected: any = { 762 | data: { 763 | id: '10', 764 | type: 'models' 765 | }, 766 | links: { 767 | self: domain + '/models' 768 | } 769 | }; 770 | 771 | expect(_.matches(expected)(result)).toBe(true); 772 | }); 773 | 774 | it('should add top level links for a collection', () => { 775 | let model1: Model = bookshelf.Model.forge({id: '5'}); 776 | let model2: Model = bookshelf.Model.forge({id: '6'}); 777 | let collection: Collection = bookshelf.Collection.forge([model1, model2]); 778 | 779 | let result: any = mapper.map(collection, 'models'); 780 | 781 | let expected: any = { 782 | data: [{ 783 | id: '5', 784 | type: 'models' 785 | }, 786 | { 787 | id: '6', 788 | type: 'models' 789 | }], 790 | links: { 791 | self: domain + '/models' 792 | } 793 | }; 794 | 795 | expect(_.matches(expected)(result)).toBe(true); 796 | }); 797 | 798 | it('should add primary data links', () => { 799 | let model: Model = bookshelf.Model.forge({id: '5'}); 800 | 801 | let result: any = mapper.map(model, 'models'); 802 | 803 | let expected: any = { 804 | data: { 805 | id: '5', 806 | type: 'models', 807 | links: { 808 | self: domain + '/models' + '/5' 809 | } 810 | } 811 | }; 812 | 813 | expect(_.matches(expected)(result)).toBe(true); 814 | 815 | }); 816 | 817 | it('should add primary data links for a collection', () => { 818 | let model1: Model = bookshelf.Model.forge({id: '5'}); 819 | let model2: Model = bookshelf.Model.forge({id: '6'}); 820 | let collection: Collection = bookshelf.Collection.forge([model1, model2]); 821 | 822 | let result: any = mapper.map(collection, 'models'); 823 | 824 | let expected: any = { 825 | data: [{ 826 | id: '5', 827 | type: 'models', 828 | links: { 829 | self: domain + '/models' + '/5' 830 | } 831 | }, 832 | { 833 | id: '6', 834 | type: 'models', 835 | links: { 836 | self: domain + '/models' + '/6' 837 | } 838 | }] 839 | }; 840 | 841 | expect(_.matches(expected)(result)).toBe(true); 842 | }); 843 | 844 | it('should add related links', () => { 845 | let model: Model = bookshelf.Model.forge({id: '5'}); 846 | (model as any).relations['related-model'] = bookshelf.Model.forge({id: '10'}); 847 | 848 | let result: any = mapper.map(model, 'models'); 849 | 850 | let expected: any = { 851 | data: { 852 | relationships: { 853 | 'related-model': { 854 | data: { 855 | id: '10', 856 | type: 'related-models' // TODO check correct casing 857 | }, 858 | links: { 859 | self: domain + '/models/' + '5' + '/relationships/' + 'related-model', 860 | related: domain + '/models/' + '5' + '/related-model' 861 | } 862 | } 863 | } 864 | } 865 | }; 866 | 867 | expect(_.matches(expected)(result)).toBe(true); 868 | 869 | }); 870 | 871 | it('should add related links for nested relationships', () => { 872 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 873 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 874 | let model3: Model = bookshelf.Model.forge({id: '7', attr: 'value'}); 875 | 876 | (model1 as any).relations['related-model'] = model2; 877 | (model2 as any).relations['nested-related-model'] = model3; 878 | 879 | let result: any = mapper.map(model1, 'models'); 880 | 881 | let expected: any = { 882 | data: { 883 | relationships: { 884 | 'related-model': { 885 | data: { 886 | type: 'related-models', 887 | id: '6' 888 | } 889 | } 890 | } 891 | }, 892 | included: [ 893 | { 894 | id: '6', 895 | type: 'related-models', 896 | attributes: { 897 | attr: 'value' 898 | }, 899 | relationships: { 900 | 'nested-related-model': { 901 | data: { 902 | type: 'nested-related-models', 903 | id: '7' 904 | }, 905 | links: { 906 | self: `${domain}/related-models/6/relationships/nested-related-model`, 907 | related: `${domain}/related-models/6/nested-related-model` 908 | } 909 | } 910 | } 911 | }, 912 | { 913 | id: '7', 914 | type: 'nested-related-models', 915 | attributes: { 916 | attr: 'value' 917 | } 918 | } 919 | ] 920 | }; 921 | 922 | expect(_.matches(expected)(result)).toBe(true); 923 | }); 924 | 925 | it('should add related links for nested relationships within a collection', () => { 926 | 927 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 928 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 929 | 930 | (model1 as any).relations['related-model'] = model2; 931 | (model2 as any).relations['nested-related-models'] = bookshelf.Collection.forge([ 932 | bookshelf.Model.forge({id: '10', attr: 'value'}), 933 | bookshelf.Model.forge({id: '11', attr: 'value'}) 934 | ]); 935 | 936 | let collection: Collection = bookshelf.Collection.forge([model1]); 937 | 938 | let result: any = mapper.map(collection, 'models'); 939 | 940 | let expected: any = { 941 | included: [ 942 | { 943 | id: '6', 944 | type: 'related-models', 945 | attributes: { 946 | attr: 'value' 947 | }, 948 | relationships: { 949 | 'nested-related-models': { 950 | data: [{ 951 | type: 'nested-related-models', 952 | id: '10' 953 | }, { 954 | type: 'nested-related-models', 955 | id: '11' 956 | }], 957 | links: { 958 | self: `${domain}/related-models/6/relationships/nested-related-models`, 959 | related: `${domain}/related-models/6/nested-related-models` 960 | } 961 | } 962 | } 963 | }, 964 | { 965 | id: '10', 966 | type: 'nested-related-models', 967 | attributes: { 968 | attr: 'value' 969 | } 970 | }, 971 | { 972 | id: '11', 973 | type: 'nested-related-models', 974 | attributes: { 975 | attr: 'value' 976 | } 977 | } 978 | ], 979 | data: [{ 980 | relationships: { 981 | 'related-model': { 982 | data: { 983 | type: 'related-models', 984 | id: '6' 985 | } 986 | } 987 | } 988 | }] 989 | }; 990 | 991 | expect(_.matches(expected)(result)).toBe(true); 992 | 993 | }); 994 | 995 | it('should add pagination links', () => { 996 | let limit: number = 10; 997 | let offset: number = 40; 998 | let total: number = 100; 999 | 1000 | let elements: Model[] = _.range(10).map((num: number) => { 1001 | return bookshelf.Model.forge({id: num, attr: 'value' + num}); 1002 | }); 1003 | 1004 | let collection: Collection = bookshelf.Collection.forge(elements); 1005 | 1006 | let result: any = mapper.map(collection, 'models', { 1007 | pagination: { limit, offset, total } 1008 | }); 1009 | 1010 | let expected: any = { 1011 | links: { 1012 | first: domain + '/models?page[limit]=10&page[offset]=0', 1013 | prev: domain + '/models?page[limit]=10&page[offset]=30', 1014 | next: domain + '/models?page[limit]=10&page[offset]=50', 1015 | last: domain + '/models?page[limit]=10&page[offset]=90' 1016 | } 1017 | }; 1018 | 1019 | expect(_.matches(expected)(result)).toBe(true); 1020 | }); 1021 | 1022 | it('should not add pagination links if no pagination data is passed', () => { 1023 | let elements: Model[] = _.range(10).map((num: number) => { 1024 | return bookshelf.Model.forge({id: num, attr: 'value' + num}); 1025 | }); 1026 | 1027 | let collection: Collection = bookshelf.Collection.forge(elements); 1028 | 1029 | let result: any = mapper.map(collection, 'models'); 1030 | 1031 | expect(result.links).toBeDefined(); 1032 | expect(Object.keys(result.links)).not.toContain('prev'); 1033 | expect(Object.keys(result.links)).not.toContain('first'); 1034 | expect(Object.keys(result.links)).not.toContain('next'); 1035 | expect(Object.keys(result.links)).not.toContain('last'); 1036 | }); 1037 | 1038 | it('should support bookshelf\'s new `rowCount` property passed by `Model#fetchPage`', () => { 1039 | let limit: number = 10; 1040 | let offset: number = 40; 1041 | let total: number = 100; 1042 | 1043 | let elements: Model[] = _.range(10).map((num: number) => { 1044 | return bookshelf.Model.forge({id: num, attr: 'value' + num}); 1045 | }); 1046 | 1047 | let collection: Collection = bookshelf.Collection.forge(elements); 1048 | 1049 | let result: any = mapper.map(collection, 'models', { 1050 | pagination: { limit, offset, rowCount: total } 1051 | }); 1052 | 1053 | let expected: any = { 1054 | links: { 1055 | first: domain + '/models?page[limit]=' + limit + '&page[offset]=' + 0, 1056 | prev: domain + '/models?page[limit]=' + limit + '&page[offset]=' + (offset - limit), 1057 | next: domain + '/models?page[limit]=' + limit + '&page[offset]=' + (offset + limit), 1058 | last: domain + '/models?page[limit]=' + limit + '&page[offset]=' + (total - limit) 1059 | } 1060 | }; 1061 | 1062 | expect(_.matches(expected)(result)).toBe(true); 1063 | }); 1064 | 1065 | it('should omit `first` and `prev` pagination links if offset = 0', () => { 1066 | let limit: number = 5; 1067 | let offset: number = 0; 1068 | let total: number = 10; 1069 | 1070 | let collection: Collection = bookshelf.Collection.forge([]); 1071 | 1072 | let result: any = mapper.map(collection, 'models', { 1073 | pagination: { limit, offset, total } 1074 | }); 1075 | 1076 | expect(result.links).toBeDefined(); 1077 | expect(Object.keys(result.links)).not.toContain('first'); 1078 | expect(Object.keys(result.links)).not.toContain('prev'); 1079 | expect(Object.keys(result.links)).toContain('next'); 1080 | expect(Object.keys(result.links)).toContain('last'); 1081 | }); 1082 | 1083 | it('should omit `next` and `last` pagination links if at last page', () => { 1084 | let limit: number = 5; 1085 | let offset: number = 5; 1086 | let total: number = 10; 1087 | 1088 | let collection: Collection = bookshelf.Collection.forge([]); 1089 | 1090 | let result: any = mapper.map(collection, 'models', { 1091 | pagination: { limit, offset, total } 1092 | }); 1093 | 1094 | expect(result.links).toBeDefined(); 1095 | expect(Object.keys(result.links)).toContain('first'); 1096 | expect(Object.keys(result.links)).toContain('prev'); 1097 | expect(Object.keys(result.links)).not.toContain('next'); 1098 | expect(Object.keys(result.links)).not.toContain('last'); 1099 | }); 1100 | 1101 | it('should not add pagination links if collection is empty', () => { 1102 | let limit: number = 10; 1103 | let offset: number = 40; 1104 | let total: number = 0; 1105 | 1106 | let collection: Collection = bookshelf.Collection.forge([]); 1107 | 1108 | let result: any = mapper.map(collection, 'models', { 1109 | pagination: { limit, offset, total } 1110 | }); 1111 | 1112 | expect(result.links).toBeDefined(); 1113 | expect(Object.keys(result.links)).not.toContain('prev'); 1114 | expect(Object.keys(result.links)).not.toContain('first'); 1115 | expect(Object.keys(result.links)).not.toContain('next'); 1116 | expect(Object.keys(result.links)).not.toContain('last'); 1117 | }); 1118 | 1119 | it('should not add pagination links if total <= limit', () => { 1120 | let limit: number = 10; 1121 | let offset: number = 0; 1122 | let total: number = 5; 1123 | 1124 | let elements: Model[] = _.range(total).map((num: number) => { 1125 | return bookshelf.Model.forge({id: num, attr: 'value' + num}); 1126 | }); 1127 | 1128 | let collection: Collection = bookshelf.Collection.forge(elements); 1129 | 1130 | let result: any = mapper.map(collection, 'models', { 1131 | pagination: { limit, offset, total } 1132 | }); 1133 | 1134 | expect(result.links).toBeDefined(); 1135 | expect(Object.keys(result.links)).not.toContain('prev'); 1136 | expect(Object.keys(result.links)).not.toContain('first'); 1137 | expect(Object.keys(result.links)).not.toContain('next'); 1138 | expect(Object.keys(result.links)).not.toContain('last'); 1139 | }); 1140 | 1141 | it('should not overlap last page with the penultimate page', () => { 1142 | let limit: number = 3; 1143 | let offset: number = 3; 1144 | let total: number = 10; 1145 | 1146 | let elements: Model[] = _.range(10).map((num: number) => { 1147 | return bookshelf.Model.forge({id: num, attr: 'value' + num}); 1148 | }); 1149 | 1150 | let collection: Collection = bookshelf.Collection.forge(elements); 1151 | 1152 | let result: any = mapper.map(collection, 'models', { 1153 | pagination: { limit, offset, total } 1154 | }); 1155 | 1156 | let expected: any = { 1157 | links: { 1158 | next: domain + '/models?page[limit]=' + 3 + '&page[offset]=' + 6, 1159 | last: domain + '/models?page[limit]=' + 1 + '&page[offset]=' + 9 1160 | } 1161 | }; 1162 | 1163 | expect(_.matches(expected)(result)).toBe(true); 1164 | }); 1165 | 1166 | it('should not serialize links when `enableLinks: false`', () => { 1167 | 1168 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1169 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 1170 | 1171 | (model1 as any).relations['related-model'] = model2; 1172 | (model2 as any).relations['nested-related-models'] = bookshelf.Collection.forge([ 1173 | bookshelf.Model.forge({id: '10', attr: 'value'}) 1174 | ]); 1175 | 1176 | let collection: Collection = bookshelf.Collection.forge([model1]); 1177 | 1178 | let result: any = mapper.map(collection, 'models', { enableLinks: false }); 1179 | 1180 | expect(result.links).not.toBeDefined(); 1181 | expect(result.data[0].relationships['related-model'].links).not.toBeDefined(); 1182 | expect(result.included[0].links).not.toBeDefined(); 1183 | expect(result.included[1].links).not.toBeDefined(); 1184 | }); 1185 | 1186 | }); 1187 | 1188 | describe('Bookshelf relations', () => { 1189 | let bookshelf: bs; 1190 | let mapper: Mapper.Bookshelf; 1191 | let domain: string = 'https://domain.com'; 1192 | 1193 | beforeAll(() => { 1194 | bookshelf = bs(knex(({ client: 'sqlite3', useNullAsDefault: true } as knex.Config))); 1195 | mapper = new Mapper.Bookshelf(domain); 1196 | }); 1197 | 1198 | afterAll((done: Function) => { 1199 | bookshelf.knex.destroy(done); 1200 | }); 1201 | 1202 | it('should add relationships object', () => { 1203 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1204 | (model as any).relations['related-model'] = bookshelf.Model.forge({id: '10', attr2: 'value2'}); 1205 | (model as any).relations['related-model'] 1206 | .relations['inner-related-model'] = bookshelf.Model.forge({id: '20', attr3: 'value3'}); 1207 | 1208 | let result: any = mapper.map(model, 'model'); 1209 | 1210 | let expected: any = { 1211 | data: { 1212 | id: '5', 1213 | type: 'models', 1214 | attributes: { 1215 | attr: 'value' 1216 | }, 1217 | relationships: { 1218 | 'related-model': { 1219 | data: { 1220 | id: '10', 1221 | type: 'related-models' 1222 | } 1223 | } 1224 | } 1225 | }, 1226 | included: [ 1227 | { 1228 | type: 'related-models', 1229 | id: '10', 1230 | attributes: { 1231 | attr2: 'value2' 1232 | }, 1233 | relationships: { 1234 | 'inner-related-model': { 1235 | data: { 1236 | id: '20', 1237 | type: 'inner-related-models' 1238 | } 1239 | } 1240 | } 1241 | }, 1242 | { 1243 | type: 'inner-related-models', 1244 | id: '20', 1245 | attributes: { 1246 | attr3: 'value3' 1247 | } 1248 | } 1249 | ] 1250 | }; 1251 | 1252 | expect(_.matches(expected)(result)).toBe(true); 1253 | 1254 | }); 1255 | 1256 | it('should put the single related object in the included array', () => { 1257 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1258 | (model as any).relations['related-model'] = bookshelf.Model.forge({id: '10', attr2: 'value2'}); 1259 | 1260 | let result: any = mapper.map(model, 'models'); 1261 | 1262 | let expected: any = { 1263 | included: [ 1264 | { 1265 | id: '10', 1266 | type: 'related-models', 1267 | attributes: { 1268 | attr2: 'value2' 1269 | } 1270 | } 1271 | ] 1272 | }; 1273 | 1274 | expect(_.matches(expected)(result)).toBe(true); 1275 | }); 1276 | 1277 | it('should return empty array when collection is empty', () => { 1278 | let collection: Collection = bookshelf.Collection.forge([]); 1279 | 1280 | let result: any = mapper.map(collection, 'models'); 1281 | 1282 | let expected: any = { 1283 | data : [] 1284 | }; 1285 | 1286 | expect(_.matches(expected)(result)).toBe(true); 1287 | }); 1288 | 1289 | it('should put the array of related objects in the included array', () => { 1290 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1291 | 1292 | (model1 as any).relations['related-models'] = bookshelf.Collection.forge([ 1293 | bookshelf.Model.forge({id: '10', attr2: 'value20'}), 1294 | bookshelf.Model.forge({id: '11', attr2: 'value21'}) 1295 | ]); 1296 | 1297 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 1298 | 1299 | (model2 as any).relations['related-models'] = bookshelf.Collection.forge([ 1300 | bookshelf.Model.forge({id: '12', attr2: 'value22'}), 1301 | bookshelf.Model.forge({id: '13', attr2: 'value23'}) 1302 | ]); 1303 | 1304 | let collection: Collection = bookshelf.Collection.forge([model1, model2]); 1305 | 1306 | let result: any = mapper.map(collection, 'models'); 1307 | 1308 | let expected: any = { 1309 | included: [ 1310 | { 1311 | id: '10', 1312 | type: 'related-models', 1313 | attributes: { 1314 | attr2: 'value20' 1315 | } 1316 | }, 1317 | { 1318 | id: '11', 1319 | type: 'related-models', 1320 | attributes: { 1321 | attr2: 'value21' 1322 | } 1323 | }, 1324 | { 1325 | id: '12', 1326 | type: 'related-models', 1327 | attributes: { 1328 | attr2: 'value22' 1329 | } 1330 | }, 1331 | { 1332 | id: '13', 1333 | type: 'related-models', 1334 | attributes: { 1335 | attr2: 'value23' 1336 | } 1337 | } 1338 | ] 1339 | }; 1340 | 1341 | expect(_.matches(expected)(result)).toBe(true); 1342 | 1343 | }); 1344 | 1345 | it('should put the array of related objects in the included array with same related', () => { 1346 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1347 | 1348 | (model1 as any).relations['related1-models'] = bookshelf.Collection.forge([ 1349 | bookshelf.Model.forge({id: '10', attr2: 'value20'}), 1350 | bookshelf.Model.forge({id: '11', attr2: 'value21'}) 1351 | ]); 1352 | 1353 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 1354 | 1355 | (model2 as any).relations['related1-models'] = bookshelf.Collection.forge([ 1356 | bookshelf.Model.forge({id: '11', attr2: 'value21'}), 1357 | bookshelf.Model.forge({id: '12', attr2: 'value22'}) 1358 | ]); 1359 | 1360 | let collection: Collection = bookshelf.Collection.forge([model1, model2]); 1361 | 1362 | let result: any = mapper.map(collection, 'models'); 1363 | 1364 | let expected: any = { 1365 | included: [ 1366 | { 1367 | id: '10', 1368 | type: 'related1-models', 1369 | attributes: { 1370 | attr2: 'value20' 1371 | } 1372 | }, 1373 | { 1374 | id: '11', 1375 | type: 'related1-models', 1376 | attributes: { 1377 | attr2: 'value21' 1378 | } 1379 | }, 1380 | { 1381 | id: '12', 1382 | type: 'related1-models', 1383 | attributes: { 1384 | attr2: 'value22' 1385 | } 1386 | } 1387 | ] 1388 | }; 1389 | 1390 | expect(_.matches(expected)(result)).toBe(true); 1391 | 1392 | }); 1393 | 1394 | it('should put the array of related objects in the included array with different related', () => { 1395 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1396 | 1397 | (model1 as any).relations['related1-models'] = bookshelf.Collection.forge([ 1398 | bookshelf.Model.forge({id: '10', attr2: 'value20'}), 1399 | bookshelf.Model.forge({id: '11', attr2: 'value21'}) 1400 | ]); 1401 | 1402 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 1403 | 1404 | (model2 as any).relations['related2-models'] = bookshelf.Collection.forge([ 1405 | bookshelf.Model.forge({id: '12', attr2: 'value22'}), 1406 | bookshelf.Model.forge({id: '13', attr2: 'value23'}) 1407 | ]); 1408 | 1409 | let model3: Model = bookshelf.Model.forge({id: '7', attr: 'value'}); 1410 | 1411 | (model3 as any).relations['related2-models'] = bookshelf.Collection.forge([ 1412 | bookshelf.Model.forge({id: '13', attr2: 'value23'}), 1413 | bookshelf.Model.forge({id: '14', attr2: 'value24'}) 1414 | ]); 1415 | 1416 | let collection: Collection = bookshelf.Collection.forge([model1, model2, model3]); 1417 | 1418 | let result: any = mapper.map(collection, 'models'); 1419 | 1420 | let expected: any = { 1421 | included: [ 1422 | { 1423 | id: '10', 1424 | type: 'related1-models', 1425 | attributes: { 1426 | attr2: 'value20' 1427 | } 1428 | }, 1429 | { 1430 | id: '11', 1431 | type: 'related1-models', 1432 | attributes: { 1433 | attr2: 'value21' 1434 | } 1435 | }, 1436 | { 1437 | id: '12', 1438 | type: 'related2-models', 1439 | attributes: { 1440 | attr2: 'value22' 1441 | } 1442 | }, 1443 | { 1444 | id: '13', 1445 | type: 'related2-models', 1446 | attributes: { 1447 | attr2: 'value23' 1448 | } 1449 | }, 1450 | { 1451 | id: '14', 1452 | type: 'related2-models', 1453 | attributes: { 1454 | attr2: 'value24' 1455 | } 1456 | } 1457 | ] 1458 | }; 1459 | 1460 | expect(_.matches(expected)(result)).toBe(true); 1461 | 1462 | }); 1463 | 1464 | it('should support including nested relationships', () => { 1465 | 1466 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1467 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 1468 | let model3: Model = bookshelf.Model.forge({id: '7', attr: 'value'}); 1469 | 1470 | (model1 as any).relations['related-model'] = model2; 1471 | (model2 as any).relations['nested-related-model'] = model3; 1472 | 1473 | let result: any = mapper.map(model1, 'models'); 1474 | 1475 | let expected: any = { 1476 | included: [ 1477 | { 1478 | id: '6', 1479 | type: 'related-models', 1480 | attributes: { 1481 | attr: 'value' 1482 | }, 1483 | relationships: { 1484 | 'nested-related-model': { 1485 | data: { 1486 | type: 'nested-related-models', 1487 | id: '7' 1488 | } 1489 | } 1490 | } 1491 | }, 1492 | { 1493 | id: '7', 1494 | type: 'nested-related-models', 1495 | attributes: { 1496 | attr: 'value' 1497 | } 1498 | } 1499 | ], 1500 | data: { 1501 | relationships: { 1502 | 'related-model': { 1503 | data: { 1504 | type: 'related-models', 1505 | id: '6' 1506 | } 1507 | } 1508 | } 1509 | } 1510 | }; 1511 | 1512 | expect(_.matches(expected)(result)).toBe(true); 1513 | 1514 | }); 1515 | 1516 | it('should support including nested relationships', () => { 1517 | 1518 | let customModel: any = bookshelf.Model.extend({ 1519 | idAttribute : ['id1', 'id2'] 1520 | }); 1521 | 1522 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1523 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 1524 | let model3: Model = customModel.forge({id1: '7', id2: '8', attr: 'value'}); 1525 | 1526 | (model1 as any).relations['related-model'] = model2; 1527 | (model2 as any).relations['nested-related-model'] = model3; 1528 | 1529 | let result: any = mapper.map(model1, 'models'); 1530 | 1531 | let expected: any = { 1532 | included: [ 1533 | { 1534 | id: '6', 1535 | type: 'related-models', 1536 | attributes: { 1537 | attr: 'value' 1538 | }, 1539 | relationships: { 1540 | 'nested-related-model': { 1541 | data: { 1542 | type: 'nested-related-models', 1543 | id: '7,8' 1544 | } 1545 | } 1546 | } 1547 | }, 1548 | { 1549 | id: '7,8', 1550 | type: 'nested-related-models', 1551 | attributes: { 1552 | attr: 'value' 1553 | } 1554 | } 1555 | ], 1556 | data: { 1557 | relationships: { 1558 | 'related-model': { 1559 | data: { 1560 | type: 'related-models', 1561 | id: '6' 1562 | } 1563 | } 1564 | } 1565 | } 1566 | }; 1567 | 1568 | expect(_.matches(expected)(result)).toBe(true); 1569 | 1570 | }); 1571 | 1572 | it('should support including nested has-many relationships', () => { 1573 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1574 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 1575 | 1576 | (model1 as any).relations['related-models'] = bookshelf.Collection.forge([model2]); 1577 | (model2 as any).relations['nested-related-models'] = bookshelf.Collection.forge([ 1578 | bookshelf.Model.forge({id: '10', attr: 'value'}), 1579 | bookshelf.Model.forge({id: '11', attr: 'value'}) 1580 | ]); 1581 | 1582 | let collection: Collection = bookshelf.Collection.forge([model1]); 1583 | 1584 | let result: any = mapper.map(collection, 'models'); 1585 | 1586 | let expected: any = { 1587 | included: [ 1588 | { 1589 | id: '6', 1590 | type: 'related-models', 1591 | attributes: { 1592 | attr: 'value' 1593 | }, 1594 | relationships: { 1595 | 'nested-related-models': { 1596 | data: [ 1597 | { 1598 | id: '10', 1599 | type: 'nested-related-models' 1600 | }, 1601 | { 1602 | id: '11', 1603 | type: 'nested-related-models' 1604 | } 1605 | ], 1606 | links: { 1607 | self: `${domain}/related-models/6/relationships/nested-related-models`, 1608 | related: `${domain}/related-models/6/nested-related-models` 1609 | } 1610 | } 1611 | } 1612 | }, 1613 | { 1614 | id: '10', 1615 | type: 'nested-related-models', 1616 | attributes: { 1617 | attr: 'value' 1618 | } 1619 | }, 1620 | { 1621 | id: '11', 1622 | type: 'nested-related-models', 1623 | attributes: { 1624 | attr: 'value' 1625 | } 1626 | } 1627 | ], 1628 | data: [ 1629 | { 1630 | relationships: { 1631 | 'related-models': { 1632 | data: [ 1633 | { 1634 | id: '6', 1635 | type: 'related-models' 1636 | } 1637 | ] 1638 | } 1639 | } 1640 | } 1641 | ] 1642 | }; 1643 | 1644 | expect(_.matches(expected)(result)).toBe(true); 1645 | }); 1646 | 1647 | it('should support including nested relationships when acting on a collection', () => { 1648 | 1649 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1650 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 1651 | 1652 | (model1 as any).relations['related-model'] = model2; 1653 | (model2 as any).relations['nested-related-models'] = bookshelf.Collection.forge([ 1654 | bookshelf.Model.forge({id: '10', attr: 'value'}), 1655 | bookshelf.Model.forge({id: '11', attr: 'value'}) 1656 | ]); 1657 | 1658 | let collection: Collection = bookshelf.Collection.forge([model1]); 1659 | 1660 | let result: any = mapper.map(collection, 'models'); 1661 | 1662 | let expected: any = { 1663 | included: [ 1664 | { 1665 | id: '6', 1666 | type: 'related-models', 1667 | attributes: { 1668 | attr: 'value' 1669 | }, 1670 | relationships: { 1671 | 'nested-related-models': { 1672 | data: [{ 1673 | type: 'nested-related-models', 1674 | id: '10' 1675 | }, { 1676 | type: 'nested-related-models', 1677 | id: '11' 1678 | }] 1679 | } 1680 | } 1681 | }, 1682 | { 1683 | id: '10', 1684 | type: 'nested-related-models', 1685 | attributes: { 1686 | attr: 'value' 1687 | } 1688 | }, 1689 | { 1690 | id: '11', 1691 | type: 'nested-related-models', 1692 | attributes: { 1693 | attr: 'value' 1694 | } 1695 | } 1696 | ], 1697 | data: [{ 1698 | relationships: { 1699 | 'related-model': { 1700 | data: { 1701 | type: 'related-models', 1702 | id: '6' 1703 | } 1704 | } 1705 | } 1706 | }] 1707 | }; 1708 | 1709 | expect(_.matches(expected)(result)).toBe(true); 1710 | 1711 | }); 1712 | 1713 | it('should put the array of related objects in the included array with proper attributes even if relation is empty', () => { 1714 | let model1: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1715 | 1716 | (model1 as any).relations['related-models'] = bookshelf.Collection.forge(); 1717 | 1718 | let model2: Model = bookshelf.Model.forge({id: '6', attr: 'value'}); 1719 | 1720 | (model2 as any).relations['related-models'] = bookshelf.Collection.forge([ 1721 | bookshelf.Model.forge({id: '12', attr2: 'value22'}), 1722 | bookshelf.Model.forge({id: '13', attr2: 'value23'}) 1723 | ]); 1724 | 1725 | let collection: Collection = bookshelf.Collection.forge([model1, model2]); 1726 | 1727 | let result: any = mapper.map(collection, 'models'); 1728 | 1729 | let expected: any = { 1730 | included: [ 1731 | { 1732 | id: '12', 1733 | type: 'related-models', 1734 | attributes: { 1735 | attr2: 'value22' 1736 | } 1737 | }, 1738 | { 1739 | id: '13', 1740 | type: 'related-models', 1741 | attributes: { 1742 | attr2: 'value23' 1743 | } 1744 | } 1745 | ] 1746 | }; 1747 | 1748 | expect(_.matches(expected)(result)).toBe(true); 1749 | 1750 | }); 1751 | 1752 | it('should give an option to ignore relations', () => { 1753 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1754 | (model as any).relations['related-models'] = bookshelf.Collection.forge([ 1755 | bookshelf.Model.forge({id: '10', attr2: 'value20'}), 1756 | bookshelf.Model.forge({id: '11', attr2: 'value21'}) 1757 | ]); 1758 | 1759 | let result1: any = mapper.map(model, 'models', {relations: { included: true }}); 1760 | let result2: any = mapper.map(model, 'models', {relations: false}); 1761 | 1762 | let expected1: any = { 1763 | included: [ 1764 | { 1765 | id: '10', 1766 | type: 'related-models', 1767 | attributes: { 1768 | attr2: 'value20' 1769 | } 1770 | }, 1771 | { 1772 | id: '11', 1773 | type: 'related-models', 1774 | attributes: { 1775 | attr2: 'value21' 1776 | } 1777 | } 1778 | ] 1779 | }; 1780 | 1781 | expect(_.matches(expected1)(result1)).toBe(true); 1782 | expect(_.has(result2, 'data.relationships.related-models')).toBe(false); 1783 | expect(_.has(result2, 'included')).toBe(false); 1784 | 1785 | }); 1786 | 1787 | it('should give an option to choose which relations to add', () => { 1788 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1789 | (model as any).relations['related-one'] = bookshelf.Model.forge({id: '10', attr1: 'value1'}); 1790 | (model as any).relations['related-two'] = bookshelf.Model.forge({id: '20', attr2: 'value2'}); 1791 | 1792 | let result: any = mapper.map(model, 'models', {relations: { fields: ['related-two'], included: true }}); 1793 | let result2: any = mapper.map(model, 'models', {relations: { fields: ['related-two'], included: false }}); 1794 | 1795 | let expected: any = { 1796 | id: '20', 1797 | type: 'related-twos', 1798 | attributes: { 1799 | attr2: 'value2' 1800 | } 1801 | }; 1802 | 1803 | expect(result.included.length).toEqual(1); 1804 | expect(_.matches(expected)(result.included[0])).toBe(true); 1805 | 1806 | expect(_.has(result2, 'data.relationships.related-two')).toBe(true); 1807 | expect(_.has(result2, 'included')).toBe(false); 1808 | }); 1809 | 1810 | it('should give an option to choose which relations to include', () => { 1811 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1812 | (model as any).relations['related-one'] = bookshelf.Model.forge({id: '10', attr1: 'value1'}); 1813 | (model as any).relations['related-two'] = bookshelf.Model.forge({id: '20', attr2: 'value2'}); 1814 | 1815 | let result: any = mapper.map(model, 'models', {relations: { included: true }}); 1816 | let result2: any = mapper.map(model, 'models', { relations: { included: ['related-two']}}); 1817 | let result3: any = mapper.map(model, 'models', { relations: { fields: ['related-one'], included: ['related-one', 'related-two']}}); 1818 | 1819 | let expected: any = { 1820 | included: [ 1821 | { 1822 | id: '10', 1823 | type: 'related-ones', 1824 | attributes: { 1825 | attr1: 'value1' 1826 | } 1827 | }, 1828 | { 1829 | id: '20', 1830 | type: 'related-twos', 1831 | attributes: { 1832 | attr2: 'value2' 1833 | } 1834 | } 1835 | ] 1836 | }; 1837 | 1838 | expect(_.matches(expected)(result)).toBe(true); 1839 | 1840 | expect(_.find(result2.included, { type: 'related-ones'})).not.toBeDefined(); 1841 | expect(_.find(result2.included, { type: 'related-twos'})).toBeDefined(); 1842 | 1843 | expect(_.find(result3.included, { type: 'related-twos'})).not.toBeDefined(); 1844 | expect(_.find(result3.included, { type: 'related-ones'})).toBeDefined(); 1845 | }); 1846 | 1847 | it('should specify an option to format specific types using an object', () => { 1848 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1849 | (model as any).relations['related-one'] = bookshelf.Model.forge({id: '10', attr1: 'value1'}); 1850 | (model as any).relations['related-two'] = bookshelf.Model.forge({id: '20', attr2: 'value2'}); 1851 | (model as any).relations['related-three'] = bookshelf.Model.forge({id: '30', attr3: 'value3'}); 1852 | 1853 | let result: any = mapper.map(model, 'resource', {typeForModel: {'related-one': 'inners', 'related-two': 'non-plural'}}); 1854 | 1855 | let expected: any = { 1856 | data: { 1857 | type: 'resources' 1858 | }, 1859 | included: [ 1860 | { 1861 | id: '10', 1862 | type: 'inners', 1863 | attributes: { 1864 | attr1: 'value1' 1865 | } 1866 | }, 1867 | { 1868 | id: '20', 1869 | type: 'non-plural', 1870 | attributes: { 1871 | attr2: 'value2' 1872 | } 1873 | }, 1874 | { 1875 | id: '30', 1876 | type: 'related-threes', 1877 | attributes: { 1878 | attr3: 'value3' 1879 | } 1880 | } 1881 | ] 1882 | }; 1883 | 1884 | expect(_.matches(expected)(result)).toBe(true); 1885 | }); 1886 | 1887 | it('should specify an option to format the type using a function', () => { 1888 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1889 | (model as any).relations['related-one'] = bookshelf.Model.forge({id: '10', attr1: 'value1'}); 1890 | (model as any).relations['related-two'] = bookshelf.Model.forge({id: '20', attr2: 'value2'}); 1891 | 1892 | let result: any = mapper.map(model, 'resource', {typeForModel: () => 'models'}); 1893 | 1894 | let expected: any = { 1895 | data: { 1896 | type: 'models' 1897 | }, 1898 | included: [ 1899 | { 1900 | id: '10', 1901 | type: 'models', 1902 | attributes: { 1903 | attr1: 'value1' 1904 | } 1905 | }, 1906 | { 1907 | id: '20', 1908 | type: 'models', 1909 | attributes: { 1910 | attr2: 'value2' 1911 | } 1912 | } 1913 | ] 1914 | }; 1915 | 1916 | expect(_.matches(expected)(result)).toBe(true); 1917 | }); 1918 | 1919 | it('should give an option to modify attribute properties with a function', () => { 1920 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1921 | (model as any).relations['related-one'] = bookshelf.Model.forge({id: '10', attr1: 'value1'}); 1922 | (model as any).relations['related-two'] = bookshelf.Model.forge({id: '20', attr2: 'value2'}); 1923 | 1924 | let result: any = mapper.map(model, 'models', {keyForAttr: _.toUpper}); 1925 | 1926 | let expected: any = { 1927 | data: { 1928 | attributes: { 1929 | ATTR: 'value' 1930 | } 1931 | }, 1932 | included: [ 1933 | { 1934 | attributes: { 1935 | ATTR1: 'value1' 1936 | } 1937 | }, 1938 | { 1939 | attributes: { 1940 | ATTR2: 'value2' 1941 | } 1942 | } 1943 | ] 1944 | }; 1945 | 1946 | expect(_.matches(expected)(result)).toBe(true); 1947 | }); 1948 | 1949 | it('should merge for the template correctly', () => { 1950 | let elements: Model[] = _.range(3).map((num: number) => { 1951 | let model: Model = bookshelf.Model.forge({id: num, attr: 'value' + num}); 1952 | (model as any).relations.rels = bookshelf.Collection.forge([]); 1953 | return model; 1954 | }); 1955 | 1956 | (elements[0].related('rels') as Collection).add(bookshelf.Model.forge({id: 3, attr: 'value'})); 1957 | let collection: Collection = bookshelf.Collection.forge(elements); 1958 | 1959 | let result: any = mapper.map(collection, 'models'); 1960 | 1961 | let expected: any = { 1962 | data: [ { 1963 | type: 'models', 1964 | id: '0', 1965 | relationships: { 1966 | rels: { 1967 | data: [ { type: 'rels', id: '3' } ] 1968 | } 1969 | } 1970 | } ], 1971 | included: [ { 1972 | type: 'rels', 1973 | id: '3', 1974 | attributes: { attr: 'value' } 1975 | } ] 1976 | }; 1977 | 1978 | expect(_.matches(expected)(result)).toBe(true); 1979 | }); 1980 | 1981 | it('should give an API to merge relations attributes', () => { 1982 | pending('Not targeted for release 1.x'); 1983 | }); 1984 | 1985 | it('should give an option to include relations', () => { 1986 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 1987 | (model as any).relations['related-models'] = bookshelf.Collection.forge([ 1988 | bookshelf.Model.forge({id: '10', attr2: 'value20'}), 1989 | bookshelf.Model.forge({id: '11', attr2: 'value21'}) 1990 | ]); 1991 | 1992 | let result1: any = mapper.map(model, 'models', {relations: { included: true }}); 1993 | let result2: any = mapper.map(model, 'models', {relations: { included: false }}); 1994 | let result3: any = mapper.map(model, 'models', {relations: false}); 1995 | 1996 | let expected1: any = { 1997 | included: [ 1998 | { 1999 | id: '10', 2000 | type: 'related-models', 2001 | attributes: { 2002 | attr2: 'value20' 2003 | } 2004 | }, 2005 | { 2006 | id: '11', 2007 | type: 'related-models', 2008 | attributes: { 2009 | attr2: 'value21' 2010 | } 2011 | } 2012 | ] 2013 | }; 2014 | 2015 | expect(_.matches(expected1)(result1)).toBe(true); 2016 | expect(_.has(result1, 'data.relationships.related-models')).toBe(true); 2017 | 2018 | expect(_.has(result2, 'data.relationships.related-models')).toBe(true); 2019 | expect(_.has(result2, 'included')).toBe(false); 2020 | 2021 | expect(_.has(result3, 'data.relationships.related-models')).toBe(false); 2022 | expect(_.has(result3, 'included')).toBe(false); 2023 | }); 2024 | }); 2025 | 2026 | describe('Serializer options', () => { 2027 | let bookshelf: bs; 2028 | let mapper: Mapper.Bookshelf; 2029 | let domain: string = 'https://domain.com'; 2030 | 2031 | beforeAll(() => { 2032 | bookshelf = bs(knex(({ client: 'sqlite3', useNullAsDefault: true } as knex.Config))); 2033 | }); 2034 | 2035 | it('should not overwrite typeForAttribute function passed to serializer', () => { 2036 | mapper = new Mapper.Bookshelf(domain, {typeForAttribute: () => 'type'}); 2037 | 2038 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 2039 | let result: any = mapper.map(model, 'model'); 2040 | 2041 | let expected: any = { 2042 | data: { 2043 | type: 'type' 2044 | } 2045 | }; 2046 | 2047 | expect(_.matches(expected)(result)).toBe(true); 2048 | }); 2049 | 2050 | it('should not overwrite keyForAttribute function passed to serializer', () => { 2051 | mapper = new Mapper.Bookshelf(domain, {keyForAttribute: _.kebabCase}); 2052 | 2053 | let model: Model = bookshelf.Model.forge({id: '5', oneAttr: 'value'}); 2054 | let result: any = mapper.map(model, 'model'); 2055 | 2056 | let expected: any = { 2057 | data: { 2058 | attributes: { 2059 | 'one-attr': 'value' 2060 | } 2061 | } 2062 | }; 2063 | 2064 | expect(_.matches(expected)(result)).toBe(true); 2065 | }); 2066 | 2067 | it('should overwrite pluralizeType option passed to serializer', () => { 2068 | mapper = new Mapper.Bookshelf(domain, {pluralizeType: false}); 2069 | 2070 | let model: Model = bookshelf.Model.forge({id: '5', attr: 'value'}); 2071 | let result: any = mapper.map(model, 'model'); 2072 | 2073 | let expected: any = { 2074 | data: { 2075 | type: 'models' 2076 | } 2077 | }; 2078 | 2079 | expect(_.matches(expected)(result)).toBe(true); 2080 | }); 2081 | 2082 | }); 2083 | 2084 | describe('Plugins', () => { 2085 | let bookshelf: bs; 2086 | let mapper: Mapper.Bookshelf; 2087 | let domain: string = 'https://domain.com'; 2088 | 2089 | beforeAll(() => { 2090 | bookshelf = bs(knex(({ client: 'sqlite3', useNullAsDefault: true } as knex.Config))); 2091 | bookshelf.plugin('visibility'); 2092 | mapper = new Mapper.Bookshelf(domain); 2093 | }); 2094 | 2095 | afterAll((done: Function) => { 2096 | bookshelf.knex.destroy(done); 2097 | }); 2098 | 2099 | describe('Visibility', () => { 2100 | 2101 | it('should respect the visible property', () => { 2102 | let topModel: any = bookshelf.Model.extend({ 2103 | visible : ['first_name', 'last_name'] 2104 | }); 2105 | let relModel: any = bookshelf.Model.extend({ 2106 | visible : ['description'] 2107 | }); 2108 | 2109 | let model: Model = topModel.forge({ 2110 | id: 1, 2111 | first_name: 'Joe', 2112 | last_name: 'Doe', 2113 | email: 'joe@example.com' 2114 | }); 2115 | (model as any).relations.foo = relModel.forge({ 2116 | id: 2, 2117 | description: "Joe's foo", 2118 | secret: "Pls don't tell anyone" 2119 | }); 2120 | 2121 | let collection: Collection = bookshelf.Collection.forge([ model ]); 2122 | let result: any = mapper.map(collection, 'model'); 2123 | let expected: any = { 2124 | data: [{ 2125 | type: 'models', 2126 | id: '1', 2127 | attributes: { 2128 | first_name: 'Joe', 2129 | last_name: 'Doe' 2130 | }, 2131 | relationships: { 2132 | foo: { 2133 | data: { 2134 | type: 'foos', 2135 | id: '2' 2136 | } 2137 | } 2138 | } 2139 | }], 2140 | included: [{ 2141 | type: 'foos', 2142 | id: '2', 2143 | attributes: { 2144 | description: "Joe's foo" 2145 | } 2146 | }] 2147 | }; 2148 | 2149 | expect(_.isMatch(result, expected)).toBe(true); 2150 | expect(_.keys(result.data[0].attributes)).toEqual(['first_name', 'last_name']); 2151 | expect(_.keys(result.included[0].attributes)).toEqual(['description']); 2152 | }); 2153 | 2154 | it('should respect the hidden property', () => { 2155 | let topModel: any = bookshelf.Model.extend({ 2156 | hidden : ['email'] 2157 | }); 2158 | let relModel: any = bookshelf.Model.extend({ 2159 | hidden : ['secret'] 2160 | }); 2161 | 2162 | let model: Model = topModel.forge({ 2163 | id: 1, 2164 | first_name: 'Joe', 2165 | last_name: 'Doe', 2166 | email: 'joe@example.com' 2167 | }); 2168 | (model as any).relations.foo = relModel.forge({ 2169 | id: 2, 2170 | description: "Joe's foo", 2171 | secret: "Pls don't tell anyone" 2172 | }); 2173 | 2174 | let collection: Collection = bookshelf.Collection.forge([ model ]); 2175 | let result: any = mapper.map(collection, 'model'); 2176 | let expected: any = { 2177 | data: [{ 2178 | type: 'models', 2179 | id: '1', 2180 | attributes: { 2181 | first_name: 'Joe', 2182 | last_name: 'Doe' 2183 | }, 2184 | relationships: { 2185 | foo: { 2186 | data: { 2187 | type: 'foos', 2188 | id: '2' 2189 | } 2190 | } 2191 | } 2192 | }], 2193 | included: [{ 2194 | type: 'foos', 2195 | id: '2', 2196 | attributes: { 2197 | description: "Joe's foo" 2198 | } 2199 | }] 2200 | }; 2201 | 2202 | expect(_.isMatch(result, expected)).toBe(true); 2203 | expect(_.keys(result.data[0].attributes)).toEqual(['first_name', 'last_name']); 2204 | expect(_.keys(result.included[0].attributes)).toEqual(['description']); 2205 | }); 2206 | 2207 | }); 2208 | }); 2209 | 2210 | describe('Issues', () => { 2211 | let bookshelf: bs; 2212 | let mapper: Mapper.Bookshelf; 2213 | let domain: string = 'https://domain.com'; 2214 | 2215 | beforeAll(() => { 2216 | bookshelf = bs(knex(({ client: 'sqlite3', useNullAsDefault: true } as knex.Config))); 2217 | mapper = new Mapper.Bookshelf(domain); 2218 | }); 2219 | 2220 | afterAll((done: Function) => { 2221 | bookshelf.knex.destroy(done); 2222 | }); 2223 | 2224 | describe('#35', () => { 2225 | beforeAll(() => { 2226 | bookshelf.plugin('virtuals'); 2227 | mapper = new Mapper.Bookshelf(domain); 2228 | }) 2229 | 2230 | it('should return virtuals', () => { 2231 | let userModel: any = bookshelf.Model.extend({ 2232 | virtuals: { 2233 | full_name: function(this: Model) { 2234 | return `${this.get('first_name')} ${this.get('last_name')}` 2235 | } 2236 | } 2237 | }); 2238 | 2239 | let user: Model = userModel.forge({ 2240 | id: 1, 2241 | first_name: 'Al', 2242 | last_name: 'Bundy' 2243 | }); 2244 | 2245 | let result: any = mapper.map(user, 'user') 2246 | let expected: any = { 2247 | links: { 2248 | self: 'https://domain.com/users' 2249 | }, 2250 | data: { 2251 | type: 'users', 2252 | id: '1', 2253 | links: { 2254 | self: 'https://domain.com/users/1' 2255 | }, 2256 | attributes: { 2257 | first_name: 'Al', 2258 | last_name: 'Bundy', 2259 | full_name: 'Al Bundy' 2260 | } 2261 | } 2262 | } 2263 | 2264 | expect(_.isMatch(result, expected)).toBe(true) 2265 | }) 2266 | 2267 | it('shouldn\'t return virtuals if outputVirtuals is set to false', () => { 2268 | let userModel: any = bookshelf.Model.extend({ 2269 | virtuals: { 2270 | full_name: function(this: Model) { 2271 | return `${this.get('first_name')} ${this.get('last_name')}` 2272 | } 2273 | }, 2274 | outputVirtuals: false 2275 | }); 2276 | 2277 | let user: Model = userModel.forge({ 2278 | id: 1, 2279 | first_name: 'Al', 2280 | last_name: 'Bundy' 2281 | }); 2282 | 2283 | let result: any = mapper.map(user, 'user') 2284 | let expected: any = { 2285 | links: { 2286 | self: 'https://domain.com/users' 2287 | }, 2288 | data: { 2289 | type: 'users', 2290 | id: '1', 2291 | links: { 2292 | self: 'https://domain.com/users/1' 2293 | }, 2294 | attributes: { 2295 | first_name: 'Al', 2296 | last_name: 'Bundy' 2297 | } 2298 | } 2299 | } 2300 | 2301 | expect(_.isMatch(result, expected)).toBe(true) 2302 | }) 2303 | 2304 | it('outputVirtuals as mapper option should override the default outputVirtuals', () => { 2305 | let userModel1: any = bookshelf.Model.extend({ 2306 | virtuals: { 2307 | full_name: function(this: Model) { 2308 | return `${this.get('first_name')} ${this.get('last_name')}` 2309 | } 2310 | }, 2311 | outputVirtuals: false 2312 | }); 2313 | 2314 | let userModel2: any = bookshelf.Model.extend({ 2315 | virtuals: { 2316 | full_name: function(this: Model) { 2317 | return `${this.get('first_name')} ${this.get('last_name')}` 2318 | } 2319 | }, 2320 | outputVirtuals: true 2321 | }); 2322 | 2323 | let user1: Model = userModel1.forge({ 2324 | id: 1, 2325 | first_name: 'Al', 2326 | last_name: 'Bundy' 2327 | }); 2328 | 2329 | let user2: Model = userModel1.forge({ 2330 | id: 1, 2331 | first_name: 'Al', 2332 | last_name: 'Bundy' 2333 | }); 2334 | 2335 | let result1_with: any = mapper.map(user1, 'user', {outputVirtuals: true}) 2336 | let result1_without: any = mapper.map(user1, 'user', {outputVirtuals: false}) 2337 | let result2_with: any = mapper.map(user2, 'user', {outputVirtuals: true}) 2338 | let result2_without: any = mapper.map(user2, 'user', {outputVirtuals: false}) 2339 | 2340 | let expected_with: any = { 2341 | links: { 2342 | self: 'https://domain.com/users' 2343 | }, 2344 | data: { 2345 | type: 'users', 2346 | id: '1', 2347 | links: { 2348 | self: 'https://domain.com/users/1' 2349 | }, 2350 | attributes: { 2351 | first_name: 'Al', 2352 | last_name: 'Bundy' 2353 | } 2354 | } 2355 | } 2356 | 2357 | let expected_without: any = { 2358 | links: { 2359 | self: 'https://domain.com/users' 2360 | }, 2361 | data: { 2362 | type: 'users', 2363 | id: '1', 2364 | links: { 2365 | self: 'https://domain.com/users/1' 2366 | }, 2367 | attributes: { 2368 | first_name: 'Al', 2369 | last_name: 'Bundy' 2370 | } 2371 | } 2372 | } 2373 | 2374 | expect(_.isMatch(result1_with, expected_with)).toBe(true) 2375 | expect(_.isMatch(result2_with, expected_with)).toBe(true) 2376 | expect(_.isMatch(result1_without, expected_without)).toBe(true) 2377 | expect(_.isMatch(result2_without, expected_without)).toBe(true) 2378 | }) 2379 | }); 2380 | 2381 | it('#77', () => { 2382 | 2383 | // model with full relations 2384 | let model1: Model = bookshelf.Model.forge({ 2385 | id: 14428, 2386 | foo_id: 2973, 2387 | bar_id: 59, 2388 | name: 'Bla #14428', 2389 | created_at: null, 2390 | updated_at: null, 2391 | deleted_at: null 2392 | }); 2393 | (model1 as any).relations.foo = bookshelf.Model.forge({ 2394 | id: 2973, 2395 | name: 'Foo #2973', 2396 | created_at: null, 2397 | updated_at: null, 2398 | deleted_at: null 2399 | }); 2400 | (model1 as any).relations.bar = bookshelf.Model.forge({ 2401 | id: 59, 2402 | foo_id: 2973, 2403 | name: 'Bar #59', 2404 | created_at: null, 2405 | updated_at: null, 2406 | deleted_at: null 2407 | }); 2408 | 2409 | // model with one relation bar_id = null 2410 | let model2: Model = bookshelf.Model.forge({ 2411 | id: 14417, 2412 | foo_id: 2973, 2413 | bar_id: null, 2414 | name: 'Bla #14417', 2415 | created_at: null, 2416 | updated_at: null, 2417 | deleted_at: null 2418 | }); 2419 | (model2 as any).relations.foo = bookshelf.Model.forge({ 2420 | id: 2973, 2421 | name: 'Foo #2973', 2422 | created_at: null, 2423 | updated_at: null, 2424 | deleted_at: null 2425 | }); 2426 | (model2 as any).relations.bar = bookshelf.Model.forge({}); 2427 | 2428 | let collection1: Collection = bookshelf.Collection.forge([ 2429 | model1, model2 2430 | ]); 2431 | let result1: any = mapper.map(collection1, 'model'); 2432 | let expected1: any = { 2433 | included: [{ 2434 | type: 'foos', 2435 | id: '2973' 2436 | }, { 2437 | type: 'bars', 2438 | id: '59' 2439 | }] 2440 | }; 2441 | 2442 | expect(_.matches(expected1)(result1)).toBe(true); 2443 | 2444 | let collection2: Collection = bookshelf.Collection.forge([ 2445 | model2, model1 2446 | ]); 2447 | let result2: any = mapper.map(collection2, 'model'); 2448 | let expected2: any = { 2449 | included: [{ 2450 | type: 'foos', 2451 | id: '2973' 2452 | }, { 2453 | type: 'bars', 2454 | id: '59' 2455 | }] 2456 | }; 2457 | 2458 | expect(_.matches(expected2)(result2)).toBe(true); 2459 | 2460 | }); 2461 | 2462 | it('#81', () => { 2463 | 2464 | let user: Model = bookshelf.Model.forge({ 2465 | id: 1, 2466 | email: 'email@gmail.com', 2467 | first_name: 'Ad', 2468 | last_name: 'Oner', 2469 | org_id: 1, 2470 | connect_type: '', 2471 | created_at: '2016-07-04T10:48:27.000Z', 2472 | updated_at: '2016-10-09T19:10:38.000Z' 2473 | }); 2474 | (user as any).relations.organization = bookshelf.Model.forge({ 2475 | 'id': 1, 2476 | phone: '', 2477 | company_name: 'MyCompany', 2478 | created_at: '2016-07-04T10:46:53.000Z', 2479 | updated_at: '2016-07-04T10:46:53.000Z' 2480 | }); 2481 | 2482 | let result: any = mapper.map(user, 'user'); 2483 | let expected: any = { 2484 | data: { 2485 | type: 'users', 2486 | id: '1', 2487 | relationships: { 2488 | organization: { 2489 | data: { 2490 | type: 'organizations', 2491 | id: '1' 2492 | } 2493 | } 2494 | } 2495 | }, 2496 | included: [{ 2497 | type: 'organizations', 2498 | id: '1' 2499 | }] 2500 | }; 2501 | 2502 | expect(_.isMatch(result, expected)).toBe(true); 2503 | 2504 | }); 2505 | 2506 | describe('#101', () => { 2507 | 2508 | it('should not replicate included complex object attributes', () => { 2509 | let elements: Model[] = _.range(3).map((num: number) => { 2510 | const model = bookshelf.Model.forge({id: num, attr: 'value' + num}); 2511 | 2512 | (model as any).relations['related-model'] = bookshelf.Model.forge({ 2513 | id: num+1, 2514 | attr2: ['value' + (num+1), 'value' + (num*10)] 2515 | }); 2516 | 2517 | 2518 | return model; 2519 | }); 2520 | 2521 | let collection: Collection = bookshelf.Collection.forge(elements); 2522 | 2523 | let result: any = mapper.map(collection, 'models'); 2524 | 2525 | let expectedIncluded: any = [ 2526 | { 2527 | type: "related-models", 2528 | id: "1", 2529 | attributes: { attr2: [ "value1", "value0" ] }, 2530 | links: { self: "https://domain.com/related-models/1" } 2531 | }, 2532 | { 2533 | type: "related-models", 2534 | id: "2", 2535 | attributes: { attr2: [ "value2", "value10" ] }, 2536 | links: { self: "https://domain.com/related-models/2" } 2537 | }, 2538 | { 2539 | type: "related-models", 2540 | id: "3", 2541 | attributes: { attr2: [ "value3", "value20" ] }, 2542 | links: { self: "https://domain.com/related-models/3" } 2543 | } 2544 | ]; 2545 | 2546 | expect(result.included).toEqual(expectedIncluded); 2547 | expect(_.matches(expectedIncluded)(result.included)).toBe(true); 2548 | 2549 | }); 2550 | 2551 | }); 2552 | 2553 | }); 2554 | -------------------------------------------------------------------------------- /spec/support/jasmine.json: -------------------------------------------------------------------------------- 1 | { 2 | "spec_dir": "es5/spec", 3 | "spec_files": [ 4 | "**/*[sS]pec.js" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/bookshelf/extras.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The purpose of this module is to extend the initially defined properties, 3 | * behaviors and characteristics of the bookshelf API 4 | */ 5 | 6 | import { Model as BModel, Collection as BCollection } from 'bookshelf'; 7 | import { MapOpts, RelationTypeOpt, RelationOpts } from '../interfaces'; 8 | 9 | // Bookshelf Options 10 | export interface BookOpts extends MapOpts { 11 | // Attributes-related 12 | keyForAttr: (attr: string) => string; 13 | 14 | // Relations-related 15 | relations: boolean | RelationOpts; 16 | typeForModel: RelationTypeOpt; 17 | 18 | // Links-related 19 | enableLinks: boolean; 20 | 21 | // Virtuals-related; 22 | outputVirtuals?: boolean; 23 | } 24 | 25 | /** 26 | * Internal form of the relations property of bookshelf objects 27 | */ 28 | export interface RelationsObject { 29 | [relationName: string]: Data; 30 | } 31 | 32 | export interface Attributes { 33 | [attrName: string]: any; 34 | } 35 | 36 | /** 37 | * Bookshelf Model including some private properties 38 | */ 39 | export interface Model extends BModel { 40 | id: any; 41 | 42 | // TODO: PR to fix Bookshelf types 43 | // idAttribute?: string | string[]; 44 | idAttribute: any; 45 | 46 | attributes: Attributes; 47 | relations: RelationsObject; 48 | virtuals?: any; 49 | outputVirtuals?: boolean; 50 | } 51 | 52 | /** 53 | * Bookshelf Collection including some private properties 54 | */ 55 | export interface Collection extends BCollection { 56 | models: Model[]; 57 | length: number; 58 | } 59 | 60 | export type Data = Model | Collection; 61 | 62 | /** 63 | * Bookshelf Model Type Guard 64 | * https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html 65 | */ 66 | export function isModel(data: Data): data is Model { 67 | return data ? !isCollection(data) : false; 68 | } 69 | 70 | /** 71 | * Bookshelf Collection Type Guard 72 | * https://basarat.gitbooks.io/typescript/content/docs/types/typeGuard.html 73 | */ 74 | export function isCollection(data: Data): data is Collection { 75 | // Type recognition based on duck-typing 76 | return data ? (data as Collection).models !== undefined : false; 77 | } 78 | -------------------------------------------------------------------------------- /src/bookshelf/index.ts: -------------------------------------------------------------------------------- 1 | import { assign, identity } from 'lodash'; 2 | import { pluralize as plural } from 'inflection'; 3 | 4 | import { SerialOpts, Serializer } from '../serializer'; 5 | import { Mapper, MapOpts, LinkOpts } from '../interfaces'; 6 | import { Data, BookOpts } from './extras'; 7 | import { Information, processData, toJSON } from './utils'; 8 | 9 | /** 10 | * Mapper class for Bookshelf sources 11 | */ 12 | export class Bookshelf implements Mapper { 13 | 14 | /** 15 | * Standard constructor 16 | */ 17 | constructor(public baseUrl: string, public serialOpts?: SerialOpts) { } 18 | 19 | /** 20 | * Maps bookshelf data to a JSON-API 1.0 compliant object 21 | * 22 | * The `any` type data source is set for typing compatibility, but must be removed if possible 23 | * TODO fix data any type 24 | */ 25 | map(data: Data | any, type: string, mapOpts: MapOpts = {}): any { 26 | 27 | // Set default values for the options 28 | const { 29 | attributes, 30 | keyForAttr = identity, 31 | relations = true, 32 | typeForModel = (attr: string) => plural(attr), 33 | enableLinks = true, 34 | pagination, 35 | query, 36 | meta, 37 | outputVirtuals 38 | }: MapOpts = mapOpts; 39 | 40 | const bookOpts: BookOpts = { 41 | attributes, keyForAttr, 42 | relations, typeForModel, 43 | enableLinks, pagination, 44 | query, outputVirtuals 45 | }; 46 | 47 | const linkOpts: LinkOpts = { baseUrl: this.baseUrl, type, pag: pagination }; 48 | 49 | const info: Information = { bookOpts, linkOpts }; 50 | const template: SerialOpts = processData(info, data); 51 | 52 | const typeForAttribute: (attr: string) => string = 53 | typeof typeForModel === 'function' 54 | ? typeForModel 55 | : (attr: string) => typeForModel[attr] || plural(attr); // pluralize when falsy 56 | 57 | // Override the template with the provided serializer options 58 | assign(template, { typeForAttribute, keyForAttribute: keyForAttr, meta }, this.serialOpts); 59 | 60 | // Return the data in JSON API format 61 | const json: any = toJSON(data); 62 | return new Serializer(type, template).serialize(json); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/bookshelf/links.ts: -------------------------------------------------------------------------------- 1 | import { assign, omit, isEmpty, isNil } from 'lodash'; 2 | import { pluralize as plural } from 'inflection'; 3 | import { stringify as queryParams } from 'qs'; 4 | 5 | import { Model } from './extras'; 6 | import { LinkOpts, PagOpts, QueryOpts } from '../interfaces'; 7 | import { LinkObj } from '../serializer'; 8 | 9 | function urlConcat(...parts: string[]): string { 10 | return parts.join('/'); 11 | } 12 | 13 | /** 14 | * Creates top level links object, for primary data and pagination links. 15 | */ 16 | export function topLinks(linkOpts: LinkOpts): LinkObj { 17 | let { baseUrl, type, pag }: LinkOpts = linkOpts; 18 | 19 | let obj: LinkObj = { 20 | self: urlConcat(baseUrl, plural(type)) 21 | }; 22 | 23 | // Build pagination if available 24 | if (!isNil(pag)) { 25 | 26 | // Support Bookshelf's built-in paging parameters 27 | if (!isNil(pag.rowCount)) { 28 | pag.total = pag.rowCount; 29 | } 30 | 31 | // Only add pagination links when more than 1 page 32 | if (!isNil(pag.total) && pag.total > 0 && pag.total > pag.limit) { 33 | assign(obj, pagLinks(linkOpts)); 34 | } 35 | } 36 | 37 | return obj; 38 | } 39 | 40 | /** 41 | * Create links object, for pagination links. 42 | * Since its used only inside other functions in this model, its not exported 43 | */ 44 | function pagLinks(linkOpts: LinkOpts): LinkObj | undefined { 45 | let { baseUrl, type, pag, query = {} }: LinkOpts = linkOpts; 46 | 47 | if (pag === undefined) { 48 | return undefined; 49 | } 50 | 51 | const { offset, limit, total}: PagOpts = pag; 52 | // All links are based on the resource type 53 | let baseLink: string = urlConcat(baseUrl, plural(type)); 54 | 55 | // Stringify the query string without page element 56 | query = omit(query, ['page', 'page[limit]', 'page[offset]']) as QueryOpts; 57 | baseLink = baseLink + '?' + queryParams(query, {encode: false}); 58 | 59 | let obj: LinkObj = {} as LinkObj; 60 | 61 | // Add leading pag links if not at the first page 62 | if (offset > 0) { 63 | obj.first = () => { 64 | let page: any = {page: {limit, offset: 0}}; 65 | return baseLink + queryParams(page, {encode: false}); 66 | }; 67 | 68 | obj.prev = () => { 69 | let page: any = {page: {limit, offset: offset - limit}}; 70 | return baseLink + queryParams(page, {encode: false}); 71 | }; 72 | } 73 | 74 | // Add trailing pag links if not at the last page 75 | if (total && (offset + limit < total)) { 76 | obj.next = () => { 77 | let page: any = {page: {limit, offset: offset + limit}}; 78 | return baseLink + queryParams(page, {encode: false}); 79 | }; 80 | 81 | obj.last = () => { 82 | // Avoiding overlapping with the penultimate page 83 | let lastLimit: number = (total - (offset % limit)) % limit; 84 | // If the limit fits perfectly in the total, reset it to the original 85 | lastLimit = lastLimit === 0 ? limit : lastLimit; 86 | 87 | let lastOffset: number = total - lastLimit; 88 | let page: any = {page: {limit: lastLimit, offset: lastOffset }}; 89 | return baseLink + queryParams(page, {encode: false}); 90 | }; 91 | } 92 | 93 | return !isEmpty(obj) ? obj : undefined; 94 | } 95 | 96 | /** 97 | * Creates links object for a resource 98 | */ 99 | export function dataLinks(linkOpts: LinkOpts): LinkObj { 100 | let { baseUrl, type }: LinkOpts = linkOpts; 101 | let baseLink: string = urlConcat(baseUrl, plural(type)); 102 | 103 | return { 104 | self: function(resource: Model): string { 105 | return urlConcat(baseLink, resource.id); 106 | } 107 | }; 108 | } 109 | 110 | /** 111 | * Creates links object for a relationship 112 | */ 113 | export function relationshipLinks(linkOpts: LinkOpts, related: string): LinkObj { 114 | let { baseUrl, type }: LinkOpts = linkOpts; 115 | let baseLink: string = urlConcat(baseUrl, plural(type)); 116 | 117 | return { 118 | self: function(resource: any, current: any, parent: Model): string { 119 | return urlConcat(baseLink, parent.id, 'relationships', related); 120 | }, 121 | related: function(resource: any, current: any, parent: Model): string { 122 | return urlConcat(baseLink, parent.id, related); 123 | } 124 | }; 125 | } 126 | 127 | /** 128 | * Creates links object for a related resource, to be used for the included's array 129 | */ 130 | export function includedLinks(linkOpts: LinkOpts): LinkObj { 131 | let { baseUrl, type }: LinkOpts = linkOpts; 132 | let baseLink: string = urlConcat(baseUrl, plural(type)); 133 | 134 | return { 135 | self: function(primary: Model, current: Model): string { 136 | return urlConcat(baseLink, current.id); 137 | } 138 | }; 139 | } 140 | -------------------------------------------------------------------------------- /src/bookshelf/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The main purpose of this module is to provide utility functions 3 | * that follows the restrictions of the Bookshelf/Mapper/Serializer APIs 4 | * with the goal of simplifying the logic of the main 'map' method. 5 | */ 6 | 7 | import { 8 | assign, 9 | clone, 10 | cloneDeep, 11 | filter, 12 | includes, 13 | intersection, 14 | isArray, 15 | isNil, 16 | isString, 17 | isUndefined, 18 | escapeRegExp, 19 | forOwn, 20 | has, 21 | keys, 22 | map, 23 | mapValues, 24 | merge, 25 | omit, 26 | reduce, 27 | some, 28 | toString, 29 | update 30 | } from 'lodash'; 31 | 32 | import { LinkOpts, RelationOpts } from '../interfaces'; 33 | import { SerialOpts } from '../serializer'; 34 | import { AttrMatcher, AttributesOpt } from '../interfaces'; 35 | import { topLinks, dataLinks, relationshipLinks, includedLinks } from './links'; 36 | import { BookOpts, Data, Model, isModel, isCollection } from './extras'; 37 | 38 | /** 39 | * Main structure used through most utility and recursive functions 40 | */ 41 | export interface Information { 42 | bookOpts: BookOpts; 43 | linkOpts: LinkOpts; 44 | } 45 | 46 | /** 47 | * Start the data processing with top level information, 48 | * then handle resources recursively in processSample 49 | */ 50 | export function processData(info: Information, data: Data): SerialOpts { 51 | let { bookOpts: { enableLinks }, linkOpts }: Information = info; 52 | 53 | let template: SerialOpts = processSample(info, sample(data)); 54 | 55 | if (enableLinks) { 56 | template.dataLinks = dataLinks(linkOpts); 57 | template.topLevelLinks = topLinks(linkOpts); 58 | } 59 | 60 | return template; 61 | } 62 | 63 | /** 64 | * Recursively adds data-related properties to the 65 | * template to be sent to the serializer 66 | */ 67 | function processSample(info: Information, sample: Sample): SerialOpts { 68 | let { bookOpts, linkOpts }: Information = info; 69 | let { enableLinks }: BookOpts = bookOpts; 70 | 71 | let template: SerialOpts = { 72 | // Add list of valid attributes 73 | attributes: getAttrsList(sample, bookOpts) 74 | }; 75 | 76 | // Nested relations (recursive) template generation 77 | forOwn(sample.relations, (relSample: Sample, relName: string): void => { 78 | if (!relationAllowed(bookOpts, relName)) { return; } 79 | 80 | let relLinkOpts: LinkOpts = assign(clone(linkOpts), {type: relName}); 81 | let relTemplate: SerialOpts = processSample({bookOpts, linkOpts: relLinkOpts}, relSample); 82 | relTemplate.ref = 'id'; // Add reference in nested resources 83 | 84 | // Related links 85 | if (enableLinks) { 86 | relTemplate.relationshipLinks = relationshipLinks(linkOpts, relName); 87 | relTemplate.includedLinks = includedLinks(relLinkOpts); 88 | } 89 | 90 | // Include links as compound document 91 | if (!includeAllowed(bookOpts, relName)) { 92 | relTemplate.included = false; 93 | } 94 | 95 | template[relName] = relTemplate; 96 | (template.attributes as string[]).push(relName); 97 | }); 98 | 99 | return template; 100 | } 101 | 102 | /** 103 | * Representation of a sample, a model with only models in the relations, 104 | * no collections 105 | */ 106 | interface Sample extends Model { 107 | relations: { 108 | [relationName: string]: Sample 109 | }; 110 | } 111 | 112 | /** 113 | * Convert any data into a model representing 114 | * a complete sample to be used in the template generation 115 | */ 116 | function sample(data: Data): Sample { 117 | if (isModel(data)) { 118 | // Override type because we will overwrite relations 119 | const sampled: Sample = cloneDeep(omit(data, 'relations')) as Sample; 120 | sampled.relations = mapValues(data.relations, sample); 121 | 122 | return sampled; 123 | } else if (isCollection(data)) { 124 | const first: Model = data.head(); 125 | const rest: Model[] = data.tail(); 126 | return reduce(rest, mergeSample, sample(first)); 127 | } else { 128 | return {} as Sample; 129 | } 130 | } 131 | 132 | /** 133 | * Merge two models into a representation of both 134 | */ 135 | function mergeSample(main: Sample, toMerge: Model): Sample { 136 | const sampled: Sample = sample(toMerge); 137 | main.attributes = merge(main.attributes, sampled.attributes); 138 | main.relations = merge(main.relations, sampled.relations); 139 | return main; 140 | } 141 | 142 | function matches(matcher: AttrMatcher, str: string): boolean { 143 | let reg: RegExp; 144 | 145 | if (typeof matcher === 'string') { 146 | reg = RegExp(`^${escapeRegExp(matcher)}$`); 147 | } else { 148 | reg = matcher; 149 | } 150 | 151 | return reg.test(str); 152 | } 153 | /** 154 | * Retrieve model's attribute names 155 | * following filtering rules 156 | */ 157 | function getAttrsList(data: Model, bookOpts: BookOpts): string[] { 158 | let idAttr: undefined | string | string[] = data.idAttribute; 159 | if (isString(idAttr)) { 160 | idAttr = [ idAttr ]; 161 | } else if (isUndefined(idAttr)) { 162 | idAttr = []; 163 | } 164 | 165 | let attrs: string[] = keys(data.attributes); 166 | let outputVirtuals = data.outputVirtuals; 167 | 168 | if (!isNil(bookOpts.outputVirtuals)) { 169 | outputVirtuals = bookOpts.outputVirtuals 170 | } 171 | 172 | if (data.virtuals && outputVirtuals) { 173 | attrs = attrs.concat(keys(data.virtuals)) 174 | } 175 | 176 | let { attributes = { omit: idAttr } }: BookOpts = bookOpts; 177 | 178 | // cast it to the object version of the option 179 | if (attributes instanceof Array) { 180 | attributes = { include : attributes }; 181 | } 182 | let { omit, include }: AttributesOpt = attributes; 183 | 184 | return filter(attrs, (attr: string) => { 185 | let included: boolean = true; 186 | let omitted: boolean = false; 187 | 188 | if (include) { 189 | included = some(include, (m: AttrMatcher) => matches(m, attr)); 190 | } 191 | 192 | if (omit) { 193 | omitted = some(omit, (m: AttrMatcher) => matches(m, attr)); 194 | } 195 | 196 | // `omit` has more precedence than `include` option 197 | return ! omitted && included; 198 | }); 199 | } 200 | 201 | /** 202 | * Based on Bookshelf options, determine if a relation must be included 203 | */ 204 | function relationAllowed(bookOpts: BookOpts, relName: string): boolean { 205 | let { relations }: BookOpts = bookOpts; 206 | 207 | if (typeof relations === 'boolean') { 208 | return relations; 209 | } else { 210 | let { fields }: RelationOpts = relations; 211 | return ! fields || includes(fields, relName); 212 | } 213 | } 214 | 215 | /** 216 | * Based on Bookshelf options, determine if a relation must be included 217 | */ 218 | function includeAllowed(bookOpts: BookOpts, relName: string): boolean { 219 | let { relations }: BookOpts = bookOpts; 220 | 221 | if (typeof relations === 'boolean') { 222 | return relations; 223 | } else { 224 | let { fields, included }: RelationOpts = relations; 225 | 226 | if (typeof included === 'boolean') { 227 | return included; 228 | } else { 229 | // If included is an array, only allow relations that are in that array 230 | let allowed: string[] = included; 231 | 232 | if (fields) { 233 | // If fields specified, ensure that the included relations 234 | // are listed as one of the relations to be serialized 235 | allowed = intersection(fields, included); 236 | } 237 | 238 | return includes(allowed, relName); 239 | } 240 | } 241 | } 242 | 243 | /** 244 | * Convert a bookshelf model or collection to 245 | * json adding the id attribute if missing 246 | */ 247 | export function toJSON(data: Data): any { 248 | 249 | let json: any = null; 250 | 251 | if (isModel(data)) { 252 | json = data.toJSON({shallow: true}); // serialize without the relations 253 | 254 | // When idAttribute is a composite id, calling .id returns `undefined` 255 | const idAttr: undefined | string | string[] = data.idAttribute; 256 | if (isArray(idAttr)) { 257 | // the id will be the values in order separated by comma 258 | data.id = map(idAttr, (attr: string) => data.attributes[attr]).join(','); 259 | } 260 | 261 | // Assign the id for the model if it's not present already 262 | if (! has(json, 'id')) { 263 | json.id = data.id; 264 | } 265 | 266 | update(json, 'id', toString); 267 | 268 | // Loop over model relations to call toJSON recursively on them 269 | forOwn(data.relations, function (relData: Data, relName: string): void { 270 | json[relName] = toJSON(relData); 271 | }); 272 | 273 | } else if (isCollection(data)) { 274 | // Run a recursive toJSON on each model of the collection 275 | json = data.map(toJSON); 276 | } 277 | 278 | return json; 279 | } 280 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './bookshelf'; 2 | export * from './interfaces'; 3 | export * from './serializer'; 4 | -------------------------------------------------------------------------------- /src/interfaces/common.ts: -------------------------------------------------------------------------------- 1 | import { PagOpts, QueryOpts } from './links'; 2 | import { RelationTypeOpt, RelationOpts } from './relations'; 3 | 4 | //// GENERAL INTERFACES FOR MAPPERS 5 | 6 | // Mapper 7 | export interface Mapper { 8 | map(data: any, type: string, mapOpts?: MapOpts): any; 9 | } 10 | 11 | export type AttrMatcher = RegExp | string; 12 | 13 | export type AttributesOpt = { 14 | omit?: AttrMatcher[], 15 | include?: AttrMatcher[] 16 | }; 17 | 18 | // Mapper Options 19 | export interface MapOpts { 20 | // Attributes-related 21 | attributes?: AttrMatcher[] | AttributesOpt; 22 | keyForAttr?: (attr: string) => string; 23 | 24 | // Relations-related 25 | relations?: boolean | RelationOpts; 26 | typeForModel?: RelationTypeOpt; 27 | 28 | // Links-related 29 | enableLinks?: boolean; 30 | pagination?: PagOpts; 31 | query?: QueryOpts; 32 | 33 | // Meta-related 34 | meta?: { [key:string]: any }; 35 | 36 | // Virtuals-related 37 | outputVirtuals?: boolean; 38 | } 39 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common'; 2 | export * from './links'; 3 | export * from './relations'; 4 | -------------------------------------------------------------------------------- /src/interfaces/links.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Query Parameters map 3 | */ 4 | export interface QueryOpts { 5 | [key: string]: string; 6 | } 7 | 8 | /** 9 | * Pagination variables 10 | */ 11 | export interface PagOpts { 12 | offset: number; 13 | limit: number; 14 | total?: number; 15 | rowCount?: number; 16 | } 17 | 18 | /** 19 | * Data required to form links 20 | */ 21 | export interface LinkOpts { 22 | baseUrl: string; 23 | type: string; 24 | pag?: PagOpts; 25 | query?: QueryOpts; 26 | } 27 | -------------------------------------------------------------------------------- /src/interfaces/relations.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Map to specify type for the passed relations 3 | */ 4 | export interface RelationTypeMap { 5 | [relationName: string]: string; 6 | } 7 | 8 | /** 9 | * Function to pass directly as the typeForAttributes option to the serializer 10 | */ 11 | export type RelationTypeFunction = (attribute: string) => string; 12 | 13 | /** 14 | * The relationTypes option can be a function or an object 15 | */ 16 | export type RelationTypeOpt = RelationTypeMap | RelationTypeFunction; 17 | 18 | /** 19 | * Relationship options 20 | */ 21 | export interface RelationOpts { 22 | included: boolean | string[]; 23 | fields?: string[]; 24 | } 25 | -------------------------------------------------------------------------------- /src/serializer/index.ts: -------------------------------------------------------------------------------- 1 | import * as jas from 'jsonapi-serializer'; 2 | 3 | export type LinkFunc = (primary: any, related?: any, parent?: any) => string; 4 | 5 | export type Link = string | LinkFunc; 6 | 7 | export interface LinkObj { 8 | self?: Link; 9 | related?: Link; 10 | 11 | first?: Link; 12 | last?: Link; 13 | 14 | prev?: Link; 15 | next?: Link; 16 | } 17 | 18 | export interface SerialOpts { 19 | attributes?: string[]; 20 | ref?: string; 21 | included?: boolean; 22 | 23 | topLevelLinks?: LinkObj; 24 | dataLinks?: LinkObj; 25 | relationshipLinks?: LinkObj; 26 | includedLinks?: LinkObj; 27 | 28 | relationshipMeta?: any; 29 | ignoreRelationshipData?: boolean; 30 | 31 | keyForAttribute?: (attribute: any) => string; 32 | typeForAttribute?: (attribute: any) => any; 33 | pluralizeType?: boolean; 34 | 35 | meta?: any; 36 | 37 | // TODO improve type-checking of relationship options 38 | [relationships: string]: any; 39 | } 40 | 41 | export interface SerializerCtor { 42 | new(type: string, opts: SerialOpts): Serializer; 43 | } 44 | 45 | export interface Serializer { 46 | serialize(data: any): any; 47 | } 48 | 49 | // tslint:disable-next-line variable-name 50 | export let Serializer: SerializerCtor = jas.Serializer; 51 | -------------------------------------------------------------------------------- /src/serializer/jsonapi-serializer.skel.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'jsonapi-serializer' { 2 | const content: any; 3 | export = content; 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "lib": ["es6"], 5 | "module": "commonjs", 6 | "outDir": "es5", 7 | "sourceMap": true, 8 | "target": "es5", 9 | "strictNullChecks": true, 10 | "noImplicitAny": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true 13 | }, 14 | "exclude": [ 15 | "node_modules" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.publish.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [ 4 | "src/**/*.ts" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.base.json", 3 | "include": [ 4 | "src/**/*.ts", 5 | "spec/**/*.ts" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "align": [ 4 | true, 5 | "parameters", 6 | "statements" 7 | ], 8 | "ban": false, 9 | "class-name": true, 10 | "comment-format": [ 11 | true, 12 | "check-space" 13 | ], 14 | "curly": true, 15 | "eofline": true, 16 | "forin": true, 17 | "indent": [ 18 | true, 19 | "spaces" 20 | ], 21 | "interface-name": false, 22 | "jsdoc-format": true, 23 | "label-position": true, 24 | "label-undefined": true, 25 | "max-line-length": [ 26 | true, 27 | 140 28 | ], 29 | "member-access": false, 30 | "member-ordering": [ 31 | true, 32 | "public-before-private", 33 | "static-before-instance", 34 | "variables-before-functions" 35 | ], 36 | "no-any": false, 37 | "no-arg": true, 38 | "no-bitwise": true, 39 | "no-angle-bracket-type-assertion": true, 40 | "no-conditional-assignment": true, 41 | "no-consecutive-blank-lines": true, 42 | "no-console": [ 43 | true, 44 | "debug", 45 | "info", 46 | "time", 47 | "timeEnd", 48 | "trace" 49 | ], 50 | "no-construct": true, 51 | "no-constructor-vars": false, 52 | "no-debugger": true, 53 | "no-duplicate-key": true, 54 | "no-duplicate-variable": true, 55 | "no-empty": false, 56 | "no-eval": true, 57 | "no-inferrable-types": false, 58 | "no-internal-module": false, 59 | "no-null-keyword": false, 60 | "no-require-imports": true, 61 | "no-shadowed-variable": true, 62 | "no-string-literal": true, 63 | "no-switch-case-fall-through": true, 64 | "no-trailing-whitespace": true, 65 | "no-unreachable": true, 66 | "no-unused-expression": true, 67 | "no-unused-variable": true, 68 | "no-use-before-declare": true, 69 | "no-var-keyword": true, 70 | "no-var-requires": true, 71 | "object-literal-sort-keys": false, 72 | "one-line": [ 73 | true, 74 | "check-open-brace", 75 | "check-catch", 76 | "check-else", 77 | "check-whitespace" 78 | ], 79 | "quotemark": [ 80 | true, 81 | "single", 82 | "avoid-escape" 83 | ], 84 | "radix": true, 85 | "semicolon": true, 86 | "switch-default": true, 87 | "trailing-comma": [ 88 | true, 89 | { 90 | "multiline": "never", 91 | "singleline": "never" 92 | } 93 | ], 94 | "triple-equals": [ 95 | true, 96 | "allow-null-check" 97 | ], 98 | "typedef": [ 99 | true, 100 | "call-signature", 101 | "parameter", 102 | "arrow-parameter", 103 | "property-declaration", 104 | "variable-declaration", 105 | "member-variable-declaration" 106 | ], 107 | "typedef-whitespace": [ 108 | true, 109 | { 110 | "call-signature": "nospace", 111 | "index-signature": "nospace", 112 | "parameter": "nospace", 113 | "property-declaration": "nospace", 114 | "variable-declaration": "nospace" 115 | }, 116 | { 117 | "call-signature": "space", 118 | "index-signature": "space", 119 | "parameter": "space", 120 | "property-declaration": "space", 121 | "variable-declaration": "space" 122 | } 123 | ], 124 | "use-strict": [ 125 | true, 126 | "check-module" 127 | ], 128 | "variable-name": [ 129 | true, 130 | "check-format", 131 | "allow-leading-underscore", 132 | "ban-keywords" 133 | ], 134 | "whitespace": [ 135 | true, 136 | "check-branch", 137 | "check-decl", 138 | "check-operator", 139 | "check-separator", 140 | "check-type" 141 | ] 142 | } 143 | } 144 | --------------------------------------------------------------------------------