@haus-tech/product-import-export-plugin 3.0.10 → 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.
|
@@ -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",
|