Skip to content

Commit

Permalink
perf(core): Improved performance of validateVariantOptionIds (#337)
Browse files Browse the repository at this point in the history
These changes reduce the amount of data loaded into memory which hopefully prevents some server crashes. #328.

Also added the eager option for allowing performance gains in certain queries.
  • Loading branch information
Tyratox authored May 11, 2020
1 parent 1fb5fb4 commit 7d19b9c
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 14 deletions.
14 changes: 10 additions & 4 deletions packages/core/src/service/helpers/utils/channel-aware-orm-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,14 @@ export function findByIdsInChannel<T extends ChannelAware | VendureEntity>(
ids: ID[],
channelId: ID,
findOptions?: FindManyOptions<T>,
eager = true,
) {
const qb = connection.getRepository(entity).createQueryBuilder('product');
FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, findOptions);
// tslint:disable-next-line:no-non-null-assertion
FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
if (eager) {
// tslint:disable-next-line:no-non-null-assertion
FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
}
return qb
.leftJoin('product.channels', 'channel')
.andWhere('channel.id = :channelId', { channelId })
Expand All @@ -35,11 +38,14 @@ export function findOneInChannel<T extends ChannelAware | VendureEntity>(
id: ID,
channelId: ID,
findOptions?: FindManyOptions<T>,
eager = true,
) {
const qb = connection.getRepository(entity).createQueryBuilder('product');
FindOptionsUtils.applyFindManyOptionsOrConditionsToQueryBuilder(qb, findOptions);
// tslint:disable-next-line:no-non-null-assertion
FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
if (eager) {
// tslint:disable-next-line:no-non-null-assertion
FindOptionsUtils.joinEagerRelations(qb, qb.alias, qb.expressionMap.mainAlias!.metadata);
}
return qb
.leftJoin('product.channels', 'channel')
.andWhere('product.id = :id', { id })
Expand Down
11 changes: 10 additions & 1 deletion packages/core/src/service/helpers/utils/get-entity-or-throw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,26 @@ export async function getEntityOrThrow<T extends VendureEntity | ChannelAware>(
id: ID,
channelId: ID,
findOptions?: FindOneOptions<T>,
eager?: boolean,
): Promise<T>;
export async function getEntityOrThrow<T extends VendureEntity>(
connection: Connection,
entityType: Type<T>,
id: ID,
findOptionsOrChannelId?: FindOneOptions<T> | ID,
maybeFindOptions?: FindOneOptions<T>,
eager?: boolean,
): Promise<T> {
let entity: T | undefined;
if (isId(findOptionsOrChannelId)) {
entity = await findOneInChannel(connection, entityType, id, findOptionsOrChannelId, maybeFindOptions);
entity = await findOneInChannel(
connection,
entityType,
id,
findOptionsOrChannelId,
maybeFindOptions,
eager,
);
} else {
entity = await connection.getRepository(entityType).findOne(id, findOptionsOrChannelId);
}
Expand Down
43 changes: 34 additions & 9 deletions packages/core/src/service/services/product-variant.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,25 +362,50 @@ export class ProductVariantService {
}

private async validateVariantOptionIds(ctx: RequestContext, input: CreateProductVariantInput) {
const product = await getEntityOrThrow(this.connection, Product, input.productId, ctx.channelId, {
relations: ['optionGroups', 'optionGroups.options', 'variants', 'variants.options'],
});
const optionIds = [...(input.optionIds || [])];
// this could be done with less queries but depending on the data, node will crash
// https://github.com/vendure-ecommerce/vendure/issues/328
const optionGroups = (
await getEntityOrThrow(
this.connection,
Product,
input.productId,
ctx.channelId,
{
relations: ['optionGroups', 'optionGroups.options'],
},
false,
)
).optionGroups;

if (optionIds.length !== product.optionGroups.length) {
this.throwIncompatibleOptionsError(product.optionGroups);
const optionIds = input.optionIds || [];

if (optionIds.length !== optionGroups.length) {
this.throwIncompatibleOptionsError(optionGroups);
}
if (
!samplesEach(
optionIds,
product.optionGroups.map(g => g.options.map(o => o.id)),
optionGroups.map((g) => g.options.map((o) => o.id)),
)
) {
this.throwIncompatibleOptionsError(product.optionGroups);
this.throwIncompatibleOptionsError(optionGroups);
}

const product = await getEntityOrThrow(
this.connection,
Product,
input.productId,
ctx.channelId,
{
relations: ['variants', 'variants.options'],
},
false,
);

const inputOptionIds = this.sortJoin(optionIds, ',');

product.variants.forEach(variant => {
const variantOptionIds = this.sortJoin(variant.options, ',', 'id');
const inputOptionIds = this.sortJoin(input.optionIds || [], ',');
if (variantOptionIds === inputOptionIds) {
throw new UserInputError('error.product-variant-options-combination-already-exists', {
optionNames: this.sortJoin(variant.options, ', ', 'code'),
Expand Down

0 comments on commit 7d19b9c

Please sign in to comment.