Query related object after save

I add a new item into a class, and now would like to use the afterSave Cloud Code to edit a related table.

My data structure ties into the User class. So each user can enter a score for each golf hole played (this is a golf score) into a Score class. The User object is included to this save to as a Pointer data type.

In the afterSave function, how do I then query a 3rd class with this same User object?

Parse.Cloud.afterSave("Score", function(request, response) {
     newScore = request.object;
     holeNo = newScore.get("HoleNo");
     user = newScore.get("UserID"); // UserID is the class field that is the Pointer to User class

    console.log("User: " + user); // Returns "[object Object]" in the console
    console.log("User: " + user.get("username")); // Returns "undefined"

    leaderQuery = new Parse.Query("Leaderboard");
    leaderQuery.equalTo("UserID", user); // If I print the result.length, also returns "undefined"
};

Any help with what I am doing wrong?

Is user a Parse Object? Adding "User: " + user will convert any object to [object Object] which is unhelpful. Try console.log("User:", user). You can also log using console.log("User", user.attributes) to get the full Parse Object. I believe pointers need to be fetched in cloud functions though.

Your “LeaderBoard” query might require masterKey, depending on your ACLs.

Parse.Cloud.afterSave("Score", async (request) => {
  const user = request.object.get("UserID"); // you could also use request.user, if the user saving the score is supposed to be the same as UserID.
  const leaderQuery = new Parse.Query("Leaderboard");
  leaderQuery.equalTo("UserID", user);
  const leaderObject = await leaderQuery.first(); // might need master depending on ACLs
  if (!leaderObject) {
    // no leaderboard object
  }
  // do what you need with leader object here
});

Let me know if i’ve interpreted your issue incorrectly!

Thanks for the suggestions, but there is no change in behaviour. User is Parse’s default (special) User class - for registering users, etc.

Your use of:

const user = request.object.get("UserID");

Should be the same as my existing, just putting everything inside my newScore object:

newScore = request.object;
user = newScore.get("UserID");

No?

If I then try:

console.log("Current User: " + user.get("username"));

It also returns “undefined”

What do you mean by “pointers need to be fetched in cloud functions”?

Because the User object is contained within my save call, I expected the full object to be available and reusable in the afterSave function.

I believe you have to call user.fetch() to use .get on a pointer in cloud code.

But that shouldn’t stop your query from working, could you share your leaderboard class as well?

Thanks for that. What code are you using to pull the Leaderboard objects (the bit after leaderQuery.equalTo("UserID", user); in your example)?

I have copied and ran your code, however I am still unable to replicate your issue. Is this setup correct (other than the additional keys not added)?

 Parse.Cloud.afterSave("Score", async request => {
      const newScore = request.object;
      const holeNo = newScore.get("HoleNo");
      const user = newScore.get("UserID"); // UserID is the class field that is the Pointer to User class
 
     console.log("User: " + user); // Returns "[object Object]" in the console
     console.log("User: " + user.get("username")); // Returns "undefined"

     console.log("User: ", user); // ParseUser { _objCount: 132,}
     console.log("User: ", user.get("username")); // Returns "undefined"
 
     const leaderQuery = new Parse.Query("Leaderboard");
     leaderQuery.equalTo("UserID", user); 
     let leaderObject = await leaderQuery.first();
     console.log('LeaderObject:',leaderObject); // Returns a Parse Object
     console.log('Captains',leaderObject.get('CompId')); // Returns Captains
 });

    const user = new Parse.User();
    user.set('password', 'asdf');
    user.set('email', '[email protected]');
    user.set('username', 'zxcv');
    await user.signUp();

    const obj = new Parse.Object('Leaderboard');
    obj.set('UserID',user);
    obj.set('CompId','Captains')
    await obj.save();

    const score = new Parse.Object('Score');
    score.set('HoleNo',2)
    score.set('HolePar',4)
    score.set('UserID',user);
    await score.save();

the definition of my afterSave (differs from yours) is:

Parse.Cloud.afterSave("Score", function(request, response) {
     // all the code in here...
};

You use an “async request” format - how / why is this different? I am expecting it to do its processing only when it has received the data? I am near-newbie to JS.

After the Leaderboard query, there should be 1 item per User per competition (CompID key). This object / row needs updating with (I presume, though cannot test until I have the User object stuff working):

leaderPlayer.increment("holesPlayed");
var relativeScore = leaderQuery.get("RelativeScore") + holeRelative; // Works out par, birdie, bogey
var grossScore = leaderQuery.get("GrossScore") + holeGross; // Cumulative total
leaderPlayer.set("RelativeScore", relativeScore);
leaderPlayer.set("GrossScore", grossScore);

leaderPlayer.save();

Thank you for that. I thought you might have had more code in your afterSave that you didn’t share. I use async so I can await the query result before proceeding. Otherwise you’ll need .then, as query lookups are promises.

Do you call query.first() or query.find()? Your query won’t be resolved until you run that function and await the result.

If you’d prefer not to use async, you can use:

Parse.Cloud.afterSave("Score", function(request) {
  // afterSave functions only need one parameter
  const newScore = request.object;
  const holeNo = newScore.get("HoleNo");
  const user = newScore.get("UserID"); // UserID is the class field that is the Pointer to User class

 console.log("User: ", user); // ParseUser { _objCount: 132,}
 console.log("User: ", user.get("username")); // Returns "undefined"

 const leaderQuery = new Parse.Query("Leaderboard");
 leaderQuery.equalTo("UserID", user); 
 leaderQuery.first().then(leaderObject => {
    console.log('LeaderObject:',leaderObject);
    console.log('Captains',leaderObject.get('CompId'));
    // do what you want with leaderObject here
 });

});

There is other code, but I (hopefully) am figuring that out.

The bit I am stuck on is that the console.log("User: " + user.get("username")); and other console.log line aren’t returning the User data from the original afterSave request data - for me. In my earlier code, the comments after a line were to direct you to the output I am getting.

I have re-run replicating your async alternative code. I still have the same problem. My edited version, straight after var declarations, is:

console.log("data available pre: " + user.isDataAvailable());
if (!user.isDataAvailable()) {

    user.fetch();
}
     console.log("data available post: " + user.isDataAvailable());
     console.log("Current User: " + user.get("username"));

Returns:

data available pre: false,
data available post: false,
Current User: undefined,

This means I can’t do the Leaderboard query, as User is null / empty / nil, or whatever state JavaScript calls this.

Not that it should matter but the data entry is coming in from an iOS app. I am just watching the Parse-server container’s log files

The iOS (Swift) code making the initial save is:

func addScore(toTable table: String, atHole holeNo: Int, withScore holeScore: Int) {
        
        let currentUser = PFUser.current()
        print("\(currentUser)")
        
        let score = PFObject(className: "Score")
        score["UserID"] = currentUser
        score["CourseName"] = "Lakes"
        score["HoleNo"] = holeNo
        score["HolePar"] = 4
        score["HoleSI"] = 3
        score["ScoreGross"] = holeScore
        score["ScoreHcap"] = 13
        
        score.saveInBackground { (success: Bool, error: Error?) in
            if (success) {
                print("Saved")
            } else {
                print("Error: \(String(describing: error))")
            }
        }
        
    }

Did this code output a leaderboard object for you?

.fetch is a promise as well, so you’ll need await user.fetch() or user.fetch().then... before accessing username.

OMG - I think we found it! This promise thing is central, I think…

Just changed, inside the if statement, to:

await user.fetch();

Re-ran. Output below contains a User with data:

data available pre: false,
info: afterSave triggered for Score for user cmwNOTJjhW:,
Input: {“UserID”:{"__type":“Pointer”,“className”:"_User",“objectId”:“cmwNOTJjhW”},“HoleNo”:2,“HoleSI”:3,“Club”:“MAGC”,“HolePar”:4,“CourseName”:“Lakes”,“ScoreGross”:5,“ScoreHcap”:13,“createdAt”:“2020-08-20T21:06:03.625Z”,“updatedAt”:“2020-08-20T21:06:03.625Z”,“objectId”:“8Oi1kXxpi9”} {“className”:“Score”,“triggerType”:“afterSave”,“user”:“cmwNOTJjhW”},
verbose: REQUEST for [GET] /parse/classes/_User/cmwNOTJjhW: {} {“method”:“GET”,“url”:"/parse/classes/_User/cmwNOTJjhW",“headers”:{“user-agent”:“node-XMLHttpRequest, Parse/js2.12.0 (NodeJS 12.16.1)”,“accept”:"/",“content-type”:“text/plain”,“host”:“server:1337”,“content-length”:“161”,“connection”:“close”},“body”:{}},
verbose: RESPONSE from [GET] /parse/classes/_User/cmwNOTJjhW: {,
“response”: {,
“objectId”: “cmwNOTJjhW”,
“username”: “myUsername”,
“phone”: “415-392-0202”,
“emailVerified”: true,
“createdAt”: “2020-08-20T08:59:04.800Z”,
“updatedAt”: “2020-08-20T08:59:27.820Z”,
“ACL”: {,
": {,
“read”: true,
},
“cmwNOTJjhW”: {,
“read”: true,
“write”: true,
},
},
},
} {“result”:{“response”:{“objectId”:“cmwNOTJjhW”,“username”:“myUsername”,“phone”:“415-392-0202”,“emailVerified”:true,“createdAt”:“2020-08-20T08:59:04.800Z”,“updatedAt”:“2020-08-20T08:59:27.820Z”,“ACL”:{"
”:{“read”:true},“cmwNOTJjhW”:{“read”:true,“write”:true}}}}},
data available post: true,
Current User: myUsername,
info: afterSave triggered for Score for user cmwNOTJjhW:,

The output above, line 2 is the end of the afterSave. Is this async performed on a different thread?

The extra Leaderboard query code returns (and so works as intended):

LeaderObject: ParseObject {,
  _objCount: 7,,
  className: 'Leaderboard',,
  id: 'CASm3XDkO9',
},
Captains Captains 2020,

No worries, glad we figured it out! Important to remember that any database actions (such as fetch, save, find, etc) in JS need to be resolved before you access the data.

If you use async, the afterSave will wait until any promises with await in front of them are completed. If you’re using .then, (unless you return a promise in the afterSave block), the afterSave will trigger as completed and the promise will continue.