Skip to content

Select queries should not erase cached data from unselected fields #409

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

Merged
merged 2 commits into from
May 23, 2017
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
88 changes: 86 additions & 2 deletions src/ParseQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,70 @@ function quote(s: string) {
return '\\Q' + s.replace('\\E', '\\E\\\\E\\Q') + '\\E';
}

/**
* Handles pre-populating the result data of a query with select fields,
* making sure that the data object contains keys for all objects that have
* been requested with a select, so that our cached state updates correctly.
*/
function handleSelectResult(data: any, select: Array<string>){
var serverDataMask = {};

select.forEach((field) => {
let hasSubObjectSelect = field.indexOf(".") !== -1;
if (!hasSubObjectSelect && !data.hasOwnProperty(field)){
// this field was selected, but is missing from the retrieved data
data[field] = undefined
} else if (hasSubObjectSelect) {
// this field references a sub-object,
// so we need to walk down the path components
let pathComponents = field.split(".");
var obj = data;
var serverMask = serverDataMask;

pathComponents.forEach((component, index, arr) => {
// add keys if the expected data is missing
if (!obj[component]) {
obj[component] = (index == arr.length-1) ? undefined : {};
}
obj = obj[component];

//add this path component to the server mask so we can fill it in later if needed
if (index < arr.length-1) {
if (!serverMask[component]) {
serverMask[component] = {};
}
}
});
}
});

if (Object.keys(serverDataMask).length > 0) {
// When selecting from sub-objects, we don't want to blow away the missing
// information that we may have retrieved before. We've already added any
// missing selected keys to sub-objects, but we still need to add in the
// data for any previously retrieved sub-objects that were not selected.

let serverData = CoreManager.getObjectStateController().getServerData({id:data.objectId, className:data.className});

function copyMissingDataWithMask(src, dest, mask, copyThisLevel){
//copy missing elements at this level
if (copyThisLevel) {
for (var key in src) {
if (src.hasOwnProperty(key) && !dest.hasOwnProperty(key)) {
dest[key] = src[key]
}
}
}
for (var key in mask) {
//traverse into objects as needed
copyMissingDataWithMask(src[key], dest[key], mask[key], true);
}
}

copyMissingDataWithMask(serverData, data, serverDataMask, false);
}
}

/**
* Creates a new parse Parse.Query for the given Parse.Object subclass.
* @class Parse.Query
Expand Down Expand Up @@ -273,6 +337,8 @@ export default class ParseQuery {

let controller = CoreManager.getQueryController();

let select = this._select;

return controller.find(
this.className,
this.toJSON(),
Expand All @@ -285,7 +351,15 @@ export default class ParseQuery {
if (!data.className) {
data.className = override;
}
return ParseObject.fromJSON(data, true);

// Make sure the data object contains keys for all objects that
// have been requested with a select, so that our cached state
// updates correctly.
if (select) {
handleSelectResult(data, select);
}

return ParseObject.fromJSON(data, !select);
});
})._thenRunCallbacks(options);
}
Expand Down Expand Up @@ -371,6 +445,8 @@ export default class ParseQuery {
var params = this.toJSON();
params.limit = 1;

var select = this._select;

return controller.find(
this.className,
params,
Expand All @@ -383,7 +459,15 @@ export default class ParseQuery {
if (!objects[0].className) {
objects[0].className = this.className;
}
return ParseObject.fromJSON(objects[0], true);

// Make sure the data object contains keys for all objects that
// have been requested with a select, so that our cached state
// updates correctly.
if (select) {
handleSelectResult(objects[0], select);
}

return ParseObject.fromJSON(objects[0], !select);
})._thenRunCallbacks(options);
}

Expand Down
223 changes: 223 additions & 0 deletions src/__tests__/ParseQuery-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,14 @@

jest.dontMock('../CoreManager');
jest.dontMock('../encode');
jest.dontMock('../decode');
jest.dontMock('../ParseError');
jest.dontMock('../ParseGeoPoint');
jest.dontMock('../ParsePromise');
jest.dontMock('../ParseQuery');
jest.dontMock('../SingleInstanceStateController');
jest.dontMock('../UniqueInstanceStateController');
jest.dontMock('../ObjectStateMutations');

var mockObject = function(className) {
this.className = className;
Expand Down Expand Up @@ -1332,4 +1336,223 @@ describe('ParseQuery', () => {
done();
});
});



it('overrides cached object with query results', (done) => {
jest.dontMock("../ParseObject");
jest.resetModules();
ParseObject = require('../ParseObject').default;
CoreManager = require('../CoreManager');
ParseQuery = require('../ParseQuery').default;

ParseObject.enableSingleInstance();

var objectToReturn = {
objectId: 'T01',
name: 'Name',
other: 'other',
className:"Thing",
createdAt: '2017-01-10T10:00:00Z'
};

CoreManager.setQueryController({
find(className, params, options) {
return ParsePromise.as({
results: [objectToReturn]
});
}
});

var q = new ParseQuery("Thing");
var testObject;
q.find().then((results) => {
testObject = results[0];

expect(testObject.get("name")).toBe("Name");
expect(testObject.get("other")).toBe("other");

objectToReturn = { objectId: 'T01', name: 'Name2'};
var q2 = new ParseQuery("Thing");
return q2.find();
}).then((results) => {
expect(results[0].get("name")).toBe("Name2");
expect(results[0].has("other")).toBe(false);
}).then(() => {
expect(testObject.get("name")).toBe("Name2");
expect(testObject.has("other")).toBe(false);
done();
});
});

it('does not override unselected fields with select query results', (done) => {
jest.dontMock("../ParseObject");
jest.resetModules();
ParseObject = require('../ParseObject').default;
CoreManager = require('../CoreManager');
ParseQuery = require('../ParseQuery').default;

ParseObject.enableSingleInstance();

var objectToReturn = {
objectId: 'T01',
name: 'Name',
other: 'other',
tbd: 'exists',
className:"Thing",
createdAt: '2017-01-10T10:00:00Z',
subObject: {key1:"value", key2:"value2", key3:"thisWillGoAway"}
};

CoreManager.setQueryController({
find(className, params, options) {
return ParsePromise.as({
results: [objectToReturn]
});
}
});

var q = new ParseQuery("Thing");
var testObject;
return q.find().then((results) => {
testObject = results[0];

expect(testObject.get("name")).toBe("Name");
expect(testObject.get("other")).toBe("other");
expect(testObject.has("tbd")).toBe(true);
expect(testObject.get("subObject").key1).toBe("value");
expect(testObject.get("subObject").key2).toBe("value2");
expect(testObject.get("subObject").key3).toBe("thisWillGoAway");

var q2 = new ParseQuery("Thing");
q2.select("other", "tbd", "subObject.key1", "subObject.key3");
objectToReturn = { objectId: 'T01', other: 'other2', subObject:{key1:"updatedValue"}};
return q2.find();
}).then((results) => {
expect(results[0].get("name")).toBe("Name"); //query didn't select this
expect(results[0].get("other")).toBe("other2"); //query selected and updated this
expect(results[0].has("tbd")).toBe(false); //query selected this and it wasn't returned
//sub-objects should work similarly
expect(results[0].get("subObject").key1).toBe("updatedValue");
expect(results[0].get("subObject").key2).toBe("value2");
expect(results[0].get("subObject").key3).toBeUndefined();
}).then(() => {
expect(testObject.get("name")).toBe("Name");
expect(testObject.get("other")).toBe("other2");
expect(testObject.has("tbd")).toBe(false);
expect(testObject.get("subObject").key1).toBe("updatedValue");
expect(testObject.get("subObject").key2).toBe("value2");
expect(testObject.get("subObject").key3).toBeUndefined();
done();
}, (error) => {
done.fail(error);
});
});

it('overrides cached object with first() results', (done) => {
jest.dontMock("../ParseObject");
jest.resetModules();
ParseObject = require('../ParseObject').default;
CoreManager = require('../CoreManager');
ParseQuery = require('../ParseQuery').default;

ParseObject.enableSingleInstance();

var objectToReturn = {
objectId: 'T01',
name: 'Name',
other: 'other',
className:"Thing",
createdAt: '2017-01-10T10:00:00Z'
};

CoreManager.setQueryController({
find(className, params, options) {
return ParsePromise.as({
results: [objectToReturn]
});
}
});

var q = new ParseQuery("Thing");
var testObject;
q.first().then((result) => {
testObject = result;

expect(testObject.get("name")).toBe("Name");
expect(testObject.get("other")).toBe("other");

objectToReturn = { objectId: 'T01', name: 'Name2'};
var q2 = new ParseQuery("Thing");
return q2.first();
}).then((result) => {
expect(result.get("name")).toBe("Name2");
expect(result.has("other")).toBe(false);
}).then(() => {
expect(testObject.get("name")).toBe("Name2");
expect(testObject.has("other")).toBe(false);
done();
});
});

it('does not override unselected fields for first() on select query', (done) => {
jest.dontMock("../ParseObject");
jest.resetModules();
ParseObject = require('../ParseObject').default;
CoreManager = require('../CoreManager');
ParseQuery = require('../ParseQuery').default;

ParseObject.enableSingleInstance();

var objectToReturn = {
objectId: 'T01',
name: 'Name',
other: 'other',
tbd: 'exists',
className:"Thing",
subObject: {key1:"value", key2:"value2", key3:"thisWillGoAway"},
createdAt: '2017-01-10T10:00:00Z',
};

CoreManager.setQueryController({
find(className, params, options) {
return ParsePromise.as({
results: [objectToReturn]
});
}
});

var q = new ParseQuery("Thing");
var testObject;
return q.first().then((result) => {
testObject = result;

expect(testObject.get("name")).toBe("Name");
expect(testObject.get("other")).toBe("other");
expect(testObject.has("tbd")).toBe(true);

var q2 = new ParseQuery("Thing");
q2.select("other", "tbd", "subObject.key1", "subObject.key3");
objectToReturn = { objectId: 'T01', other: 'other2', subObject:{key1:"updatedValue"}};
return q2.first();
}).then((result) => {
expect(result.get("name")).toBe("Name"); //query didn't select this
expect(result.get("other")).toBe("other2"); //query selected and updated this
expect(result.has("tbd")).toBe(false); //query selected this and it wasn't returned
//sub-objects should work similarly
expect(result.get("subObject").key1).toBe("updatedValue");
expect(result.get("subObject").key2).toBe("value2");
expect(result.get("subObject").key3).toBeUndefined();
}).then(() => {
expect(testObject.get("name")).toBe("Name");
expect(testObject.get("other")).toBe("other2");
expect(testObject.has("tbd")).toBe(false);
expect(testObject.get("subObject").key1).toBe("updatedValue");
expect(testObject.get("subObject").key2).toBe("value2");
expect(testObject.get("subObject").key3).toBeUndefined();
done();
}, (error) => {
done.fail(error);
});
});
});