From 96f0410693a252aa9e3153e8605e9530e80f1c6e Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 3 Jun 2024 15:46:56 +0200 Subject: [PATCH] fix(core): Fix nulling of primitive custom fields when updating relation Fixes #2840 --- .../e2e/custom-field-relations.e2e-spec.ts | 202 ++++++++++++++++-- .../custom-field-relation.service.ts | 14 +- .../services/shipping-method.service.ts | 6 +- 3 files changed, 194 insertions(+), 28 deletions(-) diff --git a/packages/core/e2e/custom-field-relations.e2e-spec.ts b/packages/core/e2e/custom-field-relations.e2e-spec.ts index e35153454a..20e54f12ab 100644 --- a/packages/core/e2e/custom-field-relations.e2e-spec.ts +++ b/packages/core/e2e/custom-field-relations.e2e-spec.ts @@ -40,17 +40,7 @@ import { AddItemToOrderMutationVariables } from './graphql/generated-e2e-shop-ty import { ADD_ITEM_TO_ORDER } from './graphql/shop-definitions'; import { sortById } from './utils/test-order-utils'; -// From https://github.com/microsoft/TypeScript/issues/13298#issuecomment-654906323 -// to ensure that we _always_ test all entities which support custom fields -type ValueOf = T[keyof T]; -type NonEmptyArray = [T, ...T[]]; -type MustInclude = [T] extends [ValueOf] ? U : never; -const enumerate = - () => - >(...elements: MustInclude) => - elements; - -const entitiesWithCustomFields = enumerate()( +const entitiesWithCustomFields: Array = [ 'Address', 'Administrator', 'Asset', @@ -77,11 +67,12 @@ const entitiesWithCustomFields = enumerate()( 'TaxRate', 'User', 'Zone', -); +]; const customFieldConfig: CustomFields = {}; for (const entity of entitiesWithCustomFields) { customFieldConfig[entity] = [ + { name: 'primitive', type: 'string', list: false, defaultValue: 'test' }, { name: 'single', type: 'relation', entity: Asset, graphQLType: 'Asset', list: false }, { name: 'multi', type: 'relation', entity: Asset, graphQLType: 'Asset', list: true }, ]; @@ -154,7 +145,7 @@ describe('Custom field relations', () => { } `); - const single = globalSettings.serverConfig.customFieldConfig.Customer[0]; + const single = globalSettings.serverConfig.customFieldConfig.Customer[1]; expect(single.entity).toBe('Asset'); expect(single.scalarFields).toEqual([ 'id', @@ -388,6 +379,7 @@ describe('Custom field relations', () => { const customFieldsSelection = ` customFields { + primitive single { id } @@ -508,6 +500,25 @@ describe('Custom field relations', () => { assertCustomFieldIds(updateCollection.customFields, 'T_2', ['T_3', 'T_4']); }); + + // https://github.com/vendure-ecommerce/vendure/issues/2840 + it('updating custom field relation on Collection does not delete primitive values', async () => { + const { updateCollection } = await adminClient.query(gql` + mutation { + updateCollection( + input: { + id: "${collectionId}" + customFields: { singleId: "T_3" } + } + ) { + id + ${customFieldsSelection} + } + } + `); + expect(updateCollection.customFields.single).toEqual({ id: 'T_3' }); + expect(updateCollection.customFields.primitive).toBe('test'); + }); }); describe('Customer entity', () => { @@ -606,6 +617,25 @@ describe('Custom field relations', () => { `); assertCustomFieldIds(updateFacet.customFields, 'T_2', ['T_3', 'T_4']); }); + + // https://github.com/vendure-ecommerce/vendure/issues/2840 + it('updating custom field relation on Facet does not delete primitive values', async () => { + const { updateFacet } = await adminClient.query(gql` + mutation { + updateFacet( + input: { + id: "${facetId}" + customFields: { singleId: "T_3" } + } + ) { + id + ${customFieldsSelection} + } + } + `); + expect(updateFacet.customFields.single).toEqual({ id: 'T_3' }); + expect(updateFacet.customFields.primitive).toBe('test'); + }); }); describe('FacetValue entity', () => { @@ -647,11 +677,26 @@ describe('Custom field relations', () => { `); assertCustomFieldIds(updateFacetValues[0].customFields, 'T_2', ['T_3', 'T_4']); }); - }); - // describe('Fulfillment entity', () => { - // // Currently no GraphQL API to set customFields on fulfillments - // }); + // https://github.com/vendure-ecommerce/vendure/issues/2840 + it('updating custom field relation on FacetValue does not delete primitive values', async () => { + const { updateFacetValues } = await adminClient.query(gql` + mutation { + updateFacetValues( + input: { + id: "${facetValueId}" + customFields: { singleId: "T_3" } + } + ) { + id + ${customFieldsSelection} + } + } + `); + expect(updateFacetValues[0].customFields.single).toEqual({ id: 'T_3' }); + expect(updateFacetValues[0].customFields.primitive).toBe('test'); + }); + }); describe('GlobalSettings entity', () => { it('admin updateGlobalSettings', async () => { @@ -807,6 +852,25 @@ describe('Custom field relations', () => { assertCustomFieldIds(updateProduct.customFields, 'T_2', ['T_3', 'T_4']); }); + // https://github.com/vendure-ecommerce/vendure/issues/2840 + it('updating custom field relation on Product does not delete primitive values', async () => { + const { updateProduct } = await adminClient.query(gql` + mutation { + updateProduct( + input: { + id: "${productId}" + customFields: { singleId: "T_3" } + } + ) { + id + ${customFieldsSelection} + } + } + `); + expect(updateProduct.customFields.single).toEqual({ id: 'T_3' }); + expect(updateProduct.customFields.primitive).toBe('test'); + }); + let productVariantId: string; it('admin createProductVariant', async () => { const { createProductVariants } = await adminClient.query(gql` @@ -846,6 +910,25 @@ describe('Custom field relations', () => { assertCustomFieldIds(updateProductVariants[0].customFields, 'T_2', ['T_3', 'T_4']); }); + // https://github.com/vendure-ecommerce/vendure/issues/2840 + it('updating custom field relation on ProductVariant does not delete primitive values', async () => { + const { updateProductVariants } = await adminClient.query(gql` + mutation { + updateProductVariants( + input: [{ + id: "${productVariantId}" + customFields: { singleId: "T_3" } + }] + ) { + id + ${customFieldsSelection} + } + } + `); + expect(updateProductVariants[0].customFields.single).toEqual({ id: 'T_3' }); + expect(updateProductVariants[0].customFields.primitive).toBe('test'); + }); + describe('issue 1664', () => { // https://github.com/vendure-ecommerce/vendure/issues/1664 it('successfully gets product by id with eager-loading custom field relation', async () => { @@ -1013,6 +1096,25 @@ describe('Custom field relations', () => { assertCustomFieldIds(updateProductOptionGroup.customFields, 'T_2', ['T_3', 'T_4']); }); + // https://github.com/vendure-ecommerce/vendure/issues/2840 + it('updating custom field relation on ProductOptionGroup does not delete primitive values', async () => { + const { updateProductOptionGroup } = await adminClient.query(gql` + mutation { + updateProductOptionGroup( + input: { + id: "${productOptionGroupId}" + customFields: { singleId: "T_3" } + } + ) { + id + ${customFieldsSelection} + } + } + `); + expect(updateProductOptionGroup.customFields.single).toEqual({ id: 'T_3' }); + expect(updateProductOptionGroup.customFields.primitive).toBe('test'); + }); + let productOptionId: string; it('admin createProductOption', async () => { const { createProductOption } = await adminClient.query(gql` @@ -1051,11 +1153,26 @@ describe('Custom field relations', () => { `); assertCustomFieldIds(updateProductOption.customFields, 'T_2', ['T_3', 'T_4']); }); - }); - // describe('User entity', () => { - // // Currently no GraphQL API to set User custom fields - // }); + // https://github.com/vendure-ecommerce/vendure/issues/2840 + it('updating custom field relation on ProductOption does not delete primitive values', async () => { + const { updateProductOption } = await adminClient.query(gql` + mutation { + updateProductOption( + input: { + id: "${productOptionId}" + customFields: { singleId: "T_3" } + } + ) { + id + ${customFieldsSelection} + } + } + `); + expect(updateProductOption.customFields.single).toEqual({ id: 'T_3' }); + expect(updateProductOption.customFields.primitive).toBe('test'); + }); + }); describe('ShippingMethod entity', () => { let shippingMethodId: string; @@ -1112,6 +1229,26 @@ describe('Custom field relations', () => { assertCustomFieldIds(updateShippingMethod.customFields, 'T_2', ['T_3', 'T_4']); }); + // https://github.com/vendure-ecommerce/vendure/issues/2840 + it('updating custom field relation on ShippingMethod does not delete primitive values', async () => { + const { updateShippingMethod } = await adminClient.query(gql` + mutation { + updateShippingMethod( + input: { + id: "${shippingMethodId}" + translations: [] + customFields: { singleId: "T_3" } + } + ) { + id + ${customFieldsSelection} + } + } + `); + expect(updateShippingMethod.customFields.single).toEqual({ id: 'T_3' }); + expect(updateShippingMethod.customFields.primitive).toBe('test'); + }); + it('shop eligibleShippingMethods (ShippingMethodQuote)', async () => { const { eligibleShippingMethods } = await shopClient.query(gql` query { @@ -1127,7 +1264,7 @@ describe('Custom field relations', () => { const testShippingMethodQuote = eligibleShippingMethods.find( (quote: any) => quote.code === 'test', ); - assertCustomFieldIds(testShippingMethodQuote.customFields, 'T_2', ['T_3', 'T_4']); + assertCustomFieldIds(testShippingMethodQuote.customFields, 'T_3', ['T_3', 'T_4']); }); }); @@ -1175,6 +1312,25 @@ describe('Custom field relations', () => { assertCustomFieldIds(updatePaymentMethod.customFields, 'T_2', ['T_3', 'T_4']); }); + // https://github.com/vendure-ecommerce/vendure/issues/2840 + it('updating custom field relation on PaymentMethod does not delete primitive values', async () => { + const { updatePaymentMethod } = await adminClient.query(gql` + mutation { + updatePaymentMethod( + input: { + id: "${paymentMethodId}" + customFields: { singleId: "T_3" } + } + ) { + id + ${customFieldsSelection} + } + } + `); + expect(updatePaymentMethod.customFields.single).toEqual({ id: 'T_3' }); + expect(updatePaymentMethod.customFields.primitive).toBe('test'); + }); + it('shop eligiblePaymentMethods (PaymentMethodQuote)', async () => { const { eligiblePaymentMethods } = await shopClient.query(gql` query { @@ -1186,7 +1342,7 @@ describe('Custom field relations', () => { } } `); - assertCustomFieldIds(eligiblePaymentMethods[0].customFields, 'T_2', ['T_3', 'T_4']); + assertCustomFieldIds(eligiblePaymentMethods[0].customFields, 'T_3', ['T_3', 'T_4']); }); }); diff --git a/packages/core/src/service/helpers/custom-field-relation/custom-field-relation.service.ts b/packages/core/src/service/helpers/custom-field-relation/custom-field-relation.service.ts index f02f824f16..0ba411fa1b 100644 --- a/packages/core/src/service/helpers/custom-field-relation/custom-field-relation.service.ts +++ b/packages/core/src/service/helpers/custom-field-relation/custom-field-relation.service.ts @@ -17,7 +17,10 @@ import { VendureEntity } from '../../../entity/base/base.entity'; @Injectable() export class CustomFieldRelationService { - constructor(private connection: TransactionalConnection, private configService: ConfigService) {} + constructor( + private connection: TransactionalConnection, + private configService: ConfigService, + ) {} /** * @description @@ -54,7 +57,14 @@ export class CustomFieldRelationService { .findOne({ where: { id: idOrIds } }); } if (relations !== undefined) { - entity.customFields = { ...entity.customFields, [field.name]: relations }; + const entityWithCustomFields = await this.connection + .getRepository(ctx, entityType) + .findOne({ where: { id: entity.id } as any, loadEagerRelations: false }); + entity.customFields = { + ...entity.customFields, + ...entityWithCustomFields?.customFields, + [field.name]: relations, + }; await this.connection .getRepository(ctx, entityType) .save(pick(entity, ['id', 'customFields']) as any, { reload: false }); diff --git a/packages/core/src/service/services/shipping-method.service.ts b/packages/core/src/service/services/shipping-method.service.ts index a9b6dc151b..0eb7d82c5c 100644 --- a/packages/core/src/service/services/shipping-method.service.ts +++ b/packages/core/src/service/services/shipping-method.service.ts @@ -168,15 +168,15 @@ export class ShippingMethodService { input.fulfillmentHandler, ); } - await this.connection - .getRepository(ctx, ShippingMethod) - .save(updatedShippingMethod, { reload: false }); await this.customFieldRelationService.updateRelations( ctx, ShippingMethod, input, updatedShippingMethod, ); + await this.connection + .getRepository(ctx, ShippingMethod) + .save(updatedShippingMethod, { reload: false }); await this.eventBus.publish(new ShippingMethodEvent(ctx, shippingMethod, 'updated', input)); return assertFound(this.findOne(ctx, shippingMethod.id)); }