Procházet zdrojové kódy

fix(core): Channel cache can handle more than 1000 channels

Fixes #2233
Michael Bromley před 2 roky
rodič
revize
2218d42ac5

+ 17 - 2
packages/core/src/service/helpers/list-query-builder/list-query-builder.ts

@@ -112,6 +112,16 @@ export type ExtendedListQueryOptions<T extends VendureEntity> = {
      * ```
      */
     customPropertyMap?: { [name: string]: string };
+    /**
+     * @description
+     * When set to `true`, the configured `shopListQueryLimit` and `adminListQueryLimit` values will be ignored,
+     * allowing unlimited results to be returned. Use caution when exposing an unlimited list query to the public,
+     * as it could become a vector for a denial of service attack if an attacker requests a very large list.
+     *
+     * @since 2.0.2
+     * @default false
+     */
+    ignoreQueryLimits?: boolean;
 };
 
 /**
@@ -206,7 +216,7 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
     ): SelectQueryBuilder<T> {
         const apiType = extendedOptions.ctx?.apiType ?? 'shop';
         const rawConnection = this.connection.rawConnection;
-        const { take, skip } = this.parseTakeSkipParams(apiType, options);
+        const { take, skip } = this.parseTakeSkipParams(apiType, options, extendedOptions.ignoreQueryLimits);
 
         const repo = extendedOptions.ctx
             ? this.connection.getRepository(extendedOptions.ctx, entity)
@@ -285,9 +295,14 @@ export class ListQueryBuilder implements OnApplicationBootstrap {
     private parseTakeSkipParams(
         apiType: ApiType,
         options: ListQueryOptions<any>,
+        ignoreQueryLimits = false,
     ): { take: number; skip: number } {
         const { shopListQueryLimit, adminListQueryLimit } = this.configService.apiOptions;
-        const takeLimit = apiType === 'admin' ? adminListQueryLimit : shopListQueryLimit;
+        const takeLimit = ignoreQueryLimits
+            ? Number.MAX_SAFE_INTEGER
+            : apiType === 'admin'
+            ? adminListQueryLimit
+            : shopListQueryLimit;
         if (options.take && options.take > takeLimit) {
             throw new UserInputError('error.list-query-limit-exceeded', { limit: takeLimit });
         }

+ 16 - 2
packages/core/src/service/services/channel.service.ts

@@ -85,8 +85,22 @@ export class ChannelService {
             ttl: this.configService.entityOptions.channelCacheTtl,
             refresh: {
                 fn: async ctx => {
-                    const { items } = await this.findAll(ctx);
-                    return items;
+                    const result = await this.listQueryBuilder
+                        .build(
+                            Channel,
+                            {},
+                            {
+                                ctx,
+                                relations: ['defaultShippingZone', 'defaultTaxZone'],
+                                ignoreQueryLimits: true,
+                            },
+                        )
+                        .getManyAndCount()
+                        .then(([items, totalItems]) => ({
+                            items,
+                            totalItems,
+                        }));
+                    return result.items;
                 },
                 defaultArgs: [RequestContext.empty()],
             },

+ 56 - 0
packages/dev-server/scripts/generate-many-channels.ts

@@ -0,0 +1,56 @@
+/* eslint-disable no-console */
+import {
+    bootstrapWorker,
+    ChannelService,
+    CurrencyCode,
+    isGraphQlErrorResult,
+    LanguageCode,
+    RequestContextService,
+    RoleService,
+} from '@vendure/core';
+
+import { devConfig } from '../dev-config';
+
+const CHANNEL_COUNT = 1001;
+
+generateManyChannels()
+    .then(() => process.exit(0))
+    .catch(() => process.exit(1));
+
+// Used for testing scenarios where there are many channels
+// such as https://github.com/vendure-ecommerce/vendure/issues/2233
+async function generateManyChannels() {
+    const { app } = await bootstrapWorker(devConfig);
+    const requestContextService = app.get(RequestContextService);
+    const channelService = app.get(ChannelService);
+    const roleService = app.get(RoleService);
+
+    const ctxAdmin = await requestContextService.create({
+        apiType: 'admin',
+    });
+
+    const superAdminRole = await roleService.getSuperAdminRole(ctxAdmin);
+    const customerRole = await roleService.getCustomerRole(ctxAdmin);
+
+    for (let i = CHANNEL_COUNT; i > 0; i--) {
+        const channel = await channelService.create(ctxAdmin, {
+            code: `channel-test-${i}`,
+            token: `channel--test-${i}`,
+            defaultLanguageCode: LanguageCode.en,
+            availableLanguageCodes: [LanguageCode.en],
+            pricesIncludeTax: true,
+            defaultCurrencyCode: CurrencyCode.USD,
+            availableCurrencyCodes: [CurrencyCode.USD],
+            sellerId: 1,
+            defaultTaxZoneId: 1,
+            defaultShippingZoneId: 1,
+        });
+        if (isGraphQlErrorResult(channel)) {
+            console.log(channel.message);
+        } else {
+            console.log(`Created channel ${channel.code}`);
+            await roleService.assignRoleToChannel(ctxAdmin, superAdminRole.id, channel.id);
+            await roleService.assignRoleToChannel(ctxAdmin, customerRole.id, channel.id);
+        }
+    }
+}

+ 4 - 1
packages/dev-server/scripts/generate-past-orders.ts

@@ -19,6 +19,10 @@ generatePastOrders()
     .then(() => process.exit(0))
     .catch(() => process.exit(1));
 
+const DAYS_TO_COVER = 30;
+
+// This script generates a large number of past Orders over the past <DAYS_TO_COVER> days.
+// It is useful for testing scenarios where there are a large number of Orders in the system.
 async function generatePastOrders() {
     const { app } = await bootstrapWorker(devConfig);
     const requestContextService = app.get(RequestContextService);
@@ -38,7 +42,6 @@ async function generatePastOrders() {
     const { items: variants } = await productVariantService.findAll(ctxAdmin, { take: 500 });
     const { items: customers } = await customerService.findAll(ctxAdmin, { take: 500 }, ['user']);
 
-    const DAYS_TO_COVER = 30;
     for (let i = DAYS_TO_COVER; i > 0; i--) {
         const numberOfOrders = Math.floor(Math.random() * 10) + 5;
         Logger.info(