From 2218d42ac542f34575796671a5d39ea4b7012985 Mon Sep 17 00:00:00 2001 From: Michael Bromley Date: Mon, 19 Jun 2023 11:40:37 +0200 Subject: [PATCH] fix(core): Channel cache can handle more than 1000 channels Fixes #2233 --- .../list-query-builder/list-query-builder.ts | 19 ++++++- .../src/service/services/channel.service.ts | 18 +++++- .../scripts/generate-many-channels.ts | 56 +++++++++++++++++++ .../scripts/generate-past-orders.ts | 5 +- 4 files changed, 93 insertions(+), 5 deletions(-) create mode 100644 packages/dev-server/scripts/generate-many-channels.ts diff --git a/packages/core/src/service/helpers/list-query-builder/list-query-builder.ts b/packages/core/src/service/helpers/list-query-builder/list-query-builder.ts index 665f99d9e5..8e53cc011a 100644 --- a/packages/core/src/service/helpers/list-query-builder/list-query-builder.ts +++ b/packages/core/src/service/helpers/list-query-builder/list-query-builder.ts @@ -112,6 +112,16 @@ export type ExtendedListQueryOptions = { * ``` */ 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 { 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, + 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 }); } diff --git a/packages/core/src/service/services/channel.service.ts b/packages/core/src/service/services/channel.service.ts index bd9dd24362..a9165994c4 100644 --- a/packages/core/src/service/services/channel.service.ts +++ b/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()], }, diff --git a/packages/dev-server/scripts/generate-many-channels.ts b/packages/dev-server/scripts/generate-many-channels.ts new file mode 100644 index 0000000000..d1a6ff460f --- /dev/null +++ b/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); + } + } +} diff --git a/packages/dev-server/scripts/generate-past-orders.ts b/packages/dev-server/scripts/generate-past-orders.ts index eb8bc4b401..169c0b04b5 100644 --- a/packages/dev-server/scripts/generate-past-orders.ts +++ b/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. +// 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(