Просмотр исходного кода

fix(core): Fix self-referencing relations `Not unique table/alias` (#2740)

Relates to #2738
Eugene Nitsenko 1 год назад
Родитель
Сommit
357ba4947e

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

@@ -144,6 +144,12 @@ export class TestEntity extends VendureEntity implements Translatable, HasCustom
 
     @Column(() => TestEntityCustomFields)
     customFields: TestEntityCustomFields;
+
+    @ManyToOne(() => TestEntity, (type) => type.parent)
+    parent: TestEntity | null;
+
+    @Column('int', { nullable: true })
+    parentId: ID | null;
 }
 
 @Entity()
@@ -188,6 +194,7 @@ export class ListQueryResolver {
             .build(TestEntity, args.options, {
                 ctx,
                 relations: [
+                    'parent',
                     'orderRelation',
                     'orderRelation.customer',
                     'customFields.relation',
@@ -206,7 +213,7 @@ export class ListQueryResolver {
                     }
                 }
                 return {
-                    items: items.map(i => translateDeep(i, ctx.languageCode)),
+                    items: items.map(i => translateDeep(i, ctx.languageCode, ['parent'])),
                     totalItems,
                 };
             });
@@ -215,7 +222,7 @@ export class ListQueryResolver {
     @Query()
     testEntitiesGetMany(@Ctx() ctx: RequestContext, @Args() args: any) {
         return this.listQueryBuilder
-            .build(TestEntity, args.options, { ctx, relations: ['prices'] })
+            .build(TestEntity, args.options, { ctx, relations: ['prices', 'parent'] })
             .getMany()
             .then(items => {
                 for (const item of items) {
@@ -224,7 +231,7 @@ export class ListQueryResolver {
                         item.activePrice = item.prices.find(p => p.channelId === 1)!.price;
                     }
                 }
-                return items.map(i => translateDeep(i, ctx.languageCode));
+                return items.map(i => translateDeep(i, ctx.languageCode, ['parent']));
             });
     }
 }
@@ -274,6 +281,7 @@ const apiExtensions = gql`
         nullableId: ID
         nullableDate: DateTime
         customFields: TestEntityCustomFields!
+        parent: TestEntity
     }
 
     type TestEntityList implements PaginatedList {
@@ -324,9 +332,9 @@ export class ListQueryPlugin implements OnApplicationBootstrap {
     ) {}
 
     async onApplicationBootstrap() {
-        const count = await this.connection.getRepository(TestEntity).count();
+        const count = await this.connection.rawConnection.getRepository(TestEntity).count();
         if (count === 0) {
-            const testEntities = await this.connection.getRepository(TestEntity).save([
+            const testEntities = await this.connection.rawConnection.getRepository(TestEntity).save([
                 new TestEntity({
                     label: 'A',
                     description: 'Lorem ipsum', // 11
@@ -392,6 +400,11 @@ export class ListQueryPlugin implements OnApplicationBootstrap {
                 }),
             ]);
 
+            // test entity with self-referencing relation without tree structure decorator
+            testEntities[0].parent = testEntities[1];
+            testEntities[3].parent = testEntities[1];
+            await this.connection.rawConnection.getRepository(TestEntity).save([testEntities[0], testEntities[3]]);
+
             const translations: any = {
                 A: { [LanguageCode.en]: 'apple', [LanguageCode.de]: 'apfel' },
                 B: { [LanguageCode.en]: 'bike', [LanguageCode.de]: 'fahrrad' },
@@ -408,7 +421,7 @@ export class ListQueryPlugin implements OnApplicationBootstrap {
             };
 
             for (const testEntity of testEntities) {
-                await this.connection.getRepository(TestEntityPrice).save([
+                await this.connection.rawConnection.getRepository(TestEntityPrice).save([
                     new TestEntityPrice({
                         price: testEntity.description.length,
                         channelId: 1,
@@ -424,7 +437,7 @@ export class ListQueryPlugin implements OnApplicationBootstrap {
                 for (const code of [LanguageCode.en, LanguageCode.de]) {
                     const translation = translations[testEntity.label][code];
                     if (translation) {
-                        await this.connection.getRepository(TestEntityTranslation).save(
+                        await this.connection.rawConnection.getRepository(TestEntityTranslation).save(
                             new TestEntityTranslation({
                                 name: translation,
                                 base: testEntity,
@@ -436,13 +449,13 @@ export class ListQueryPlugin implements OnApplicationBootstrap {
 
                 if (nestedData[testEntity.label]) {
                     for (const nestedContent of nestedData[testEntity.label]) {
-                        await this.connection.getRepository(CustomFieldRelationTestEntity).save(
+                        await this.connection.rawConnection.getRepository(CustomFieldRelationTestEntity).save(
                             new CustomFieldRelationTestEntity({
                                 parent: testEntity,
                                 data: nestedContent.data,
                             }),
                         );
-                        await this.connection.getRepository(CustomFieldOtherRelationTestEntity).save(
+                        await this.connection.rawConnection.getRepository(CustomFieldOtherRelationTestEntity).save(
                             new CustomFieldOtherRelationTestEntity({
                                 parent: testEntity,
                                 data: nestedContent.data,
@@ -452,7 +465,7 @@ export class ListQueryPlugin implements OnApplicationBootstrap {
                 }
             }
         } else {
-            const testEntities = await this.connection.getRepository(TestEntity).find();
+            const testEntities = await this.connection.rawConnection.getRepository(TestEntity).find();
             const ctx = await this.requestContextService.create({ apiType: 'admin' });
             const customers = await this.connection.rawConnection.getRepository(Customer).find();
             let i = 0;
@@ -463,7 +476,7 @@ export class ListQueryPlugin implements OnApplicationBootstrap {
                     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
                     const order = await this.orderService.create(ctx, customer.user!.id);
                     testEntity.orderRelation = order;
-                    await this.connection.getRepository(TestEntity).save(testEntity);
+                    await this.connection.rawConnection.getRepository(TestEntity).save(testEntity);
                 } catch (e: any) {
                     Logger.error(e);
                 }

+ 23 - 7
packages/core/e2e/list-query-builder.e2e-spec.ts

@@ -12,6 +12,7 @@ import { ListQueryPlugin } from './fixtures/test-plugins/list-query-plugin';
 import { LanguageCode, SortOrder } from './graphql/generated-e2e-admin-types';
 import { assertThrowsWithMessage } from './utils/assert-throws-with-message';
 import { fixPostgresTimezone } from './utils/fix-pg-timezone';
+import { sortById } from './utils/test-order-utils';
 
 fixPostgresTimezone();
 
@@ -55,14 +56,14 @@ describe('ListQueryBuilder', () => {
 
             expect(testEntities.totalItems).toBe(6);
             expect(getItemLabels(testEntities.items)).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
-            expect(testEntities.items.map((i: any) => i.name)).toEqual([
+            expect(testEntities.items.map((i: any) => i.name)).toEqual(expect.arrayContaining([
                 'apple',
                 'bike',
                 'cake',
                 'dog',
                 'egg',
                 'baum', // if default en lang does not exist, use next available lang
-            ]);
+            ]));
         });
 
         it('all de', async () => {
@@ -76,14 +77,14 @@ describe('ListQueryBuilder', () => {
 
             expect(testEntities.totalItems).toBe(6);
             expect(getItemLabels(testEntities.items)).toEqual(['A', 'B', 'C', 'D', 'E', 'F']);
-            expect(testEntities.items.map((i: any) => i.name)).toEqual([
+            expect(testEntities.items.map((i: any) => i.name)).toEqual(expect.arrayContaining([
                 'apfel',
                 'fahrrad',
                 'kuchen',
                 'hund',
                 'egg', // falls back to en translation when de doesn't exist
                 'baum',
-            ]);
+            ]));
         });
 
         it('take', async () => {
@@ -1207,9 +1208,9 @@ describe('ListQueryBuilder', () => {
     // https://github.com/vendure-ecommerce/vendure/issues/1586
     it('using the getMany() of the resulting QueryBuilder', async () => {
         const { testEntitiesGetMany } = await adminClient.query(GET_ARRAY_LIST, {});
-        expect(testEntitiesGetMany.sort((a: any, b: any) => a.id - b.id).map((x: any) => x.price)).toEqual([
-            11, 9, 22, 14, 13, 33,
-        ]);
+        const actualPrices = testEntitiesGetMany.sort(sortById).map((x: any) => x.price).sort((a: number, b: number) => a - b);
+        const expectedPrices = [11, 9, 22, 14, 13, 33].sort((a, b) => a - b);
+        expect(actualPrices).toEqual(expectedPrices);
     });
 
     // https://github.com/vendure-ecommerce/vendure/issues/1611
@@ -1285,6 +1286,11 @@ describe('ListQueryBuilder', () => {
                     id: 'T_1',
                     label: 'A',
                     name: 'apple',
+                    parent: {
+                        id: 'T_2',
+                        label: 'B',
+                        name: 'bike',
+                    },
                     orderRelation: {
                         customer: {
                             firstName: 'Hayden',
@@ -1297,6 +1303,11 @@ describe('ListQueryBuilder', () => {
                     id: 'T_4',
                     label: 'D',
                     name: 'dog',
+                    parent: {
+                        id: 'T_2',
+                        label: 'B',
+                        name: 'bike',
+                    },
                     orderRelation: {
                         customer: {
                             firstName: 'Hayden',
@@ -1412,6 +1423,11 @@ const GET_LIST_WITH_ORDERS = gql`
                 id
                 label
                 name
+                parent {
+                    id
+                    label
+                    name
+                }
                 orderRelation {
                     id
                     customer {

+ 28 - 10
packages/core/src/service/helpers/list-query-builder/list-query-builder.ts

@@ -660,6 +660,29 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         return qb.expressionMap.joinAttributes.some(ja => ja.alias.name === alias);
     }
 
+    /**
+     * @description
+     * Check if the current entity has one or more self-referencing relations
+     * to determine if it is a tree type or has tree relations.
+     * @param metadata
+     * @private
+     */
+    private isTreeEntityMetadata(metadata: EntityMetadata): boolean {
+        if (metadata.treeType !== undefined) {
+            return true;
+        }
+
+        for (const relation of metadata.relations) {
+            if (relation.isTreeParent || relation.isTreeChildren) {
+                return true;
+            }
+            if (relation.inverseEntityMetadata === metadata) {
+                return true;
+            }
+        }
+        return false
+    }
+
     /**
      * 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
@@ -686,17 +709,16 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         entity: EntityTarget<T>,
         requestedRelations: string[] = [],
     ): Set<string> {
-        const metadata = qb.connection.getMetadata(entity);
+        const sourceMetadata = qb.connection.getMetadata(entity);
+        const isTreeSourceMetadata = this.isTreeEntityMetadata(sourceMetadata)
         const processedRelations = new Set<string>();
 
         const processRelation = (
             currentMetadata: EntityMetadata,
-            currentParentIsTreeType: boolean,
             currentPath: string,
             currentAlias: string,
         ) => {
-            const currentMetadataIsTreeType = metadata.treeType;
-            if (!currentParentIsTreeType && !currentMetadataIsTreeType) {
+            if (!this.isTreeEntityMetadata(currentMetadata) && !isTreeSourceMetadata) {
                 return;
             }
 
@@ -719,16 +741,13 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
                     qb.leftJoinAndSelect(`${currentAlias}.${part}`, nextAlias);
                 }
 
-                const isTreeParent = relationMetadata.isTreeParent;
-                const isTreeChildren = relationMetadata.isTreeChildren;
-                const isTree = isTreeParent || isTreeChildren;
+                const isTree = this.isTreeEntityMetadata(relationMetadata.inverseEntityMetadata);
 
                 if (isTree) {
                     relationMetadata.inverseEntityMetadata.relations.forEach(subRelation => {
                         if (subRelation.isEager) {
                             processRelation(
                                 relationMetadata.inverseEntityMetadata,
-                                !!relationMetadata.inverseEntityMetadata.treeType,
                                 subRelation.propertyPath,
                                 nextAlias,
                             );
@@ -739,7 +758,6 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
                 if (nextPath) {
                     processRelation(
                         relationMetadata.inverseEntityMetadata,
-                        !!relationMetadata.inverseEntityMetadata.treeType,
                         nextPath,
                         nextAlias,
                     );
@@ -749,7 +767,7 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
         };
 
         requestedRelations.forEach(relationPath => {
-            processRelation(metadata, !!metadata.treeType, relationPath, qb.alias);
+            processRelation(sourceMetadata, relationPath, qb.alias);
         });
 
         return processedRelations;