diff --git a/packages/core/e2e/custom-field-relations.e2e-spec.ts b/packages/core/e2e/custom-field-relations.e2e-spec.ts index 9d6fc4845b..c92b8ca3e6 100644 --- a/packages/core/e2e/custom-field-relations.e2e-spec.ts +++ b/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; + 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 }), + ]); + }); + }); }); diff --git a/packages/core/e2e/fixtures/test-plugins/with-custom-entity.ts b/packages/core/e2e/fixtures/test-plugins/with-custom-entity.ts new file mode 100644 index 0000000000..67a44b0c66 --- /dev/null +++ b/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) { + 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 {} diff --git a/packages/core/src/config/custom-field/custom-field-types.ts b/packages/core/src/config/custom-field/custom-field-types.ts index 06642f5e7d..9f3b15b968 100644 --- a/packages/core/src/config/custom-field/custom-field-types.ts +++ b/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 -> & { entity: Type; graphQLType?: string; eager?: boolean }; +> & { + entity: Type; + 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. * diff --git a/packages/core/src/entity/register-custom-entity-fields.ts b/packages/core/src/entity/register-custom-entity-fields.ts index 81c28dacc9..6a869eb0f9 100644 --- a/packages/core/src/entity/register-custom-entity-fields.ts +++ b/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 {