diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 905b7647c1..ca142134f0 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -121,10 +121,10 @@ describe('transformWhere', () => { }); }); -describe('untransformObject', () => { +describe('mongoObjectToParseObject', () => { it('built-in timestamps', (done) => { var input = {createdAt: new Date(), updatedAt: new Date()}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(typeof output.createdAt).toEqual('string'); expect(typeof output.updatedAt).toEqual('string'); done(); @@ -132,7 +132,7 @@ describe('untransformObject', () => { it('pointer', (done) => { var input = {_p_userPointer: '_User$123'}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(typeof output.userPointer).toEqual('object'); expect(output.userPointer).toEqual( {__type: 'Pointer', className: '_User', objectId: '123'} @@ -142,14 +142,14 @@ describe('untransformObject', () => { it('null pointer', (done) => { var input = {_p_userPointer: null}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(output.userPointer).toBeUndefined(); done(); }); it('file', (done) => { var input = {picture: 'pic.jpg'}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(typeof output.picture).toEqual('object'); expect(output.picture).toEqual({__type: 'File', name: 'pic.jpg'}); done(); @@ -157,7 +157,7 @@ describe('untransformObject', () => { it('geopoint', (done) => { var input = {location: [180, -180]}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(typeof output.location).toEqual('object'); expect(output.location).toEqual( {__type: 'GeoPoint', longitude: 180, latitude: -180} @@ -167,7 +167,7 @@ describe('untransformObject', () => { it('nested array', (done) => { var input = {arr: [{_testKey: 'testValue' }]}; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(Array.isArray(output.arr)).toEqual(true); expect(output.arr).toEqual([{ _testKey: 'testValue'}]); done(); @@ -185,7 +185,7 @@ describe('untransformObject', () => { }, regularKey: "some data", }]} - let output = transform.untransformObject(dummySchema, null, input); + let output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(dd(output, input)).toEqual(undefined); done(); }); @@ -253,7 +253,7 @@ describe('transform schema key changes', () => { _rperm: ["*"], _wperm: ["Kevin"] }; - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(typeof output.ACL).toEqual('object'); expect(output._rperm).toBeUndefined(); expect(output._wperm).toBeUndefined(); @@ -267,7 +267,7 @@ describe('transform schema key changes', () => { long: mongodb.Long.fromNumber(Number.MAX_SAFE_INTEGER), double: new mongodb.Double(Number.MAX_VALUE) } - var output = transform.untransformObject(dummySchema, null, input); + var output = transform.mongoObjectToParseObject(dummySchema, null, input); expect(output.long).toBe(Number.MAX_SAFE_INTEGER); expect(output.double).toBe(Number.MAX_VALUE); done(); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 21603a0956..80fe442c1f 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -1120,4 +1120,54 @@ describe('miscellaneous', function() { done(); }) }); + + it('does not change inner object key names _auth_data_something', done => { + new Parse.Object('O').save({ innerObj: {_auth_data_facebook: 7}}) + .then(object => new Parse.Query('O').get(object.id)) + .then(object => { + expect(object.get('innerObj')).toEqual({_auth_data_facebook: 7}); + done(); + }); + }); + + it('does not change inner object key names _p_somethign', done => { + new Parse.Object('O').save({ innerObj: {_p_data: 7}}) + .then(object => new Parse.Query('O').get(object.id)) + .then(object => { + expect(object.get('innerObj')).toEqual({_p_data: 7}); + done(); + }); + }); + + it('does not change inner object key names _rperm, _wperm', done => { + new Parse.Object('O').save({ innerObj: {_rperm: 7, _wperm: 8}}) + .then(object => new Parse.Query('O').get(object.id)) + .then(object => { + expect(object.get('innerObj')).toEqual({_rperm: 7, _wperm: 8}); + done(); + }); + }); + + it('does not change inner objects if the key has the same name as a geopoint field on the class, and the value is an array of length 2, or if the key has the same name as a file field on the class, and the value is a string', done => { + let file = new Parse.File('myfile.txt', { base64: 'eAo=' }); + file.save() + .then(f => { + let obj = new Parse.Object('O'); + obj.set('fileField', f); + obj.set('geoField', new Parse.GeoPoint(0, 0)); + obj.set('innerObj', { + fileField: "data", + geoField: [1,2], + }); + return obj.save(); + }) + .then(object => object.fetch()) + .then(object => { + expect(object.get('innerObj')).toEqual({ + fileField: "data", + geoField: [1,2], + }); + done(); + }); + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index e194dfe00f..b4e1f84eb1 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -197,6 +197,14 @@ export class MongoStorageAdapter { }); } + // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. + // Accepts the schemaController for legacy reasons. + find(className, query, { skip, limit, sort }, schemaController) { + return this.adaptiveCollection(className) + .then(collection => collection.find(query, { skip, limit, sort })) + .then(objects => objects.map(object => transform.mongoObjectToParseObject(schemaController, className, object))); + } + get transform() { return transform; } diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 33aa666b09..3bb008f78a 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -713,25 +713,49 @@ function transformUpdateOperator({ } } -const specialKeysForUntransform = [ - '_id', - '_hashed_password', - '_acl', - '_email_verify_token', - '_perishable_token', - '_tombstone', - '_session_token', - 'updatedAt', - '_updated_at', - 'createdAt', - '_created_at', - 'expiresAt', - '_expiresAt', -]; +const nestedMongoObjectToNestedParseObject = mongoObject => { + switch(typeof mongoObject) { + case 'string': + case 'number': + case 'boolean': + return mongoObject; + case 'undefined': + case 'symbol': + case 'function': + throw 'bad value in mongoObjectToParseObject'; + case 'object': + if (mongoObject === null) { + return null; + } + if (mongoObject instanceof Array) { + return mongoObject.map(nestedMongoObjectToNestedParseObject); + } + + if (mongoObject instanceof Date) { + return Parse._encode(mongoObject); + } + + if (mongoObject instanceof mongodb.Long) { + return mongoObject.toNumber(); + } + + if (mongoObject instanceof mongodb.Double) { + return mongoObject.value; + } + + if (BytesCoder.isValidDatabaseObject(mongoObject)) { + return BytesCoder.databaseToJSON(mongoObject); + } + + return _.mapValues(mongoObject, nestedMongoObjectToNestedParseObject); + default: + throw 'unknown js type'; + } +} // Converts from a mongo-format object to a REST-format object. // Does not strip out anything based on a lack of authentication. -function untransformObject(schema, className, mongoObject, isNestedObject = false) { +const mongoObjectToParseObject = (schema, className, mongoObject) => { switch(typeof mongoObject) { case 'string': case 'number': @@ -740,15 +764,13 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals case 'undefined': case 'symbol': case 'function': - throw 'bad value in untransformObject'; + throw 'bad value in mongoObjectToParseObject'; case 'object': if (mongoObject === null) { return null; } if (mongoObject instanceof Array) { - return mongoObject.map(arrayEntry => { - return untransformObject(schema, className, arrayEntry, true); - }); + return mongoObject.map(nestedMongoObjectToNestedParseObject); } if (mongoObject instanceof Date) { @@ -769,10 +791,6 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals var restObject = untransformACL(mongoObject); for (var key in mongoObject) { - if (isNestedObject && _.includes(specialKeysForUntransform, key)) { - restObject[key] = untransformObject(schema, className, mongoObject[key], true); - continue; - } switch(key) { case '_id': restObject['objectId'] = '' + mongoObject[key]; @@ -840,7 +858,7 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals objectId: objData[1] }; break; - } else if (!isNestedObject && key[0] == '_' && key != '__type') { + } else if (key[0] == '_' && key != '__type') { throw ('bad key in untransform: ' + key); } else { var expectedType = schema.getExpectedType(className, key); @@ -854,80 +872,16 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals break; } } - restObject[key] = untransformObject(schema, className, mongoObject[key], true); + restObject[key] = nestedMongoObjectToNestedParseObject(mongoObject[key]); } } - if (!isNestedObject) { - let relationFields = schema.getRelationFields(className); - Object.assign(restObject, relationFields); - } - return restObject; + return { ...restObject, ...schema.getRelationFields(className) }; default: throw 'unknown js type'; } } -function transformSelect(selectObject, key ,objects) { - var values = []; - for (var result of objects) { - values.push(result[key]); - } - delete selectObject['$select']; - if (Array.isArray(selectObject['$in'])) { - selectObject['$in'] = selectObject['$in'].concat(values); - } else { - selectObject['$in'] = values; - } -} - -function transformDontSelect(dontSelectObject, key, objects) { - var values = []; - for (var result of objects) { - values.push(result[key]); - } - delete dontSelectObject['$dontSelect']; - if (Array.isArray(dontSelectObject['$nin'])) { - dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values); - } else { - dontSelectObject['$nin'] = values; - } -} - -function transformInQuery(inQueryObject, className, results) { - var values = []; - for (var result of results) { - values.push({ - __type: 'Pointer', - className: className, - objectId: result.objectId - }); - } - delete inQueryObject['$inQuery']; - if (Array.isArray(inQueryObject['$in'])) { - inQueryObject['$in'] = inQueryObject['$in'].concat(values); - } else { - inQueryObject['$in'] = values; - } -} - -function transformNotInQuery(notInQueryObject, className, results) { - var values = []; - for (var result of results) { - values.push({ - __type: 'Pointer', - className: className, - objectId: result.objectId - }); - } - delete notInQueryObject['$notInQuery']; - if (Array.isArray(notInQueryObject['$nin'])) { - notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values); - } else { - notInQueryObject['$nin'] = values; - } -} - var DateCoder = { JSONToDatabase(json) { return new Date(json.iso); @@ -1021,9 +975,5 @@ module.exports = { parseObjectToMongoObjectForCreate, transformUpdate, transformWhere, - transformSelect, - transformDontSelect, - transformInQuery, - transformNotInQuery, - untransformObject + mongoObjectToParseObject, }; diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 522a2f1443..67240a9128 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -146,12 +146,8 @@ DatabaseController.prototype.validateObject = function(className, object, query, }); }; -// Like transform.untransformObject but you need to provide a className. // Filters out any data that shouldn't be on this REST-formatted object. -DatabaseController.prototype.untransformObject = function( - schema, isMaster, aclGroup, className, mongoObject) { - var object = this.transform.untransformObject(schema, className, mongoObject); - +const filterSensitiveData = (isMaster, aclGroup, className, object) => { if (className !== '_User') { return object; } @@ -705,12 +701,8 @@ DatabaseController.prototype.find = function(className, query, { delete mongoOptions.limit; return collection.count(mongoWhere, mongoOptions); } else { - return collection.find(mongoWhere, mongoOptions) - .then(mongoResults => { - return mongoResults.map(result => { - return this.untransformObject(schemaController, isMaster, aclGroup, className, result); - }); - }); + return this.adapter.find(className, mongoWhere, mongoOptions, schemaController) + .then(objects => objects.map(object => filterSensitiveData(isMaster, aclGroup, className, object))); } }); }); diff --git a/src/RestQuery.js b/src/RestQuery.js index 34324f5e0e..a1d48fa7e8 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -188,6 +188,23 @@ RestQuery.prototype.validateClientClassCreation = function() { } }; +function transformInQuery(inQueryObject, className, results) { + var values = []; + for (var result of results) { + values.push({ + __type: 'Pointer', + className: className, + objectId: result.objectId + }); + } + delete inQueryObject['$inQuery']; + if (Array.isArray(inQueryObject['$in'])) { + inQueryObject['$in'] = inQueryObject['$in'].concat(values); + } else { + inQueryObject['$in'] = values; + } +} + // Replaces a $inQuery clause by running the subquery, if there is an // $inQuery clause. // The $inQuery clause turns into an $in with values that are just @@ -213,12 +230,29 @@ RestQuery.prototype.replaceInQuery = function() { this.config, this.auth, inQueryValue.className, inQueryValue.where, additionalOptions); return subquery.execute().then((response) => { - this.config.database.transform.transformInQuery(inQueryObject, subquery.className, response.results); + transformInQuery(inQueryObject, subquery.className, response.results); // Recurse to repeat return this.replaceInQuery(); }); }; +function transformNotInQuery(notInQueryObject, className, results) { + var values = []; + for (var result of results) { + values.push({ + __type: 'Pointer', + className: className, + objectId: result.objectId + }); + } + delete notInQueryObject['$notInQuery']; + if (Array.isArray(notInQueryObject['$nin'])) { + notInQueryObject['$nin'] = notInQueryObject['$nin'].concat(values); + } else { + notInQueryObject['$nin'] = values; + } +} + // Replaces a $notInQuery clause by running the subquery, if there is an // $notInQuery clause. // The $notInQuery clause turns into a $nin with values that are just @@ -244,12 +278,25 @@ RestQuery.prototype.replaceNotInQuery = function() { this.config, this.auth, notInQueryValue.className, notInQueryValue.where, additionalOptions); return subquery.execute().then((response) => { - this.config.database.transform.transformNotInQuery(notInQueryObject, subquery.className, response.results); + transformNotInQuery(notInQueryObject, subquery.className, response.results); // Recurse to repeat return this.replaceNotInQuery(); }); }; +const transformSelect = (selectObject, key ,objects) => { + var values = []; + for (var result of objects) { + values.push(result[key]); + } + delete selectObject['$select']; + if (Array.isArray(selectObject['$in'])) { + selectObject['$in'] = selectObject['$in'].concat(values); + } else { + selectObject['$in'] = values; + } +} + // Replaces a $select clause by running the subquery, if there is a // $select clause. // The $select clause turns into an $in with values selected out of @@ -281,12 +328,25 @@ RestQuery.prototype.replaceSelect = function() { this.config, this.auth, selectValue.query.className, selectValue.query.where, additionalOptions); return subquery.execute().then((response) => { - this.config.database.transform.transformSelect(selectObject, selectValue.key, response.results); + transformSelect(selectObject, selectValue.key, response.results); // Keep replacing $select clauses return this.replaceSelect(); }) }; +const transformDontSelect = (dontSelectObject, key, objects) => { + var values = []; + for (var result of objects) { + values.push(result[key]); + } + delete dontSelectObject['$dontSelect']; + if (Array.isArray(dontSelectObject['$nin'])) { + dontSelectObject['$nin'] = dontSelectObject['$nin'].concat(values); + } else { + dontSelectObject['$nin'] = values; + } +} + // Replaces a $dontSelect clause by running the subquery, if there is a // $dontSelect clause. // The $dontSelect clause turns into an $nin with values selected out of @@ -316,7 +376,7 @@ RestQuery.prototype.replaceDontSelect = function() { this.config, this.auth, dontSelectValue.query.className, dontSelectValue.query.where, additionalOptions); return subquery.execute().then((response) => { - this.config.database.transform.transformDontSelect(dontSelectObject, dontSelectValue.key, response.results); + transformDontSelect(dontSelectObject, dontSelectValue.key, response.results); // Keep replacing $dontSelect clauses return this.replaceDontSelect(); })