diff --git a/src/ObjectStateMutations.ts b/src/ObjectStateMutations.ts index 2a11cd856..b3d2674cb 100644 --- a/src/ObjectStateMutations.ts +++ b/src/ObjectStateMutations.ts @@ -114,16 +114,25 @@ export function estimateAttributes( } } else { if (attr.includes('.')) { - // convert a.b.c into { a: { b: { c: value } } } + // similar to nestedSet function const fields = attr.split('.'); const last = fields[fields.length - 1]; let object = data; for (let i = 0; i < fields.length - 1; i++) { const key = fields[i]; if (!(key in object)) { - object[key] = {}; + const nextKey = fields[i + 1]; + if (!isNaN(nextKey)) { + object[key] = []; + } else { + object[key] = {}; + } } else { - object[key] = { ...object[key] }; + if (Array.isArray(object[key])) { + object[key] = [ ...object[key] ]; + } else { + object[key] = { ...object[key] }; + } } object = object[key]; } @@ -137,18 +146,34 @@ export function estimateAttributes( return data; } +/** + * Allows setting properties/variables deep in an object. + * Converts a.b into { a: { b: value } } for dot notation on Objects + * Converts a.0.b into { a: [{ b: value }] } for dot notation on Arrays + * + * @param obj The object to assign the value to + * @param key The key to assign. If it's in a deeper path, then use dot notation (`prop1.prop2.prop3`) + * Note that intermediate object(s) in the nested path are automatically created if they don't exist. + * @param value The value to assign. If it's an `undefined` then the key is deleted. + */ function nestedSet(obj, key, value) { - const path = key.split('.'); - for (let i = 0; i < path.length - 1; i++) { - if (!(path[i] in obj)) { - obj[path[i]] = {}; + const paths = key.split('.'); + for (let i = 0; i < paths.length - 1; i++) { + const path = paths[i]; + if (!(path in obj)) { + const nextPath = paths[i + 1]; + if (!isNaN(nextPath)) { + obj[path] = []; + } else { + obj[path] = {}; + } } - obj = obj[path[i]]; + obj = obj[path]; } if (typeof value === 'undefined') { - delete obj[path[path.length - 1]]; + delete obj[paths[paths.length - 1]]; } else { - obj[path[path.length - 1]] = value; + obj[paths[paths.length - 1]] = value; } } @@ -159,7 +184,17 @@ export function commitServerChanges( ) { const ParseObject = CoreManager.getParseObject(); for (const attr in changes) { - const val = changes[attr]; + let val = changes[attr]; + // Check for JSON array { '0': { something }, '1': { something } } + if ( + val && + typeof val === 'object' && + !Array.isArray(val) && + Object.keys(val).length > 0 && + Object.keys(val).some(k => !isNaN(parseInt(k))) + ) { + val = Object.values(val); + } nestedSet(serverData, attr, val); if ( val && diff --git a/src/__tests__/ObjectStateMutations-test.js b/src/__tests__/ObjectStateMutations-test.js index 34f7885a1..898f7e606 100644 --- a/src/__tests__/ObjectStateMutations-test.js +++ b/src/__tests__/ObjectStateMutations-test.js @@ -196,6 +196,53 @@ describe('ObjectStateMutations', () => { }); }); + it('can estimate attributes for nested array documents', () => { + // Test without initial value + let serverData = { _id: 'someId', className: 'bug' }; + let pendingOps = [{ 'items.0.count': new ParseOps.IncrementOp(1) }]; + expect( + ObjectStateMutations.estimateAttributes(serverData, pendingOps, 'someClass', 'someId') + ).toEqual({ + _id: 'someId', + items: [{ count: 1 }], + className: 'bug', + }); + + // Test one level nested + serverData = { + _id: 'someId', + items: [{ value: 'a', count: 5 }, { value: 'b', count: 1 } ], + className: 'bug', + number: 2 + } + pendingOps = [{ 'items.0.count': new ParseOps.IncrementOp(1) }]; + expect( + ObjectStateMutations.estimateAttributes(serverData, pendingOps, 'someClass', 'someId') + ).toEqual({ + _id: 'someId', + items: [{ value: 'a', count: 6 }, { value: 'b', count: 1 }], + className: 'bug', + number: 2 + }); + + // Test multiple level nested fields + serverData = { + _id: 'someId', + items: [{ value: { count: 54 }, count: 5 }, { value: 'b', count: 1 }], + className: 'bug', + number: 2 + } + pendingOps = [{ 'items.0.value.count': new ParseOps.IncrementOp(6) }]; + expect( + ObjectStateMutations.estimateAttributes(serverData, pendingOps, 'someClass', 'someId') + ).toEqual({ + _id: 'someId', + items: [{ value: { count: 60 }, count: 5 }, { value: 'b', count: 1 }], + className: 'bug', + number: 2 + }); + }); + it('can commit changes from the server', () => { const serverData = {}; const objectCache = {}; @@ -218,6 +265,34 @@ describe('ObjectStateMutations', () => { expect(objectCache).toEqual({ data: '{"count":5}' }); }); + it('can commit dot notation array changes from the server', () => { + const serverData = { items: [{ value: 'a', count: 5 }, { value: 'b', count: 1 }] }; + ObjectStateMutations.commitServerChanges(serverData, {}, { + 'items.0.count': 15, + 'items.1.count': 4, + }); + expect(serverData).toEqual({ items: [{ value: 'a', count: 15 }, { value: 'b', count: 4 }] }); + }); + + it('can commit dot notation array changes from the server to empty serverData', () => { + const serverData = {}; + ObjectStateMutations.commitServerChanges(serverData, {}, { + 'items.0.count': 15, + 'items.1.count': 4, + }); + expect(serverData).toEqual({ items: [{ count: 15 }, { count: 4 }] }); + }); + + it('can commit nested json array changes from the server to empty serverData', () => { + const serverData = {}; + const objectCache = {}; + ObjectStateMutations.commitServerChanges(serverData, objectCache, { + items: { '0': { count: 20 }, '1': { count: 5 } } + }); + expect(serverData).toEqual({ items: [ { count: 20 }, { count: 5 } ] }); + expect(objectCache).toEqual({ items: '[{"count":20},{"count":5}]' }); + }); + it('can generate a default state for implementations', () => { expect(ObjectStateMutations.defaultState()).toEqual({ serverData: {},