Cloud Function returned "Cannot read property 'attributes' of undefined" -- Moralis DB timing problem?

Hi,

@ivan , @filip

For the Videos “I cloned rarible in 24hours”… I guess I have a timing problem retrieving linked table data.

My server (0.0.232): https://1bxtbthptjkt.moralis.io:2053/server
=> testserevr to Ropsten (Toronto).

The call to the Cloud Function getItem returns “Cannot read property ‘attributes’ of undefined” when called as a subscribe() - create action. Not when normally called.

What helps me is the Log at the server: Dashboard -> Logs -> Info (not auto updating, so click the ‘Info’ link in the left menu panel :wink: )

To add logging, see: https://docs.moralis.io/moralis-server/cloud-functions#console-log

This is the getItem function inside the Cloud Functions of the server:

Moralis.Cloud.define("getItem", async (request) => {
    const logger = Moralis.Cloud.getLogger();
    
    const query = new Moralis.Query("ItemsForSale");
    const uid = request.params.uid;
	logger.info("getItem uid=" + uid); 
    query.equalTo("uid", uid);
    query.select("uid","askingPrice","tokenAddress","tokenId", "token.token_uri", "token.symbol","token.owner_of","token.id","user.avatar","user.username");
  
    const queryResult = await query.first({useMasterKey:true});
    if (!queryResult) return {};
	logger.info("getItem queryResult=" + JSON.stringify(queryResult.attributes)); 
    return {
        "uid": queryResult.attributes.uid,
        "tokenId": queryResult.attributes.tokenId,
        "tokenAddress": queryResult.attributes.tokenAddress,
        "askingPrice": queryResult.attributes.askingPrice,
  
        "symbol": queryResult.attributes.token.attributes.symbol,
        "tokenUri": queryResult.attributes.token.attributes.token_uri,
        "ownerOf": queryResult.attributes.token.attributes.owner_of,
        "tokenObjectId": queryResult.attributes.token.id,
  
        "sellerUsername": queryResult.attributes.user.attributes.username,
        "sellerAvatar": queryResult.attributes.user.attributes.avatar,
      };
  });

In the main.js the getItem is called (1 of 2 times):

onItemAdded = async (item) => {
    console.log("onItemAdded... item=", item);
    const params = {uid: `${item.attributes.uid}`};
    console.log("onItemAdded... params=", JSON.stringify(params));
    const addedItem = await Moralis.Cloud.run("getItem", params);
    console.log("onItemAdded... addedItem=", addedItem);
    if (addedItem){
        user = await Moralis.User.current();
        console.log("onItemAdded... user=", user);
        if (user){
            if (user.get('accounts').includes(addedItem.ownerOf)){
                const userItemListing = document.getElementById(`user-item-${item.tokenObjectId}`);
                console.log("onItemAdded... userItemListing=", userItemListing);
                if (userItemListing) userItemListing.parentNode.removeChild(userItemListing);

                getAndRenderItemData(addedItem, renderUserItem);
                return;
            }
        }
        getAndRenderItemData(addedItem, renderItem);
    }
}

And this onItemAdded is called from init as a subscribed action, when an item is created:

init = async () => {
    console.log("init...");
    hideElement(userItemsSection);
    window.web3 = await Moralis.Web3.enable();
    window.tokenContract = new web3.eth.Contract(tokenContractAbi, TOKEN_CONTRACT_ADDRESS);
    window.marketplaceContract = new web3.eth.Contract(marketplaceContractAbi, MARKETPLACE_CONTRACT_ADDRESS);
    initUser();
    loadItems();

    const soldItemsQuery = new Moralis.Query('SoldItems');
    const soldItemsSubscription = await soldItemsQuery.subscribe();
    soldItemsSubscription.on("create", onItemSold);

    const itemsAddedQuery = new Moralis.Query('ItemsForSale');
    const itemsAddedSubscription = await itemsAddedQuery.subscribe();
    itemsAddedSubscription.on("create", onItemAdded);
}

After creating an Item, after the 3 Metamask actions, when the item is added to the Moralis ItemsForSale table the subscribed action becomes activated and so the onItemAdded function, which run the getItem of the Cloud Functions.

The browsers console shows:

The server Info Log shows:

2021-06-13T15:19:04.050Z - TypeError: Cannot read property 'attributes' of undefined
    at eval (eval at customUserPlugin (/moralis-server/cloud/main.js:8:21), <anonymous>:1:3315)
    at runMicrotasks (<anonymous>)
    at processTicksAndRejections (internal/process/task_queues.js:95:5)
2021-06-13T15:19:04.048Z - Failed running cloud function getItem for user M7FC1EmtVPGBjCf8KUivaIbb with:
  Input: {"uid":"25"}
  Error: {"message":"Cannot read property 'attributes' of undefined","code":141}
2021-06-13T15:19:04.047Z - getItem queryResult={"uid":"25","tokenId":"29","tokenAddress":"0x21a56aec91d44e84244b9e50a061c70b38ce322b","askingPrice":"11117777","createdAt":"2021-06-13T15:19:03.772Z","updatedAt":"2021-06-13T15:19:03.772Z"}
2021-06-13T15:19:04.038Z - getItem uid=25

Viewing the queryResult formatted:
afbeelding

I MISS the related Token and User fields!!
Is this a timing problem, since the subscribe action is directly after the creation of the record??

If i add the getItem call to the loadUserItems (to call it when opening “My Items” and see it only in Console of browser):

loadUserItems = async () => {
    const params = {uid: "19"};
    console.log("loadUserItems ##... params=", JSON.stringify(params));
    const addedItem = await Moralis.Cloud.run("getItem", params);
    console.log("loadUserItems ** addedItem=", addedItem);

    console.log("loadUserItems...");
    const ownedItems = await Moralis.Cloud.run("getUserItems");
    console.log("loadUserItems... ownedItems=", ownedItems);
    ownedItems.forEach(item => {
        console.log("loadUserItems... item=", item);
        const userItemListing = document.getElementById(`user-item-${item.tokenObjectId}`);
        console.log("loadUserItems... userItemListing=", userItemListing);
        if (userItemListing) return;
        getAndRenderItemData(item, renderUserItem);
    });
}

I get the right response with token and user fields:
afbeelding

The error i get “Cannot read property ‘attributes’ of undefined” is pointing to the line:
“symbol”: queryResult.attributes.token.attributes.symbol,
of the getItem Clouf Function (!!).

No… even delaying the onItemAdded function gives the “Cannot read property ‘attributes’ of undefined” error.

Changed in main.js:

onItemAdded = (item) => {
    var _item = item;
    var d = new Date();
    var n = d.toLocaleTimeString();
    console.log("onItemAdded...1A " + n);
    console.log("onItemAdded...1A item=", _item);
    setTimeout( function(){_onItemAdded(_item)}, 30000);
}

_onItemAdded = async (item) => {
    var d = new Date();
    var n = d.toLocaleTimeString();
    console.log("onItemAdded...2 " + n);
    console.log("onItemAdded...2 item=", item);
    const params = {uid: `${item.attributes.uid}`};
    console.log("onItemAdded... params=", JSON.stringify(params));
    const addedItem = await Moralis.Cloud.run("getItem", params);
    console.log("onItemAdded... addedItem=", addedItem);
    if (addedItem){
        user = await Moralis.User.current();
        console.log("onItemAdded... user=", user);
        if (user){
            if (user.get('accounts').includes(addedItem.ownerOf)){
                const userItemListing = document.getElementById(`user-item-${item.tokenObjectId}`);
                console.log("onItemAdded... userItemListing=", userItemListing);
                if (userItemListing) userItemListing.parentNode.removeChild(userItemListing);

                getAndRenderItemData(addedItem, renderUserItem);
                return;
            }
        }
        getAndRenderItemData(addedItem, renderItem);
    }
}

Browsers console.log:

This error usually occurs when you try to read an attribute of array, not of object(which u probably wanted). Recheck the value types. I think you have an array somewhere instead of an object.

1 Like

Further investigation learns:
In the Moralis table “ItemsForSale” the 2 fields “user Pointer” and “token Pointer” are filled after a long(er) delay, can take 2…3 minutes (!!)
(Shown via regular refresh of that table).

These links are set via the Cloud Function:

  Moralis.Cloud.beforeSave("ItemsForSale", async (request) => {
    const query = new Moralis.Query("EthNFTOwners"); // ("NFTTokenOwners");
    query.equalTo("token_address", request.object.get('tokenAddress'));
    query.equalTo("token_id", request.object.get('tokenId'));
    const object = await query.first();
    if (object){
      const owner = object.attributes.owner_of;
      const userQuery = new Moralis.Query(Moralis.User);
      userQuery.equalTo("accounts", owner);
      const userObject = await userQuery.first({useMasterKey:true});
      if (userObject){
          request.object.set('user', userObject);
      }
      request.object.set('token', object);
    }
  });

When the 2 fields are filled, then the call to “getItem” returns the correct data, including the token and user fields.

@ivan , @filip
If this behaviour is normal for linked fields (filled after minutes) then this is not a acceptable situation for a production situation.

What can be done?

1 Like

Team will check tomorrow

2 Likes

Great(ly) appreciated.
I can send whole project on request

The updateAt field shows the time when the 2 links in fields ‘user Pointer’ and ‘token Pointer’ are updated from ‘null’ to their value, after the creation of the record itself with time in createdAt field.

Costs minutes!

Shown in the table ItemsForSale:

To get record(s) unless the user | token links are present I have updated the functions getItems and getItem in the Cloud Functions to:

  Moralis.Cloud.define("getItems", async (request) => {
    const query = new Moralis.Query("ItemsForSale");
    query.notEqualTo("isSold", true);
    query.select("uid","askingPrice","tokenAddress","tokenId", "token.token_uri", "token.symbol","token.owner_of","token.id", "user.avatar","user.username");

    const queryResults = await query.find({useMasterKey:true});
 
    var tokenExist, userExist;
    const results = [];
    for (let i = 0; i < queryResults.length; ++i) {
  
      //if (!queryResults[i].attributes.token || !queryResults[i].attributes.user) continue;
  
      tokenExist = (typeof queryResults[i].attributes.token !== 'undefined');
      userExist = (typeof queryResults[i].attributes.user !== 'undefined');
      results.push({
        "uid": queryResults[i].attributes.uid,
        "tokenId": queryResults[i].attributes.tokenId,
        "tokenAddress": queryResults[i].attributes.tokenAddress,
        "askingPrice": queryResults[i].attributes.askingPrice,
  
        "symbol": (tokenExist ? queryResults[i].attributes.token.attributes.symbol : ''),
        "tokenUri": (tokenExist ? queryResults[i].attributes.token.attributes.token_uri : ''),
        "ownerOf": (tokenExist ? queryResults[i].attributes.token.attributes.owner_of : ''),
        "tokenObjectId": (tokenExist ? queryResults[i].attributes.token.id : ''),
        
        "sellerUsername": (userExist ? queryResults[i].attributes.user.attributes.username : ''),
        "sellerAvatar": (userExist ? queryResults[i].attributes.user.attributes.avatar : ''),
      });
    }
  
    return results;
  });
  
  Moralis.Cloud.define("getItem", async (request) => {
    const query = new Moralis.Query("ItemsForSale");
    query.equalTo("uid", request.params.uid);
    query.select("uid","askingPrice","tokenAddress","tokenId", "token.token_uri", "token.symbol","token.owner_of","token.id","user.avatar","user.username");
  
    const queryResult = await query.first({useMasterKey:true});
    if (!queryResult) return {};

    const tokenExist = (typeof queryResult.attributes.token !== 'undefined');
    const userExist = (typeof queryResult.attributes.user !== 'undefined');
    return {
        "uid": queryResult.attributes.uid,
        "tokenId": queryResult.attributes.tokenId,
        "tokenAddress": queryResult.attributes.tokenAddress,
        "askingPrice": queryResult.attributes.askingPrice,
  
        "symbol": (tokenExist ? queryResult.attributes.token.attributes.symbol : ''),
        "tokenUri": (tokenExist ? queryResult.attributes.token.attributes.token_uri : ''),
        "ownerOf": (tokenExist ? queryResult.attributes.token.attributes.owner_of : ''),
        "tokenObjectId": (tokenExist ? queryResult.attributes.token.id : ''),
  
        "sellerUsername": (userExist ? queryResult.attributes.user.attributes.username : ''),
        "sellerAvatar": (userExist ? queryResult.attributes.user.attributes.avatar : ''),
      };
  });

Ahh, this is a bit tricky one.
The issue is caused by the Pending tables.
When the NFT is minted, it is placed in the EthNFTOwnersPending until a certain number of blocks have passed.
The ItemsForSale event is synced without any delay though, which means that the EthNFTOwner is still inside the EthNFTOwnersPending when the beforeSave is looking for it in the confirmed table.
The solution here would be to have a fallback to the pending table if you don’t find it in the confirmed table.

2 Likes

Basically - you can’t know the owner for sure instantly - we wait for a few blocks to pass to be sure transaction is not reversed

Until then, we classify it as “Pending Owner”

This is a change from when Rarible series was recorded

So if you want instant owner - check “PendingOwner”, pending will become “Owner” when a few blocks have passed and you know for sure that the tx won’t be dropped

2 Likes

Thank you @Capplequoppe, @ivan!

That makes it clear, :slight_smile:

Have a nice day!

1 Like

Is this solution okay?

Moralis.Cloud.beforeSave("ItemsForSale", async (request) => {
  const query = new Moralis.Query("EthNFTOwners"); // ("NFTTokenOwners");
  query.equalTo("token_address", request.object.get("tokenAddress"));
  query.equalTo("token_id", request.object.get("tokenId"));
  const object = await query.first();
  if (object) {
    const owner = object.attributes.owner_of;
    const userQuery = new Moralis.Query(Moralis.User);
    userQuery.equalTo("accounts", owner);
    const userObject = await userQuery.first({ useMasterKey: true });
    if (userObject) {
      request.object.set("user", userObject);
    }
    request.object.set("token", object);
  } else {
    // Check is EthNFTOwnersPending for transaction
    const query = new Moralis.Query("EthNFTOwnersPending"); // ("NFTTokenOwners");
    query.equalTo("token_address", request.object.get("tokenAddress"));
    query.equalTo("token_id", request.object.get("tokenId"));
    const objectPending = await query.first();
    if (objectPending) {
      const owner = objectPending.attributes.owner_of;
      const userQuery = new Moralis.Query(Moralis.User);
      userQuery.equalTo("accounts", owner);
      const userObject = await userQuery.first({ useMasterKey: true });
      if (userObject) {
        request.object.set("user", userObject);
      }
      request.object.set("token", object);
    }
  }
});

Hey @sleepybyte

I didn’t find any errors while reading the code. But in this case, it is advisable for you to check the status of the transaction after it has passed to the EthNFTOwners.

2 Likes