Jelajahi Sumber

fix(core): Fix EntityHydrator error on long table names (#2959)

Fixes #2899
Jonas Osburg 1 tahun lalu
induk
melakukan
bcfcf7d67c

+ 47 - 2
packages/core/e2e/entity-hydrator.e2e-spec.ts

@@ -22,7 +22,11 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 import { initialData } from '../../../e2e-common/e2e-initial-data';
 import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';
 
-import { AdditionalConfig, HydrationTestPlugin } from './fixtures/test-plugins/hydration-test-plugin';
+import {
+    AdditionalConfig,
+    HydrationTestPlugin,
+    TreeEntity,
+} from './fixtures/test-plugins/hydration-test-plugin';
 import { UpdateChannelMutation, UpdateChannelMutationVariables } from './graphql/generated-e2e-admin-types';
 import {
     AddItemToOrderDocument,
@@ -54,11 +58,17 @@ describe('Entity hydration', () => {
 
         const connection = server.app.get(TransactionalConnection).rawConnection;
         const asset = await connection.getRepository(Asset).findOne({ where: {} });
-        await connection.getRepository(AdditionalConfig).save(
+        const additionalConfig = await connection.getRepository(AdditionalConfig).save(
             new AdditionalConfig({
                 backgroundImage: asset,
             }),
         );
+        const parent = await connection
+            .getRepository(TreeEntity)
+            .save(new TreeEntity({ additionalConfig, image1: asset, image2: asset }));
+        await connection
+            .getRepository(TreeEntity)
+            .save(new TreeEntity({ parent, image1: asset, image2: asset }));
     }, TEST_SETUP_TIMEOUT_MS);
 
     afterAll(async () => {
@@ -382,6 +392,35 @@ describe('Entity hydration', () => {
             expect(line.productVariantId).toBe(line.productVariant.id);
         }
     });
+
+    /*
+     * Postgres has a character limit for alias names which can cause issues when joining
+     * multiple aliases with the same prefix
+     * https://github.com/vendure-ecommerce/vendure/issues/2899
+     */
+    it('Hydrates properties with very long names', async () => {
+        await adminClient.query<UpdateChannelMutation, UpdateChannelMutationVariables>(UPDATE_CHANNEL, {
+            input: {
+                id: 'T_1',
+                customFields: {
+                    additionalConfigId: 'T_1',
+                },
+            },
+        });
+
+        const { hydrateChannelWithVeryLongPropertyName } = await adminClient.query<{
+            hydrateChannelWithVeryLongPropertyName: any;
+        }>(GET_HYDRATED_CHANNEL_LONG_ALIAS, {
+            id: 'T_1',
+        });
+
+        const entity = (
+            hydrateChannelWithVeryLongPropertyName.customFields.additionalConfig as AdditionalConfig
+        ).treeEntity[0];
+        const child = entity.childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself[0];
+        expect(child.image1).toBeDefined();
+        expect(child.image2).toBeDefined();
+    });
 });
 
 function getVariantWithName(product: Product, name: string) {
@@ -432,3 +471,9 @@ const GET_HYDRATED_CHANNEL_NESTED = gql`
         hydrateChannelWithNestedRelation(id: $id)
     }
 `;
+
+const GET_HYDRATED_CHANNEL_LONG_ALIAS = gql`
+    query GetHydratedChannelNested($id: ID!) {
+        hydrateChannelWithVeryLongPropertyName(id: $id)
+    }
+`;

+ 58 - 2
packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts

@@ -19,7 +19,7 @@ import {
     VendurePlugin,
 } from '@vendure/core';
 import gql from 'graphql-tag';
-import { Entity, ManyToOne } from 'typeorm';
+import { Entity, ManyToOne, OneToMany } from 'typeorm';
 
 @Resolver()
 export class TestAdminPluginResolver {
@@ -141,6 +141,30 @@ export class TestAdminPluginResolver {
         });
         return channel;
     }
+
+    @Query()
+    async hydrateChannelWithVeryLongPropertyName(@Ctx() ctx: RequestContext, @Args() args: { id: ID }) {
+        const channel = await this.channelService.findOne(ctx, args.id);
+        await this.entityHydrator.hydrate(ctx, channel!, {
+            relations: ['customFields.additionalConfig.treeEntity'],
+        });
+
+        // Make sure we start on a tree entity to make use of tree-relations-qb-joiner.ts
+        await Promise.all(
+            ((channel!.customFields as any).additionalConfig.treeEntity as TreeEntity[]).map(treeEntity =>
+                this.entityHydrator.hydrate(ctx, treeEntity, {
+                    relations: [
+                        'childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself',
+                        'childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself',
+                        'childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself.image1',
+                        'childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself.image2',
+                    ],
+                }),
+            ),
+        );
+
+        return channel;
+    }
 }
 
 @Entity()
@@ -151,11 +175,42 @@ export class AdditionalConfig extends VendureEntity {
 
     @ManyToOne(() => Asset, { onDelete: 'SET NULL', nullable: true })
     backgroundImage: Asset;
+
+    @OneToMany(() => TreeEntity, entity => entity.additionalConfig)
+    treeEntity: TreeEntity[];
+}
+
+@Entity()
+export class TreeEntity extends VendureEntity {
+    constructor(input?: DeepPartial<TreeEntity>) {
+        super(input);
+    }
+
+    @ManyToOne(() => AdditionalConfig, e => e.treeEntity, { nullable: true })
+    additionalConfig: AdditionalConfig;
+
+    @OneToMany(() => TreeEntity, entity => entity.parent)
+    childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself: TreeEntity[];
+
+    @ManyToOne(
+        () => TreeEntity,
+        entity => entity.childrenPropertyWithAVeryLongNameThatExceedsPostgresLimitsEasilyByItself,
+        {
+            nullable: true,
+        },
+    )
+    parent: TreeEntity;
+
+    @ManyToOne(() => Asset)
+    image1: Asset;
+
+    @ManyToOne(() => Asset)
+    image2: Asset;
 }
 
 @VendurePlugin({
     imports: [PluginCommonModule],
-    entities: [AdditionalConfig],
+    entities: [AdditionalConfig, TreeEntity],
     adminApiExtensions: {
         resolvers: [TestAdminPluginResolver],
         schema: gql`
@@ -168,6 +223,7 @@ export class AdditionalConfig extends VendureEntity {
                 hydrateOrderReturnQuantities(id: ID!): JSON
                 hydrateChannel(id: ID!): JSON
                 hydrateChannelWithNestedRelation(id: ID!): JSON
+                hydrateChannelWithVeryLongPropertyName(id: ID!): JSON
             }
         `,
     },

+ 7 - 2
packages/core/src/service/helpers/utils/tree-relations-qb-joiner.ts

@@ -1,6 +1,6 @@
 import { EntityMetadata, FindOneOptions, SelectQueryBuilder } from 'typeorm';
 import { EntityTarget } from 'typeorm/common/EntityTarget';
-import { FindOptionsRelationByString, FindOptionsRelations } from 'typeorm/find-options/FindOptionsRelations';
+import { DriverUtils } from 'typeorm/driver/DriverUtils';
 
 import { findOptionsObjectToArray } from '../../../connection/find-options-object-to-array';
 import { VendureEntity } from '../../../entity';
@@ -108,7 +108,12 @@ export function joinTreeRelationsDynamically<T extends VendureEntity>(
         if (relationMetadata.isEager) {
             joinConnector = '__';
         }
-        const nextAlias = `${currentAlias}${joinConnector}${part.replace(/\./g, '_')}`;
+        const nextAlias = DriverUtils.buildAlias(
+            qb.connection.driver,
+            { shorten: false },
+            currentAlias,
+            part.replace(/\./g, '_'),
+        );
         const nextPath = parts.join('.');
         const fullPath = [...(parentPath || []), part].join('.');
         if (!qb.expressionMap.joinAttributes.some(ja => ja.alias.name === nextAlias)) {