Skip to content

Commit 25ec684

Browse files
authored
feat: Support dot notation on array fields (#2120)
1 parent 8e189cf commit 25ec684

File tree

2 files changed

+121
-11
lines changed

2 files changed

+121
-11
lines changed

src/ObjectStateMutations.ts

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -114,16 +114,25 @@ export function estimateAttributes(
114114
}
115115
} else {
116116
if (attr.includes('.')) {
117-
// convert a.b.c into { a: { b: { c: value } } }
117+
// similar to nestedSet function
118118
const fields = attr.split('.');
119119
const last = fields[fields.length - 1];
120120
let object = data;
121121
for (let i = 0; i < fields.length - 1; i++) {
122122
const key = fields[i];
123123
if (!(key in object)) {
124-
object[key] = {};
124+
const nextKey = fields[i + 1];
125+
if (!isNaN(nextKey)) {
126+
object[key] = [];
127+
} else {
128+
object[key] = {};
129+
}
125130
} else {
126-
object[key] = { ...object[key] };
131+
if (Array.isArray(object[key])) {
132+
object[key] = [ ...object[key] ];
133+
} else {
134+
object[key] = { ...object[key] };
135+
}
127136
}
128137
object = object[key];
129138
}
@@ -137,18 +146,34 @@ export function estimateAttributes(
137146
return data;
138147
}
139148

149+
/**
150+
* Allows setting properties/variables deep in an object.
151+
* Converts a.b into { a: { b: value } } for dot notation on Objects
152+
* Converts a.0.b into { a: [{ b: value }] } for dot notation on Arrays
153+
*
154+
* @param obj The object to assign the value to
155+
* @param key The key to assign. If it's in a deeper path, then use dot notation (`prop1.prop2.prop3`)
156+
* Note that intermediate object(s) in the nested path are automatically created if they don't exist.
157+
* @param value The value to assign. If it's an `undefined` then the key is deleted.
158+
*/
140159
function nestedSet(obj, key, value) {
141-
const path = key.split('.');
142-
for (let i = 0; i < path.length - 1; i++) {
143-
if (!(path[i] in obj)) {
144-
obj[path[i]] = {};
160+
const paths = key.split('.');
161+
for (let i = 0; i < paths.length - 1; i++) {
162+
const path = paths[i];
163+
if (!(path in obj)) {
164+
const nextPath = paths[i + 1];
165+
if (!isNaN(nextPath)) {
166+
obj[path] = [];
167+
} else {
168+
obj[path] = {};
169+
}
145170
}
146-
obj = obj[path[i]];
171+
obj = obj[path];
147172
}
148173
if (typeof value === 'undefined') {
149-
delete obj[path[path.length - 1]];
174+
delete obj[paths[paths.length - 1]];
150175
} else {
151-
obj[path[path.length - 1]] = value;
176+
obj[paths[paths.length - 1]] = value;
152177
}
153178
}
154179

@@ -159,7 +184,17 @@ export function commitServerChanges(
159184
) {
160185
const ParseObject = CoreManager.getParseObject();
161186
for (const attr in changes) {
162-
const val = changes[attr];
187+
let val = changes[attr];
188+
// Check for JSON array { '0': { something }, '1': { something } }
189+
if (
190+
val &&
191+
typeof val === 'object' &&
192+
!Array.isArray(val) &&
193+
Object.keys(val).length > 0 &&
194+
Object.keys(val).some(k => !isNaN(parseInt(k)))
195+
) {
196+
val = Object.values(val);
197+
}
163198
nestedSet(serverData, attr, val);
164199
if (
165200
val &&

src/__tests__/ObjectStateMutations-test.js

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,53 @@ describe('ObjectStateMutations', () => {
196196
});
197197
});
198198

199+
it('can estimate attributes for nested array documents', () => {
200+
// Test without initial value
201+
let serverData = { _id: 'someId', className: 'bug' };
202+
let pendingOps = [{ 'items.0.count': new ParseOps.IncrementOp(1) }];
203+
expect(
204+
ObjectStateMutations.estimateAttributes(serverData, pendingOps, 'someClass', 'someId')
205+
).toEqual({
206+
_id: 'someId',
207+
items: [{ count: 1 }],
208+
className: 'bug',
209+
});
210+
211+
// Test one level nested
212+
serverData = {
213+
_id: 'someId',
214+
items: [{ value: 'a', count: 5 }, { value: 'b', count: 1 } ],
215+
className: 'bug',
216+
number: 2
217+
}
218+
pendingOps = [{ 'items.0.count': new ParseOps.IncrementOp(1) }];
219+
expect(
220+
ObjectStateMutations.estimateAttributes(serverData, pendingOps, 'someClass', 'someId')
221+
).toEqual({
222+
_id: 'someId',
223+
items: [{ value: 'a', count: 6 }, { value: 'b', count: 1 }],
224+
className: 'bug',
225+
number: 2
226+
});
227+
228+
// Test multiple level nested fields
229+
serverData = {
230+
_id: 'someId',
231+
items: [{ value: { count: 54 }, count: 5 }, { value: 'b', count: 1 }],
232+
className: 'bug',
233+
number: 2
234+
}
235+
pendingOps = [{ 'items.0.value.count': new ParseOps.IncrementOp(6) }];
236+
expect(
237+
ObjectStateMutations.estimateAttributes(serverData, pendingOps, 'someClass', 'someId')
238+
).toEqual({
239+
_id: 'someId',
240+
items: [{ value: { count: 60 }, count: 5 }, { value: 'b', count: 1 }],
241+
className: 'bug',
242+
number: 2
243+
});
244+
});
245+
199246
it('can commit changes from the server', () => {
200247
const serverData = {};
201248
const objectCache = {};
@@ -218,6 +265,34 @@ describe('ObjectStateMutations', () => {
218265
expect(objectCache).toEqual({ data: '{"count":5}' });
219266
});
220267

268+
it('can commit dot notation array changes from the server', () => {
269+
const serverData = { items: [{ value: 'a', count: 5 }, { value: 'b', count: 1 }] };
270+
ObjectStateMutations.commitServerChanges(serverData, {}, {
271+
'items.0.count': 15,
272+
'items.1.count': 4,
273+
});
274+
expect(serverData).toEqual({ items: [{ value: 'a', count: 15 }, { value: 'b', count: 4 }] });
275+
});
276+
277+
it('can commit dot notation array changes from the server to empty serverData', () => {
278+
const serverData = {};
279+
ObjectStateMutations.commitServerChanges(serverData, {}, {
280+
'items.0.count': 15,
281+
'items.1.count': 4,
282+
});
283+
expect(serverData).toEqual({ items: [{ count: 15 }, { count: 4 }] });
284+
});
285+
286+
it('can commit nested json array changes from the server to empty serverData', () => {
287+
const serverData = {};
288+
const objectCache = {};
289+
ObjectStateMutations.commitServerChanges(serverData, objectCache, {
290+
items: { '0': { count: 20 }, '1': { count: 5 } }
291+
});
292+
expect(serverData).toEqual({ items: [ { count: 20 }, { count: 5 } ] });
293+
expect(objectCache).toEqual({ items: '[{"count":20},{"count":5}]' });
294+
});
295+
221296
it('can generate a default state for implementations', () => {
222297
expect(ObjectStateMutations.defaultState()).toEqual({
223298
serverData: {},

0 commit comments

Comments
 (0)