UNPKG

@vendure/core

Version:

A modern, headless ecommerce framework

454 lines 23.6 kB
"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.ChannelService = void 0; const common_1 = require("@nestjs/common"); const generated_types_1 = require("@vendure/common/lib/generated-types"); const shared_constants_1 = require("@vendure/common/lib/shared-constants"); const unique_1 = require("@vendure/common/lib/unique"); const request_context_1 = require("../../api/common/request-context"); const error_result_1 = require("../../common/error/error-result"); const errors_1 = require("../../common/error/errors"); const generated_graphql_admin_errors_1 = require("../../common/error/generated-graphql-admin-errors"); const instrument_decorator_1 = require("../../common/instrument-decorator"); const self_refreshing_cache_1 = require("../../common/self-refreshing-cache"); const utils_1 = require("../../common/utils"); const config_service_1 = require("../../config/config.service"); const transactional_connection_1 = require("../../connection/transactional-connection"); const channel_entity_1 = require("../../entity/channel/channel.entity"); const order_entity_1 = require("../../entity/order/order.entity"); const product_variant_price_entity_1 = require("../../entity/product-variant/product-variant-price.entity"); const product_variant_entity_1 = require("../../entity/product-variant/product-variant.entity"); const seller_entity_1 = require("../../entity/seller/seller.entity"); const session_entity_1 = require("../../entity/session/session.entity"); const zone_entity_1 = require("../../entity/zone/zone.entity"); const event_bus_1 = require("../../event-bus"); const change_channel_event_1 = require("../../event-bus/events/change-channel-event"); const channel_event_1 = require("../../event-bus/events/channel-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 patch_entity_1 = require("../helpers/utils/patch-entity"); const global_settings_service_1 = require("./global-settings.service"); /** * @description * Contains methods relating to {@link Channel} entities. * * @docsCategory services */ let ChannelService = class ChannelService { constructor(connection, configService, globalSettingsService, customFieldRelationService, eventBus, listQueryBuilder) { this.connection = connection; this.configService = configService; this.globalSettingsService = globalSettingsService; this.customFieldRelationService = customFieldRelationService; this.eventBus = eventBus; this.listQueryBuilder = listQueryBuilder; } /** * When the app is bootstrapped, ensure a default Channel exists and populate the * channel lookup array. * * @internal */ async initChannels() { await this.ensureDefaultChannelExists(); await this.ensureCacheExists(); } /** * Creates a channels cache, that can be used to reduce number of channel queries to database * * @internal */ async createCache() { return (0, self_refreshing_cache_1.createSelfRefreshingCache)({ name: 'ChannelService.allChannels', ttl: this.configService.entityOptions.channelCacheTtl, refresh: { fn: async (ctx) => { const result = await this.listQueryBuilder .build(channel_entity_1.Channel, {}, { ctx, relations: ['defaultShippingZone', 'defaultTaxZone'], ignoreQueryLimits: true, }) .getManyAndCount() .then(([items, totalItems]) => ({ items, totalItems, })); return result.items; }, defaultArgs: [request_context_1.RequestContext.empty()], }, }); } /** * @description * Assigns a ChannelAware entity to the default Channel as well as any channel * specified in the RequestContext. This method will not save the entity to the database, but * assigns the `channels` property of the entity. */ async assignToCurrentChannel(entity, ctx) { const defaultChannel = await this.getDefaultChannel(ctx); const channelIds = (0, unique_1.unique)([ctx.channelId, defaultChannel.id]); entity.channels = channelIds.map(id => ({ id })); await this.eventBus.publish(new change_channel_event_1.ChangeChannelEvent(ctx, entity, [ctx.channelId], 'assigned')); return entity; } /** * This method is used to bypass a bug with Typeorm when working with ManyToMany relationships. * For some reason, a regular query does not return all the channels that an entity has. * This is a most optimized way to get all the channels that an entity has. * * @param ctx - The RequestContext object. * @param entityType - The type of the entity. * @param entityId - The ID of the entity. * @returns A promise that resolves to an array of objects, each containing a channel ID. * @private */ async getAssignedEntityChannels(ctx, entityType, entityId) { var _a, _b, _c; const repository = this.connection.getRepository(ctx, entityType); const metadata = repository.metadata; const channelsRelation = metadata.findRelationWithPropertyPath('channels'); if (!channelsRelation) { throw new errors_1.InternalServerError(`Could not find the channels relation for entity ${metadata.name}`); } const junctionTableName = (_a = channelsRelation.junctionEntityMetadata) === null || _a === void 0 ? void 0 : _a.tableName; const junctionColumnName = (_b = channelsRelation.junctionEntityMetadata) === null || _b === void 0 ? void 0 : _b.columns[0].databaseName; const inverseJunctionColumnName = (_c = channelsRelation.junctionEntityMetadata) === null || _c === void 0 ? void 0 : _c.inverseColumns[0].databaseName; if (!junctionTableName || !junctionColumnName || !inverseJunctionColumnName) { throw new errors_1.InternalServerError(`Could not find necessary join table information for the channels relation of entity ${metadata.name}`); } return await this.connection .getRepository(ctx, entityType) .manager.createQueryBuilder() .select(`channel.${inverseJunctionColumnName}`, 'channelId') .from(junctionTableName, 'channel') .where(`channel.${junctionColumnName} = :entityId`, { entityId }) .execute(); } /** * @description * Assigns the entity to the given Channels and saves all changes to the database. */ async assignToChannels(ctx, entityType, entityId, channelIds) { const relations = []; // This is a work-around for https://github.com/vendurehq/vendure/issues/1391 // A better API would be to allow the consumer of this method to supply an entity instance // so that this join could be done prior to invoking this method. // TODO: overload the assignToChannels method to allow it to take an entity instance if (entityType === order_entity_1.Order) { relations.push('lines', 'shippingLines', 'surcharges'); } const entity = await this.connection.getEntityOrThrow(ctx, entityType, entityId, { loadEagerRelations: false, relationLoadStrategy: 'query', where: { id: entityId, }, relations, }); const assignedChannels = await this.getAssignedEntityChannels(ctx, entityType, entityId); const newChannelIds = channelIds.filter(id => !assignedChannels.some(ec => (0, utils_1.idsAreEqual)(ec.channelId, id))); if (!newChannelIds.length) { return entity; } await this.connection .getRepository(ctx, entityType) .createQueryBuilder() .relation('channels') .of(entity.id) .add(newChannelIds); await this.eventBus.publish(new change_channel_event_1.ChangeChannelEvent(ctx, entity, newChannelIds, 'assigned', entityType)); return entity; } /** * @description * Removes the entity from the given Channels and saves. */ async removeFromChannels(ctx, entityType, entityId, channelIds) { const entity = await this.connection.getRepository(ctx, entityType).findOne({ loadEagerRelations: false, relationLoadStrategy: 'query', where: { id: entityId, }, }); if (!entity) { return; } const assignedChannels = await this.getAssignedEntityChannels(ctx, entityType, entityId); const existingChannelIds = channelIds.filter(id => assignedChannels.some(ec => (0, utils_1.idsAreEqual)(ec.channelId, id))); if (!existingChannelIds.length) { return; } await this.connection .getRepository(ctx, entityType) .createQueryBuilder() .relation('channels') .of(entity.id) .remove(existingChannelIds); await this.eventBus.publish(new change_channel_event_1.ChangeChannelEvent(ctx, entity, existingChannelIds, 'removed', entityType)); return entity; } async getChannelFromToken(ctxOrToken, token) { const [ctx, channelToken] = // eslint-disable-next-line @typescript-eslint/no-non-null-assertion ctxOrToken instanceof request_context_1.RequestContext ? [ctxOrToken, token] : [undefined, ctxOrToken]; const allChannels = await this.allChannels.value(ctx); if (allChannels.length === 1 || channelToken === '') { // there is only the default channel, so return it return this.getDefaultChannel(ctx); } const channel = allChannels.find(c => c.token === channelToken); if (!channel) { throw new errors_1.ChannelNotFoundError(channelToken); } return channel; } /** * @description * Returns the default Channel. */ async getDefaultChannel(ctx) { const allChannels = await this.allChannels.value(ctx); const defaultChannel = allChannels.find(channel => channel.code === shared_constants_1.DEFAULT_CHANNEL_CODE); if (!defaultChannel) { throw new errors_1.InternalServerError('error.default-channel-not-found'); } return defaultChannel; } findAll(ctx, options, relations) { return this.listQueryBuilder .build(channel_entity_1.Channel, options, { relations: relations !== null && relations !== void 0 ? relations : ['defaultShippingZone', 'defaultTaxZone'], ctx, }) .getManyAndCount() .then(([items, totalItems]) => ({ items, totalItems, })); } findOne(ctx, id) { return this.connection .getRepository(ctx, channel_entity_1.Channel) .findOne({ where: { id }, relations: ['defaultShippingZone', 'defaultTaxZone'] }) .then(result => result !== null && result !== void 0 ? result : undefined); } async create(ctx, input) { var _a, _b; const defaultCurrencyCode = input.defaultCurrencyCode || input.currencyCode; if (!defaultCurrencyCode) { throw new errors_1.UserInputError('Either a defaultCurrencyCode or currencyCode must be provided'); } const channel = new channel_entity_1.Channel(Object.assign(Object.assign({}, input), { defaultCurrencyCode, availableCurrencyCodes: (_a = input.availableCurrencyCodes) !== null && _a !== void 0 ? _a : (defaultCurrencyCode ? [defaultCurrencyCode] : []), availableLanguageCodes: (_b = input.availableLanguageCodes) !== null && _b !== void 0 ? _b : [input.defaultLanguageCode] })); const defaultLanguageValidationResult = await this.validateDefaultLanguageCode(ctx, input); if ((0, error_result_1.isGraphQlErrorResult)(defaultLanguageValidationResult)) { return defaultLanguageValidationResult; } if (input.defaultTaxZoneId) { channel.defaultTaxZone = await this.connection.getEntityOrThrow(ctx, zone_entity_1.Zone, input.defaultTaxZoneId); } if (input.defaultShippingZoneId) { channel.defaultShippingZone = await this.connection.getEntityOrThrow(ctx, zone_entity_1.Zone, input.defaultShippingZoneId); } const newChannel = await this.connection.getRepository(ctx, channel_entity_1.Channel).save(channel); if (input.sellerId) { const seller = await this.connection.getEntityOrThrow(ctx, seller_entity_1.Seller, input.sellerId); newChannel.seller = seller; await this.connection.getRepository(ctx, channel_entity_1.Channel).save(newChannel); } await this.customFieldRelationService.updateRelations(ctx, channel_entity_1.Channel, input, newChannel); await this.allChannels.refresh(ctx); await this.eventBus.publish(new channel_event_1.ChannelEvent(ctx, newChannel, 'created', input)); return newChannel; } async update(ctx, input) { const channel = await this.findOne(ctx, input.id); if (!channel) { throw new errors_1.EntityNotFoundError('Channel', input.id); } const originalDefaultCurrencyCode = channel.defaultCurrencyCode; const defaultLanguageValidationResult = await this.validateDefaultLanguageCode(ctx, input); if ((0, error_result_1.isGraphQlErrorResult)(defaultLanguageValidationResult)) { return defaultLanguageValidationResult; } const updatedChannel = (0, patch_entity_1.patchEntity)(channel, input); if (input.defaultTaxZoneId) { updatedChannel.defaultTaxZone = await this.connection.getEntityOrThrow(ctx, zone_entity_1.Zone, input.defaultTaxZoneId); } if (input.defaultShippingZoneId) { updatedChannel.defaultShippingZone = await this.connection.getEntityOrThrow(ctx, zone_entity_1.Zone, input.defaultShippingZoneId); } if (input.sellerId) { const seller = await this.connection.getEntityOrThrow(ctx, seller_entity_1.Seller, input.sellerId); updatedChannel.seller = seller; } if (input.currencyCode) { updatedChannel.defaultCurrencyCode = input.currencyCode; } if (input.currencyCode || input.defaultCurrencyCode) { const newCurrencyCode = input.defaultCurrencyCode || input.currencyCode; updatedChannel.availableCurrencyCodes = (0, unique_1.unique)([ ...updatedChannel.availableCurrencyCodes, updatedChannel.defaultCurrencyCode, ]); if (originalDefaultCurrencyCode !== newCurrencyCode) { // When updating the default currency code for a Channel, we also need to update // and ProductVariantPrices in that channel which use the old currency code. const [selectQbQuery, selectQbParams] = this.connection .getRepository(ctx, product_variant_entity_1.ProductVariant) .createQueryBuilder('variant') .select('variant.id', 'id') .innerJoin(product_variant_price_entity_1.ProductVariantPrice, 'pvp', 'pvp.variantId = variant.id') .andWhere('pvp.channelId = :channelId') .andWhere('pvp.currencyCode = :newCurrencyCode') .groupBy('variant.id') .getQueryAndParameters(); const qb = this.connection .getRepository(ctx, product_variant_price_entity_1.ProductVariantPrice) .createQueryBuilder('pvp') .update() .where('channelId = :channelId') .andWhere('currencyCode = :oldCurrencyCode') .set({ currencyCode: newCurrencyCode }) .setParameters({ channelId: channel.id, oldCurrencyCode: originalDefaultCurrencyCode, newCurrencyCode, }); if (this.connection.rawConnection.options.type === 'mysql') { // MySQL does not support sub-queries joining the table that is being updated, // it will cause a "You can't specify target table 'product_variant_price' for update in FROM clause" error. // This is a work-around from https://stackoverflow.com/a/9843719/772859 qb.andWhere(`variantId NOT IN (SELECT id FROM (${selectQbQuery}) as temp)`, selectQbParams); } else { qb.andWhere(`variantId NOT IN (${selectQbQuery})`, selectQbParams); } await qb.execute(); } } if (input.availableCurrencyCodes && !updatedChannel.availableCurrencyCodes.includes(updatedChannel.defaultCurrencyCode)) { throw new errors_1.UserInputError(`error.available-currency-codes-must-include-default`, { defaultCurrencyCode: updatedChannel.defaultCurrencyCode, }); } await this.connection.getRepository(ctx, channel_entity_1.Channel).save(updatedChannel, { reload: false }); await this.customFieldRelationService.updateRelations(ctx, channel_entity_1.Channel, input, updatedChannel); await this.allChannels.refresh(ctx); await this.eventBus.publish(new channel_event_1.ChannelEvent(ctx, channel, 'updated', input)); return (0, utils_1.assertFound)(this.findOne(ctx, channel.id)); } async delete(ctx, id) { const channel = await this.connection.getEntityOrThrow(ctx, channel_entity_1.Channel, id); if (channel.code === shared_constants_1.DEFAULT_CHANNEL_CODE) return { result: generated_types_1.DeletionResult.NOT_DELETED, message: ctx.translate('error.cannot-delete-default-channel'), }; const deletedChannel = new channel_entity_1.Channel(channel); await this.connection.getRepository(ctx, session_entity_1.Session).delete({ activeChannelId: id }); await this.connection.getRepository(ctx, channel_entity_1.Channel).delete(id); await this.connection.getRepository(ctx, product_variant_price_entity_1.ProductVariantPrice).delete({ channelId: id, }); await this.eventBus.publish(new channel_event_1.ChannelEvent(ctx, deletedChannel, 'deleted', id)); return { result: generated_types_1.DeletionResult.DELETED, }; } /** * @description * Type guard method which returns true if the given entity is an * instance of a class which implements the {@link ChannelAware} interface. */ isChannelAware(entity) { const entityType = Object.getPrototypeOf(entity).constructor; return !!this.connection.rawConnection .getMetadata(entityType) .relations.find(r => r.type === channel_entity_1.Channel && r.propertyName === 'channels'); } /** * Ensures channel cache exists. If not, this method creates one. */ async ensureCacheExists() { if (this.allChannels) { return; } this.allChannels = await this.createCache(); } /** * There must always be a default Channel. If none yet exists, this method creates one. * Also ensures the default Channel token matches the defaultChannelToken config setting. */ async ensureDefaultChannelExists() { const { defaultChannelToken } = this.configService; let defaultChannel = await this.connection.rawConnection.getRepository(channel_entity_1.Channel).findOne({ where: { code: shared_constants_1.DEFAULT_CHANNEL_CODE, }, relations: ['seller'], }); if (!defaultChannel) { defaultChannel = new channel_entity_1.Channel({ code: shared_constants_1.DEFAULT_CHANNEL_CODE, defaultLanguageCode: this.configService.defaultLanguageCode, availableLanguageCodes: [this.configService.defaultLanguageCode], pricesIncludeTax: false, defaultCurrencyCode: generated_types_1.CurrencyCode.USD, availableCurrencyCodes: [generated_types_1.CurrencyCode.USD], token: defaultChannelToken, }); } else if (defaultChannelToken && defaultChannel.token !== defaultChannelToken) { defaultChannel.token = defaultChannelToken; await this.connection.rawConnection .getRepository(channel_entity_1.Channel) .save(defaultChannel, { reload: false }); } if (!defaultChannel.seller) { const seller = await this.connection.rawConnection.getRepository(seller_entity_1.Seller).find(); if (seller.length === 0) { throw new errors_1.InternalServerError('No Sellers were found. Could not initialize default Channel.'); } defaultChannel.seller = seller[0]; await this.connection.rawConnection .getRepository(channel_entity_1.Channel) .save(defaultChannel, { reload: false }); } } async validateDefaultLanguageCode(ctx, input) { if (input.defaultLanguageCode) { const availableLanguageCodes = await this.globalSettingsService .getSettings(ctx) .then(s => s.availableLanguages); if (!availableLanguageCodes.includes(input.defaultLanguageCode)) { return new generated_graphql_admin_errors_1.LanguageNotAvailableError({ languageCode: input.defaultLanguageCode }); } } } }; exports.ChannelService = ChannelService; exports.ChannelService = ChannelService = __decorate([ (0, common_1.Injectable)(), (0, instrument_decorator_1.Instrument)(), __metadata("design:paramtypes", [transactional_connection_1.TransactionalConnection, config_service_1.ConfigService, global_settings_service_1.GlobalSettingsService, custom_field_relation_service_1.CustomFieldRelationService, event_bus_1.EventBus, list_query_builder_1.ListQueryBuilder]) ], ChannelService); //# sourceMappingURL=channel.service.js.map