Ver código fonte

Implement basic product options

Michael Bromley 7 anos atrás
pai
commit
76f6260338

+ 16 - 0
modules/core/entity/product-option-group/product-option-group-translation.entity.ts

@@ -0,0 +1,16 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
+import { Translation } from '../../locale/locale-types';
+import { ProductOptionGroup } from './product-option-group.interface';
+import { ProductOptionGroupEntity } from './product-option-group.entity';
+
+@Entity('product_option_group_translation')
+export class ProductOptionGroupTranslationEntity implements Translation<ProductOptionGroup> {
+    @PrimaryGeneratedColumn() id: number;
+
+    @Column() languageCode: string;
+
+    @Column() name: string;
+
+    @ManyToOne(type => ProductOptionGroupEntity, base => base.translations)
+    base: ProductOptionGroupEntity;
+}

+ 22 - 0
modules/core/entity/product-option-group/product-option-group.entity.ts

@@ -0,0 +1,22 @@
+import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
+import { Translatable } from '../../locale/locale-types';
+import { ProductOptionGroup } from './product-option-group.interface';
+import { ProductOptionGroupTranslationEntity } from './product-option-group-translation.entity';
+import { ProductOptionEntity } from '../product-option/product-option.entity';
+
+@Entity('product_option_group')
+export class ProductOptionGroupEntity implements Translatable<ProductOptionGroup> {
+    @PrimaryGeneratedColumn() id: number;
+
+    @Column() code: string;
+
+    @CreateDateColumn() createdAt: string;
+
+    @UpdateDateColumn() updatedAt: string;
+
+    @OneToMany(type => ProductOptionGroupTranslationEntity, translation => translation.base)
+    translations: ProductOptionGroupTranslationEntity[];
+
+    @OneToMany(type => ProductOptionEntity, product => product)
+    options: ProductOptionEntity[];
+}

+ 7 - 0
modules/core/entity/product-option-group/product-option-group.graphql

@@ -0,0 +1,7 @@
+type ProductOptionGroup {
+    id: Int
+    code: String
+    name: String
+    createdAt: String
+    updatedAt: String
+}

+ 9 - 0
modules/core/entity/product-option-group/product-option-group.interface.ts

@@ -0,0 +1,9 @@
+import { LocaleString } from '../../locale/locale-types';
+
+export interface ProductOptionGroup {
+    id: number;
+    code: string;
+    name: LocaleString;
+    createdAt: string;
+    updatedAt: string;
+}

+ 16 - 0
modules/core/entity/product-option/product-option-translation.entity.ts

@@ -0,0 +1,16 @@
+import { Column, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm';
+import { Translation } from '../../locale/locale-types';
+import { ProductOption } from './product-option.interface';
+import { ProductOptionEntity } from './product-option.entity';
+
+@Entity('product_option_translation')
+export class ProductOptionTranslationEntity implements Translation<ProductOption> {
+    @PrimaryGeneratedColumn() id: number;
+
+    @Column() languageCode: string;
+
+    @Column() name: string;
+
+    @ManyToOne(type => ProductOptionEntity, base => base.translations)
+    base: ProductOptionEntity;
+}

+ 30 - 0
modules/core/entity/product-option/product-option.entity.ts

@@ -0,0 +1,30 @@
+import { Translatable } from '../../locale/locale-types';
+import { ProductOption } from './product-option.interface';
+import {
+    Column,
+    CreateDateColumn,
+    Entity,
+    ManyToOne,
+    OneToMany,
+    PrimaryGeneratedColumn,
+    UpdateDateColumn,
+} from 'typeorm';
+import { ProductOptionTranslationEntity } from './product-option-translation.entity';
+import { ProductOptionGroupEntity } from '../product-option-group/product-option-group.entity';
+
+@Entity('product_option')
+export class ProductOptionEntity implements Translatable<ProductOption> {
+    @PrimaryGeneratedColumn() id: number;
+
+    @Column() code: string;
+
+    @CreateDateColumn() createdAt: string;
+
+    @UpdateDateColumn() updatedAt: string;
+
+    @OneToMany(type => ProductOptionTranslationEntity, translation => translation.base)
+    translations: ProductOptionTranslationEntity[];
+
+    @ManyToOne(type => ProductOptionGroupEntity)
+    group: ProductOptionGroupEntity;
+}

+ 7 - 0
modules/core/entity/product-option/product-option.graphql

@@ -0,0 +1,7 @@
+type ProductOption {
+    id: Int
+    code: String
+    name: String
+    createdAt: String
+    updatedAt: String
+}

+ 10 - 0
modules/core/entity/product-option/product-option.interface.ts

@@ -0,0 +1,10 @@
+import { ProductOptionGroup } from '../product-option-group/product-option-group.interface';
+import { LocaleString } from '../../locale/locale-types';
+
+export interface ProductOption {
+    id: number;
+    code: string;
+    name: LocaleString;
+    createdAt: string;
+    updatedAt: string;
+}

+ 7 - 0
modules/core/entity/product-variant/product-variant.entity.ts

@@ -4,6 +4,8 @@ import {
     Column,
     CreateDateColumn,
     Entity,
+    JoinTable,
+    ManyToMany,
     ManyToOne,
     OneToMany,
     PrimaryGeneratedColumn,
@@ -11,6 +13,7 @@ import {
 } from 'typeorm';
 import { ProductEntity } from '../product/product.entity';
 import { ProductVariantTranslationEntity } from './product-variant-translation.entity';
+import { ProductOptionEntity } from '../product-option/product-option.entity';
 
 @Entity('product_variant')
 export class ProductVariantEntity implements Translatable<ProductVariant> {
@@ -31,4 +34,8 @@ export class ProductVariantEntity implements Translatable<ProductVariant> {
 
     @ManyToOne(type => ProductEntity, product => product.variants)
     product: ProductEntity[];
+
+    @ManyToMany(type => ProductOptionEntity)
+    @JoinTable()
+    options: ProductOptionEntity[];
 }

+ 1 - 0
modules/core/entity/product-variant/product-variant.graphql

@@ -4,6 +4,7 @@ type ProductVariant {
     name: String
     image: String
     price: Int
+    options: [ProductOption]
     createdAt: String
     updatedAt: String
 }

+ 2 - 0
modules/core/entity/product-variant/product-variant.interface.ts

@@ -1,4 +1,5 @@
 import { LocaleString } from '../../locale/locale-types';
+import { ProductOption } from '../product-option/product-option.interface';
 
 export class ProductVariant {
     id: number;
@@ -6,6 +7,7 @@ export class ProductVariant {
     name: LocaleString;
     image: string;
     price: string;
+    options: ProductOption[];
     createdAt: string;
     updatedAt: string;
 }

+ 19 - 2
modules/core/entity/product/product.entity.ts

@@ -1,9 +1,22 @@
-import { Column, CreateDateColumn, Entity, OneToMany, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
+import {
+    Column,
+    CreateDateColumn,
+    Entity,
+    JoinTable,
+    ManyToMany,
+    OneToMany,
+    PrimaryGeneratedColumn,
+    UpdateDateColumn,
+} from 'typeorm';
 import { Translatable } from '../../locale/locale-types';
 import { Product } from './product.interface';
 import { ProductTranslationEntity } from './product-translation.entity';
 import { ProductVariantEntity } from '../product-variant/product-variant.entity';
 import { ProductVariant } from '../product-variant/product-variant.interface';
+import { ProductOptionEntity } from '../product-option/product-option.entity';
+import { ProductOption } from '../product-option/product-option.interface';
+import { ProductOptionGroup } from '../product-option-group/product-option-group.interface';
+import { ProductOptionGroupEntity } from '../product-option-group/product-option-group.entity';
 
 @Entity('product')
 export class ProductEntity implements Translatable<Product> {
@@ -19,5 +32,9 @@ export class ProductEntity implements Translatable<Product> {
     translations: ProductTranslationEntity[];
 
     @OneToMany(type => ProductVariantEntity, variant => variant.product)
-    variants: ProductVariant[];
+    variants: ProductVariantEntity[];
+
+    @ManyToMany(type => ProductOptionGroupEntity)
+    @JoinTable()
+    optionGroups: ProductOptionGroupEntity[];
 }

+ 1 - 0
modules/core/entity/product/product.graphql

@@ -5,6 +5,7 @@ type Product {
     description: String
     image: String
     variants: [ProductVariant]
+    optionGroups: [ProductOptionGroup]
     createdAt: String
     updatedAt: String
 }

+ 2 - 0
modules/core/entity/product/product.interface.ts

@@ -1,5 +1,6 @@
 import { LocaleString } from '../../locale/locale-types';
 import { ProductVariant } from '../product-variant/product-variant.interface';
+import { ProductOptionGroup } from '../product-option-group/product-option-group.interface';
 
 export interface Product {
     id: number;
@@ -7,6 +8,7 @@ export interface Product {
     slug: LocaleString;
     description: LocaleString;
     image: string;
+    optionGroups: ProductOptionGroup[];
     variants: ProductVariant[];
     createdAt: string;
     updatedAt: string;

+ 3 - 7
modules/core/locale/locale-types.ts

@@ -13,12 +13,9 @@ export type NonTranslateableKeys<T> = { [K in keyof T]: T[K] extends LocaleStrin
  */
 export type Translatable<T> =
     // Translatable must include all non-translatable keys of the interface
-    { [K in NonTranslateableKeys<T>]: T[K] } &
+    { [K in NonTranslateableKeys<T>]: T[K] extends Array<any> ? Array<Translatable<T[K][number]>> : T[K] } &
         // Translatable must not include any translatable keys (these are instead handled by the Translation)
-        {
-            [K in TranslatableKeys<T>]?: never
-        } & // Translatable must include a reference to all translations of the translatable keys
-        { translations: Translation<T>[] };
+        { [K in TranslatableKeys<T>]?: never } & { translations: Translation<T>[] }; // Translatable must include a reference to all translations of the translatable keys
 
 /**
  * Translations of localizable entities should implement this type.
@@ -28,5 +25,4 @@ export type Translation<T> =
     {
         languageCode: string;
         base: Translatable<T>;
-    } & // Translation must include all translatable keys as a string type
-    { [K in TranslatableKeys<T>]: string };
+    } & { [K in TranslatableKeys<T>]: string }; // Translation must include all translatable keys as a string type

+ 40 - 14
modules/core/repository/product-repository.ts

@@ -1,12 +1,13 @@
-import { EntityRepository, Repository, SelectQueryBuilder } from "typeorm";
-import { ProductEntity } from "../entity/product/product.entity";
-import { Product } from "../entity/product/product.interface";
-import { translate } from "../locale/locale.service";
-import { ProductVariant } from "../entity/product-variant/product-variant.interface";
+import { EntityRepository, Repository, SelectQueryBuilder } from 'typeorm';
+import { ProductEntity } from '../entity/product/product.entity';
+import { Product } from '../entity/product/product.interface';
+import { translate } from '../locale/locale.service';
+import { ProductVariant } from '../entity/product-variant/product-variant.interface';
+import { ProductOptionGroup } from '../entity/product-option-group/product-option-group.interface';
+import { ProductOption } from '../entity/product-option/product-option.interface';
 
 @EntityRepository(ProductEntity)
 export class ProductRepository extends Repository<ProductEntity> {
-
     /**
      * Returns an array of Products including ProductVariants, translated into the
      * specified language.
@@ -23,7 +24,7 @@ export class ProductRepository extends Repository<ProductEntity> {
      */
     localeFindOne(id: number, languageCode: string): Promise<Product> {
         return this.getProductQueryBuilder(languageCode)
-            .andWhere('ProductEntity.id = :id', { id })
+            .andWhere('product.id = :id', { id })
             .getMany()
             .then(result => this.translateProductAndVariants(result))
             .then(res => res[0]);
@@ -32,19 +33,44 @@ export class ProductRepository extends Repository<ProductEntity> {
     private getProductQueryBuilder(languageCode: string): SelectQueryBuilder<ProductEntity> {
         const code = languageCode || 'en';
 
-        return this.manager.createQueryBuilder(ProductEntity, 'product')
+        return this.manager
+            .createQueryBuilder(ProductEntity, 'product')
             .leftJoinAndSelect('product.variants', 'variant')
-            .leftJoinAndSelect('product.translations', 'product_translation')
-            .leftJoinAndSelect('variant.translations', 'product_variant_translation')
-            .where('product_translation.languageCode = :code', { code })
-            .andWhere('product_variant_translation.languageCode = :code', { code })
+            .leftJoinAndSelect('product.optionGroups', 'option_group')
+            .leftJoinAndSelect(
+                'product.translations',
+                'product_translation',
+                'product_translation.languageCode = :code',
+            )
+            .leftJoinAndSelect(
+                'variant.translations',
+                'product_variant_translation',
+                'product_variant_translation.languageCode = :code',
+            )
+            .leftJoinAndSelect(
+                'option_group.translations',
+                'option_group_translation',
+                'option_group_translation.languageCode = :code',
+            )
+            .leftJoinAndSelect('variant.options', 'variant_options')
+            .leftJoinAndSelect(
+                'variant_options.translations',
+                'variant_options_translation',
+                'variant_options_translation.languageCode = :code',
+            )
+            .setParameters({ code });
     }
 
     private translateProductAndVariants(result: ProductEntity[]): Product[] {
         return result.map(productEntity => {
             const product = translate<Product>(productEntity);
-            product.variants = product.variants.map(variant => translate<ProductVariant>(variant as any));
+            product.variants = product.variants.map(variant => {
+                const v = translate<ProductVariant>(variant as any);
+                v.options = v.options.map(vop => translate<ProductOption>(vop as any));
+                return v;
+            });
+            product.optionGroups = product.optionGroups.map(group => translate<ProductOptionGroup>(group as any));
             return product;
         });
-    };
+    }
 }

+ 93 - 16
modules/testing/mock-data.service.ts

@@ -1,15 +1,19 @@
-import * as faker from "faker/locale/en_GB";
-import { Connection, createConnection } from "typeorm";
-import { CustomerEntity } from "../core/entity/customer/customer.entity";
-import { ProductVariantEntity } from "../core/entity/product-variant/product-variant.entity";
-import { ProductEntity } from "../core/entity/product/product.entity";
-import { ProductVariantTranslationEntity } from "../core/entity/product-variant/product-variant-translation.entity";
-import { AddressEntity } from "../core/entity/address/address.entity";
-import { Role } from "../core/auth/role";
-import { ProductTranslationEntity } from "../core/entity/product/product-translation.entity";
-import { PasswordService } from "../core/auth/password.service";
-import { UserEntity } from "../core/entity/user/user.entity";
-import { AdministratorEntity } from "../core/entity/administrator/administrator.entity";
+import * as faker from 'faker/locale/en_GB';
+import { Connection, createConnection } from 'typeorm';
+import { CustomerEntity } from '../core/entity/customer/customer.entity';
+import { ProductVariantEntity } from '../core/entity/product-variant/product-variant.entity';
+import { ProductEntity } from '../core/entity/product/product.entity';
+import { ProductVariantTranslationEntity } from '../core/entity/product-variant/product-variant-translation.entity';
+import { AddressEntity } from '../core/entity/address/address.entity';
+import { Role } from '../core/auth/role';
+import { ProductTranslationEntity } from '../core/entity/product/product-translation.entity';
+import { PasswordService } from '../core/auth/password.service';
+import { UserEntity } from '../core/entity/user/user.entity';
+import { AdministratorEntity } from '../core/entity/administrator/administrator.entity';
+import { ProductOptionGroupEntity } from '../core/entity/product-option-group/product-option-group.entity';
+import { ProductOptionGroupTranslationEntity } from '../core/entity/product-option-group/product-option-group-translation.entity';
+import { ProductOptionEntity } from '../core/entity/product-option/product-option.entity';
+import { ProductOptionTranslationEntity } from '../core/entity/product-option/product-option-translation.entity';
 
 /**
  * A Class used for generating mock data.
@@ -33,8 +37,10 @@ export class MockDataService {
 
             await this.clearAllTables();
             await this.populateCustomersAndAddresses();
-            await this.populateProducts();
             await this.populateAdministrators();
+
+            const sizeOptionGroup = await this.populateOptions();
+            await this.populateProducts(sizeOptionGroup);
         });
     }
 
@@ -43,8 +49,71 @@ export class MockDataService {
         console.log('Cleared all tables');
     }
 
-    async populateProducts() {
+    async populateOptions(): Promise<ProductOptionGroupEntity> {
+        const sizeGroup = new ProductOptionGroupEntity();
+        sizeGroup.code = 'size';
+
+        const sizeGroupEN = new ProductOptionGroupTranslationEntity();
+        sizeGroupEN.languageCode = 'en';
+        sizeGroupEN.name = 'Size';
+        await this.connection.manager.save(sizeGroupEN);
+        const sizeGroupDE = new ProductOptionGroupTranslationEntity();
+
+        sizeGroupDE.languageCode = 'de';
+        sizeGroupDE.name = 'Größe';
+        await this.connection.manager.save(sizeGroupDE);
+
+        sizeGroup.translations = [sizeGroupEN, sizeGroupDE];
+        await this.connection.manager.save(sizeGroup);
+
+        await this.populateSizeOptions(sizeGroup);
+
+        console.log('created size options');
+        return sizeGroup;
+    }
+
+    private async populateSizeOptions(sizeGroup: ProductOptionGroupEntity) {
+        const sizeSmall = new ProductOptionEntity();
+        sizeSmall.code = 'small';
+
+        const sizeSmallEN = new ProductOptionTranslationEntity();
+        sizeSmallEN.languageCode = 'en';
+        sizeSmallEN.name = 'Small';
+        await this.connection.manager.save(sizeSmallEN);
+
+        const sizeSmallDE = new ProductOptionTranslationEntity();
+        sizeSmallDE.languageCode = 'de';
+        sizeSmallDE.name = 'Klein';
+        await this.connection.manager.save(sizeSmallDE);
+
+        sizeSmall.translations = [sizeSmallEN, sizeSmallDE];
+        sizeSmall.group = sizeGroup;
+        await this.connection.manager.save(sizeSmall);
+
+        const sizeLarge = new ProductOptionEntity();
+        sizeLarge.code = 'large';
+
+        const sizeLargeEN = new ProductOptionTranslationEntity();
+        sizeLargeEN.languageCode = 'en';
+        sizeLargeEN.name = 'Large';
+        await this.connection.manager.save(sizeLargeEN);
+
+        const sizeLargeDE = new ProductOptionTranslationEntity();
+        sizeLargeDE.languageCode = 'de';
+        sizeLargeDE.name = 'Groß';
+        await this.connection.manager.save(sizeLargeDE);
+
+        sizeLarge.translations = [sizeLargeEN, sizeLargeDE];
+        sizeLarge.group = sizeGroup;
+        await this.connection.manager.save(sizeLarge);
+
+        sizeGroup.options = [sizeSmall, sizeLarge];
+    }
+
+    async populateProducts(optionGroup: ProductOptionGroupEntity) {
         for (let i = 0; i < 5; i++) {
+            const addOption = i === 2 || i === 4;
+
             const product = new ProductEntity();
             product.image = faker.image.imageUrl();
 
@@ -71,12 +140,20 @@ export class MockDataService {
                 await this.connection.manager.save(variantTranslation1);
                 await this.connection.manager.save(variantTranslation2);
 
+                if (addOption) {
+                    variant.options = [optionGroup.options[0]];
+                } else {
+                    variant.options = [];
+                }
                 variant.translations = [variantTranslation1, variantTranslation2];
                 await this.connection.manager.save(variant);
                 console.log(`${j + 1}. created product variant ${variantName}`);
                 variants.push(variant);
             }
 
+            if (addOption) {
+                product.optionGroups = [optionGroup];
+            }
             product.variants = variants;
             product.translations = [translation1, translation2];
             await this.connection.manager.save(product);
@@ -154,7 +231,7 @@ export class MockDataService {
     private makeProductVariantTranslation(langCode: string, name: string): ProductVariantTranslationEntity {
         const productVariantTranslation = new ProductVariantTranslationEntity();
         productVariantTranslation.languageCode = langCode;
-        productVariantTranslation.name = `${langCode} ${name}`;;
+        productVariantTranslation.name = `${langCode} ${name}`;
         return productVariantTranslation;
     }
-}
+}

+ 1 - 0
tsconfig.json

@@ -5,6 +5,7 @@
     "noImplicitAny": false,
     "removeComments": true,
     "noLib": false,
+    "skipLibCheck": true,
     "lib": ["es2017"],
     "allowSyntheticDefaultImports": true,
     "emitDecoratorMetadata": true,