diff --git a/packages/core/e2e/entity-hydrator.e2e-spec.ts b/packages/core/e2e/entity-hydrator.e2e-spec.ts index e6ff9034ab..bdea1bbb95 100644 --- a/packages/core/e2e/entity-hydrator.e2e-spec.ts +++ b/packages/core/e2e/entity-hydrator.e2e-spec.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import { + Asset, ChannelService, EntityHydrator, mergeConfig, @@ -21,7 +22,7 @@ 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 { HydrationTestPlugin } from './fixtures/test-plugins/hydration-test-plugin'; +import { AdditionalConfig, HydrationTestPlugin } from './fixtures/test-plugins/hydration-test-plugin'; import { UpdateChannelMutation, UpdateChannelMutationVariables } from './graphql/generated-e2e-admin-types'; import { AddItemToOrderDocument, @@ -50,6 +51,14 @@ describe('Entity hydration', () => { customerCount: 2, }); await adminClient.asSuperAdmin(); + + const connection = server.app.get(TransactionalConnection).rawConnection; + const asset = await connection.getRepository(Asset).findOne({ where: {} }); + await connection.getRepository(AdditionalConfig).save( + new AdditionalConfig({ + backgroundImage: asset, + }), + ); }, TEST_SETUP_TIMEOUT_MS); afterAll(async () => { @@ -240,6 +249,45 @@ describe('Entity hydration', () => { expect(hydrateChannel.customFields.thumb.id).toBe('T_2'); }); + it('hydrates a nested custom field', async () => { + await adminClient.query(UPDATE_CHANNEL, { + input: { + id: 'T_1', + customFields: { + additionalConfigId: 'T_1', + }, + }, + }); + + const { hydrateChannelWithNestedRelation } = await adminClient.query<{ + hydrateChannelWithNestedRelation: any; + }>(GET_HYDRATED_CHANNEL_NESTED, { + id: 'T_1', + }); + + expect(hydrateChannelWithNestedRelation.customFields.additionalConfig).toBeDefined(); + }); + + // https://github.com/vendure-ecommerce/vendure/issues/2682 + it('hydrates a nested custom field where the first level is null', async () => { + await adminClient.query(UPDATE_CHANNEL, { + input: { + id: 'T_1', + customFields: { + additionalConfigId: null, + }, + }, + }); + + const { hydrateChannelWithNestedRelation } = await adminClient.query<{ + hydrateChannelWithNestedRelation: any; + }>(GET_HYDRATED_CHANNEL_NESTED, { + id: 'T_1', + }); + + expect(hydrateChannelWithNestedRelation.customFields.additionalConfig).toBeNull(); + }); + // https://github.com/vendure-ecommerce/vendure/issues/2013 describe('hydration of OrderLine ProductVariantPrices', () => { let order: Order | undefined; @@ -378,3 +426,9 @@ const GET_HYDRATED_CHANNEL = gql` hydrateChannel(id: $id) } `; + +const GET_HYDRATED_CHANNEL_NESTED = gql` + query GetHydratedChannelNested($id: ID!) { + hydrateChannelWithNestedRelation(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 7ac97dd6b1..270fbaabc5 100644 --- a/packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts +++ b/packages/core/e2e/fixtures/test-plugins/hydration-test-plugin.ts @@ -4,6 +4,7 @@ import { Asset, ChannelService, Ctx, + DeepPartial, EntityHydrator, ID, LanguageCode, @@ -14,9 +15,11 @@ import { ProductVariantService, RequestContext, TransactionalConnection, + VendureEntity, VendurePlugin, } from '@vendure/core'; import gql from 'graphql-tag'; +import { Entity, ManyToOne } from 'typeorm'; @Resolver() export class TestAdminPluginResolver { @@ -125,10 +128,34 @@ export class TestAdminPluginResolver { }); return channel; } + + @Query() + async hydrateChannelWithNestedRelation(@Ctx() ctx: RequestContext, @Args() args: { id: ID }) { + const channel = await this.channelService.findOne(ctx, args.id); + await this.entityHydrator.hydrate(ctx, channel!, { + relations: [ + 'customFields.thumb', + 'customFields.additionalConfig', + 'customFields.additionalConfig.backgroundImage', + ], + }); + return channel; + } +} + +@Entity() +export class AdditionalConfig extends VendureEntity { + constructor(input?: DeepPartial) { + super(input); + } + + @ManyToOne(() => Asset, { onDelete: 'SET NULL', nullable: true }) + backgroundImage: Asset; } @VendurePlugin({ imports: [PluginCommonModule], + entities: [AdditionalConfig], adminApiExtensions: { resolvers: [TestAdminPluginResolver], schema: gql` @@ -140,11 +167,19 @@ export class TestAdminPluginResolver { hydrateOrder(id: ID!): JSON hydrateOrderReturnQuantities(id: ID!): JSON hydrateChannel(id: ID!): JSON + hydrateChannelWithNestedRelation(id: ID!): JSON } `, }, configuration: config => { config.customFields.Channel.push({ name: 'thumb', type: 'relation', entity: Asset, nullable: true }); + config.customFields.Channel.push({ + name: 'additionalConfig', + type: 'relation', + entity: AdditionalConfig, + graphQLType: 'JSON', + nullable: true, + }); return config; }, }) diff --git a/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts b/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts index 7d2497a1c7..a035bb19e1 100644 --- a/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts +++ b/packages/core/src/service/helpers/entity-hydrator/entity-hydrator.service.ts @@ -252,6 +252,8 @@ export class EntityHydrator { visit(item, parts.slice()); } } + } else if (target === null) { + result.push(target); } else { if (parts.length === 0) { result.push(target);