Browse Source

perf(core): Optimize order operations

This commit introduces a number of small optimizations to order-related code paths:

- Relations decorator added to `addItemToOrder` and `adjustOrderLine`
- Relations array passed down through key OrderService methods
- Ids used for tax calculations, so entire entities do not need to be joined
Michael Bromley 1 year ago
parent
commit
e3d6c21ea3

+ 13 - 4
packages/core/src/api/resolvers/shop/shop-order.resolver.ts

@@ -76,7 +76,8 @@ export class ShopOrderResolver {
     async order(
         @Ctx() ctx: RequestContext,
         @Args() args: QueryOrderArgs,
-        @Relations(Order) relations: RelationPaths<Order>,
+        @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+        relations: RelationPaths<Order>,
     ): Promise<Order | undefined> {
         const requiredRelations: RelationPaths<Order> = ['customer', 'customer.user'];
         const order = await this.orderService.findOne(
@@ -98,7 +99,8 @@ export class ShopOrderResolver {
     @Allow(Permission.Owner)
     async activeOrder(
         @Ctx() ctx: RequestContext,
-        @Relations(Order) relations: RelationPaths<Order>,
+        @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+        relations: RelationPaths<Order>,
         @Args() args: ActiveOrderArgs,
     ): Promise<Order | undefined> {
         if (ctx.authorizedAsOwnerOnly) {
@@ -107,7 +109,7 @@ export class ShopOrderResolver {
                 args[ACTIVE_ORDER_INPUT_FIELD_NAME],
             );
             if (sessionOrder) {
-                return this.orderService.findOne(ctx, sessionOrder.id);
+                return this.orderService.findOne(ctx, sessionOrder.id, relations);
             } else {
                 return;
             }
@@ -119,7 +121,8 @@ export class ShopOrderResolver {
     async orderByCode(
         @Ctx() ctx: RequestContext,
         @Args() args: QueryOrderByCodeArgs,
-        @Relations(Order) relations: RelationPaths<Order>,
+        @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+        relations: RelationPaths<Order>,
     ): Promise<Order | undefined> {
         if (ctx.authorizedAsOwnerOnly) {
             const requiredRelations: RelationPaths<Order> = ['customer', 'customer.user'];
@@ -294,6 +297,8 @@ export class ShopOrderResolver {
     async addItemToOrder(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationAddItemToOrderArgs & ActiveOrderArgs,
+        @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+        relations: RelationPaths<Order>,
     ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         const order = await this.activeOrderService.getActiveOrder(
             ctx,
@@ -306,6 +311,7 @@ export class ShopOrderResolver {
             args.productVariantId,
             args.quantity,
             (args as any).customFields,
+            relations,
         );
     }
 
@@ -315,6 +321,8 @@ export class ShopOrderResolver {
     async adjustOrderLine(
         @Ctx() ctx: RequestContext,
         @Args() args: MutationAdjustOrderLineArgs & ActiveOrderArgs,
+        @Relations({ entity: Order, omit: ['aggregateOrder', 'sellerOrders'] })
+        relations: RelationPaths<Order>,
     ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         if (args.quantity === 0) {
             return this.removeOrderLine(ctx, { orderLineId: args.orderLineId });
@@ -330,6 +338,7 @@ export class ShopOrderResolver {
             args.orderLineId,
             args.quantity,
             (args as any).customFields,
+            relations,
         );
     }
 

+ 3 - 0
packages/core/src/entity/order-line/order-line.entity.ts

@@ -76,6 +76,9 @@ export class OrderLine extends VendureEntity implements HasCustomFields {
     @ManyToOne(type => TaxCategory)
     taxCategory: TaxCategory;
 
+    @EntityId({ nullable: true })
+    taxCategoryId: ID;
+
     @Index()
     @ManyToOne(type => Asset, asset => asset.featuredInVariants, { onDelete: 'SET NULL' })
     featuredAsset: Asset;

+ 16 - 3
packages/core/src/entity/tax-rate/tax-rate.entity.ts

@@ -1,5 +1,5 @@
 import { TaxLine } from '@vendure/common/lib/generated-types';
-import { DeepPartial } from '@vendure/common/lib/shared-types';
+import { DeepPartial, ID } from '@vendure/common/lib/shared-types';
 import { Column, Entity, Index, ManyToOne } from 'typeorm';
 
 import { grossPriceOf, netPriceOf, taxComponentOf, taxPayableOn } from '../../common/tax-utils';
@@ -8,6 +8,7 @@ import { HasCustomFields } from '../../config/custom-field/custom-field-types';
 import { VendureEntity } from '../base/base.entity';
 import { CustomTaxRateFields } from '../custom-entity-fields';
 import { CustomerGroup } from '../customer-group/customer-group.entity';
+import { EntityId } from '../entity-id.decorator';
 import { TaxCategory } from '../tax-category/tax-category.entity';
 import { DecimalTransformer } from '../value-transformers';
 import { Zone } from '../zone/zone.entity';
@@ -38,10 +39,16 @@ export class TaxRate extends VendureEntity implements HasCustomFields {
     @ManyToOne(type => TaxCategory, taxCategory => taxCategory.taxRates)
     category: TaxCategory;
 
+    @EntityId({ nullable: true })
+    categoryId: ID;
+
     @Index()
     @ManyToOne(type => Zone, zone => zone.taxRates)
     zone: Zone;
 
+    @EntityId({ nullable: true })
+    zoneId: ID;
+
     @Index()
     @ManyToOne(type => CustomerGroup, customerGroup => customerGroup.taxRates, { nullable: true })
     customerGroup?: CustomerGroup;
@@ -84,7 +91,13 @@ export class TaxRate extends VendureEntity implements HasCustomFields {
         };
     }
 
-    test(zone: Zone, taxCategory: TaxCategory): boolean {
-        return idsAreEqual(taxCategory.id, this.category.id) && idsAreEqual(zone.id, this.zone.id);
+    test(zone: Zone | ID, taxCategory: TaxCategory | ID): boolean {
+        const taxCategoryId = this.isId(taxCategory) ? taxCategory : taxCategory.id;
+        const zoneId = this.isId(zone) ? zone : zone.id;
+        return idsAreEqual(taxCategoryId, this.categoryId) && idsAreEqual(zoneId, this.zoneId);
+    }
+
+    private isId<T>(entityOrId: T | ID): entityOrId is ID {
+        return typeof entityOrId === 'string' || typeof entityOrId === 'number';
     }
 }

+ 10 - 11
packages/core/src/service/helpers/order-calculator/order-calculator.ts

@@ -1,6 +1,7 @@
 import { Injectable } from '@nestjs/common';
 import { filterAsync } from '@vendure/common/lib/filter-async';
 import { AdjustmentType } from '@vendure/common/lib/generated-types';
+import { ID } from '@vendure/common/lib/shared-types';
 
 import { RequestContext } from '../../../api/common/request-context';
 import { RequestContextCacheService } from '../../../cache/request-context-cache.service';
@@ -76,7 +77,6 @@ export class OrderCalculator {
                 ctx,
                 order,
                 updatedOrderLine,
-                activeTaxZone,
                 this.createTaxRateGetter(ctx, activeTaxZone),
             );
         }
@@ -113,7 +113,7 @@ export class OrderCalculator {
     private async applyTaxes(ctx: RequestContext, order: Order, activeZone: Zone) {
         const getTaxRate = this.createTaxRateGetter(ctx, activeZone);
         for (const line of order.lines) {
-            await this.applyTaxesToOrderLine(ctx, order, line, activeZone, getTaxRate);
+            await this.applyTaxesToOrderLine(ctx, order, line, getTaxRate);
         }
         this.calculateOrderTotals(order);
     }
@@ -126,10 +126,9 @@ export class OrderCalculator {
         ctx: RequestContext,
         order: Order,
         line: OrderLine,
-        activeZone: Zone,
-        getTaxRate: (taxCategory: TaxCategory) => Promise<TaxRate>,
+        getTaxRate: (taxCategoryId: ID) => Promise<TaxRate>,
     ) {
-        const applicableTaxRate = await getTaxRate(line.taxCategory);
+        const applicableTaxRate = await getTaxRate(line.taxCategoryId);
         const { taxLineCalculationStrategy } = this.configService.taxOptions;
         line.taxLines = await taxLineCalculationStrategy.calculate({
             ctx,
@@ -147,16 +146,16 @@ export class OrderCalculator {
     private createTaxRateGetter(
         ctx: RequestContext,
         activeZone: Zone,
-    ): (taxCategory: TaxCategory) => Promise<TaxRate> {
-        const taxRateCache = new Map<TaxCategory, TaxRate>();
+    ): (taxCategoryId: ID) => Promise<TaxRate> {
+        const taxRateCache = new Map<ID, TaxRate>();
 
-        return async (taxCategory: TaxCategory): Promise<TaxRate> => {
-            const cached = taxRateCache.get(taxCategory);
+        return async (taxCategoryId: ID): Promise<TaxRate> => {
+            const cached = taxRateCache.get(taxCategoryId);
             if (cached) {
                 return cached;
             }
-            const rate = await this.taxRateService.getApplicableTaxRate(ctx, activeZone, taxCategory);
-            taxRateCache.set(taxCategory, rate);
+            const rate = await this.taxRateService.getApplicableTaxRate(ctx, activeZone, taxCategoryId);
+            taxRateCache.set(taxCategoryId, rate);
             return rate;
         };
     }

+ 1 - 1
packages/core/src/service/helpers/order-modifier/order-modifier.ts

@@ -141,7 +141,7 @@ export class OrderModifier {
     ): Promise<OrderLine | undefined> {
         for (const line of order.lines) {
             const match =
-                idsAreEqual(line.productVariant.id, productVariantId) &&
+                idsAreEqual(line.productVariantId, productVariantId) &&
                 (await this.customFieldsAreEqual(ctx, line, customFields, line.customFields));
             if (match) {
                 return line;

+ 2 - 0
packages/core/src/service/helpers/order-splitter/order-splitter.ts

@@ -90,7 +90,9 @@ export class OrderSplitter {
                 ...pick(line, [
                     'quantity',
                     'productVariant',
+                    'productVariantId',
                     'taxCategory',
+                    'taxCategoryId',
                     'featuredAsset',
                     'shippingLine',
                     'shippingLineId',

+ 1 - 0
packages/core/src/service/services/order-testing.service.ts

@@ -130,6 +130,7 @@ export class OrderTestingService {
                 taxLines: [],
                 quantity: line.quantity,
                 taxCategory: productVariant.taxCategory,
+                taxCategoryId: productVariant.taxCategoryId,
             });
             mockOrder.lines.push(orderLine);
 

+ 24 - 6
packages/core/src/service/services/order.service.ts

@@ -489,7 +489,7 @@ export class OrderService {
      * @since 2.2.0
      */
     async updateOrderCustomer(ctx: RequestContext, { customerId, orderId, note }: SetOrderCustomerInput) {
-        const order = await this.getOrderOrThrow(ctx, orderId);
+        const order = await this.getOrderOrThrow(ctx, orderId, ['channels', 'customer']);
         const currentCustomer = order.customer;
         if (currentCustomer?.id === customerId) {
             // No change in customer, so just return the order as-is
@@ -539,6 +539,7 @@ export class OrderService {
         productVariantId: ID,
         quantity: number,
         customFields?: { [key: string]: any },
+        relations?: RelationPaths<Order>,
     ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const existingOrderLine = await this.orderModifier.getExistingOrderLine(
@@ -597,7 +598,7 @@ export class OrderService {
             await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
         }
         const quantityWasAdjustedDown = correctedQuantity < quantity;
-        const updatedOrder = await this.applyPriceAdjustments(ctx, order, [orderLine]);
+        const updatedOrder = await this.applyPriceAdjustments(ctx, order, [orderLine], relations);
         if (quantityWasAdjustedDown) {
             return new InsufficientStockError({ quantityAvailable: correctedQuantity, order: updatedOrder });
         } else {
@@ -615,6 +616,7 @@ export class OrderService {
         orderLineId: ID,
         quantity: number,
         customFields?: { [key: string]: any },
+        relations?: RelationPaths<Order>,
     ): Promise<ErrorResultUnion<UpdateOrderItemsResult, Order>> {
         const order = await this.getOrderOrThrow(ctx, orderId);
         const orderLine = this.getOrderLineOrThrow(order, orderLineId);
@@ -661,7 +663,7 @@ export class OrderService {
             await this.orderModifier.updateOrderLineQuantity(ctx, orderLine, correctedQuantity, order);
         }
         const quantityWasAdjustedDown = correctedQuantity < quantity;
-        const updatedOrder = await this.applyPriceAdjustments(ctx, order, updatedOrderLines);
+        const updatedOrder = await this.applyPriceAdjustments(ctx, order, updatedOrderLines, relations);
         if (quantityWasAdjustedDown) {
             return new InsufficientStockError({ quantityAvailable: correctedQuantity, order: updatedOrder });
         } else {
@@ -1664,8 +1666,23 @@ export class OrderService {
         return order;
     }
 
-    private async getOrderOrThrow(ctx: RequestContext, orderId: ID): Promise<Order> {
-        const order = await this.findOne(ctx, orderId);
+    private async getOrderOrThrow(
+        ctx: RequestContext,
+        orderId: ID,
+        relations?: RelationPaths<Order>,
+    ): Promise<Order> {
+        const order = await this.findOne(
+            ctx,
+            orderId,
+            relations ?? [
+                'lines',
+                'lines.productVariant',
+                'lines.productVariant.productVariantPrices',
+                'shippingLines',
+                'surcharges',
+                'customer',
+            ],
+        );
         if (!order) {
             throw new EntityNotFoundError('Order', orderId);
         }
@@ -1731,6 +1748,7 @@ export class OrderService {
         ctx: RequestContext,
         order: Order,
         updatedOrderLines?: OrderLine[],
+        relations?: RelationPaths<Order>,
     ): Promise<Order> {
         const promotions = await this.promotionService.getActivePromotionsInChannel(ctx);
         const activePromotionsPre = await this.promotionService.getActivePromotionsOnOrder(ctx, order.id);
@@ -1816,7 +1834,7 @@ export class OrderService {
         await this.connection.getRepository(ctx, ShippingLine).save(order.shippingLines, { reload: false });
         await this.promotionService.runPromotionSideEffects(ctx, order, activePromotionsPre);
 
-        return assertFound(this.findOne(ctx, order.id));
+        return assertFound(this.findOne(ctx, order.id, relations));
     }
 
     /**

+ 5 - 1
packages/core/src/service/services/tax-rate.service.ts

@@ -164,7 +164,11 @@ export class TaxRateService {
      * Returns the applicable TaxRate based on the specified Zone and TaxCategory. Used when calculating Order
      * prices.
      */
-    async getApplicableTaxRate(ctx: RequestContext, zone: Zone, taxCategory: TaxCategory): Promise<TaxRate> {
+    async getApplicableTaxRate(
+        ctx: RequestContext,
+        zone: Zone | ID,
+        taxCategory: TaxCategory | ID,
+    ): Promise<TaxRate> {
         const rate = (await this.getActiveTaxRates(ctx)).find(r => r.test(zone, taxCategory));
         return rate || this.defaultTaxRate;
     }