@haus-tech/product-import-export-plugin 3.0.10 → 3.0.11-rc.1

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 { EntityHydrator, ID, ProductService, RequestContext, StockLevelService, ChannelService, ConfigService } from '@vendure/core';
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, entityHydratorService: EntityHydrator, channelService: ChannelService, configService: ConfigService);
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, entityHydratorService, channelService, configService) {
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
- for (const product of products) {
110
- const records = [];
111
- const hydratedProduct = await this.entityHydratorService.hydrate(ctx, product, {
112
- relations: [
113
- 'variants',
114
- 'facetValues',
115
- 'facetValues.facet',
116
- 'optionGroups',
117
- 'assets',
118
- 'variants.assets',
119
- 'variants.facetValues',
120
- 'variants.facetValues.facet',
121
- 'variants.options',
122
- ],
123
- });
124
- const { assets = [], facetValues = [], optionGroups = [], variants = [], translations = [], customFields = {}, } = hydratedProduct;
125
- const nameTranslations = this.mapTranslations(translations, 'name', languages);
126
- const slugTranslations = this.mapTranslations(translations, 'slug', languages);
127
- const descriptionTranslations = this.mapTranslations(translations, 'description', languages);
128
- // Filter out all variants that are soft deleted
129
- const activeVariants = variants.filter((v) => !v.deletedAt);
130
- const productAssets = assets.length === 0
131
- ? ''
132
- : assets.length > 0
133
- ? this.handleAssets(assets.map(({ asset }) => asset), exportAssetsAs)
134
- : this.handleAssets([hydratedProduct.featuredAsset], exportAssetsAs);
135
- const productFacets = languages.reduce((acc, lang) => {
136
- acc[lang] = facetValues
137
- .map((facetValue) => this.mapFacetTranslations(facetValue, lang))
138
- .filter((facet) => facet)
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 optionGroupNames = languages.reduce((acc, lang) => {
143
- acc[lang] = (0, lodash_1.sortBy)(optionGroups, (g) => g.id)
144
- .map((group) => this.mapTranslations(group.translations, 'name', [lang])[lang])
145
- .filter((name) => name)
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
- for (const variant of activeVariants) {
150
- const variantValues = languages.reduce((acc, lang) => {
151
- acc[lang] = ((0, lodash_1.sortBy)(variant.options, (o) => o.groupId) || [])
152
- .map((option) => this.mapTranslations(option.translations, 'name', [lang])[lang])
153
- .filter((name) => name)
154
- .join('|');
155
- return acc;
156
- }, {});
157
- const variantAssets = this.handleAssets(variant.assets.map(({ asset }) => asset), exportAssetsAs);
158
- const variantFacets = languages.reduce((acc, lang) => {
159
- acc[lang] = (variant.facetValues || [])
160
- .map((facet) => this.mapFacetTranslations(facet, lang))
161
- .filter((facet) => facet)
162
- .join('|');
163
- return acc;
164
- }, {});
165
- const stockOnHand = await this.getStockOnHand(ctx, variant.id); // Adjusted to fetch stock details
166
- const variantTranslations = variant.translations;
167
- const variantNameTranslations = this.mapTranslations(variantTranslations, 'name', languages);
168
- const record = {};
169
- const convertToHTML = (text) => {
170
- return text
171
- .replace(/\n/g, '<br>')
172
- .replace(/\t/g, '&nbsp;&nbsp;&nbsp;&nbsp;')
173
- .replace(/,\s*'/g, ", '");
174
- };
175
- for (const lang of languages) {
176
- const escapedDescription = convertToHTML(descriptionTranslations[lang]);
177
- const escapedName = nameTranslations[lang]
178
- .replace(/"/g, "'")
179
- .replace(/\s+/g, ' ')
180
- .replace(/,\s*'/g, ", '")
181
- .replace(/'/g, "''");
182
- record.productId = records.length === 0 ? product.id : '';
183
- record[`name:${lang}`] = records.length === 0 ? escapedName || '' : '';
184
- record[`slug:${lang}`] = records.length === 0 ? slugTranslations[lang] || '' : '';
185
- record[`description:${lang}`] = records.length === 0 ? escapedDescription || '' : '';
186
- record[`facets:${lang}`] = records.length === 0 ? productFacets[lang] : '';
187
- record[`optionGroups:${lang}`] = records.length === 0 ? optionGroupNames[lang] : '';
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, '&nbsp;&nbsp;&nbsp;&nbsp;')
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
- const productVariantPrices = variant.productVariantPrices.filter((price) => price.channelId === ctx.channelId);
192
- record.assets = records.length === 0 ? productAssets : '';
193
- record.sku = variant.sku;
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
- await csvWriter.writeRecords(records);
226
+ records.push(record);
213
227
  }
214
- return exportFile;
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.10",
3
+ "version": "3.0.11-rc.1",
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,10 @@
24
24
  "prepublishOnly": "yarn && yarn build"
25
25
  },
26
26
  "devDependencies": {
27
- "csv-parse": "^5.5.6",
28
- "csv-stringify": "^6.5.1"
27
+ "@types/fs-extra": "^11.0.4",
28
+ "bottleneck": "^2.19.5",
29
+ "csv-parse": "^5.6.0",
30
+ "csv-stringify": "^6.5.2"
29
31
  },
30
32
  "peerDependencies": {
31
33
  "csv-parse": "^5.5.6",