ソースを参照

Merge branch 'fix/order-item-performance' into next

Michael Bromley 4 年 前
コミット
fa2d94bc97

+ 0 - 2
packages/core/e2e/order-taxes.e2e-spec.ts

@@ -1,7 +1,6 @@
 /* tslint:disable:no-non-null-assertion */
 import { summate } from '@vendure/common/lib/shared-utils';
 import { createErrorResultGuard, createTestEnvironment, ErrorResultGuard } from '@vendure/testing';
-import gql from 'graphql-tag';
 import path from 'path';
 
 import { initialData } from '../../../e2e-common/e2e-initial-data';
@@ -11,7 +10,6 @@ import { testSuccessfulPaymentMethod } from './fixtures/test-payment-methods';
 import { GetProductsWithVariantPrices, UpdateChannel } from './graphql/generated-e2e-admin-types';
 import {
     AddItemToOrder,
-    AdjustmentType,
     GetActiveOrderWithPriceData,
     TestOrderFragmentFragment,
     UpdatedOrderFragment,

+ 2 - 2
packages/core/package.json

@@ -46,7 +46,7 @@
     "@nestjs/platform-express": "7.4.4",
     "@nestjs/terminus": "7.0.1",
     "@nestjs/testing": "7.4.4",
-    "@nestjs/typeorm": "7.1.3",
+    "@nestjs/typeorm": "7.1.5",
     "@types/fs-extra": "^9.0.1",
     "@vendure/common": "^0.18.3",
     "apollo-server-express": "2.18.1",
@@ -74,7 +74,7 @@
     "progress": "^2.0.3",
     "reflect-metadata": "^0.1.13",
     "rxjs": "^6.6.3",
-    "typeorm": "0.2.28"
+    "typeorm": "0.2.31"
   },
   "devDependencies": {
     "@types/bcrypt": "^3.0.0",

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

@@ -27,6 +27,9 @@ export class OrderItem extends VendureEntity {
     @ManyToOne(type => OrderLine, line => line.items, { onDelete: 'CASCADE' })
     line: OrderLine;
 
+    @EntityId()
+    lineId: ID; // TypeORM requires this ID field on the entity explicitly in order to save the foreign key via `.insert`
+
     /**
      * @description
      * The price as calculated when the OrderItem was first added to the Order. Usually will be identical to the

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

@@ -150,18 +150,29 @@ export class OrderModifier {
             if (!orderLine.items) {
                 orderLine.items = [];
             }
+            const newOrderItems = [];
             for (let i = currentQuantity; i < quantity; i++) {
-                const orderItem = await this.connection.getRepository(ctx, OrderItem).save(
+                newOrderItems.push(
                     new OrderItem({
                         listPrice: orderLine.productVariant.price,
                         listPriceIncludesTax: orderLine.productVariant.priceIncludesTax,
                         adjustments: [],
                         taxLines: [],
-                        line: orderLine,
+                        lineId: orderLine.id,
                     }),
                 );
-                orderLine.items.push(orderItem);
             }
+            const { identifiers } = await this.connection
+                .getRepository(ctx, OrderItem)
+                .createQueryBuilder()
+                .insert()
+                .into(OrderItem)
+                .values(newOrderItems)
+                .execute();
+            newOrderItems.forEach((item, i) => (item.id = identifiers[i].id));
+            orderLine.items = await this.connection
+                .getRepository(ctx, OrderItem)
+                .find({ where: { line: orderLine } });
         } else if (quantity < currentQuantity) {
             if (order.active) {
                 // When an Order is still active, it is fine to just delete
@@ -169,7 +180,12 @@ export class OrderModifier {
                 const keepItems = orderLine.items.slice(0, quantity);
                 const removeItems = orderLine.items.slice(quantity);
                 orderLine.items = keepItems;
-                await this.connection.getRepository(ctx, OrderItem).remove(removeItems);
+                await this.connection
+                    .getRepository(ctx, OrderItem)
+                    .createQueryBuilder()
+                    .delete()
+                    .whereInIds(removeItems.map(i => i.id))
+                    .execute();
             } else {
                 // When an Order is not active (i.e. Customer checked out), then we don't want to just
                 // delete the OrderItems - instead we will cancel them

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

@@ -1337,8 +1337,26 @@ export class OrderService {
             promotions,
             updatedOrderLine ? [updatedOrderLine] : [],
         );
+        const updateFields: Array<keyof OrderItem> = [
+            'initialListPrice',
+            'listPrice',
+            'listPriceIncludesTax',
+            'adjustments',
+            'taxLines',
+        ];
+        await this.connection
+            .getRepository(ctx, OrderItem)
+            .createQueryBuilder()
+            .insert()
+            .into(OrderItem, [...updateFields, 'id', 'lineId'])
+            .values(updatedItems)
+            .orUpdate({
+                conflict_target: ['id'],
+                overwrite: updateFields,
+            })
+            .updateEntity(false)
+            .execute();
         await this.connection.getRepository(ctx, Order).save(order, { reload: false });
-        await this.connection.getRepository(ctx, OrderItem).save(updatedItems, { reload: false });
         await this.connection.getRepository(ctx, ShippingLine).save(order.shippingLines, { reload: false });
         return order;
     }

+ 12 - 0
packages/dev-server/load-testing/graphql/shop/adjust-order-line.graphql

@@ -0,0 +1,12 @@
+mutation ($id: ID! $qty: Int!) {
+    adjustOrderLine(orderLineId: $id quantity: $qty) {
+        ...on Order {
+            id
+            code
+            totalQuantity
+        }
+        ...on ErrorResult {
+            errorCode
+        }
+    }
+}

+ 49 - 0
packages/dev-server/load-testing/scripts/very-large-order3.js

@@ -0,0 +1,49 @@
+// @ts-check
+import { check } from 'k6';
+import { ShopApiRequest } from '../utils/api-request.js';
+
+const searchQuery = new ShopApiRequest('shop/search.graphql');
+const addItemToOrderMutation = new ShopApiRequest('shop/add-to-order.graphql');
+const adjustOrderLineMutation = new ShopApiRequest('shop/adjust-order-line.graphql');
+
+export let options = {
+    stages: [{ duration: '1m', target: 1 }],
+};
+
+export function setup() {
+    const searchResult = searchQuery.post();
+    const items = searchResult.data.search.items;
+    return items;
+}
+
+/**
+ * Continuously adds random items to a single order for the duration of the test.
+ * Just like very-large-order.js but adds 999 items each time, and runs for only 1 minute.
+ * Both addItemToOrder and adjustOrderLine are tested as the latter needs the first and tends to be more complex/slower.
+ */
+export default function (products) {
+    const orderLineId = addToCart(randomItem(products).productVariantId, 999);
+    adjustOrderLine(orderLineId, 1);
+    adjustOrderLine(orderLineId, 999);
+    adjustOrderLine(orderLineId, 0);
+}
+
+function addToCart(variantId, qty) {
+    const result = addItemToOrderMutation.post({ id: variantId, qty });
+    check(result.data, {
+        'Product added to cart': r =>
+            !!r.addItemToOrder.lines.find(l => l.productVariant.id === variantId && l.quantity === qty),
+    });
+    return result.data.addItemToOrder.lines.find(l => l.productVariant.id === variantId).id;
+}
+
+function adjustOrderLine(orderLineId, qty) {
+    const result = adjustOrderLineMutation.post({ id: orderLineId, qty });
+    check(result.data, {
+        'Product quantity adjusted': r => r.adjustOrderLine.totalQuantity === qty,
+    });
+}
+
+function randomItem(items) {
+    return items[Math.floor(Math.random() * items.length)];
+}

+ 66 - 36
yarn.lock

@@ -3023,12 +3023,12 @@
     optional "0.1.4"
     tslib "2.0.1"
 
-"@nestjs/typeorm@7.1.3":
-  version "7.1.3"
-  resolved "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-7.1.3.tgz#348324c3c9792440d2314350d4e3f9136f98e67c"
-  integrity sha512-BoPZ3LRkJauejMN2rsbX+7edfHDADQSxZV48XvtObo1uOAhXM9yiszU06MP0tuMuu+QmdKEqsleyynNaMjAfsg==
+"@nestjs/typeorm@7.1.5":
+  version "7.1.5"
+  resolved "https://registry.yarnpkg.com/@nestjs/typeorm/-/typeorm-7.1.5.tgz#50e3bf85ff8cf78d47d8dd19210c5f02b488f5e3"
+  integrity sha512-utE1FkYM/gyCXUqw3zKYYS0YZ3DfkAnzsCx4T48cNnSDTCeWS+u3yt0FMDFjwSiQSaLrzpiSff/FaxJQvRlYow==
   dependencies:
-    uuid "8.3.0"
+    uuid "8.3.1"
 
 "@ng-select/ng-select@^5.0.3":
   version "5.0.3"
@@ -6227,17 +6227,17 @@ cli-cursor@^3.1.0:
   dependencies:
     restore-cursor "^3.1.0"
 
-cli-highlight@^2.1.4:
-  version "2.1.4"
-  resolved "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.4.tgz#098cb642cf17f42adc1c1145e07f960ec4d7522b"
-  integrity sha512-s7Zofobm20qriqDoU9sXptQx0t2R9PEgac92mENNm7xaEe1hn71IIMsXMK+6encA6WRCWWxIGQbipr3q998tlQ==
+cli-highlight@^2.1.10:
+  version "2.1.10"
+  resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.10.tgz#26a087da9209dce4fcb8cf5427dc97cd96ac173a"
+  integrity sha512-CcPFD3JwdQ2oSzy+AMG6j3LRTkNjM82kzcSKzoVw6cLanDCJNlsLjeqVTOTfOfucnWv5F0rmBemVf1m9JiIasw==
   dependencies:
-    chalk "^3.0.0"
-    highlight.js "^9.6.0"
+    chalk "^4.0.0"
+    highlight.js "^10.0.0"
     mz "^2.4.0"
     parse5 "^5.1.1"
-    parse5-htmlparser2-tree-adapter "^5.1.1"
-    yargs "^15.0.0"
+    parse5-htmlparser2-tree-adapter "^6.0.0"
+    yargs "^16.0.0"
 
 cli-spinners@^2.4.0:
   version "2.4.0"
@@ -6306,6 +6306,15 @@ cliui@^7.0.0:
     strip-ansi "^6.0.0"
     wrap-ansi "^7.0.0"
 
+cliui@^7.0.2:
+  version "7.0.4"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f"
+  integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^7.0.0"
+
 clone-buffer@^1.0.0:
   version "1.0.0"
   resolved "https://registry.npmjs.org/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58"
@@ -8182,6 +8191,11 @@ escalade@^3.0.2, escalade@^3.1.0:
   resolved "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz#e8e2d7c7a8b76f6ee64c2181d6b8151441602d4e"
   integrity sha512-mAk+hPSO8fLDkhV7V0dXazH5pDc6MrjBTPyD3VeKzxnVFjH1MIxbCdqGZB9O8+EwWakZs3ZCbDS4IpRt79V1ig==
 
+escalade@^3.1.1:
+  version "3.1.1"
+  resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
+  integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
+
 escape-goat@^3.0.0:
   version "3.0.0"
   resolved "https://registry.npmjs.org/escape-goat/-/escape-goat-3.0.0.tgz#e8b5fb658553fe8a3c4959c316c6ebb8c842b19c"
@@ -9617,10 +9631,10 @@ hex-color-regex@^1.1.0:
   resolved "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz#4c06fccb4602fe2602b3c93df82d7e7dbf1a8a8e"
   integrity sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==
 
-highlight.js@^9.6.0:
-  version "9.18.3"
-  resolved "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.3.tgz#a1a0a2028d5e3149e2380f8a865ee8516703d634"
-  integrity sha512-zBZAmhSupHIl5sITeMqIJnYCDfAEc3Gdkqj65wC1lpI468MMQeeQkhcIAvk+RylAkxrCcI9xy9piHiXeQ1BdzQ==
+highlight.js@^10.0.0:
+  version "10.6.0"
+  resolved "https://registry.yarnpkg.com/highlight.js/-/highlight.js-10.6.0.tgz#0073aa71d566906965ba6e1b7be7b2682f5e18b6"
+  integrity sha512-8mlRcn5vk/r4+QcqerapwBYTe+iPL5ih6xrNylxrnBdHQiijDETfXX7VIxC3UiCRiINBJfANBAsPzAvRQj8RpQ==
 
 hmac-drbg@^1.0.0:
   version "1.0.1"
@@ -14252,20 +14266,13 @@ parse-url@^5.0.0:
     parse-path "^4.0.0"
     protocols "^1.4.0"
 
-parse5-htmlparser2-tree-adapter@6.0.1:
+parse5-htmlparser2-tree-adapter@6.0.1, parse5-htmlparser2-tree-adapter@^6.0.0:
   version "6.0.1"
   resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz#2cdf9ad823321140370d4dbf5d3e92c7c8ddc6e6"
   integrity sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==
   dependencies:
     parse5 "^6.0.1"
 
-parse5-htmlparser2-tree-adapter@^5.1.1:
-  version "5.1.1"
-  resolved "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-5.1.1.tgz#e8c743d4e92194d5293ecde2b08be31e67461cbc"
-  integrity sha512-CF+TKjXqoqyDwHqBhFQ+3l5t83xYi6fVT1tQNg+Ye0JRLnTxWvIroCjEp1A0k4lneHNBGnICUf0cfYVYGEazqw==
-  dependencies:
-    parse5 "^5.1.1"
-
 parse5@5.1.1, parse5@^5.0.0, parse5@^5.1.1:
   version "5.1.1"
   resolved "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz#f68e4e5ba1852ac2cadc00f4555fff6c2abb6178"
@@ -18036,16 +18043,16 @@ typedarray@^0.0.6:
   resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
   integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=
 
-typeorm@0.2.28:
-  version "0.2.28"
-  resolved "https://registry.npmjs.org/typeorm/-/typeorm-0.2.28.tgz#828df288d01ca75b38e990fa1628a7cd5a29f39f"
-  integrity sha512-BTtUBGwsFzODvHY+AlWL9pvJ2uEj8qpHwmo03z43RvZkG8BAryQJQ3lZ7HlGvI9IQU8y1IYGWe97HsVr8kXB9g==
+typeorm@0.2.31:
+  version "0.2.31"
+  resolved "https://registry.yarnpkg.com/typeorm/-/typeorm-0.2.31.tgz#82b8a1b233224f81c738f53b0380386ccf360917"
+  integrity sha512-dVvCEVHH48DG0QPXAKfo0l6ecQrl3A8ucGP4Yw4myz4YEDMProebTQo8as83uyES+nrwCbu3qdkL4ncC2+qcMA==
   dependencies:
     "@sqltools/formatter" "1.2.2"
     app-root-path "^3.0.0"
     buffer "^5.5.0"
     chalk "^4.1.0"
-    cli-highlight "^2.1.4"
+    cli-highlight "^2.1.10"
     debug "^4.1.1"
     dotenv "^8.2.0"
     glob "^7.1.6"
@@ -18376,16 +18383,16 @@ uuid@8.3.0:
   resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea"
   integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==
 
+uuid@8.3.1, uuid@^8.0.0, uuid@^8.3.0:
+  version "8.3.1"
+  resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
+  integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
+
 uuid@^3.0.0, uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0:
   version "3.4.0"
   resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
   integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
 
-uuid@^8.0.0, uuid@^8.3.0:
-  version "8.3.1"
-  resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31"
-  integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg==
-
 v8-to-istanbul@^5.0.1:
   version "5.0.1"
   resolved "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-5.0.1.tgz#0608f5b49a481458625edb058488607f25498ba5"
@@ -19091,6 +19098,11 @@ y18n@^5.0.1:
   resolved "https://registry.npmjs.org/y18n/-/y18n-5.0.2.tgz#48218df5da2731b4403115c39a1af709c873f829"
   integrity sha512-CkwaeZw6dQgqgPGeTWKMXCRmMcBgETFlTml1+ZOO+q7kGst8NREJ+eWwFNPVUQ4QGdAaklbqCZHH6Zuep1RjiA==
 
+y18n@^5.0.5:
+  version "5.0.5"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.5.tgz#8769ec08d03b1ea2df2500acef561743bbb9ab18"
+  integrity sha512-hsRUr4FFrvhhRH12wOdfs38Gy7k2FFzB9qgN9v3aLykRq0dRcdcpz5C9FxdS2NuhOrI/628b/KSTJ3rwHysYSg==
+
 yaeti@^0.0.6:
   version "0.0.6"
   resolved "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577"
@@ -19167,6 +19179,11 @@ yargs-parser@^18.1.0, yargs-parser@^18.1.2, yargs-parser@^18.1.3:
     camelcase "^5.0.0"
     decamelize "^1.2.0"
 
+yargs-parser@^20.2.2:
+  version "20.2.5"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.5.tgz#5d37729146d3f894f39fc94b6796f5b239513186"
+  integrity sha512-jYRGS3zWy20NtDtK2kBgo/TlAoy5YUuhD9/LZ7z7W4j1Fdw2cqD0xEEclf8fxc8xjD6X5Qr+qQQwCEsP8iRiYg==
+
 yargs@15.3.0:
   version "15.3.0"
   resolved "https://registry.npmjs.org/yargs/-/yargs-15.3.0.tgz#403af6edc75b3ae04bf66c94202228ba119f0976"
@@ -19217,7 +19234,7 @@ yargs@^14.2.2:
     y18n "^4.0.0"
     yargs-parser "^15.0.1"
 
-yargs@^15.0.0, yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.1:
+yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.1:
   version "15.4.1"
   resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
   integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
@@ -19234,6 +19251,19 @@ yargs@^15.0.0, yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.1:
     y18n "^4.0.0"
     yargs-parser "^18.1.2"
 
+yargs@^16.0.0:
+  version "16.2.0"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66"
+  integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==
+  dependencies:
+    cliui "^7.0.2"
+    escalade "^3.1.1"
+    get-caller-file "^2.0.5"
+    require-directory "^2.1.1"
+    string-width "^4.2.0"
+    y18n "^5.0.5"
+    yargs-parser "^20.2.2"
+
 yargs@^16.0.3:
   version "16.0.3"
   resolved "https://registry.npmjs.org/yargs/-/yargs-16.0.3.tgz#7a919b9e43c90f80d4a142a89795e85399a7e54c"