Browse Source

feat(core): Update TypeORM to v0.3.20

Squashed commit of the following:

commit 901c7a8d99bfdfb783f7495433cb444b23e2bee3
Author: Michael Bromley <michael@michaelbromley.co.uk>
Date:   Fri Mar 8 13:07:51 2024 +0100

    fix(core): Fix handling of customPropertyMap in ListQueryBuilder

commit 0c82208e8a1596942c255841fa670fc6288b1668
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Wed Mar 6 17:09:46 2024 +0100

    feat(core): Improve joinTreeRelationsDynamically function

commit b6b6663332e012fa65d7db1f243715e4010aacc9
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Wed Mar 6 16:01:44 2024 +0100

    feat(core): Add unjoined translation relations and fix alias for filters

commit 380142204192c587bfd69ecee2ea50ae72ad7a0e
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Wed Mar 6 12:39:13 2024 +0100

    feat(core): Refactoring for list-query-builder.ts and add missed relations

commit 2219b448c2ac2f0541b3cf8c9c2a2b888c9cc700
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Mon Mar 4 12:27:22 2024 +0100

    feat(core): Fix test case because we have only 2 variants of product

commit 752e31aeb0bb5afa3277023ca9aff8f35ca16e6c
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Mon Mar 4 10:03:40 2024 +0100

    chore(core): Clean up code

commit ce7be86c50accf696086cc1ddc711bdb8c9b8c1c
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Mon Mar 4 09:46:25 2024 +0100

    feat(core): Add missing reverse relations

commit 47add43efd9d16caf17749b001e270de7d86e652
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Sun Mar 3 16:23:23 2024 +0100

    chore(core): Clean up code

commit 0d8fec05c3500b827a5452350b177d70b283d41d
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Sun Mar 3 15:42:22 2024 +0100

    chore(core): Clean up code

commit f9f5260a4e03f527db41ccc8c4639cf1a9bc94ec
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Sun Mar 3 15:02:52 2024 +0100

    feat(core): Fix for un-joined relations or joined with wrong alias

commit 32e0df7c5c5f7fcdb68fcd3c37dd99bcab4d4d0f
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Sun Mar 3 13:11:21 2024 +0100

    feat(core): Refactoring and extending the `tree` join function

commit 2c372214eb260b1f6477460d5763ca21e93f993a
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Fri Mar 1 18:11:34 2024 +0100

    feat(chore): Fix comment for relationLoadStrategy

commit e9762ede78142ce4e476a1523a8339b91cf7fb10
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Fri Mar 1 18:10:24 2024 +0100

    feat(core): Backward compatibility and ability to use old logic to work around bugs in collections

commit ddaaaf2c62f91c65be38b6944e10aaa77add239e
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Fri Mar 1 17:22:07 2024 +0100

    feat(core): Refactoring for a problem with Tree collection due to a bug in Typeorm

commit 42764b4fef1534e839c43513347a9997c230d4cc
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Fri Mar 1 11:59:26 2024 +0100

    feat(core): Update inverse relations and filter for entities

commit 4a9e082fcc1daff96ffaad6793664ad4c5764945
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Wed Feb 21 11:39:32 2024 +0100

    feat(core): Update expression for product-variant.entity.ts

commit ae6d95d7d6a392380a148114fda270584313829c
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Wed Feb 21 10:43:45 2024 +0100

    feat(core): Update translationsAlias and clean up code from comments

commit 937d4de00405d16a977a90e63d3a95fae1383f3b
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Tue Feb 20 19:35:40 2024 +0100

    feat(core): Fixed alias from NamingStrategy due to difference `_{relation}` -> `__{relation}`

commit dd30d22d42d1cee54498c020ada4cbf0f4713c27
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Tue Feb 20 18:00:10 2024 +0100

    feat(core): Added relations from queries and removed optimization methods

commit 6ebbcfcd3a7db8c12c2259548640e0dc81e503a5
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Tue Feb 20 14:45:29 2024 +0100

    feat(core): Change removed method eagerJoinRelationAlias to joinTableColumnName

commit e9b1169357d19a758d1d5a40f920e13efe7f7c66
Author: Eugene Nitsenko <nitsenko94@gmail.com>
Date:   Tue Feb 20 14:36:40 2024 +0100

    feat(core): Upgrade typeorm version from v0.3.11 to v0.3.20
Michael Bromley 1 year ago
parent
commit
0afc94e3ee
22 changed files with 352 additions and 278 deletions
  1. 2 2
      packages/core/e2e/fixtures/test-plugins/list-query-plugin.ts
  2. 1 1
      packages/core/e2e/money-strategy.e2e-spec.ts
  3. 1 1
      packages/core/package.json
  4. 13 1
      packages/core/src/entity/asset/asset.entity.ts
  5. 21 1
      packages/core/src/entity/channel/channel.entity.ts
  6. 2 2
      packages/core/src/entity/collection/collection.entity.ts
  7. 9 1
      packages/core/src/entity/facet-value/facet-value.entity.ts
  8. 5 1
      packages/core/src/entity/fulfillment/fulfillment.entity.ts
  9. 3 3
      packages/core/src/entity/order/order.entity.ts
  10. 1 1
      packages/core/src/entity/product-option-group/product-option-group.entity.ts
  11. 5 1
      packages/core/src/entity/product-option/product-option.entity.ts
  12. 7 7
      packages/core/src/entity/product-variant/product-variant.entity.ts
  13. 5 5
      packages/core/src/entity/product/product.entity.ts
  14. 3 0
      packages/core/src/entity/promotion/promotion.entity.ts
  15. 5 1
      packages/core/src/entity/tax-category/tax-category.entity.ts
  16. 201 211
      packages/core/src/service/helpers/list-query-builder/list-query-builder.ts
  17. 1 1
      packages/core/src/service/helpers/list-query-builder/parse-filter-params.spec.ts
  18. 3 1
      packages/core/src/service/helpers/list-query-builder/parse-filter-params.ts
  19. 3 2
      packages/core/src/service/helpers/list-query-builder/parse-sort-params.spec.ts
  20. 8 3
      packages/core/src/service/helpers/list-query-builder/parse-sort-params.ts
  21. 3 2
      packages/core/src/service/services/collection.service.ts
  22. 50 30
      yarn.lock

+ 2 - 2
packages/core/e2e/fixtures/test-plugins/list-query-plugin.ts

@@ -35,7 +35,7 @@ export class CustomFieldRelationTestEntity extends VendureEntity {
     @Column()
     data: string;
 
-    @ManyToOne(() => TestEntity)
+    @ManyToOne(() => TestEntity, testEntity => testEntity.customFields.relation)
     parent: Relation<TestEntity>;
 }
 
@@ -48,7 +48,7 @@ export class CustomFieldOtherRelationTestEntity extends VendureEntity {
     @Column()
     data: string;
 
-    @ManyToOne(() => TestEntity)
+    @ManyToOne(() => TestEntity, testEntity => testEntity.customFields.otherRelation)
     parent: Relation<TestEntity>;
 }
 

+ 1 - 1
packages/core/e2e/money-strategy.e2e-spec.ts

@@ -102,7 +102,7 @@ describe('Custom MoneyStrategy', () => {
         cheapVariantId = productVariants.items[0].id;
         expensiveVariantId = productVariants.items[1].id;
 
-        expect(CustomMoneyStrategy.transformerFromSpy).toHaveBeenCalledTimes(6);
+        expect(CustomMoneyStrategy.transformerFromSpy).toHaveBeenCalledTimes(2);
     });
 
     // https://github.com/vendure-ecommerce/vendure/issues/838

+ 1 - 1
packages/core/package.json

@@ -78,7 +78,7 @@
         "progress": "^2.0.3",
         "reflect-metadata": "^0.1.13",
         "rxjs": "^7.8.1",
-        "typeorm": "0.3.11"
+        "typeorm": "0.3.20"
     },
     "devDependencies": {
         "@types/bcrypt": "^5.0.0",

+ 13 - 1
packages/core/src/entity/asset/asset.entity.ts

@@ -1,12 +1,15 @@
 import { AssetType } from '@vendure/common/lib/generated-types';
 import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { Column, Entity, JoinTable, ManyToMany } from 'typeorm';
+import { Column, Entity, JoinTable, ManyToMany, OneToMany } from 'typeorm';
 
 import { ChannelAware, Taggable } from '../../common/types/common-types';
 import { HasCustomFields } from '../../config/custom-field/custom-field-types';
 import { VendureEntity } from '../base/base.entity';
 import { Channel } from '../channel/channel.entity';
+import { Collection } from '../collection/collection.entity';
 import { CustomAssetFields } from '../custom-entity-fields';
+import { Product } from '../product/product.entity';
+import { ProductVariant } from '../product-variant/product-variant.entity';
 import { Tag } from '../tag/tag.entity';
 
 /**
@@ -49,6 +52,15 @@ export class Asset extends VendureEntity implements Taggable, ChannelAware, HasC
     @JoinTable()
     channels: Channel[];
 
+    @OneToMany(type => Collection, collection => collection.featuredAsset)
+    featuredInCollections?: Collection[];
+
+    @OneToMany(type => ProductVariant, productVariant => productVariant.featuredAsset)
+    featuredInVariants?: ProductVariant[];
+
+    @OneToMany(type => Product, product => product.featuredAsset)
+    featuredInProducts?: Product[];
+
     @Column(type => CustomAssetFields)
     customFields: CustomAssetFields;
 }

+ 21 - 1
packages/core/src/entity/channel/channel.entity.ts

@@ -1,10 +1,15 @@
 import { CurrencyCode, LanguageCode } from '@vendure/common/lib/generated-types';
 import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
-import { Column, Entity, Index, ManyToOne } from 'typeorm';
+import { Column, Entity, Index, ManyToMany, ManyToOne } from 'typeorm';
 
 import { VendureEntity } from '../base/base.entity';
+import { Collection } from '../collection/collection.entity';
 import { CustomChannelFields } from '../custom-entity-fields';
 import { EntityId } from '../entity-id.decorator';
+import { Facet } from '../facet/facet.entity';
+import { FacetValue } from '../facet-value/facet-value.entity';
+import { Product } from '../product/product.entity';
+import { ProductVariant } from '../product-variant/product-variant.entity';
 import { Seller } from '../seller/seller.entity';
 import { Zone } from '../zone/zone.entity';
 
@@ -103,6 +108,21 @@ export class Channel extends VendureEntity {
 
     @Column() pricesIncludeTax: boolean;
 
+    @ManyToMany(type => Product, product => product.channels)
+    products: Product[];
+
+    @ManyToMany(type => ProductVariant, productVariant => productVariant.channels)
+    productVariants: ProductVariant[];
+
+    @ManyToMany(type => FacetValue, facetValue => facetValue.channels)
+    facetValues: FacetValue[];
+
+    @ManyToMany(type => Facet, facet => facet.channels)
+    facets: Facet[];
+
+    @ManyToMany(type => Collection, collection => collection.channels)
+    collections: Collection[];
+
     private generateToken(): string {
         const randomString = () => Math.random().toString(36).substr(3, 10);
         return `${randomString()}${randomString()}`;

+ 2 - 2
packages/core/src/entity/collection/collection.entity.ts

@@ -61,7 +61,7 @@ export class Collection
     translations: Array<Translation<Collection>>;
 
     @Index()
-    @ManyToOne(type => Asset, { onDelete: 'SET NULL' })
+    @ManyToOne(type => Asset, asset => asset.featuredInCollections, { onDelete: 'SET NULL' })
     featuredAsset: Asset;
 
     @OneToMany(type => CollectionAsset, collectionAsset => collectionAsset.collection)
@@ -90,7 +90,7 @@ export class Collection
     @EntityId({ nullable: true })
     parentId: ID;
 
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.collections)
     @JoinTable()
     channels: Channel[];
 }

+ 9 - 1
packages/core/src/entity/facet-value/facet-value.entity.ts

@@ -9,6 +9,8 @@ import { Channel } from '../channel/channel.entity';
 import { CustomFacetValueFields } from '../custom-entity-fields';
 import { EntityId } from '../entity-id.decorator';
 import { Facet } from '../facet/facet.entity';
+import { Product } from '../product/product.entity';
+import { ProductVariant } from '../product-variant/product-variant.entity';
 
 import { FacetValueTranslation } from './facet-value-translation.entity';
 
@@ -40,7 +42,13 @@ export class FacetValue extends VendureEntity implements Translatable, HasCustom
     @Column(type => CustomFacetValueFields)
     customFields: CustomFacetValueFields;
 
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.facetValues)
     @JoinTable()
     channels: Channel[];
+
+    @ManyToMany(() => Product, product => product.facetValues)
+    products: Product[];
+
+    @ManyToMany(type => ProductVariant, productVariant => productVariant.facetValues)
+    productVariants: ProductVariant[];
 }

+ 5 - 1
packages/core/src/entity/fulfillment/fulfillment.entity.ts

@@ -1,10 +1,11 @@
 import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { Column, Entity, OneToMany } from 'typeorm';
+import { Column, Entity, ManyToMany, OneToMany } from 'typeorm';
 
 import { HasCustomFields } from '../../config/custom-field/custom-field-types';
 import { FulfillmentState } from '../../service/helpers/fulfillment-state-machine/fulfillment-state';
 import { VendureEntity } from '../base/base.entity';
 import { CustomFulfillmentFields } from '../custom-entity-fields';
+import { Order } from '../order/order.entity';
 import { FulfillmentLine } from '../order-line-reference/fulfillment-line.entity';
 
 /**
@@ -34,6 +35,9 @@ export class Fulfillment extends VendureEntity implements HasCustomFields {
     @OneToMany(type => FulfillmentLine, fulfillmentLine => fulfillmentLine.fulfillment)
     lines: FulfillmentLine[];
 
+    @ManyToMany(type => Order, order => order.fulfillments)
+    orders: Order[];
+
     @Column(type => CustomFulfillmentFields)
     customFields: CustomFulfillmentFields;
 }

+ 3 - 3
packages/core/src/entity/order/order.entity.ts

@@ -91,7 +91,7 @@ export class Order extends VendureEntity implements ChannelAware, HasCustomField
     orderPlacedAt?: Date;
 
     @Index()
-    @ManyToOne(type => Customer)
+    @ManyToOne(type => Customer, customer => customer.orders)
     customer?: Customer;
 
     @EntityId({ nullable: true })
@@ -122,7 +122,7 @@ export class Order extends VendureEntity implements ChannelAware, HasCustomField
      * Promotions applied to the order. Only gets populated after the payment process has completed,
      * i.e. the Order is no longer active.
      */
-    @ManyToMany(type => Promotion)
+    @ManyToMany(type => Promotion, promotion => promotion.orders)
     @JoinTable()
     promotions: Promotion[];
 
@@ -133,7 +133,7 @@ export class Order extends VendureEntity implements ChannelAware, HasCustomField
     @OneToMany(type => Payment, payment => payment.order)
     payments: Payment[];
 
-    @ManyToMany(type => Fulfillment)
+    @ManyToMany(type => Fulfillment, fulfillment => fulfillment.orders)
     @JoinTable()
     fulfillments: Fulfillment[];
 

+ 1 - 1
packages/core/src/entity/product-option-group/product-option-group.entity.ts

@@ -40,7 +40,7 @@ export class ProductOptionGroup
     options: ProductOption[];
 
     @Index()
-    @ManyToOne(type => Product)
+    @ManyToOne(type => Product, product => product.optionGroups)
     product: Product;
 
     @Column(type => CustomProductOptionGroupFields)

+ 5 - 1
packages/core/src/entity/product-option/product-option.entity.ts

@@ -1,5 +1,5 @@
 import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
-import { Column, Entity, Index, ManyToOne, OneToMany } from 'typeorm';
+import { Column, Entity, Index, ManyToMany, ManyToOne, OneToMany } from 'typeorm';
 
 import { SoftDeletable } from '../../common/types/common-types';
 import { LocaleString, Translatable, Translation } from '../../common/types/locale-types';
@@ -8,6 +8,7 @@ import { VendureEntity } from '../base/base.entity';
 import { CustomProductOptionFields } from '../custom-entity-fields';
 import { EntityId } from '../entity-id.decorator';
 import { ProductOptionGroup } from '../product-option-group/product-option-group.entity';
+import { ProductVariant } from '../product-variant/product-variant.entity';
 
 import { ProductOptionTranslation } from './product-option-translation.entity';
 
@@ -39,6 +40,9 @@ export class ProductOption extends VendureEntity implements Translatable, HasCus
     @EntityId()
     groupId: ID;
 
+    @ManyToMany(type => ProductVariant, variant => variant.options)
+    productVariants: ProductVariant[];
+
     @Column(type => CustomProductOptionFields)
     customFields: CustomProductOptionFields;
 }

+ 7 - 7
packages/core/src/entity/product-variant/product-variant.entity.ts

@@ -70,7 +70,7 @@ export class ProductVariant
     currencyCode: CurrencyCode;
 
     @Calculated({
-        expression: 'productvariant_productVariantPrices.price',
+        expression: 'productvariant__productVariantPrices.price',
     })
     get price(): number {
         if (this.listPrice == null) {
@@ -86,7 +86,7 @@ export class ProductVariant
         // results due to this expression not taking taxes into account. This is because the tax
         // rate is calculated at run-time in the application layer based on the current context,
         // and is unknown to the database.
-        expression: 'productvariant_productVariantPrices.price',
+        expression: 'productvariant__productVariantPrices.price',
     })
     get priceWithTax(): number {
         if (this.listPrice == null) {
@@ -103,7 +103,7 @@ export class ProductVariant
     taxRateApplied: TaxRate;
 
     @Index()
-    @ManyToOne(type => Asset, { onDelete: 'SET NULL' })
+    @ManyToOne(type => Asset, asset => asset.featuredInVariants, { onDelete: 'SET NULL' })
     featuredAsset: Asset;
 
     @OneToMany(type => ProductVariantAsset, productVariantAsset => productVariantAsset.productVariant, {
@@ -112,7 +112,7 @@ export class ProductVariant
     assets: ProductVariantAsset[];
 
     @Index()
-    @ManyToOne(type => TaxCategory)
+    @ManyToOne(type => TaxCategory, taxCategory => taxCategory.productVariants)
     taxCategory: TaxCategory;
 
     @OneToMany(type => ProductVariantPrice, price => price.variant, { eager: true })
@@ -153,11 +153,11 @@ export class ProductVariant
     @OneToMany(type => StockMovement, stockMovement => stockMovement.productVariant)
     stockMovements: StockMovement[];
 
-    @ManyToMany(type => ProductOption)
+    @ManyToMany(type => ProductOption, productOption => productOption.productVariants)
     @JoinTable()
     options: ProductOption[];
 
-    @ManyToMany(type => FacetValue)
+    @ManyToMany(type => FacetValue, facetValue => facetValue.productVariants)
     @JoinTable()
     facetValues: FacetValue[];
 
@@ -167,7 +167,7 @@ export class ProductVariant
     @ManyToMany(type => Collection, collection => collection.productVariants)
     collections: Collection[];
 
-    @ManyToMany(type => Channel)
+    @ManyToMany(type => Channel, channel => channel.productVariants)
     @JoinTable()
     channels: Channel[];
 }

+ 5 - 5
packages/core/src/entity/product/product.entity.ts

@@ -44,7 +44,7 @@ export class Product
     enabled: boolean;
 
     @Index()
-    @ManyToOne(type => Asset, { onDelete: 'SET NULL' })
+    @ManyToOne(type => Asset, asset => asset.featuredInProducts, { onDelete: 'SET NULL' })
     featuredAsset: Asset;
 
     @OneToMany(type => ProductAsset, productAsset => productAsset.product)
@@ -59,14 +59,14 @@ export class Product
     @OneToMany(type => ProductOptionGroup, optionGroup => optionGroup.product)
     optionGroups: ProductOptionGroup[];
 
-    @ManyToMany(type => FacetValue)
+    @ManyToMany(type => FacetValue, facetValue => facetValue.products)
     @JoinTable()
     facetValues: FacetValue[];
 
-    @Column(type => CustomProductFields)
-    customFields: CustomProductFields;
-
     @ManyToMany(type => Channel)
     @JoinTable()
     channels: Channel[];
+
+    @Column(type => CustomProductFields)
+    customFields: CustomProductFields;
 }

+ 3 - 0
packages/core/src/entity/promotion/promotion.entity.ts

@@ -111,6 +111,9 @@ export class Promotion
     @JoinTable()
     channels: Channel[];
 
+    @ManyToMany(type => Order, order => order.promotions)
+    orders: Order[];
+
     @Column(type => CustomPromotionFields)
     customFields: CustomPromotionFields;
 

+ 5 - 1
packages/core/src/entity/tax-category/tax-category.entity.ts

@@ -1,9 +1,10 @@
 import { DeepPartial } from '@vendure/common/lib/shared-types';
-import { Column, Entity } from 'typeorm';
+import { Column, Entity, OneToMany } from 'typeorm';
 
 import { HasCustomFields } from '../../config/custom-field/custom-field-types';
 import { VendureEntity } from '../base/base.entity';
 import { CustomTaxCategoryFields } from '../custom-entity-fields';
+import { ProductVariant } from '../product-variant/product-variant.entity';
 
 /**
  * @description
@@ -23,4 +24,7 @@ export class TaxCategory extends VendureEntity implements HasCustomFields {
 
     @Column(type => CustomTaxCategoryFields)
     customFields: CustomTaxCategoryFields;
+
+    @OneToMany(type => ProductVariant, productVariant => productVariant.taxCategory)
+    productVariants: ProductVariant[];
 }

+ 201 - 211
packages/core/src/service/helpers/list-query-builder/list-query-builder.ts

@@ -4,32 +4,28 @@ import { ID, Type } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
 import {
     Brackets,
-    FindManyOptions,
+    EntityMetadata,
     FindOneOptions,
     FindOptionsWhere,
-    In,
     Repository,
     SelectQueryBuilder,
     WhereExpressionBuilder,
 } from 'typeorm';
+import { EntityTarget } from 'typeorm/common/EntityTarget';
 import { BetterSqlite3Driver } from 'typeorm/driver/better-sqlite3/BetterSqlite3Driver';
 import { SqljsDriver } from 'typeorm/driver/sqljs/SqljsDriver';
-import { FindOptionsUtils } from 'typeorm/find-options/FindOptionsUtils';
 
-import { ApiType } from '../../../api/common/get-api-type';
-import { RequestContext } from '../../../api/common/request-context';
-import { UserInputError } from '../../../common/error/errors';
+import { ApiType, RequestContext } from '../../../api';
 import {
     FilterParameter,
     ListQueryOptions,
     NullOptionals,
     SortParameter,
-} from '../../../common/types/common-types';
-import { ConfigService } from '../../../config/config.service';
-import { CustomFields } from '../../../config/custom-field/custom-field-types';
-import { Logger } from '../../../config/logger/vendure-logger';
-import { TransactionalConnection } from '../../../connection/transactional-connection';
-import { VendureEntity } from '../../../entity/base/base.entity';
+    UserInputError,
+} from '../../../common';
+import { ConfigService, CustomFields, Logger } from '../../../config';
+import { TransactionalConnection } from '../../../connection';
+import { VendureEntity } from '../../../entity';
 
 import { getColumnMetadata, getEntityAlias } from './connection-utils';
 import { getCalculatedColumns } from './get-calculated-columns';
@@ -248,7 +244,7 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         return false;
     }
 
-    /**
+    /*
      * @description
      * Creates and configures a SelectQueryBuilder for queries that return paginated lists of entities.
      */
@@ -258,7 +254,6 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         extendedOptions: ExtendedListQueryOptions<T> = {},
     ): SelectQueryBuilder<T> {
         const apiType = extendedOptions.ctx?.apiType ?? 'shop';
-        const rawConnection = this.connection.rawConnection;
         const { take, skip } = this.parseTakeSkipParams(apiType, options, extendedOptions.ignoreQueryLimits);
 
         const repo = extendedOptions.ctx
@@ -266,45 +261,47 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
             : this.connection.rawConnection.getRepository(entity);
         const alias = extendedOptions.entityAlias || entity.name.toLowerCase();
         const minimumRequiredRelations = this.getMinimumRequiredRelations(repo, options, extendedOptions);
-        const qb = repo.createQueryBuilder(alias).setFindOptions({
-            relations: minimumRequiredRelations,
+        const qb = repo.createQueryBuilder(alias);
+
+        let relations = unique([...minimumRequiredRelations, ...(extendedOptions?.relations ?? [])]);
+
+        // Special case for the 'collection' entity, which has a complex nested structure
+        // and requires special handling to ensure that only the necessary relations are joined.
+        // This is bypassed an issue in TypeORM where it would join the same relation multiple times.
+        // See https://github.com/typeorm/typeorm/issues/9936 for more context.
+        const processedRelations = this.joinTreeRelationsDynamically(qb, entity, relations);
+
+        // Remove any relations which are related to the 'collection' tree, as these are handled separately
+        // to avoid duplicate joins.
+        relations = relations.filter(relationPath => !processedRelations.has(relationPath));
+
+        qb.setFindOptions({
+            relations,
             take,
             skip,
             where: extendedOptions.where || {},
-            // We would like to be able to use this feature
-            // rather than our custom `optimizeGetManyAndCountMethod()` implementation,
-            // but at this time (TypeORM 0.3.12) it throws an error in the case of
-            // a Collection that joins its parent entity.
-            // relationLoadStrategy: 'query',
+            relationLoadStrategy: 'query',
         });
-        // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-        FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
 
         // join the tables required by calculated columns
         this.joinCalculatedColumnRelations(qb, entity, options);
 
-        const { customPropertyMap, entityAlias } = extendedOptions;
+        const { customPropertyMap } = extendedOptions;
         if (customPropertyMap) {
             this.normalizeCustomPropertyMap(customPropertyMap, options, qb);
         }
         const customFieldsForType = this.configService.customFields[entity.name as keyof CustomFields];
         const sortParams = Object.assign({}, options.sort, extendedOptions.orderBy);
-        this.applyTranslationConditions(qb, entity, sortParams, extendedOptions.ctx, alias);
+        this.applyTranslationConditions(qb, entity, sortParams, extendedOptions.ctx);
         const sort = parseSortParams(
-            rawConnection,
+            qb.connection,
             entity,
             sortParams,
             customPropertyMap,
-            entityAlias,
+            qb.alias,
             customFieldsForType,
         );
-        const filter = parseFilterParams(
-            rawConnection,
-            entity,
-            options.filter,
-            customPropertyMap,
-            entityAlias,
-        );
+        const filter = parseFilterParams(qb.connection, entity, options.filter, customPropertyMap, qb.alias);
 
         if (filter.length) {
             const filterOperator = options.filterOperator ?? LogicalOperator.AND;
@@ -326,14 +323,12 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         }
 
         if (extendedOptions.channelId) {
-            qb.leftJoin(`${alias}.channels`, 'lqb__channel').andWhere('lqb__channel.id = :channelId', {
+            qb.innerJoin(`${qb.alias}.channels`, 'lqb__channel', 'lqb__channel.id = :channelId', {
                 channelId: extendedOptions.channelId,
             });
         }
 
         qb.orderBy(sort);
-        this.optimizeGetManyAndCountMethod(qb, repo, extendedOptions, minimumRequiredRelations);
-        this.optimizeGetManyMethod(qb, repo, extendedOptions, minimumRequiredRelations);
         return qb;
     }
 
@@ -403,6 +398,7 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         if (extendedOptions.channelId) {
             requiredRelations.push('channels');
         }
+
         if (extendedOptions.customPropertyMap) {
             const metadata = repository.metadata;
 
@@ -432,163 +428,13 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         return !!(options.sort?.[property] || options.filter?.[property]);
     }
 
-    /**
-     * @description
-     * This will monkey-patch the `getManyAndCount()` method in order to implement a more efficient
-     * parallel-query based approach to joining multiple relations. This is loosely based on the
-     * solution outlined here: https://github.com/typeorm/typeorm/issues/3857#issuecomment-633006643
-     *
-     * TODO: When upgrading to TypeORM v0.3+, this will likely become redundant due to the new
-     * `relationLoadStrategy` feature.
-     */
-    private optimizeGetManyAndCountMethod<T extends VendureEntity>(
-        qb: SelectQueryBuilder<T>,
-        repo: Repository<T>,
-        extendedOptions: ExtendedListQueryOptions<T>,
-        alreadyJoined: string[],
-    ) {
-        const originalGetManyAndCount = qb.getManyAndCount.bind(qb);
-        qb.getManyAndCount = async () => {
-            const relations = unique(extendedOptions.relations ?? []);
-            const [entities, count] = await originalGetManyAndCount();
-            if (relations == null || alreadyJoined.sort().join() === relations?.sort().join()) {
-                // No further relations need to be joined, so we just
-                // return the regular result.
-                return [entities, count];
-            }
-            const result = await this.parallelLoadRelations(entities, relations, alreadyJoined, repo);
-            return [result, count];
-        };
-    }
-    /**
-     * @description
-     * This will monkey-patch the `getMany()` method in order to implement a more efficient
-     * parallel-query based approach to joining multiple relations. This is loosely based on the
-     * solution outlined here: https://github.com/typeorm/typeorm/issues/3857#issuecomment-633006643
-     *
-     * TODO: When upgrading to TypeORM v0.3+, this will likely become redundant due to the new
-     * `relationLoadStrategy` feature.
-     */
-    private optimizeGetManyMethod<T extends VendureEntity>(
-        qb: SelectQueryBuilder<T>,
-        repo: Repository<T>,
-        extendedOptions: ExtendedListQueryOptions<T>,
-        alreadyJoined: string[],
-    ) {
-        const originalGetMany = qb.getMany.bind(qb);
-        qb.getMany = async () => {
-            const relations = unique(extendedOptions.relations ?? []);
-            const entities = await originalGetMany();
-            if (relations == null || alreadyJoined.sort().join() === relations?.sort().join()) {
-                // No further relations need to be joined, so we just
-                // return the regular result.
-                return entities;
-            }
-            return this.parallelLoadRelations(entities, relations, alreadyJoined, repo);
-        };
-    }
-
-    private async parallelLoadRelations<T extends VendureEntity>(
-        entities: T[],
-        relations: string[],
-        alreadyJoined: string[],
-        repo: Repository<T>,
-    ): Promise<T[]> {
-        const entityMap = new Map(entities.map(e => [e.id, e]));
-        const entitiesIds = entities.map(({ id }) => id);
-
-        const splitRelations = relations
-            .map(r => r.split('.'))
-            .filter(path => {
-                // There is an issue in TypeORM currently which causes
-                // an error when trying to join nested relations inside
-                // customFields. See https://github.com/vendure-ecommerce/vendure/issues/1664
-                // The work-around is to omit them and rely on the GraphQL resolver
-                // layer to handle.
-                if (path[0] === 'customFields' && 2 < path.length) {
-                    return false;
-                }
-                return true;
-            });
-        const groupedRelationsMap = new Map<string, string[]>();
-
-        for (const relationParts of splitRelations) {
-            const group = groupedRelationsMap.get(relationParts[0]);
-            if (group) {
-                group.push(relationParts.join('.'));
-            } else {
-                groupedRelationsMap.set(relationParts[0], [relationParts.join('.')]);
-            }
-        }
-
-        // If the extendedOptions includes relations that were already joined, then
-        // we ignore those now so as not to do the work of joining twice.
-        for (const tableName of alreadyJoined) {
-            if (groupedRelationsMap.get(tableName)?.length === 1) {
-                groupedRelationsMap.delete(tableName);
-            }
-        }
-
-        const entitiesIdsWithRelations = await Promise.all(
-            Array.from(groupedRelationsMap.values())?.map(relationPaths => {
-                return repo
-                    .find({
-                        where: { id: In(entitiesIds) },
-                        select: ['id'],
-                        relations: relationPaths,
-                        loadEagerRelations: true,
-                    } as FindManyOptions<T>)
-                    .then(results =>
-                        results.map(r => ({
-                            relations: relationPaths[0].startsWith('customFields.')
-                                ? relationPaths
-                                : [relationPaths[0]],
-                            entity: r,
-                        })),
-                    );
-            }),
-        ).then(all => all.flat());
-        for (const entry of entitiesIdsWithRelations) {
-            const finalEntity = entityMap.get(entry.entity.id);
-            for (const relation of entry.relations) {
-                if (finalEntity) {
-                    this.assignDeep(relation, entry.entity, finalEntity);
-                }
-            }
-        }
-        return Array.from(entityMap.values());
-    }
-
-    private assignDeep<T>(relation: string | keyof T, source: T, target: T) {
-        if (typeof relation === 'string') {
-            const parts = relation.split('.');
-            let resolvedTarget: any = target;
-            let resolvedSource: any = source;
-
-            for (const part of parts.slice(0, parts.length - 1)) {
-                if (!resolvedTarget[part]) {
-                    resolvedTarget[part] = {};
-                }
-                if (!resolvedSource[part]) {
-                    return;
-                }
-                resolvedTarget = resolvedTarget[part];
-                resolvedSource = resolvedSource[part];
-            }
-
-            resolvedTarget[parts[parts.length - 1]] = resolvedSource[parts[parts.length - 1]];
-        } else {
-            target[relation] = source[relation];
-        }
-    }
-
     /**
      * If a customPropertyMap is provided, we need to take the path provided and convert it to the actual
      * relation aliases being used by the SelectQueryBuilder.
      *
      * This method mutates the customPropertyMap object.
      */
-    private normalizeCustomPropertyMap(
+    private normalizeCustomPropertyMap<T extends VendureEntity>(
         customPropertyMap: { [name: string]: string },
         options: ListQueryOptions<any>,
         qb: SelectQueryBuilder<any>,
@@ -597,23 +443,46 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
             if (!this.customPropertyIsBeingUsed(property, options)) {
                 continue;
             }
-            const parts = customPropertyMap[property].split('.');
-            const entityPart = 2 <= parts.length ? parts[parts.length - 2] : qb.alias;
-            const columnPart = parts[parts.length - 1];
-
-            const relationMetadata =
-                qb.expressionMap.mainAlias?.metadata.findRelationWithPropertyPath(entityPart);
-            const relationAlias =
-                qb.expressionMap.aliases.find(a => a.metadata.tableNameWithoutPrefix === entityPart) ??
-                qb.expressionMap.joinAttributes.find(ja => ja.relationCache === relationMetadata)?.alias;
-            if (relationAlias) {
-                customPropertyMap[property] = `${relationAlias.name}.${columnPart}`;
-            } else {
-                Logger.error(
-                    `The customPropertyMap entry "${property}:${value}" could not be resolved to a related table`,
+            let parts = customPropertyMap[property].split('.');
+            const normalizedRelationPath: string[] = [];
+            let entityMetadata = qb.expressionMap.mainAlias?.metadata;
+            let entityAlias = qb.alias;
+            while (parts.length > 1) {
+                const entityPart = 2 <= parts.length ? parts[0] : qb.alias;
+                const columnPart = parts[parts.length - 1];
+
+                if (!entityMetadata) {
+                    Logger.error(`Could not get metadata for entity ${qb.alias}`);
+                    continue;
+                }
+                const relationMetadata = entityMetadata.findRelationWithPropertyPath(entityPart);
+                if (!relationMetadata ?? !relationMetadata?.propertyName) {
+                    Logger.error(
+                        `The customPropertyMap entry "${property}:${value}" could not be resolved to a related table`,
+                    );
+                    delete customPropertyMap[property];
+                    return;
+                }
+                const alias = qb.connection.namingStrategy.joinTableName(
+                    entityMetadata.tableName,
+                    relationMetadata.propertyName,
+                    '',
+                    '',
                 );
-                delete customPropertyMap[property];
+                if (!this.isRelationAlreadyJoined(qb, alias)) {
+                    qb.leftJoinAndSelect(`${entityAlias}.${relationMetadata.propertyName}`, alias);
+                }
+                parts = parts.slice(1);
+                entityMetadata = relationMetadata?.inverseEntityMetadata;
+                normalizedRelationPath.push(entityAlias);
+
+                if (parts.length === 1) {
+                    normalizedRelationPath.push(alias, columnPart);
+                } else {
+                    entityAlias = alias;
+                }
             }
+            customPropertyMap[property] = normalizedRelationPath.slice(-2).join('.');
         }
     }
 
@@ -667,16 +536,11 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         entity: Type<T>,
         sortParams: NullOptionals<SortParameter<T>> & FindOneOptions<T>['order'],
         ctx?: RequestContext,
-        entityAlias?: string,
     ) {
         const languageCode = ctx?.languageCode || this.configService.defaultLanguageCode;
 
-        const {
-            columns,
-            translationColumns,
-            alias: defaultAlias,
-        } = getColumnMetadata(this.connection.rawConnection, entity);
-        const alias = entityAlias ?? defaultAlias;
+        const { translationColumns } = getColumnMetadata(qb.connection, entity);
+        const alias = qb.alias;
 
         const sortKeys = Object.keys(sortParams);
         let sortingOnTranslatableKey = false;
@@ -687,10 +551,15 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         }
 
         if (translationColumns.length && sortingOnTranslatableKey) {
-            const translationsAlias = qb.connection.namingStrategy.eagerJoinRelationAlias(
+            const translationsAlias = qb.connection.namingStrategy.joinTableName(
                 alias,
                 'translations',
+                '',
+                '',
             );
+            if (!this.isRelationAlreadyJoined(qb, translationsAlias)) {
+                qb.leftJoinAndSelect(`${alias}.translations`, translationsAlias);
+            }
 
             qb.andWhere(
                 new Brackets(qb1 => {
@@ -764,4 +633,125 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
             driver.databaseConnection.create_function('regexp', regexpFn);
         }
     }
+
+    /**
+     * These method are designed to address specific challenges encountered with TypeORM
+     * when dealing with complex relation structures, particularly around the 'collection'
+     * entity and other similar entities, and they nested relations ('parent', 'children'). The need for these custom
+     * implementations arises from limitations in handling deeply nested relations and ensuring
+     * efficient query generation without duplicate joins, as discussed in TypeORM issue #9936.
+     * See https://github.com/typeorm/typeorm/issues/9936 for more context.
+     */
+
+    /**
+     * Verifies if a relation has already been joined in a query builder to prevent duplicate joins.
+     * This method ensures query efficiency and correctness by maintaining unique joins within the query builder.
+     *
+     * @param {SelectQueryBuilder<T>} qb The query builder instance where the joins are being added.
+     * @param {string} alias The join alias to check for uniqueness. This alias is used to determine if the relation
+     *                       has already been joined to avoid adding duplicate join statements.
+     * @returns boolean Returns true if the relation has already been joined (based on the alias), false otherwise.
+     * @template T extends VendureEntity The entity type for which the query builder is configured.
+     */
+    private isRelationAlreadyJoined<T extends VendureEntity>(
+        qb: SelectQueryBuilder<T>,
+        alias: string,
+    ): boolean {
+        return qb.expressionMap.joinAttributes.some(ja => ja.alias.name === alias);
+    }
+
+    /**
+     * Dynamically joins tree relations and their eager relations to a query builder. This method is specifically
+     * designed for entities utilizing TypeORM tree decorators (@TreeParent, @TreeChildren) and aims to address
+     * the challenge of efficiently managing deeply nested relations and avoiding duplicate joins. The method
+     * automatically handles the joining of related entities marked with tree relation decorators and eagerly
+     * loaded relations, ensuring efficient data retrieval and query generation.
+     *
+     * The method iterates over the requested relations paths, joining each relation dynamically. For tree relations,
+     * it also recursively joins all associated eager relations. This approach avoids the manual specification of joins
+     * and leverages TypeORM's relation metadata to automate the process.
+     *
+     * @param {SelectQueryBuilder<T>} qb The query builder instance to which the relations will be joined.
+     * @param {EntityTarget<T>} entity The target entity class or schema name. This parameter is used to access
+     *                                 the entity's metadata and analyze its relations.
+     * @param {string[]} requestedRelations An array of strings representing the relation paths to be dynamically joined.
+     *                                      Each string in the array should denote a path to a relation (e.g., 'parent.parent.children').
+     * @returns {Set<string>} A Set containing the paths of relations that were dynamically joined. This set can be used
+     *                        to track which relations have been processed and potentially avoid duplicate processing.
+     * @template T extends VendureEntity The type of the entity for which relations are being joined. This type parameter
+     *                                    should extend VendureEntity to ensure compatibility with Vendure's data access layer.
+     */
+    private joinTreeRelationsDynamically<T extends VendureEntity>(
+        qb: SelectQueryBuilder<T>,
+        entity: EntityTarget<T>,
+        requestedRelations: string[] = [],
+    ): Set<string> {
+        const metadata = qb.connection.getMetadata(entity);
+        const processedRelations = new Set<string>();
+
+        const processRelation = (
+            currentMetadata: EntityMetadata,
+            currentParentIsTreeType: boolean,
+            currentPath: string,
+            currentAlias: string,
+        ) => {
+            const currentMetadataIsTreeType = metadata.treeType;
+            if (!currentParentIsTreeType && !currentMetadataIsTreeType) {
+                return;
+            }
+
+            const parts = currentPath.split('.');
+            const part = parts.shift();
+
+            if (!part || !currentMetadata) return;
+
+            const relationMetadata = currentMetadata.findRelationWithPropertyPath(part);
+            if (relationMetadata) {
+                const isEager = relationMetadata.isEager;
+                let joinConnector = '_';
+                if (isEager) {
+                    joinConnector = '__';
+                }
+                const nextAlias = `${currentAlias}${joinConnector}${part}`;
+                const nextPath = parts.join('.');
+
+                if (!this.isRelationAlreadyJoined(qb, nextAlias)) {
+                    qb.leftJoinAndSelect(`${currentAlias}.${part}`, nextAlias);
+                }
+
+                const isTreeParent = relationMetadata.isTreeParent;
+                const isTreeChildren = relationMetadata.isTreeChildren;
+                const isTree = isTreeParent || isTreeChildren;
+
+                if (isTree) {
+                    relationMetadata.inverseEntityMetadata.relations.forEach(subRelation => {
+                        if (subRelation.isEager) {
+                            processRelation(
+                                relationMetadata.inverseEntityMetadata,
+                                !!relationMetadata.inverseEntityMetadata.treeType,
+                                subRelation.propertyPath,
+                                nextAlias,
+                            );
+                        }
+                    });
+                }
+
+                if (nextPath) {
+                    processRelation(
+                        relationMetadata.inverseEntityMetadata,
+                        !!relationMetadata.inverseEntityMetadata.treeType,
+                        nextPath,
+                        nextAlias,
+                    );
+                }
+                processedRelations.add(currentPath);
+            }
+        };
+
+        requestedRelations.forEach(relationPath => {
+            processRelation(metadata, !!metadata.treeType, relationPath, qb.alias);
+        });
+
+        return processedRelations;
+    }
 }

+ 1 - 1
packages/core/src/service/helpers/list-query-builder/parse-filter-params.spec.ts

@@ -66,7 +66,7 @@ describe('parseFilterParams()', () => {
             },
         };
         const result = parseFilterParams(connection as any, Product, filterParams);
-        expect(result[0].clause).toBe('product_translations.name = :arg1');
+        expect(result[0].clause).toBe('product__translations.name = :arg1');
         expect(result[0].parameters).toEqual({ arg1: 'foo' });
         expect(result[1].clause).toBe('product.id = :arg2');
         expect(result[1].parameters).toEqual({ arg2: '123' });

+ 3 - 1
packages/core/src/service/helpers/list-query-builder/parse-filter-params.ts

@@ -67,9 +67,11 @@ export function parseFilterParams<
             if (columns.find(c => c.propertyName === key)) {
                 fieldName = `${alias}.${key}`;
             } else if (translationColumns.find(c => c.propertyName === key)) {
-                const translationsAlias = connection.namingStrategy.eagerJoinRelationAlias(
+                const translationsAlias = connection.namingStrategy.joinTableName(
                     alias,
                     'translations',
+                    '',
+                    '',
                 );
                 fieldName = `${translationsAlias}.${key}`;
             } else if (calculatedColumnExpression) {

+ 3 - 2
packages/core/src/service/helpers/list-query-builder/parse-sort-params.spec.ts

@@ -1,4 +1,5 @@
 import { Type } from '@vendure/common/lib/shared-types';
+import { fail } from 'assert';
 import { DefaultNamingStrategy } from 'typeorm';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 import { RelationMetadata } from 'typeorm/metadata/RelationMetadata';
@@ -73,7 +74,7 @@ describe('parseSortParams()', () => {
         const result = parseSortParams(connection as any, Product, sortParams);
         expect(result).toEqual({
             'product.id': 'ASC',
-            'product_translations.name': 'DESC',
+            'product__translations.name': 'DESC',
         });
     });
 
@@ -111,7 +112,7 @@ describe('parseSortParams()', () => {
             productCustomFields,
         );
         expect(result).toEqual({
-            'product_translations.customFields.shortName': 'ASC',
+            'product__translations.customFields.shortName': 'ASC',
         });
     });
 

+ 8 - 3
packages/core/src/service/helpers/list-query-builder/parse-sort-params.ts

@@ -1,6 +1,7 @@
 import { Type } from '@vendure/common/lib/shared-types';
 import { unique } from '@vendure/common/lib/unique';
-import { Connection, OrderByCondition } from 'typeorm';
+import { OrderByCondition } from 'typeorm';
+import { DataSource } from 'typeorm/data-source/DataSource';
 import { ColumnMetadata } from 'typeorm/metadata/ColumnMetadata';
 
 import { UserInputError } from '../../../common/error/errors';
@@ -17,9 +18,12 @@ import { getCalculatedColumns } from './get-calculated-columns';
  * @param connection
  * @param entity
  * @param sortParams
+ * @param customPropertyMap
+ * @param entityAlias
+ * @param customFields
  */
 export function parseSortParams<T extends VendureEntity>(
-    connection: Connection,
+    connection: DataSource,
     entity: Type<T>,
     sortParams?: NullOptionals<SortParameter<T>> | null,
     customPropertyMap?: { [name: string]: string },
@@ -39,7 +43,8 @@ export function parseSortParams<T extends VendureEntity>(
         if (matchingColumn) {
             output[`${alias}.${matchingColumn.propertyPath}`] = order as any;
         } else if (translationColumns.find(c => c.propertyName === key)) {
-            const translationsAlias = connection.namingStrategy.eagerJoinRelationAlias(alias, 'translations');
+            const translationsAlias = connection.namingStrategy.joinTableName(alias, 'translations', '', '');
+
             const pathParts = [translationsAlias];
             const isLocaleStringCustomField =
                 customFields?.find(f => f.name === key)?.type === 'localeString';

+ 3 - 2
packages/core/src/service/services/collection.service.ts

@@ -170,8 +170,9 @@ export class CollectionService implements OnModuleInit {
         });
 
         if (options?.topLevelOnly === true) {
-            qb.leftJoin('collection.parent', 'parent');
-            qb.andWhere('parent.isRoot = :isRoot', { isRoot: true });
+            qb.innerJoin('collection.parent', 'parent_filter', 'parent_filter.isRoot = :isRoot', {
+                isRoot: true,
+            });
         }
 
         return qb.getManyAndCount().then(async ([collections, totalItems]) => {

+ 50 - 30
yarn.lock

@@ -5217,7 +5217,7 @@
   resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553"
   integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==
 
-"@sqltools/formatter@^1.2.2":
+"@sqltools/formatter@^1.2.5":
   version "1.2.5"
   resolved "https://registry.yarnpkg.com/@sqltools/formatter/-/formatter-1.2.5.tgz#3abc203c79b8c3e90fd6c156a0c62d5403520e12"
   integrity sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==
@@ -6723,7 +6723,7 @@ apollo-upload-client@^17.0.0:
   dependencies:
     extract-files "^11.0.0"
 
-app-root-path@^3.0.0:
+app-root-path@^3.1.0:
   version "3.1.0"
   resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-3.1.0.tgz#5971a2fc12ba170369a7a1ef018c71e6e47c2e86"
   integrity sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==
@@ -8866,7 +8866,7 @@ dataloader@2.2.2, dataloader@^2.2.2:
   resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.2.tgz#216dc509b5abe39d43a9b9d97e6e5e473dfbe3e0"
   integrity sha512-8YnDaaf7N3k/q5HnTJVuzSyLETjoZjVmHc4AeKAzOvKHEFQKcn64OKBfzHYtE9zGjctNM7V9I0MfnUVLpi7M5g==
 
-date-fns@^2.28.0, date-fns@^2.30.0:
+date-fns@^2.30.0:
   version "2.30.0"
   resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.30.0.tgz#f367e644839ff57894ec6ac480de40cae4b0f4d0"
   integrity sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==
@@ -9298,6 +9298,11 @@ dotenv@^16.0.0:
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
   integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
 
+dotenv@^16.0.3:
+  version "16.4.5"
+  resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.4.5.tgz#cdd3b3b604cb327e286b4762e13502f717cb099f"
+  integrity sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==
+
 dotenv@~10.0.0:
   version "10.0.0"
   resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-10.0.0.tgz#3d4227b8fb95f81096cdd2b66653fb2c7085ba81"
@@ -11012,6 +11017,17 @@ glob@^10.2.2, glob@^10.3.10, glob@^10.3.3:
     minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
     path-scurry "^1.10.1"
 
+glob@^10.3.10:
+  version "10.3.10"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-10.3.10.tgz#0351ebb809fd187fe421ab96af83d3a70715df4b"
+  integrity sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==
+  dependencies:
+    foreground-child "^3.1.0"
+    jackspeak "^2.3.5"
+    minimatch "^9.0.1"
+    minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+    path-scurry "^1.10.1"
+
 glob@^7.1.1, glob@^7.1.3, glob@^7.1.4, glob@^7.1.7, glob@^7.2.0:
   version "7.2.3"
   resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
@@ -12519,6 +12535,15 @@ jackspeak@^2.3.5:
   optionalDependencies:
     "@pkgjs/parseargs" "^0.11.0"
 
+jackspeak@^2.3.5:
+  version "2.3.6"
+  resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-2.3.6.tgz#647ecc472238aee4b06ac0e461acc21a8c505ca8"
+  integrity sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==
+  dependencies:
+    "@isaacs/cliui" "^8.0.2"
+  optionalDependencies:
+    "@pkgjs/parseargs" "^0.11.0"
+
 jake@^10.8.5:
   version "10.8.7"
   resolved "https://registry.yarnpkg.com/jake/-/jake-10.8.7.tgz#63a32821177940c33f356e0ba44ff9d34e1c7d8f"
@@ -16934,6 +16959,11 @@ reflect-metadata@^0.1.13, reflect-metadata@^0.1.2:
   resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.14.tgz#24cf721fe60677146bb77eeb0e1f9dece3d65859"
   integrity sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==
 
+reflect-metadata@^0.2.1:
+  version "0.2.1"
+  resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.2.1.tgz#8d5513c0f5ef2b4b9c3865287f3c0940c1f67f74"
+  integrity sha512-i5lLI6iw9AU3Uu4szRNPPEkomnkjRTaVt9hy/bn5g/oSzekBSMeLZblcjP74AW0vBabqERLLIrz+gR8QYR54Tw==
+
 regenerate-unicode-properties@^10.1.0:
   version "10.1.1"
   resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz#6b0e05489d9076b04c436f318d9b067bba459480"
@@ -19036,28 +19066,26 @@ typedarray@^0.0.6:
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
 
-typeorm@0.3.11:
-  version "0.3.11"
-  resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.11.tgz#09b6ab0b0574bf33c1faf7344bab6c363cf28921"
-  integrity sha512-pzdOyWbVuz/z8Ww6gqvBW4nylsM0KLdUCDExr2gR20/x1khGSVxQkjNV/3YqliG90jrWzrknYbYscpk8yxFJVg==
+typeorm@0.3.20:
+  version "0.3.20"
+  resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.3.20.tgz#4b61d737c6fed4e9f63006f88d58a5e54816b7ab"
+  integrity sha512-sJ0T08dV5eoZroaq9uPKBoNcGslHBR4E4y+EBHs//SiGbblGe7IeduP/IH4ddCcj0qp3PHwDwGnuvqEAnKlq/Q==
   dependencies:
-    "@sqltools/formatter" "^1.2.2"
-    app-root-path "^3.0.0"
+    "@sqltools/formatter" "^1.2.5"
+    app-root-path "^3.1.0"
     buffer "^6.0.3"
-    chalk "^4.1.0"
+    chalk "^4.1.2"
     cli-highlight "^2.1.11"
-    date-fns "^2.28.0"
-    debug "^4.3.3"
-    dotenv "^16.0.0"
-    glob "^7.2.0"
-    js-yaml "^4.1.0"
-    mkdirp "^1.0.4"
-    reflect-metadata "^0.1.13"
+    dayjs "^1.11.9"
+    debug "^4.3.4"
+    dotenv "^16.0.3"
+    glob "^10.3.10"
+    mkdirp "^2.1.3"
+    reflect-metadata "^0.2.1"
     sha.js "^2.4.11"
-    tslib "^2.3.1"
-    uuid "^8.3.2"
-    xml2js "^0.4.23"
-    yargs "^17.3.1"
+    tslib "^2.5.0"
+    uuid "^9.0.0"
+    yargs "^17.6.2"
 
 typescript@5.1.6:
   version "5.1.6"
@@ -20027,14 +20055,6 @@ xml2js@0.5.0:
     sax ">=0.6.0"
     xmlbuilder "~11.0.0"
 
-xml2js@^0.4.23:
-  version "0.4.23"
-  resolved "https://registry.yarnpkg.com/xml2js/-/xml2js-0.4.23.tgz#a0c69516752421eb2ac758ee4d4ccf58843eac66"
-  integrity sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==
-  dependencies:
-    sax ">=0.6.0"
-    xmlbuilder "~11.0.0"
-
 xmlbuilder@~11.0.0:
   version "11.0.1"
   resolved "https://registry.yarnpkg.com/xmlbuilder/-/xmlbuilder-11.0.1.tgz#be9bae1c8a046e76b31127726347d0ad7002beb3"
@@ -20157,7 +20177,7 @@ yargs@17.7.1:
     y18n "^5.0.5"
     yargs-parser "^21.1.1"
 
-yargs@17.7.2, yargs@^17.0.0, yargs@^17.2.1, yargs@^17.3.1, yargs@^17.5.1, yargs@^17.6.2, yargs@^17.7.2:
+yargs@17.7.2, yargs@^17.0.0, yargs@^17.2.1, yargs@^17.5.1, yargs@^17.6.2, yargs@^17.7.2:
   version "17.7.2"
   resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269"
   integrity sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==