@haus-tech/product-import-export-plugin 3.0.9 → 3.0.11-rc.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -13,6 +13,7 @@ exports.ProductImporter = void 0;
|
|
|
13
13
|
const common_1 = require("@nestjs/common");
|
|
14
14
|
const core_1 = require("@vendure/core");
|
|
15
15
|
const rxjs_1 = require("rxjs");
|
|
16
|
+
const generated_types_1 = require("@vendure/common/lib/generated-types");
|
|
16
17
|
const extended_fast_importer_service_1 = require("../../services/extended-fast-importer.service");
|
|
17
18
|
const lodash_1 = require("lodash");
|
|
18
19
|
const normalize_string_1 = require("../../helpers/normalize-string");
|
|
@@ -312,12 +313,16 @@ let ProductImporter = class ProductImporter {
|
|
|
312
313
|
if (existingProduct && updatingStrategy === 'replace') {
|
|
313
314
|
await this.fastImporter.removeAllAssetsFromProduct(existingProduct);
|
|
314
315
|
}
|
|
316
|
+
const getFeaturedAssetId = (product) => {
|
|
317
|
+
if (product?.featuredAsset) {
|
|
318
|
+
const assetIsImage = product.featuredAsset.type === generated_types_1.AssetType.IMAGE;
|
|
319
|
+
return assetIsImage ? product.featuredAsset.id : undefined;
|
|
320
|
+
}
|
|
321
|
+
const firstAsset = productAssets.find((a) => a.type === generated_types_1.AssetType.IMAGE);
|
|
322
|
+
return firstAsset ? firstAsset.id : undefined;
|
|
323
|
+
};
|
|
315
324
|
const productData = {
|
|
316
|
-
featuredAssetId: existingProduct
|
|
317
|
-
? undefined
|
|
318
|
-
: productAssets.length
|
|
319
|
-
? productAssets[0].id
|
|
320
|
-
: undefined,
|
|
325
|
+
featuredAssetId: getFeaturedAssetId(existingProduct),
|
|
321
326
|
assetIds: productAssets.map((a) => a.id),
|
|
322
327
|
facetValueIds: updatingStrategy === 'replace' ? [] : undefined,
|
|
323
328
|
translations: await Promise.all(product.translations.map(async (translation) => {
|
|
@@ -440,14 +445,18 @@ let ProductImporter = class ProductImporter {
|
|
|
440
445
|
// Remove all assets from the product
|
|
441
446
|
await this.fastImporter.removeAllAssetsFromVariant(existingVariant);
|
|
442
447
|
}
|
|
448
|
+
const getFeaturedAssetId = (variant) => {
|
|
449
|
+
if (variant?.featuredAsset) {
|
|
450
|
+
const assetIsImage = variant.featuredAsset.type === generated_types_1.AssetType.IMAGE;
|
|
451
|
+
return assetIsImage ? variant.featuredAsset.id : undefined;
|
|
452
|
+
}
|
|
453
|
+
const firstAsset = variantAssets.find((a) => a.type === generated_types_1.AssetType.IMAGE);
|
|
454
|
+
return firstAsset ? firstAsset.id : undefined;
|
|
455
|
+
};
|
|
443
456
|
const variantData = {
|
|
444
457
|
productId: createdProductId,
|
|
445
458
|
facetValueIds: updatingStrategy === 'replace' ? [] : undefined,
|
|
446
|
-
featuredAssetId: existingVariant
|
|
447
|
-
? undefined
|
|
448
|
-
: variantAssets.length
|
|
449
|
-
? variantAssets[0].id
|
|
450
|
-
: undefined,
|
|
459
|
+
featuredAssetId: getFeaturedAssetId(existingVariant),
|
|
451
460
|
assetIds: variantAssets.map((a) => a.id),
|
|
452
461
|
sku: variant.sku,
|
|
453
462
|
taxCategoryId: this.getMatchingTaxCategoryId(variant.taxCategory, taxCategories.items),
|
|
@@ -69,6 +69,9 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
|
|
|
69
69
|
if (input.featuredAssetId) {
|
|
70
70
|
p.featuredAsset = { id: input.featuredAssetId };
|
|
71
71
|
}
|
|
72
|
+
else {
|
|
73
|
+
(0, lodash_1.set)(p, 'featuredAssetId', null);
|
|
74
|
+
}
|
|
72
75
|
},
|
|
73
76
|
});
|
|
74
77
|
if (input.assetIds) {
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ID, ProductService, RequestContext, StockLevelService, ChannelService, ConfigService, Product, LanguageCode } from '@vendure/core';
|
|
2
2
|
import { PluginInitOptions } from '../types';
|
|
3
|
+
import { CsvWriter } from 'csv-writer/src/lib/csv-writer';
|
|
3
4
|
export type RelationType = 'string' | 'boolean' | 'localeString' | 'text' | 'localeText' | 'int' | 'float' | 'datetime' | 'relation';
|
|
4
5
|
export declare class ProductExportService {
|
|
5
6
|
private options;
|
|
6
7
|
private productService;
|
|
7
8
|
private stockLevelService;
|
|
8
|
-
private entityHydratorService;
|
|
9
9
|
private channelService;
|
|
10
10
|
private configService;
|
|
11
|
-
constructor(options: PluginInitOptions, productService: ProductService, stockLevelService: StockLevelService,
|
|
12
|
-
createExportFile(ctx: RequestContext, selectionIds: ID[], fileName: string, selectedCustomFields: string, exportAssetsAs: 'url' | 'json', selectedExportFields: string): Promise<string>;
|
|
11
|
+
constructor(options: PluginInitOptions, productService: ProductService, stockLevelService: StockLevelService, channelService: ChannelService, configService: ConfigService);
|
|
12
|
+
createExportFile(ctx: RequestContext, selectionIds: ID[], fileName: string, selectedCustomFields: string, exportAssetsAs: 'url' | 'json', selectedExportFields: string, pageSize?: number, concurrency?: number): Promise<string>;
|
|
13
|
+
exportProduct(ctx: RequestContext, product: Product, languages: LanguageCode[], filteredCustomFieldNames: string[], exportAssetsAs: 'url' | 'json', csvWriter: CsvWriter<any>, records?: any[]): Promise<void>;
|
|
13
14
|
private handleAssets;
|
|
14
15
|
private handleCustomFields;
|
|
15
16
|
private getStockOnHand;
|
|
@@ -46,23 +46,21 @@ const csv_writer_1 = require("csv-writer");
|
|
|
46
46
|
const path = __importStar(require("path"));
|
|
47
47
|
const os_1 = __importDefault(require("os"));
|
|
48
48
|
const lodash_1 = require("lodash");
|
|
49
|
+
const bottleneck_1 = __importDefault(require("bottleneck"));
|
|
49
50
|
let ProductExportService = class ProductExportService {
|
|
50
51
|
options;
|
|
51
52
|
productService;
|
|
52
53
|
stockLevelService;
|
|
53
|
-
entityHydratorService;
|
|
54
54
|
channelService;
|
|
55
55
|
configService;
|
|
56
|
-
constructor(options, productService, stockLevelService,
|
|
56
|
+
constructor(options, productService, stockLevelService, channelService, configService) {
|
|
57
57
|
this.options = options;
|
|
58
58
|
this.productService = productService;
|
|
59
59
|
this.stockLevelService = stockLevelService;
|
|
60
|
-
this.entityHydratorService = entityHydratorService;
|
|
61
60
|
this.channelService = channelService;
|
|
62
61
|
this.configService = configService;
|
|
63
62
|
}
|
|
64
|
-
async createExportFile(ctx, selectionIds, fileName, selectedCustomFields, exportAssetsAs, selectedExportFields) {
|
|
65
|
-
const products = await this.productService.findByIds(ctx, selectionIds);
|
|
63
|
+
async createExportFile(ctx, selectionIds, fileName, selectedCustomFields, exportAssetsAs, selectedExportFields, pageSize = 50, concurrency = 5) {
|
|
66
64
|
const channel = await this.channelService.findOne(ctx, ctx.channelId);
|
|
67
65
|
if (!channel) {
|
|
68
66
|
throw new Error('Channel not found');
|
|
@@ -105,113 +103,129 @@ let ProductExportService = class ProductExportService {
|
|
|
105
103
|
const csvWriter = (0, csv_writer_1.createObjectCsvWriter)({
|
|
106
104
|
path: exportFile,
|
|
107
105
|
header: filteredHeaders,
|
|
106
|
+
append: false,
|
|
108
107
|
});
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
108
|
+
const limiter = new bottleneck_1.default({
|
|
109
|
+
maxConcurrent: concurrency,
|
|
110
|
+
});
|
|
111
|
+
let currentPage = 1;
|
|
112
|
+
let hasMore = true;
|
|
113
|
+
const records = [];
|
|
114
|
+
while (hasMore) {
|
|
115
|
+
const { items, totalItems } = await this.productService.findAll(ctx, {
|
|
116
|
+
filter: {
|
|
117
|
+
id: { in: selectionIds },
|
|
118
|
+
},
|
|
119
|
+
skip: (currentPage - 1) * pageSize,
|
|
120
|
+
take: pageSize,
|
|
121
|
+
}, [
|
|
122
|
+
'variants',
|
|
123
|
+
'facetValues',
|
|
124
|
+
'facetValues.facet',
|
|
125
|
+
'optionGroups',
|
|
126
|
+
'assets',
|
|
127
|
+
'variants.assets',
|
|
128
|
+
'variants.facetValues',
|
|
129
|
+
'variants.facetValues.facet',
|
|
130
|
+
'variants.options',
|
|
131
|
+
]);
|
|
132
|
+
hasMore = currentPage * pageSize < totalItems;
|
|
133
|
+
const exportPromises = items.map((product) => limiter.schedule(async () => this.exportProduct(ctx, product, languages, filteredCustomFieldNames, exportAssetsAs, csvWriter, records)));
|
|
134
|
+
await Promise.all(exportPromises);
|
|
135
|
+
currentPage++;
|
|
136
|
+
}
|
|
137
|
+
return exportFile;
|
|
138
|
+
}
|
|
139
|
+
async exportProduct(ctx, product, languages, filteredCustomFieldNames, exportAssetsAs, csvWriter, records = []) {
|
|
140
|
+
const { assets = [], facetValues = [], optionGroups = [], variants = [], translations = [], customFields = {}, } = product;
|
|
141
|
+
const nameTranslations = this.mapTranslations(translations, 'name', languages);
|
|
142
|
+
const slugTranslations = this.mapTranslations(translations, 'slug', languages);
|
|
143
|
+
const descriptionTranslations = this.mapTranslations(translations, 'description', languages);
|
|
144
|
+
// Filter out all variants that are soft deleted
|
|
145
|
+
const activeVariants = variants.filter((v) => !v.deletedAt);
|
|
146
|
+
const productAssets = assets.length === 0
|
|
147
|
+
? ''
|
|
148
|
+
: assets.length > 0
|
|
149
|
+
? this.handleAssets(assets.map(({ asset }) => asset), exportAssetsAs)
|
|
150
|
+
: this.handleAssets([product.featuredAsset], exportAssetsAs);
|
|
151
|
+
const productFacets = languages.reduce((acc, lang) => {
|
|
152
|
+
acc[lang] = facetValues
|
|
153
|
+
.map((facetValue) => this.mapFacetTranslations(facetValue, lang))
|
|
154
|
+
.filter((facet) => facet)
|
|
155
|
+
.join('|');
|
|
156
|
+
return acc;
|
|
157
|
+
}, {});
|
|
158
|
+
const optionGroupNames = languages.reduce((acc, lang) => {
|
|
159
|
+
acc[lang] = (0, lodash_1.sortBy)(optionGroups, (g) => g.id)
|
|
160
|
+
.map((group) => this.mapTranslations(group.translations, 'name', [lang])[lang])
|
|
161
|
+
.filter((name) => name)
|
|
162
|
+
.join('|');
|
|
163
|
+
return acc;
|
|
164
|
+
}, {});
|
|
165
|
+
for (const variant of activeVariants) {
|
|
166
|
+
const variantValues = languages.reduce((acc, lang) => {
|
|
167
|
+
acc[lang] = ((0, lodash_1.sortBy)(variant.options, (o) => o.groupId) || [])
|
|
168
|
+
.map((option) => this.mapTranslations(option.translations, 'name', [lang])[lang])
|
|
169
|
+
.filter((name) => name)
|
|
139
170
|
.join('|');
|
|
140
171
|
return acc;
|
|
141
172
|
}, {});
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
.
|
|
173
|
+
const variantAssets = this.handleAssets(variant.assets.map(({ asset }) => asset), exportAssetsAs);
|
|
174
|
+
const variantFacets = languages.reduce((acc, lang) => {
|
|
175
|
+
acc[lang] = (variant.facetValues || [])
|
|
176
|
+
.map((facet) => this.mapFacetTranslations(facet, lang))
|
|
177
|
+
.filter((facet) => facet)
|
|
146
178
|
.join('|');
|
|
147
179
|
return acc;
|
|
148
180
|
}, {});
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
record[
|
|
187
|
-
|
|
188
|
-
record[`optionValues:${lang}`] = variantValues[lang];
|
|
189
|
-
record[`variantFacets:${lang}`] = variantFacets[lang];
|
|
181
|
+
const stockOnHand = await this.getStockOnHand(ctx, variant.id); // Adjusted to fetch stock details
|
|
182
|
+
const variantTranslations = variant.translations;
|
|
183
|
+
const variantNameTranslations = this.mapTranslations(variantTranslations, 'name', languages);
|
|
184
|
+
const record = {};
|
|
185
|
+
const convertToHTML = (text) => {
|
|
186
|
+
return text
|
|
187
|
+
.replace(/\n/g, '<br>')
|
|
188
|
+
.replace(/\t/g, ' ')
|
|
189
|
+
.replace(/,\s*'/g, ", '");
|
|
190
|
+
};
|
|
191
|
+
for (const lang of languages) {
|
|
192
|
+
const escapedDescription = convertToHTML(descriptionTranslations[lang]);
|
|
193
|
+
const escapedName = nameTranslations[lang]
|
|
194
|
+
.replace(/"/g, "'")
|
|
195
|
+
.replace(/\s+/g, ' ')
|
|
196
|
+
.replace(/,\s*'/g, ", '")
|
|
197
|
+
.replace(/'/g, "''");
|
|
198
|
+
record.productId = records.length === 0 ? product.id : '';
|
|
199
|
+
record[`name:${lang}`] = records.length === 0 ? escapedName || '' : '';
|
|
200
|
+
record[`slug:${lang}`] = records.length === 0 ? slugTranslations[lang] || '' : '';
|
|
201
|
+
record[`description:${lang}`] = records.length === 0 ? escapedDescription || '' : '';
|
|
202
|
+
record[`facets:${lang}`] = records.length === 0 ? productFacets[lang] : '';
|
|
203
|
+
record[`optionGroups:${lang}`] = records.length === 0 ? optionGroupNames[lang] : '';
|
|
204
|
+
record[`optionValues:${lang}`] = variantValues[lang];
|
|
205
|
+
record[`variantFacets:${lang}`] = variantFacets[lang];
|
|
206
|
+
}
|
|
207
|
+
const productVariantPrices = variant.productVariantPrices.filter((price) => price.channelId === ctx.channelId);
|
|
208
|
+
record.assets = records.length === 0 ? productAssets : '';
|
|
209
|
+
record.sku = variant.sku;
|
|
210
|
+
record.price = productVariantPrices[0]?.price / 100; // Assuming the price is stored in cents
|
|
211
|
+
record.taxCategory = 'standard'; // Replace with actual tax category if available
|
|
212
|
+
record.stockOnHand = stockOnHand;
|
|
213
|
+
record.trackInventory = variant.trackInventory.toLowerCase();
|
|
214
|
+
record.variantAssets = variantAssets;
|
|
215
|
+
for (const field of filteredCustomFieldNames) {
|
|
216
|
+
const [entity, fieldName, type] = field.split(':');
|
|
217
|
+
if (entity === 'product') {
|
|
218
|
+
record[field] =
|
|
219
|
+
this.handleCustomFields(customFields, fieldName, type, exportAssetsAs) || '';
|
|
190
220
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
record.price = productVariantPrices[0]?.price / 100; // Assuming the price is stored in cents
|
|
195
|
-
record.taxCategory = 'standard'; // Replace with actual tax category if available
|
|
196
|
-
record.stockOnHand = stockOnHand;
|
|
197
|
-
record.trackInventory = variant.trackInventory.toLowerCase();
|
|
198
|
-
record.variantAssets = variantAssets;
|
|
199
|
-
for (const field of filteredCustomFieldNames) {
|
|
200
|
-
const [entity, fieldName, type] = field.split(':');
|
|
201
|
-
if (entity === 'product') {
|
|
202
|
-
record[field] =
|
|
203
|
-
this.handleCustomFields(customFields, fieldName, type, exportAssetsAs) || '';
|
|
204
|
-
}
|
|
205
|
-
else if (entity === 'variant') {
|
|
206
|
-
record[field] =
|
|
207
|
-
this.handleCustomFields(variant.customFields, fieldName, type, exportAssetsAs) || '';
|
|
208
|
-
}
|
|
221
|
+
else if (entity === 'variant') {
|
|
222
|
+
record[field] =
|
|
223
|
+
this.handleCustomFields(variant.customFields, fieldName, type, exportAssetsAs) || '';
|
|
209
224
|
}
|
|
210
|
-
records.push(record);
|
|
211
225
|
}
|
|
212
|
-
|
|
226
|
+
records.push(record);
|
|
213
227
|
}
|
|
214
|
-
|
|
228
|
+
await csvWriter.writeRecords(records);
|
|
215
229
|
}
|
|
216
230
|
handleAssets(assets, exportAssetsAs) {
|
|
217
231
|
if (!assets.length) {
|
|
@@ -337,7 +351,6 @@ ProductExportService = __decorate([
|
|
|
337
351
|
__param(0, (0, common_1.Inject)(constants_1.PRODUCT_IMPORT_EXPORT_PLUGIN_OPTIONS)),
|
|
338
352
|
__metadata("design:paramtypes", [Object, core_1.ProductService,
|
|
339
353
|
core_1.StockLevelService,
|
|
340
|
-
core_1.EntityHydrator,
|
|
341
354
|
core_1.ChannelService,
|
|
342
355
|
core_1.ConfigService])
|
|
343
356
|
], ProductExportService);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haus-tech/product-import-export-plugin",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.11-rc.0",
|
|
4
4
|
"description": "A Vendure plugin for importing and exporting products using a CSV file",
|
|
5
5
|
"author": "Haus Tech",
|
|
6
6
|
"repository": {
|
|
@@ -24,8 +24,9 @@
|
|
|
24
24
|
"prepublishOnly": "yarn && yarn build"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"
|
|
28
|
-
"csv-
|
|
27
|
+
"@types/fs-extra": "^11.0.4",
|
|
28
|
+
"csv-parse": "^5.6.0",
|
|
29
|
+
"csv-stringify": "^6.5.2"
|
|
29
30
|
},
|
|
30
31
|
"peerDependencies": {
|
|
31
32
|
"csv-parse": "^5.5.6",
|