Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Deep Filtering Feature #2452

Conversation

kamalbennani
Copy link
Contributor

@kamalbennani kamalbennani commented Dec 8, 2018

⚠️ This is a "Work In Progress" PR ⚠️
⚠️ The API has been changed, the description is outdated ⚠️

My PR is a:

  • 💥 Breaking change
  • 🐛 Bug fix #issueNumber
  • 💅 Enhancement
  • 🚀 New feature

Main update on the:

  • Admin
  • Documentation
  • Framework
  • Plugin

Proposal

I think that generated service can be simplified a lot.
The problem with the actual implementation is that:

  • The logic is duplicated in a lot of different places (API Services + Content Manager + User Permissions etc ..)
    Which make it hard, very hard to maintain for both the developers and Strapi collaborators (Due to updates/Migrations)

We could use a simple technique where we implement a sort of a Query Service that handles fetching for us.

Here is an example of the code:

Mongoose Proposal

const { has, isEmpty, get } = require('lodash');
const { buildQueryJoins, buildQueryFilter } = require('./query-utils');

class Query {
  constructor(model) {
    this.model = model;
  }

  buildQuery(filter) {
    const filterStage = buildQueryFilter(this.model, filter.where);
    const query = this.model.aggregate(filterStage);

    return query;
  }

  find(filter) {
    this.query = this.buildQuery(filter);
    if (has(filter, 'start')) this.query.skip(filter.start);
    if (has(filter, 'limit')) this.query.limit(filter.limit);
    if (!isEmpty(filter.sort)) this.query.sort(filter.sort);

    return this;
  }

  count(filter) {
    this.query = this.buildQuery(filter);
    this.query = this.query
      .count("count")
      .then(result => get(result, `0.count`, 0));
    return this;
  }

  populate(populate) {
    // @TODO add a warning message if the query is undefined
    const queryJoins = buildQueryJoins(this.model, { whitelistedPopulate: populate });
    this.query = this.query.append(queryJoins);
    return this;
  }

  execute() {
    return this.query;
  }
}

Post Service

module.exports = {

  /**
   * Promise to fetch all courses.
   *
   * @return {Promise}
   */

  fetchAll: (params, populate) => {
    // Conver JSON filter to MongoDB filter
    const filter = new Builder(Post, params).convert();

    return new Query(Post) // Generic Query which will be implemented by Mongoose and Bookshelf
      .find(filter) // Built-in Deep Relation Filter
      .populate(populate) // List of relations to populate
      .execute();
  },

  veryComplexeFilter: (params, populate) => {
    // Conver JSON filter to MongoDB filter
    const filter = new Builder(Post)
         .eq('active', true) // All active posts
         .contains("author.company", "strapi") // The author's company contains strapi
         .between("likes", [10, 20]) // Where the likes are between 10 and 20
         .limit(10)
         .sort(['likes', 'desc'])
         .convert(); // It converts the JSON filter to MongoDB filter by populating only the relation needed to fulfill this query

    return new Query(Post) // Generic Query which will be implemented by Mongoose and Bookshelf
      .find(filter) // Built-in Deep Relation Filter
      .populate(populate) // List of relations to populate
      .execute();
  }
}

@lauriejim @Aurelsicoko What do you think guys?

WIP (I'll add a complete description later on with concrete examples)

TODOs

Hook REST GraphQL
Bookshelf
  • One-to-Many Deep Filter
  • Many-to-One Deep Filter
  • Many-to-Many Deep Filter
  • Morph Deep Filter
  • One-to-Many Deep Filter
  • Many-to-One Deep Filter
  • Many-to-Many Deep Filter
  • Morph Deep Filter
Mongoose
  • One-to-Many Deep Filter
  • Many-to-One Deep Filter
  • Many-to-Many Deep Filter
  • Morph Deep Filter
  • One-to-Many Deep Filter
  • Many-to-One Deep Filter
  • Many-to-Many Deep Filter
  • Morph Deep Filter

Scenarios

image
image

image
image

image
image

image
image

image

@kamalbennani kamalbennani force-pushed the feature/deep-filtering-mongoose-bookshelf branch from a716001 to c5ccdee Compare December 8, 2018 22:54
@@ -29,22 +29,19 @@ module.exports = {
.map(ast => ast.alias);

return <%= globalID %>.query(function(qb) {
_.forEach(filters.where, (where, key) => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Aurelsicoko @lauriejim We need to find a way to let the users modify the logic if they want, but the question is: "Do they really need to modify the Core Query System?"

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I mean we can, for instance, put this utility functions inside of a folder that they can update but I don't see why should we duplicate the code in the different places (API Services / Content Manager and so on)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right! We could centralise everything in a single place but the users need to keep control if they want to edit something. It should not be hidden somewhere to avoid updates...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Aurelsicoko I think doing something like what @soupette did with the docs plugin and centralize everything but allow for model level overrides

symbol: value.symbol || '='
};
}
qb.where(fieldKey, value.symbol, value.value);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Should we ignore the field if it doesn't exist in the model's attributes (because the actual behaviour now is that you get a 500 Internal Error message)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kamalbennani @lauriejim realistically I think it should give a 4xx error. The error is with the user and their query. Ignoring it could make it look like there is nothing wrong. It should still toss an error, but in this case it isn't the server's fault 😜

);

return controller(ctx, next);
return controller(Object.assign({}, ctx, queryOpts, { send: ctx.send }), next, { populate: [] });
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't populate any relation in the GraphQL context (Which will make things go faster :yay:)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've already did an update like that in this PR #2380, we should try to synchronise our work.

default:
// Where.
queryOpts.query = strapi.utils.models.convertParams(name, {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to convertParams here because it's already done in the content manager

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right but I'm not 100% sure... it should work without converting the parameters but we're not doing randomly, I think there is a reason but I can't remember why.

@lauriejim lauriejim changed the title Add Deep Filtering Feature [Rework] [WIP] Add Deep Filtering Feature [Rework] Dec 9, 2018
@kamalbennani kamalbennani force-pushed the feature/deep-filtering-mongoose-bookshelf branch from c5ccdee to edbd3ca Compare December 9, 2018 12:18
}
}).fetchAll({
withRelated: populate || _.keys(_.groupBy(_.reject(this.associations, { autoPopulate: false }), 'alias'))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does anyone remember why here we reject the non-autoPopulated association but in the mongoose version we don't (https://github.com/strapi/strapi/pull/2452/files#diff-5a892334926e352d367245b7876dd6bdL10)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're rejecting the relations which don't have the autoPopulate set to true (see https://github.com/strapi/strapi/blob/master/packages/strapi-generate-api/templates/mongoose/service.template#L24).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant that we are not doing it in packages/strapi-plugin-upload/config/queries/mongoose.js

}
}
},
"collectionName": "users-permissions_user"
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be removed!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

queryOpts.query.id.symbol = 'IN';
// Construct the "where" query to only retrieve entries which are
// related to this entry.
_.set(queryOpts, ['query', association.via], obj[ref.primaryKey]);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that there is no need to separate the manyToMany case from the rest of the logic, Could please confirm this @lauriejim @Aurelsicoko

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not only the manyToMany case but it's also the oneToMany or manyToOne process.

const fieldKey = (association && association.nature === 'manyToMany')
? `${association.tableCollectionName}.${attributes[key].attribute}_${attributes[key].column}`
: `${strapiModel.collectionName}.${key}`;
if (_.isArray(value.value) && value.symbol !== 'IN') {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is probably no need for this check! because we've removed the check in the GraphQL manyToMany relation

@kamalbennani kamalbennani force-pushed the feature/deep-filtering-mongoose-bookshelf branch from edbd3ca to 07d621b Compare December 9, 2018 15:51
@kamalbennani
Copy link
Contributor Author

What's the best way to implement the unit tests, Yesterday I tried to run the existing test but it took at least 30/40 mins without success

@lauriejim
Copy link
Contributor

Jest test are broken on master we fixed it on cypress branch. A pull request is open.
Will be merge soon as possible.

@abdonrd
Copy link
Contributor

abdonrd commented Dec 12, 2018

Cypress PR (#2348) merged! 🎉

@jacargentina
Copy link
Contributor

@lauriejim how can I help to get this merged, I NEED it 🙏

@lukefanning
Copy link

lukefanning commented Dec 19, 2018

Sorry guys quick question I'm new to strapi, does this PR only refer to being able to do a deep filter using GET params or does this also apply to GraphQL queries? I seem to be unable to deep filter with GraphQL

@jacargentina
Copy link
Contributor

@ssg-luke both of them, GET and GraphQL (right @kamalbennani ?)

@kamalbennani
Copy link
Contributor Author

@ssg-luke both of them, GET and GraphQL (right @kamalbennani ?)

Yes :D

@kamalbennani kamalbennani force-pushed the feature/deep-filtering-mongoose-bookshelf branch 2 times, most recently from b711340 to 03614fb Compare December 24, 2018 12:08
@kamalbennani kamalbennani force-pushed the feature/deep-filtering-mongoose-bookshelf branch from 8a7834e to 5434289 Compare January 2, 2019 23:28
return <%= globalID %>
.count()
.where(filters.where);
// Convert `params` object to filter compatible with Mongo.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove indentation

@kamalbennani kamalbennani force-pushed the feature/deep-filtering-mongoose-bookshelf branch 3 times, most recently from e2cc569 to a971616 Compare January 3, 2019 21:09
@jacargentina
Copy link
Contributor

@kamalbennani @lauriejim any way i can help with this? It is becoming very hard to do simple queries without it. 🙏

@kamalbennani
Copy link
Contributor Author

kamalbennani commented Jan 4, 2019

@jacargentina We're doing our best to be able to merge this as soon as possible, we need to make sure that it breaks nothing and make it easy to integrate with existing Strapi projects effortlessly

@kamalbennani
Copy link
Contributor Author

@kret13 it seems to be working just fine on a newly generated project :/
image

Could you provide us some steps to reproduce the same behaviour

@kret13
Copy link

kret13 commented Feb 21, 2019

I haven't mentioned that it doesn't work when you add limit. Try with limit field to make count be number lesser than totalCount.
So steps are:

  1. make a query on someConnection with filtering on any relation field that is not it's id
  2. add limit to filter and try to make results of count and totalCount be different. For example, if there is totalCount of batches with name:"something" is equal 2, limit that to 1 so that it should display count: 1 and totalCount: 2

@kamalbennani
Copy link
Contributor Author

@kret13 I'll try to reproduce it again, thanks mate 💪
Could you push the tests more and tell us if everything works as excepted for you (except the aggregation of course :p)

@kamalbennani
Copy link
Contributor Author

@kret13 it's fixed now, could you try again

@kret13
Copy link

kret13 commented Feb 22, 2019

I tried this again and it seems that it's good now.

@lauriejim lauriejim added this to the 3.0.0-alpha.25 milestone Feb 25, 2019
@lauriejim lauriejim added the flag: 💥 Breaking change This PR contains breaking changes and should not be merged label Feb 25, 2019
@lauriejim lauriejim changed the title [Ready For Review / QA] Add Deep Filtering Feature [commit-by-commit] Add Deep Filtering Feature Feb 25, 2019
@lauriejim
Copy link
Contributor

I got a log when I filter with relation attribute

[2019-02-25T15:15:43.575Z] warn Your filter: {
  "author.name": "admin",
  "limit": "1"
} contains a field "author.name" that doesn't appear neither on your model definition nor in the basic filter operators,
            This field will be ignored for now.
[2019-02-25T15:15:43.582Z] debug GET /articles?author.name=admin&limit=1

Look like an error but it's not.


I have to check with actual master branch but when I install plugins, plugins permission disappear.
It's really strange.

@lauriejim
Copy link
Contributor

The issue I report not happen on master investigation have to be done to fix that.

@lauriejim lauriejim removed this from the 3.0.0-alpha.25 milestone Mar 6, 2019
@jacargentina
Copy link
Contributor

@lauriejim 3 months "birthday" soon. Any chance to get this merged on master ?

@derrickmehaffy
Copy link
Member

@jacargentina it is being tested, last I saw last week there was issues that needed to be worked out that are being looked into.

Maybe Jim can give a timeline but due to the size of this PR it cannot be merged without very detailed testing.

@derrickmehaffy derrickmehaffy mentioned this pull request Mar 7, 2019
13 tasks
@alexandrebodin alexandrebodin changed the base branch from master to feature/deep-filtering-mongoose-bookshelf March 11, 2019 10:09
@alexandrebodin alexandrebodin added this to the 3.0.0-alpha.25 milestone Mar 11, 2019
@alexandrebodin alexandrebodin merged commit fce66b5 into strapi:feature/deep-filtering-mongoose-bookshelf Mar 11, 2019
@alexandrebodin alexandrebodin mentioned this pull request Mar 11, 2019
24 tasks
@alexandrebodin
Copy link
Member

alexandrebodin commented Mar 11, 2019

Hi guys,

Thank you for your work @kamalbennani We are taking over the PR to help release it asap for the community ;)

We are opening a new PR with the same commits merged into a freature branch on the main repo.
Here is the new PR #2961

Go there if you have new comments or questions 👍

@strapi strapi locked and limited conversation to collaborators Mar 11, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
flag: 💥 Breaking change This PR contains breaking changes and should not be merged source: core:content-manager Source is core/content-manager package
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet