diff --git a/integration/test/ParseObjectTest.js b/integration/test/ParseObjectTest.js
index 425071672..84df7e350 100644
--- a/integration/test/ParseObjectTest.js
+++ b/integration/test/ParseObjectTest.js
@@ -1340,6 +1340,93 @@ describe('Parse Object', () => {
});
});
+ it('can fetchWithInclude', async () => {
+ const parent = new TestObject();
+ const child = new TestObject();
+ child.set('field', 'isChild');
+ parent.set('child', child);
+ await parent.save();
+
+ const obj1 = TestObject.createWithoutData(parent.id);
+ const fetchedObj1 = await obj1.fetchWithInclude('child');
+ assert.equal(obj1.get('child').get('field'), 'isChild');
+ assert.equal(fetchedObj1.get('child').get('field'), 'isChild');
+
+ const obj2 = TestObject.createWithoutData(parent.id);
+ const fetchedObj2 = await obj2.fetchWithInclude(['child']);
+ assert.equal(obj2.get('child').get('field'), 'isChild');
+ assert.equal(fetchedObj2.get('child').get('field'), 'isChild');
+
+ const obj3 = TestObject.createWithoutData(parent.id);
+ const fetchedObj3 = await obj3.fetchWithInclude([ ['child'] ]);
+ assert.equal(obj3.get('child').get('field'), 'isChild');
+ assert.equal(fetchedObj3.get('child').get('field'), 'isChild');
+ });
+
+ it('can fetchWithInclude dot notation', async () => {
+ const parent = new TestObject();
+ const child = new TestObject();
+ const grandchild = new TestObject();
+ grandchild.set('field', 'isGrandchild');
+ child.set('grandchild', grandchild);
+ parent.set('child', child);
+ await Parse.Object.saveAll([parent, child, grandchild]);
+
+ const obj1 = TestObject.createWithoutData(parent.id);
+ await obj1.fetchWithInclude('child.grandchild');
+ assert.equal(obj1.get('child').get('grandchild').get('field'), 'isGrandchild');
+
+ const obj2 = TestObject.createWithoutData(parent.id);
+ await obj2.fetchWithInclude(['child.grandchild']);
+ assert.equal(obj2.get('child').get('grandchild').get('field'), 'isGrandchild');
+
+ const obj3 = TestObject.createWithoutData(parent.id);
+ await obj3.fetchWithInclude([ ['child.grandchild'] ]);
+ assert.equal(obj3.get('child').get('grandchild').get('field'), 'isGrandchild');
+ });
+
+ it('can fetchAllWithInclude', async () => {
+ const parent = new TestObject();
+ const child = new TestObject();
+ child.set('field', 'isChild');
+ parent.set('child', child);
+ await parent.save();
+
+ const obj1 = TestObject.createWithoutData(parent.id);
+ await Parse.Object.fetchAllWithInclude([obj1], 'child');
+ assert.equal(obj1.get('child').get('field'), 'isChild');
+
+ const obj2 = TestObject.createWithoutData(parent.id);
+ await Parse.Object.fetchAllWithInclude([obj2], ['child']);
+ assert.equal(obj2.get('child').get('field'), 'isChild');
+
+ const obj3 = TestObject.createWithoutData(parent.id);
+ await Parse.Object.fetchAllWithInclude([obj3], [ ['child'] ]);
+ assert.equal(obj3.get('child').get('field'), 'isChild');
+ });
+
+ it('can fetchAllWithInclude dot notation', async () => {
+ const parent = new TestObject();
+ const child = new TestObject();
+ const grandchild = new TestObject();
+ grandchild.set('field', 'isGrandchild');
+ child.set('grandchild', grandchild);
+ parent.set('child', child);
+ await Parse.Object.saveAll([parent, child, grandchild]);
+
+ const obj1 = TestObject.createWithoutData(parent.id);
+ await Parse.Object.fetchAllWithInclude([obj1], 'child.grandchild');
+ assert.equal(obj1.get('child').get('grandchild').get('field'), 'isGrandchild');
+
+ const obj2 = TestObject.createWithoutData(parent.id);
+ await Parse.Object.fetchAllWithInclude([obj2], ['child.grandchild']);
+ assert.equal(obj2.get('child').get('grandchild').get('field'), 'isGrandchild');
+
+ const obj3 = TestObject.createWithoutData(parent.id);
+ await Parse.Object.fetchAllWithInclude([obj3], [ ['child.grandchild'] ]);
+ assert.equal(obj3.get('child').get('grandchild').get('field'), 'isGrandchild');
+ });
+
it('fires errors when readonly attributes are changed', (done) => {
let LimitedObject = Parse.Object.extend('LimitedObject');
LimitedObject.readOnlyAttributes = function() {
diff --git a/integration/test/ParseUserTest.js b/integration/test/ParseUserTest.js
index af976fa4c..85bc4251e 100644
--- a/integration/test/ParseUserTest.js
+++ b/integration/test/ParseUserTest.js
@@ -193,6 +193,55 @@ describe('Parse User', () => {
});
});
+ it('can fetch non-auth user with include', async () => {
+ Parse.User.enableUnsafeCurrentUser();
+
+ const child = new Parse.Object('TestObject');
+ child.set('field', 'test');
+ let user = new Parse.User();
+ user.set('password', 'asdf');
+ user.set('email', 'asdf@exxample.com');
+ user.set('username', 'zxcv');
+ user.set('child', child);
+ await user.signUp();
+
+ const query = new Parse.Query(Parse.User);
+ const userNotAuthed = await query.get(user.id);
+
+ assert.equal(userNotAuthed.get('child').get('field'), undefined);
+
+ const fetchedUser = await userNotAuthed.fetchWithInclude('child');
+
+ assert.equal(userNotAuthed.get('child').get('field'), 'test');
+ assert.equal(fetchedUser.get('child').get('field'), 'test');
+ });
+
+ it('can fetch auth user with include', async () => {
+ Parse.User.enableUnsafeCurrentUser();
+
+ const child = new Parse.Object('TestObject');
+ child.set('field', 'test');
+ let user = new Parse.User();
+ user.set('password', 'asdf');
+ user.set('email', 'asdf@exxample.com');
+ user.set('username', 'zxcv');
+ user.set('child', child);
+ await user.signUp();
+
+ user = await Parse.User.logIn('zxcv', 'asdf');
+
+ assert.equal(user.get('child').get('field'), undefined);
+ assert.equal(Parse.User.current().get('child').get('field'), undefined);
+
+ const fetchedUser = await user.fetchWithInclude('child');
+ const current = await Parse.User.currentAsync();
+
+ assert.equal(user.get('child').get('field'), 'test');
+ assert.equal(current.get('child').get('field'), 'test');
+ assert.equal(fetchedUser.get('child').get('field'), 'test');
+ assert.equal(Parse.User.current().get('child').get('field'), 'test');
+ });
+
it('can store the current user', (done) => {
Parse.User.enableUnsafeCurrentUser();
let user = new Parse.User();
diff --git a/src/ParseObject.js b/src/ParseObject.js
index a156eae65..fbd4c4eee 100644
--- a/src/ParseObject.js
+++ b/src/ParseObject.js
@@ -970,6 +970,8 @@ class ParseObject {
* be used for this request.
*
sessionToken: A valid session token, used for making a request on
* behalf of a specific user.
+ * include: The name(s) of the key(s) to include. Can be a string, an array of strings,
+ * or an array of array of strings.
*
* @return {Promise} A promise that is fulfilled when the fetch
* completes.
@@ -983,10 +985,48 @@ class ParseObject {
if (options.hasOwnProperty('sessionToken')) {
fetchOptions.sessionToken = options.sessionToken;
}
+ if (options.hasOwnProperty('include')) {
+ fetchOptions.include = [];
+ if (Array.isArray(options.include)) {
+ options.include.forEach((key) => {
+ if (Array.isArray(key)) {
+ fetchOptions.include = fetchOptions.include.concat(key);
+ } else {
+ fetchOptions.include.push(key);
+ }
+ });
+ } else {
+ fetchOptions.include.push(options.include);
+ }
+ }
var controller = CoreManager.getObjectController();
return controller.fetch(this, true, fetchOptions);
}
+ /**
+ * Fetch the model from the server. If the server's representation of the
+ * model differs from its current attributes, they will be overriden.
+ *
+ * Includes nested Parse.Objects for the provided key. You can use dot
+ * notation to specify which fields in the included object are also fetched.
+ *
+ * @param {String|Array>} keys The name(s) of the key(s) to include.
+ * @param {Object} options
+ * Valid options are:
+ * - useMasterKey: In Cloud Code and Node only, causes the Master Key to
+ * be used for this request.
+ *
- sessionToken: A valid session token, used for making a request on
+ * behalf of a specific user.
+ *
+ * @return {Promise} A promise that is fulfilled when the fetch
+ * completes.
+ */
+ fetchWithInclude(keys: String|Array>, options: RequestOptions): Promise {
+ options = options || {};
+ options.include = keys;
+ return this.fetch(options);
+ }
+
/**
* Set a hash of model attributes, and save the model to the server.
* updatedAt will be updated when the request returns.
@@ -1133,9 +1173,17 @@ class ParseObject {
*
* @param {Array} list A list of Parse.Object
.
* @param {Object} options
+ * Valid options are:
+ * - useMasterKey: In Cloud Code and Node only, causes the Master Key to
+ * be used for this request.
+ *
- sessionToken: A valid session token, used for making a request on
+ * behalf of a specific user.
+ *
- include: The name(s) of the key(s) to include. Can be a string, an array of strings,
+ * or an array of array of strings.
+ *
* @static
*/
- static fetchAll(list: Array, options) {
+ static fetchAll(list: Array, options: RequestOptions) {
var options = options || {};
var queryOptions = {};
@@ -1145,6 +1193,20 @@ class ParseObject {
if (options.hasOwnProperty('sessionToken')) {
queryOptions.sessionToken = options.sessionToken;
}
+ if (options.hasOwnProperty('include')) {
+ queryOptions.include = [];
+ if (Array.isArray(options.include)) {
+ options.include.forEach((key) => {
+ if (Array.isArray(key)) {
+ queryOptions.include = queryOptions.include.concat(key);
+ } else {
+ queryOptions.include.push(key);
+ }
+ });
+ } else {
+ queryOptions.include.push(options.include);
+ }
+ }
return CoreManager.getObjectController().fetch(
list,
true,
@@ -1152,6 +1214,40 @@ class ParseObject {
);
}
+ /**
+ * Fetches the given list of Parse.Object.
+ *
+ * Includes nested Parse.Objects for the provided key. You can use dot
+ * notation to specify which fields in the included object are also fetched.
+ *
+ * If any error is encountered, stops and calls the error handler.
+ *
+ *
+ * Parse.Object.fetchAllWithInclude([object1, object2, ...], [pointer1, pointer2, ...])
+ * .then((list) => {
+ * // All the objects were fetched.
+ * }, (error) => {
+ * // An error occurred while fetching one of the objects.
+ * });
+ *
+ *
+ * @param {Array} list A list of Parse.Object
.
+ * @param {String|Array>} keys The name(s) of the key(s) to include.
+ * @param {Object} options
+ * Valid options are:
+ * - useMasterKey: In Cloud Code and Node only, causes the Master Key to
+ * be used for this request.
+ *
- sessionToken: A valid session token, used for making a request on
+ * behalf of a specific user.
+ *
+ * @static
+ */
+ static fetchAllWithInclude(list: Array, keys: String|Array>, options: RequestOptions) {
+ options = options || {};
+ options.include = keys;
+ return ParseObject.fetchAll(list, options);
+ }
+
/**
* Fetches the given list of Parse.Object if needed.
* If any error is encountered, stops and calls the error handler.
@@ -1570,6 +1666,9 @@ var DefaultController = {
}
var query = new ParseQuery(className);
query.containedIn('objectId', ids);
+ if (options && options.include) {
+ query.include(options.include);
+ }
query._limit = ids.length;
return query.find(options).then((objects) => {
var idMap = {};
@@ -1604,10 +1703,14 @@ var DefaultController = {
});
} else {
var RESTController = CoreManager.getRESTController();
+ const params = {};
+ if (options && options.include) {
+ params.include = options.include.join();
+ }
return RESTController.request(
'GET',
'classes/' + target.className + '/' + target._getId(),
- {},
+ params,
options
).then((response, status, xhr) => {
if (target instanceof ParseObject) {
diff --git a/src/ParseUser.js b/src/ParseUser.js
index 8e73e72e3..d2612e5ff 100644
--- a/src/ParseUser.js
+++ b/src/ParseUser.js
@@ -463,6 +463,19 @@ class ParseUser extends ParseObject {
});
}
+ /**
+ * Wrap the default fetchWithInclude behavior with functionality to save to local
+ * storage if this is current user.
+ */
+ fetchWithInclude(...args: Array): Promise {
+ return super.fetchWithInclude.apply(this, args).then(() => {
+ if (this.isCurrent()) {
+ return CoreManager.getUserController().updateUserOnDisk(this);
+ }
+ return this;
+ });
+ }
+
static readOnlyAttributes() {
return ['sessionToken'];
}
diff --git a/src/RESTController.js b/src/RESTController.js
index 225dea5d6..200411e06 100644
--- a/src/RESTController.js
+++ b/src/RESTController.js
@@ -17,6 +17,7 @@ export type RequestOptions = {
useMasterKey?: boolean;
sessionToken?: string;
installationId?: string;
+ include?: any;
};
export type FullOptions = {
diff --git a/src/__tests__/ParseObject-test.js b/src/__tests__/ParseObject-test.js
index 9665bebd2..f4704f971 100644
--- a/src/__tests__/ParseObject-test.js
+++ b/src/__tests__/ParseObject-test.js
@@ -57,6 +57,11 @@ mockQuery.prototype.containedIn = function(field, ids) {
}));
});
};
+
+mockQuery.prototype.include = function(keys) {
+ this._include = keys;
+};
+
mockQuery.prototype.find = function() {
return Promise.resolve(this.results);
};
@@ -848,6 +853,64 @@ describe('ParseObject', () => {
expect(o.op('count')).toBe(undefined);
});
+ it('can fetchWithInclude', async () => {
+ const objectController = CoreManager.getObjectController();
+ const spy = jest.spyOn(
+ objectController,
+ 'fetch'
+ )
+ .mockImplementationOnce((target, forceFetch, options) => {})
+ .mockImplementationOnce((target, forceFetch, options) => {})
+ .mockImplementationOnce((target, forceFetch, options) => {});
+
+ const parent = new ParseObject('Person');
+ await parent.fetchWithInclude('child', { useMasterKey: true, sessionToken: '123'});
+ await parent.fetchWithInclude(['child']);
+ await parent.fetchWithInclude([['child']]);
+ expect(objectController.fetch).toHaveBeenCalledTimes(3);
+
+ expect(objectController.fetch.mock.calls[0]).toEqual([
+ parent, true, { useMasterKey: true, sessionToken: '123', include: ['child'] }
+ ]);
+ expect(objectController.fetch.mock.calls[1]).toEqual([
+ parent, true, { include: ['child'] }
+ ]);
+ expect(objectController.fetch.mock.calls[2]).toEqual([
+ parent, true, { include: ['child'] }
+ ]);
+
+ spy.mockRestore();
+ });
+
+ it('can fetchAllWithInclude', async () => {
+ const objectController = CoreManager.getObjectController();
+ const spy = jest.spyOn(
+ objectController,
+ 'fetch'
+ )
+ .mockImplementationOnce((target, forceFetch, options) => {})
+ .mockImplementationOnce((target, forceFetch, options) => {})
+ .mockImplementationOnce((target, forceFetch, options) => {});
+
+ const parent = new ParseObject('Person');
+ await ParseObject.fetchAllWithInclude([parent], 'child', { useMasterKey: true, sessionToken: '123'});
+ await ParseObject.fetchAllWithInclude([parent], ['child']);
+ await ParseObject.fetchAllWithInclude([parent], [['child']]);
+ expect(objectController.fetch).toHaveBeenCalledTimes(3);
+
+ expect(objectController.fetch.mock.calls[0]).toEqual([
+ [parent], true, { useMasterKey: true, sessionToken: '123', include: ['child'] }
+ ]);
+ expect(objectController.fetch.mock.calls[1]).toEqual([
+ [parent], true, { include: ['child'] }
+ ]);
+ expect(objectController.fetch.mock.calls[2]).toEqual([
+ [parent], true, { include: ['child'] }
+ ]);
+
+ spy.mockRestore();
+ });
+
it('can save the object', (done) => {
CoreManager.getRESTController()._setXHR(
mockXHR([{
@@ -1501,6 +1564,47 @@ describe('ObjectController', () => {
});
});
+ it('can fetch a single object with include', async (done) => {
+ var objectController = CoreManager.getObjectController();
+ var xhr = {
+ setRequestHeader: jest.fn(),
+ open: jest.fn(),
+ send: jest.fn()
+ };
+ RESTController._setXHR(function() { return xhr; });
+ var o = new ParseObject('Person');
+ o.id = 'pid';
+ objectController.fetch(o, false, { include: ['child'] }).then(() => {
+ expect(xhr.open.mock.calls[0]).toEqual(
+ ['POST', 'https://api.parse.com/1/classes/Person/pid', true]
+ );
+ var body = JSON.parse(xhr.send.mock.calls[0]);
+ expect(body._method).toBe('GET');
+ done();
+ });
+ await flushPromises();
+
+ xhr.status = 200;
+ xhr.responseText = JSON.stringify({});
+ xhr.readyState = 4;
+ xhr.onreadystatechange();
+ jest.runAllTicks();
+ });
+
+ it('can fetch an array of objects with include', async () => {
+ var objectController = CoreManager.getObjectController();
+ var objects = [];
+ for (var i = 0; i < 5; i++) {
+ objects[i] = new ParseObject('Person');
+ objects[i].id = 'pid' + i;
+ }
+ const results = await objectController.fetch(objects, false, { include: ['child'] });
+ expect(results.length).toBe(5);
+ expect(results[0] instanceof ParseObject).toBe(true);
+ expect(results[0].id).toBe('pid0');
+ expect(results[0].className).toBe('Person');
+ });
+
it('can destroy an object', async () => {
var objectController = CoreManager.getObjectController();
var xhr = {
diff --git a/src/__tests__/ParseUser-test.js b/src/__tests__/ParseUser-test.js
index 44f14988d..0bfdc7851 100644
--- a/src/__tests__/ParseUser-test.js
+++ b/src/__tests__/ParseUser-test.js
@@ -517,6 +517,83 @@ describe('ParseUser', () => {
});
});
+ it('updates the current user on disk when fetched with include', async () => {
+ ParseUser.enableUnsafeCurrentUser();
+ ParseUser._clearCache();
+ Storage._clear();
+ CoreManager.setRESTController({
+ request() {
+ return Promise.resolve({
+ objectId: 'uid6',
+ }, 200);
+ },
+ ajax() {},
+ });
+ const child = new ParseObject('TestObject');
+ child.set('foo', 'bar');
+ await child.save();
+
+ let u = await ParseUser.signUp('spot', 'fetchWithInclude');
+ expect(u.isCurrent()).toBe(true);
+ ParseUser._clearCache();
+ CoreManager.setRESTController({
+ request() {
+ return Promise.resolve({
+ child: child.toJSON(),
+ count: 15,
+ }, 200);
+ },
+ ajax() {},
+ });
+ u = await u.fetchWithInclude('child');
+
+ ParseUser._clearCache();
+ ParseObject._clearAllState();
+ expect(u.attributes).toEqual({});
+ expect(u.get('count')).toBe(undefined);
+ const current = await ParseUser.currentAsync();
+ expect(current.id).toBe('uid6');
+ expect(current.get('count')).toBe(15);
+ expect(current.get('child').foo).toBe('bar');
+ });
+
+ it('does not update non-auth user when fetched with include', async () => {
+ ParseUser.enableUnsafeCurrentUser();
+ ParseUser._clearCache();
+ Storage._clear();
+ CoreManager.setRESTController({
+ request() {
+ return Promise.resolve({
+ objectId: 'uid6',
+ }, 200);
+ },
+ ajax() {},
+ });
+ const child = new ParseObject('TestObject');
+ child.set('foo', 'bar');
+ await child.save();
+
+ let u = await ParseUser.signUp('spot', 'fetchWithInclude');
+ await ParseUser.logOut();
+ expect(u.isCurrent()).toBe(false);
+ ParseUser._clearCache();
+ CoreManager.setRESTController({
+ request() {
+ return Promise.resolve({
+ child: child.toJSON(),
+ count: 15,
+ }, 200);
+ },
+ ajax() {},
+ });
+ const fetchedUser = await u.fetchWithInclude('child');
+
+ const current = await ParseUser.currentAsync();
+ expect(current).toBe(null);
+ expect(fetchedUser.get('count')).toBe(15);
+ expect(fetchedUser.get('child').foo).toBe('bar');
+ });
+
it('clears the current user on disk when logged out', (done) => {
ParseUser.enableUnsafeCurrentUser();
ParseUser._clearCache();