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

fix(core): Handle nullable relations in EntityHydrator (#2683)

Fixes #2682
Jonas Osburg 1 год назад
Родитель
Сommit
4e1f4084f5

+ 55 - 1
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<UpdateChannelMutation, UpdateChannelMutationVariables>(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<UpdateChannelMutation, UpdateChannelMutationVariables>(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)
+    }
+`;

+ 35 - 0
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<AdditionalConfig>) {
+        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;
     },
 })

+ 2 - 0
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);