parse-server icon indicating copy to clipboard operation
parse-server copied to clipboard

Parse-server V6.3.1 query.distinct("score") throw ParseError: Invalid aggregate stage 'hint'

Open zurmokeeper opened this issue 2 years ago • 11 comments

New Issue Checklist

Issue Description

const distinctResult = await query.distinct("score");

parse-server v6.3.1 This code will give you an error.

ParseError: Invalid aggregate stage 'hint'.

Steps to reproduce

src/cloud/main.js

Parse.Cloud.define('getObject', async (request) => {
    try {
      const className = 'GameScoreXXX';
      const objectId = "3JrNlj8Wkf";
      const query = new Parse.Query(className);
      const distinctResult = await query.distinct("score");   // throw ParseError: Invalid aggregate stage 'hint'.
      const object = await query.get(objectId);
      return object.toJSON();
    } catch (error) {
     console.log('error-->', error)
      throw new Parse.Error(500, 'Failed to update object: ' + error.message);
    }
});


// src/index.js
var express = require('express');
var ParseServer = require('parse-server').ParseServer;
const PORT = 1337;
...........................................
var api = new ParseServer({
    databaseURI: database.uri,
    cloud: server.cloud,
    appId: server.appId,
    masterKey: server.masterKey
});

var app = express();
(async ()=>{
    await api.start()    
    app.use("/parse", api.app)  

    var httpServer = require('http').createServer(app)
    httpServer.listen(PORT, function() {
        console.error('parse-server-mojitest running on port ' + PORT + '.')
    })
})()

Actual Outcome

ParseError: Invalid aggregate stage 'hint'.

Expected Outcome

no error message

Environment

Server

  • Parse Server version: V6.3.1
  • Operating system: win11
  • Local or remote host (AWS, Azure, Google Cloud, Heroku, Digital Ocean, etc): local

Database

  • System (MongoDB or Postgres): MongoDB
  • Database version: 5.0
  • Local or remote host (MongoDB Atlas, mLab, AWS, Azure, Google Cloud, etc): Local

Just use the third release tool to request it directly

Logs

Some Detail

// Parse-server V5.6.1  src/Routers/AggregateRouter.js  

export class AggregateRouter extends ClassesRouter {
  handleFind(req) {
    const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
    const options = {};
    if (body.distinct) {
      options.distinct = String(body.distinct);
    }
     //  Here body.hint : underfined, so it doesn't delete to the
    if (body.hint) {
      options.hint = body.hint;
      delete body.hint;
    }

   // so body.hint : underfined
}

So when stageName=hint   here, it's a direct error

  static transformStage(stageName, stage) {
    const skipKeys = ['distinct', 'where'];
    if (skipKeys.includes(stageName)) {
      return;
    }
    if (stageName[0] !== '$') {
      throw new Parse.Error(Parse.Error.INVALID_QUERY, `Invalid aggregate stage '${stageName}'.`);
    }
}

}

zurmokeeper avatar Nov 10 '23 09:11 zurmokeeper

Thanks for opening this issue!

  • 🚀 You can help us to fix this issue faster by opening a pull request with a failing test. See our Contribution Guide for how to make a pull request, or read our New Contributor's Guide if this is your first time contributing.

I am having the same problem

theolundqvist avatar Dec 03 '23 18:12 theolundqvist

I'm updating an older Parse 5.2.x server running against MongoDB 4.4. I noticed this deprecation warning in my logs:

DeprecationWarning: Using aggregation stages without a leading '$' is deprecated and will be removed in a future version. Try $distinct instead.

Could your error be related to this? If you are running MongoDB 5 or higher maybe they no longer support distinct queries without the leading '$'.

I came here to validate that a new parse server implemented this change. BTW, your description says you are running Parse Server 6.3.1 but the extra details say Parse Server 5.6.1. Can you verify that?

Are you in a position to test this against MongoDB 4.4 and/or against the latest Parse Server?

chadpav avatar Jan 16 '24 18:01 chadpav

Following up on my comment above, I can verify that these issues are related. I updated my Parse Server 5.2.x server all the way to MongoDB 7.x and still received the DeprecationWarning. Everything worked though.

Next, I upgraded to the latest 5.6.x Parse Server and everything was working.

Finally, I upgraded to latest Parse Server 6.4.x and now I get the same error as described in the initial bug report. I rolled back to 6.0.x and confirmed the bug was introduced between 5.6 -> 6.0.

I am using a distinct query on an object pointer in Cloud Code if that helps narrow it down. I'll keep digging as well.

chadpav avatar Jan 19 '24 21:01 chadpav

Update

  • This was a planned deprecation which started 2-3 years ago
  • I have a workaround... but I believe this is a bug in the Parse Server distinct method not converting to mongo pipelines in the AggregateRouter correctly. I tried to locate the bug but gave up after a while.
  • You can experiment with using the mongo db aggregate pipeline syntax directly (I got that to work too but it's not as friendly as the Parse api's)
  • The workaround that I'm using for now is supplying a query hint. I'm not exactly sure how it fixes the issue but I tried it on a hunch from something the original poster said. Boom! I'm back in business.
  • This is a still a bug, hopefully @mtrezza will prioritize. 👍

My pseudocode:

...
  var games = Parse.Object.extend('Games')
  var query = new Parse.Query(games)
  query.equalTo('stadiumRef', stadiumRef); // stadiumRef is an object pointer to a record in the stadiums collection

  query.hint('teamRef'); // <== Adding this fixes my distinct query in Parse Server >=6.0

  // Get count of distinct team's that have played a game in the stadium
  query.distinct('teamRef', {useMasterKey: true})
    .then(function(results) {
....

chadpav avatar Jan 20 '24 14:01 chadpav

@chadpav Would you want to open a PR with a failing test, and do you have a suggestion for a bug fix? Once we have a failing test we can also put a bounty to expedite the fixing.

mtrezza avatar Feb 14 '24 20:02 mtrezza

I have a work around and I did try to dig into the server code but it's a little beyond my skillset to do so. Hopefully I've narrowed down the reproduction steps for you guys.

chadpav avatar Feb 15 '24 17:02 chadpav

It happens because ParseQuery.distinct,aggregate in Parse-SDK-JS usually sends hint: undefined to AggregateRouter.handleFind which does not remove the hint from the request body unless it holds a value then it's passed to AggregateRouter.getPipeline(body);. It has been modified to make sure aggregate operators are preceded by a $ which is not the case for hint: undefiend hence the invalid aggregate stage error is thrown.

Original code:

handleFind(req) {
    const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
    const options = {};
    if (body.distinct) {
      options.distinct = String(body.distinct);
    }
    if (body.hint) {
      options.hint = body.hint;
      delete body.hint;
    }
    if (body.explain) {
      options.explain = body.explain;
      delete body.explain;
    }
    if (body.comment) {
      options.comment = body.comment;
      delete body.comment;
    }
    if (body.readPreference) {
      options.readPreference = body.readPreference;
      delete body.readPreference;
    }
    options.pipeline = AggregateRouter.getPipeline(body);
    if (typeof body.where === 'string') {
      body.where = JSON.parse(body.where);
    }
    return rest
      .find(
        req.config,
        req.auth,
        this.className(req),
        body.where,
        options,
        req.info.clientSDK,
        req.info.context
      )
      .then(response => {
        for (const result of response.results) {
          if (typeof result === 'object') {
            UsersRouter.removeHiddenProperties(result);
          }
        }
        return { response };
      });
  }

A fix:

handleFind(req) {
    const body = Object.assign(req.body, ClassesRouter.JSONFromQuery(req.query));
    const options = {};
    if (body.distinct) {
      options.distinct = String(body.distinct);
    }
    ['hint', 'explain', 'comment', 'readPreference'].forEach(key => {
      if (body[key]) options[key] = body[key];
      delete body[key];
    });
    options.pipeline = AggregateRouter.getPipeline(body);
    if (typeof body.where === 'string') {
      body.where = JSON.parse(body.where);
    }
    return rest
      .find(
        req.config,
        req.auth,
        this.className(req),
        body.where,
        options,
        req.info.clientSDK,
        req.info.context
      )
      .then(response => {
        for (const result of response.results) {
          if (typeof result === 'object') {
            UsersRouter.removeHiddenProperties(result);
          }
        }
        return { response };
      });
  }

A test:

const Config = require('../lib/Config');
const ClientSDK = require('../lib/ClientSDK');

  it('"handleFind" properly handles "req.body.hint" and invalid aggregate stage error should not be thrown', async () => {
   const config = Config.get('test');
   const clientSDK = ClientSDK.fromString('i0.0.0');
   const req = {
     config,
     params: { className: '_User' },
     body: {
       distinct: 'first_name',
       where: {
         objectId: 'someId',
       },
       hint: undefined,
     },
     info: {
       clientSDK,
       context: '1',
     },
     auth: { readOnly: false },
   };

   const expected = { response: { results: [] } };

   const aggregateRouter = new AggregateRouter();
   const result = await aggregateRouter.handleFind(req);

   expect(result).toEqual(expected);
 });

Chilldev avatar Aug 11 '24 16:08 Chilldev

@Chilldev would you want to submit a PR with a failing test?

mtrezza avatar Aug 11 '24 22:08 mtrezza

@mtrezza Sure. Let me check the contribution guide.

Chilldev avatar Aug 12 '24 10:08 Chilldev