Browse Source

feat(server): Implement localeString type for customFields

Michael Bromley 7 năm trước cách đây
mục cha
commit
8285037502

+ 5 - 1
server/dev-config.ts

@@ -21,6 +21,10 @@ export const devConfig: VendureConfig = {
         database: 'vendure-dev',
     },
     customFields: {
-        Product: [{ name: 'infoUrl', type: 'string' }, { name: 'color', type: 'string' }],
+        Product: [
+            { name: 'infoUrl', type: 'string' },
+            { name: 'downloadable', type: 'boolean' },
+            { name: 'nickname', type: 'localeString' },
+        ],
     },
 };

+ 3 - 1
server/src/entity/base/has-custom-fields.ts

@@ -3,5 +3,7 @@
  * with custom fields.
  */
 export interface HasCustomFields {
-    customFields: any;
+    customFields: CustomFieldsObject;
 }
+
+export type CustomFieldsObject = object;

+ 31 - 4
server/src/entity/custom-entity-fields.ts

@@ -12,40 +12,63 @@ export class CustomCustomerFields {}
 @Entity()
 export class CustomProductFields {}
 @Entity()
+export class CustomProductFieldsTranslation {}
+@Entity()
 export class CustomProductOptionFields {}
 @Entity()
+export class CustomProductOptionFieldsTranslation {}
+@Entity()
 export class CustomProductOptionGroupFields {}
 @Entity()
+export class CustomProductOptionGroupFieldsTranslation {}
+@Entity()
 export class CustomProductVariantFields {}
 @Entity()
+export class CustomProductVariantFieldsTranslation {}
+@Entity()
 export class CustomUserFields {}
 
 /**
  * Dynamically add columns to the custom field entity based on the CustomFields config.
  */
-function registerEntityCustomFields(entityName: keyof CustomFields, ctor: { new (): any }) {
+function registerEntityCustomFields(
+    entityName: keyof CustomFields,
+    ctor: { new (): any },
+    translation = false,
+) {
     const customFields = config.customFields && config.customFields[entityName];
     if (customFields) {
         for (const customField of customFields) {
             const { name, type } = customField;
-            Column({ type: getColumnType(type), name })(new ctor(), name);
+            const registerColumn = () => Column({ type: getColumnType(type), name })(new ctor(), name);
+
+            if (translation) {
+                if (type === 'localeString') {
+                    registerColumn();
+                }
+            } else {
+                if (type !== 'localeString') {
+                    registerColumn();
+                }
+            }
         }
     }
 }
 
 function getColumnType(type: CustomFieldType): ColumnType {
+    const dbEngine = config.dbConnectionOptions.type;
     switch (type) {
         case 'string':
         case 'localeString':
             return 'varchar';
         case 'boolean':
-            return 'bool';
+            return dbEngine === 'mysql' ? 'tinyint' : 'bool';
         case 'int':
             return 'int';
         case 'float':
             return 'double';
         case 'datetime':
-            return 'datetime';
+            return dbEngine === 'mysql' ? 'datetime' : 'timestamp';
         default:
             assertNever(type);
     }
@@ -55,7 +78,11 @@ function getColumnType(type: CustomFieldType): ColumnType {
 registerEntityCustomFields('Address', CustomAddressFields);
 registerEntityCustomFields('Customer', CustomCustomerFields);
 registerEntityCustomFields('Product', CustomProductFields);
+registerEntityCustomFields('Product', CustomProductFieldsTranslation, true);
 registerEntityCustomFields('ProductOption', CustomProductOptionFields);
+registerEntityCustomFields('ProductOption', CustomProductOptionFieldsTranslation, true);
 registerEntityCustomFields('ProductOptionGroup', CustomProductOptionGroupFields);
+registerEntityCustomFields('ProductOptionGroup', CustomProductOptionGroupFieldsTranslation, true);
 registerEntityCustomFields('ProductVariant', CustomProductVariantFields);
+registerEntityCustomFields('ProductVariant', CustomProductVariantFieldsTranslation, true);
 registerEntityCustomFields('User', CustomUserFields);

+ 7 - 1
server/src/entity/product-option-group/product-option-group-translation.entity.ts

@@ -4,11 +4,14 @@ import { DeepPartial } from '../../../../shared/shared-types';
 import { LanguageCode } from '../../locale/language-code';
 import { Translation } from '../../locale/locale-types';
 import { VendureEntity } from '../base/base.entity';
+import { HasCustomFields } from '../base/has-custom-fields';
+import { CustomProductOptionGroupFieldsTranslation } from '../custom-entity-fields';
 
 import { ProductOptionGroup } from './product-option-group.entity';
 
 @Entity()
-export class ProductOptionGroupTranslation extends VendureEntity implements Translation<ProductOptionGroup> {
+export class ProductOptionGroupTranslation extends VendureEntity
+    implements Translation<ProductOptionGroup>, HasCustomFields {
     constructor(input?: DeepPartial<Translation<ProductOptionGroup>>) {
         super(input);
     }
@@ -19,4 +22,7 @@ export class ProductOptionGroupTranslation extends VendureEntity implements Tran
 
     @ManyToOne(type => ProductOptionGroup, base => base.translations)
     base: ProductOptionGroup;
+
+    @Column(type => CustomProductOptionGroupFieldsTranslation)
+    customFields: CustomProductOptionGroupFieldsTranslation;
 }

+ 7 - 1
server/src/entity/product-option/product-option-translation.entity.ts

@@ -4,11 +4,14 @@ import { DeepPartial } from '../../../../shared/shared-types';
 import { LanguageCode } from '../../locale/language-code';
 import { Translation } from '../../locale/locale-types';
 import { VendureEntity } from '../base/base.entity';
+import { HasCustomFields } from '../base/has-custom-fields';
+import { CustomProductOptionFieldsTranslation } from '../custom-entity-fields';
 
 import { ProductOption } from './product-option.entity';
 
 @Entity()
-export class ProductOptionTranslation extends VendureEntity implements Translation<ProductOption> {
+export class ProductOptionTranslation extends VendureEntity
+    implements Translation<ProductOption>, HasCustomFields {
     constructor(input?: DeepPartial<Translation<ProductOption>>) {
         super(input);
     }
@@ -19,4 +22,7 @@ export class ProductOptionTranslation extends VendureEntity implements Translati
 
     @ManyToOne(type => ProductOption, base => base.translations)
     base: ProductOption;
+
+    @Column(type => CustomProductOptionFieldsTranslation)
+    customFields: CustomProductOptionFieldsTranslation;
 }

+ 8 - 9
server/src/entity/product-variant/product-variant-translation.entity.ts

@@ -1,21 +1,17 @@
-import {
-    Column,
-    CreateDateColumn,
-    Entity,
-    ManyToOne,
-    PrimaryGeneratedColumn,
-    UpdateDateColumn,
-} from 'typeorm';
+import { Column, Entity, ManyToOne } from 'typeorm';
 
 import { DeepPartial } from '../../../../shared/shared-types';
 import { LanguageCode } from '../../locale/language-code';
 import { Translation } from '../../locale/locale-types';
 import { VendureEntity } from '../base/base.entity';
+import { HasCustomFields } from '../base/has-custom-fields';
+import { CustomProductVariantFieldsTranslation } from '../custom-entity-fields';
 
 import { ProductVariant } from './product-variant.entity';
 
 @Entity()
-export class ProductVariantTranslation extends VendureEntity implements Translation<ProductVariant> {
+export class ProductVariantTranslation extends VendureEntity
+    implements Translation<ProductVariant>, HasCustomFields {
     constructor(input?: DeepPartial<Translation<ProductVariant>>) {
         super(input);
     }
@@ -26,4 +22,7 @@ export class ProductVariantTranslation extends VendureEntity implements Translat
 
     @ManyToOne(type => ProductVariant, base => base.translations)
     base: ProductVariant;
+
+    @Column(type => CustomProductVariantFieldsTranslation)
+    customFields: CustomProductVariantFieldsTranslation;
 }

+ 6 - 1
server/src/entity/product/product-translation.entity.ts

@@ -4,11 +4,13 @@ import { DeepPartial } from '../../../../shared/shared-types';
 import { LanguageCode } from '../../locale/language-code';
 import { Translation, TranslationInput } from '../../locale/locale-types';
 import { VendureEntity } from '../base/base.entity';
+import { HasCustomFields } from '../base/has-custom-fields';
+import { CustomProductFieldsTranslation } from '../custom-entity-fields';
 
 import { Product } from './product.entity';
 
 @Entity()
-export class ProductTranslation extends VendureEntity implements Translation<Product> {
+export class ProductTranslation extends VendureEntity implements Translation<Product>, HasCustomFields {
     constructor(input?: DeepPartial<TranslationInput<Product>>) {
         super(input);
     }
@@ -23,4 +25,7 @@ export class ProductTranslation extends VendureEntity implements Translation<Pro
 
     @ManyToOne(type => Product, base => base.translations)
     base: Product;
+
+    @Column(type => CustomProductFieldsTranslation)
+    customFields: CustomProductFieldsTranslation;
 }

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

@@ -1,6 +1,7 @@
 import { ID } from '../../../shared/shared-types';
 import { UnwrappedArray } from '../common/common-types';
 import { VendureEntity } from '../entity/base/base.entity';
+import { CustomFieldsObject } from '../entity/base/has-custom-fields';
 
 import { LanguageCode } from './language-code';
 import { TranslatableRelationsKeys } from './translate-entity';
@@ -21,6 +22,8 @@ export type NonTranslateableKeys<T> = { [K in keyof T]: T[K] extends LocaleStrin
  */
 export interface Translatable { translations: Array<Translation<any>>; }
 
+export type TranslationCustomFields<T> = { [K in keyof T]: K extends 'customFields' ? K : never }[keyof T];
+
 // prettier-ignore
 /**
  * Translations of localizable entities should implement this type.
@@ -33,7 +36,8 @@ export type Translation<T> =
         base: T;
     } &
     // Translation must include all translatable keys as a string type
-    { [K in TranslatableKeys<T>]: string; };
+    { [K in TranslatableKeys<T>]: string; } &
+    { [K in TranslationCustomFields<T>]: CustomFieldsObject; };
 
 /**
  * This is the type of a translation object when provided as input to a create or update operation.

+ 42 - 0
server/src/locale/translate-entity.spec.ts

@@ -31,6 +31,7 @@ describe('translateEntity()', () => {
             description: '',
         });
         productTranslationEN.base = { id: 1 } as any;
+        productTranslationEN.customFields = {};
 
         productTranslationDE = new ProductTranslation({
             id: '3',
@@ -40,10 +41,12 @@ describe('translateEntity()', () => {
             description: '',
         });
         productTranslationDE.base = { id: 1 } as any;
+        productTranslationDE.customFields = {};
 
         product = new Product();
         product.id = '1';
         product.translations = [productTranslationEN, productTranslationDE];
+        product.customFields = {};
     });
 
     it('should unwrap the matching translation', () => {
@@ -70,6 +73,45 @@ describe('translateEntity()', () => {
         expect(result).toHaveProperty('languageCode', 'en');
     });
 
+    describe('customFields handling', () => {
+        it('should leave customFields with no localeStrings intact', () => {
+            const customFields = {
+                aBooleanField: true,
+            };
+            product.customFields = customFields;
+            const result = translateEntity(product, LanguageCode.EN);
+
+            expect(result.customFields).toEqual(customFields);
+        });
+
+        it('should translate customFields with localeStrings', () => {
+            const translatedCustomFields = {
+                aLocaleString1: 'translated1',
+                aLocaleString2: 'translated2',
+            };
+            product.translations[0].customFields = translatedCustomFields;
+            const result = translateEntity(product, LanguageCode.EN);
+
+            expect(result.customFields).toEqual(translatedCustomFields);
+        });
+
+        it('should translate customFields with localeStrings and other types', () => {
+            const productCustomFields = {
+                aBooleanField: true,
+                aStringField: 'foo',
+            };
+            const translatedCustomFields = {
+                aLocaleString1: 'translated1',
+                aLocaleString2: 'translated2',
+            };
+            product.customFields = productCustomFields;
+            product.translations[0].customFields = translatedCustomFields;
+            const result = translateEntity(product, LanguageCode.EN);
+
+            expect(result.customFields).toEqual({ ...productCustomFields, ...translatedCustomFields });
+        });
+    });
+
     it('throw if there are no translations available', () => {
         product.translations = [];
 

+ 3 - 1
server/src/locale/translate-entity.ts

@@ -51,7 +51,9 @@ export function translateEntity<T extends Translatable>(
     Object.setPrototypeOf(translated, Object.getPrototypeOf(translatable));
 
     for (const [key, value] of Object.entries(translation)) {
-        if (key !== 'base' && key !== 'id') {
+        if (key === 'customFields') {
+            Object.assign(translated[key], value);
+        } else if (key !== 'base' && key !== 'id') {
             translated[key] = value;
         }
     }