Skip to content

Support Fetch With Include #631

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 6 commits into from
Aug 12, 2018
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
87 changes: 87 additions & 0 deletions integration/test/ParseObjectTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
49 changes: 49 additions & 0 deletions integration/test/ParseUserTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -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', '[email protected]');
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', '[email protected]');
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();
Expand Down
107 changes: 105 additions & 2 deletions src/ParseObject.js
Original file line number Diff line number Diff line change
Expand Up @@ -970,6 +970,8 @@ class ParseObject {
* be used for this request.
* <li>sessionToken: A valid session token, used for making a request on
* behalf of a specific user.
* <li>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.
* </ul>
* @return {Promise} A promise that is fulfilled when the fetch
* completes.
Expand All @@ -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<string|Array<string>>} keys The name(s) of the key(s) to include.
* @param {Object} options
* Valid options are:<ul>
* <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
* be used for this request.
* <li>sessionToken: A valid session token, used for making a request on
* behalf of a specific user.
* </ul>
* @return {Promise} A promise that is fulfilled when the fetch
* completes.
*/
fetchWithInclude(keys: String|Array<string|Array<string>>, 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.
Expand Down Expand Up @@ -1133,9 +1173,17 @@ class ParseObject {
*
* @param {Array} list A list of <code>Parse.Object</code>.
* @param {Object} options
* Valid options are:<ul>
* <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
* be used for this request.
* <li>sessionToken: A valid session token, used for making a request on
* behalf of a specific user.
* <li>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.
* </ul>
* @static
*/
static fetchAll(list: Array<ParseObject>, options) {
static fetchAll(list: Array<ParseObject>, options: RequestOptions) {
var options = options || {};

var queryOptions = {};
Expand All @@ -1145,13 +1193,61 @@ 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,
queryOptions
);
}

/**
* 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.
*
* <pre>
* Parse.Object.fetchAllWithInclude([object1, object2, ...], [pointer1, pointer2, ...])
* .then((list) => {
* // All the objects were fetched.
* }, (error) => {
* // An error occurred while fetching one of the objects.
* });
* </pre>
*
* @param {Array} list A list of <code>Parse.Object</code>.
* @param {String|Array<string|Array<string>>} keys The name(s) of the key(s) to include.
* @param {Object} options
* Valid options are:<ul>
* <li>useMasterKey: In Cloud Code and Node only, causes the Master Key to
* be used for this request.
* <li>sessionToken: A valid session token, used for making a request on
* behalf of a specific user.
* </ul>
* @static
*/
static fetchAllWithInclude(list: Array<ParseObject>, keys: String|Array<string|Array<string>>, 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.
Expand Down Expand Up @@ -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 = {};
Expand Down Expand Up @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions src/ParseUser.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>): Promise {
return super.fetchWithInclude.apply(this, args).then(() => {
if (this.isCurrent()) {
return CoreManager.getUserController().updateUserOnDisk(this);
}
return this;
});
}

static readOnlyAttributes() {
return ['sessionToken'];
}
Expand Down
1 change: 1 addition & 0 deletions src/RESTController.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export type RequestOptions = {
useMasterKey?: boolean;
sessionToken?: string;
installationId?: string;
include?: any;
};

export type FullOptions = {
Expand Down
Loading