소스 검색

feat(core): Support bi-directional relations in customFields (#2365)

Co-authored-by: Alexis Kobrinski <alexis.kobrinski@medicalvalues.de>
Ⓐlexis Kobrinski 2 년 전
부모
커밋
0313ce54b2

+ 59 - 1
packages/core/e2e/custom-field-relations.e2e-spec.ts

@@ -1,4 +1,5 @@
 import {
+    assertFound,
     Asset,
     Collection,
     Country,
@@ -11,15 +12,21 @@ import {
     LogLevel,
     manualFulfillmentHandler,
     mergeConfig,
+    PluginCommonModule,
     Product,
     ProductOption,
     ProductOptionGroup,
     ProductVariant,
+    RequestContext,
     ShippingMethod,
+    TransactionalConnection,
+    VendureEntity,
+    VendurePlugin,
 } from '@vendure/core';
 import { createTestEnvironment } from '@vendure/testing';
 import gql from 'graphql-tag';
 import path from 'path';
+import { Repository } from 'typeorm';
 import { afterAll, beforeAll, describe, expect, it } from 'vitest';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
@@ -27,6 +34,7 @@ import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-conf
 
 import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
 import { TestPlugin1636_1664 } from './fixtures/test-plugins/issue-1636-1664/issue-1636-1664-plugin';
+import { TestCustomEntity, WithCustomEntity } from './fixtures/test-plugins/with-custom-entity';
 import { AddItemToOrderMutationVariables } from './graphql/generated-e2e-shop-types';
 import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions';
 import { sortById } from './utils/test-order-utils';
@@ -107,7 +115,7 @@ const customConfig = mergeConfig(testConfig(), {
         timezone: 'Z',
     },
     customFields: customFieldConfig,
-    plugins: [TestPlugin1636_1664],
+    plugins: [TestPlugin1636_1664, WithCustomEntity],
 });
 
 describe('Custom field relations', () => {
@@ -1180,4 +1188,54 @@ describe('Custom field relations', () => {
         expect(updateCustomerAddress.customFields.single).toEqual(null);
         expect(updateCustomerAddress.customFields.multi).toEqual([{ id: 'T_1' }]);
     });
+
+    describe('bi-direction relations', () => {
+        let customEntityRepository: Repository<TestCustomEntity>;
+        let customEntity: TestCustomEntity;
+        let collectionIdInternal: number;
+
+        beforeAll(async () => {
+            customEntityRepository = server.app
+                .get(TransactionalConnection)
+                .getRepository(RequestContext.empty(), TestCustomEntity);
+
+            const customEntityId = (await customEntityRepository.save({})).id;
+
+            const { createCollection } = await adminClient.query(gql`
+                    mutation {
+                        createCollection(
+                            input: {
+                                translations: [
+                                    { languageCode: en, name: "Test", description: "test", slug: "test" }
+                                ]
+                                filters: []
+                                customFields: { customEntityListIds: [${customEntityId}] customEntityId: ${customEntityId} }
+                            }
+                        ) {
+                            id
+                        }
+                    }
+                `);
+            collectionIdInternal = parseInt(createCollection.id.replace('T_', ''), 10);
+
+            customEntity = await assertFound(
+                customEntityRepository.findOne({
+                    where: { id: customEntityId },
+                    relations: ['customEntityListInverse', 'customEntityInverse'],
+                }),
+            );
+        });
+
+        it('can create inverse relation for list=false', () => {
+            expect(customEntity.customEntityInverse).toEqual([
+                expect.objectContaining({ id: collectionIdInternal }),
+            ]);
+        });
+
+        it('can create inverse relation for list=true', () => {
+            expect(customEntity.customEntityListInverse).toEqual([
+                expect.objectContaining({ id: collectionIdInternal }),
+            ]);
+        });
+    });
 });

+ 58 - 0
packages/core/e2e/fixtures/test-plugins/with-custom-entity.ts

@@ -0,0 +1,58 @@
+import { Collection, PluginCommonModule, VendureEntity, VendurePlugin } from '@vendure/core';
+import gql from 'graphql-tag';
+import { DeepPartial, Entity, ManyToMany, OneToMany } from 'typeorm';
+
+@Entity()
+export class TestCustomEntity extends VendureEntity {
+    constructor(input?: DeepPartial<TestCustomEntity>) {
+        super(input);
+    }
+
+    @ManyToMany(() => Collection, x => (x as any).customFields?.customEntityList, {
+        eager: true,
+    })
+    customEntityListInverse: Collection[];
+
+    @OneToMany(() => Collection, (a: Collection) => (a.customFields as any).customEntity)
+    customEntityInverse: Collection[];
+}
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    entities: [TestCustomEntity],
+    adminApiExtensions: {
+        schema: gql`
+            type TestCustomEntity {
+                id: ID!
+            }
+        `,
+    },
+    configuration: config => {
+        config.customFields = {
+            ...(config.customFields ?? {}),
+            Collection: [
+                ...(config.customFields?.Collection ?? []),
+                {
+                    name: 'customEntity',
+                    type: 'relation',
+                    entity: TestCustomEntity,
+                    list: false,
+                    public: false,
+                    inverseSide: (t: TestCustomEntity) => t.customEntityInverse,
+                    graphQLType: 'TestCustomEntity',
+                },
+                {
+                    name: 'customEntityList',
+                    type: 'relation',
+                    entity: TestCustomEntity,
+                    list: true,
+                    public: false,
+                    inverseSide: (t: TestCustomEntity) => t.customEntityListInverse,
+                    graphQLType: 'TestCustomEntity',
+                },
+            ],
+        };
+        return config;
+    },
+})
+export class WithCustomEntity {}

+ 8 - 1
packages/core/src/config/custom-field/custom-field-types.ts

@@ -96,7 +96,12 @@ export type DateTimeCustomFieldConfig = TypedCustomFieldConfig<'datetime', Graph
 export type RelationCustomFieldConfig = TypedCustomFieldConfig<
     'relation',
     Omit<GraphQLRelationCustomFieldConfig, 'entity' | 'scalarFields'>
-> & { entity: Type<VendureEntity>; graphQLType?: string; eager?: boolean };
+> & {
+    entity: Type<VendureEntity>;
+    graphQLType?: string;
+    eager?: boolean;
+    inverseSide: string | ((object: VendureEntity) => any);
+};
 
 /**
  * @description
@@ -180,6 +185,8 @@ export type CustomFieldConfig =
  *
  * * `entity: VendureEntity`: The entity which this custom field is referencing
  * * `eager?: boolean`: Whether to [eagerly load](https://typeorm.io/#/eager-and-lazy-relations) the relation. Defaults to false.
+ * * `inverseSide?: inverseSide: string | ((object: any) => any`: The inverse side for
+ *     [bi-directional relations](https://typeorm.io/many-to-many-relations#bi-directional-relations)
  * * `graphQLType?: string`: The name of the GraphQL type that corresponds to the entity.
  *     Can be omitted if it is the same, which is usually the case.
  *

+ 6 - 2
packages/core/src/entity/register-custom-entity-fields.ts

@@ -80,10 +80,14 @@ function registerCustomFieldsForEntity(
             const registerColumn = () => {
                 if (customField.type === 'relation') {
                     if (customField.list) {
-                        ManyToMany(type => customField.entity, { eager: customField.eager })(instance, name);
+                        ManyToMany(type => customField.entity, customField.inverseSide, {
+                            eager: customField.eager,
+                        })(instance, name);
                         JoinTable()(instance, name);
                     } else {
-                        ManyToOne(type => customField.entity, { eager: customField.eager })(instance, name);
+                        ManyToOne(type => customField.entity, customField.inverseSide, {
+                            eager: customField.eager,
+                        })(instance, name);
                         JoinColumn()(instance, name);
                     }
                 } else {