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 @@ -209,6 +209,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 @@ -217,6 +218,7 @@ function _UnsafeRestQuery(
case 'limit':
case 'readPreference':
case 'comment':
case 'ignoreIncludeErrors':
this.findOptions[option] = restOptions[option];
break;
case 'order':
Expand Down Expand Up @@ -1018,6 +1020,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 @@ -1059,7 +1068,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 @@ -1100,13 +1111,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
Comment on lines +1114 to 1116
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: Incomplete comment.

The comment has an incomplete sentence: "where if it is true" ends abruptly.

Consider completing the sentence:

-// `options` is an optional options object; options currently include
-// `preserveMissing?: boolean` where if it is true
+// `options` is an optional options object; options currently include:
+// `preserveMissing?: boolean` - if true, missing pointer replacements are kept as-is
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// `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
// `options` is an optional options object; options currently include:
// `preserveMissing?: boolean` - if true, missing pointer replacements are kept as-is
// Returns something analogous to object, but with the appropriate
🤖 Prompt for AI Agents
In src/RestQuery.js around lines 1114 to 1116, the comment block ends with an
incomplete sentence ("where if it is true"). Update the comment to complete that
sentence and make the behavior clear (for example: explain what preserveMissing
true does — e.g., "where if it is true, missing properties on the source object
should be preserved in the result instead of being omitted"), and ensure the
full comment reads as a complete description of the options and the returned
value.

// 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 @@ -1115,7 +1128,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 @@ -1124,7 +1141,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
Loading