Skip to content

Commit

Permalink
fix(core): Channel cache can handle more than 1000 channels
Browse files Browse the repository at this point in the history
Fixes #2233
  • Loading branch information
michaelbromley committed Jun 19, 2023
1 parent adca2dd commit 2218d42
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
};

/**
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 });
}
Expand Down
18 changes: 16 additions & 2 deletions packages/core/src/service/services/channel.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()],
},
Expand Down
56 changes: 56 additions & 0 deletions packages/dev-server/scripts/generate-many-channels.ts
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
5 changes: 4 additions & 1 deletion packages/dev-server/scripts/generate-past-orders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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(
Expand Down

0 comments on commit 2218d42

Please sign in to comment.