vendure-import-products-plugin
Version:
- This plugin imports the products from the specified url every specified day.
588 lines (587 loc) • 29.3 kB
JavaScript
;
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);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProductImportService = exports.NEW_VARIANT_STOCK_VALUE = void 0;
require("./types");
const axios_1 = require("@nestjs/axios");
const common_1 = require("@nestjs/common");
const core_1 = require("@vendure/core");
const get_superadmin_context_1 = require("./get-superadmin-context");
const typeorm_1 = require("typeorm");
const normalize_string_1 = require("@vendure/common/lib/normalize-string");
const schedule_1 = require("@nestjs/schedule");
const cron_1 = require("cron");
const constants_1 = require("./constants");
const rxjs_1 = require("rxjs");
const vendure_brands_plugin_1 = require("vendure-brands-plugin");
const generated_1 = require("vendure-brands-plugin/dist/gql/generated");
exports.NEW_VARIANT_STOCK_VALUE = 9999;
let ProductImportService = class ProductImportService {
constructor(hydrator, httpService, channelService, productVariantService, configArgService, facetService, stockLocationService, facetValueService, translatableSaver, searchService, productService, taxCategoryService, connection, eventBus, processContext, collectionService, schedulerRegistry, jobQueueService, stockMovementService, brandService, options, configService) {
this.hydrator = hydrator;
this.httpService = httpService;
this.channelService = channelService;
this.productVariantService = productVariantService;
this.configArgService = configArgService;
this.facetService = facetService;
this.stockLocationService = stockLocationService;
this.facetValueService = facetValueService;
this.translatableSaver = translatableSaver;
this.searchService = searchService;
this.productService = productService;
this.taxCategoryService = taxCategoryService;
this.connection = connection;
this.eventBus = eventBus;
this.processContext = processContext;
this.collectionService = collectionService;
this.schedulerRegistry = schedulerRegistry;
this.jobQueueService = jobQueueService;
this.stockMovementService = stockMovementService;
this.brandService = brandService;
this.options = options;
this.configService = configService;
}
async onApplicationBootstrap() {
this.jobQueue = await this.jobQueueService.createQueue({
name: 'import-products',
process: async (job) => {
try {
await this.importProducts();
}
catch (e) {
core_1.Logger.error(`Error importing products: ${e}`, constants_1.loggerCtx);
}
},
});
if (this.options?.everyThisDay === undefined) {
throw new core_1.UserInputError(`You need to configure ProductImportPluginOptions.everyThisOtherDay`);
}
// CronExpression.EVERY_5_MINUTES;
// const job = new CronJob("0 */2 * * * *", async () => {
// this.jobQueue.add({},{retries: 2})
// });
const job = new cron_1.CronJob(`0 0 */${this.options.everyThisDay} * *`, async () => {
this.jobQueue.add({}, { retries: 2 });
});
this.schedulerRegistry.addCronJob('name', job);
job.start();
if (this.processContext.isWorker) {
setTimeout(async () => await this.importProducts(), 120 * 1000);
}
// this.importProducts();
}
async importProducts() {
if (this.options?.url === undefined) {
throw new core_1.UserInputError(`You need to configure "ProductImportPluginOptions.url"`);
}
const { data } = await (0, rxjs_1.firstValueFrom)(this.httpService.get(this.options.url).pipe((0, rxjs_1.catchError)((error) => {
core_1.Logger.error(error.message);
return (0, rxjs_1.of)({ data: { Items: [] } });
})));
const products = data?.Items;
if (!products || products.length === 0) {
core_1.Logger.info(`No products to import`);
return;
}
core_1.Logger.info(`Started importing ${products.length} Products`);
const webHookIds = products.map((p) => p.id);
const defaultChannel = await this.channelService.getDefaultChannel();
const ctx = await (0, get_superadmin_context_1.getSuperadminContext)(defaultChannel, this.connection, this.configService);
//we first get the products
const vendureProducts = await this.getExistingRemoteProducts(ctx, webHookIds);
//for those which exist we go thru each and update each
const productRepo = this.connection.getRepository(ctx, core_1.Product);
const newRemoteProducts = [];
for (let remoteProduct of products) {
const vendureProduct = vendureProducts.find((vP) => vP.customFields.webhookId === remoteProduct.id);
if (vendureProduct) {
//we first update the product name
await this.updateProduct(ctx, vendureProduct, defaultChannel.defaultLanguageCode, remoteProduct);
//then the default variant's name and price
const defaultVariant = await this.updateDefaultVariant(ctx, vendureProduct, defaultChannel, remoteProduct);
await this.updateProductCollection(ctx, defaultVariant, remoteProduct.Collection, defaultChannel.defaultLanguageCode);
await this.updateProductFacet(ctx, vendureProduct, remoteProduct, defaultChannel.defaultLanguageCode);
}
else {
newRemoteProducts.push(remoteProduct);
}
}
await productRepo.save(vendureProducts);
//for those which dont we create a new Product
for (let newProduct of newRemoteProducts) {
const product = await this.createProduct(ctx, newProduct, defaultChannel.defaultLanguageCode);
const defaultVariant = product?.variants[0];
if (!product || !defaultVariant) {
throw new core_1.UserInputError("Couldn't create a Product");
}
await this.updateProductCollection(ctx, defaultVariant, newProduct.Collection, defaultChannel.defaultLanguageCode);
await this.updateProductFacet(ctx, product, newProduct, defaultChannel.defaultLanguageCode);
}
await this.searchService.reindex(ctx);
this.eventBus.publish(new core_1.ProductVariantEvent(ctx, [], 'created'));
core_1.Logger.info(`Done importing ${products.length} Products`);
}
async getExistingRemoteProducts(ctx, webHookIds) {
const productsRepo = this.connection.getRepository(ctx, core_1.Product);
return await productsRepo
.createQueryBuilder('product')
.select('productTranslations.id')
.addSelect('productTranslations.name')
.addSelect('productTranslations.languageCode')
.addSelect('variantTranslations.id')
.addSelect('variantTranslations.name')
.addSelect('variantTranslations.languageCode')
.addSelect('product.id')
.addSelect('product.customFields.webhookId')
.addSelect('product.customFields.unit')
.addSelect('product.customFields.Measurement')
.addSelect('product.customFields.brand')
.addSelect('productVariantPrices.id')
.addSelect('productVariantPrices.price')
.addSelect('productVariantPrices.channelId')
.addSelect('variant.id')
.addSelect('variant.sku')
.addSelect('collection.id')
.addSelect('collection.filters')
.addSelect('facetValueTranslations.id')
.addSelect('facetValueTranslations.name')
.addSelect('facetValueTranslations.languageCode')
.addSelect('facetTranslations.id')
.addSelect('facetTranslations.name')
.addSelect('facetTranslations.languageCode')
.addSelect('collectionTranslation.id')
.addSelect('collectionTranslation.name')
.addSelect('collectionTranslation.languageCode')
.leftJoin('product.variants', 'variant')
.leftJoin('product.translations', 'productTranslations')
.leftJoin('product.facetValues', 'facetValue')
.leftJoin('facetValue.translations', 'facetValueTranslations')
.leftJoin('facetValue.facet', 'facet')
.leftJoin('facet.translations', 'facetTranslations')
.leftJoin('variant.productVariantPrices', 'productVariantPrices')
.leftJoin('variant.collections', 'collection')
.leftJoin('variant.translations', 'variantTranslations')
.leftJoin('collection.translations', 'collectionTranslation')
.setFindOptions({
where: {
customFields: {
webhookId: (0, typeorm_1.In)(webHookIds),
},
deletedAt: (0, typeorm_1.IsNull)(),
variants: {
deletedAt: (0, typeorm_1.IsNull)(),
},
},
})
.getMany();
}
async getOrCreateBrand(ctx, brandName) {
let brand;
brand = await this.brandService.findOneByName(ctx, brandName);
if (!brand) {
brand = await this.brandService.create(ctx, {
name: brandName,
priority: generated_1.Priority.LOW,
});
}
return brand;
}
async updateProduct(ctx, vendureProduct, languageCode, remoteProduct) {
await this.hydrator.hydrate(ctx, vendureProduct, {
relations: ['customFields.brand'],
});
let translation = vendureProduct.translations.find((tr) => languageCode === tr.languageCode);
const productTranslationRepo = this.connection.getRepository(ctx, core_1.ProductTranslation);
vendureProduct.customFields.unit = remoteProduct.unit;
vendureProduct.customFields.Measurement = remoteProduct.measurement;
if (translation) {
const randomString = Math.random().toString(36).substring(2, 6);
translation.name = remoteProduct.name;
translation.slug = `${(0, normalize_string_1.normalizeString)(`${remoteProduct.name}`, '-')}-${randomString}`;
}
else {
const randomString = Math.random().toString(36).substring(2, 6);
translation = new core_1.ProductTranslation();
translation.languageCode = languageCode;
translation.base = vendureProduct;
translation.name = remoteProduct.name;
translation.slug = `${(0, normalize_string_1.normalizeString)(`${remoteProduct.name}`, '-')}-${randomString}`;
vendureProduct.translations.push(translation);
}
await productTranslationRepo.save(translation);
if (remoteProduct.brand) {
const brand = await this.getOrCreateBrand(ctx, remoteProduct.brand);
await this.connection.getRepository(ctx, core_1.Product).save((0, core_1.patchEntity)(vendureProduct, {
customFields: {
...vendureProduct.customFields,
brandName: brand.name,
brand: brand,
},
}));
}
else {
await this.connection.getRepository(ctx, core_1.Product).save((0, core_1.patchEntity)(vendureProduct, {
customFields: {
...vendureProduct.customFields,
brandName: null,
brand: null,
},
}));
}
}
async updateProductVariantName(ctx, defaultVariant, languageCode, remoteProductName) {
let translation = defaultVariant.translations.find((tr) => languageCode === tr.languageCode);
const productTranslationRepo = this.connection.getRepository(ctx, core_1.ProductVariantTranslation);
if (translation) {
translation.name = remoteProductName;
}
else {
translation = new core_1.ProductVariantTranslation();
translation.languageCode = languageCode;
translation.base = defaultVariant;
translation.name = remoteProductName;
defaultVariant.translations.push(translation);
}
await productTranslationRepo.save(translation);
}
async createProduct(ctx, newProduct, languageCode) {
//if a product doesn't exist, so wont its variant
const randomString = Math.random().toString(36).substring(2, 6);
const createProductInput = {
translations: [
{
languageCode,
description: '',
name: newProduct.name,
slug: `${(0, normalize_string_1.normalizeString)(`${newProduct.name}`, '-')}-${randomString}`,
},
],
customFields: {
webhookId: newProduct.id,
unit: newProduct.unit,
Measurement: newProduct.measurement,
},
};
const product = await this.translatableSaver.create({
ctx,
input: createProductInput,
entityType: core_1.Product,
translationType: core_1.ProductTranslation,
beforeSave: async (p) => {
await this.channelService.assignToCurrentChannel(p, ctx);
},
});
if (newProduct.brand) {
const brand = await this.getOrCreateBrand(ctx, newProduct.brand);
await this.connection.getRepository(ctx, core_1.Product).save((0, core_1.patchEntity)(product, {
customFields: {
...product.customFields,
brandName: brand.name,
brand: brand,
},
}));
}
await this.createProductVariant(ctx, product.id, newProduct, languageCode);
return this.productService.findOne(ctx, product.id, [
'featuredAsset',
'assets',
'channels',
'facetValues',
'facetValues.facet',
'variants',
]);
}
async updateDefaultVariant(ctx, vendureProduct, defaultChannel, remoteProduct) {
let defautVariant = vendureProduct.variants.find((variant) => variant.sku === remoteProduct.sku);
let defautVariantPriceInDefaultChannel = defautVariant?.productVariantPrices.find((variantPrice) => defaultChannel.id === variantPrice.channelId);
const moneyStrategy = this.configService.entityOptions.moneyStrategy;
const productVariantRepo = this.connection.getRepository(ctx, core_1.ProductVariant);
const productRepo = this.connection.getRepository(ctx, core_1.Product);
const productVariantPriceRepo = this.connection.getRepository(ctx, core_1.ProductVariantPrice);
if (defautVariant && defautVariantPriceInDefaultChannel) {
defautVariantPriceInDefaultChannel.price = moneyStrategy.round(parseFloat(remoteProduct.price) * 100);
await this.updateProductVariantName(ctx, defautVariant, defaultChannel.defaultLanguageCode, remoteProduct.name),
await productVariantPriceRepo.save(defautVariantPriceInDefaultChannel),
await productVariantRepo.save(defautVariant);
}
else if (defautVariant) {
defautVariantPriceInDefaultChannel = new core_1.ProductVariantPrice();
defautVariantPriceInDefaultChannel.channelId = defaultChannel.id;
defautVariantPriceInDefaultChannel.currencyCode =
defaultChannel.defaultCurrencyCode;
defautVariantPriceInDefaultChannel.price = moneyStrategy.round(parseFloat(remoteProduct.price) * 100);
defautVariantPriceInDefaultChannel.variant = defautVariant;
if (defautVariant?.productVariantPrices?.length) {
defautVariant.productVariantPrices.push(defautVariantPriceInDefaultChannel);
}
else {
defautVariant.productVariantPrices = [
defautVariantPriceInDefaultChannel,
];
}
await productVariantPriceRepo.save(defautVariantPriceInDefaultChannel),
await productVariantRepo.save(defautVariant),
await this.updateProductVariantName(ctx, defautVariant, defaultChannel.defaultLanguageCode, remoteProduct.name);
}
else {
// we need to create a default variant
defautVariant = await this.createProductVariant(ctx, vendureProduct.id, remoteProduct, defaultChannel.defaultLanguageCode);
vendureProduct.variants.push(defautVariant);
await productRepo.save(vendureProduct);
}
return defautVariant;
}
async createProductVariant(ctx, productId, remoteProduct, languageCode) {
const taxCategories = await this.taxCategoryService.findAll(ctx);
const taxCategory = taxCategories.items.find((t) => t.isDefault === true) ??
taxCategories.items[0];
const defaultStockLocation = await this.stockLocationService.defaultStockLocation(ctx);
const moneyStrategy = this.configService.entityOptions.moneyStrategy;
const createProductVariantInput = {
productId,
sku: remoteProduct.sku,
taxCategoryId: taxCategory.id,
translations: [
{
languageCode,
name: remoteProduct.name,
},
],
price: moneyStrategy.round(parseFloat(remoteProduct.price) * 100),
};
const inputWithoutPrice = {
...createProductVariantInput,
};
delete inputWithoutPrice.price;
const createdVariant = await this.translatableSaver.create({
ctx,
input: inputWithoutPrice,
entityType: core_1.ProductVariant,
translationType: core_1.ProductVariantTranslation,
beforeSave: async (variant) => {
variant.product = { id: createProductVariantInput.productId };
variant.taxCategory = {
id: createProductVariantInput.taxCategoryId,
};
await this.channelService.assignToCurrentChannel(variant, ctx);
},
typeOrmSubscriberData: {
channelId: ctx.channelId,
taxCategoryId: createProductVariantInput.taxCategoryId,
},
});
await Promise.all([
this.productVariantService.createOrUpdateProductVariantPrice(ctx, createdVariant.id, createProductVariantInput.price ?? 0, ctx.channelId),
this.stockMovementService.adjustProductVariantStock(ctx, createdVariant.id, [
{
stockLocationId: defaultStockLocation.id,
stockOnHand: exports.NEW_VARIANT_STOCK_VALUE,
},
]),
]);
return createdVariant;
}
async updateProductCollection(ctx, defautVariant, remoteProductCollection, languageCode) {
let collection = defautVariant.collections?.find((c) => !!c.translations.find((collectionTranslation) => collectionTranslation.name === remoteProductCollection &&
collectionTranslation.languageCode === languageCode));
const collectionFilterInput = {
code: 'variant-id-filter',
arguments: [
{ name: 'variantIds', value: `[\"${defautVariant.id}\"]` },
{ name: 'combineWithAnd', value: 'false' },
],
};
const newCollectionFilter = this.configArgService.parseInput('CollectionFilter', collectionFilterInput);
const productVariantRepo = this.connection.getRepository(ctx, core_1.ProductVariant);
if (!collection) {
// in this case the collection exists but the variant is not assigned to it
const colllectionRepo = this.connection.getRepository(ctx, core_1.Collection);
collection = await colllectionRepo
.createQueryBuilder('collection')
.leftJoin('collection.translations', 'collectionTranslatons')
.setFindOptions({
where: {
translations: { name: remoteProductCollection, languageCode },
},
})
.getOne();
let savedCollection;
if (collection?.filters?.length) {
const variantIdFilter = collection.filters.find((filterDef) => filterDef.code === core_1.variantIdCollectionFilter.code);
if (variantIdFilter) {
const variantIdsList = JSON.parse(variantIdFilter.args[0].value);
variantIdsList.push(defautVariant.id.toString());
variantIdFilter.args[0].value = JSON.stringify(variantIdsList);
}
else {
collection.filters.push(newCollectionFilter);
}
savedCollection = await colllectionRepo.save(collection);
}
if (collection && !collection.filters?.length) {
collection.filters = [newCollectionFilter];
savedCollection = await colllectionRepo.save(collection);
}
if (savedCollection && defautVariant.collections?.length) {
defautVariant.collections.push(savedCollection);
}
if (savedCollection && !defautVariant.collections?.length) {
defautVariant.collections = [savedCollection];
}
}
if (!collection) {
//in this case the collection doesn't exist
const randomString = Math.random().toString(36).substring(2, 6);
const input = {
filters: [collectionFilterInput],
translations: [
{
description: '',
languageCode: languageCode,
name: remoteProductCollection,
slug: `${(0, normalize_string_1.normalizeString)(`${remoteProductCollection}`, '-')}-${randomString}`,
},
],
};
collection = await this.collectionService.create(ctx, input);
if (defautVariant.collections?.length) {
defautVariant.collections.push(collection);
}
else {
defautVariant.collections = [collection];
}
}
defautVariant = await productVariantRepo.save(defautVariant);
}
async updateProductFacet(ctx, vendureProduct, remoteProduct, languageCode) {
let facetValue = vendureProduct.facetValues?.find((facetValue) => !!facetValue.translations.find((facetValueTranslation) => facetValueTranslation.name === remoteProduct.childfacet &&
facetValueTranslation.languageCode === languageCode));
// let facetExists= facetValue?.facet.translations.some((facetTranslation)=> facetTranslation.name === remoteProduct.parentfacet && facetTranslation.languageCode === languageCode)
const productRepo = this.connection.getRepository(ctx, core_1.Product);
if (!facetValue) {
//lets check if the facet value exist
const facetValueRepo = this.connection.getRepository(ctx, core_1.FacetValue);
facetValue = await facetValueRepo
.createQueryBuilder('facetValue')
.leftJoinAndSelect('facetValue.translations', 'translation')
.setFindOptions({
where: {
translations: { name: remoteProduct.childfacet, languageCode },
},
})
.getOne();
if (facetValue && vendureProduct.facetValues?.length) {
vendureProduct.facetValues.push(facetValue);
}
if (facetValue && !vendureProduct.facetValues?.length) {
vendureProduct.facetValues = [facetValue];
}
await productRepo.save(vendureProduct);
}
if (!facetValue) {
//okay, the facet value doesn't ecist. let's check if the facet exist
const facetValueRepo = this.connection.getRepository(ctx, core_1.FacetValue);
const facetRepo = this.connection.getRepository(ctx, core_1.Facet);
const facet = await facetRepo
.createQueryBuilder('facet')
.leftJoinAndSelect('facet.translations', 'translation')
.setFindOptions({
where: {
translations: { name: remoteProduct.parentfacet, languageCode },
},
})
.getOne();
if (facet) {
const randomString = Math.random().toString(36).substring(2, 6);
const createFacetValueInput = {
code: `${(0, normalize_string_1.normalizeString)(`${remoteProduct.childfacet}`, '-')}-${randomString}`,
facetId: facet.id,
translations: [
{
languageCode: languageCode,
name: remoteProduct.childfacet,
},
],
};
facetValue = await this.facetValueService.create(ctx, facet, createFacetValueInput);
}
if (facetValue && vendureProduct.facetValues?.length) {
vendureProduct.facetValues.push(facetValue);
}
if (facetValue && !vendureProduct.facetValues?.length) {
vendureProduct.facetValues = [facetValue];
}
await productRepo.save(vendureProduct);
}
if (!facetValue) {
//neither the facet value nor the facet exist
const randomString = Math.random().toString(36).substring(2, 6);
const input = {
code: `${(0, normalize_string_1.normalizeString)(`${remoteProduct.parentfacet}`, '-')}-${randomString}`,
isPrivate: false,
translations: [
{
languageCode: languageCode,
name: remoteProduct.parentfacet,
},
],
};
const facet = await this.facetService.create(ctx, input);
const anotherRandomString = Math.random().toString(36).substring(2, 6);
const createFacetValueInput = {
code: `${(0, normalize_string_1.normalizeString)(`${remoteProduct.childfacet}`, '-')}-${anotherRandomString}`,
facetId: facet.id,
translations: [
{
languageCode: languageCode,
name: remoteProduct.childfacet,
},
],
};
facetValue = await this.facetValueService.create(ctx, facet, createFacetValueInput);
if (vendureProduct.facetValues?.length) {
vendureProduct.facetValues.push(facetValue);
}
else {
vendureProduct.facetValues = [facetValue];
}
await productRepo.save(vendureProduct);
}
}
};
exports.ProductImportService = ProductImportService;
exports.ProductImportService = ProductImportService = __decorate([
(0, common_1.Injectable)(),
__param(20, (0, common_1.Inject)(constants_1.PLUGIN_INIT_OPTIONS)),
__metadata("design:paramtypes", [core_1.EntityHydrator,
axios_1.HttpService,
core_1.ChannelService,
core_1.ProductVariantService,
core_1.ConfigArgService,
core_1.FacetService,
core_1.StockLocationService,
core_1.FacetValueService,
core_1.TranslatableSaver,
core_1.SearchService,
core_1.ProductService,
core_1.TaxCategoryService,
core_1.TransactionalConnection,
core_1.EventBus,
core_1.ProcessContext,
core_1.CollectionService,
schedule_1.SchedulerRegistry,
core_1.JobQueueService,
core_1.StockMovementService,
vendure_brands_plugin_1.BrandService, Object, core_1.ConfigService])
], ProductImportService);