Skip to content
Open
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
124 changes: 124 additions & 0 deletions spec/rest.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,130 @@ describe('rest create', () => {
);
});

it('supports ignoreIncludeErrors for unreadable pointers', async () => {
const schemaController = await config.database.loadSchema();
await schemaController.addClassIfNotExists(
'IncludeChild',
{ owner: { type: 'Pointer', targetClass: '_User' } },
{
get: { pointerFields: ['owner'] },
find: { pointerFields: ['owner'] },
}
);
await config.schemaCache.clear();

const owner = await Parse.User.signUp('includeOwner', 'password');
const child = new Parse.Object('IncludeChild');
child.set('owner', owner);
child.set('label', 'unreadable');
await child.save(null, { useMasterKey: true });

const parent = new Parse.Object('IncludeParent');
parent.set('child', child);
const parentACL = new Parse.ACL();
parentACL.setPublicReadAccess(true);
parentACL.setPublicWriteAccess(false);
parent.setACL(parentACL);
await parent.save(null, { useMasterKey: true });

await Parse.User.logOut();

const headers = {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json',
};
const baseUrl = `${Parse.serverURL}/classes/IncludeParent/${parent.id}?include=child`;

await expectAsync(
request({
method: 'GET',
url: baseUrl,
headers,
})
).toBeRejectedWith(
jasmine.objectContaining({
status: 404,
data: jasmine.objectContaining({ code: Parse.Error.OBJECT_NOT_FOUND }),
})
);

// when we try to include and unreadable child & ignore include errors
// then the raw pointer is simply returned unhydrated.
const response = await request({
method: 'GET',
url: `${baseUrl}&ignoreIncludeErrors=true`,
headers,
});

expect(response.status).toBe(200);
expect(response.data.child).toEqual(
jasmine.objectContaining({
__type: 'Pointer',
className: 'IncludeChild',
objectId: child.id,
})
);
});

it('preserves unresolved pointers in arrays when ignoreIncludeErrors is true', async () => {
const childOne = await new Parse.Object('IgnoreIncludeChild').save({ name: 'first' });
const childTwo = await new Parse.Object('IgnoreIncludeChild').save({ name: 'second' });

const parent = new Parse.Object('IgnoreIncludeParent');
parent.set('primary', childOne);
parent.set('others', [childOne, childTwo]);
await parent.save();

await childOne.destroy({ useMasterKey: true });

const headers = {
'X-Parse-Application-Id': Parse.applicationId,
'X-Parse-REST-API-Key': 'rest',
'Content-Type': 'application/json',
};
const baseUrl = `${Parse.serverURL}/classes/IgnoreIncludeParent/${parent.id}?include=primary,others`;

const defaultResponse = await request({
method: 'GET',
url: baseUrl,
headers,
});
expect(defaultResponse.status).toBe(200);
expect(Array.isArray(defaultResponse.data.others)).toBeTrue();
expect(defaultResponse.data.others.length).toBe(1);

const response = await request({
method: 'GET',
url: `${baseUrl}&ignoreIncludeErrors=true`,
headers,
});

expect(response.status).toBe(200);
expect(response.data.primary).toEqual(
jasmine.objectContaining({
__type: 'Pointer',
className: 'IgnoreIncludeChild',
objectId: childOne.id,
})
);
expect(response.data.others.length).toBe(2);
expect(response.data.others[0]).toEqual(
jasmine.objectContaining({
__type: 'Pointer',
className: 'IgnoreIncludeChild',
objectId: childOne.id,
})
);
expect(response.data.others[1]).toEqual(
jasmine.objectContaining({
__type: 'Object',
className: 'IgnoreIncludeChild',
objectId: childTwo.id,
})
);
});

it('locks down session', done => {
let currentUser;
Parse.User.signUp('foo', 'bar')
Expand Down
1 change: 1 addition & 0 deletions src/Adapters/Storage/StorageAdapter.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type QueryOptions = {
action?: string,
addsField?: boolean,
comment?: string,
ignoreIncludeErrors?: boolean,
};

export type UpdateQueryOptions = {
Expand Down
8 changes: 8 additions & 0 deletions src/Controllers/DatabaseController.js
Original file line number Diff line number Diff line change
Expand Up @@ -1189,6 +1189,7 @@ class DatabaseController {
caseInsensitive = false,
explain,
comment,
ignoreIncludeErrors,
}: any = {},
auth: any = {},
validSchemaController: SchemaController.SchemaController
Expand Down Expand Up @@ -1285,6 +1286,13 @@ class DatabaseController {
}
if (!query) {
if (op === 'get') {
// If there's no query returned; then it didn't pass `addPointerPermissions`
// permissions checks
// Default is to return OBJECT_NOT_FOUND, but if we ignore include errors we can
// return [] here.
if (ignoreIncludeErrors) {
return [];
}
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
} else {
return [];
Expand Down
31 changes: 24 additions & 7 deletions src/RestQuery.js
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ function _UnsafeRestQuery(
case 'includeAll':
this.includeAll = true;
break;
// Propagate these options from restOptions to findOptions too
case 'explain':
case 'hint':
case 'distinct':
Expand All @@ -213,6 +214,7 @@ function _UnsafeRestQuery(
case 'limit':
case 'readPreference':
case 'comment':
case 'ignoreIncludeErrors':
this.findOptions[option] = restOptions[option];
break;
case 'order':
Expand Down Expand Up @@ -1013,6 +1015,13 @@ function includePath(config, auth, response, path, context, restOptions = {}) {
} else if (restOptions.readPreference) {
includeRestOptions.readPreference = restOptions.readPreference;
}
// Flag for replacePointers if missing pointers should be preserved without throwing errors
// defaults to false to continue previous behaviour
let preserveMissing = false;
if (restOptions.ignoreIncludeErrors) {
includeRestOptions.ignoreIncludeErrors = restOptions.ignoreIncludeErrors;
preserveMissing = true;
}

const queryPromises = Object.keys(pointersHash).map(async className => {
const objectIds = Array.from(pointersHash[className]);
Expand Down Expand Up @@ -1054,7 +1063,9 @@ function includePath(config, auth, response, path, context, restOptions = {}) {
}, {});

var resp = {
results: replacePointers(response.results, path, replace),
results: replacePointers(response.results, path, replace, {
preserveMissing,
}),
};
if (response.count) {
resp.count = response.count;
Expand Down Expand Up @@ -1095,13 +1106,15 @@ function findPointers(object, path) {
// in, or it may be a single object.
// Path is a list of fields to search into.
// replace is a map from object id -> object.
// `options` is an optional options object; options currently include
// `preserveMissing?: boolean` where if it is true
// Returns something analogous to object, but with the appropriate
// pointers inflated.
function replacePointers(object, path, replace) {
function replacePointers(object, path, replace, options = {}) {
const preserveMissing = !!options.preserveMissing;
if (object instanceof Array) {
return object
.map(obj => replacePointers(obj, path, replace))
.filter(obj => typeof obj !== 'undefined');
const mapped = object.map(obj => replacePointers(obj, path, replace, options));
return preserveMissing ? mapped : mapped.filter(obj => typeof obj !== 'undefined');
}

if (typeof object !== 'object' || !object) {
Expand All @@ -1110,7 +1123,11 @@ function replacePointers(object, path, replace) {

if (path.length === 0) {
if (object && object.__type === 'Pointer') {
return replace[object.objectId];
const replacement = replace[object.objectId];
if (typeof replacement === 'undefined') {
return preserveMissing ? object : undefined;
}
return replacement;
}
return object;
}
Expand All @@ -1119,7 +1136,7 @@ function replacePointers(object, path, replace) {
if (!subobject) {
return object;
}
var newsub = replacePointers(subobject, path.slice(1), replace);
var newsub = replacePointers(subobject, path.slice(1), replace, options);
var answer = {};
for (var key in object) {
if (key == path[0]) {
Expand Down
8 changes: 8 additions & 0 deletions src/Routers/ClassesRouter.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const ALLOWED_GET_QUERY_KEYS = [
'readPreference',
'includeReadPreference',
'subqueryReadPreference',
'ignoreIncludeErrors',
];

export class ClassesRouter extends PromiseRouter {
Expand Down Expand Up @@ -75,6 +76,9 @@ export class ClassesRouter extends PromiseRouter {
if (typeof body.subqueryReadPreference === 'string') {
options.subqueryReadPreference = body.subqueryReadPreference;
}
if (body.ignoreIncludeErrors != null) {
options.ignoreIncludeErrors = !!body.ignoreIncludeErrors;
}

return rest
.get(
Expand Down Expand Up @@ -174,6 +178,7 @@ export class ClassesRouter extends PromiseRouter {
'hint',
'explain',
'comment',
'ignoreIncludeErrors',
];

for (const key of Object.keys(body)) {
Expand Down Expand Up @@ -226,6 +231,9 @@ export class ClassesRouter extends PromiseRouter {
if (body.comment && typeof body.comment === 'string') {
options.comment = body.comment;
}
if (body.ignoreIncludeErrors) {
options.ignoreIncludeErrors = true;
}
return options;
}

Expand Down