From bcfcf7d67cbb95d3b1c17c217ee7bc85f4e26238 Mon Sep 17 00:00:00 2001 From: Jonas Osburg Date: Wed, 14 Aug 2024 02:58:47 +0200 Subject: [PATCH] fix(core): Fix EntityHydrator error on long table names (#2959) Fixes #2899 --- packages/core/e2e/entity-hydrator.e2e-spec.ts | 49 ++++++++++++++- .../test-plugins/hydration-test-plugin.ts | 60 ++++++++++++++++++- .../helpers/utils/tree-relations-qb-joiner.ts | 9 ++- 3 files changed, 112 insertions(+), 6 deletions(-) diff --git a/packages/core/e2e/entity-hydrator.e2e-spec.ts b/packages/core/e2e/entity-hydrator.e2e-spec.ts index bdea1bbb95..2f79df542a 100644 --- a/packages/core/e2e/entity-hydrator.e2e-spec.ts +++ b/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(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) + } +`; diff --git a/packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts b/packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts index 270fbaabc5..eb21b7710a 100644 --- a/packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts +++ b/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) { + 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 } `, }, diff --git a/packages/core/src/service/helpers/utils/tree-relations-qb-joiner.ts b/packages/core/src/service/helpers/utils/tree-relations-qb-joiner.ts index 0d372b7e12..db4e6dbb07 100644 --- a/packages/core/src/service/helpers/utils/tree-relations-qb-joiner.ts +++ b/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( 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)) {