@vendure/core
Version:
A modern, headless ecommerce framework
451 lines • 23.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.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/vendure-ecommerce/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');
}
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)));
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, channelIds, '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, channelIds, '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