Skip to content

Commit

Permalink
feat: Support dot notation on array fields (#2120)
Browse files Browse the repository at this point in the history
  • Loading branch information
dplewis authored May 17, 2024
1 parent 8e189cf commit 25ec684
Show file tree
Hide file tree
Showing 2 changed files with 121 additions and 11 deletions.
57 changes: 46 additions & 11 deletions src/ObjectStateMutations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand All @@ -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;
}
}

Expand All @@ -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 &&
Expand Down
75 changes: 75 additions & 0 deletions src/__tests__/ObjectStateMutations-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {};
Expand All @@ -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: {},
Expand Down

0 comments on commit 25ec684

Please sign in to comment.