@vendure/core
Version:
A modern, headless ecommerce framework
394 lines • 20.5 kB
JavaScript
"use strict";
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProductOptionGroupService = void 0;
const common_1 = require("@nestjs/common");
const generated_types_1 = require("@vendure/common/lib/generated-types");
const typeorm_1 = require("typeorm");
const common_2 = require("../../common");
const generated_graphql_admin_errors_1 = require("../../common/error/generated-graphql-admin-errors");
const instrument_decorator_1 = require("../../common/instrument-decorator");
const utils_1 = require("../../common/utils");
const transactional_connection_1 = require("../../connection/transactional-connection");
const product_option_group_translation_entity_1 = require("../../entity/product-option-group/product-option-group-translation.entity");
const product_option_group_entity_1 = require("../../entity/product-option-group/product-option-group.entity");
const product_option_entity_1 = require("../../entity/product-option/product-option.entity");
const product_variant_entity_1 = require("../../entity/product-variant/product-variant.entity");
const product_entity_1 = require("../../entity/product/product.entity");
const event_bus_1 = require("../../event-bus");
const product_option_group_event_1 = require("../../event-bus/events/product-option-group-event");
const custom_field_relation_service_1 = require("../helpers/custom-field-relation/custom-field-relation.service");
const list_query_builder_1 = require("../helpers/list-query-builder/list-query-builder");
const translatable_saver_1 = require("../helpers/translatable-saver/translatable-saver");
const translator_service_1 = require("../helpers/translator/translator.service");
const channel_service_1 = require("./channel.service");
const product_option_service_1 = require("./product-option.service");
const role_service_1 = require("./role.service");
/**
* @description
* Contains methods relating to {@link ProductOptionGroup} entities.
*
* @docsCategory services
*/
let ProductOptionGroupService = class ProductOptionGroupService {
constructor(connection, translatableSaver, customFieldRelationService, productOptionService, eventBus, translator, listQueryBuilder, channelService, roleService) {
this.connection = connection;
this.translatableSaver = translatableSaver;
this.customFieldRelationService = customFieldRelationService;
this.productOptionService = productOptionService;
this.eventBus = eventBus;
this.translator = translator;
this.listQueryBuilder = listQueryBuilder;
this.channelService = channelService;
this.roleService = roleService;
}
findAll(ctx, options, relations) {
return this.listQueryBuilder
.build(product_option_group_entity_1.ProductOptionGroup, options, {
relations: relations !== null && relations !== void 0 ? relations : ['options'],
ctx,
channelId: ctx.channelId,
where: {
deletedAt: (0, typeorm_1.IsNull)(),
},
})
.getManyAndCount()
.then(([groups, totalItems]) => ({
items: groups.map(group => this.translator.translate(group, ctx, ['options'])),
totalItems,
}));
}
findOne(ctx, id, relations, findOneOptions) {
return this.connection
.findOneInChannel(ctx, product_option_group_entity_1.ProductOptionGroup, id, ctx.channelId, {
relations: relations !== null && relations !== void 0 ? relations : ['options'],
})
.then(group => {
if (!group) {
return undefined;
}
if (group.deletedAt && !(findOneOptions === null || findOneOptions === void 0 ? void 0 : findOneOptions.includeSoftDeleted)) {
return undefined;
}
return this.translator.translate(group, ctx, ['options']);
});
}
getOptionGroupsByProductId(ctx, id) {
return this.connection
.getRepository(ctx, product_option_group_entity_1.ProductOptionGroup)
.createQueryBuilder('optionGroup')
.leftJoinAndSelect('optionGroup.translations', 'translations')
.leftJoinAndSelect('optionGroup.options', 'options')
.leftJoinAndSelect('options.translations', 'optionTranslations')
.innerJoin('optionGroup.products', 'product', 'product.id = :productId', { productId: id })
.where('optionGroup.deletedAt IS NULL')
.orderBy('optionGroup.id', 'ASC')
.getMany()
.then(groups => groups.map(group => this.translator.translate(group, ctx, ['options'])));
}
/**
* @description
* Returns all Channels to which the ProductOptionGroup is assigned.
*/
async getOptionGroupChannels(ctx, optionGroupId) {
const optionGroup = await this.connection.getEntityOrThrow(ctx, product_option_group_entity_1.ProductOptionGroup, optionGroupId, {
relations: ['channels'],
channelId: ctx.channelId,
});
return optionGroup.channels;
}
/**
* @description
* Returns the number of non-deleted Products in the current channel
* that use the given ProductOptionGroup.
*/
getProductCount(ctx, optionGroupId) {
return this.connection
.getRepository(ctx, product_entity_1.Product)
.createQueryBuilder('product')
.innerJoin('product.optionGroups', 'optionGroup', 'optionGroup.id = :optionGroupId', {
optionGroupId,
})
.innerJoin('product.channels', 'channel', 'channel.id = :channelId', {
channelId: ctx.channelId,
})
.where('product.deletedAt IS NULL')
.getCount();
}
async create(ctx, input) {
const group = await this.translatableSaver.create({
ctx,
input,
entityType: product_option_group_entity_1.ProductOptionGroup,
translationType: product_option_group_translation_entity_1.ProductOptionGroupTranslation,
beforeSave: async (g) => {
await this.channelService.assignToCurrentChannel(g, ctx);
},
});
const groupWithRelations = await this.customFieldRelationService.updateRelations(ctx, product_option_group_entity_1.ProductOptionGroup, input, group);
await this.eventBus.publish(new product_option_group_event_1.ProductOptionGroupEvent(ctx, groupWithRelations, 'created', input));
return (0, utils_1.assertFound)(this.findOne(ctx, group.id));
}
async update(ctx, input) {
// Ensure the entity belongs to the active channel before updating.
await this.connection.getEntityOrThrow(ctx, product_option_group_entity_1.ProductOptionGroup, input.id, {
channelId: ctx.channelId,
});
const group = await this.translatableSaver.update({
ctx,
input,
entityType: product_option_group_entity_1.ProductOptionGroup,
translationType: product_option_group_translation_entity_1.ProductOptionGroupTranslation,
});
await this.customFieldRelationService.updateRelations(ctx, product_option_group_entity_1.ProductOptionGroup, input, group);
await this.eventBus.publish(new product_option_group_event_1.ProductOptionGroupEvent(ctx, group, 'updated', input));
return (0, utils_1.assertFound)(this.findOne(ctx, group.id));
}
/**
* @description
* Deletes a ProductOptionGroup. If the group is in use by any Products, the deletion
* will fail unless `force` is set to `true`.
*/
async delete(ctx, id, force = false) {
var _a, _b, _c, _d;
const optionGroup = await this.connection.getEntityOrThrow(ctx, product_option_group_entity_1.ProductOptionGroup, id, {
relationLoadStrategy: 'query',
loadEagerRelations: false,
relations: ['options', 'products'],
channelId: ctx.channelId,
});
const productCount = (_b = (_a = optionGroup.products) === null || _a === void 0 ? void 0 : _a.filter(p => p.deletedAt == null).length) !== null && _b !== void 0 ? _b : 0;
if (productCount > 0 && !force) {
return {
result: generated_types_1.DeletionResult.NOT_DELETED,
message: ctx.translate('message.product-option-group-used', {
code: optionGroup.code,
count: productCount,
}),
};
}
if (productCount > 0 && force) {
// Detach from all products
for (const product of optionGroup.products) {
await this.connection
.getRepository(ctx, product_entity_1.Product)
.createQueryBuilder()
.relation('optionGroups')
.of(product.id)
.remove(id);
}
}
const deletedOptionGroup = new product_option_group_entity_1.ProductOptionGroup(optionGroup);
// Delete child options first
const optionsToDelete = (_d = (_c = optionGroup.options) === null || _c === void 0 ? void 0 : _c.filter(o => !o.deletedAt)) !== null && _d !== void 0 ? _d : [];
for (const option of optionsToDelete) {
const { result, message } = await this.productOptionService.delete(ctx, option.id);
if (result === generated_types_1.DeletionResult.NOT_DELETED) {
return { result, message };
}
}
const hasOptionsWhichAreInUse = await this.groupOptionsAreInUse(ctx, optionGroup);
if (hasOptionsWhichAreInUse > 0) {
// soft delete
optionGroup.deletedAt = new Date();
await this.connection.getRepository(ctx, product_option_group_entity_1.ProductOptionGroup).save(optionGroup, { reload: false });
}
else {
// hard delete
await this.connection.getRepository(ctx, product_option_group_entity_1.ProductOptionGroup).remove(optionGroup);
}
await this.eventBus.publish(new product_option_group_event_1.ProductOptionGroupEvent(ctx, deletedOptionGroup, 'deleted', id));
return {
result: generated_types_1.DeletionResult.DELETED,
};
}
/**
* @description
* Deletes the ProductOptionGroup and any associated ProductOptions. If the ProductOptionGroup
* is still referenced by a soft-deleted Product, then a soft-delete will be used to preserve
* referential integrity. Otherwise a hard-delete will be performed.
*
* @deprecated Use {@link ProductOptionGroupService.delete} instead.
*/
async deleteGroupAndOptionsFromProduct(ctx, id, productId) {
const optionGroup = await this.connection.getEntityOrThrow(ctx, product_option_group_entity_1.ProductOptionGroup, id, {
relationLoadStrategy: 'query',
loadEagerRelations: false,
relations: ['options', 'products'],
});
const deletedOptionGroup = new product_option_group_entity_1.ProductOptionGroup(optionGroup);
const inUseByActiveProducts = await this.isInUseByOtherProducts(ctx, optionGroup, productId);
if (inUseByActiveProducts > 0) {
return {
result: generated_types_1.DeletionResult.NOT_DELETED,
message: ctx.translate('message.product-option-group-used', {
code: optionGroup.code,
count: inUseByActiveProducts,
}),
};
}
const optionsToDelete = optionGroup.options && optionGroup.options.filter(group => !group.deletedAt);
for (const option of optionsToDelete) {
const { result, message } = await this.productOptionService.delete(ctx, option.id);
if (result === generated_types_1.DeletionResult.NOT_DELETED) {
await this.connection.rollBackTransaction(ctx);
return { result, message };
}
}
const hasOptionsWhichAreInUse = await this.groupOptionsAreInUse(ctx, optionGroup);
if (hasOptionsWhichAreInUse > 0) {
// soft delete
optionGroup.deletedAt = new Date();
await this.connection.getRepository(ctx, product_option_group_entity_1.ProductOptionGroup).save(optionGroup, { reload: false });
}
else {
// hard delete
const product = await this.connection.getRepository(ctx, product_entity_1.Product).findOne({
relationLoadStrategy: 'query',
loadEagerRelations: false,
where: { id: productId },
relations: ['optionGroups'],
});
if (product) {
product.optionGroups = product.optionGroups.filter(og => !(0, utils_1.idsAreEqual)(og.id, id));
await this.connection.getRepository(ctx, product_entity_1.Product).save(product, { reload: false });
}
await this.connection.getRepository(ctx, product_option_group_entity_1.ProductOptionGroup).remove(optionGroup);
}
await this.eventBus.publish(new product_option_group_event_1.ProductOptionGroupEvent(ctx, deletedOptionGroup, 'deleted', id));
return {
result: generated_types_1.DeletionResult.DELETED,
};
}
/**
* @description
* Assigns ProductOptionGroups to the specified Channel
*/
async assignProductOptionGroupsToChannel(ctx, input) {
const hasPermission = await this.roleService.userHasAnyPermissionsOnChannel(ctx, input.channelId, [
generated_types_1.Permission.UpdateCatalog,
generated_types_1.Permission.UpdateProduct,
]);
if (!hasPermission) {
throw new common_2.ForbiddenError();
}
const groupsToAssign = await this.connection
.getRepository(ctx, product_option_group_entity_1.ProductOptionGroup)
.find({ where: { id: (0, typeorm_1.In)(input.productOptionGroupIds) }, relations: ['options'] });
const optionsToAssign = groupsToAssign.reduce((options, group) => [...options, ...group.options], []);
await Promise.all([
...groupsToAssign.map(group => this.channelService.assignToChannels(ctx, product_option_group_entity_1.ProductOptionGroup, group.id, [input.channelId])),
...optionsToAssign.map(option => this.channelService.assignToChannels(ctx, product_option_entity_1.ProductOption, option.id, [input.channelId])),
]);
return this.connection
.findByIdsInChannel(ctx, product_option_group_entity_1.ProductOptionGroup, groupsToAssign.map(g => g.id), ctx.channelId, {})
.then(groups => groups.map(group => this.translator.translate(group, ctx, ['options'])));
}
/**
* @description
* Removes ProductOptionGroups from the specified Channel
*/
async removeProductOptionGroupsFromChannel(ctx, input) {
const hasPermission = await this.roleService.userHasAnyPermissionsOnChannel(ctx, input.channelId, [
generated_types_1.Permission.DeleteCatalog,
generated_types_1.Permission.DeleteProduct,
]);
if (!hasPermission) {
throw new common_2.ForbiddenError();
}
const defaultChannel = await this.channelService.getDefaultChannel(ctx);
if ((0, utils_1.idsAreEqual)(input.channelId, defaultChannel.id)) {
throw new common_2.UserInputError('error.items-cannot-be-removed-from-default-channel');
}
const groupsToRemove = await this.connection
.getRepository(ctx, product_option_group_entity_1.ProductOptionGroup)
.find({ where: { id: (0, typeorm_1.In)(input.productOptionGroupIds) }, relations: ['options'] });
const results = [];
for (const group of groupsToRemove) {
// Check if this group is in use by products in the target channel
const productCount = await this.connection
.getRepository(ctx, product_entity_1.Product)
.createQueryBuilder('product')
.innerJoin('product.optionGroups', 'optionGroup', 'optionGroup.id = :groupId', {
groupId: group.id,
})
.innerJoin('product.channels', 'channel', 'channel.id = :channelId', {
channelId: input.channelId,
})
.where('product.deletedAt IS NULL')
.getCount();
const variantCount = await this.connection
.getRepository(ctx, product_variant_entity_1.ProductVariant)
.createQueryBuilder('variant')
.leftJoin('variant.options', 'option')
.innerJoin('variant.channels', 'channel', 'channel.id = :channelId', {
channelId: input.channelId,
})
.where('option.groupId = :groupId', { groupId: group.id })
.andWhere('variant.deletedAt IS NULL')
.getCount();
const isInUse = !!(productCount || variantCount);
if (!isInUse || input.force) {
await this.channelService.removeFromChannels(ctx, product_option_group_entity_1.ProductOptionGroup, group.id, [
input.channelId,
]);
await Promise.all(group.options.map(option => this.channelService.removeFromChannels(ctx, product_option_entity_1.ProductOption, option.id, [
input.channelId,
])));
const result = await this.findOne(ctx, group.id);
if (result) {
results.push(result);
}
}
else {
results.push(new generated_graphql_admin_errors_1.ProductOptionGroupInUseError({
optionGroupCode: group.code,
productCount,
variantCount,
}));
}
}
return results;
}
async isInUseByOtherProducts(ctx, productOptionGroup, targetProductId) {
return this.connection
.getRepository(ctx, product_entity_1.Product)
.createQueryBuilder('product')
.leftJoin('product.optionGroups', 'optionGroup')
.where('product.deletedAt IS NULL')
.andWhere('optionGroup.id = :id', { id: productOptionGroup.id })
.andWhere('product.id != :productId', { productId: targetProductId })
.getCount();
}
async groupOptionsAreInUse(ctx, productOptionGroup) {
return this.connection
.getRepository(ctx, product_variant_entity_1.ProductVariant)
.createQueryBuilder('variant')
.leftJoin('variant.options', 'option')
.where('option.groupId = :groupId', { groupId: productOptionGroup.id })
.getCount();
}
};
exports.ProductOptionGroupService = ProductOptionGroupService;
exports.ProductOptionGroupService = ProductOptionGroupService = __decorate([
(0, common_1.Injectable)(),
(0, instrument_decorator_1.Instrument)(),
__metadata("design:paramtypes", [transactional_connection_1.TransactionalConnection,
translatable_saver_1.TranslatableSaver,
custom_field_relation_service_1.CustomFieldRelationService,
product_option_service_1.ProductOptionService,
event_bus_1.EventBus,
translator_service_1.TranslatorService,
list_query_builder_1.ListQueryBuilder,
channel_service_1.ChannelService,
role_service_1.RoleService])
], ProductOptionGroupService);
//# sourceMappingURL=product-option-group.service.js.map