Browse Source

feat(server): Add assets to ProductVariant

Relates to #45
Michael Bromley 7 years ago
parent
commit
ed95074acc

File diff suppressed because it is too large
+ 0 - 0
schema.json


+ 23 - 0
server/e2e/product.e2e-spec.ts

@@ -472,6 +472,29 @@ describe('Product resolver', () => {
                 expect(updatedVariant.price).toBe(432);
             });
 
+            it('updateProductVariants updates assets', async () => {
+                const firstVariant = variants[0];
+                const result = await client.query<
+                    UpdateProductVariants.Mutation,
+                    UpdateProductVariants.Variables
+                >(UPDATE_PRODUCT_VARIANTS, {
+                    input: [
+                        {
+                            id: firstVariant.id,
+                            assetIds: ['T_1', 'T_2'],
+                            featuredAssetId: 'T_2',
+                        },
+                    ],
+                });
+                const updatedVariant = result.updateProductVariants[0];
+                if (!updatedVariant) {
+                    fail('no updated variant returned.');
+                    return;
+                }
+                expect(updatedVariant.assets.map(a => a.id)).toEqual(['T_1', 'T_2']);
+                expect(updatedVariant.featuredAsset!.id).toBe('T_2');
+            });
+
             it('updateProductVariants updates taxCategory and priceBeforeTax', async () => {
                 const firstVariant = variants[0];
                 const result = await client.query<

+ 1 - 1
server/src/api/resolvers/product.resolver.ts

@@ -127,7 +127,7 @@ export class ProductResolver {
 
     @Mutation()
     @Allow(Permission.UpdateCatalog)
-    @Decode('taxCategoryId', 'facetValueIds')
+    @Decode('taxCategoryId', 'facetValueIds', 'featuredAssetId', 'assetIds')
     async updateProductVariants(
         @Ctx() ctx: RequestContext,
         @Args() args: UpdateProductVariantsMutationArgs,

+ 8 - 0
server/src/entity/product-variant/product-variant.entity.ts

@@ -2,6 +2,7 @@ import { Column, Entity, JoinTable, ManyToMany, ManyToOne, OneToMany } from 'typ
 
 import { DeepPartial, HasCustomFields } from '../../../../shared/shared-types';
 import { LocaleString, Translatable, Translation } from '../../common/types/locale-types';
+import { Asset } from '../asset/asset.entity';
 import { VendureEntity } from '../base/base.entity';
 import { CustomProductVariantFields } from '../custom-entity-fields';
 import { FacetValue } from '../facet-value/facet-value.entity';
@@ -48,6 +49,13 @@ export class ProductVariant extends VendureEntity implements Translatable, HasCu
      */
     taxRateApplied: TaxRate;
 
+    @ManyToOne(type => Asset)
+    featuredAsset: Asset;
+
+    @ManyToMany(type => Asset)
+    @JoinTable()
+    assets: Asset[];
+
     @ManyToOne(type => TaxCategory)
     taxCategory: TaxCategory;
 

+ 6 - 0
server/src/entity/product-variant/product-variant.graphql

@@ -5,6 +5,8 @@ type ProductVariant implements Node {
     languageCode: LanguageCode!
     sku: String!
     name: String!
+    featuredAsset: Asset
+    assets: [Asset!]!
     price: Int!
     priceIncludesTax: Boolean!
     priceWithTax: Int!
@@ -35,6 +37,8 @@ input CreateProductVariantInput {
     price: Int
     taxCategoryId: ID!
     optionCodes: [String!]
+    featuredAssetId: ID
+    assetIds: [ID!]
 }
 
 input UpdateProductVariantInput {
@@ -44,4 +48,6 @@ input UpdateProductVariantInput {
     sku: String
     taxCategoryId: ID
     price: Int
+    featuredAssetId: ID
+    assetIds: [ID!]
 }

+ 39 - 0
server/src/service/helpers/asset-updater/asset-updater.ts

@@ -0,0 +1,39 @@
+import { Injectable } from '@nestjs/common';
+import { InjectConnection } from '@nestjs/typeorm';
+import { Connection } from 'typeorm';
+
+import { Asset, VendureEntity } from '../../../entity';
+import { AssetService } from '../../services/asset.service';
+
+export interface EntityWithAssets extends VendureEntity {
+    featuredAsset: Asset;
+    assets: Asset[];
+}
+
+export interface AssetInput {
+    featuredAssetId?: string | null;
+    assetIds?: string[] | null;
+}
+
+@Injectable()
+export class AssetUpdater {
+    constructor(@InjectConnection() private connection: Connection, private assetService: AssetService) {}
+
+    /**
+     * Updates the assets / featuredAsset of an entity, ensuring that only valid assetIds are used.
+     */
+    async updateEntityAssets<T extends EntityWithAssets>(product: T, input: AssetInput) {
+        if (input.assetIds || input.featuredAssetId) {
+            if (input.assetIds) {
+                const assets = await this.assetService.findByIds(input.assetIds);
+                product.assets = assets;
+            }
+            if (input.featuredAssetId) {
+                const featuredAsset = await this.assetService.findOne(input.featuredAssetId);
+                if (featuredAsset) {
+                    product.featuredAsset = featuredAsset;
+                }
+            }
+        }
+    }
+}

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

@@ -5,6 +5,7 @@ import { getConfig } from '../config/config-helpers';
 import { ConfigModule } from '../config/config.module';
 import { EventBusModule } from '../event-bus/event-bus.module';
 
+import { AssetUpdater } from './helpers/asset-updater/asset-updater';
 import { ListQueryBuilder } from './helpers/list-query-builder/list-query-builder';
 import { OrderCalculator } from './helpers/order-calculator/order-calculator';
 import { OrderMerger } from './helpers/order-merger/order-merger';
@@ -82,6 +83,7 @@ const exportedProviders = [
         OrderMerger,
         ListQueryBuilder,
         ShippingCalculator,
+        AssetUpdater,
     ],
     exports: exportedProviders,
 })

+ 4 - 21
server/src/service/services/product-category.service.ts

@@ -14,15 +14,14 @@ import { IllegalOperationError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
 import { assertFound, idsAreEqual } from '../../common/utils';
-import { FacetValue } from '../../entity/facet-value/facet-value.entity';
 import { ProductCategoryTranslation } from '../../entity/product-category/product-category-translation.entity';
 import { ProductCategory } from '../../entity/product-category/product-category.entity';
+import { AssetUpdater } from '../helpers/asset-updater/asset-updater';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { getEntityOrThrow } from '../helpers/utils/get-entity-or-throw';
 import { translateDeep, translateTree } from '../helpers/utils/translate-entity';
 
-import { AssetService } from './asset.service';
 import { ChannelService } from './channel.service';
 import { FacetValueService } from './facet-value.service';
 
@@ -32,7 +31,7 @@ export class ProductCategoryService {
     constructor(
         @InjectConnection() private connection: Connection,
         private channelService: ChannelService,
-        private assetService: AssetService,
+        private assetUpdater: AssetUpdater,
         private facetValueService: FacetValueService,
         private listQueryBuilder: ListQueryBuilder,
         private translatableSaver: TranslatableSaver,
@@ -115,9 +114,9 @@ export class ProductCategoryService {
                 if (input.facetValueIds) {
                     category.facetValues = await this.facetValueService.findByIds(input.facetValueIds);
                 }
+                await this.assetUpdater.updateEntityAssets(category, input);
             },
         });
-        await this.saveAssetInputs(productCategory, input);
         return assertFound(this.findOne(ctx, productCategory.id));
     }
 
@@ -133,9 +132,9 @@ export class ProductCategoryService {
                 if (input.facetValueIds) {
                     category.facetValues = await this.facetValueService.findByIds(input.facetValueIds);
                 }
+                await this.assetUpdater.updateEntityAssets(category, input);
             },
         });
-        await this.saveAssetInputs(productCategory, input);
         return assertFound(this.findOne(ctx, productCategory.id));
     }
 
@@ -181,22 +180,6 @@ export class ProductCategoryService {
         return assertFound(this.findOne(ctx, input.categoryId));
     }
 
-    private async saveAssetInputs(productCategory: ProductCategory, input: any) {
-        if (input.assetIds || input.featuredAssetId) {
-            if (input.assetIds) {
-                const assets = await this.assetService.findByIds(input.assetIds);
-                productCategory.assets = assets;
-            }
-            if (input.featuredAssetId) {
-                const featuredAsset = await this.assetService.findOne(input.featuredAssetId);
-                if (featuredAsset) {
-                    productCategory.featuredAsset = featuredAsset;
-                }
-            }
-            await this.connection.manager.save(productCategory);
-        }
-    }
-
     /**
      * Returns the next position value in the given parent category.
      */

+ 20 - 2
server/src/service/services/product-variant.service.ts

@@ -15,6 +15,7 @@ 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 { AssetUpdater } from '../helpers/asset-updater/asset-updater';
 import { TaxCalculator } from '../helpers/tax-calculator/tax-calculator';
 import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
 import { translateDeep } from '../helpers/utils/translate-entity';
@@ -31,6 +32,7 @@ export class ProductVariantService {
         private facetValueService: FacetValueService,
         private taxRateService: TaxRateService,
         private taxCalculator: TaxCalculator,
+        private assetUpdater: AssetUpdater,
         private translatableSaver: TranslatableSaver,
     ) {}
 
@@ -53,7 +55,14 @@ export class ProductVariantService {
                 where: {
                     product: { id: productId } as any,
                 },
-                relations: ['options', 'facetValues', 'facetValues.facet', 'taxCategory'],
+                relations: [
+                    'options',
+                    'facetValues',
+                    'facetValues.facet',
+                    'taxCategory',
+                    'assets',
+                    'featuredAsset',
+                ],
             })
             .then(variants =>
                 variants.map(variant => {
@@ -85,6 +94,7 @@ export class ProductVariantService {
                 }
                 variant.product = product;
                 variant.taxCategory = { id: input.taxCategoryId } as any;
+                await this.assetUpdater.updateEntityAssets(variant, input);
             },
             typeOrmSubscriberData: {
                 channelId: ctx.channelId,
@@ -108,6 +118,7 @@ export class ProductVariantService {
                 if (input.facetValueIds) {
                     updatedVariant.facetValues = await this.facetValueService.findByIds(input.facetValueIds);
                 }
+                await this.assetUpdater.updateEntityAssets(updatedVariant, input);
             },
             typeOrmSubscriberData: {
                 channelId: ctx.channelId,
@@ -116,7 +127,14 @@ export class ProductVariantService {
         });
         const variant = await assertFound(
             this.connection.manager.getRepository(ProductVariant).findOne(input.id, {
-                relations: ['options', 'facetValues', 'facetValues.facet', 'taxCategory'],
+                relations: [
+                    'options',
+                    'facetValues',
+                    'facetValues.facet',
+                    'taxCategory',
+                    'assets',
+                    'featuredAsset',
+                ],
             }),
         );
         return translateDeep(this.applyChannelPriceAndTax(variant, ctx), DEFAULT_LANGUAGE_CODE, [

+ 6 - 20
server/src/service/services/product.service.ts

@@ -12,11 +12,11 @@ import { assertFound } from '../../common/utils';
 import { ProductOptionGroup } from '../../entity/product-option-group/product-option-group.entity';
 import { ProductTranslation } from '../../entity/product/product-translation.entity';
 import { Product } from '../../entity/product/product.entity';
+import { AssetUpdater } from '../helpers/asset-updater/asset-updater';
 import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
 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';
 import { ProductVariantService } from './product-variant.service';
 import { TaxRateService } from './tax-rate.service';
@@ -26,7 +26,7 @@ export class ProductService {
     constructor(
         @InjectConnection() private connection: Connection,
         private channelService: ChannelService,
-        private assetService: AssetService,
+        private assetUpdater: AssetUpdater,
         private productVariantService: ProductVariantService,
         private taxRateService: TaxRateService,
         private listQueryBuilder: ListQueryBuilder,
@@ -69,9 +69,9 @@ export class ProductService {
             translationType: ProductTranslation,
             beforeSave: async p => {
                 this.channelService.assignToChannels(p, ctx);
+                await this.assetUpdater.updateEntityAssets(p, input);
             },
         });
-        await this.saveAssetInputs(product, input);
         return assertFound(this.findOne(ctx, product.id));
     }
 
@@ -80,8 +80,10 @@ export class ProductService {
             input,
             entityType: Product,
             translationType: ProductTranslation,
+            beforeSave: async p => {
+                await this.assetUpdater.updateEntityAssets(p, input);
+            },
         });
-        await this.saveAssetInputs(product, input);
         return assertFound(this.findOne(ctx, product.id));
     }
 
@@ -118,22 +120,6 @@ export class ProductService {
         return assertFound(this.findOne(ctx, productId));
     }
 
-    private async saveAssetInputs(product: Product, input: CreateProductInput | UpdateProductInput) {
-        if (input.assetIds || input.featuredAssetId) {
-            if (input.assetIds) {
-                const assets = await this.assetService.findByIds(input.assetIds);
-                product.assets = assets;
-            }
-            if (input.featuredAssetId) {
-                const featuredAsset = await this.assetService.findOne(input.featuredAssetId);
-                if (featuredAsset) {
-                    product.featuredAsset = featuredAsset;
-                }
-            }
-            await this.connection.manager.save(product);
-        }
-    }
-
     private async getProductWithOptionGroups(productId: ID): Promise<Product> {
         const product = await this.connection
             .getRepository(Product)

+ 14 - 0
shared/generated-types.ts

@@ -340,6 +340,8 @@ export interface ProductVariant extends Node {
     languageCode: LanguageCode;
     sku: string;
     name: string;
+    featuredAsset?: Asset | null;
+    assets: Asset[];
     price: number;
     priceIncludesTax: boolean;
     priceWithTax: number;
@@ -1278,6 +1280,8 @@ export interface UpdateProductVariantInput {
     sku?: string | null;
     taxCategoryId?: string | null;
     price?: number | null;
+    featuredAssetId?: string | null;
+    assetIds?: string[] | null;
     customFields?: Json | null;
 }
 
@@ -1380,6 +1384,8 @@ export interface CreateProductVariantInput {
     price?: number | null;
     taxCategoryId: string;
     optionCodes?: string[] | null;
+    featuredAssetId?: string | null;
+    assetIds?: string[] | null;
     customFields?: Json | null;
 }
 
@@ -3125,6 +3131,8 @@ export namespace ProductVariantResolvers {
         languageCode?: LanguageCodeResolver<LanguageCode, any, Context>;
         sku?: SkuResolver<string, any, Context>;
         name?: NameResolver<string, any, Context>;
+        featuredAsset?: FeaturedAssetResolver<Asset | null, any, Context>;
+        assets?: AssetsResolver<Asset[], any, Context>;
         price?: PriceResolver<number, any, Context>;
         priceIncludesTax?: PriceIncludesTaxResolver<boolean, any, Context>;
         priceWithTax?: PriceWithTaxResolver<number, any, Context>;
@@ -3146,6 +3154,12 @@ export namespace ProductVariantResolvers {
     >;
     export type SkuResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type NameResolver<R = string, Parent = any, Context = any> = Resolver<R, Parent, Context>;
+    export type FeaturedAssetResolver<R = Asset | null, Parent = any, Context = any> = Resolver<
+        R,
+        Parent,
+        Context
+    >;
+    export type AssetsResolver<R = Asset[], Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type PriceResolver<R = number, Parent = any, Context = any> = Resolver<R, Parent, Context>;
     export type PriceIncludesTaxResolver<R = boolean, Parent = any, Context = any> = Resolver<
         R,

Some files were not shown because too many files changed in this diff