parse-community / Parse-SDK-JS

The JavaScript SDK for Parse Platform
https://parseplatform.org
Apache License 2.0
1.32k stars 597 forks source link

Returning a ParseObject that has been turned into JSON from Cloud Code reverts pointers back to ParseObjects when received on frontend #258

Closed ibrennan closed 8 years ago

ibrennan commented 8 years ago

Version affected: _1.8.2 (and previous) _Enviroments: Node.JS v4.4.3, OSX El Capitan Chrome 49.0.2623.110 Steps to reproduce:

  1. Create some dummy data:
var Post = Parse.Object.extend("Post");
var Comment = Parse.Object.extend("Comment");

var myPost = new Post();
myPost.set("title", "I'm Hungry");

var myOtherPost = new Post();
myOtherPost.set("title", "I'm not hungry now");

var myComment = new Comment();
myComment.set("content", "Let's do Sushirrito.");
myComment.set("parent", myPost);
myComment.save();

var myOtherComment = new Comment();
myOtherComment.set("content", "Glad you enjoyed your lunch");
myOtherComment.set("parent", myOtherPost);
myOtherComment.save();
  1. Now let's write a simple cloud function to get the data, and turn it all into JSON and return to frontend. If you run the following you'll notice that the console logged comments variable is correctly represented as a nested Object...
Parse.Cloud.define('getComments', function(request, response){
    var Comment = Parse.Object.extend("Comment");
    var query = new Parse.Query(Comment);
    query.include('parent');
    query.find({
        success: function(comments) {
            console.log('comments pre .toJSON()', comments);
            comments = comments.map(function(comment) {
                return comment.toJSON();
            });

            // At this point comments is correctly format JSON:
            console.log('comments post .toJSON()', comments);

            // Return the JSON to frontend:
            response.success(comments);
        },
        error: function(e) {
            console.error(e);
            response.error(e);
        }
    });
});
  1. When calling the above code you'd expect it to return that same Object to the frontend, but instead what happens is this:
Parse.Cloud.run('getComments', {}).then(function(comments) {
    console.log('getComments result:', comments);
}, function(error) {
    console.error(error);
});

Summary Something appears to happen to the Object between the response.success() and the resolved data from Parse.Cloud.run()

Server side console.log result: screen shot 2016-04-14 at 15 40 12

Frontend result: screen shot 2016-04-14 at 15 40 25

ibrennan commented 8 years ago

Just to further compound the potential bug, on the Cloud Code if we do:

response.success(JSON.stringify(comments));

This will mean the response is sent as a String representation of the JSON. On the frontend we then parse the String and turn it back into JSON:

comments = JSON.parse(comments);

This results in the correctly formatted JSON representation of the data

andrewimm commented 8 years ago

Every response coming from the server is decoded to re-inflate special types that don't have a native JSON representation: Date, Parse.File, Parse.GeoPoint, etc. This also happens for Parse.Object. Any time an object comes back with a __type field, the SDK interprets it as an object that needs to be inflated. This is exactly the intended behavior, and the only way the SDK can work in the first place.

To pass back objects that you don't want to be inflated, you'd want to avoid passing the __type field back. Here's the confusing part... the brief view of output you include above shows the __type fields, but calling toJSON() in your code as described would not include that field, nor the className field. Here's the relevant code: https://github.com/ParsePlatform/Parse-SDK-JS/blob/master/src/ParseObject.js#L410

I'm going to need some more information, because the code you wrote shouldn't be outputting the output you provided, and the output you provided would be expected by design to produce the client counterparts you're seeing.

ibrennan commented 8 years ago

Thanks for the speedy reply @andrewimm, makes sense about what you're saying regarding the re-inflating of types.

I can't see the in the code you linked to where __type and className get stripped out though? I followed it back to the encode.js and couldn't see reference to it there.

Would you like anything more from me, it seems like we should be able to reproduce the __type and className field removal issue in a simple script

andrewimm commented 8 years ago

Sorry, I was trying to point out that the two fields aren't added in the first place, not that they're stripped out. When actual Parse Objects are passed back to the client from cloud code, the middleware calls a private method called _toFullJSON() to add those missing fields.

For now, could I just get the full console output of the cloud code you've included above? If necessary, I'll ask you for a repro case later.

ibrennan commented 8 years ago

Here's the output of comments, which is the array result of the .find():

[ ParseObjectSubclass { className: 'Comment', _objCount: 0, id: 'Ems4SSqa6w' },
  ParseObjectSubclass { className: 'Comment', _objCount: 2, id: 'glwheqifgI' },
  ParseObjectSubclass { className: 'Comment', _objCount: 4, id: 'xv3pofphRb' },
  ParseObjectSubclass { className: 'Comment', _objCount: 6, id: 'BViHmBCEhX' },
  ParseObjectSubclass { className: 'Comment', _objCount: 8, id: '6mOLGIC7OY' },
  ParseObjectSubclass { className: 'Comment', _objCount: 10, id: 'bONunA0osL' },
  ParseObjectSubclass { className: 'Comment', _objCount: 12, id: 'w7smqUS8T5' },
  ParseObjectSubclass { className: 'Comment', _objCount: 14, id: 'ZRrz4KRW8M' },
  ParseObjectSubclass { className: 'Comment', _objCount: 16, id: 'CZYFaopk3F' },
  ParseObjectSubclass { className: 'Comment', _objCount: 18, id: 'KAFQODiZ6H' } ]

Here's the output from the Object at index 0 in that array after calling .toJSON() on it:

{ content: 'Let\'s do Sushirrito.',
  parent:
   { title: 'I\'m Hungry',
     updatedAt: '2016-04-14T14:25:20.847Z',
     createdAt: '2016-04-14T14:25:20.847Z',
     objectId: 'te85roF7fv',
     __type: 'Object',
     className: 'Post' },
  updatedAt: '2016-04-14T14:25:20.919Z',
  createdAt: '2016-04-14T14:25:20.919Z',
  objectId: 'Ems4SSqa6w' }

The __type and className are only being exposed on the 'parent' key, which is a Pointer to another Class object.

andrewimm commented 8 years ago

Okay. I'd actually classify all of this to be expected behavior. When encoding an object to JSON, we still expect that nested fields will contain the appropriate __type fields, since it's impossible to add these after the fact if they're desired. And since the __type fields are being passed, I'd expect the client the inflate these into Parse.Objects.

I'd recommend that you simply rewrite your delivery code to ensure the nested fields are also coming back in your expected format. Either something like:

comments.map(function(comment) {
  var json = comment.toJSON();
  if (json.parent) {
    delete json.parent.__type;
  }
});

or write your own encoding function that only passes back the fields you need on the client, whitelist style.

Closing this as behaving as intended.

parkej60 commented 8 years ago

How am I supposed to call .toJSON on the root level object and the loop through it and call .toJSON on all of it's properties as well? For instance what if I have a Date, Parse.File and Parse.GeoPoint all associated to this one object? Maybe I'm missing something, but this seems really awkward to me.

andrewimm commented 8 years ago

@parkej60 toJSON recursively calls toJSON on all its nested children as well. The reason this whole thing seems awkward is because it is not the way you are supposed to return a Parse Object from Cloud Code. If you call response.success() with your result, the encoding is properly done for you.

parkej60 commented 8 years ago

@andrewimm I am using response.success, but not directly return parse objects from Cloud Code. I'm sending something like this response.success({"queryIdentifier":0,"results":restaurantArray}); restaurantArray is an array of Parse Objects.

andrewimm commented 8 years ago

@parkej60 you should be able to call exactly that, and it should work. If restaurantArray is an array of viable Parse Objects, it should encode properly.

parkej60 commented 8 years ago

Well, I'll keep digging into it then I guess.

parkej60 commented 8 years ago

@andrewimm Ok So here's an interesting example.

var Restaurant = Parse.Object.extend("Restaurant");
var query = new Parse.Query(Restaurant);
query.limit(20);
query.descending('rating');
query.find().then(function(restaurants){
           view.$el.html(view.template({restaurants:restaurants}));
},function(error){
            console.error("Error: " + error.code + " " + error.message);
});

This also returns the objects in the format and it's a direct server query.

{
_objCount: 0
className: "Restaurant"
id: "NtfeOn3dFH"
}

Here's an example of the database structure for this object.

{
    "_id": "NtfeOn3dFH",
    "_created_at": {
        "$date": "2014-05-20T04:50:22.292Z"
    },
    "_p_createdBy": "_User$3eVAVkR4Rk",
    "_updated_at": {
        "$date": "2014-06-04T22:01:04.324Z"
    },
    "address1": "615 W. Randolph",
    "address2": "",
    "approved": true,
    "beenThereCount": 6,
    "body": "a wine bar and restaurant with a menu of rustic food, including handcrafted charcuterie, inspired by the wine-growing regions of Southern France, Italy, Portugal and the coast of Spain. In addition to cuisine that reflects the aromas, flavors and colors of the Mediterranean, the wine bar features a moderately priced wine list that focuses on boutique vineyards throughout southwestern Europe.",
    "city": "Chicago",
    "country": "US",
    "dress_code": "",
    "facebook_handle": "http://www.facebook.com/home.php#!/pages/avec/55233720395",
    "favoriteCount": 48,
    "godfatherRecommended": true,
    "hashtags": [],
    "head_chef": "",
    "inTop100": true,
    "location": [
        -87.6433717,
        41.884343
    ],
    "lowerCaseCity": "chicago",
    "lowerCaseNeighborhood": "near west side",
    "lowerCaseTerms": [],
    "lowerCaseTitle": "avec",
    "menu_url": "http://avecrestaurant.com/menus/3-dinner-menu#",
    "neighborhood": "Near West Side",
    "nid": "129",
    "on_opentable": "0",
    "only_local": "1",
    "phone": "312-377-2002",
    "picture": "14014353-b5f6-4679-9c7b-9c38278f6f9f-129-129.jpg",
    "pictureThumbnail": "a166b80a-0332-4861-89fb-4123069a46f3-thumbnail.jpg",
    "pinterest": "",
    "price_level_id": 3,
    "rating": 3606,
    "recommendCount": 13,
    "reservation_url": "",
    "state": "IL",
    "terms": [
        "Fusion / Eclectic"
    ],
    "title": "Avec",
    "twitter_handle": "",
    "vaultedCount": 11,
    "website": "http://www.avecrestaurant.com/",
    "words": [
        "a",
        "wine",
        "bar",
        "restaurant",
        "with",
        "a",
        "menu",
        "of",
        "rustic",
        "food",
        "including",
        "handcrafted",
        "charcuterie",
        "inspired",
        "by",
        "wine",
        "growing",
        "regions",
        "of",
        "southern",
        "france",
        "italy",
        "portugal",
        "coast",
        "of",
        "spain",
        "addition",
        "to",
        "cuisine",
        "that",
        "reflects",
        "aromas",
        "flavors",
        "colors",
        "of",
        "mediterranean",
        "wine",
        "bar",
        "features",
        "a",
        "moderately",
        "priced",
        "wine",
        "list",
        "that",
        "focuses",
        "on",
        "boutique",
        "vineyards",
        "throughout",
        "southwestern",
        "europe"
    ],
    "zipCode": "60661"
}
arladmin commented 4 years ago

Hi @parkej60 , were you able to work out a solution for this? As this still seems to be an issue, at least in my case.

tiavina-mika commented 3 years ago

Hi! so finally is there any solutions for this issue?