Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Support dot notation on array fields #2120

Merged
merged 4 commits into from
May 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading