diff --git a/src/index.ts b/src/index.ts index 29de0cc..c4bb4c2 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,7 +19,7 @@ import objectWithout from './utils/object-without'; import take from './utils/take'; import mergeDeep from './utils/merge-deep'; import setDeep from './utils/set-deep'; -import getDeep from './utils/get-deep'; +import getDeep, { getSubObject } from './utils/get-deep'; import { Changes, @@ -947,8 +947,9 @@ export class BufferedChangeset implements IChangeset { const baseContent = this.safeGet(content, baseKey); const subContent = this.getDeep(baseContent, remaining.join('.')); + const subChanges = getSubObject(changes, key); // give back and object that can further retrieve changes and/or content - const tree = new ObjectTreeNode(result, subContent, this.safeGet); + const tree = new ObjectTreeNode(subChanges, subContent, this.getDeep); return tree.proxy; } else if (typeof result !== 'undefined') { return result; @@ -976,7 +977,7 @@ export class BufferedChangeset implements IChangeset { } // may still access a value on the changes or content objects - const tree = new ObjectTreeNode(subChanges, subContent, this.safeGet); + const tree = new ObjectTreeNode(subChanges, subContent, this.getDeep); return tree.proxy; } diff --git a/src/utils/get-deep.ts b/src/utils/get-deep.ts index 5e045b8..3b6782f 100644 --- a/src/utils/get-deep.ts +++ b/src/utils/get-deep.ts @@ -1,3 +1,5 @@ +import Change from '../-private/change'; + /** * Handles both single key or nested string keys ('person.name') * @@ -12,7 +14,7 @@ export default function getDeep>( if (path.indexOf('.') === -1) { return obj[path as string]; } - const parts = typeof path === 'string' ? path.split('.') : path; + const parts: string[] = typeof path === 'string' ? path.split('.') : path; for (let i = 0; i < parts.length; i++) { if (obj === undefined || obj === null) { @@ -25,3 +27,32 @@ export default function getDeep>( return obj; } + +/** + * Returns subObject while skipping `Change` instances + * + * @method getSubObject + */ +export function getSubObject>(root: T, path: string | string[]): any { + let obj: T = root; + + if (path.indexOf('.') === -1) { + return obj[path as string]; + } + + const parts: string[] = typeof path === 'string' ? path.split('.') : path; + + for (let i = 0; i < parts.length; i++) { + if (obj === undefined || obj === null) { + return undefined; + } + + if (obj[parts[i]] instanceof Change) { + obj = obj[parts[i]].value; + } else { + obj = obj[parts[i]]; + } + } + + return obj; +} diff --git a/src/utils/object-tree-node.ts b/src/utils/object-tree-node.ts index ed73af6..cb85f75 100644 --- a/src/utils/object-tree-node.ts +++ b/src/utils/object-tree-node.ts @@ -1,5 +1,6 @@ import { ProxyHandler, Content } from '../types'; import isObject from './is-object'; +import setDeep from './set-deep'; import Change from '../-private/change'; import normalizeObject from './normalize-object'; @@ -98,6 +99,14 @@ class ObjectTreeNode implements ProxyHandler { this.children = Object.create(null); } + get(key: string) { + return this.safeGet(this.changes, key); + } + + set(key: string, value: unknown) { + return setDeep(this.changes, key, value); + } + unwrap(): Record { let changes = this.changes; diff --git a/test/index.test.ts b/test/index.test.ts index 83ef810..e7073c4 100644 --- a/test/index.test.ts +++ b/test/index.test.ts @@ -584,22 +584,6 @@ describe('Unit | Utility | changeset', () => { expect(result).toBeUndefined(); }); - it('nested objects will return correct values', () => { - dummyModel['org'] = { - asia: { sg: '_initial' }, // for the sake of disambiguating nulls - usa: { - ca: null, - ny: null, - ma: { name: null } - } - }; - - const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); - expect(dummyChangeset.get('org.asia.sg')).toBe('_initial'); - dummyChangeset.set('org.asia.sg', 'sg'); - expect(dummyChangeset.get('org.asia.sg')).toBe('sg'); - }); - it('#get nested objects can contain arrays', () => { expect.assertions(7); dummyModel.name = 'Bob'; @@ -1194,6 +1178,31 @@ describe('Unit | Utility | changeset', () => { expect(result.sync).toEqual(false); }); + it('#set nested objects at various level of tree will return correct values', () => { + dummyModel['org'] = { + asia: { sg: '_initial' }, // for the sake of disambiguating nulls + usa: { + ca: null, + ny: null, + ma: { name: null } + } + }; + + const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); + expect(dummyChangeset.get('org.asia.sg')).toBe('_initial'); + + dummyChangeset.set('org.asia.sg', 'sg'); + expect(dummyChangeset.get('org.asia.sg')).toBe('sg'); + + dummyChangeset.get('org.asia').set('sg', 'SG'); + expect(dummyChangeset.get('org.asia.sg')).toBe('SG'); + + dummyChangeset.get('org').set('asia.sg', 'sg'); + expect(dummyChangeset.get('org.asia.sg')).toBe('sg'); + + expect(dummyChangeset.get('org').get('asia.sg')).toBe('sg'); + }); + it('it accepts async validations', async () => { const dummyChangeset = Changeset(dummyModel, lookupValidator(dummyValidations)); /* const dummyChangeset = Changeset(dummyModel, dummyValidator); */ diff --git a/test/utils/get-deep.test.ts b/test/utils/get-deep.test.ts index 6b1c0f6..1c77681 100644 --- a/test/utils/get-deep.test.ts +++ b/test/utils/get-deep.test.ts @@ -1,4 +1,5 @@ -import getDeep from '../../src/utils/get-deep'; +import getDeep, { getSubObject } from '../../src/utils/get-deep'; +import Change from '../../src/-private/change'; describe('Unit | Utility | get deep', () => { it('it returns value', () => { @@ -28,4 +29,57 @@ describe('Unit | Utility | get deep', () => { expect(value).toEqual({ other: 'Ivan' }); }); + + it('it returns Change', () => { + const objA = { name: new Change({ other: 'Ivan' }), foo: { other: 'bar' } }; + + let value = getDeep(objA, 'name'); + expect(value).toEqual(new Change({ other: 'Ivan' })); + + value = getDeep(objA, 'name.other'); + expect(value).toEqual(undefined); + }); +}); + +describe('Unit | Utility | get sub object', () => { + it('it returns value', () => { + const objA = { other: 'Ivan' }; + const value = getSubObject(objA, 'foo'); + + expect(value).toBeUndefined(); + }); + + it('it returns value from nested', () => { + const objA = { name: { other: 'Ivan' } }; + const value = getSubObject(objA, 'name'); + + expect(value).toEqual({ other: 'Ivan' }); + }); + + it('it returns value from deep nested', () => { + const objA = { name: { other: 'Ivan' } }; + const value = getSubObject(objA, 'name.other'); + + expect(value).toBe('Ivan'); + }); + + it('it returns multiple values from nested', () => { + const objA = { name: { other: 'Ivan' }, foo: { other: 'bar' } }; + const value = getSubObject(objA, 'name'); + + expect(value).toEqual({ other: 'Ivan' }); + }); + + it('it returns object inside Change', () => { + const objA = { name: new Change({ other: 'Ivan' }), foo: { other: 'bar' } }; + + let value = getSubObject(objA, 'name'); + expect(value).toEqual(objA.name); + + value = getSubObject(objA, 'foo'); + expect(value).toEqual(objA.foo); + + value = getSubObject(objA, 'name.other'); + expect(value).toEqual('Ivan'); + }); });