Prechádzať zdrojové kódy

refactor(server): Refactor the way translatable entities are saved

Michael Bromley 7 rokov pred
rodič
commit
0eae72192a
29 zmenil súbory, kde vykonal 219 pridanie a 226 odobranie
  1. 1 1
      server/src/common/types/locale-types.ts
  2. 0 37
      server/src/service/helpers/create-translatable.ts
  3. 79 0
      server/src/service/helpers/translatable-saver/translatable-saver.ts
  4. 8 8
      server/src/service/helpers/translatable-saver/translation-differ.spec.ts
  5. 4 4
      server/src/service/helpers/translatable-saver/translation-differ.ts
  6. 0 14
      server/src/service/helpers/translation-updater.mock.ts
  7. 0 16
      server/src/service/helpers/translation-updater.service.ts
  8. 0 43
      server/src/service/helpers/update-translatable.ts
  9. 2 2
      server/src/service/helpers/utils/get-entity-or-throw.ts
  10. 1 1
      server/src/service/helpers/utils/patch-entity.ts
  11. 7 7
      server/src/service/helpers/utils/translate-entity.spec.ts
  12. 3 3
      server/src/service/helpers/utils/translate-entity.ts
  13. 2 2
      server/src/service/service.module.ts
  14. 1 1
      server/src/service/services/administrator.service.ts
  15. 2 2
      server/src/service/services/channel.service.ts
  16. 1 1
      server/src/service/services/country.service.ts
  17. 2 2
      server/src/service/services/customer-group.service.ts
  18. 14 9
      server/src/service/services/facet-value.service.ts
  19. 13 9
      server/src/service/services/facet.service.ts
  20. 1 1
      server/src/service/services/order.service.ts
  21. 13 13
      server/src/service/services/product-option-group.service.ts
  22. 12 5
      server/src/service/services/product-option.service.ts
  23. 31 28
      server/src/service/services/product-variant.service.ts
  24. 15 10
      server/src/service/services/product.service.ts
  25. 1 1
      server/src/service/services/promotion.service.ts
  26. 1 1
      server/src/service/services/role.service.ts
  27. 1 1
      server/src/service/services/tax-category.service.ts
  28. 2 2
      server/src/service/services/tax-rate.service.ts
  29. 2 2
      server/src/service/services/zone.service.ts

+ 1 - 1
server/src/common/types/locale-types.ts

@@ -1,7 +1,7 @@
 import { LanguageCode } from 'shared/generated-types';
 import { CustomFieldsObject, ID } from 'shared/shared-types';
 
-import { TranslatableRelationsKeys } from '../../service/helpers/translate-entity';
+import { TranslatableRelationsKeys } from '../../service/helpers/utils/translate-entity';
 
 import { UnwrappedArray } from './common-types';
 

+ 0 - 37
server/src/service/helpers/create-translatable.ts

@@ -1,37 +0,0 @@
-import { Type } from 'shared/shared-types';
-import { Connection } from 'typeorm';
-
-import { Translatable, TranslatedInput, Translation } from '../../common/types/locale-types';
-
-/**
- * Returns a "save" function which uses the provided connection and dto to
- * save a translatable entity and its translations to the DB.
- */
-export function createTranslatable<T extends Translatable>(
-    entityType: Type<T>,
-    translationType: Type<Translation<T>>,
-    beforeSave?: (newEntity: T) => void,
-) {
-    return async function saveTranslatable(
-        connection: Connection,
-        input: TranslatedInput<T>,
-        data?: any,
-    ): Promise<T> {
-        const entity = new entityType(input);
-        const translations: Array<Translation<T>> = [];
-
-        if (input.translations) {
-            for (const translationInput of input.translations) {
-                const translation = new translationType(translationInput);
-                translations.push(translation);
-                await connection.manager.save(translation);
-            }
-        }
-
-        entity.translations = translations;
-        if (typeof beforeSave === 'function') {
-            await beforeSave(entity);
-        }
-        return await connection.manager.save(entity, { data });
-    };
-}

+ 79 - 0
server/src/service/helpers/translatable-saver/translatable-saver.ts

@@ -0,0 +1,79 @@
+import { Injectable } from '@nestjs/common';
+import { InjectConnection } from '@nestjs/typeorm';
+import { omit } from 'shared/omit';
+import { ID, Type } from 'shared/shared-types';
+import { Connection } from 'typeorm';
+
+import { Translatable, TranslatedInput, Translation } from '../../../common/types/locale-types';
+import { patchEntity } from '../utils/patch-entity';
+
+import { TranslationDiffer } from './translation-differ';
+
+export interface CreateTranslatableOptions<T> {
+    entityType: Type<T>;
+    translationType: Type<Translation<T>>;
+    input: TranslatedInput<T>;
+    beforeSave?: (newEntity: T) => any | Promise<any>;
+    typeOrmSubscriberData?: any;
+}
+
+export interface UpdateTranslatableOptions<T> extends CreateTranslatableOptions<T> {
+    input: TranslatedInput<T> & { id: ID };
+}
+
+/**
+ * A helper which contains methods for creating and updating entities which implement the Translatable interface.
+ */
+@Injectable()
+export class TranslatableSaver {
+    constructor(@InjectConnection() private connection: Connection) {}
+
+    /**
+     * Create a translatable entity, including creating any translation entities according
+     * to the `translations` array.
+     */
+    async create<T extends Translatable>(options: CreateTranslatableOptions<T>): Promise<T> {
+        const { entityType, translationType, input, beforeSave, typeOrmSubscriberData } = options;
+
+        const entity = new entityType(input);
+        const translations: Array<Translation<T>> = [];
+
+        if (input.translations) {
+            for (const translationInput of input.translations) {
+                const translation = new translationType(translationInput);
+                translations.push(translation);
+                await this.connection.manager.save(translation);
+            }
+        }
+
+        entity.translations = translations;
+        if (typeof beforeSave === 'function') {
+            await beforeSave(entity);
+        }
+        return await this.connection.manager.save(entity, { data: typeOrmSubscriberData });
+    }
+
+    /**
+     * Update a translatable entity. Performs a diff of the `translations` array in order to
+     * perform the correct operation on the translations.
+     */
+    async update<T extends Translatable>(options: UpdateTranslatableOptions<T>): Promise<T> {
+        const { entityType, translationType, input, beforeSave, typeOrmSubscriberData } = options;
+        const existingTranslations = await this.connection.getRepository(translationType).find({
+            where: { base: input.id },
+            relations: ['base'],
+        });
+
+        const differ = new TranslationDiffer(translationType, this.connection.manager);
+        const diff = differ.diff(existingTranslations, input.translations);
+        const entity = await differ.applyDiff(
+            new entityType({ ...input, translations: existingTranslations }),
+            diff,
+        );
+        const updatedEntity = patchEntity(entity as any, omit(input, ['translations']));
+        if (typeof beforeSave === 'function') {
+            await beforeSave(entity);
+        }
+        return this.connection.manager.save(updatedEntity, { data: typeOrmSubscriberData });
+    }
+}

+ 8 - 8
server/src/service/helpers/translation-updater.spec.ts → server/src/service/helpers/translatable-saver/translation-differ.spec.ts

@@ -1,11 +1,11 @@
 import { LanguageCode } from 'shared/generated-types';
 
-import { TranslationInput } from '../../common/types/locale-types';
-import { ProductTranslation } from '../../entity/product/product-translation.entity';
-import { Product } from '../../entity/product/product.entity';
-import { MockEntityManager } from '../../testing/connection.mock';
+import { TranslationInput } from '../../../common/types/locale-types';
+import { ProductTranslation } from '../../../entity/product/product-translation.entity';
+import { Product } from '../../../entity/product/product.entity';
+import { MockEntityManager } from '../../../testing/connection.mock';
 
-import { TranslationUpdater } from './translation-updater';
+import { TranslationDiffer } from './translation-differ';
 
 describe('TranslationUpdater', () => {
     describe('diff()', () => {
@@ -48,7 +48,7 @@ describe('TranslationUpdater', () => {
                 },
             ];
 
-            const diff = new TranslationUpdater(ProductTranslation as any, entityManager).diff(
+            const diff = new TranslationDiffer(ProductTranslation as any, entityManager).diff(
                 existing,
                 updated,
             );
@@ -70,7 +70,7 @@ describe('TranslationUpdater', () => {
                     description: '',
                 },
             ];
-            const diff = new TranslationUpdater(ProductTranslation as any, entityManager).diff(
+            const diff = new TranslationDiffer(ProductTranslation as any, entityManager).diff(
                 existing,
                 updated,
             );
@@ -92,7 +92,7 @@ describe('TranslationUpdater', () => {
                     description: '',
                 },
             ];
-            const diff = new TranslationUpdater(ProductTranslation as any, entityManager).diff(
+            const diff = new TranslationDiffer(ProductTranslation as any, entityManager).diff(
                 existing,
                 updated,
             );

+ 4 - 4
server/src/service/helpers/translation-updater.ts → server/src/service/helpers/translatable-saver/translation-differ.ts

@@ -1,9 +1,9 @@
 import { DeepPartial } from 'shared/shared-types';
 import { EntityManager } from 'typeorm';
 
-import { Translatable, Translation, TranslationInput } from '../../common/types/locale-types';
-import { foundIn, not } from '../../common/utils';
-import { I18nError } from '../../i18n/i18n-error';
+import { Translatable, Translation, TranslationInput } from '../../../common/types/locale-types';
+import { foundIn, not } from '../../../common/utils';
+import { I18nError } from '../../../i18n/i18n-error';
 
 export interface TranslationContructor<T> {
     new (input?: DeepPartial<TranslationInput<T>> | DeepPartial<Translation<T>>): Translation<T>;
@@ -17,7 +17,7 @@ export interface TranslationDiff<T> {
 /**
  * This class is to be used when performing an update on a Translatable entity.
  */
-export class TranslationUpdater<Entity extends Translatable> {
+export class TranslationDiffer<Entity extends Translatable> {
     constructor(private translationCtor: TranslationContructor<Entity>, private manager: EntityManager) {}
 
     /**

+ 0 - 14
server/src/service/helpers/translation-updater.mock.ts

@@ -1,14 +0,0 @@
-import { MockClass } from '../../testing/testing-types';
-
-import { TranslationUpdater } from './translation-updater';
-import { TranslationUpdaterService } from './translation-updater.service';
-
-export class MockTranslationUpdater implements MockClass<TranslationUpdater<any>> {
-    diff = jest.fn();
-    applyDiff = jest.fn();
-}
-
-export class MockTranslationUpdaterService implements MockClass<TranslationUpdaterService> {
-    mockUpdater = new MockTranslationUpdater();
-    create = jest.fn().mockReturnValue(this.mockUpdater);
-}

+ 0 - 16
server/src/service/helpers/translation-updater.service.ts

@@ -1,16 +0,0 @@
-import { Injectable } from '@nestjs/common';
-import { InjectEntityManager } from '@nestjs/typeorm';
-import { EntityManager } from 'typeorm';
-
-import { Translatable } from '../../common/types/locale-types';
-
-import { TranslationContructor, TranslationUpdater } from './translation-updater';
-
-@Injectable()
-export class TranslationUpdaterService {
-    constructor(@InjectEntityManager() private manager: EntityManager) {}
-
-    create<T extends Translatable>(translationCtor: TranslationContructor<T>): TranslationUpdater<T> {
-        return new TranslationUpdater(translationCtor, this.manager);
-    }
-}

+ 0 - 43
server/src/service/helpers/update-translatable.ts

@@ -1,43 +0,0 @@
-import { omit } from 'shared/omit';
-import { ID, Type } from 'shared/shared-types';
-import { Connection } from 'typeorm';
-
-import { Translatable, TranslatedInput, Translation } from '../../common/types/locale-types';
-
-import { patchEntity } from './patch-entity';
-import { TranslationUpdaterService } from './translation-updater.service';
-
-/**
- * Returns a "save" function which uses the provided connection and dto to
- * update a translatable entity and its translations to the DB.
- */
-export function updateTranslatable<T extends Translatable>(
-    entityType: Type<T>,
-    translationType: Type<Translation<T>>,
-    translationUpdaterService: TranslationUpdaterService,
-    beforeSave?: (newEntity: T) => void,
-) {
-    return async function saveTranslatable(
-        connection: Connection,
-        input: TranslatedInput<T> & { id: ID },
-        data?: any,
-    ): Promise<T> {
-        const existingTranslations = await connection.getRepository(translationType).find({
-            where: { base: input.id },
-            relations: ['base'],
-        });
-
-        const translationUpdater = translationUpdaterService.create(translationType);
-        const diff = translationUpdater.diff(existingTranslations, input.translations);
-
-        const entity = await translationUpdater.applyDiff(
-            new entityType({ ...input, translations: existingTranslations }),
-            diff,
-        );
-        const updatedEntity = patchEntity(entity as any, omit(input, ['translations']));
-        if (typeof beforeSave === 'function') {
-            await beforeSave(entity);
-        }
-        return connection.manager.save(updatedEntity, { data });
-    };
-}

+ 2 - 2
server/src/service/helpers/get-entity-or-throw.ts → server/src/service/helpers/utils/get-entity-or-throw.ts

@@ -1,8 +1,8 @@
 import { ID, Type } from 'shared/shared-types';
 import { Connection } from 'typeorm';
 
-import { VendureEntity } from '../../entity/base/base.entity';
-import { I18nError } from '../../i18n/i18n-error';
+import { VendureEntity } from '../../../entity/base/base.entity';
+import { I18nError } from '../../../i18n/i18n-error';
 
 /**
  * Attempts to find an entity of the given type and id, and throws an error if not found.

+ 1 - 1
server/src/service/helpers/patch-entity.ts → server/src/service/helpers/utils/patch-entity.ts

@@ -1,4 +1,4 @@
-import { VendureEntity } from '../../entity/base/base.entity';
+import { VendureEntity } from '../../../entity/base/base.entity';
 
 export type InputPatch<T> = { [K in keyof T]?: T[K] | null };
 

+ 7 - 7
server/src/service/helpers/translate-entity.spec.ts → server/src/service/helpers/utils/translate-entity.spec.ts

@@ -1,12 +1,12 @@
 import { LanguageCode } from 'shared/generated-types';
 
-import { Translatable, Translation } from '../../common/types/locale-types';
-import { ProductOptionTranslation } from '../../entity/product-option/product-option-translation.entity';
-import { ProductOption } from '../../entity/product-option/product-option.entity';
-import { ProductVariantTranslation } from '../../entity/product-variant/product-variant-translation.entity';
-import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
-import { ProductTranslation } from '../../entity/product/product-translation.entity';
-import { Product } from '../../entity/product/product.entity';
+import { Translatable, Translation } from '../../../common/types/locale-types';
+import { ProductOptionTranslation } from '../../../entity/product-option/product-option-translation.entity';
+import { ProductOption } from '../../../entity/product-option/product-option.entity';
+import { ProductVariantTranslation } from '../../../entity/product-variant/product-variant-translation.entity';
+import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
+import { ProductTranslation } from '../../../entity/product/product-translation.entity';
+import { Product } from '../../../entity/product/product.entity';
 
 import { translateDeep, translateEntity } from './translate-entity';
 

+ 3 - 3
server/src/service/helpers/translate-entity.ts → server/src/service/helpers/utils/translate-entity.ts

@@ -1,9 +1,9 @@
 import { LanguageCode } from 'shared/generated-types';
 
-import { UnwrappedArray } from '../../common/types/common-types';
-import { I18nError } from '../../i18n/i18n-error';
+import { UnwrappedArray } from '../../../common/types/common-types';
+import { I18nError } from '../../../i18n/i18n-error';
 
-import { Translatable, Translated } from '../../common/types/locale-types';
+import { Translatable, Translated } from '../../../common/types/locale-types';
 
 // prettier-ignore
 export type TranslatableRelationsKeys<T> = {

+ 2 - 2
server/src/service/service.module.ts

@@ -8,7 +8,7 @@ import { ListQueryBuilder } from './helpers/list-query-builder/list-query-builde
 import { OrderCalculator } from './helpers/order-calculator/order-calculator';
 import { PasswordCiper } from './helpers/password-cipher/password-ciper';
 import { TaxCalculator } from './helpers/tax-calculator/tax-calculator';
-import { TranslationUpdaterService } from './helpers/translation-updater.service';
+import { TranslatableSaver } from './helpers/translatable-saver/translatable-saver';
 import { AdministratorService } from './services/administrator.service';
 import { AssetService } from './services/asset.service';
 import { AuthService } from './services/auth.service';
@@ -63,7 +63,7 @@ const exportedProviders = [
     providers: [
         ...exportedProviders,
         PasswordCiper,
-        TranslationUpdaterService,
+        TranslatableSaver,
         TaxCalculator,
         OrderCalculator,
         ListQueryBuilder,

+ 1 - 1
server/src/service/services/administrator.service.ts

@@ -11,7 +11,7 @@ import { User } from '../../entity/user/user.entity';
 import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { PasswordCiper } from '../helpers/password-cipher/password-ciper';
-import { patchEntity } from '../helpers/patch-entity';
+import { patchEntity } from '../helpers/utils/patch-entity';
 
 import { RoleService } from './role.service';
 

+ 2 - 2
server/src/service/services/channel.service.ts

@@ -13,8 +13,8 @@ import { ConfigService } from '../../config/config.service';
 import { Channel } from '../../entity/channel/channel.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { I18nError } from '../../i18n/i18n-error';
-import { getEntityOrThrow } from '../helpers/get-entity-or-throw';
-import { patchEntity } from '../helpers/patch-entity';
+import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
+import { patchEntity } from '../helpers/utils/patch-entity';
 
 @Injectable()
 export class ChannelService {

+ 1 - 1
server/src/service/services/country.service.ts

@@ -9,7 +9,7 @@ import { assertFound } from '../../common/utils';
 import { Country } from '../../entity/country/country.entity';
 import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
-import { patchEntity } from '../helpers/patch-entity';
+import { patchEntity } from '../helpers/utils/patch-entity';
 
 @Injectable()
 export class CountryService {

+ 2 - 2
server/src/service/services/customer-group.service.ts

@@ -13,8 +13,8 @@ import { Connection } from 'typeorm';
 import { assertFound } from '../../common/utils';
 import { CustomerGroup } from '../../entity/customer-group/customer-group.entity';
 import { Customer } from '../../entity/customer/customer.entity';
-import { getEntityOrThrow } from '../helpers/get-entity-or-throw';
-import { patchEntity } from '../helpers/patch-entity';
+import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
+import { patchEntity } from '../helpers/utils/patch-entity';
 
 @Injectable()
 export class CustomerGroupService {

+ 14 - 9
server/src/service/services/facet-value.service.ts

@@ -16,16 +16,14 @@ import { FacetValueTranslation } from '../../entity/facet-value/facet-value-tran
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { Facet } from '../../entity/facet/facet.entity';
 
-import { createTranslatable } from '../helpers/create-translatable';
-import { translateDeep } from '../helpers/translate-entity';
-import { TranslationUpdaterService } from '../helpers/translation-updater.service';
-import { updateTranslatable } from '../helpers/update-translatable';
+import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { translateDeep } from '../helpers/utils/translate-entity';
 
 @Injectable()
 export class FacetValueService {
     constructor(
         @InjectConnection() private connection: Connection,
-        private translationUpdaterService: TranslationUpdaterService,
+        private translatableSaver: TranslatableSaver,
     ) {}
 
     findAll(lang: LanguageCode): Promise<Array<Translated<FacetValue>>> {
@@ -48,14 +46,21 @@ export class FacetValueService {
         facet: Facet,
         input: CreateFacetValueInput | CreateFacetValueWithFacetInput,
     ): Promise<Translated<FacetValue>> {
-        const save = createTranslatable(FacetValue, FacetValueTranslation, fv => (fv.facet = facet));
-        const facetValue = await save(this.connection, input);
+        const facetValue = await this.translatableSaver.create({
+            input,
+            entityType: FacetValue,
+            translationType: FacetValueTranslation,
+            beforeSave: fv => (fv.facet = facet),
+        });
         return assertFound(this.findOne(facetValue.id, DEFAULT_LANGUAGE_CODE));
     }
 
     async update(input: UpdateFacetValueInput): Promise<Translated<FacetValue>> {
-        const save = updateTranslatable(FacetValue, FacetValueTranslation, this.translationUpdaterService);
-        const facetValue = await save(this.connection, input);
+        const facetValue = await this.translatableSaver.update({
+            input,
+            entityType: FacetValue,
+            translationType: FacetValueTranslation,
+        });
         return assertFound(this.findOne(facetValue.id, DEFAULT_LANGUAGE_CODE));
     }
 }

+ 13 - 9
server/src/service/services/facet.service.ts

@@ -11,17 +11,15 @@ import { assertFound } from '../../common/utils';
 import { FacetTranslation } from '../../entity/facet/facet-translation.entity';
 import { Facet } from '../../entity/facet/facet.entity';
 
-import { createTranslatable } from '../helpers/create-translatable';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
-import { translateDeep } from '../helpers/translate-entity';
-import { TranslationUpdaterService } from '../helpers/translation-updater.service';
-import { updateTranslatable } from '../helpers/update-translatable';
+import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { translateDeep } from '../helpers/utils/translate-entity';
 
 @Injectable()
 export class FacetService {
     constructor(
         @InjectConnection() private connection: Connection,
-        private translationUpdaterService: TranslationUpdaterService,
+        private translatableSaver: TranslatableSaver,
         private listQueryBuilder: ListQueryBuilder,
     ) {}
 
@@ -52,14 +50,20 @@ export class FacetService {
     }
 
     async create(input: CreateFacetInput): Promise<Translated<Facet>> {
-        const save = createTranslatable(Facet, FacetTranslation);
-        const facet = await save(this.connection, input);
+        const facet = await this.translatableSaver.create({
+            input,
+            entityType: Facet,
+            translationType: FacetTranslation,
+        });
         return assertFound(this.findOne(facet.id, DEFAULT_LANGUAGE_CODE));
     }
 
     async update(input: UpdateFacetInput): Promise<Translated<Facet>> {
-        const save = updateTranslatable(Facet, FacetTranslation, this.translationUpdaterService);
-        const facet = await save(this.connection, input);
+        const facet = await this.translatableSaver.update({
+            input,
+            entityType: Facet,
+            translationType: FacetTranslation,
+        });
         return assertFound(this.findOne(facet.id, DEFAULT_LANGUAGE_CODE));
     }
 }

+ 1 - 1
server/src/service/services/order.service.ts

@@ -14,7 +14,7 @@ import { Promotion } from '../../entity/promotion/promotion.entity';
 import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { OrderCalculator } from '../helpers/order-calculator/order-calculator';
-import { translateDeep } from '../helpers/translate-entity';
+import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { ProductVariantService } from './product-variant.service';
 

+ 13 - 13
server/src/service/services/product-option-group.service.ts

@@ -14,16 +14,14 @@ import { assertFound } from '../../common/utils';
 import { ProductOptionGroupTranslation } from '../../entity/product-option-group/product-option-group-translation.entity';
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 
-import { createTranslatable } from '../helpers/create-translatable';
-import { translateDeep } from '../helpers/translate-entity';
-import { TranslationUpdaterService } from '../helpers/translation-updater.service';
-import { updateTranslatable } from '../helpers/update-translatable';
+import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { translateDeep } from '../helpers/utils/translate-entity';
 
 @Injectable()
 export class ProductOptionGroupService {
     constructor(
         @InjectConnection() private connection: Connection,
-        private translationUpdaterService: TranslationUpdaterService,
+        private translatableSaver: TranslatableSaver,
     ) {}
 
     findAll(lang: LanguageCode, filterTerm?: string): Promise<Array<Translated<ProductOptionGroup>>> {
@@ -49,18 +47,20 @@ export class ProductOptionGroupService {
     }
 
     async create(input: CreateProductOptionGroupInput): Promise<Translated<ProductOptionGroup>> {
-        const save = createTranslatable(ProductOptionGroup, ProductOptionGroupTranslation);
-        const group = await save(this.connection, input);
+        const group = await this.translatableSaver.create({
+            input,
+            entityType: ProductOptionGroup,
+            translationType: ProductOptionGroupTranslation,
+        });
         return assertFound(this.findOne(group.id, DEFAULT_LANGUAGE_CODE));
     }
 
     async update(input: UpdateProductOptionGroupInput): Promise<Translated<ProductOptionGroup>> {
-        const save = updateTranslatable(
-            ProductOptionGroup,
-            ProductOptionGroupTranslation,
-            this.translationUpdaterService,
-        );
-        const group = await save(this.connection, input);
+        const group = await this.translatableSaver.update({
+            input,
+            entityType: ProductOptionGroup,
+            translationType: ProductOptionGroupTranslation,
+        });
         return assertFound(this.findOne(group.id, DEFAULT_LANGUAGE_CODE));
     }
 }

+ 12 - 5
server/src/service/services/product-option.service.ts

@@ -11,12 +11,15 @@ import { ProductOptionGroup } from '../../entity/product-option-group/product-op
 import { ProductOptionTranslation } from '../../entity/product-option/product-option-translation.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
 
-import { createTranslatable } from '../helpers/create-translatable';
-import { translateDeep } from '../helpers/translate-entity';
+import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { translateDeep } from '../helpers/utils/translate-entity';
 
 @Injectable()
 export class ProductOptionService {
-    constructor(@InjectConnection() private connection: Connection) {}
+    constructor(
+        @InjectConnection() private connection: Connection,
+        private translatableSaver: TranslatableSaver,
+    ) {}
 
     findAll(lang: LanguageCode): Promise<Array<Translated<ProductOption>>> {
         return this.connection.manager
@@ -38,8 +41,12 @@ export class ProductOptionService {
         group: ProductOptionGroup,
         input: CreateProductOptionInput,
     ): Promise<Translated<ProductOption>> {
-        const save = createTranslatable(ProductOption, ProductOptionTranslation, po => (po.group = group));
-        const option = await save(this.connection, input);
+        const option = await this.translatableSaver.create({
+            input,
+            entityType: ProductOption,
+            translationType: ProductOptionTranslation,
+            beforeSave: po => (po.group = group),
+        });
         return assertFound(this.findOne(option.id, DEFAULT_LANGUAGE_CODE));
     }
 }

+ 31 - 28
server/src/service/services/product-variant.service.ts

@@ -9,20 +9,15 @@ import { RequestContext } from '../../api/common/request-context';
 import { DEFAULT_LANGUAGE_CODE } from '../../common/constants';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
-import { Channel } from '../../entity/channel/channel.entity';
 import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { ProductOption } from '../../entity/product-option/product-option.entity';
 import { ProductVariantTranslation } from '../../entity/product-variant/product-variant-translation.entity';
 import { ProductVariant } from '../../entity/product-variant/product-variant.entity';
 import { Product } from '../../entity/product/product.entity';
-import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
-import { Zone } from '../../entity/zone/zone.entity';
 import { I18nError } from '../../i18n/i18n-error';
-import { createTranslatable } from '../helpers/create-translatable';
 import { TaxCalculator } from '../helpers/tax-calculator/tax-calculator';
-import { translateDeep } from '../helpers/translate-entity';
-import { TranslationUpdaterService } from '../helpers/translation-updater.service';
-import { updateTranslatable } from '../helpers/update-translatable';
+import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { TaxCategoryService } from './tax-category.service';
 import { TaxRateService } from './tax-rate.service';
@@ -34,7 +29,7 @@ export class ProductVariantService {
         private taxCategoryService: TaxCategoryService,
         private taxRateService: TaxRateService,
         private taxCalculator: TaxCalculator,
-        private translationUpdaterService: TranslationUpdaterService,
+        private translatableSaver: TranslatableSaver,
     ) {}
 
     findOne(ctx: RequestContext, productVariantId: ID): Promise<Translated<ProductVariant> | undefined> {
@@ -54,28 +49,33 @@ export class ProductVariantService {
         product: Product,
         input: CreateProductVariantInput,
     ): Promise<ProductVariant> {
-        const save = createTranslatable(ProductVariant, ProductVariantTranslation, async variant => {
-            const { optionCodes } = input;
-            if (optionCodes && optionCodes.length) {
-                const options = await this.connection.getRepository(ProductOption).find();
-                const selectedOptions = options.filter(og => optionCodes.includes(og.code));
-                variant.options = selectedOptions;
-            }
-            variant.product = product;
-            variant.taxCategory = { id: input.taxCategoryId } as any;
-        });
-        return await save(this.connection, input, {
-            channelId: ctx.channelId,
-            taxCategoryId: input.taxCategoryId,
+        return await this.translatableSaver.create({
+            input,
+            entityType: ProductVariant,
+            translationType: ProductVariantTranslation,
+            beforeSave: async variant => {
+                const { optionCodes } = input;
+                if (optionCodes && optionCodes.length) {
+                    const options = await this.connection.getRepository(ProductOption).find();
+                    const selectedOptions = options.filter(og => optionCodes.includes(og.code));
+                    variant.options = selectedOptions;
+                }
+                variant.product = product;
+                variant.taxCategory = { id: input.taxCategoryId } as any;
+            },
+            typeOrmSubscriberData: {
+                channelId: ctx.channelId,
+                taxCategoryId: input.taxCategoryId,
+            },
         });
     }
 
     async update(ctx: RequestContext, input: UpdateProductVariantInput): Promise<Translated<ProductVariant>> {
-        const save = updateTranslatable(
-            ProductVariant,
-            ProductVariantTranslation,
-            this.translationUpdaterService,
-            async updatedVariant => {
+        await this.translatableSaver.update({
+            input,
+            entityType: ProductVariant,
+            translationType: ProductVariantTranslation,
+            beforeSave: async updatedVariant => {
                 if (input.taxCategoryId) {
                     const taxCategory = await this.taxCategoryService.findOne(input.taxCategoryId);
                     if (taxCategory) {
@@ -83,8 +83,11 @@ export class ProductVariantService {
                     }
                 }
             },
-        );
-        await save(this.connection, input, { channelId: ctx.channelId, taxCategoryId: input.taxCategoryId });
+            typeOrmSubscriberData: {
+                channelId: ctx.channelId,
+                taxCategoryId: input.taxCategoryId,
+            },
+        });
         const variant = await assertFound(
             this.connection.manager.getRepository(ProductVariant).findOne(input.id, {
                 relations: ['options', 'facetValues', 'taxCategory'],

+ 15 - 10
server/src/service/services/product.service.ts

@@ -12,11 +12,9 @@ import { ProductOptionGroup } from '../../entity/product-option-group/product-op
 import { ProductTranslation } from '../../entity/product/product-translation.entity';
 import { Product } from '../../entity/product/product.entity';
 import { I18nError } from '../../i18n/i18n-error';
-import { createTranslatable } from '../helpers/create-translatable';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
-import { translateDeep } from '../helpers/translate-entity';
-import { TranslationUpdaterService } from '../helpers/translation-updater.service';
-import { updateTranslatable } from '../helpers/update-translatable';
+import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
+import { translateDeep } from '../helpers/utils/translate-entity';
 
 import { AssetService } from './asset.service';
 import { ChannelService } from './channel.service';
@@ -27,12 +25,12 @@ import { TaxRateService } from './tax-rate.service';
 export class ProductService {
     constructor(
         @InjectConnection() private connection: Connection,
-        private translationUpdaterService: TranslationUpdaterService,
         private channelService: ChannelService,
         private assetService: AssetService,
         private productVariantService: ProductVariantService,
         private taxRateService: TaxRateService,
         private listQueryBuilder: ListQueryBuilder,
+        private translatableSaver: TranslatableSaver,
     ) {}
 
     findAll(
@@ -95,17 +93,24 @@ export class ProductService {
     }
 
     async create(ctx: RequestContext, input: CreateProductInput): Promise<Translated<Product>> {
-        const save = createTranslatable(Product, ProductTranslation, async p => {
-            this.channelService.assignToChannels(p, ctx);
+        const product = await this.translatableSaver.create({
+            input,
+            entityType: Product,
+            translationType: ProductTranslation,
+            beforeSave: async p => {
+                this.channelService.assignToChannels(p, ctx);
+            },
         });
-        const product = await save(this.connection, input);
         await this.saveAssetInputs(product, input);
         return assertFound(this.findOne(ctx, product.id));
     }
 
     async update(ctx: RequestContext, input: UpdateProductInput): Promise<Translated<Product>> {
-        const save = updateTranslatable(Product, ProductTranslation, this.translationUpdaterService);
-        const product = await save(this.connection, input);
+        const product = await this.translatableSaver.update({
+            input,
+            entityType: Product,
+            translationType: ProductTranslation,
+        });
         await this.saveAssetInputs(product, input);
         return assertFound(this.findOne(ctx, product.id));
     }

+ 1 - 1
server/src/service/services/promotion.service.ts

@@ -19,7 +19,7 @@ import { PromotionCondition } from '../../config/promotion/promotion-condition';
 import { Promotion } from '../../entity/promotion/promotion.entity';
 import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
-import { patchEntity } from '../helpers/patch-entity';
+import { patchEntity } from '../helpers/utils/patch-entity';
 
 import { ChannelService } from './channel.service';
 

+ 1 - 1
server/src/service/services/role.service.ts

@@ -15,7 +15,7 @@ import { assertFound } from '../../common/utils';
 import { Role } from '../../entity/role/role.entity';
 import { I18nError } from '../../i18n/i18n-error';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
-import { patchEntity } from '../helpers/patch-entity';
+import { patchEntity } from '../helpers/utils/patch-entity';
 
 import { ChannelService } from './channel.service';
 

+ 1 - 1
server/src/service/services/tax-category.service.ts

@@ -7,7 +7,7 @@ import { Connection } from 'typeorm';
 import { assertFound } from '../../common/utils';
 import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
 import { I18nError } from '../../i18n/i18n-error';
-import { patchEntity } from '../helpers/patch-entity';
+import { patchEntity } from '../helpers/utils/patch-entity';
 
 @Injectable()
 export class TaxCategoryService {

+ 2 - 2
server/src/service/services/tax-rate.service.ts

@@ -10,9 +10,9 @@ import { TaxCategory } from '../../entity/tax-category/tax-category.entity';
 import { TaxRate } from '../../entity/tax-rate/tax-rate.entity';
 import { Zone } from '../../entity/zone/zone.entity';
 import { I18nError } from '../../i18n/i18n-error';
-import { getEntityOrThrow } from '../helpers/get-entity-or-throw';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
-import { patchEntity } from '../helpers/patch-entity';
+import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
+import { patchEntity } from '../helpers/utils/patch-entity';
 
 export class TaxRateService {
     /**

+ 2 - 2
server/src/service/services/zone.service.ts

@@ -13,8 +13,8 @@ import { Connection } from 'typeorm';
 import { assertFound } from '../../common/utils';
 import { Country } from '../../entity/country/country.entity';
 import { Zone } from '../../entity/zone/zone.entity';
-import { getEntityOrThrow } from '../helpers/get-entity-or-throw';
-import { patchEntity } from '../helpers/patch-entity';
+import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
+import { patchEntity } from '../helpers/utils/patch-entity';
 
 @Injectable()
 export class ZoneService {