Skip to content

Commit

Permalink
API Generator: Add search argument to OneToManyFilter and `ManyTo…
Browse files Browse the repository at this point in the history
…ManyFilter` (#2238)

This PR adds search filter to OneToMany and ManyToMany relations, that
can be used where a select is impossible to use because of too many
related entities. Doing a poor-mans fulltext search doesn't have this
problem (also has poor performance on the server side though)

The old method (how search was implemented for list queries) didn't
apply here well, so I decided do go a different approach: Instead of
generating code (the service) that lists all fields to query, we find
those fields at runtime now. We are using (1) MikroORM metadata and (2)
our CrudField resolver, the search boolean. Just as before.

And with this new approach it is possible to do that also for related
entities that makes implementing the search possible.

---------

Co-authored-by: Johannes Obermair <[email protected]>
  • Loading branch information
nsams and johnnyomair authored Jul 25, 2024
1 parent d1b7f5f commit 4485d15
Show file tree
Hide file tree
Showing 29 changed files with 192 additions and 376 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-islands-perform.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@comet/cms-api": major
---

filtersToMikroOrmQuery: Move second argument (`applyFilter` callback) into an options object
7 changes: 7 additions & 0 deletions .changeset/sixty-cougars-begin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@comet/cms-api": minor
---

API Generator: Add `search` argument to `OneToManyFilter` and `ManyToManyFilter`

Performs a search like the `search` argument in the list query.
9 changes: 9 additions & 0 deletions .changeset/ten-buses-lie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"@comet/cms-api": major
---

API Generator: Remove generated service

The `Service#getFindCondition` method is replaced with the new `gqlArgsToMikroOrmQuery` function, which detects an entity's searchable fields from its metadata.
Consequently, the generated service isn't needed anymore and will therefore no longer be generated.
Remove the service from the module after re-running the API Generator.
8 changes: 4 additions & 4 deletions demo/admin/src/products/ProductsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
import { Add as AddIcon, Edit, StateFilled } from "@comet/admin-icons";
import { DamImageBlock } from "@comet/cms-admin";
import { Button, IconButton, useTheme } from "@mui/material";
import { DataGridPro, GridFilterInputSingleSelect, GridToolbarQuickFilter } from "@mui/x-data-grid-pro";
import { DataGridPro, GridFilterInputSingleSelect, GridFilterInputValue, GridToolbarQuickFilter } from "@mui/x-data-grid-pro";
import gql from "graphql-tag";
import * as React from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
Expand Down Expand Up @@ -150,14 +150,14 @@ export function ProductsGrid() {
renderCell: (params) => <>{params.row.tags.map((tag) => tag.title).join(", ")}</>,
filterOperators: [
{
value: "contains",
label: "Search",
value: "search",
getApplyFilterFn: (filterItem) => {
throw new Error("not implemented, we filter server side");
},
InputComponent: GridFilterInputSingleSelect,
InputComponent: GridFilterInputValue,
},
],
valueOptions: relationsData?.productTags.nodes.map((i) => ({ value: i.id, label: i.title })),
},
{
field: "inStock",
Expand Down
22 changes: 12 additions & 10 deletions demo/api/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -454,16 +454,6 @@ type ManufacturerCountry {
used: Float!
}

type PaginatedManufacturers {
nodes: [Manufacturer!]!
totalCount: Int!
}

type PaginatedManufacturerCountries {
nodes: [ManufacturerCountry!]!
totalCount: Int!
}

type ProductCategory {
id: ID!
title: String!
Expand Down Expand Up @@ -563,6 +553,16 @@ enum ProductType {
Tie
}

type PaginatedManufacturers {
nodes: [Manufacturer!]!
totalCount: Int!
}

type PaginatedManufacturerCountries {
nodes: [ManufacturerCountry!]!
totalCount: Int!
}

type PaginatedProducts {
nodes: [Product!]!
totalCount: Int!
Expand Down Expand Up @@ -915,6 +915,7 @@ input NewsCategoryEnumFilter {

input OneToManyFilter {
contains: ID
search: String
}

input NewsSort {
Expand Down Expand Up @@ -988,6 +989,7 @@ input ManyToOneFilter {

input ManyToManyFilter {
contains: ID
search: String
}

input ProductSort {
Expand Down
5 changes: 2 additions & 3 deletions demo/api/src/news/generated/news.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
BlocksTransformerService,
DamImageBlock,
extractGraphqlFields,
gqlArgsToMikroOrmQuery,
RequiredPermission,
RootBlockDataScalar,
} from "@comet/cms-api";
Expand All @@ -20,14 +21,12 @@ import { NewsComment } from "../entities/news-comment.entity";
import { NewsInput, NewsUpdateInput } from "./dto/news.input";
import { NewsListArgs } from "./dto/news-list.args";
import { PaginatedNews } from "./dto/paginated-news";
import { NewsService } from "./news.service";

@Resolver(() => News)
@RequiredPermission(["news"])
export class NewsResolver {
constructor(
private readonly entityManager: EntityManager,
private readonly newsService: NewsService,
@InjectRepository(News) private readonly repository: EntityRepository<News>,
private readonly blocksTransformer: BlocksTransformerService,
) {}
Expand All @@ -51,7 +50,7 @@ export class NewsResolver {
@Args() { scope, status, search, filter, sort, offset, limit }: NewsListArgs,
@Info() info: GraphQLResolveInfo,
): Promise<PaginatedNews> {
const where = this.newsService.getFindCondition({ search, filter });
const where = gqlArgsToMikroOrmQuery({ search, filter }, this.repository);
where.status = { $in: status };
where.scope = scope;

Expand Down
25 changes: 0 additions & 25 deletions demo/api/src/news/generated/news.service.ts

This file was deleted.

2 changes: 0 additions & 2 deletions demo/api/src/news/news.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import { News, NewsContentScope } from "@src/news/entities/news.entity";

import { NewsComment } from "./entities/news-comment.entity";
import { NewsResolver } from "./generated/news.resolver";
import { NewsService } from "./generated/news.service";
import { NewsCommentResolver } from "./news-comment.resolver";
import { NewsFieldResolver } from "./news-field.resolver";

Expand All @@ -14,7 +13,6 @@ import { NewsFieldResolver } from "./news-field.resolver";
providers: [
NewsResolver,
NewsCommentResolver,
NewsService,
NewsFieldResolver,
DependenciesResolverFactory.create(News),
DependentsResolverFactory.create(News),
Expand Down
25 changes: 0 additions & 25 deletions demo/api/src/products/generated/manufacturer-countries.service.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file has been generated by comet api-generator.
// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
import { AffectedEntity, RequiredPermission } from "@comet/cms-api";
import { AffectedEntity, gqlArgsToMikroOrmQuery, RequiredPermission } from "@comet/cms-api";
import { FindOptions } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
import { EntityManager, EntityRepository } from "@mikro-orm/postgresql";
Expand All @@ -9,14 +9,12 @@ import { Args, ID, Query, Resolver } from "@nestjs/graphql";
import { ManufacturerCountry } from "../entities/manufacturer-country.entity";
import { ManufacturerCountriesArgs } from "./dto/manufacturer-countries.args";
import { PaginatedManufacturerCountries } from "./dto/paginated-manufacturer-countries";
import { ManufacturerCountriesService } from "./manufacturer-countries.service";

@Resolver(() => ManufacturerCountry)
@RequiredPermission(["manufacturerCountries"], { skipScopeCheck: true })
export class ManufacturerCountryResolver {
constructor(
private readonly entityManager: EntityManager,
private readonly manufacturerCountriesService: ManufacturerCountriesService,
@InjectRepository(ManufacturerCountry) private readonly repository: EntityRepository<ManufacturerCountry>,
) {}

Expand All @@ -29,7 +27,7 @@ export class ManufacturerCountryResolver {

@Query(() => PaginatedManufacturerCountries)
async manufacturerCountries(@Args() { search, filter, offset, limit }: ManufacturerCountriesArgs): Promise<PaginatedManufacturerCountries> {
const where = this.manufacturerCountriesService.getFindCondition({ search, filter });
const where = gqlArgsToMikroOrmQuery({ search, filter }, this.repository);

const options: FindOptions<ManufacturerCountry> = { offset, limit };

Expand Down
6 changes: 2 additions & 4 deletions demo/api/src/products/generated/manufacturer.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file has been generated by comet api-generator.
// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
import { AffectedEntity, RequiredPermission } from "@comet/cms-api";
import { AffectedEntity, gqlArgsToMikroOrmQuery, RequiredPermission } from "@comet/cms-api";
import { FindOptions } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
import { EntityManager, EntityRepository } from "@mikro-orm/postgresql";
Expand All @@ -10,14 +10,12 @@ import { Manufacturer } from "../entities/manufacturer.entity";
import { ManufacturerInput, ManufacturerUpdateInput } from "./dto/manufacturer.input";
import { ManufacturersArgs } from "./dto/manufacturers.args";
import { PaginatedManufacturers } from "./dto/paginated-manufacturers";
import { ManufacturersService } from "./manufacturers.service";

@Resolver(() => Manufacturer)
@RequiredPermission(["manufacturers"], { skipScopeCheck: true })
export class ManufacturerResolver {
constructor(
private readonly entityManager: EntityManager,
private readonly manufacturersService: ManufacturersService,
@InjectRepository(Manufacturer) private readonly repository: EntityRepository<Manufacturer>,
) {}

Expand All @@ -30,7 +28,7 @@ export class ManufacturerResolver {

@Query(() => PaginatedManufacturers)
async manufacturers(@Args() { search, filter, sort, offset, limit }: ManufacturersArgs): Promise<PaginatedManufacturers> {
const where = this.manufacturersService.getFindCondition({ search, filter });
const where = gqlArgsToMikroOrmQuery({ search, filter }, this.repository);

const options: FindOptions<Manufacturer> = { offset, limit };

Expand Down
33 changes: 0 additions & 33 deletions demo/api/src/products/generated/manufacturers.service.ts

This file was deleted.

25 changes: 0 additions & 25 deletions demo/api/src/products/generated/product-categories.service.ts

This file was deleted.

6 changes: 2 additions & 4 deletions demo/api/src/products/generated/product-category.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file has been generated by comet api-generator.
// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
import { AffectedEntity, extractGraphqlFields, RequiredPermission } from "@comet/cms-api";
import { AffectedEntity, extractGraphqlFields, gqlArgsToMikroOrmQuery, RequiredPermission } from "@comet/cms-api";
import { FindOptions } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
import { EntityManager, EntityRepository } from "@mikro-orm/postgresql";
Expand All @@ -12,14 +12,12 @@ import { ProductCategory } from "../entities/product-category.entity";
import { PaginatedProductCategories } from "./dto/paginated-product-categories";
import { ProductCategoriesArgs } from "./dto/product-categories.args";
import { ProductCategoryInput, ProductCategoryUpdateInput } from "./dto/product-category.input";
import { ProductCategoriesService } from "./product-categories.service";

@Resolver(() => ProductCategory)
@RequiredPermission(["products"], { skipScopeCheck: true })
export class ProductCategoryResolver {
constructor(
private readonly entityManager: EntityManager,
private readonly productCategoriesService: ProductCategoriesService,
@InjectRepository(ProductCategory) private readonly repository: EntityRepository<ProductCategory>,
) {}

Expand All @@ -42,7 +40,7 @@ export class ProductCategoryResolver {
@Args() { search, filter, sort, offset, limit }: ProductCategoriesArgs,
@Info() info: GraphQLResolveInfo,
): Promise<PaginatedProductCategories> {
const where = this.productCategoriesService.getFindCondition({ search, filter });
const where = gqlArgsToMikroOrmQuery({ search, filter }, this.repository);

const fields = extractGraphqlFields(info, { root: "nodes" });
const populate: string[] = [];
Expand Down
6 changes: 2 additions & 4 deletions demo/api/src/products/generated/product-tag.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// This file has been generated by comet api-generator.
// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.
import { AffectedEntity, extractGraphqlFields, RequiredPermission } from "@comet/cms-api";
import { AffectedEntity, extractGraphqlFields, gqlArgsToMikroOrmQuery, RequiredPermission } from "@comet/cms-api";
import { FindOptions, Reference } from "@mikro-orm/core";
import { InjectRepository } from "@mikro-orm/nestjs";
import { EntityManager, EntityRepository } from "@mikro-orm/postgresql";
Expand All @@ -13,14 +13,12 @@ import { ProductToTag } from "../entities/product-to-tag.entity";
import { PaginatedProductTags } from "./dto/paginated-product-tags";
import { ProductTagInput, ProductTagUpdateInput } from "./dto/product-tag.input";
import { ProductTagsArgs } from "./dto/product-tags.args";
import { ProductTagsService } from "./product-tags.service";

@Resolver(() => ProductTag)
@RequiredPermission(["products"], { skipScopeCheck: true })
export class ProductTagResolver {
constructor(
private readonly entityManager: EntityManager,
private readonly productTagsService: ProductTagsService,
@InjectRepository(ProductTag) private readonly repository: EntityRepository<ProductTag>,
@InjectRepository(ProductToTag) private readonly productToTagRepository: EntityRepository<ProductToTag>,
@InjectRepository(Product) private readonly productRepository: EntityRepository<Product>,
Expand All @@ -38,7 +36,7 @@ export class ProductTagResolver {
@Args() { search, filter, sort, offset, limit }: ProductTagsArgs,
@Info() info: GraphQLResolveInfo,
): Promise<PaginatedProductTags> {
const where = this.productTagsService.getFindCondition({ search, filter });
const where = gqlArgsToMikroOrmQuery({ search, filter }, this.repository);

const fields = extractGraphqlFields(info, { root: "nodes" });
const populate: string[] = [];
Expand Down
Loading

0 comments on commit 4485d15

Please sign in to comment.