Browse Source

fix(core): Resolve all ProductVariant price fields in GraphQL API

Relates to #763
Michael Bromley 4 years ago
parent
commit
2bd289ae9c

+ 31 - 2
packages/core/src/api/resolvers/entity/product-variant-entity.resolver.ts

@@ -1,11 +1,11 @@
 import { Args, Parent, ResolveField, Resolver } from '@nestjs/graphql';
-import { StockMovementListOptions } from '@vendure/common/lib/generated-types';
+import { CurrencyCode, StockMovementListOptions } from '@vendure/common/lib/generated-types';
 import { DEFAULT_CHANNEL_CODE } from '@vendure/common/lib/shared-constants';
 import { PaginatedList } from '@vendure/common/lib/shared-types';
 
 import { Translated } from '../../../common/types/locale-types';
 import { idsAreEqual } from '../../../common/utils';
-import { Asset, Channel, FacetValue, Product, ProductOption } from '../../../entity';
+import { Asset, Channel, FacetValue, Product, ProductOption, TaxRate } from '../../../entity';
 import { ProductVariant } from '../../../entity/product-variant/product-variant.entity';
 import { StockMovement } from '../../../entity/stock-movement/stock-movement.entity';
 import { LocaleStringHydrator } from '../../../service/helpers/locale-string-hydrator/locale-string-hydrator';
@@ -30,6 +30,35 @@ export class ProductVariantEntityResolver {
         return this.localeStringHydrator.hydrateLocaleStringField(ctx, productVariant, 'name');
     }
 
+    @ResolveField()
+    async price(@Ctx() ctx: RequestContext, @Parent() productVariant: ProductVariant): Promise<number> {
+        return this.productVariantService.hydratePriceFields(ctx, productVariant, 'price');
+    }
+
+    @ResolveField()
+    async priceWithTax(
+        @Ctx() ctx: RequestContext,
+        @Parent() productVariant: ProductVariant,
+    ): Promise<number> {
+        return this.productVariantService.hydratePriceFields(ctx, productVariant, 'priceWithTax');
+    }
+
+    @ResolveField()
+    async currencyCode(
+        @Ctx() ctx: RequestContext,
+        @Parent() productVariant: ProductVariant,
+    ): Promise<CurrencyCode> {
+        return this.productVariantService.hydratePriceFields(ctx, productVariant, 'currencyCode');
+    }
+
+    @ResolveField()
+    async taxRateApplied(
+        @Ctx() ctx: RequestContext,
+        @Parent() productVariant: ProductVariant,
+    ): Promise<TaxRate> {
+        return this.productVariantService.hydratePriceFields(ctx, productVariant, 'taxRateApplied');
+    }
+
     @ResolveField()
     async product(
         @Ctx() ctx: RequestContext,

+ 49 - 0
packages/core/src/service/services/product-variant.service.ts

@@ -13,6 +13,7 @@ import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
 import { FindOptionsUtils } from 'typeorm';
 
 import { RequestContext } from '../../api/common/request-context';
+import { RequestContextCacheService } from '../../cache/request-context-cache.service';
 import { ForbiddenError, InternalServerError, UserInputError } from '../../common/error/errors';
 import { ListQueryOptions } from '../../common/types/common-types';
 import { Translated } from '../../common/types/locale-types';
@@ -62,6 +63,7 @@ export class ProductVariantService {
         private channelService: ChannelService,
         private roleService: RoleService,
         private customFieldRelationService: CustomFieldRelationService,
+        private requestCache: RequestContextCacheService,
     ) {}
 
     async findAll(
@@ -490,6 +492,53 @@ export class ProductVariantService {
         };
     }
 
+    /**
+     * This method is intended to be used by the ProductVariant GraphQL entity resolver to resolve the
+     * price-related fields which need to be populated at run-time using the `applyChannelPriceAndTax`
+     * method.
+     *
+     * Is optimized to make as few DB calls as possible using caching based on the open request.
+     */
+    async hydratePriceFields<F extends 'currencyCode' | 'price' | 'priceWithTax' | 'taxRateApplied'>(
+        ctx: RequestContext,
+        variant: ProductVariant,
+        priceField: F,
+    ): Promise<ProductVariant[F]> {
+        const cacheKey = `hydrate-variant-price-fields-${variant.id}`;
+        let populatePricesPromise = this.requestCache.get<Promise<ProductVariant>>(ctx, cacheKey);
+
+        if (!populatePricesPromise) {
+            populatePricesPromise = new Promise(async (resolve, reject) => {
+                try {
+                    if (!variant.productVariantPrices?.length) {
+                        const variantWithPrices = await this.connection.getEntityOrThrow(
+                            ctx,
+                            ProductVariant,
+                            variant.id,
+                            { relations: ['productVariantPrices'] },
+                        );
+                        variant.productVariantPrices = variantWithPrices.productVariantPrices;
+                    }
+                    if (!variant.taxCategory) {
+                        const variantWithTaxCategory = await this.connection.getEntityOrThrow(
+                            ctx,
+                            ProductVariant,
+                            variant.id,
+                            { relations: ['taxCategory'] },
+                        );
+                        variant.taxCategory = variantWithTaxCategory.taxCategory;
+                    }
+                    resolve(await this.applyChannelPriceAndTax(variant, ctx));
+                } catch (e) {
+                    reject(e);
+                }
+            });
+            this.requestCache.set(ctx, cacheKey, populatePricesPromise);
+        }
+        const hydratedVariant = await populatePricesPromise;
+        return hydratedVariant[priceField];
+    }
+
     /**
      * Populates the `price` field with the price for the specified channel.
      */