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: nested filter input-types #143

Merged
merged 13 commits into from
May 30, 2023
Merged

add: nested filter input-types #143

merged 13 commits into from
May 30, 2023

Conversation

hedwiggggg
Copy link
Contributor

@hedwiggggg hedwiggggg commented May 25, 2023

Hello, in this pull request I added the possibility to create filter types at any depth. So far I have only run the unit-tests.

So still missing:

  • run e2e-tests
  • add unit-tests
  • add e2e-tests
  • add documentation

Internal tests already ran successfully, but I noticed a bug with the query builder. In the generated joins the alias is set to the filter-property. So a hypothetical filter like this:

const articlesFilter: ArticleFilter = {
  group: {
    articles: {
      group: {
        name: {
          eq: 'tools',
        },
      },
    },
  },
};

Ends with the error: table name "group" specified more than once. Here is the log:

query failed: SELECT "Article"."id" AS "Article_id", "Article"."created_at" AS "Article_created_at", "Article"."updated_at" AS "Article_updated_at", "Article"."deleted_at" AS "Article_deleted_at", "Article"."status" AS "Article_status", "Article"."order_no" AS "Article_order_no", "Article"."name" AS "Article_name", "Article"."description" AS "Article_description", "Article"."condition" AS "Article_condition", "Article"."weight" AS "Article_weight", "Article"."unit" AS "Article_unit", "Article"."is_sellable" AS "Article_is_sellable", "Article"."is_storable" AS "Article_is_storable", "Article"."is_discountable" AS "Article_is_discountable", "Article"."group_id" AS "Article_group_id", "Article"."country_of_origin" AS "Article_country_of_origin", "Article"."customs_tariff_number" AS "Article_customs_tariff_number", "Article"."supplier_id" AS "Article_supplier_id", "Article"."supplier_order_number" AS "Article_supplier_order_number", "Article"."designation" AS "Article_designation" FROM "article" "Article" LEFT JOIN "article_group" "group" ON "group"."id"="Article"."group_id" AND ("group"."deleted_at" IS NULL)  LEFT JOIN "article" "articles" ON "articles"."group_id"="group"."id" AND ("articles"."deleted_at" IS NULL)  LEFT JOIN "article_group" "group" ON "group"."id"="articles"."group_id" AND ("group"."deleted_at" IS NULL) WHERE ( (((((("group"."name" = $1))))) AND 1=1 AND 1=1) ) AND ( "Article"."deleted_at" IS NULL ) ORDER BY "Article"."id" ASC LIMIT 26 -- PARAMETERS: ["Tools"]

But this is a problem for an own issue / pull request.

@hedwiggggg
Copy link
Contributor Author

To configure the filter-depth for an entity, you pass the filterDepth property to the QueryOptions-decorator.

@ObjectType('TodoItem')
// `filterDepth` is a number, 1 is the default
@QueryOptions({ filterDepth: 3 })
// `filterDepth: Number.POSITIVE_INFINITY` will result in a infinitely deep filter
@QueryOptions({ filterDepth: Number.POSITIVE_INFINITY })
export class TodoItemDTO {
  @IDField(() => ID)
  id!: number;

  @FilterableField()
  title!: string;

  @FilterableField({ nullable: true })
  description?: string;

  @FilterableField()
  completed!: boolean;

  @FilterableField(() => GraphQLISODateTime)
  created!: Date;

  @FilterableField(() => GraphQLISODateTime)
  updated!: Date;
}

@codecov-commenter
Copy link

codecov-commenter commented May 25, 2023

Codecov Report

Merging #143 (2444712) into master (11a647d) will increase coverage by 0.15%.
The diff coverage is 90.00%.

@@            Coverage Diff             @@
##           master     #143      +/-   ##
==========================================
+ Coverage   78.16%   78.31%   +0.15%     
==========================================
  Files         619      626       +7     
  Lines        8682     8771      +89     
  Branches      831      839       +8     
==========================================
+ Hits         6786     6869      +83     
+ Misses       1169     1167       -2     
- Partials      727      735       +8     
Impacted Files Coverage Δ
...query-graphql/src/decorators/relation.decorator.ts 91.42% <ø> (ø)
...kages/query-graphql/src/types/query/filter.type.ts 77.92% <69.44%> (-7.57%) ⬇️
examples/filters-deep/e2e/fixtures.ts 100.00% <100.00%> (ø)
examples/filters-deep/src/app.module.ts 100.00% <100.00%> (ø)
...mples/filters-deep/src/category/category.entity.ts 100.00% <100.00%> (ø)
examples/filters-deep/src/post/post.entity.ts 100.00% <100.00%> (ø)
examples/filters-deep/src/post/post.module.ts 100.00% <100.00%> (ø)
examples/filters-deep/src/user/user.entity.ts 100.00% <100.00%> (ø)
examples/filters-deep/src/user/user.module.ts 100.00% <100.00%> (ø)

... and 2 files with indirect coverage changes

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@TriPSs
Copy link
Owner

TriPSs commented May 25, 2023

Thanks for the PR! Could you show me a query filter example that before did work and does not now? I'm not quite sure I completely understand what this does.

@hedwiggggg
Copy link
Contributor Author

hedwiggggg commented May 25, 2023

I'm not quite sure I completely understand what this does.

This PR implements the feature to generate infinitely deep input-filter-types (doug-martin/nestjs-query#805 and #121).

Could you show me a query filter example that before did work and does not now?

No because the problem with the query-builder only occurs if the nesting is deeper than one level, which is impossible until now. But this PR changes that, that's why I stumbled upon this bug. But this PR is only for the infinitely deep filters.

@hedwiggggg
Copy link
Contributor Author

This is how the generated schema will look like if the filterDepth is Number.POSITIVE_INFINITY.

image

@hedwiggggg
Copy link
Contributor Author

This is how the generated schema will look like if the filterDepth is 2.

image

return filterableRelations
}

function getOrCreateFilterType<T>(TClass: Class<T>, name: string, depth = 0): FilterConstructor<T> {
Copy link
Owner

Choose a reason for hiding this comment

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

Since we call getOrCreateFilterType already with dept = 1 do we need the dept = 0 here?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, because we call it with depth = 1 only on the (Delete / Update / Subscription / Aggregate)FilterType. The base FilterType is called without an argument.

What we can do is to remove the default value and move the depth = 0 into the base FilterType.

Copy link
Owner

Choose a reason for hiding this comment

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

Okay check, maybe indeed call it there with dept = 0 so we don't need the default.

@TriPSs
Copy link
Owner

TriPSs commented May 25, 2023

So if I understand correctly, default it create a AddressFilterBankFilter so you can filter the address on the bank relation, if Number.POSITIVE_INFINITY is used it will use the "normal" BankFilter with all the filters that Bank has. And if you provide filterDepth = 2 it will create a other one so you can filter address -> bank(1) -> other bank relation(2) correct?

@hedwiggggg
Copy link
Contributor Author

So if I understand correctly, default it create a AddressFilterBankFilter so you can filter the address on the bank relation, if Number.POSITIVE_INFINITY is used it will use the "normal" BankFilter with all the filters that Bank has. And if you provide filterDepth = 2 it will create a other one so you can filter address -> bank(1) -> other bank relation(2) correct?

Exactly.

@TriPSs
Copy link
Owner

TriPSs commented May 25, 2023

And a question: If we where to release this like this it will not change any schema except if you provide the filterDepth?

@hedwiggggg
Copy link
Contributor Author

And a question: If we where to release this like this it will not change any schema except if you provide the filterDepth?

Nope, otherwise the test wouldn't have passed.

@TriPSs
Copy link
Owner

TriPSs commented May 25, 2023

And a question: If we where to release this like this it will not change any schema except if you provide the filterDepth?

Nope, otherwise the test wouldn't have passed.

True, thought of that after I send it 😅

Everything looks ok, it's a nice addition this so thanks for that! If the other tasks are also completed we can merge and release!

@hedwiggggg
Copy link
Contributor Author

Should be ready now @TriPSs 🚀

@hedwiggggg hedwiggggg changed the title add: nested filter input-types (work in progress) add: nested filter input-types May 26, 2023
@TriPSs
Copy link
Owner

TriPSs commented May 26, 2023

Thanks for the PR! Changes look good, I do want to check this out locally before merging if you don't mind.

@TriPSs
Copy link
Owner

TriPSs commented May 29, 2023

@hedwiggggg after checking this locally I did notice one thing:

@FilterableRelation('relation', () => Relation, {
  // This one does not work
  filterDepth: 2,
})

Can we either remove it or add support for it? (I do think it's nice we can specific it for a specific relation instead of all relations)

@hedwiggggg
Copy link
Contributor Author

Unfortunately, this can only be realized with breaking changes, since the target depth would have to be embedded in the filter name. Otherwise, it cannot be guaranteed which filter depth will be generated because the root filter names are the same for different depths. And configuring each relation individually, would possibly lead to different depths for the same filter type.

I have specially designed this feature so that there are no breaking changes.

@hedwiggggg
Copy link
Contributor Author

tldr; we would need to generate n filter-types for the same entity where n is equal to the count of the unique {Entity, filterDepth} occurrences.

In the following example we would need 3 different filter types, but in the current implementation all filter types would have the same name, which is invalid in GraphQL.

// Example-A.entity.ts
@FilterableRelation('relation', () => Relation, { filterDepth: 2 })
// Example-B.entity.ts
@FilterableRelation('relation', () => Relation)
// Example-C.entity.ts
@FilterableRelation('relation', () => Relation, { filterDepth: 3 })

@TriPSs
Copy link
Owner

TriPSs commented May 29, 2023

Understandable, could we then remove it from there so the option is not shown?

@hedwiggggg
Copy link
Contributor Author

hedwiggggg commented May 30, 2023

I thought about it again last night. There is a way to configure this per relation, without breaking-changes. By default the filter depth is 1. With this filter depth the generated name can also be ${ObjectTypeName}Filter by default. If the depth differs from the default filter depth, then we embed the target depth in the name.

// Example-A.entity.ts
@ObjectType('EntityA')
@FilterableRelation('relation', () => relation)
class EntityA { /* ... */ }

// leads to
// input EntityAFilter {
// ...
// }
// Example-A.entity.ts
@ObjectType('EntityA')
@FilterableRelation('relation', () => relation, { filterDepth: 2 )
class EntityA { /* ... */ }

// leads to
// input EntityADepth2Filter {
// ...
// }

But nevertheless I would take it out for now and put the feature-request into a separate issue, since I don't have the resources for it right now.

Edit: above assumption is complete bs, because the parentEntity-name is prefixed for n-depth filters. A better example would be:

// Example-A.entity.ts
@ObjectType('EntityA')
@FilterableRelation('relation', () => relation)
class EntityA { /* ... */ }

// leads to
// input EntityAFilter {
//    relation: EntityAFilterRelationFilter
// }
// Example-B.entity.ts
@ObjectType('EntityB')
@FilterableRelation('relation', () => relation, { filterDepth: 2 )
class EntityB { /* ... */ }

// leads to
// input EntityBFilter {
//   relation: EntityBFilterRelationFilter
// }

This means that we don't get name conflicts when we specify a fliterDepth per relationship. I will have a look at this in the future.

@TriPSs
Copy link
Owner

TriPSs commented May 30, 2023

@hedwiggggg massive thanks for this addition!

@TriPSs TriPSs merged commit b6ff9eb into TriPSs:master May 30, 2023
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants