Skip to content

Commit 05ba963

Browse files
dstarkeflovilmart
authored andcommitted
fix(ParseQuery): Select queries should not erase cached data from unselected fields (#409)
* Select queries should not erase cached data from unselected fields * fix some spacing
1 parent 1cd3726 commit 05ba963

File tree

2 files changed

+309
-2
lines changed

2 files changed

+309
-2
lines changed

src/ParseQuery.js

Lines changed: 86 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,70 @@ function quote(s: string) {
4444
return '\\Q' + s.replace('\\E', '\\E\\\\E\\Q') + '\\E';
4545
}
4646

47+
/**
48+
* Handles pre-populating the result data of a query with select fields,
49+
* making sure that the data object contains keys for all objects that have
50+
* been requested with a select, so that our cached state updates correctly.
51+
*/
52+
function handleSelectResult(data: any, select: Array<string>){
53+
var serverDataMask = {};
54+
55+
select.forEach((field) => {
56+
let hasSubObjectSelect = field.indexOf(".") !== -1;
57+
if (!hasSubObjectSelect && !data.hasOwnProperty(field)){
58+
// this field was selected, but is missing from the retrieved data
59+
data[field] = undefined
60+
} else if (hasSubObjectSelect) {
61+
// this field references a sub-object,
62+
// so we need to walk down the path components
63+
let pathComponents = field.split(".");
64+
var obj = data;
65+
var serverMask = serverDataMask;
66+
67+
pathComponents.forEach((component, index, arr) => {
68+
// add keys if the expected data is missing
69+
if (!obj[component]) {
70+
obj[component] = (index == arr.length-1) ? undefined : {};
71+
}
72+
obj = obj[component];
73+
74+
//add this path component to the server mask so we can fill it in later if needed
75+
if (index < arr.length-1) {
76+
if (!serverMask[component]) {
77+
serverMask[component] = {};
78+
}
79+
}
80+
});
81+
}
82+
});
83+
84+
if (Object.keys(serverDataMask).length > 0) {
85+
// When selecting from sub-objects, we don't want to blow away the missing
86+
// information that we may have retrieved before. We've already added any
87+
// missing selected keys to sub-objects, but we still need to add in the
88+
// data for any previously retrieved sub-objects that were not selected.
89+
90+
let serverData = CoreManager.getObjectStateController().getServerData({id:data.objectId, className:data.className});
91+
92+
function copyMissingDataWithMask(src, dest, mask, copyThisLevel){
93+
//copy missing elements at this level
94+
if (copyThisLevel) {
95+
for (var key in src) {
96+
if (src.hasOwnProperty(key) && !dest.hasOwnProperty(key)) {
97+
dest[key] = src[key]
98+
}
99+
}
100+
}
101+
for (var key in mask) {
102+
//traverse into objects as needed
103+
copyMissingDataWithMask(src[key], dest[key], mask[key], true);
104+
}
105+
}
106+
107+
copyMissingDataWithMask(serverData, data, serverDataMask, false);
108+
}
109+
}
110+
47111
/**
48112
* Creates a new parse Parse.Query for the given Parse.Object subclass.
49113
* @class Parse.Query
@@ -273,6 +337,8 @@ export default class ParseQuery {
273337

274338
let controller = CoreManager.getQueryController();
275339

340+
let select = this._select;
341+
276342
return controller.find(
277343
this.className,
278344
this.toJSON(),
@@ -285,7 +351,15 @@ export default class ParseQuery {
285351
if (!data.className) {
286352
data.className = override;
287353
}
288-
return ParseObject.fromJSON(data, true);
354+
355+
// Make sure the data object contains keys for all objects that
356+
// have been requested with a select, so that our cached state
357+
// updates correctly.
358+
if (select) {
359+
handleSelectResult(data, select);
360+
}
361+
362+
return ParseObject.fromJSON(data, !select);
289363
});
290364
})._thenRunCallbacks(options);
291365
}
@@ -371,6 +445,8 @@ export default class ParseQuery {
371445
var params = this.toJSON();
372446
params.limit = 1;
373447

448+
var select = this._select;
449+
374450
return controller.find(
375451
this.className,
376452
params,
@@ -383,7 +459,15 @@ export default class ParseQuery {
383459
if (!objects[0].className) {
384460
objects[0].className = this.className;
385461
}
386-
return ParseObject.fromJSON(objects[0], true);
462+
463+
// Make sure the data object contains keys for all objects that
464+
// have been requested with a select, so that our cached state
465+
// updates correctly.
466+
if (select) {
467+
handleSelectResult(objects[0], select);
468+
}
469+
470+
return ParseObject.fromJSON(objects[0], !select);
387471
})._thenRunCallbacks(options);
388472
}
389473

src/__tests__/ParseQuery-test.js

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99

1010
jest.dontMock('../CoreManager');
1111
jest.dontMock('../encode');
12+
jest.dontMock('../decode');
1213
jest.dontMock('../ParseError');
1314
jest.dontMock('../ParseGeoPoint');
1415
jest.dontMock('../ParsePromise');
1516
jest.dontMock('../ParseQuery');
17+
jest.dontMock('../SingleInstanceStateController');
18+
jest.dontMock('../UniqueInstanceStateController');
19+
jest.dontMock('../ObjectStateMutations');
1620

1721
var mockObject = function(className) {
1822
this.className = className;
@@ -1332,4 +1336,223 @@ describe('ParseQuery', () => {
13321336
done();
13331337
});
13341338
});
1339+
1340+
1341+
1342+
it('overrides cached object with query results', (done) => {
1343+
jest.dontMock("../ParseObject");
1344+
jest.resetModules();
1345+
ParseObject = require('../ParseObject').default;
1346+
CoreManager = require('../CoreManager');
1347+
ParseQuery = require('../ParseQuery').default;
1348+
1349+
ParseObject.enableSingleInstance();
1350+
1351+
var objectToReturn = {
1352+
objectId: 'T01',
1353+
name: 'Name',
1354+
other: 'other',
1355+
className:"Thing",
1356+
createdAt: '2017-01-10T10:00:00Z'
1357+
};
1358+
1359+
CoreManager.setQueryController({
1360+
find(className, params, options) {
1361+
return ParsePromise.as({
1362+
results: [objectToReturn]
1363+
});
1364+
}
1365+
});
1366+
1367+
var q = new ParseQuery("Thing");
1368+
var testObject;
1369+
q.find().then((results) => {
1370+
testObject = results[0];
1371+
1372+
expect(testObject.get("name")).toBe("Name");
1373+
expect(testObject.get("other")).toBe("other");
1374+
1375+
objectToReturn = { objectId: 'T01', name: 'Name2'};
1376+
var q2 = new ParseQuery("Thing");
1377+
return q2.find();
1378+
}).then((results) => {
1379+
expect(results[0].get("name")).toBe("Name2");
1380+
expect(results[0].has("other")).toBe(false);
1381+
}).then(() => {
1382+
expect(testObject.get("name")).toBe("Name2");
1383+
expect(testObject.has("other")).toBe(false);
1384+
done();
1385+
});
1386+
});
1387+
1388+
it('does not override unselected fields with select query results', (done) => {
1389+
jest.dontMock("../ParseObject");
1390+
jest.resetModules();
1391+
ParseObject = require('../ParseObject').default;
1392+
CoreManager = require('../CoreManager');
1393+
ParseQuery = require('../ParseQuery').default;
1394+
1395+
ParseObject.enableSingleInstance();
1396+
1397+
var objectToReturn = {
1398+
objectId: 'T01',
1399+
name: 'Name',
1400+
other: 'other',
1401+
tbd: 'exists',
1402+
className:"Thing",
1403+
createdAt: '2017-01-10T10:00:00Z',
1404+
subObject: {key1:"value", key2:"value2", key3:"thisWillGoAway"}
1405+
};
1406+
1407+
CoreManager.setQueryController({
1408+
find(className, params, options) {
1409+
return ParsePromise.as({
1410+
results: [objectToReturn]
1411+
});
1412+
}
1413+
});
1414+
1415+
var q = new ParseQuery("Thing");
1416+
var testObject;
1417+
return q.find().then((results) => {
1418+
testObject = results[0];
1419+
1420+
expect(testObject.get("name")).toBe("Name");
1421+
expect(testObject.get("other")).toBe("other");
1422+
expect(testObject.has("tbd")).toBe(true);
1423+
expect(testObject.get("subObject").key1).toBe("value");
1424+
expect(testObject.get("subObject").key2).toBe("value2");
1425+
expect(testObject.get("subObject").key3).toBe("thisWillGoAway");
1426+
1427+
var q2 = new ParseQuery("Thing");
1428+
q2.select("other", "tbd", "subObject.key1", "subObject.key3");
1429+
objectToReturn = { objectId: 'T01', other: 'other2', subObject:{key1:"updatedValue"}};
1430+
return q2.find();
1431+
}).then((results) => {
1432+
expect(results[0].get("name")).toBe("Name"); //query didn't select this
1433+
expect(results[0].get("other")).toBe("other2"); //query selected and updated this
1434+
expect(results[0].has("tbd")).toBe(false); //query selected this and it wasn't returned
1435+
//sub-objects should work similarly
1436+
expect(results[0].get("subObject").key1).toBe("updatedValue");
1437+
expect(results[0].get("subObject").key2).toBe("value2");
1438+
expect(results[0].get("subObject").key3).toBeUndefined();
1439+
}).then(() => {
1440+
expect(testObject.get("name")).toBe("Name");
1441+
expect(testObject.get("other")).toBe("other2");
1442+
expect(testObject.has("tbd")).toBe(false);
1443+
expect(testObject.get("subObject").key1).toBe("updatedValue");
1444+
expect(testObject.get("subObject").key2).toBe("value2");
1445+
expect(testObject.get("subObject").key3).toBeUndefined();
1446+
done();
1447+
}, (error) => {
1448+
done.fail(error);
1449+
});
1450+
});
1451+
1452+
it('overrides cached object with first() results', (done) => {
1453+
jest.dontMock("../ParseObject");
1454+
jest.resetModules();
1455+
ParseObject = require('../ParseObject').default;
1456+
CoreManager = require('../CoreManager');
1457+
ParseQuery = require('../ParseQuery').default;
1458+
1459+
ParseObject.enableSingleInstance();
1460+
1461+
var objectToReturn = {
1462+
objectId: 'T01',
1463+
name: 'Name',
1464+
other: 'other',
1465+
className:"Thing",
1466+
createdAt: '2017-01-10T10:00:00Z'
1467+
};
1468+
1469+
CoreManager.setQueryController({
1470+
find(className, params, options) {
1471+
return ParsePromise.as({
1472+
results: [objectToReturn]
1473+
});
1474+
}
1475+
});
1476+
1477+
var q = new ParseQuery("Thing");
1478+
var testObject;
1479+
q.first().then((result) => {
1480+
testObject = result;
1481+
1482+
expect(testObject.get("name")).toBe("Name");
1483+
expect(testObject.get("other")).toBe("other");
1484+
1485+
objectToReturn = { objectId: 'T01', name: 'Name2'};
1486+
var q2 = new ParseQuery("Thing");
1487+
return q2.first();
1488+
}).then((result) => {
1489+
expect(result.get("name")).toBe("Name2");
1490+
expect(result.has("other")).toBe(false);
1491+
}).then(() => {
1492+
expect(testObject.get("name")).toBe("Name2");
1493+
expect(testObject.has("other")).toBe(false);
1494+
done();
1495+
});
1496+
});
1497+
1498+
it('does not override unselected fields for first() on select query', (done) => {
1499+
jest.dontMock("../ParseObject");
1500+
jest.resetModules();
1501+
ParseObject = require('../ParseObject').default;
1502+
CoreManager = require('../CoreManager');
1503+
ParseQuery = require('../ParseQuery').default;
1504+
1505+
ParseObject.enableSingleInstance();
1506+
1507+
var objectToReturn = {
1508+
objectId: 'T01',
1509+
name: 'Name',
1510+
other: 'other',
1511+
tbd: 'exists',
1512+
className:"Thing",
1513+
subObject: {key1:"value", key2:"value2", key3:"thisWillGoAway"},
1514+
createdAt: '2017-01-10T10:00:00Z',
1515+
};
1516+
1517+
CoreManager.setQueryController({
1518+
find(className, params, options) {
1519+
return ParsePromise.as({
1520+
results: [objectToReturn]
1521+
});
1522+
}
1523+
});
1524+
1525+
var q = new ParseQuery("Thing");
1526+
var testObject;
1527+
return q.first().then((result) => {
1528+
testObject = result;
1529+
1530+
expect(testObject.get("name")).toBe("Name");
1531+
expect(testObject.get("other")).toBe("other");
1532+
expect(testObject.has("tbd")).toBe(true);
1533+
1534+
var q2 = new ParseQuery("Thing");
1535+
q2.select("other", "tbd", "subObject.key1", "subObject.key3");
1536+
objectToReturn = { objectId: 'T01', other: 'other2', subObject:{key1:"updatedValue"}};
1537+
return q2.first();
1538+
}).then((result) => {
1539+
expect(result.get("name")).toBe("Name"); //query didn't select this
1540+
expect(result.get("other")).toBe("other2"); //query selected and updated this
1541+
expect(result.has("tbd")).toBe(false); //query selected this and it wasn't returned
1542+
//sub-objects should work similarly
1543+
expect(result.get("subObject").key1).toBe("updatedValue");
1544+
expect(result.get("subObject").key2).toBe("value2");
1545+
expect(result.get("subObject").key3).toBeUndefined();
1546+
}).then(() => {
1547+
expect(testObject.get("name")).toBe("Name");
1548+
expect(testObject.get("other")).toBe("other2");
1549+
expect(testObject.has("tbd")).toBe(false);
1550+
expect(testObject.get("subObject").key1).toBe("updatedValue");
1551+
expect(testObject.get("subObject").key2).toBe("value2");
1552+
expect(testObject.get("subObject").key3).toBeUndefined();
1553+
done();
1554+
}, (error) => {
1555+
done.fail(error);
1556+
});
1557+
});
13351558
});

0 commit comments

Comments
 (0)