├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── SECURITY.md ├── cjs └── object-path-immutable.js ├── esm └── object-path-immutable.js ├── object-path-immutable.d.ts ├── package-lock.json ├── package.json ├── rollup.config.js ├── src └── object-path-immutable.js ├── test └── test.js └── umd └── object-path-immutable.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .settings 3 | .idea 4 | npm-debug.log 5 | generated 6 | coverage 7 | .DS_Store 8 | .history 9 | .nyc_output 10 | .vscode 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "10" 5 | - "12" 6 | - "14" 7 | script: npm run coveralls 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | =========== 3 | 4 | ### 4.0 5 | 6 | - **Breaking change**: the previous default export is now called `wrap()` 7 | - **Possible breaking change**: `object-path-immutable` now uses ES modules which means if you are in an ESM environment you will have to use named exports. 8 | 9 | ### 3.0 10 | 11 | - **Possible breaking change** `merge` not does not accept options anymore 12 | - Removed dependency on `deepmerge` 13 | 14 | ### 2.0 15 | 16 | - **Possible breaking change** The library now has dependencies and is building with Rollup, therefore the UMD entry point is now `dist/object-path-immutable`. 17 | If you are using this `object-path-immutable` with Node or another module bundler, this change should not affect you. 18 | - Added `merge` function 19 | 20 | ### 1.0 21 | 22 | - **Breaking change**: The way the library handles empty paths has changed. Before this change,all the methods were returning the original object. The new behavior is as follows. 23 | - `set(src, path, value)`: `value` is returned 24 | - `update(src, path, updater)`: `value` will be passed to `updater()` and the result returned 25 | - `set(src, path, ...values)`: `values` will be concatenated to `src` if `src` is an array, otherwise `values` will be returned 26 | - `insert(src, path, value, at)`: if `src` is an array then it will be cloned and `value` will be inserted at `at`, otherwise `[value]` will be returned 27 | - `del(src, path)`: returns `undefined` 28 | - `assign(src, path, target)`: Target is assigned to a clone of `src` and returned 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Mario Casciaro 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![build](https://img.shields.io/travis/mariocasciaro/object-path-immutable.svg?style=flat-square)](https://travis-ci.org/mariocasciaro/object-path-immutable) 2 | [![coverage](https://img.shields.io/coveralls/mariocasciaro/object-path-immutable.svg?style=flat-square)](https://coveralls.io/r/mariocasciaro/object-path-immutable) 3 | [![downloads](https://img.shields.io/npm/dm/object-path-immutable.svg?style=flat-square)](https://www.npmjs.com/package/object-path-immutable) 4 | [![version](https://img.shields.io/npm/v/object-path-immutable.svg?style=flat-square)](https://www.npmjs.com/package/object-path-immutable) 5 | [![deps](https://img.shields.io/david/mariocasciaro/object-path-immutable.svg?style=flat-square)](https://david-dm.org/mariocasciaro/object-path-immutable) 6 | [![devdeps](https://img.shields.io/david/dev/mariocasciaro/object-path-immutable.svg?style=flat-square)](https://david-dm.org/mariocasciaro/object-path-immutable#info=devDependencies) 7 | 8 | object-path-immutable 9 | =========== 10 | 11 | Tiny JS library to modify deep object properties without modifying the original object (immutability). 12 | Works great with React (especially when using `setState()`) and Redux (inside a reducer). 13 | 14 | This can be seen as a simpler and more intuitive alternative to the *React Immutability Helpers* and *Immutable.js*. 15 | 16 | ## Changelog 17 | 18 | [View Changelog](CHANGELOG.md) 19 | 20 | ## Install 21 | 22 | npm install object-path-immutable --save 23 | 24 | ## Quick usage 25 | 26 | The following, sets a property without modifying the original object. 27 | It will minimize the number of clones down the line. The resulting object is just a plain JS object literal, 28 | so be warned that it will not be protected against property mutations (like `Immutable.js`) 29 | 30 | ```javascript 31 | const obj = { 32 | a: { 33 | b: 'c', 34 | c: ['d', 'f'] 35 | } 36 | } 37 | 38 | const newObj = immutable.set(obj, 'a.b', 'f') 39 | // { 40 | // a: { 41 | // b: 'f', 42 | // c: ['d', 'f'] 43 | // } 44 | // } 45 | 46 | // obj !== newObj 47 | // obj.a !== newObj.a 48 | // obj.a.b !== newObj.a.b 49 | 50 | // However: 51 | // obj.a.c === newObj.a.c 52 | ``` 53 | 54 | ### Wrap mode 55 | 56 | You can also chain the api's and call `value()` at the end to retrieve the resulting object. 57 | 58 | ```javascript 59 | const newObj = immutable.wrap(obj).set('a.b', 'f').del('a.c.0').value() 60 | ``` 61 | 62 | ## API 63 | 64 | ```javascript 65 | // Premises 66 | 67 | const obj = { 68 | a: { 69 | b: 'c', 70 | c: ['d', 'f'] 71 | } 72 | } 73 | 74 | import * as immutable from 'object-path-immutable' 75 | ``` 76 | 77 | #### set (initialObject, path, value) 78 | 79 | Changes an object property. 80 | 81 | - Path can be either a string or an array. 82 | 83 | ```javascript 84 | const newObj1 = immutable.set(obj, 'a.b', 'f') 85 | const newObj2 = immutable.set(obj, ['a', 'b'], 'f') 86 | 87 | // { 88 | // a: { 89 | // b: 'f', 90 | // c: ['d', 'f'] 91 | // } 92 | // } 93 | 94 | // Note that if the path is specified as a string, numbers are automatically interpreted as array indexes. 95 | 96 | const newObj = immutable.set(obj, 'a.c.1', 'fooo') 97 | // { 98 | // a: { 99 | // b: 'f', 100 | // c: ['d', 'fooo'] 101 | // } 102 | // } 103 | ``` 104 | 105 | #### update (initialObject, path, updater) 106 | 107 | Updates an object property. 108 | 109 | ```javascript 110 | const obj = { 111 | a: { 112 | b: 1 113 | } 114 | } 115 | 116 | const newObj = immutable.update(obj, ['a', 'b'], v => v + 1) 117 | 118 | // { 119 | // a: { 120 | // b: 2, 121 | // } 122 | // } 123 | ``` 124 | 125 | #### push (initialObject, path, value) 126 | 127 | Push into a deep array (it will create intermediate objects/arrays if necessary). 128 | 129 | ```javascript 130 | const newObj = immutable.push(obj, 'a.d', 'f') 131 | // { 132 | // a: { 133 | // b: 'f', 134 | // c: ['d', 'f'], 135 | // d: ['f'] 136 | // } 137 | // } 138 | ``` 139 | 140 | #### del (initialObject, path) 141 | 142 | Deletes a property. 143 | 144 | ```javascript 145 | const newObj = immutable.del(obj, 'a.c') 146 | // { 147 | // a: { 148 | // b: 'f' 149 | // } 150 | // } 151 | ``` 152 | 153 | Can also delete a deep array item using splice 154 | 155 | ```javascript 156 | const newObj = immutable.del(obj, 'a.c.0') 157 | // { 158 | // a: { 159 | // b: 'f', 160 | // c: ['f'] 161 | // } 162 | // } 163 | ``` 164 | 165 | #### assign (initialObject, path, payload) 166 | 167 | Shallow copy properties. 168 | 169 | ```javascript 170 | const newObj = immutable.assign(obj, 'a', { b: 'f', g: 'h' }) 171 | // { 172 | // a: { 173 | // b: 'f', 174 | // c: ['d, 'f'], 175 | // g: 'h' 176 | // } 177 | // } 178 | ``` 179 | 180 | #### insert (initialObject, path, payload, position) 181 | 182 | Insert property at the specific array index. 183 | 184 | ```javascript 185 | const newObj = immutable.insert(obj, 'a.c', 'k', 1) 186 | // var obj = { 187 | // a: { 188 | // b: 'c', 189 | // c: ['d, 'k' 'f'], 190 | // } 191 | // } 192 | ``` 193 | 194 | 195 | #### merge (initialObject, path, value) 196 | 197 | Deep merge properties. 198 | 199 | ```javascript 200 | const newObj = immutable.merge(obj, 'a.c', {b: 'd'}) 201 | ``` 202 | 203 | ### Getters (not available in wrap mode) 204 | 205 | #### get (object, path, defaultValue) 206 | 207 | Retrieve a deep object property. Imported from [object-path](https://github.com/mariocasciaro/object-path) for convenience. 208 | 209 | ## Equivalent library with side effects 210 | 211 | [object-path](https://github.com/mariocasciaro/object-path) 212 | 213 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | 2 | Reporting a security issue 3 | =========== 4 | 5 | Please report any suspected security vulnerabilities responsibly to protect the users of this package. Try not share them publicly before the issue is confirmed and a fix is produced. 6 | 7 | Send us an email at report @ mario.fyi to privately report any security vulnerability to us. 8 | -------------------------------------------------------------------------------- /cjs/object-path-immutable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } 4 | 5 | var isPlainObject = require('is-plain-object'); 6 | var op = _interopDefault(require('object-path')); 7 | 8 | var _hasOwnProperty = Object.prototype.hasOwnProperty; 9 | 10 | function isEmpty (value) { 11 | if (isNumber(value)) { 12 | return false 13 | } 14 | if (!value) { 15 | return true 16 | } 17 | if (isArray(value) && value.length === 0) { 18 | return true 19 | } else if (!isString(value)) { 20 | for (var i in value) { 21 | if (_hasOwnProperty.call(value, i)) { 22 | return false 23 | } 24 | } 25 | return true 26 | } 27 | return false 28 | } 29 | 30 | function isNumber (value) { 31 | return typeof value === 'number' 32 | } 33 | 34 | function isString (obj) { 35 | return typeof obj === 'string' 36 | } 37 | 38 | function isArray (obj) { 39 | return Array.isArray(obj) 40 | } 41 | 42 | function assignToObj (target, source) { 43 | for (var key in source) { 44 | if (_hasOwnProperty.call(source, key)) { 45 | target[key] = source[key]; 46 | } 47 | } 48 | return target 49 | } 50 | 51 | function getKey (key) { 52 | var intKey = parseInt(key); 53 | if (intKey.toString() === key) { 54 | return intKey 55 | } 56 | return key 57 | } 58 | 59 | function clone (obj, createIfEmpty, assumeArray) { 60 | if (obj == null) { 61 | if (createIfEmpty) { 62 | if (assumeArray) { 63 | return [] 64 | } 65 | 66 | return {} 67 | } 68 | 69 | return obj 70 | } else if (isArray(obj)) { 71 | return obj.slice() 72 | } 73 | 74 | return assignToObj({}, obj) 75 | } 76 | 77 | function _deepMerge (dest, src) { 78 | if (dest !== src && isPlainObject.isPlainObject(dest) && isPlainObject.isPlainObject(src)) { 79 | var merged = {}; 80 | for (var key in dest) { 81 | if (_hasOwnProperty.call(dest, key)) { 82 | if (_hasOwnProperty.call(src, key)) { 83 | merged[key] = _deepMerge(dest[key], src[key]); 84 | } else { 85 | merged[key] = dest[key]; 86 | } 87 | } 88 | } 89 | 90 | for (key in src) { 91 | if (_hasOwnProperty.call(src, key)) { 92 | merged[key] = _deepMerge(dest[key], src[key]); 93 | } 94 | } 95 | return merged 96 | } 97 | return src 98 | } 99 | 100 | function _changeImmutable (dest, src, path, changeCallback) { 101 | if (isNumber(path)) { 102 | path = [path]; 103 | } 104 | if (isEmpty(path)) { 105 | return src 106 | } 107 | if (isString(path)) { 108 | return _changeImmutable(dest, src, path.split('.').map(getKey), changeCallback) 109 | } 110 | var currentPath = path[0]; 111 | 112 | if (!dest || dest === src) { 113 | dest = clone(src, true, isNumber(currentPath)); 114 | } 115 | 116 | if (path.length === 1) { 117 | return changeCallback(dest, currentPath) 118 | } 119 | 120 | if (src != null) { 121 | src = src[currentPath]; 122 | } 123 | 124 | dest[currentPath] = _changeImmutable(dest[currentPath], src, path.slice(1), changeCallback); 125 | 126 | return dest 127 | } 128 | 129 | var api = {}; 130 | api.set = function set (dest, src, path, value) { 131 | if (isEmpty(path)) { 132 | return value 133 | } 134 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 135 | clonedObj[finalPath] = value; 136 | return clonedObj 137 | }) 138 | }; 139 | 140 | api.update = function update (dest, src, path, updater) { 141 | if (isEmpty(path)) { 142 | return updater(clone(src)) 143 | } 144 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 145 | clonedObj[finalPath] = updater(clonedObj[finalPath]); 146 | return clonedObj 147 | }) 148 | }; 149 | 150 | api.push = function push (dest, src, path /*, values */) { 151 | var values = Array.prototype.slice.call(arguments, 3); 152 | if (isEmpty(path)) { 153 | if (!isArray(src)) { 154 | return values 155 | } else { 156 | return src.concat(values) 157 | } 158 | } 159 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 160 | if (!isArray(clonedObj[finalPath])) { 161 | clonedObj[finalPath] = values; 162 | } else { 163 | clonedObj[finalPath] = clonedObj[finalPath].concat(values); 164 | } 165 | return clonedObj 166 | }) 167 | }; 168 | 169 | api.insert = function insert (dest, src, path, value, at) { 170 | at = ~~at; 171 | if (isEmpty(path)) { 172 | if (!isArray(src)) { 173 | return [value] 174 | } 175 | 176 | var first = src.slice(0, at); 177 | first.push(value); 178 | return first.concat(src.slice(at)) 179 | } 180 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 181 | var arr = clonedObj[finalPath]; 182 | if (!isArray(arr)) { 183 | if (arr != null && typeof arr !== 'undefined') { 184 | throw new Error('Expected ' + path + 'to be an array. Instead got ' + typeof path) 185 | } 186 | arr = []; 187 | } 188 | 189 | var first = arr.slice(0, at); 190 | first.push(value); 191 | clonedObj[finalPath] = first.concat(arr.slice(at)); 192 | return clonedObj 193 | }) 194 | }; 195 | 196 | api.del = function del (dest, src, path) { 197 | if (isEmpty(path)) { 198 | return undefined 199 | } 200 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 201 | if (Array.isArray(clonedObj)) { 202 | if (clonedObj[finalPath] !== undefined) { 203 | clonedObj.splice(finalPath, 1); 204 | } 205 | } else { 206 | if (_hasOwnProperty.call(clonedObj, finalPath)) { 207 | delete clonedObj[finalPath]; 208 | } 209 | } 210 | return clonedObj 211 | }) 212 | }; 213 | 214 | api.assign = function assign (dest, src, path, source) { 215 | if (isEmpty(path)) { 216 | if (isEmpty(source)) { 217 | return src 218 | } 219 | return assignToObj(clone(src), source) 220 | } 221 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 222 | source = Object(source); 223 | var target = clone(clonedObj[finalPath], true); 224 | assignToObj(target, source); 225 | 226 | clonedObj[finalPath] = target; 227 | return clonedObj 228 | }) 229 | }; 230 | 231 | api.merge = function assign (dest, src, path, source) { 232 | if (isEmpty(path)) { 233 | if (isEmpty(source)) { 234 | return src 235 | } 236 | return _deepMerge(src, source) 237 | } 238 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 239 | source = Object(source); 240 | clonedObj[finalPath] = _deepMerge(clonedObj[finalPath], source); 241 | return clonedObj 242 | }) 243 | }; 244 | 245 | function wrap (src) { 246 | var dest = src; 247 | var committed = false; 248 | 249 | var transaction = Object.keys(api).reduce(function (proxy, prop) { 250 | /* istanbul ignore else */ 251 | if (typeof api[prop] === 'function') { 252 | proxy[prop] = function () { 253 | var args = [dest, src].concat(Array.prototype.slice.call(arguments)); 254 | 255 | if (committed) { 256 | throw new Error('Cannot call ' + prop + ' after `value`') 257 | } 258 | 259 | dest = api[prop].apply(null, args); 260 | 261 | return transaction 262 | }; 263 | } 264 | 265 | return proxy 266 | }, {}); 267 | 268 | transaction.value = function () { 269 | committed = true; 270 | return dest 271 | }; 272 | 273 | return transaction 274 | } 275 | 276 | var set = api.set.bind(null, null); 277 | var update = api.update.bind(null, null); 278 | var push = api.push.bind(null, null); 279 | var insert = api.insert.bind(null, null); 280 | var del = api.del.bind(null, null); 281 | var assign = api.assign.bind(null, null); 282 | var merge = api.merge.bind(null, null); 283 | var get = op.get; 284 | 285 | exports.assign = assign; 286 | exports.del = del; 287 | exports.get = get; 288 | exports.insert = insert; 289 | exports.merge = merge; 290 | exports.push = push; 291 | exports.set = set; 292 | exports.update = update; 293 | exports.wrap = wrap; 294 | -------------------------------------------------------------------------------- /esm/object-path-immutable.js: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from 'is-plain-object'; 2 | import op from 'object-path'; 3 | 4 | var _hasOwnProperty = Object.prototype.hasOwnProperty; 5 | 6 | function isEmpty (value) { 7 | if (isNumber(value)) { 8 | return false 9 | } 10 | if (!value) { 11 | return true 12 | } 13 | if (isArray(value) && value.length === 0) { 14 | return true 15 | } else if (!isString(value)) { 16 | for (var i in value) { 17 | if (_hasOwnProperty.call(value, i)) { 18 | return false 19 | } 20 | } 21 | return true 22 | } 23 | return false 24 | } 25 | 26 | function isNumber (value) { 27 | return typeof value === 'number' 28 | } 29 | 30 | function isString (obj) { 31 | return typeof obj === 'string' 32 | } 33 | 34 | function isArray (obj) { 35 | return Array.isArray(obj) 36 | } 37 | 38 | function assignToObj (target, source) { 39 | for (var key in source) { 40 | if (_hasOwnProperty.call(source, key)) { 41 | target[key] = source[key]; 42 | } 43 | } 44 | return target 45 | } 46 | 47 | function getKey (key) { 48 | var intKey = parseInt(key); 49 | if (intKey.toString() === key) { 50 | return intKey 51 | } 52 | return key 53 | } 54 | 55 | function clone (obj, createIfEmpty, assumeArray) { 56 | if (obj == null) { 57 | if (createIfEmpty) { 58 | if (assumeArray) { 59 | return [] 60 | } 61 | 62 | return {} 63 | } 64 | 65 | return obj 66 | } else if (isArray(obj)) { 67 | return obj.slice() 68 | } 69 | 70 | return assignToObj({}, obj) 71 | } 72 | 73 | function _deepMerge (dest, src) { 74 | if (dest !== src && isPlainObject(dest) && isPlainObject(src)) { 75 | var merged = {}; 76 | for (var key in dest) { 77 | if (_hasOwnProperty.call(dest, key)) { 78 | if (_hasOwnProperty.call(src, key)) { 79 | merged[key] = _deepMerge(dest[key], src[key]); 80 | } else { 81 | merged[key] = dest[key]; 82 | } 83 | } 84 | } 85 | 86 | for (key in src) { 87 | if (_hasOwnProperty.call(src, key)) { 88 | merged[key] = _deepMerge(dest[key], src[key]); 89 | } 90 | } 91 | return merged 92 | } 93 | return src 94 | } 95 | 96 | function _changeImmutable (dest, src, path, changeCallback) { 97 | if (isNumber(path)) { 98 | path = [path]; 99 | } 100 | if (isEmpty(path)) { 101 | return src 102 | } 103 | if (isString(path)) { 104 | return _changeImmutable(dest, src, path.split('.').map(getKey), changeCallback) 105 | } 106 | var currentPath = path[0]; 107 | 108 | if (!dest || dest === src) { 109 | dest = clone(src, true, isNumber(currentPath)); 110 | } 111 | 112 | if (path.length === 1) { 113 | return changeCallback(dest, currentPath) 114 | } 115 | 116 | if (src != null) { 117 | src = src[currentPath]; 118 | } 119 | 120 | dest[currentPath] = _changeImmutable(dest[currentPath], src, path.slice(1), changeCallback); 121 | 122 | return dest 123 | } 124 | 125 | var api = {}; 126 | api.set = function set (dest, src, path, value) { 127 | if (isEmpty(path)) { 128 | return value 129 | } 130 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 131 | clonedObj[finalPath] = value; 132 | return clonedObj 133 | }) 134 | }; 135 | 136 | api.update = function update (dest, src, path, updater) { 137 | if (isEmpty(path)) { 138 | return updater(clone(src)) 139 | } 140 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 141 | clonedObj[finalPath] = updater(clonedObj[finalPath]); 142 | return clonedObj 143 | }) 144 | }; 145 | 146 | api.push = function push (dest, src, path /*, values */) { 147 | var values = Array.prototype.slice.call(arguments, 3); 148 | if (isEmpty(path)) { 149 | if (!isArray(src)) { 150 | return values 151 | } else { 152 | return src.concat(values) 153 | } 154 | } 155 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 156 | if (!isArray(clonedObj[finalPath])) { 157 | clonedObj[finalPath] = values; 158 | } else { 159 | clonedObj[finalPath] = clonedObj[finalPath].concat(values); 160 | } 161 | return clonedObj 162 | }) 163 | }; 164 | 165 | api.insert = function insert (dest, src, path, value, at) { 166 | at = ~~at; 167 | if (isEmpty(path)) { 168 | if (!isArray(src)) { 169 | return [value] 170 | } 171 | 172 | var first = src.slice(0, at); 173 | first.push(value); 174 | return first.concat(src.slice(at)) 175 | } 176 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 177 | var arr = clonedObj[finalPath]; 178 | if (!isArray(arr)) { 179 | if (arr != null && typeof arr !== 'undefined') { 180 | throw new Error('Expected ' + path + 'to be an array. Instead got ' + typeof path) 181 | } 182 | arr = []; 183 | } 184 | 185 | var first = arr.slice(0, at); 186 | first.push(value); 187 | clonedObj[finalPath] = first.concat(arr.slice(at)); 188 | return clonedObj 189 | }) 190 | }; 191 | 192 | api.del = function del (dest, src, path) { 193 | if (isEmpty(path)) { 194 | return undefined 195 | } 196 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 197 | if (Array.isArray(clonedObj)) { 198 | if (clonedObj[finalPath] !== undefined) { 199 | clonedObj.splice(finalPath, 1); 200 | } 201 | } else { 202 | if (_hasOwnProperty.call(clonedObj, finalPath)) { 203 | delete clonedObj[finalPath]; 204 | } 205 | } 206 | return clonedObj 207 | }) 208 | }; 209 | 210 | api.assign = function assign (dest, src, path, source) { 211 | if (isEmpty(path)) { 212 | if (isEmpty(source)) { 213 | return src 214 | } 215 | return assignToObj(clone(src), source) 216 | } 217 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 218 | source = Object(source); 219 | var target = clone(clonedObj[finalPath], true); 220 | assignToObj(target, source); 221 | 222 | clonedObj[finalPath] = target; 223 | return clonedObj 224 | }) 225 | }; 226 | 227 | api.merge = function assign (dest, src, path, source) { 228 | if (isEmpty(path)) { 229 | if (isEmpty(source)) { 230 | return src 231 | } 232 | return _deepMerge(src, source) 233 | } 234 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 235 | source = Object(source); 236 | clonedObj[finalPath] = _deepMerge(clonedObj[finalPath], source); 237 | return clonedObj 238 | }) 239 | }; 240 | 241 | function wrap (src) { 242 | var dest = src; 243 | var committed = false; 244 | 245 | var transaction = Object.keys(api).reduce(function (proxy, prop) { 246 | /* istanbul ignore else */ 247 | if (typeof api[prop] === 'function') { 248 | proxy[prop] = function () { 249 | var args = [dest, src].concat(Array.prototype.slice.call(arguments)); 250 | 251 | if (committed) { 252 | throw new Error('Cannot call ' + prop + ' after `value`') 253 | } 254 | 255 | dest = api[prop].apply(null, args); 256 | 257 | return transaction 258 | }; 259 | } 260 | 261 | return proxy 262 | }, {}); 263 | 264 | transaction.value = function () { 265 | committed = true; 266 | return dest 267 | }; 268 | 269 | return transaction 270 | } 271 | 272 | var set = api.set.bind(null, null); 273 | var update = api.update.bind(null, null); 274 | var push = api.push.bind(null, null); 275 | var insert = api.insert.bind(null, null); 276 | var del = api.del.bind(null, null); 277 | var assign = api.assign.bind(null, null); 278 | var merge = api.merge.bind(null, null); 279 | var get = op.get; 280 | 281 | export { assign, del, get, insert, merge, push, set, update, wrap }; 282 | -------------------------------------------------------------------------------- /object-path-immutable.d.ts: -------------------------------------------------------------------------------- 1 | type Path = string | ReadonlyArray; 2 | 3 | interface WrappedObject { 4 | set(path?: Path, value?: any): WrappedObject 5 | push(path?: Path, value?: any): WrappedObject 6 | del(path?: Path): WrappedObject 7 | assign(path?: Path, source?: any): WrappedObject 8 | merge(path?: Path, source?: any): WrappedObject 9 | update(path?: Path, updater?: (formerValue: any) => any): WrappedObject 10 | insert(path?: Path, value?: any, index?: number): WrappedObject 11 | value(): T 12 | } 13 | 14 | declare module 'object-path-immutable' { 15 | export function wrap(obj: T): WrappedObject 16 | export function get(src: T, path?: Path, defaultValue?: S): S 17 | export function set(src: T, path?: Path, value?: any): T 18 | export function push(src: T, path?: Path, value?: any): T 19 | export function del(src: T, path?: Path): T 20 | export function assign(src: T, path?: Path, source?: any): T 21 | export function merge(src: T, path?: Path, source?: any): T 22 | export function update(src: T, path?: Path, updater?: (formerValue: any) => any): T 23 | export function insert(src: T, path?: Path, value?: any, index?: number): T 24 | } 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "object-path-immutable", 3 | "version": "4.1.2", 4 | "description": "Modify deep object properties without modifying the original object (immutability). Works great with React and Redux.", 5 | "author": "Mario Casciaro ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/mariocasciaro/object-path-immutable", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/mariocasciaro/object-path-immutable.git" 11 | }, 12 | "types": "./object-path-immutable.d.ts", 13 | "engines": { 14 | "node": ">=0.10.0" 15 | }, 16 | "main": "cjs/object-path-immutable.js", 17 | "module": "esm/object-path-immutable.js", 18 | "sideEffects": false, 19 | "scripts": { 20 | "build": "rollup -c", 21 | "standard": "standard", 22 | "test": "npm run build && mocha test/test.js", 23 | "coveralls": "nyc npm test && nyc report --reporter=text-lcov | coveralls", 24 | "coverage": "nyc npm test", 25 | "prepublish": "npm run coverage", 26 | "standard-fix": "standard --fix" 27 | }, 28 | "dependencies": { 29 | "is-plain-object": "^5.0.0", 30 | "object-path": "^0.11.8" 31 | }, 32 | "devDependencies": { 33 | "chai": "^4.3.4", 34 | "coveralls": "^3.1.1", 35 | "mocha": "^9.1.1", 36 | "mocha-lcov-reporter": "^1.3.0", 37 | "nyc": "^15.1.0", 38 | "rollup": "^1.32.1", 39 | "rollup-plugin-commonjs": "^10.1.0", 40 | "rollup-plugin-node-resolve": "^5.2.0", 41 | "standard": "^16.0.3" 42 | }, 43 | "keywords": [ 44 | "deep", 45 | "path", 46 | "access", 47 | "get", 48 | "property", 49 | "dot", 50 | "prop", 51 | "object", 52 | "obj", 53 | "notation", 54 | "segment", 55 | "value", 56 | "nested", 57 | "key", 58 | "immutable", 59 | "immutability", 60 | "react", 61 | "redux", 62 | "state" 63 | ], 64 | "standard": { 65 | "ignore": [ 66 | "umd", 67 | "esm", 68 | "cjs" 69 | ] 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from 'rollup-plugin-node-resolve' 2 | import commonjs from 'rollup-plugin-commonjs' 3 | import pkg from './package.json' 4 | 5 | export default [{ 6 | input: 'src/object-path-immutable.js', 7 | plugins: [ 8 | commonjs(), 9 | resolve() 10 | ], 11 | output: [{ 12 | name: 'objectPathImmutable', 13 | file: `umd/${pkg.name}.js`, 14 | format: 'umd' 15 | }] 16 | }, { 17 | input: 'src/object-path-immutable.js', 18 | output: { 19 | file: `cjs/${pkg.name}.js`, 20 | format: 'cjs', 21 | esModule: false 22 | }, 23 | external: ['is-plain-object', 'object-path'] 24 | }, { 25 | input: 'src/object-path-immutable.js', 26 | output: { 27 | file: `esm/${pkg.name}.js`, 28 | format: 'esm' 29 | }, 30 | external: ['is-plain-object', 'object-path'] 31 | }] 32 | -------------------------------------------------------------------------------- /src/object-path-immutable.js: -------------------------------------------------------------------------------- 1 | import { isPlainObject } from 'is-plain-object' 2 | import op from 'object-path' 3 | 4 | var _hasOwnProperty = Object.prototype.hasOwnProperty 5 | 6 | function isEmpty (value) { 7 | if (isNumber(value)) { 8 | return false 9 | } 10 | if (!value) { 11 | return true 12 | } 13 | if (isArray(value) && value.length === 0) { 14 | return true 15 | } else if (!isString(value)) { 16 | for (var i in value) { 17 | if (_hasOwnProperty.call(value, i)) { 18 | return false 19 | } 20 | } 21 | return true 22 | } 23 | return false 24 | } 25 | 26 | function isNumber (value) { 27 | return typeof value === 'number' 28 | } 29 | 30 | function isString (obj) { 31 | return typeof obj === 'string' 32 | } 33 | 34 | function isArray (obj) { 35 | return Array.isArray(obj) 36 | } 37 | 38 | function assignToObj (target, source) { 39 | for (var key in source) { 40 | if (_hasOwnProperty.call(source, key)) { 41 | target[key] = source[key] 42 | } 43 | } 44 | return target 45 | } 46 | 47 | function getKey (key) { 48 | var intKey = parseInt(key) 49 | if (intKey.toString() === key) { 50 | return intKey 51 | } 52 | return key 53 | } 54 | 55 | function clone (obj, createIfEmpty, assumeArray) { 56 | if (obj == null) { 57 | if (createIfEmpty) { 58 | if (assumeArray) { 59 | return [] 60 | } 61 | 62 | return {} 63 | } 64 | 65 | return obj 66 | } else if (isArray(obj)) { 67 | return obj.slice() 68 | } 69 | 70 | return assignToObj({}, obj) 71 | } 72 | 73 | function _deepMerge (dest, src) { 74 | if (dest !== src && isPlainObject(dest) && isPlainObject(src)) { 75 | var merged = {} 76 | for (var key in dest) { 77 | if (_hasOwnProperty.call(dest, key)) { 78 | if (_hasOwnProperty.call(src, key)) { 79 | merged[key] = _deepMerge(dest[key], src[key]) 80 | } else { 81 | merged[key] = dest[key] 82 | } 83 | } 84 | } 85 | 86 | for (key in src) { 87 | if (_hasOwnProperty.call(src, key)) { 88 | merged[key] = _deepMerge(dest[key], src[key]) 89 | } 90 | } 91 | return merged 92 | } 93 | return src 94 | } 95 | 96 | function _changeImmutable (dest, src, path, changeCallback) { 97 | if (isNumber(path)) { 98 | path = [path] 99 | } 100 | if (isEmpty(path)) { 101 | return src 102 | } 103 | if (isString(path)) { 104 | return _changeImmutable(dest, src, path.split('.').map(getKey), changeCallback) 105 | } 106 | var currentPath = path[0] 107 | 108 | if (!dest || dest === src) { 109 | dest = clone(src, true, isNumber(currentPath)) 110 | } 111 | 112 | if (path.length === 1) { 113 | return changeCallback(dest, currentPath) 114 | } 115 | 116 | if (src != null) { 117 | src = src[currentPath] 118 | } 119 | 120 | dest[currentPath] = _changeImmutable(dest[currentPath], src, path.slice(1), changeCallback) 121 | 122 | return dest 123 | } 124 | 125 | var api = {} 126 | api.set = function set (dest, src, path, value) { 127 | if (isEmpty(path)) { 128 | return value 129 | } 130 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 131 | clonedObj[finalPath] = value 132 | return clonedObj 133 | }) 134 | } 135 | 136 | api.update = function update (dest, src, path, updater) { 137 | if (isEmpty(path)) { 138 | return updater(clone(src)) 139 | } 140 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 141 | clonedObj[finalPath] = updater(clonedObj[finalPath]) 142 | return clonedObj 143 | }) 144 | } 145 | 146 | api.push = function push (dest, src, path /*, values */) { 147 | var values = Array.prototype.slice.call(arguments, 3) 148 | if (isEmpty(path)) { 149 | if (!isArray(src)) { 150 | return values 151 | } else { 152 | return src.concat(values) 153 | } 154 | } 155 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 156 | if (!isArray(clonedObj[finalPath])) { 157 | clonedObj[finalPath] = values 158 | } else { 159 | clonedObj[finalPath] = clonedObj[finalPath].concat(values) 160 | } 161 | return clonedObj 162 | }) 163 | } 164 | 165 | api.insert = function insert (dest, src, path, value, at) { 166 | at = ~~at 167 | if (isEmpty(path)) { 168 | if (!isArray(src)) { 169 | return [value] 170 | } 171 | 172 | var first = src.slice(0, at) 173 | first.push(value) 174 | return first.concat(src.slice(at)) 175 | } 176 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 177 | var arr = clonedObj[finalPath] 178 | if (!isArray(arr)) { 179 | if (arr != null && typeof arr !== 'undefined') { 180 | throw new Error('Expected ' + path + 'to be an array. Instead got ' + typeof path) 181 | } 182 | arr = [] 183 | } 184 | 185 | var first = arr.slice(0, at) 186 | first.push(value) 187 | clonedObj[finalPath] = first.concat(arr.slice(at)) 188 | return clonedObj 189 | }) 190 | } 191 | 192 | api.del = function del (dest, src, path) { 193 | if (isEmpty(path)) { 194 | return undefined 195 | } 196 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 197 | if (Array.isArray(clonedObj)) { 198 | if (clonedObj[finalPath] !== undefined) { 199 | clonedObj.splice(finalPath, 1) 200 | } 201 | } else { 202 | if (_hasOwnProperty.call(clonedObj, finalPath)) { 203 | delete clonedObj[finalPath] 204 | } 205 | } 206 | return clonedObj 207 | }) 208 | } 209 | 210 | api.assign = function assign (dest, src, path, source) { 211 | if (isEmpty(path)) { 212 | if (isEmpty(source)) { 213 | return src 214 | } 215 | return assignToObj(clone(src), source) 216 | } 217 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 218 | source = Object(source) 219 | var target = clone(clonedObj[finalPath], true) 220 | assignToObj(target, source) 221 | 222 | clonedObj[finalPath] = target 223 | return clonedObj 224 | }) 225 | } 226 | 227 | api.merge = function assign (dest, src, path, source) { 228 | if (isEmpty(path)) { 229 | if (isEmpty(source)) { 230 | return src 231 | } 232 | return _deepMerge(src, source) 233 | } 234 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 235 | source = Object(source) 236 | clonedObj[finalPath] = _deepMerge(clonedObj[finalPath], source) 237 | return clonedObj 238 | }) 239 | } 240 | 241 | export function wrap (src) { 242 | var dest = src 243 | var committed = false 244 | 245 | var transaction = Object.keys(api).reduce(function (proxy, prop) { 246 | /* istanbul ignore else */ 247 | if (typeof api[prop] === 'function') { 248 | proxy[prop] = function () { 249 | var args = [dest, src].concat(Array.prototype.slice.call(arguments)) 250 | 251 | if (committed) { 252 | throw new Error('Cannot call ' + prop + ' after `value`') 253 | } 254 | 255 | dest = api[prop].apply(null, args) 256 | 257 | return transaction 258 | } 259 | } 260 | 261 | return proxy 262 | }, {}) 263 | 264 | transaction.value = function () { 265 | committed = true 266 | return dest 267 | } 268 | 269 | return transaction 270 | } 271 | 272 | export var set = api.set.bind(null, null) 273 | export var update = api.update.bind(null, null) 274 | export var push = api.push.bind(null, null) 275 | export var insert = api.insert.bind(null, null) 276 | export var del = api.del.bind(null, null) 277 | export var assign = api.assign.bind(null, null) 278 | export var merge = api.merge.bind(null, null) 279 | export var get = op.get 280 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /* globals describe, it */ 2 | 3 | var expect = require('chai').expect 4 | var op = require('../') 5 | 6 | function getTestObj () { 7 | return { 8 | a: 'b', 9 | b: { 10 | c: [], 11 | d: ['a', 'b'], 12 | e: [{}, { f: 'g' }], 13 | f: 'i' 14 | } 15 | } 16 | } 17 | 18 | describe('get', function () { 19 | it('should return the value using unicode key', function () { 20 | var obj = { 21 | '15\u00f8C': { 22 | '3\u0111': 1 23 | } 24 | } 25 | expect(op.get(obj, '15\u00f8C.3\u0111')).to.be.equal(1) 26 | expect(op.get(obj, ['15\u00f8C', '3\u0111'])).to.be.equal(1) 27 | }) 28 | 29 | it('should return the value using dot in key', function () { 30 | var obj = { 31 | 'a.b': { 32 | 'looks.like': 1 33 | } 34 | } 35 | expect(op.get(obj, 'a.b.looks.like')).to.be.equal(undefined) 36 | expect(op.get(obj, ['a.b', 'looks.like'])).to.be.equal(1) 37 | }) 38 | 39 | it('should return the value under shallow object', function () { 40 | var obj = getTestObj() 41 | expect(op.get(obj, 'a')).to.be.equal('b') 42 | expect(op.get(obj, ['a'])).to.be.equal('b') 43 | }) 44 | 45 | it('should work with number path', function () { 46 | var obj = getTestObj() 47 | expect(op.get(obj.b.d, 0)).to.be.equal('a') 48 | expect(op.get(obj.b, 0)).to.be.equal(undefined) 49 | }) 50 | 51 | it('should return the value under deep object', function () { 52 | var obj = getTestObj() 53 | expect(op.get(obj, 'b.f')).to.be.equal('i') 54 | expect(op.get(obj, ['b', 'f'])).to.be.equal('i') 55 | }) 56 | 57 | it('should return the value under array', function () { 58 | var obj = getTestObj() 59 | expect(op.get(obj, 'b.d.0')).to.be.equal('a') 60 | expect(op.get(obj, ['b', 'd', 0])).to.be.equal('a') 61 | }) 62 | 63 | it('should return the value under array deep', function () { 64 | var obj = getTestObj() 65 | expect(op.get(obj, 'b.e.1.f')).to.be.equal('g') 66 | expect(op.get(obj, ['b', 'e', 1, 'f'])).to.be.equal('g') 67 | }) 68 | 69 | it('should return undefined for missing values under object', function () { 70 | var obj = getTestObj() 71 | expect(op.get(obj, 'a.b')).to.be.equal(undefined) 72 | expect(op.get(obj, ['a', 'b'])).to.be.equal(undefined) 73 | }) 74 | 75 | it('should return undefined for missing values under array', function () { 76 | var obj = getTestObj() 77 | expect(op.get(obj, 'b.d.5')).to.be.equal(undefined) 78 | expect(op.get(obj, ['b', 'd', '5'])).to.be.equal(undefined) 79 | }) 80 | 81 | it('should return the value under integer-like key', function () { 82 | var obj = { '1a': 'foo' } 83 | expect(op.get(obj, '1a')).to.be.equal('foo') 84 | expect(op.get(obj, ['1a'])).to.be.equal('foo') 85 | }) 86 | 87 | it('should return the default value when the key doesnt exist', function () { 88 | var obj = { '1a': 'foo' } 89 | expect(op.get(obj, '1b', null)).to.be.equal(null) 90 | expect(op.get(obj, ['1b'], null)).to.be.equal(null) 91 | }) 92 | 93 | it('should return the default value when path is empty', function () { 94 | var obj = { '1a': 'foo' } 95 | expect(op.get(obj, '', null)).to.be.deep.equal({ '1a': 'foo' }) 96 | expect(op.get(obj, [])).to.be.deep.equal({ '1a': 'foo' }) 97 | expect(op.get({ }, ['1'])).to.be.equal(undefined) 98 | }) 99 | 100 | it('should return the default value when object is null or undefined', function () { 101 | expect(op.get(null, 'test', 'a')).to.be.deep.equal('a') 102 | expect(op.get(undefined, 'test', 'a')).to.be.deep.equal('a') 103 | }) 104 | 105 | it( 106 | 'should not fail on an object with a null prototype', 107 | function assertSuccessForObjWithNullProto () { 108 | var foo = 'FOO' 109 | var objWithNullProto = Object.create(null) 110 | objWithNullProto.foo = foo 111 | expect(op.get(objWithNullProto, 'foo')).to.equal(foo) 112 | } 113 | ) 114 | 115 | it('should skip non own properties', function () { 116 | var Base = function (enabled) { } 117 | Base.prototype = { 118 | one: { 119 | two: true 120 | } 121 | } 122 | var Extended = function () { 123 | Base.call(this, true) 124 | } 125 | Extended.prototype = Object.create(Base.prototype) 126 | 127 | var extended = new Extended() 128 | 129 | expect(op.get(extended, ['one', 'two'])).to.be.equal(undefined) 130 | extended.enabled = true 131 | 132 | expect(op.get(extended, 'enabled')).to.be.equal(true) 133 | expect(op.get(extended, 'one')).to.be.equal(undefined) 134 | }) 135 | }) 136 | 137 | describe('set', function () { 138 | it('should set a deep key without modifying the original object', function () { 139 | var obj = { 140 | a: { 141 | b: 1 142 | }, 143 | c: { 144 | d: 2 145 | } 146 | } 147 | 148 | var newObj = op.set(obj, 'a.b', 3) 149 | 150 | expect(newObj).not.to.be.equal(obj) 151 | expect(newObj.a).not.to.be.equal(obj.a) 152 | expect(obj.a).to.be.eql({ b: 1 }) 153 | // this should be the same 154 | expect(newObj.c).to.be.equal(obj.c) 155 | 156 | expect(newObj.a.b).to.be.equal(3) 157 | }) 158 | 159 | it('should set a deep array', function () { 160 | var obj = { 161 | a: { 162 | b: [1, 2, 3] 163 | }, 164 | c: { 165 | d: 2 166 | } 167 | } 168 | 169 | var newObj = op.set(obj, 'a.b.1', 4) 170 | 171 | expect(newObj).not.to.be.equal(obj) 172 | expect(newObj.a).not.to.be.equal(obj.a) 173 | expect(newObj.a.b).not.to.be.equal(obj.a.b) 174 | expect(newObj.c).to.be.equal(obj.c) 175 | 176 | expect(newObj.a.b).to.be.eql([1, 4, 3]) 177 | }) 178 | 179 | it('should create intermediate objects and array', function () { 180 | var obj = { 181 | a: {}, 182 | c: { 183 | d: 2 184 | } 185 | } 186 | 187 | var newObj = op.set(obj, 'a.b.1.f', 'a') 188 | 189 | expect(newObj).not.to.be.equal(obj) 190 | expect(newObj.a).not.to.be.equal(obj.a) 191 | expect(obj.a).to.be.eql({}) 192 | expect(newObj.a).to.be.eql({ b: [, { f: 'a' }] }) // eslint-disable-line no-sparse-arrays 193 | }) 194 | 195 | it('should return the input value if passed an empty path', function () { 196 | var obj = {} 197 | 198 | expect(op.set(obj, '', 'yo')).to.be.equal('yo') 199 | }) 200 | 201 | it('should set at a numeric path', function () { 202 | expect(op.set([], 0, 'yo')).to.deep.equal(['yo']) 203 | }) 204 | 205 | it('[security] it should not override an object\'s prototype', function () { 206 | op.set({}, '__proto__.injected', 'yo') 207 | expect({}.injected).to.be.undefined 208 | }) 209 | }) 210 | 211 | describe('update', function () { 212 | it('should update a deep key', function () { 213 | var obj = { 214 | a: { 215 | b: 1 216 | }, 217 | c: { 218 | d: 2 219 | } 220 | } 221 | 222 | var newObj = op.update(obj, 'a.b', function (value) { 223 | return value + 1 224 | }) 225 | 226 | expect(newObj).not.to.be.equal(obj) 227 | expect(newObj.a).not.to.be.equal(obj.a) 228 | expect(obj.a).to.be.eql({ b: 1 }) 229 | // this should be the same 230 | expect(newObj.c).to.be.equal(obj.c) 231 | 232 | expect(newObj.a.b).to.be.equal(2) 233 | }) 234 | 235 | it('should work on empty path', function () { 236 | var obj = { 237 | a: { 238 | b: 1 239 | }, 240 | c: { 241 | d: 2 242 | } 243 | } 244 | 245 | var newObj = op.update(obj, [], function (value) { 246 | value.e = 3 247 | return value 248 | }) 249 | 250 | expect(newObj).not.to.be.equal(obj) 251 | expect(newObj.a).to.be.equal(obj.a) 252 | expect(obj.a).to.be.eql({ b: 1 }) 253 | // this should be the same 254 | expect(newObj.c).to.be.equal(obj.c) 255 | expect(newObj.a.b).to.be.equal(1) 256 | expect(newObj.e).to.be.equal(3) 257 | }) 258 | }) 259 | 260 | describe('insert', function () { 261 | it('should insert value into existing array without modifying the object', function () { 262 | var obj = { 263 | a: ['a'], 264 | c: {} 265 | } 266 | 267 | var newObj = op.insert(obj, 'a', 'b', 0) 268 | 269 | expect(newObj).not.to.be.equal(obj) 270 | expect(newObj.a).not.to.be.equal(obj.a) 271 | expect(newObj.c).to.be.equal(obj.c) 272 | 273 | expect(newObj.a).to.be.eql(['b', 'a']) 274 | }) 275 | 276 | it('should create intermediary array', function () { 277 | var obj = { 278 | a: [], 279 | c: {} 280 | } 281 | 282 | var newObj = op.insert(obj, 'a.0.1', 'b') 283 | 284 | expect(newObj).not.to.be.equal(obj) 285 | expect(newObj.a).not.to.be.equal(obj.a) 286 | expect(newObj.c).to.be.equal(obj.c) 287 | 288 | expect(newObj.a).to.be.eql([[, ['b']]]) // eslint-disable-line no-sparse-arrays 289 | }) 290 | 291 | it('should insert in another index', function () { 292 | var obj = { 293 | a: 'b', 294 | b: { 295 | c: [], 296 | d: ['a', 'b'], 297 | e: [{}, { f: 'g' }], 298 | f: 'i' 299 | } 300 | } 301 | 302 | var newObj = op.insert(obj, 'b.d', 'asdf', 1) 303 | 304 | expect(newObj).not.to.be.equal(obj) 305 | expect(newObj.b.d).to.be.eql(['a', 'asdf', 'b']) 306 | }) 307 | 308 | it('should handle sparse array', function () { 309 | var obj = { 310 | a: 'b', 311 | b: { 312 | c: [], 313 | d: ['a', 'b'], 314 | e: [{}, { f: 'g' }], 315 | f: 'i' 316 | } 317 | } 318 | obj.b.d = new Array(4) 319 | obj.b.d[0] = 'a' 320 | obj.b.d[1] = 'b' 321 | 322 | var newObj = op.insert(obj, 'b.d', 'asdf', 3) 323 | expect(newObj).not.to.be.equal(obj) 324 | expect(newObj.b.d[0]).to.be.eql('a') 325 | expect(newObj.b.d[1]).to.be.eql('b') 326 | // eslint-disable-next-line 327 | expect(newObj.b.d[2]).to.be.undefined 328 | expect(newObj.b.d[3]).to.be.eql('asdf') 329 | // eslint-disable-next-line 330 | expect(newObj.b.d[4]).to.be.undefined 331 | expect(newObj.b.d.length).to.be.eql(5) 332 | }) 333 | 334 | it('should throw if asked to insert into something other than an array', 335 | function () { 336 | expect(function () { 337 | op.insert({ foo: 'bar' }, 'foo', 'baz') 338 | }).to.throw() 339 | }) 340 | 341 | it('should return an array with an undefined value if passed an empty path and empty value and src is not an array', function () { 342 | var obj = {} 343 | 344 | expect(op.insert(obj, '')).to.be.deep.equal([undefined]) 345 | }) 346 | 347 | it('should insert the value in src if passed an empty path', function () { 348 | var obj = ['a', 'b', 'c'] 349 | 350 | expect(op.insert(obj, '', 'd', 1)).to.be.deep.equal(['a', 'd', 'b', 'c']) 351 | }) 352 | 353 | it('should insert at a numeric path', function () { 354 | expect(op.insert([[23, 42]], 0, 'yo', 1)).to.deep.equal([[23, 'yo', 42]]) 355 | }) 356 | }) 357 | 358 | describe('push', function () { 359 | it('should push values without modifying the object', function () { 360 | var obj = { 361 | a: ['a'], 362 | c: {} 363 | } 364 | 365 | var newObj = op.push(obj, 'a', 'b') 366 | 367 | expect(newObj).not.to.be.equal(obj) 368 | expect(newObj.a).not.to.be.equal(obj.a) 369 | expect(newObj.c).to.be.equal(obj.c) 370 | 371 | expect(newObj.a).to.be.eql(['a', 'b']) 372 | }) 373 | 374 | it('should create intermediate objects/arrays', function () { 375 | var obj = { 376 | a: [], 377 | c: {} 378 | } 379 | 380 | var newObj = op.push(obj, 'a.0.1', 'b') 381 | 382 | expect(newObj).not.to.be.equal(obj) 383 | expect(newObj.a).not.to.be.equal(obj.a) 384 | expect(newObj.c).to.be.equal(obj.c) 385 | 386 | expect(newObj.a).to.be.eql([[, ['b']]]) // eslint-disable-line no-sparse-arrays 387 | }) 388 | 389 | it('should push into the cloned original object if passed an empty path', function () { 390 | var obj = ['yoo'] 391 | 392 | expect(op.push(obj, '', 'yo')).to.deep.equal(['yoo', 'yo']) 393 | }) 394 | 395 | it('returns new array if passed an empty path and src is not an array', function () { 396 | var obj = {} 397 | 398 | expect(op.push(obj, '', 'yo')).to.deep.equal(['yo']) 399 | }) 400 | 401 | it('should push at a numeric path', function () { 402 | expect(op.push([[]], 0, 'yo')).to.deep.equal([['yo']]) 403 | }) 404 | }) 405 | 406 | describe('del', function () { 407 | it('should delete deep values without modifying the object', function () { 408 | var obj = { 409 | a: { 410 | d: 1, 411 | f: 2 412 | }, 413 | c: {} 414 | } 415 | 416 | var newObj = op.del(obj, 'a.d') 417 | 418 | expect(newObj).not.to.be.equal(obj) 419 | expect(newObj.a).not.to.be.equal(obj.a) 420 | expect(newObj.c).to.be.equal(obj.c) 421 | 422 | expect(newObj.a).to.be.eql({ f: 2 }) 423 | }) 424 | 425 | it('should delete deep values in array without modifying the object', function () { 426 | var obj = { 427 | a: ['a'], 428 | c: {} 429 | } 430 | 431 | var newObj = op.del(obj, 'a.0') 432 | 433 | expect(newObj).not.to.be.equal(obj) 434 | expect(newObj.a).not.to.be.equal(obj.a) 435 | expect(newObj.c).to.be.equal(obj.c) 436 | 437 | expect(newObj.a).to.be.eql([]) 438 | }) 439 | 440 | it('should return undefined if passed an empty path', function () { 441 | var obj = {} 442 | 443 | expect(op.del(obj, '')).to.be.equal(undefined) 444 | }) 445 | 446 | it('should del at a numeric path', function () { 447 | expect(op.del([23, 'yo', 42], 1)).to.deep.equal([23, 42]) 448 | }) 449 | 450 | it('should delete falsy value', function () { 451 | expect(op.del(['', false], 1)).to.deep.equal(['']) 452 | }) 453 | }) 454 | 455 | describe('assign', function () { 456 | it('should assign an object without modifying the original object', function () { 457 | var obj = { 458 | a: { 459 | b: 1 460 | }, 461 | c: { 462 | d: 2 463 | } 464 | } 465 | 466 | var newObj = op.assign(obj, 'a', { b: 3 }) 467 | 468 | expect(newObj).not.to.be.equal(obj) 469 | expect(newObj.a).not.to.be.equal(obj.a) 470 | expect(obj.a).to.be.eql({ b: 1 }) 471 | expect(newObj.c).to.be.equal(obj.c) 472 | 473 | expect(newObj.a.b).to.be.equal(3) 474 | }) 475 | 476 | it('should keep existing fields that are not overwritten', function () { 477 | var obj = { 478 | a: { 479 | b: 1 480 | } 481 | } 482 | 483 | var newObj = op.assign(obj, 'a', { c: 2 }) 484 | 485 | expect(newObj).not.to.be.equal(obj) 486 | expect(newObj.a).not.to.be.equal(obj.a) 487 | expect(obj.a).to.be.eql({ b: 1 }) 488 | expect(newObj.a).to.be.eql({ b: 1, c: 2 }) 489 | }) 490 | 491 | it('should create intermediate objects', function () { 492 | var obj = { 493 | a: {}, 494 | c: { 495 | d: 2 496 | } 497 | } 498 | 499 | var newObj = op.assign(obj, 'a.b', { f: 'a' }) 500 | 501 | expect(newObj).not.to.be.equal(obj) 502 | expect(newObj.a).not.to.be.equal(obj.a) 503 | expect(obj.a).to.be.eql({}) 504 | expect(newObj.a).to.be.eql({ b: { f: 'a' } }) 505 | }) 506 | 507 | it('should return the original object if passed an empty path and an empty value to assign', function () { 508 | var obj = {} 509 | 510 | expect(op.assign(obj, '', {})).to.be.equal(obj) 511 | }) 512 | 513 | it('should assign at a numeric path', function () { 514 | expect(op.assign([{ 515 | foo: 'bar' 516 | }], 0, { 517 | foo: 'baz', 518 | fiz: 'biz' 519 | })).to.deep.equal([{ 520 | foo: 'baz', 521 | fiz: 'biz' 522 | }]) 523 | }) 524 | 525 | it('does not assign inherited properties', function () { 526 | var base = { 527 | fiz: 'biz' 528 | } 529 | 530 | var source = Object.create(base) 531 | source.frob = 'nard' 532 | 533 | var obj = { 534 | foo: {} 535 | } 536 | 537 | expect(op.assign(obj, 'foo', source)).to.deep.equal({ 538 | foo: { 539 | frob: 'nard' 540 | } 541 | }) 542 | }) 543 | 544 | it('[security] it should not assign object\'s prototype', function () { 545 | op.set({}, 'test', { 546 | __proto__: { 547 | injected: true 548 | } 549 | }) 550 | expect({}.injected).to.be.undefined 551 | }) 552 | }) 553 | 554 | describe('merge', function () { 555 | it('should merge an object without modifying the original object', function () { 556 | var obj = { 557 | a: { 558 | b: 1 559 | }, 560 | c: { 561 | d: 2 562 | } 563 | } 564 | 565 | var newObj = op.merge(obj, 'a', { b: 3 }) 566 | 567 | expect(newObj).not.to.be.equal(obj) 568 | expect(newObj.a).not.to.be.equal(obj.a) 569 | expect(obj.a).to.be.eql({ b: 1 }) 570 | expect(newObj.c).to.be.equal(obj.c) 571 | 572 | expect(newObj.a.b).to.be.equal(3) 573 | }) 574 | 575 | it('should properly merge objects', function () { 576 | var obj = { 577 | a: { 578 | b: 1, 579 | c: { 580 | d: 2, 581 | e: 3 582 | } 583 | } 584 | } 585 | 586 | var newObj = op.merge(obj, 'a', { c: { e: 4 } }) 587 | 588 | expect(newObj).not.to.be.equal(obj) 589 | expect(newObj.a).not.to.be.equal(obj.a) 590 | expect(obj.a.b).to.be.eql(1) 591 | expect(newObj.a.c).to.be.eql({ 592 | d: 2, 593 | e: 4 594 | }) 595 | }) 596 | 597 | it('should not merge arrays by default', function () { 598 | var obj = { 599 | a: { 600 | b: 1, 601 | c: { 602 | d: 2, 603 | e: [1] 604 | } 605 | } 606 | } 607 | 608 | var newObj = op.merge(obj, 'a', { c: { e: [2] } }) 609 | 610 | expect(obj.a.b).to.be.eql(1) 611 | expect(newObj.a.c).to.be.eql({ 612 | d: 2, 613 | e: [2] 614 | }) 615 | }) 616 | 617 | it('should not merge objects without a path', function () { 618 | var obj = { 619 | a: { 620 | b: 1, 621 | c: { 622 | d: 2, 623 | e: [1] 624 | } 625 | } 626 | } 627 | 628 | var newObj = op.merge(obj, null, { a: { c: { e: [2] } } }) 629 | 630 | expect(obj.a.b).to.be.eql(1) 631 | expect(newObj.a.c).to.be.eql({ 632 | d: 2, 633 | e: [2] 634 | }) 635 | }) 636 | 637 | it('should work if the destination is undefined', function () { 638 | var obj = { 639 | a: { 640 | b: 1, 641 | c: { 642 | d: 2, 643 | e: [1] 644 | } 645 | } 646 | } 647 | 648 | var newObj = op.merge(obj, 'a.c.f', { a: 1 }) 649 | expect(newObj.a.c.f).to.be.eql({ a: 1 }) 650 | }) 651 | 652 | it('should work with wrap and if the destination is undefined', function () { 653 | var obj = { 654 | a: { 655 | b: 1, 656 | c: { 657 | d: 2, 658 | e: [1] 659 | } 660 | } 661 | } 662 | 663 | var newObj = op.wrap(obj).merge('a.c.f', { a: 1 }).value() 664 | expect(newObj.a.c.f).to.be.eql({ a: 1 }) 665 | }) 666 | 667 | it('[security] it should not merge into object\'s prototype', function () { 668 | op.merge({}, 'test', { 669 | __proto__: { 670 | injected: true 671 | } 672 | }) 673 | expect({}.injected).to.be.undefined 674 | }) 675 | }) 676 | 677 | describe('bind', function () { 678 | it('should execute all methods on the wrapped object', function () { 679 | var obj = { 680 | a: { 681 | d: 1, 682 | f: 2 683 | }, 684 | c: {} 685 | } 686 | 687 | var newObj = op.wrap(obj).set('a.q', 'q').del('a.d').update('a.f', function (v) { 688 | return v + 1 689 | }).value() 690 | 691 | expect(newObj).not.to.be.equal(obj) 692 | expect(newObj.a).not.to.be.equal(obj.a) 693 | expect(newObj.c).to.be.equal(obj.c) 694 | 695 | expect(newObj.a).to.be.eql({ f: 3, q: 'q' }) 696 | }) 697 | 698 | it('should return the wrapped object if no operations made', function () { 699 | var obj = {} 700 | 701 | expect(op.wrap(obj).value()).to.be.equal(obj) 702 | }) 703 | 704 | it('should throw if an operation is attempted after `value` called', function () { 705 | var transaction = op.wrap({ 706 | foo: 'bar', 707 | fiz: [], 708 | frob: {} 709 | }) 710 | 711 | transaction.value() 712 | 713 | expect(function () { 714 | transaction.set('foo', 'baz') 715 | }).to.throw() 716 | 717 | expect(function () { 718 | transaction.push('fiz', 'biz') 719 | }).to.throw() 720 | 721 | expect(function () { 722 | transaction.insert('fiz', 'biz', 23) 723 | }).to.throw() 724 | 725 | expect(function () { 726 | transaction.del('foo') 727 | }).to.throw() 728 | 729 | expect(function () { 730 | /* istanbul ignore next */ 731 | transaction.update('foo', function (v) { 732 | return v + 'bar' 733 | }) 734 | }).to.throw() 735 | 736 | expect(function () { 737 | transaction.assign('frob', { 738 | nard: 23 739 | }) 740 | }).to.throw() 741 | }) 742 | }) 743 | -------------------------------------------------------------------------------- /umd/object-path-immutable.js: -------------------------------------------------------------------------------- 1 | (function (global, factory) { 2 | typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : 3 | typeof define === 'function' && define.amd ? define(['exports'], factory) : 4 | (global = global || self, factory(global.objectPathImmutable = {})); 5 | }(this, (function (exports) { 'use strict'; 6 | 7 | /*! 8 | * is-plain-object 9 | * 10 | * Copyright (c) 2014-2017, Jon Schlinkert. 11 | * Released under the MIT License. 12 | */ 13 | 14 | function isObject(o) { 15 | return Object.prototype.toString.call(o) === '[object Object]'; 16 | } 17 | 18 | function isPlainObject(o) { 19 | var ctor,prot; 20 | 21 | if (isObject(o) === false) return false; 22 | 23 | // If has modified constructor 24 | ctor = o.constructor; 25 | if (ctor === undefined) return true; 26 | 27 | // If has modified prototype 28 | prot = ctor.prototype; 29 | if (isObject(prot) === false) return false; 30 | 31 | // If constructor does not have an Object-specific method 32 | if (prot.hasOwnProperty('isPrototypeOf') === false) { 33 | return false; 34 | } 35 | 36 | // Most likely a plain Object 37 | return true; 38 | } 39 | 40 | var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {}; 41 | 42 | function createCommonjsModule(fn, module) { 43 | return module = { exports: {} }, fn(module, module.exports), module.exports; 44 | } 45 | 46 | var objectPath = createCommonjsModule(function (module) { 47 | (function (root, factory) { 48 | 49 | /*istanbul ignore next:cant test*/ 50 | { 51 | module.exports = factory(); 52 | } 53 | })(commonjsGlobal, function () { 54 | 55 | var toStr = Object.prototype.toString; 56 | 57 | function hasOwnProperty (obj, prop) { 58 | if (obj == null) { 59 | return false 60 | } 61 | //to handle objects with null prototypes (too edge case?) 62 | return Object.prototype.hasOwnProperty.call(obj, prop) 63 | } 64 | 65 | function isEmpty (value) { 66 | if (!value) { 67 | return true 68 | } 69 | if (isArray(value) && value.length === 0) { 70 | return true 71 | } else if (typeof value !== 'string') { 72 | for (var i in value) { 73 | if (hasOwnProperty(value, i)) { 74 | return false 75 | } 76 | } 77 | return true 78 | } 79 | return false 80 | } 81 | 82 | function toString (type) { 83 | return toStr.call(type) 84 | } 85 | 86 | function isObject (obj) { 87 | return typeof obj === 'object' && toString(obj) === '[object Object]' 88 | } 89 | 90 | var isArray = Array.isArray || function (obj) { 91 | /*istanbul ignore next:cant test*/ 92 | return toStr.call(obj) === '[object Array]' 93 | }; 94 | 95 | function isBoolean (obj) { 96 | return typeof obj === 'boolean' || toString(obj) === '[object Boolean]' 97 | } 98 | 99 | function getKey (key) { 100 | var intKey = parseInt(key); 101 | if (intKey.toString() === key) { 102 | return intKey 103 | } 104 | return key 105 | } 106 | 107 | function factory (options) { 108 | options = options || {}; 109 | 110 | var objectPath = function (obj) { 111 | return Object.keys(objectPath).reduce(function (proxy, prop) { 112 | if (prop === 'create') { 113 | return proxy 114 | } 115 | 116 | /*istanbul ignore else*/ 117 | if (typeof objectPath[prop] === 'function') { 118 | proxy[prop] = objectPath[prop].bind(objectPath, obj); 119 | } 120 | 121 | return proxy 122 | }, {}) 123 | }; 124 | 125 | var hasShallowProperty; 126 | if (options.includeInheritedProps) { 127 | hasShallowProperty = function () { 128 | return true 129 | }; 130 | } else { 131 | hasShallowProperty = function (obj, prop) { 132 | return (typeof prop === 'number' && Array.isArray(obj)) || hasOwnProperty(obj, prop) 133 | }; 134 | } 135 | 136 | function getShallowProperty (obj, prop) { 137 | if (hasShallowProperty(obj, prop)) { 138 | return obj[prop] 139 | } 140 | } 141 | 142 | var getShallowPropertySafely; 143 | if (options.includeInheritedProps) { 144 | getShallowPropertySafely = function (obj, currentPath) { 145 | if (typeof currentPath !== 'string' && typeof currentPath !== 'number') { 146 | currentPath = String(currentPath); 147 | } 148 | var currentValue = getShallowProperty(obj, currentPath); 149 | if (currentPath === '__proto__' || currentPath === 'prototype' || 150 | (currentPath === 'constructor' && typeof currentValue === 'function')) { 151 | throw new Error('For security reasons, object\'s magic properties cannot be set') 152 | } 153 | return currentValue 154 | }; 155 | } else { 156 | getShallowPropertySafely = function (obj, currentPath) { 157 | return getShallowProperty(obj, currentPath) 158 | }; 159 | } 160 | 161 | function set (obj, path, value, doNotReplace) { 162 | if (typeof path === 'number') { 163 | path = [path]; 164 | } 165 | if (!path || path.length === 0) { 166 | return obj 167 | } 168 | if (typeof path === 'string') { 169 | return set(obj, path.split('.').map(getKey), value, doNotReplace) 170 | } 171 | var currentPath = path[0]; 172 | var currentValue = getShallowPropertySafely(obj, currentPath); 173 | if (path.length === 1) { 174 | if (currentValue === void 0 || !doNotReplace) { 175 | obj[currentPath] = value; 176 | } 177 | return currentValue 178 | } 179 | 180 | if (currentValue === void 0) { 181 | //check if we assume an array 182 | if (typeof path[1] === 'number') { 183 | obj[currentPath] = []; 184 | } else { 185 | obj[currentPath] = {}; 186 | } 187 | } 188 | 189 | return set(obj[currentPath], path.slice(1), value, doNotReplace) 190 | } 191 | 192 | objectPath.has = function (obj, path) { 193 | if (typeof path === 'number') { 194 | path = [path]; 195 | } else if (typeof path === 'string') { 196 | path = path.split('.'); 197 | } 198 | 199 | if (!path || path.length === 0) { 200 | return !!obj 201 | } 202 | 203 | for (var i = 0; i < path.length; i++) { 204 | var j = getKey(path[i]); 205 | 206 | if ((typeof j === 'number' && isArray(obj) && j < obj.length) || 207 | (options.includeInheritedProps ? (j in Object(obj)) : hasOwnProperty(obj, j))) { 208 | obj = obj[j]; 209 | } else { 210 | return false 211 | } 212 | } 213 | 214 | return true 215 | }; 216 | 217 | objectPath.ensureExists = function (obj, path, value) { 218 | return set(obj, path, value, true) 219 | }; 220 | 221 | objectPath.set = function (obj, path, value, doNotReplace) { 222 | return set(obj, path, value, doNotReplace) 223 | }; 224 | 225 | objectPath.insert = function (obj, path, value, at) { 226 | var arr = objectPath.get(obj, path); 227 | at = ~~at; 228 | if (!isArray(arr)) { 229 | arr = []; 230 | objectPath.set(obj, path, arr); 231 | } 232 | arr.splice(at, 0, value); 233 | }; 234 | 235 | objectPath.empty = function (obj, path) { 236 | if (isEmpty(path)) { 237 | return void 0 238 | } 239 | if (obj == null) { 240 | return void 0 241 | } 242 | 243 | var value, i; 244 | if (!(value = objectPath.get(obj, path))) { 245 | return void 0 246 | } 247 | 248 | if (typeof value === 'string') { 249 | return objectPath.set(obj, path, '') 250 | } else if (isBoolean(value)) { 251 | return objectPath.set(obj, path, false) 252 | } else if (typeof value === 'number') { 253 | return objectPath.set(obj, path, 0) 254 | } else if (isArray(value)) { 255 | value.length = 0; 256 | } else if (isObject(value)) { 257 | for (i in value) { 258 | if (hasShallowProperty(value, i)) { 259 | delete value[i]; 260 | } 261 | } 262 | } else { 263 | return objectPath.set(obj, path, null) 264 | } 265 | }; 266 | 267 | objectPath.push = function (obj, path /*, values */) { 268 | var arr = objectPath.get(obj, path); 269 | if (!isArray(arr)) { 270 | arr = []; 271 | objectPath.set(obj, path, arr); 272 | } 273 | 274 | arr.push.apply(arr, Array.prototype.slice.call(arguments, 2)); 275 | }; 276 | 277 | objectPath.coalesce = function (obj, paths, defaultValue) { 278 | var value; 279 | 280 | for (var i = 0, len = paths.length; i < len; i++) { 281 | if ((value = objectPath.get(obj, paths[i])) !== void 0) { 282 | return value 283 | } 284 | } 285 | 286 | return defaultValue 287 | }; 288 | 289 | objectPath.get = function (obj, path, defaultValue) { 290 | if (typeof path === 'number') { 291 | path = [path]; 292 | } 293 | if (!path || path.length === 0) { 294 | return obj 295 | } 296 | if (obj == null) { 297 | return defaultValue 298 | } 299 | if (typeof path === 'string') { 300 | return objectPath.get(obj, path.split('.'), defaultValue) 301 | } 302 | 303 | var currentPath = getKey(path[0]); 304 | var nextObj = getShallowPropertySafely(obj, currentPath); 305 | if (nextObj === void 0) { 306 | return defaultValue 307 | } 308 | 309 | if (path.length === 1) { 310 | return nextObj 311 | } 312 | 313 | return objectPath.get(obj[currentPath], path.slice(1), defaultValue) 314 | }; 315 | 316 | objectPath.del = function del (obj, path) { 317 | if (typeof path === 'number') { 318 | path = [path]; 319 | } 320 | 321 | if (obj == null) { 322 | return obj 323 | } 324 | 325 | if (isEmpty(path)) { 326 | return obj 327 | } 328 | if (typeof path === 'string') { 329 | return objectPath.del(obj, path.split('.')) 330 | } 331 | 332 | var currentPath = getKey(path[0]); 333 | getShallowPropertySafely(obj, currentPath); 334 | if (!hasShallowProperty(obj, currentPath)) { 335 | return obj 336 | } 337 | 338 | if (path.length === 1) { 339 | if (isArray(obj)) { 340 | obj.splice(currentPath, 1); 341 | } else { 342 | delete obj[currentPath]; 343 | } 344 | } else { 345 | return objectPath.del(obj[currentPath], path.slice(1)) 346 | } 347 | 348 | return obj 349 | }; 350 | 351 | return objectPath 352 | } 353 | 354 | var mod = factory(); 355 | mod.create = factory; 356 | mod.withInheritedProps = factory({includeInheritedProps: true}); 357 | return mod 358 | }); 359 | }); 360 | 361 | var _hasOwnProperty = Object.prototype.hasOwnProperty; 362 | 363 | function isEmpty (value) { 364 | if (isNumber(value)) { 365 | return false 366 | } 367 | if (!value) { 368 | return true 369 | } 370 | if (isArray(value) && value.length === 0) { 371 | return true 372 | } else if (!isString(value)) { 373 | for (var i in value) { 374 | if (_hasOwnProperty.call(value, i)) { 375 | return false 376 | } 377 | } 378 | return true 379 | } 380 | return false 381 | } 382 | 383 | function isNumber (value) { 384 | return typeof value === 'number' 385 | } 386 | 387 | function isString (obj) { 388 | return typeof obj === 'string' 389 | } 390 | 391 | function isArray (obj) { 392 | return Array.isArray(obj) 393 | } 394 | 395 | function assignToObj (target, source) { 396 | for (var key in source) { 397 | if (_hasOwnProperty.call(source, key)) { 398 | target[key] = source[key]; 399 | } 400 | } 401 | return target 402 | } 403 | 404 | function getKey (key) { 405 | var intKey = parseInt(key); 406 | if (intKey.toString() === key) { 407 | return intKey 408 | } 409 | return key 410 | } 411 | 412 | function clone (obj, createIfEmpty, assumeArray) { 413 | if (obj == null) { 414 | if (createIfEmpty) { 415 | if (assumeArray) { 416 | return [] 417 | } 418 | 419 | return {} 420 | } 421 | 422 | return obj 423 | } else if (isArray(obj)) { 424 | return obj.slice() 425 | } 426 | 427 | return assignToObj({}, obj) 428 | } 429 | 430 | function _deepMerge (dest, src) { 431 | if (dest !== src && isPlainObject(dest) && isPlainObject(src)) { 432 | var merged = {}; 433 | for (var key in dest) { 434 | if (_hasOwnProperty.call(dest, key)) { 435 | if (_hasOwnProperty.call(src, key)) { 436 | merged[key] = _deepMerge(dest[key], src[key]); 437 | } else { 438 | merged[key] = dest[key]; 439 | } 440 | } 441 | } 442 | 443 | for (key in src) { 444 | if (_hasOwnProperty.call(src, key)) { 445 | merged[key] = _deepMerge(dest[key], src[key]); 446 | } 447 | } 448 | return merged 449 | } 450 | return src 451 | } 452 | 453 | function _changeImmutable (dest, src, path, changeCallback) { 454 | if (isNumber(path)) { 455 | path = [path]; 456 | } 457 | if (isEmpty(path)) { 458 | return src 459 | } 460 | if (isString(path)) { 461 | return _changeImmutable(dest, src, path.split('.').map(getKey), changeCallback) 462 | } 463 | var currentPath = path[0]; 464 | 465 | if (!dest || dest === src) { 466 | dest = clone(src, true, isNumber(currentPath)); 467 | } 468 | 469 | if (path.length === 1) { 470 | return changeCallback(dest, currentPath) 471 | } 472 | 473 | if (src != null) { 474 | src = src[currentPath]; 475 | } 476 | 477 | dest[currentPath] = _changeImmutable(dest[currentPath], src, path.slice(1), changeCallback); 478 | 479 | return dest 480 | } 481 | 482 | var api = {}; 483 | api.set = function set (dest, src, path, value) { 484 | if (isEmpty(path)) { 485 | return value 486 | } 487 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 488 | clonedObj[finalPath] = value; 489 | return clonedObj 490 | }) 491 | }; 492 | 493 | api.update = function update (dest, src, path, updater) { 494 | if (isEmpty(path)) { 495 | return updater(clone(src)) 496 | } 497 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 498 | clonedObj[finalPath] = updater(clonedObj[finalPath]); 499 | return clonedObj 500 | }) 501 | }; 502 | 503 | api.push = function push (dest, src, path /*, values */) { 504 | var values = Array.prototype.slice.call(arguments, 3); 505 | if (isEmpty(path)) { 506 | if (!isArray(src)) { 507 | return values 508 | } else { 509 | return src.concat(values) 510 | } 511 | } 512 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 513 | if (!isArray(clonedObj[finalPath])) { 514 | clonedObj[finalPath] = values; 515 | } else { 516 | clonedObj[finalPath] = clonedObj[finalPath].concat(values); 517 | } 518 | return clonedObj 519 | }) 520 | }; 521 | 522 | api.insert = function insert (dest, src, path, value, at) { 523 | at = ~~at; 524 | if (isEmpty(path)) { 525 | if (!isArray(src)) { 526 | return [value] 527 | } 528 | 529 | var first = src.slice(0, at); 530 | first.push(value); 531 | return first.concat(src.slice(at)) 532 | } 533 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 534 | var arr = clonedObj[finalPath]; 535 | if (!isArray(arr)) { 536 | if (arr != null && typeof arr !== 'undefined') { 537 | throw new Error('Expected ' + path + 'to be an array. Instead got ' + typeof path) 538 | } 539 | arr = []; 540 | } 541 | 542 | var first = arr.slice(0, at); 543 | first.push(value); 544 | clonedObj[finalPath] = first.concat(arr.slice(at)); 545 | return clonedObj 546 | }) 547 | }; 548 | 549 | api.del = function del (dest, src, path) { 550 | if (isEmpty(path)) { 551 | return undefined 552 | } 553 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 554 | if (Array.isArray(clonedObj)) { 555 | if (clonedObj[finalPath] !== undefined) { 556 | clonedObj.splice(finalPath, 1); 557 | } 558 | } else { 559 | if (_hasOwnProperty.call(clonedObj, finalPath)) { 560 | delete clonedObj[finalPath]; 561 | } 562 | } 563 | return clonedObj 564 | }) 565 | }; 566 | 567 | api.assign = function assign (dest, src, path, source) { 568 | if (isEmpty(path)) { 569 | if (isEmpty(source)) { 570 | return src 571 | } 572 | return assignToObj(clone(src), source) 573 | } 574 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 575 | source = Object(source); 576 | var target = clone(clonedObj[finalPath], true); 577 | assignToObj(target, source); 578 | 579 | clonedObj[finalPath] = target; 580 | return clonedObj 581 | }) 582 | }; 583 | 584 | api.merge = function assign (dest, src, path, source) { 585 | if (isEmpty(path)) { 586 | if (isEmpty(source)) { 587 | return src 588 | } 589 | return _deepMerge(src, source) 590 | } 591 | return _changeImmutable(dest, src, path, function (clonedObj, finalPath) { 592 | source = Object(source); 593 | clonedObj[finalPath] = _deepMerge(clonedObj[finalPath], source); 594 | return clonedObj 595 | }) 596 | }; 597 | 598 | function wrap (src) { 599 | var dest = src; 600 | var committed = false; 601 | 602 | var transaction = Object.keys(api).reduce(function (proxy, prop) { 603 | /* istanbul ignore else */ 604 | if (typeof api[prop] === 'function') { 605 | proxy[prop] = function () { 606 | var args = [dest, src].concat(Array.prototype.slice.call(arguments)); 607 | 608 | if (committed) { 609 | throw new Error('Cannot call ' + prop + ' after `value`') 610 | } 611 | 612 | dest = api[prop].apply(null, args); 613 | 614 | return transaction 615 | }; 616 | } 617 | 618 | return proxy 619 | }, {}); 620 | 621 | transaction.value = function () { 622 | committed = true; 623 | return dest 624 | }; 625 | 626 | return transaction 627 | } 628 | 629 | var set = api.set.bind(null, null); 630 | var update = api.update.bind(null, null); 631 | var push = api.push.bind(null, null); 632 | var insert = api.insert.bind(null, null); 633 | var del = api.del.bind(null, null); 634 | var assign = api.assign.bind(null, null); 635 | var merge = api.merge.bind(null, null); 636 | var get = objectPath.get; 637 | 638 | exports.assign = assign; 639 | exports.del = del; 640 | exports.get = get; 641 | exports.insert = insert; 642 | exports.merge = merge; 643 | exports.push = push; 644 | exports.set = set; 645 | exports.update = update; 646 | exports.wrap = wrap; 647 | 648 | Object.defineProperty(exports, '__esModule', { value: true }); 649 | 650 | }))); 651 | --------------------------------------------------------------------------------