| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369 |
- import { Injectable } from '@nestjs/common';
- import {
- AssignFacetsToChannelInput,
- CreateFacetInput,
- DeletionResponse,
- DeletionResult,
- LanguageCode,
- Permission,
- RemoveFacetFromChannelResult,
- RemoveFacetsFromChannelInput,
- UpdateFacetInput,
- } from '@vendure/common/lib/generated-types';
- import { ID, PaginatedList } from '@vendure/common/lib/shared-types';
- import { In } from 'typeorm';
- import { RequestContext } from '../../api/common/request-context';
- import { RelationPaths } from '../../api/decorators/relations.decorator';
- import { ErrorResultUnion, FacetInUseError, ForbiddenError, UserInputError } from '../../common';
- import { ListQueryOptions } from '../../common/types/common-types';
- import { Translated } from '../../common/types/locale-types';
- import { assertFound, idsAreEqual } from '../../common/utils';
- import { ConfigService } from '../../config/config.service';
- import { TransactionalConnection } from '../../connection/transactional-connection';
- import { FacetTranslation } from '../../entity/facet/facet-translation.entity';
- import { Facet } from '../../entity/facet/facet.entity';
- import { FacetValue } from '../../entity/facet-value/facet-value.entity';
- import { EventBus } from '../../event-bus';
- import { FacetEvent } from '../../event-bus/events/facet-event';
- import { CustomFieldRelationService } from '../helpers/custom-field-relation/custom-field-relation.service';
- import { ListQueryBuilder } from '../helpers/list-query-builder/list-query-builder';
- import { TranslatableSaver } from '../helpers/translatable-saver/translatable-saver';
- import { TranslatorService } from '../helpers/translator/translator.service';
- import { translateDeep } from '../helpers/utils/translate-entity';
- import { ChannelService } from './channel.service';
- import { FacetValueService } from './facet-value.service';
- import { RoleService } from './role.service';
- /**
- * @description
- * Contains methods relating to {@link Facet} entities.
- *
- * @docsCategory services
- */
- @Injectable()
- export class FacetService {
- constructor(
- private connection: TransactionalConnection,
- private facetValueService: FacetValueService,
- private translatableSaver: TranslatableSaver,
- private listQueryBuilder: ListQueryBuilder,
- private configService: ConfigService,
- private channelService: ChannelService,
- private customFieldRelationService: CustomFieldRelationService,
- private eventBus: EventBus,
- private translator: TranslatorService,
- private roleService: RoleService,
- ) {}
- findAll(
- ctx: RequestContext,
- options?: ListQueryOptions<Facet>,
- relations?: RelationPaths<Facet>,
- ): Promise<PaginatedList<Translated<Facet>>> {
- return this.listQueryBuilder
- .build(Facet, options, {
- relations: relations ?? ['values', 'values.facet', 'channels'],
- ctx,
- channelId: ctx.channelId,
- })
- .getManyAndCount()
- .then(([facets, totalItems]) => {
- const items = facets.map(facet =>
- this.translator.translate(facet, ctx, ['values', ['values', 'facet']]),
- );
- return {
- items,
- totalItems,
- };
- });
- }
- findOne(
- ctx: RequestContext,
- facetId: ID,
- relations?: RelationPaths<Facet>,
- ): Promise<Translated<Facet> | undefined> {
- return this.connection
- .findOneInChannel(ctx, Facet, facetId, ctx.channelId, {
- relations: relations ?? ['values', 'values.facet', 'channels'],
- })
- .then(
- facet =>
- (facet && this.translator.translate(facet, ctx, ['values', ['values', 'facet']])) ??
- undefined,
- );
- }
- /**
- * @deprecated Use {@link FacetService.findByCode findByCode(ctx, facetCode, lang)} instead
- */
- findByCode(facetCode: string, lang: LanguageCode): Promise<Translated<Facet> | undefined>;
- findByCode(
- ctx: RequestContext,
- facetCode: string,
- lang: LanguageCode,
- ): Promise<Translated<Facet> | undefined>;
- findByCode(
- ctxOrFacetCode: RequestContext | string,
- facetCodeOrLang: string | LanguageCode,
- lang?: LanguageCode,
- ): Promise<Translated<Facet> | undefined> {
- const relations = ['values', 'values.facet'];
- const [repository, facetCode, languageCode] =
- ctxOrFacetCode instanceof RequestContext
- ? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- [this.connection.getRepository(ctxOrFacetCode, Facet), facetCodeOrLang, lang!]
- : [
- this.connection.rawConnection.getRepository(Facet),
- ctxOrFacetCode,
- facetCodeOrLang as LanguageCode,
- ];
- // TODO: Implement usage of channelLanguageCode
- return repository
- .findOne({
- where: {
- code: facetCode,
- },
- relations,
- })
- .then(
- facet =>
- (facet && translateDeep(facet, languageCode, ['values', ['values', 'facet']])) ??
- undefined,
- );
- }
- /**
- * @description
- * Returns the Facet which contains the given FacetValue id.
- */
- async findByFacetValueId(ctx: RequestContext, id: ID): Promise<Translated<Facet> | undefined> {
- const facet = await this.connection
- .getRepository(ctx, Facet)
- .createQueryBuilder('facet')
- .leftJoinAndSelect('facet.translations', 'translations')
- .leftJoin('facet.values', 'facetValue')
- .where('facetValue.id = :id', { id })
- .getOne();
- if (facet) {
- return this.translator.translate(facet, ctx);
- }
- }
- async create(ctx: RequestContext, input: CreateFacetInput): Promise<Translated<Facet>> {
- const facet = await this.translatableSaver.create({
- ctx,
- input,
- entityType: Facet,
- translationType: FacetTranslation,
- beforeSave: async f => {
- f.code = await this.ensureUniqueCode(ctx, f.code);
- await this.channelService.assignToCurrentChannel(f, ctx);
- },
- });
- const facetWithRelations = await this.customFieldRelationService.updateRelations(
- ctx,
- Facet,
- input,
- facet,
- );
- await this.eventBus.publish(new FacetEvent(ctx, facetWithRelations, 'created', input));
- return assertFound(this.findOne(ctx, facet.id));
- }
- async update(ctx: RequestContext, input: UpdateFacetInput): Promise<Translated<Facet>> {
- const facet = await this.translatableSaver.update({
- ctx,
- input,
- entityType: Facet,
- translationType: FacetTranslation,
- beforeSave: async f => {
- if (f.code) {
- f.code = await this.ensureUniqueCode(ctx, f.code, f.id);
- }
- },
- });
- await this.customFieldRelationService.updateRelations(ctx, Facet, input, facet);
- await this.eventBus.publish(new FacetEvent(ctx, facet, 'updated', input));
- return assertFound(this.findOne(ctx, facet.id));
- }
- async delete(ctx: RequestContext, id: ID, force: boolean = false): Promise<DeletionResponse> {
- const facet = await this.connection.getEntityOrThrow(ctx, Facet, id, {
- relations: ['values'],
- channelId: ctx.channelId,
- });
- let productCount = 0;
- let variantCount = 0;
- if (facet.values.length) {
- const counts = await this.facetValueService.checkFacetValueUsage(
- ctx,
- facet.values.map(fv => fv.id),
- );
- productCount = counts.productCount;
- variantCount = counts.variantCount;
- }
- const isInUse = !!(productCount || variantCount);
- const both = !!(productCount && variantCount) ? 'both' : 'single';
- const i18nVars = { products: productCount, variants: variantCount, both, facetCode: facet.code };
- let message = '';
- let result: DeletionResult;
- const deletedFacet = new Facet(facet);
- if (!isInUse) {
- await this.connection.getRepository(ctx, Facet).remove(facet);
- await this.eventBus.publish(new FacetEvent(ctx, deletedFacet, 'deleted', id));
- result = DeletionResult.DELETED;
- } else if (force) {
- await this.connection.getRepository(ctx, Facet).remove(facet);
- await this.eventBus.publish(new FacetEvent(ctx, deletedFacet, 'deleted', id));
- message = ctx.translate('message.facet-force-deleted', i18nVars);
- result = DeletionResult.DELETED;
- } else {
- message = ctx.translate('message.facet-used', i18nVars);
- result = DeletionResult.NOT_DELETED;
- }
- return {
- result,
- message,
- };
- }
- /**
- * Checks to ensure the Facet code is unique. If there is a conflict, then the code is suffixed
- * with an incrementing integer.
- */
- private async ensureUniqueCode(ctx: RequestContext, code: string, id?: ID) {
- let candidate = code;
- let suffix = 1;
- let conflict = false;
- const alreadySuffixed = /-\d+$/;
- do {
- const match = await this.connection
- .getRepository(ctx, Facet)
- .findOne({ where: { code: candidate } });
- conflict = !!match && ((id != null && !idsAreEqual(match.id, id)) || id == null);
- if (conflict) {
- suffix++;
- if (alreadySuffixed.test(candidate)) {
- candidate = candidate.replace(alreadySuffixed, `-${suffix}`);
- } else {
- candidate = `${candidate}-${suffix}`;
- }
- }
- } while (conflict);
- return candidate;
- }
- /**
- * @description
- * Assigns Facets to the specified Channel
- */
- async assignFacetsToChannel(
- ctx: RequestContext,
- input: AssignFacetsToChannelInput,
- ): Promise<Array<Translated<Facet>>> {
- const hasPermission = await this.roleService.userHasAnyPermissionsOnChannel(ctx, input.channelId, [
- Permission.UpdateFacet,
- Permission.UpdateCatalog,
- ]);
- if (!hasPermission) {
- throw new ForbiddenError();
- }
- const facetsToAssign = await this.connection
- .getRepository(ctx, Facet)
- .find({ where: { id: In(input.facetIds) }, relations: ['values'] });
- const valuesToAssign = facetsToAssign.reduce(
- (values, facet) => [...values, ...facet.values],
- [] as FacetValue[],
- );
- await Promise.all<any>([
- ...facetsToAssign.map(async facet => {
- return this.channelService.assignToChannels(ctx, Facet, facet.id, [input.channelId]);
- }),
- ...valuesToAssign.map(async value =>
- this.channelService.assignToChannels(ctx, FacetValue, value.id, [input.channelId]),
- ),
- ]);
- return this.connection
- .findByIdsInChannel(
- ctx,
- Facet,
- facetsToAssign.map(f => f.id),
- ctx.channelId,
- {},
- )
- .then(facets => facets.map(facet => translateDeep(facet, ctx.languageCode)));
- }
- /**
- * @description
- * Remove Facets from the specified Channel
- */
- async removeFacetsFromChannel(
- ctx: RequestContext,
- input: RemoveFacetsFromChannelInput,
- ): Promise<Array<ErrorResultUnion<RemoveFacetFromChannelResult, Facet>>> {
- const hasPermission = await this.roleService.userHasAnyPermissionsOnChannel(ctx, input.channelId, [
- Permission.DeleteFacet,
- Permission.DeleteCatalog,
- ]);
- if (!hasPermission) {
- throw new ForbiddenError();
- }
- const defaultChannel = await this.channelService.getDefaultChannel(ctx);
- if (idsAreEqual(input.channelId, defaultChannel.id)) {
- throw new UserInputError('error.items-cannot-be-removed-from-default-channel');
- }
- const facetsToRemove = await this.connection
- .getRepository(ctx, Facet)
- .find({ where: { id: In(input.facetIds) }, relations: ['values'] });
- const results: Array<ErrorResultUnion<RemoveFacetFromChannelResult, Facet>> = [];
- for (const facet of facetsToRemove) {
- let productCount = 0;
- let variantCount = 0;
- if (facet.values.length) {
- const counts = await this.facetValueService.checkFacetValueUsage(
- ctx,
- facet.values.map(fv => fv.id),
- input.channelId,
- );
- productCount = counts.productCount;
- variantCount = counts.variantCount;
- const isInUse = !!(productCount || variantCount);
- const both = !!(productCount && variantCount) ? 'both' : 'single';
- const i18nVars = { products: productCount, variants: variantCount, both };
- let result: Translated<Facet> | undefined;
- if (!isInUse || input.force) {
- await this.channelService.removeFromChannels(ctx, Facet, facet.id, [input.channelId]);
- await Promise.all(
- facet.values.map(fv =>
- this.channelService.removeFromChannels(ctx, FacetValue, fv.id, [input.channelId]),
- ),
- );
- result = await this.findOne(ctx, facet.id);
- if (result) {
- results.push(result);
- }
- } else {
- results.push(new FacetInUseError({ facetCode: facet.code, productCount, variantCount }));
- }
- }
- }
- return results;
- }
- }
|