├── .eslintrc ├── .npmignore ├── .babelrc ├── .travis.yml ├── .gitignore ├── webpack.config.js ├── LICENSE ├── package.json ├── CHANGELOG.md ├── README.md ├── src └── redux-object.js └── test └── redux-object.spec.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "parser": "babel-eslint", 4 | "rules": { 5 | "no-param-reassign": 0 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | coverage 2 | demo 3 | docs 4 | src 5 | test 6 | .nyc_output 7 | .babelrc 8 | .eslintrc 9 | .travis.yml 10 | .vscode 11 | .idea 12 | webpack.config.js 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", { 5 | "corejs": { 6 | "version": "3", 7 | "proposals": true 8 | }, 9 | "useBuiltIns": "usage" 10 | } 11 | ] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "14" 4 | - "12" 5 | - "10" 6 | - "node" 7 | env: 8 | - CXX=g++-4.8 9 | before_install: 10 | - sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test 11 | - sudo apt-get -qq update 12 | - sudo apt-get -qq install g++-4.8 13 | install: "NODE_ENV=development yarn install && yarn build" 14 | after_success: "yarn global add coveralls && yarn coverage && cat ./coverage/lcov.info | coveralls" 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | dist 39 | .vscode 40 | 41 | #IDE 42 | .idea 43 | 44 | .DS_Store 45 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const nodeExternals = require('webpack-node-externals'); 4 | const ESLintPlugin = require('eslint-webpack-plugin'); 5 | 6 | module.exports = { 7 | externals: [nodeExternals()], 8 | entry: [ 9 | './src/redux-object' 10 | ], 11 | output: { 12 | path: path.join(__dirname, 'dist'), 13 | filename: 'bundle.js', 14 | libraryTarget: 'commonjs2' 15 | }, 16 | plugins: [ 17 | new webpack.DefinePlugin({ 18 | 'process.env': { 19 | 'NODE_ENV': JSON.stringify('production') 20 | } 21 | }), 22 | new ESLintPlugin() 23 | ], 24 | module: { 25 | rules: [ 26 | { test: /\.js?$/, use: ['babel-loader'], exclude: /node_modules/ } 27 | ] 28 | }, 29 | resolve: { 30 | modules: [ 31 | path.join(__dirname, 'src'), 32 | 'node_modules' 33 | ] 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Yury Dymov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-object", 3 | "version": "1.0.1", 4 | "description": "Builds complex JS object from normalized redux store. Best works with json-api-normalizer", 5 | "main": "dist/bundle.min.js", 6 | "scripts": { 7 | "build": "cross-env NODE_ENV=production webpack --output-filename bundle.min.js", 8 | "clean": "rimraf dist coverage lib", 9 | "coverage": "nyc _mocha --require @babel/register", 10 | "lint": "eslint src --ext .js", 11 | "test": "mocha --require @babel/register" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/yury-dymov/redux-object.git" 16 | }, 17 | "keywords": [ 18 | "redux", 19 | "normalizr", 20 | "JSON", 21 | "API" 22 | ], 23 | "author": "Yury Dymov", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/yury-dymov/redux-object/issues" 27 | }, 28 | "dependencies": { 29 | "core-js": "3" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.2.2", 33 | "@babel/preset-env": "^7.13", 34 | "@babel/register": "^7.0.0", 35 | "babel-eslint": "^10.0.1", 36 | "babel-loader": "^8.0.0", 37 | "chai": "^4.2.0", 38 | "cross-env": "^5.2.0", 39 | "eslint": "^7.14.0", 40 | "eslint-config-airbnb": "^17.1.0", 41 | "eslint-plugin-import": "^2.14.0", 42 | "eslint-plugin-jsx-a11y": "^6.1.2", 43 | "eslint-plugin-react": "^7.11.1", 44 | "eslint-webpack-plugin": "^2.4.1", 45 | "immutable": "^4.0.0-rc.12", 46 | "lodash": "^4.17.5", 47 | "mocha": "^8.2.1", 48 | "nyc": "^14.1.1", 49 | "rimraf": "^2.6.2", 50 | "webpack": "^5.9.0", 51 | "webpack-cli": "^4.2.0", 52 | "webpack-node-externals": "^2.5.2" 53 | }, 54 | "browserslist": [ 55 | "edge 16", 56 | "safari 9", 57 | "firefox 57", 58 | "ie 11", 59 | "ios 9", 60 | "chrome 49" 61 | ], 62 | "homepage": "https://github.com/yury-dymov/redux-object#readme" 63 | } 64 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ### Version 1.0.1 (30th April 2021) 2 | IE 11 backward compatibility support via core-js 3 polyfilling 3 | 4 | ### Version 1.0.0 (2nd December 2020) 5 | Deps update 6 | 7 | ### Version 0.5.10 (17th June 2019) 8 | Updated vulnerable deps 9 | 10 | ### Version 0.5.9 (17th January 2019) 11 | Deps update; istanbul -> nyc for code coverage reporting 12 | 13 | ### Version 0.5.8 (16th January 2019) 14 | Introduced `resolved` property (https://github.com/yury-dymov/redux-object/pull/38) 15 | 16 | ### Version 0.5.7 (16th November 2018) 17 | Added support for links (https://github.com/yury-dymov/redux-object/pull/37). Updated vulnerable deps 18 | 19 | ### Version 0.5.6 (29th March 2018) 20 | Added support for immutable.js (https://github.com/yury-dymov/redux-object/pull/34) 21 | 22 | ### Version 0.5.5 (2nd January 2018) 23 | Fixes bug, when object doesn't have any attributes (https://github.com/yury-dymov/redux-object/pull/32) 24 | 25 | ### Version 0.5.4 (5th November 2017) 26 | Private cache attributes are not enumerable for object returned by `build` (https://github.com/yury-dymov/redux-object/pull/31) 27 | 28 | ### Version 0.5.3 (3d November 2017) 29 | Object properties are enumerable (https://github.com/yury-dymov/redux-object/pull/25) 30 | 31 | ### Version 0.5.2 (25th September 2017) 32 | Added 'meta' support per spec (https://github.com/yury-dymov/redux-object/issues/22) 33 | 34 | ### Version 0.5.1 (12th September 2017) 35 | Fixed returning empty array for relationships without data when ignoreLinks is true (https://github.com/yury-dymov/redux-object/issues/20) 36 | 37 | ### Version 0.5.0 (13th July 2017) 38 | Accessing related object not in the store returning id instead of null if related object is not loaded but id is available (https://github.com/yury-dymov/redux-object/issues/18) 39 | 40 | ### Version 0.4.5 (09th July 2017) 41 | Type is optionally propogated to objects (https://github.com/yury-dymov/redux-object/issues/16) 42 | 43 | ### Version 0.4.4 (26th May 2017) 44 | Added unminified version for development https://github.com/yury-dymov/redux-object/issues/15 45 | 46 | ### Version 0.4.2 (6th May 2017) 47 | Removed `lodash` from dependencies 48 | 49 | ### Version 0.4.1 (5th May 2017) 50 | Added options to disable lazy loading behavior for relationships and to suppress throwing error for remote relationships 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redux-object 2 | 3 | [![npm version](https://img.shields.io/npm/v/redux-object.svg?style=flat)](https://www.npmjs.com/package/redux-object) 4 | [![Downloads](http://img.shields.io/npm/dm/redux-object.svg?style=flat-square)](https://npmjs.org/package/redux-object) 5 | [![Build Status](https://img.shields.io/travis/yury-dymov/redux-object/master.svg?style=flat)](https://travis-ci.org/yury-dymov/redux-object) 6 | [![Coverage Status](https://coveralls.io/repos/github/yury-dymov/redux-object/badge.svg?branch=master)](https://coveralls.io/github/yury-dymov/redux-object?branch=master) 7 | 8 | Builds complex JS object from normalized redux store. Best works with [json-api-normalizer](https://github.com/yury-dymov/json-api-normalizer). 9 | 10 | DEMO - [https://yury-dymov.github.io/json-api-react-redux-example/](https://yury-dymov.github.io/json-api-react-redux-example/) 11 | 12 | Demo sources and description - [https://github.com/yury-dymov/json-api-react-redux-example](https://github.com/yury-dymov/json-api-react-redux-example) 13 | 14 | # API 15 | Library provides `build` function, which takes 4 parameters: redux state part, object type, ID or an array of IDs or null, and options. 16 | 17 | If ID is provided in a form of array, multiple objects are fetched. If ID is null, all objects of selected type are fetched. 18 | 19 | | Option | Default | Description | 20 | |:--------|:---------------:|:-------------| 21 | | eager | false | Controls lazy loading for the child relationship objects. By default, lazy loading is enabled. | 22 | | ignoreLinks | false | redux-object doesn't support remote objects. This option suppresses the exception thrown in case user accesses a property, which is not loaded to redux store yet. | 23 | | includeType | false | Include the record type as a property 'type' on each result. This is particularly useful for identifying the record type returned by a polymorphic relationship. | 24 | 25 | 26 | ```JavaScript 27 | import build from 'redux-object'; 28 | 29 | /* 30 | state: 31 | { 32 | data: { 33 | post: { 34 | "2620": { 35 | attributes: { 36 | "text": "hello", 37 | "id": 2620 38 | }, 39 | relationships: { 40 | daQuestion: { 41 | id: "295", 42 | type: "question" 43 | }, 44 | liker: [{ 45 | id: "1", 46 | type: "user" 47 | }, { 48 | id: "2", 49 | type: "user", 50 | }, { 51 | id: "3", 52 | type: "user" 53 | } 54 | ], 55 | comments: [] 56 | } 57 | } 58 | }, 59 | question: { 60 | "295": { 61 | attributes: { 62 | text: "hello?" 63 | } 64 | } 65 | }, 66 | user: { 67 | "1": { 68 | attributes: { 69 | id: 1, 70 | name: "Alice" 71 | } 72 | }, 73 | "2": { 74 | attributes: { 75 | id: 2, 76 | name: "Bob" 77 | } 78 | }, 79 | "3": { 80 | attributes: { 81 | id: 3, 82 | text: "Jenny" 83 | } 84 | } 85 | }, 86 | meta: { 87 | 'posts/me': { 88 | data: { 89 | post: '2620' 90 | } 91 | } 92 | } 93 | } 94 | }; 95 | */ 96 | 97 | const post = build(state.data, 'post', '2620'); 98 | 99 | console.log(post.id); // -> 2620 100 | console.log(post.text); // -> hello 101 | console.log(post.daQuestion); // -> { id: 295, text: "hello?" } 102 | console.log(post.liker.length); //-> 3 103 | console.log(post.liker[0]); // -> { id: 1, name: "Alice" } 104 | 105 | // Other examples 106 | 107 | const post = build(state.data, 'post', '2620', { eager: true }); 108 | const post = build(state.data, 'post', '2620', { eager: false, ignoreLinks: true }); 109 | ``` 110 | 111 | Child objects are lazy loaded unless eager option is explicitly provided. 112 | 113 | # License 114 | MIT (c) Yury Dymov 115 | -------------------------------------------------------------------------------- /src/redux-object.js: -------------------------------------------------------------------------------- 1 | /* eslint no-use-before-define: [1, 'nofunc'] */ 2 | 3 | // Immutable helpers 4 | 5 | function isImmutable(object) { 6 | return !!( 7 | object 8 | && typeof object.hasOwnProperty === 'function' 9 | && (object.hasOwnProperty('__ownerID') // eslint-disable-line 10 | || (object._map && object._map.hasOwnProperty('__ownerID'))) // eslint-disable-line 11 | ); 12 | } 13 | 14 | function getProperty(object, property, toJS = false) { 15 | if (!Array.isArray(property)) { 16 | property = [property]; 17 | } 18 | if (isImmutable(object)) { 19 | const res = object.getIn(property.map(p => `${p}`)); // Immutable maps cast keys to strings 20 | return (toJS && res) ? res.toJS() : res; 21 | } 22 | return property.reduce((previous, current) => previous[current], object); 23 | } 24 | 25 | function getKeys(object) { 26 | return isImmutable(object) ? object.keySeq().toArray() : Object.keys(object); 27 | } 28 | 29 | // build helpers 30 | 31 | function uniqueId(objectName, id) { 32 | if (!id) { 33 | return null; 34 | } 35 | 36 | return `${objectName}${id}`; 37 | } 38 | 39 | function buildRelationship(reducer, target, relationship, options, cache) { 40 | const { ignoreLinks } = options; 41 | const rel = target.relationships[relationship]; 42 | 43 | if (typeof rel.data !== 'undefined') { 44 | if (Array.isArray(rel.data)) { 45 | return rel.data.map(child => build(reducer, child.type, child.id, options, cache) || child); 46 | } 47 | 48 | if (rel.data === null) { 49 | return null; 50 | } 51 | 52 | return build(reducer, rel.data.type, rel.data.id, options, cache) || rel.data; 53 | } 54 | 55 | if (!ignoreLinks && rel.links) { 56 | throw new Error('Remote lazy loading is not supported (see: https://github.com/yury-dymov/json-api-normalizer/issues/2). To disable this error, include option \'ignoreLinks: true\' in the build function like so: build(reducer, type, id, { ignoreLinks: true })'); 57 | } 58 | 59 | return undefined; 60 | } 61 | 62 | 63 | export default function build(reducer, objectName, id = null, providedOpts = {}, cache = {}) { 64 | const defOpts = { eager: false, ignoreLinks: false, includeType: false }; 65 | const options = Object.assign({}, defOpts, providedOpts); 66 | const { eager, includeType } = options; 67 | 68 | if (!getProperty(reducer, objectName)) { 69 | return null; 70 | } 71 | 72 | if (id === null || Array.isArray(id)) { 73 | const idList = id || getKeys(getProperty(reducer, objectName)); 74 | 75 | return idList.map(e => build(reducer, objectName, e, options, cache)); 76 | } 77 | 78 | const ids = id.toString(); 79 | const uuid = uniqueId(objectName, ids); 80 | const cachedObject = cache[uuid]; 81 | 82 | if (cachedObject) { 83 | return cachedObject; 84 | } 85 | 86 | const ret = {}; 87 | const target = getProperty(reducer, [objectName, ids], true); 88 | 89 | if (!target) { 90 | return null; 91 | } 92 | 93 | if (target.id) { 94 | ret.id = target.id; 95 | } 96 | 97 | if (target.attributes) { 98 | Object.keys(target.attributes).forEach((key) => { ret[key] = target.attributes[key]; }); 99 | Object.defineProperty(ret, 'resolved', { value: true }); 100 | } 101 | 102 | if (target.meta) { 103 | ret.meta = target.meta; 104 | } 105 | 106 | if (target.links) { 107 | ret.links = target.links; 108 | } 109 | 110 | if (includeType && !ret.type) { 111 | ret.type = objectName; 112 | } 113 | 114 | cache[uuid] = ret; 115 | 116 | if (target.relationships) { 117 | Object.keys(target.relationships).forEach((relationship) => { 118 | if (eager) { 119 | ret[relationship] = buildRelationship(reducer, target, relationship, options, cache); 120 | } else { 121 | Object.defineProperty( 122 | ret, 123 | relationship, 124 | { 125 | enumerable: true, 126 | get: () => { 127 | const field = `__${relationship}`; 128 | 129 | if (ret[field]) { 130 | return ret[field]; 131 | } 132 | 133 | const value = buildRelationship(reducer, target, relationship, options, cache); 134 | Object.defineProperty(ret, field, { enumerable: false, value }); 135 | 136 | return ret[field]; 137 | }, 138 | }, 139 | ); 140 | } 141 | }); 142 | } 143 | 144 | if (typeof ret.id === 'undefined') { 145 | ret.id = ids; 146 | } 147 | 148 | return ret; 149 | } 150 | -------------------------------------------------------------------------------- /test/redux-object.spec.js: -------------------------------------------------------------------------------- 1 | import { expect } from 'chai'; 2 | import { fromJS } from 'immutable'; 3 | import isEqual from 'lodash/isEqual'; 4 | import cloneDeep from 'lodash/cloneDeep'; 5 | import build from '../dist/bundle.min'; 6 | 7 | const json = { 8 | post: { 9 | "2620": { 10 | id: 2620, 11 | attributes: { 12 | "text": "hello", 13 | }, 14 | links: { 15 | "self": "http://example.com/posts/2620" 16 | }, 17 | meta: { 18 | 'like-count': 49 19 | }, 20 | relationships: { 21 | daQuestion: { 22 | data: { 23 | id: "295", 24 | type: "question" 25 | } 26 | }, 27 | missingRelationship: { 28 | data: { 29 | id: "296", 30 | type: "question" 31 | } 32 | }, 33 | missingAndPresent: { 34 | data: [{ 35 | id: "295", 36 | type: "question" 37 | }, { 38 | id: "296", 39 | type: "question" 40 | }] 41 | }, 42 | liker: { 43 | data: [{ 44 | id: "1", 45 | type: "user" 46 | }, { 47 | id: "2", 48 | type: "user" 49 | }, { 50 | id: "3", 51 | type: "user" 52 | }] 53 | }, 54 | comments: { 55 | data: [] 56 | }, 57 | author: { 58 | data: null 59 | } 60 | 61 | } 62 | } 63 | }, 64 | question: { 65 | "295": { 66 | attributes: { 67 | text: "hello?" 68 | }, 69 | relationships: { 70 | posts: { 71 | data: [{ 72 | id: "2620", 73 | type: "post" 74 | }] 75 | } 76 | } 77 | } 78 | }, 79 | user: { 80 | "1": { 81 | attributes: { 82 | id: 1, 83 | text: "hello?" 84 | } 85 | }, 86 | "2": { 87 | attributes: { 88 | text: "hello?" 89 | } 90 | }, 91 | "3": { 92 | attributes: { 93 | text: "hello?" 94 | } 95 | }, 96 | "4": { 97 | attributes: {} 98 | } 99 | }, 100 | meta: { 101 | 'posts/me': { 102 | data: { 103 | post: '2620' 104 | } 105 | } 106 | } 107 | }; 108 | 109 | const runTests = (jsonFunc, description, allowMutations = true) => { 110 | describe(description, () => { 111 | describe('build single object', () => { 112 | const local = jsonFunc(json); 113 | const object = build(local, 'post', 2620); 114 | 115 | it('attributes', () => { 116 | expect(object.text).to.be.equal('hello'); 117 | }); 118 | 119 | it('resource meta', () => { 120 | expect(object.meta['like-count']).to.be.equal(49); 121 | }); 122 | 123 | it('resource links', () => { 124 | expect(object.links.self).to.be.equal('http://example.com/posts/2620'); 125 | }); 126 | 127 | it('many relationships', () => { 128 | expect(object.liker.length).to.be.equal(3); 129 | }); 130 | 131 | it('relationship', () => { 132 | expect(object.liker[0].text).to.be.equal('hello?'); 133 | }); 134 | 135 | if (allowMutations) { 136 | it('caching works', () => { 137 | local.question[295].attributes.text = 'Goodbye.'; 138 | expect(object.daQuestion.text).to.be.equal('Goodbye.'); 139 | local.question[295].attributes.text = 'See ya.'; 140 | expect(object.daQuestion.text).to.be.equal('Goodbye.'); 141 | }); 142 | } else { 143 | // Todo 144 | } 145 | 146 | it('works for empty array relationship', () => { 147 | expect(isEqual(object.comments, [])).to.be.true; 148 | }); 149 | 150 | it('works for empty relationship', () => { 151 | expect(isEqual(object.author, null)).to.be.true; 152 | }); 153 | 154 | it('id in target is not overwritten by merge', () => { 155 | expect(object.id).to.be.equal(2620); 156 | }); 157 | 158 | it('assigns correct id when in attribute', () => { 159 | expect(object.daQuestion.id).to.be.equal('295'); 160 | }); 161 | 162 | it('id in attributes is not overwritten by merge', () => { 163 | const user = build(local, 'user', 1); 164 | 165 | expect(user.id).to.be.equal(1); // and not '1' 166 | }); 167 | 168 | it('attribute is enumerable', () => { 169 | expect(object.propertyIsEnumerable('text')).to.be.true; 170 | }); 171 | 172 | it('relationship attribute is enumerable', () => { 173 | expect(object.liker[0].propertyIsEnumerable('text')).to.be.true; 174 | }); 175 | 176 | it('only enumerates public attributes', () => { 177 | expect(Object.keys(object).sort()).to.deep.equal([ 178 | 'author', 179 | 'comments', 180 | 'daQuestion', 181 | 'id', 182 | 'liker', 183 | 'links', 184 | 'meta', 185 | 'missingAndPresent', 186 | 'missingRelationship', 187 | 'text', 188 | ]); 189 | }); 190 | 191 | it('missing object should return null', () => { 192 | const user = build(local, 'user', 30); 193 | 194 | expect(user).to.be.equal(null); 195 | }); 196 | 197 | it('missing relationship should return the relationship object', () => { 198 | expect(object.missingRelationship).to.deep.equal({ id: '296', type: 'question' }); 199 | }); 200 | 201 | it('missing array relationship should return the relationship data array', () => { 202 | expect(object.missingAndPresent).to.deep.equal([ 203 | object.daQuestion, 204 | { id: '296', type: 'question' } 205 | ]); 206 | }); 207 | 208 | it('object with no attributes still should be an object', () => { 209 | const user = build(local, 'user', 4); 210 | const target = {id: "4"}; 211 | 212 | expect(user).to.be.eql(target); 213 | }); 214 | }); 215 | 216 | describe('build all objects in collection', () => { 217 | const local = jsonFunc(json); 218 | const list = build(local, 'user'); 219 | 220 | it('returns an array', () => { 221 | expect(list).to.be.instanceOf(Array); 222 | }); 223 | 224 | it('returns all items', () => { 225 | expect(list.length).to.be.equal(4); 226 | }); 227 | 228 | it('includes attributes', () => { 229 | expect(list[0].text).to.be.equal('hello?'); 230 | }); 231 | }); 232 | 233 | describe('build a specific list of objects in collection', () => { 234 | const local = jsonFunc(json); 235 | const list = build(local, 'user', [2, 4]); 236 | 237 | it('returns an array', () => { 238 | expect(list).to.be.instanceOf(Array); 239 | }); 240 | 241 | it('returns only selected items', () => { 242 | expect(list.length).to.be.equal(2); 243 | expect(list[0].id).to.be.equal('2'); 244 | expect(list[1].id).to.be.equal('4'); 245 | }); 246 | 247 | it('includes attributes', () => { 248 | expect(list[0].text).to.be.equal('hello?'); 249 | }); 250 | 251 | it('returns a null result for requested items which are not present', () => { 252 | const extra = build(local, 'user', [2, 4, 5]); 253 | expect(extra.length).to.be.equal(3); 254 | expect(extra[2]).to.be.null; 255 | }); 256 | }); 257 | 258 | describe('local eager loading', () => { 259 | const local = jsonFunc(json); 260 | const object = build(local, 'post', 2620, { eager: true }); 261 | 262 | if (allowMutations) { 263 | it('does not use lazy loading', () => { 264 | local.question[295].attributes.text = 'Goodbye.'; 265 | expect(object.daQuestion.text).to.be.equal('hello?'); 266 | }); 267 | } else { 268 | // Todo 269 | } 270 | 271 | it('should work with cycle dependencies', () => { 272 | expect(object.text).to.be.equal('hello'); 273 | expect(object.daQuestion.posts[0].daQuestion.posts[0].text).to.be.equal('hello'); 274 | }); 275 | }); 276 | 277 | describe('remote lazy loading', () => { 278 | const source = { 279 | question: { 280 | "29": { 281 | attributes: { 282 | yday: 228, 283 | text: "Какие качества Вы больше всего цените в женщинах?", 284 | slug: "tbd", 285 | id: 29 286 | }, 287 | relationships: { 288 | movie: { 289 | links: { 290 | "self": "http://localhost:3000/api/v1/actor/1c9d234b-66c4-411e-b785-955d57db5536/relationships/movie", 291 | "related": "http://localhost:3000/api/v1/actor/1c9d234b-66c4-411e-b785-955d57db5536/movie" 292 | } 293 | } 294 | } 295 | } 296 | } 297 | }; 298 | 299 | const sourceWithData = { 300 | question: { 301 | "29": { 302 | attributes: { 303 | yday: 228, 304 | text: "Какие качества Вы больше всего цените в женщинах?", 305 | slug: "tbd", 306 | id: 29 307 | }, 308 | relationships: { 309 | movie: { 310 | data: [], 311 | links: { 312 | "self": "http://localhost:3000/api/v1/actor/1c9d234b-66c4-411e-b785-955d57db5536/relationships/movie", 313 | "related": "http://localhost:3000/api/v1/actor/1c9d234b-66c4-411e-b785-955d57db5536/movie" 314 | } 315 | } 316 | } 317 | } 318 | } 319 | }; 320 | 321 | it('should throw exception', () => { 322 | const question = build(source, 'question', 29); 323 | 324 | try { 325 | question.movie; 326 | } catch (er) { 327 | return expect(er.message).not.to.be.null; 328 | } 329 | 330 | throw new Error('test failed'); 331 | }); 332 | 333 | it('should not throw exception', () => { 334 | const question = build(sourceWithData, 'question', 29); 335 | return expect(isEqual(question.movie, [])).to.be.true; 336 | }); 337 | 338 | describe('with ignoreLinks=true', () => { 339 | it('should ignore remote lazy loading links', () => { 340 | const question = build(sourceWithData, 'question', 29, { eager: false, ignoreLinks: true }); 341 | return expect(isEqual(question.movie, [])).to.be.true; 342 | }); 343 | 344 | it('returns undefined if data was undefined', () => { 345 | const question = build(source, 'question', 29, { eager: false, ignoreLinks: true }); 346 | return expect(isEqual(question.movie, undefined)).to.be.true; 347 | }); 348 | }); 349 | 350 | }); 351 | 352 | describe('Include object type', () => { 353 | const local = jsonFunc(json); 354 | const object = build(local, 'post', 2620, { includeType: true }); 355 | 356 | it('should include object type on base', () => { 357 | expect(object.type).to.be.equal('post'); 358 | }); 359 | 360 | it('should include object type on relationships', () => { 361 | expect(object.daQuestion.type).to.be.equal('question'); 362 | }); 363 | }); 364 | }); 365 | } 366 | 367 | runTests(cloneDeep, 'From Plain JS Object'); 368 | runTests(fromJS, 'From ImmutableJS Object', false); 369 | --------------------------------------------------------------------------------