UNPKG

@vendure/core

Version:

A modern, headless ecommerce framework

418 lines 21.2 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.Importer = void 0; const common_1 = require("@nestjs/common"); const normalize_string_1 = require("@vendure/common/lib/normalize-string"); const rxjs_1 = require("rxjs"); const request_context_1 = require("../../../api/common/request-context"); const errors_1 = require("../../../common/error/errors"); const config_service_1 = require("../../../config/config.service"); const index_1 = require("../../../config/index"); const channel_service_1 = require("../../../service/services/channel.service"); const facet_value_service_1 = require("../../../service/services/facet-value.service"); const facet_service_1 = require("../../../service/services/facet.service"); const tax_category_service_1 = require("../../../service/services/tax-category.service"); const asset_importer_1 = require("../asset-importer/asset-importer"); const import_parser_1 = require("../import-parser/import-parser"); const fast_importer_service_1 = require("./fast-importer.service"); const progress_bar_1 = require("./progress-bar"); /** * @description * Parses and imports Products using the CSV import format. * * Internally it is using the {@link ImportParser} to parse the CSV file, and then the * {@link FastImporterService} and the {@link AssetImporter} to actually create the resulting * entities in the Vendure database. * * @docsCategory import-export */ let Importer = class Importer { /** @internal */ constructor(configService, importParser, channelService, facetService, facetValueService, taxCategoryService, assetImporter, fastImporter) { this.configService = configService; this.importParser = importParser; this.channelService = channelService; this.facetService = facetService; this.facetValueService = facetValueService; this.taxCategoryService = taxCategoryService; this.assetImporter = assetImporter; this.fastImporter = fastImporter; this.taxCategoryMatches = {}; // These Maps are used to cache newly-created entities and prevent duplicates // from being created. this.facetMap = new Map(); this.facetValueMap = new Map(); } /** * @description * Parses the contents of the [product import CSV file](/developer-guide/importing-data/#product-import-format) and imports * the resulting Product & ProductVariants, as well as any associated Assets, Facets & FacetValues. * * The `ctxOrLanguageCode` argument is used to specify the languageCode to be used when creating the Products. */ parseAndImport(input, ctxOrLanguageCode, reportProgress = false) { let bar; return new rxjs_1.Observable(subscriber => { const p = this.doParseAndImport(input, ctxOrLanguageCode, progress => { if (reportProgress) { if (!bar) { bar = new progress_bar_1.ProgressBar(' importing [:bar] :percent :etas Importing: :prodName', { complete: '=', incomplete: ' ', total: progress.processed, width: 40, }); } bar.tick({ prodName: progress.currentProduct }); } subscriber.next(progress); }).then(value => { subscriber.next(Object.assign(Object.assign({}, value), { currentProduct: 'Complete' })); subscriber.complete(); }); }); } async doParseAndImport(input, ctxOrLanguageCode, onProgress) { const ctx = await this.getRequestContext(ctxOrLanguageCode); const parsed = await this.importParser.parseProducts(input, ctx.languageCode); if (parsed && parsed.results.length) { try { const importErrors = await this.importProducts(ctx, parsed.results, progess => { onProgress(Object.assign(Object.assign({}, progess), { processed: parsed.processed })); }); return { errors: parsed.errors.concat(importErrors), imported: parsed.results.length, processed: parsed.processed, }; } catch (err) { return { errors: [err.message], imported: 0, processed: parsed.processed, }; } } else { return { errors: [], imported: 0, processed: parsed.processed, }; } } async getRequestContext(ctxOrLanguageCode) { if (ctxOrLanguageCode instanceof request_context_1.RequestContext) { return ctxOrLanguageCode; } else { const channel = await this.channelService.getDefaultChannel(); return new request_context_1.RequestContext({ apiType: 'admin', isAuthorized: true, authorizedAsOwnerOnly: false, channel, languageCode: ctxOrLanguageCode, }); } } /** * @description * Imports the products specified in the rows object. Return an array of error messages. */ async importProducts(ctx, rows, onProgress) { var _a; let errors = []; let imported = 0; const languageCode = ctx.languageCode; const taxCategories = await this.taxCategoryService.findAll(ctx); if (taxCategories.totalItems === 0) { index_1.Logger.error([ `No TaxCategories found in the database. Ensure that at least one TaxCategory exists.`, `If you are populating from an InitialData object, ensure the 'taxRates' array is not empty.`, ].join('\n')); throw new Error(`No TaxCategories found in the database. Ensure the IntialData.taxRates array is not empty.`); } await this.fastImporter.initialize(ctx.channel); // Build a map of shared option groups (only groups with an explicit code are shared) const sharedGroupMap = await this.buildSharedOptionGroupMap(ctx, rows); for (const { product, variants } of rows) { const productMainTranslation = this.getTranslationByCodeOrFirst(product.translations, ctx.languageCode); const createProductAssets = await this.assetImporter.getAssets(product.assetPaths, ctx); const productAssets = createProductAssets.assets; if (createProductAssets.errors.length) { errors = errors.concat(createProductAssets.errors); } const customFields = this.processCustomFieldValues(product.translations[0].customFields, this.configService.customFields.Product); const createdProductId = await this.fastImporter.createProduct({ featuredAssetId: productAssets.length ? productAssets[0].id : undefined, assetIds: productAssets.map(a => a.id), facetValueIds: await this.getFacetValueIds(ctx, product.facets, ctx.languageCode), translations: product.translations.map(translation => { return { languageCode: translation.languageCode, name: translation.name, description: translation.description, slug: translation.slug, customFields: this.processCustomFieldValues(translation.customFields, this.configService.customFields.Product), }; }), customFields, }); const optionsMap = {}; for (const [optionGroup, optionGroupIndex] of product.optionGroups.map((group, i) => [group, i])) { const optionGroupMainTranslation = this.getTranslationByCodeOrFirst(optionGroup.translations, ctx.languageCode); const shared = optionGroup.code ? sharedGroupMap.get(optionGroup.code) : undefined; if (shared) { // Use the pre-created shared option group await this.fastImporter.addOptionGroupToProduct(createdProductId, shared.groupId); for (const value of optionGroupMainTranslation.values) { const optionId = shared.optionIds.get(value); if (!optionId) { throw new Error(`Option value "${value}" not found in shared option group "${(_a = optionGroup.code) !== null && _a !== void 0 ? _a : ''}"`); } optionsMap[`${optionGroupIndex}_${value}`] = optionId; } } else { // Create a product-specific option group (original behaviour) const code = (0, normalize_string_1.normalizeString)(`${productMainTranslation.name}-${optionGroupMainTranslation.name}`, '-'); const groupId = await this.fastImporter.createProductOptionGroup({ code, options: optionGroupMainTranslation.values.map(name => ({})), translations: optionGroup.translations.map(translation => { return { languageCode: translation.languageCode, name: translation.name, }; }), }); for (const [optionIndex, value] of optionGroupMainTranslation.values.map((val, index) => [index, val])) { const createdOptionId = await this.fastImporter.createProductOption({ productOptionGroupId: groupId, code: (0, normalize_string_1.normalizeString)(value, '-'), translations: optionGroup.translations.map(translation => { return { languageCode: translation.languageCode, name: translation.values[optionIndex], }; }), }); optionsMap[`${optionGroupIndex}_${value}`] = createdOptionId; } await this.fastImporter.addOptionGroupToProduct(createdProductId, groupId); } } for (const variant of variants) { const variantMainTranslation = this.getTranslationByCodeOrFirst(variant.translations, ctx.languageCode); const createVariantAssets = await this.assetImporter.getAssets(variant.assetPaths, ctx); const variantAssets = createVariantAssets.assets; if (createVariantAssets.errors.length) { errors = errors.concat(createVariantAssets.errors); } let facetValueIds = []; if (0 < variant.facets.length) { facetValueIds = await this.getFacetValueIds(ctx, variant.facets, languageCode); } const variantCustomFields = this.processCustomFieldValues(variantMainTranslation.customFields, this.configService.customFields.ProductVariant); const optionIds = variantMainTranslation.optionValues.map((v, index) => optionsMap[`${index}_${v}`]); const createdVariant = await this.fastImporter.createProductVariant({ productId: createdProductId, facetValueIds, featuredAssetId: variantAssets.length ? variantAssets[0].id : undefined, assetIds: variantAssets.map(a => a.id), sku: variant.sku, taxCategoryId: this.getMatchingTaxCategoryId(variant.taxCategory, taxCategories.items), stockOnHand: variant.stockOnHand, trackInventory: variant.trackInventory, optionIds, translations: variant.translations.map(translation => { const productTranslation = product.translations.find(t => t.languageCode === translation.languageCode); if (!productTranslation) { throw new errors_1.InternalServerError(`No translation '${translation.languageCode}' for product with slug '${productMainTranslation.slug}'`); } return { languageCode: translation.languageCode, name: [productTranslation.name, ...translation.optionValues].join(' '), customFields: this.processCustomFieldValues(translation.customFields, this.configService.customFields.ProductVariant), }; }), price: Math.round(variant.price * 100), customFields: variantCustomFields, }); } imported++; onProgress({ processed: 0, imported, errors, currentProduct: productMainTranslation.name, }); } return errors; } async getFacetValueIds(ctx, facets, languageCode) { const facetValueIds = []; for (const item of facets) { const itemMainTranslation = this.getTranslationByCodeOrFirst(item.translations, languageCode); const facetName = itemMainTranslation.facet; const valueName = itemMainTranslation.value; let facetEntity; const cachedFacet = this.facetMap.get(facetName); if (cachedFacet) { facetEntity = cachedFacet; } else { const existing = await this.facetService.findByCode(ctx, (0, normalize_string_1.normalizeString)(facetName, '-'), languageCode); if (existing) { facetEntity = existing; } else { facetEntity = await this.facetService.create(ctx, { isPrivate: false, code: (0, normalize_string_1.normalizeString)(facetName, '-'), translations: item.translations.map(translation => { return { languageCode: translation.languageCode, name: translation.facet, }; }), }); } this.facetMap.set(facetName, facetEntity); } let facetValueEntity; const facetValueMapKey = `${facetName}:${valueName}`; const cachedFacetValue = this.facetValueMap.get(facetValueMapKey); if (cachedFacetValue) { facetValueEntity = cachedFacetValue; } else { const existing = facetEntity.values.find(v => v.name === valueName); if (existing) { facetValueEntity = existing; } else { facetValueEntity = await this.facetValueService.create(ctx, facetEntity, { code: (0, normalize_string_1.normalizeString)(valueName, '-'), translations: item.translations.map(translation => { return { languageCode: translation.languageCode, name: translation.value, }; }), }); } this.facetValueMap.set(facetValueMapKey, facetValueEntity); } facetValueIds.push(facetValueEntity.id); } return facetValueIds; } processCustomFieldValues(customFields, config) { const processed = {}; for (const fieldDef of config) { const value = customFields[fieldDef.name]; if (fieldDef.list === true) { processed[fieldDef.name] = value === null || value === void 0 ? void 0 : value.split('|').filter(val => val.trim() !== ''); } else if (fieldDef.type === 'boolean') { processed[fieldDef.name] = value ? value.toLowerCase() === 'true' : undefined; } else { processed[fieldDef.name] = value ? value : undefined; } } return processed; } /** * Attempts to match a TaxCategory entity against the name supplied in the import table. If no matches * are found, the first TaxCategory id is returned. */ getMatchingTaxCategoryId(name, taxCategories) { if (this.taxCategoryMatches[name]) { return this.taxCategoryMatches[name]; } const regex = new RegExp(name, 'i'); const found = taxCategories.find(tc => !!tc.name.match(regex)); const match = found ? found : taxCategories[0]; this.taxCategoryMatches[name] = match.id; return match.id; } getTranslationByCodeOrFirst(translations, languageCode) { let translation = translations.find(t => t.languageCode === languageCode); if (!translation) { translation = translations[0]; } return translation; } /** * Performs a first-pass analysis of all parsed products to identify option groups * that should be shared. Only groups with an explicit code (from the `name:code` CSV syntax) * are shared. Groups without a code are always product-scoped. * * Returns a map of code -> { groupId, optionIds } for shared groups that have * been pre-created. */ async buildSharedOptionGroupMap(ctx, rows) { const sharedGroupMap = new Map(); // Collect the first occurrence of each explicit-code option group const codeEntries = new Map(); for (const { product } of rows) { for (const optionGroup of product.optionGroups) { if (optionGroup.code && !codeEntries.has(optionGroup.code)) { codeEntries.set(optionGroup.code, optionGroup); } } } // Pre-create each shared option group for (const [code, optionGroup] of codeEntries) { const ogMainTranslation = this.getTranslationByCodeOrFirst(optionGroup.translations, ctx.languageCode); const groupId = await this.fastImporter.createProductOptionGroup({ code, options: ogMainTranslation.values.map(() => ({})), translations: optionGroup.translations.map(translation => ({ languageCode: translation.languageCode, name: translation.name, })), }); const optionIds = new Map(); for (const [optionIndex, value] of ogMainTranslation.values.map((val, index) => [index, val])) { const createdOptionId = await this.fastImporter.createProductOption({ productOptionGroupId: groupId, code: (0, normalize_string_1.normalizeString)(value, '-'), translations: optionGroup.translations.map(translation => ({ languageCode: translation.languageCode, name: translation.values[optionIndex], })), }); optionIds.set(value, createdOptionId); } sharedGroupMap.set(code, { groupId, optionIds }); } return sharedGroupMap; } }; exports.Importer = Importer; exports.Importer = Importer = __decorate([ (0, common_1.Injectable)(), __metadata("design:paramtypes", [config_service_1.ConfigService, import_parser_1.ImportParser, channel_service_1.ChannelService, facet_service_1.FacetService, facet_value_service_1.FacetValueService, tax_category_service_1.TaxCategoryService, asset_importer_1.AssetImporter, fast_importer_service_1.FastImporterService]) ], Importer); //# sourceMappingURL=importer.js.map