Skip to content

Commit 5a9dbaa

Browse files
committed
seems to work?
1 parent f62da3f commit 5a9dbaa

File tree

5 files changed

+181
-7
lines changed

5 files changed

+181
-7
lines changed

spec/rest.spec.js

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -803,6 +803,128 @@ describe('rest create', () => {
803803
);
804804
});
805805

806+
it('supports ignoreIncludeErrors for unreadable pointers', async () => {
807+
const schemaController = await config.database.loadSchema();
808+
await schemaController.addClassIfNotExists(
809+
'IncludeChild',
810+
{ owner: { type: 'Pointer', targetClass: '_User' } },
811+
{
812+
get: { pointerFields: ['owner'] },
813+
find: { pointerFields: ['owner'] },
814+
}
815+
);
816+
await config.schemaCache.clear();
817+
818+
const owner = await Parse.User.signUp('includeOwner', 'password');
819+
const child = new Parse.Object('IncludeChild');
820+
child.set('owner', owner);
821+
child.set('label', 'unreadable');
822+
await child.save(null, { useMasterKey: true });
823+
824+
const parent = new Parse.Object('IncludeParent');
825+
parent.set('child', child);
826+
const parentACL = new Parse.ACL();
827+
parentACL.setPublicReadAccess(true);
828+
parentACL.setPublicWriteAccess(false);
829+
parent.setACL(parentACL);
830+
await parent.save(null, { useMasterKey: true });
831+
832+
await Parse.User.logOut();
833+
834+
const headers = {
835+
'X-Parse-Application-Id': Parse.applicationId,
836+
'X-Parse-REST-API-Key': 'rest',
837+
'Content-Type': 'application/json',
838+
};
839+
const baseUrl = `${Parse.serverURL}/classes/IncludeParent/${parent.id}?include=child`;
840+
841+
await expectAsync(
842+
request({
843+
method: 'GET',
844+
url: baseUrl,
845+
headers,
846+
})
847+
).toBeRejectedWith(
848+
jasmine.objectContaining({
849+
status: 404,
850+
data: jasmine.objectContaining({ code: Parse.Error.OBJECT_NOT_FOUND }),
851+
})
852+
);
853+
854+
const response = await request({
855+
method: 'GET',
856+
url: `${baseUrl}&ignoreIncludeErrors=true`,
857+
headers,
858+
});
859+
860+
expect(response.status).toBe(200);
861+
expect(response.data.child).toEqual(
862+
jasmine.objectContaining({
863+
__type: 'Pointer',
864+
className: 'IncludeChild',
865+
objectId: child.id,
866+
})
867+
);
868+
});
869+
870+
it('preserves unresolved pointers in arrays when ignoreIncludeErrors is true', async () => {
871+
const childOne = await new Parse.Object('IgnoreIncludeChild').save({ name: 'first' });
872+
const childTwo = await new Parse.Object('IgnoreIncludeChild').save({ name: 'second' });
873+
874+
const parent = new Parse.Object('IgnoreIncludeParent');
875+
parent.set('primary', childOne);
876+
parent.set('others', [childOne, childTwo]);
877+
await parent.save();
878+
879+
await childOne.destroy({ useMasterKey: true });
880+
881+
const headers = {
882+
'X-Parse-Application-Id': Parse.applicationId,
883+
'X-Parse-REST-API-Key': 'rest',
884+
'Content-Type': 'application/json',
885+
};
886+
const baseUrl = `${Parse.serverURL}/classes/IgnoreIncludeParent/${parent.id}?include=primary,others`;
887+
888+
const defaultResponse = await request({
889+
method: 'GET',
890+
url: baseUrl,
891+
headers,
892+
});
893+
expect(defaultResponse.status).toBe(200);
894+
expect(Array.isArray(defaultResponse.data.others)).toBeTrue();
895+
expect(defaultResponse.data.others.length).toBe(1);
896+
897+
const response = await request({
898+
method: 'GET',
899+
url: `${baseUrl}&ignoreIncludeErrors=true`,
900+
headers,
901+
});
902+
903+
expect(response.status).toBe(200);
904+
expect(response.data.primary).toEqual(
905+
jasmine.objectContaining({
906+
__type: 'Pointer',
907+
className: 'IgnoreIncludeChild',
908+
objectId: childOne.id,
909+
})
910+
);
911+
expect(response.data.others.length).toBe(2);
912+
expect(response.data.others[0]).toEqual(
913+
jasmine.objectContaining({
914+
__type: 'Pointer',
915+
className: 'IgnoreIncludeChild',
916+
objectId: childOne.id,
917+
})
918+
);
919+
expect(response.data.others[1]).toEqual(
920+
jasmine.objectContaining({
921+
__type: 'Object',
922+
className: 'IgnoreIncludeChild',
923+
objectId: childTwo.id,
924+
})
925+
);
926+
});
927+
806928
it('locks down session', done => {
807929
let currentUser;
808930
Parse.User.signUp('foo', 'bar')

src/Adapters/Storage/StorageAdapter.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export type QueryOptions = {
2020
action?: string,
2121
addsField?: boolean,
2222
comment?: string,
23+
ignoreIncludeErrors?: boolean,
2324
};
2425

2526
export type UpdateQueryOptions = {

src/Controllers/DatabaseController.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1189,6 +1189,7 @@ class DatabaseController {
11891189
caseInsensitive = false,
11901190
explain,
11911191
comment,
1192+
ignoreIncludeErrors,
11921193
}: any = {},
11931194
auth: any = {},
11941195
validSchemaController: SchemaController.SchemaController
@@ -1285,6 +1286,13 @@ class DatabaseController {
12851286
}
12861287
if (!query) {
12871288
if (op === 'get') {
1289+
// If there's no query returned; then it didn't pass `addPointerPermissions`
1290+
// permissions checks
1291+
// Default is to return OBJECT_NOT_FOUND, but if we ignore include errors we can
1292+
// return [] here.
1293+
if (ignoreIncludeErrors) {
1294+
return [];
1295+
}
12881296
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
12891297
} else {
12901298
return [];

src/RestQuery.js

Lines changed: 42 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,19 @@ function _UnsafeRestQuery(
113113
this.response = null;
114114
this.findOptions = {};
115115
this.context = context || {};
116+
const hasIgnoreIncludeErrors = Object.prototype.hasOwnProperty.call(
117+
restOptions,
118+
'ignoreIncludeErrors'
119+
);
120+
this.ignoreIncludeErrors = hasIgnoreIncludeErrors
121+
? !!restOptions.ignoreIncludeErrors
122+
: false;
123+
if (hasIgnoreIncludeErrors) {
124+
this.restOptions.ignoreIncludeErrors = this.ignoreIncludeErrors;
125+
if (this.ignoreIncludeErrors) {
126+
this.findOptions.ignoreIncludeErrors = true;
127+
}
128+
}
116129
if (!this.auth.isMaster) {
117130
if (this.className == '_Session') {
118131
if (!this.auth.user) {
@@ -215,6 +228,8 @@ function _UnsafeRestQuery(
215228
case 'comment':
216229
this.findOptions[option] = restOptions[option];
217230
break;
231+
case 'ignoreIncludeErrors':
232+
break;
218233
case 'order':
219234
var fields = restOptions.order.split(',');
220235
this.findOptions.sort = fields.reduce((sortMap, field) => {
@@ -741,6 +756,9 @@ _UnsafeRestQuery.prototype.runFind = async function (options = {}) {
741756
return Promise.resolve();
742757
}
743758
const findOptions = Object.assign({}, this.findOptions);
759+
if (this.ignoreIncludeErrors) {
760+
findOptions.ignoreIncludeErrors = true;
761+
}
744762
if (this.keys) {
745763
findOptions.keys = this.keys.map(key => {
746764
return key.split('.')[0];
@@ -1013,6 +1031,13 @@ function includePath(config, auth, response, path, context, restOptions = {}) {
10131031
} else if (restOptions.readPreference) {
10141032
includeRestOptions.readPreference = restOptions.readPreference;
10151033
}
1034+
// Flag for replacePointers if missing pointers should be preserved without throwing errors
1035+
// defaults to false to continue previous behaviour
1036+
let preserveMissing = false;
1037+
if (restOptions.ignoreIncludeErrors) {
1038+
includeRestOptions.ignoreIncludeErrors = restOptions.ignoreIncludeErrors;
1039+
preserveMissing = true;
1040+
}
10161041

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

10561081
var resp = {
1057-
results: replacePointers(response.results, path, replace),
1082+
results: replacePointers(response.results, path, replace, {
1083+
preserveMissing,
1084+
}),
10581085
};
10591086
if (response.count) {
10601087
resp.count = response.count;
@@ -1095,13 +1122,17 @@ function findPointers(object, path) {
10951122
// in, or it may be a single object.
10961123
// Path is a list of fields to search into.
10971124
// replace is a map from object id -> object.
1125+
// `options` is an optional options object; options currently include
1126+
// `preserveMissing?: boolean` where if it is true
10981127
// Returns something analogous to object, but with the appropriate
10991128
// pointers inflated.
1100-
function replacePointers(object, path, replace) {
1129+
function replacePointers(object, path, replace, options = {}) {
1130+
const preserveMissing = !!options.preserveMissing;
11011131
if (object instanceof Array) {
1102-
return object
1103-
.map(obj => replacePointers(obj, path, replace))
1104-
.filter(obj => typeof obj !== 'undefined');
1132+
const mapped = object.map(obj => replacePointers(obj, path, replace, options));
1133+
// TODO: Is this change really correct? If we do this then preserveMissing will essentially
1134+
// cause the array to have undefined values inside?
1135+
return preserveMissing ? mapped : mapped.filter(obj => typeof obj !== 'undefined');
11051136
}
11061137

11071138
if (typeof object !== 'object' || !object) {
@@ -1110,7 +1141,11 @@ function replacePointers(object, path, replace) {
11101141

11111142
if (path.length === 0) {
11121143
if (object && object.__type === 'Pointer') {
1113-
return replace[object.objectId];
1144+
const replacement = replace[object.objectId];
1145+
if (typeof replacement === 'undefined') {
1146+
return preserveMissing ? object : undefined;
1147+
}
1148+
return replacement;
11141149
}
11151150
return object;
11161151
}
@@ -1119,7 +1154,7 @@ function replacePointers(object, path, replace) {
11191154
if (!subobject) {
11201155
return object;
11211156
}
1122-
var newsub = replacePointers(subobject, path.slice(1), replace);
1157+
var newsub = replacePointers(subobject, path.slice(1), replace, options);
11231158
var answer = {};
11241159
for (var key in object) {
11251160
if (key == path[0]) {

src/Routers/ClassesRouter.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const ALLOWED_GET_QUERY_KEYS = [
1111
'readPreference',
1212
'includeReadPreference',
1313
'subqueryReadPreference',
14+
'ignoreIncludeErrors',
1415
];
1516

1617
export class ClassesRouter extends PromiseRouter {
@@ -75,6 +76,9 @@ export class ClassesRouter extends PromiseRouter {
7576
if (typeof body.subqueryReadPreference === 'string') {
7677
options.subqueryReadPreference = body.subqueryReadPreference;
7778
}
79+
if (body.ignoreIncludeErrors != null) {
80+
options.ignoreIncludeErrors = !!body.ignoreIncludeErrors;
81+
}
7882

7983
return rest
8084
.get(
@@ -174,6 +178,7 @@ export class ClassesRouter extends PromiseRouter {
174178
'hint',
175179
'explain',
176180
'comment',
181+
'ignoreIncludeErrors',
177182
];
178183

179184
for (const key of Object.keys(body)) {
@@ -226,6 +231,9 @@ export class ClassesRouter extends PromiseRouter {
226231
if (body.comment && typeof body.comment === 'string') {
227232
options.comment = body.comment;
228233
}
234+
if (body.ignoreIncludeErrors) {
235+
options.ignoreIncludeErrors = true;
236+
}
229237
return options;
230238
}
231239

0 commit comments

Comments
 (0)