Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 59 additions & 1 deletion packages/core/e2e/custom-field-relations.e2e-spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
assertFound,
Asset,
Collection,
Country,
Expand All @@ -11,22 +12,29 @@ 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';
import { testConfig, TEST_SETUP_TIMEOUT_MS } from '../../../e2e-common/test-config';

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';
Expand Down Expand Up @@ -107,7 +115,7 @@ const customConfig = mergeConfig(testConfig(), {
timezone: 'Z',
},
customFields: customFieldConfig,
plugins: [TestPlugin1636_1664],
plugins: [TestPlugin1636_1664, WithCustomEntity],
});

describe('Custom field relations', () => {
Expand Down Expand Up @@ -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 changes: 58 additions & 0 deletions packages/core/e2e/fixtures/test-plugins/with-custom-entity.ts
Original file line number Diff line number Diff line change
@@ -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 {}
9 changes: 8 additions & 1 deletion packages/core/src/config/custom-field/custom-field-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
Expand Down
8 changes: 6 additions & 2 deletions packages/core/src/entity/register-custom-entity-fields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down