@haus-tech/product-import-export-plugin 2.2.0-dev.0 → 2.2.0-dev.2

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,11 +1,11 @@
1
1
  /// <reference types="Multer" />
2
2
  import { LanguageCode, RequestContext } from '@vendure/core';
3
3
  import { Response } from 'express';
4
- import { PluginInitOptions } from '../types';
4
+ import { PluginInitOptions, UpdatingStrategy } from '../types';
5
5
  import { ProductImportService } from '../services/product-import.service';
6
6
  export declare class ProductImportController {
7
7
  private options;
8
8
  private service;
9
9
  constructor(options: PluginInitOptions, service: ProductImportService);
10
- updateProducts(ctx: RequestContext, file: Express.Multer.File, updateProductSlug: "true" | "false" | undefined, mainLanguage: LanguageCode, res: Response): Promise<Response<any, Record<string, any>>>;
10
+ updateProducts(ctx: RequestContext, file: Express.Multer.File, updateProductSlug: "true" | "false" | undefined, mainLanguage: LanguageCode, updatingStrategy: UpdatingStrategy, res: Response): Promise<Response<any, Record<string, any>>>;
11
11
  }
@@ -26,7 +26,7 @@ let ProductImportController = class ProductImportController {
26
26
  this.service = service;
27
27
  }
28
28
  async updateProducts(ctx, file, updateProductSlug = this.options.importOptions.defaultOptions
29
- ?.updateProductSlug || 'true', mainLanguage, res) {
29
+ ?.updateProductSlug || 'true', mainLanguage, updatingStrategy, res) {
30
30
  if (!file) {
31
31
  return res.status(400).send('No file uploaded');
32
32
  }
@@ -34,7 +34,7 @@ let ProductImportController = class ProductImportController {
34
34
  return res.status(400).send('No mainLanguage provided');
35
35
  }
36
36
  try {
37
- this.service.processFile(ctx, file, updateProductSlug === 'true', mainLanguage);
37
+ this.service.processFile(ctx, file, updateProductSlug === 'true', mainLanguage, updatingStrategy);
38
38
  return res.status(200).send('File uploaded successfully');
39
39
  }
40
40
  catch (error) {
@@ -49,9 +49,10 @@ __decorate([
49
49
  __param(1, (0, common_1.UploadedFile)()),
50
50
  __param(2, (0, common_1.Body)('updateProductSlug')),
51
51
  __param(3, (0, common_1.Body)('mainLanguage')),
52
- __param(4, (0, common_1.Res)()),
52
+ __param(4, (0, common_1.Body)('updatingStrategy')),
53
+ __param(5, (0, common_1.Res)()),
53
54
  __metadata("design:type", Function),
54
- __metadata("design:paramtypes", [core_1.RequestContext, Object, String, String, Object]),
55
+ __metadata("design:paramtypes", [core_1.RequestContext, Object, String, String, String, Object]),
55
56
  __metadata("design:returntype", Promise)
56
57
  ], ProductImportController.prototype, "updateProducts", null);
57
58
  ProductImportController = __decorate([
@@ -45,12 +45,16 @@ let ExtendedAssetImporter = class ExtendedAssetImporter {
45
45
  for (const assetPath of uniqueAssetPaths.values()) {
46
46
  const cachedAsset = this.assetMap.get(assetPath.url);
47
47
  if (cachedAsset) {
48
- if (assetPath.id && cachedAsset.name !== assetPath.name) {
48
+ if (cachedAsset.name !== assetPath.name && assetPath.name) {
49
49
  this.connection.getRepository(ctx, core_1.Asset).update(cachedAsset.id, {
50
50
  name: assetPath.name,
51
51
  });
52
+ this.assetMap.set(assetPath.url, { ...cachedAsset, name: assetPath.name });
53
+ assets.push({ ...cachedAsset, name: assetPath.name });
54
+ }
55
+ else {
56
+ assets.push(cachedAsset);
52
57
  }
53
- assets.push(cachedAsset);
54
58
  }
55
59
  else {
56
60
  try {
@@ -65,6 +69,12 @@ let ExtendedAssetImporter = class ExtendedAssetImporter {
65
69
  },
66
70
  });
67
71
  if (assetFromHash) {
72
+ // Check if the asset name has changed
73
+ if (assetPath.name && assetFromHash.name !== assetPath.name) {
74
+ await this.connection.getRepository(ctx, core_1.Asset).update(assetFromHash.id, {
75
+ name: assetPath.name,
76
+ });
77
+ }
68
78
  this.assetMap.set(assetPath.url, assetFromHash);
69
79
  assets.push(assetFromHash);
70
80
  continue;
@@ -89,6 +99,7 @@ let ExtendedAssetImporter = class ExtendedAssetImporter {
89
99
  shouldDeleteCreatedAsset = true;
90
100
  assetToUpdate = { ...assetFromHash, name: assetPath.name || assetFromHash.name };
91
101
  }
102
+ console.log('assetFromHash', assetFromHash);
92
103
  if (assetPath.id) {
93
104
  shouldDeleteCreatedAsset = true;
94
105
  assetToUpdate = {
@@ -3,7 +3,7 @@ import { ConfigService, ChannelService, FacetService, FacetValueService, TaxCate
3
3
  import { Stream } from 'stream';
4
4
  import { Observable } from 'rxjs';
5
5
  import { ExtendedFastImporterService } from '../../services/extended-fast-importer.service';
6
- import { ParsedProductWithId } from '../../types';
6
+ import { ParsedProductWithId, UpdatingStrategy } from '../../types';
7
7
  import { ImportParser } from './import-parser';
8
8
  import { ExtendedAssetImporter } from './asset-importer';
9
9
  export declare class ProductImporter {
@@ -26,7 +26,7 @@ export declare class ProductImporter {
26
26
  private customFieldTypes;
27
27
  private allAssets;
28
28
  constructor(connection: TransactionalConnection, configService: ConfigService, importParser: ImportParser, assetImporter: ExtendedAssetImporter, fastImporter: ExtendedFastImporterService, translatableSaver: TranslatableSaver, productService: ProductService, channelService: ChannelService, facetService: FacetService, facetValueService: FacetValueService, taxCategoryService: TaxCategoryService, assetService: AssetService, optionGroupService: ProductOptionGroupService);
29
- parseAndImport(input: string | Stream, ctx: RequestContext, updateProductSlug: boolean | undefined, mainLanguage: LanguageCode): Observable<ImportProgress>;
29
+ parseAndImport(input: string | Stream, ctx: RequestContext, updateProductSlug: boolean | undefined, mainLanguage: LanguageCode, updatingStrategy: UpdatingStrategy): Observable<ImportProgress>;
30
30
  private extractProductIdsFromCSV;
31
31
  private preprocessCsv;
32
32
  private streamToString;
@@ -34,7 +34,7 @@ export declare class ProductImporter {
34
34
  setMissingAssetHashes(ctx: RequestContext): Promise<void>;
35
35
  private doParseAndImport;
36
36
  private getRequestContext;
37
- importProducts(ctx: RequestContext, rows: ParsedProductWithId[], onProgress: OnProgressFn): Promise<string[]>;
37
+ importProducts(ctx: RequestContext, rows: ParsedProductWithId[], updatingStrategy: UpdatingStrategy, onProgress: OnProgressFn): Promise<string[]>;
38
38
  private getFacetValueIds;
39
39
  private processCustomFieldValues;
40
40
  /**
@@ -56,9 +56,9 @@ let ProductImporter = class ProductImporter {
56
56
  this.assetService = assetService;
57
57
  this.optionGroupService = optionGroupService;
58
58
  }
59
- parseAndImport(input, ctx, updateProductSlug = true, mainLanguage) {
59
+ parseAndImport(input, ctx, updateProductSlug = true, mainLanguage, updatingStrategy) {
60
60
  return new rxjs_1.Observable((subscriber) => {
61
- const p = this.doParseAndImport(input, updateProductSlug, mainLanguage, ctx, (progress) => {
61
+ const p = this.doParseAndImport(input, updateProductSlug, mainLanguage, updatingStrategy, ctx, (progress) => {
62
62
  subscriber.next(progress);
63
63
  }).then((value) => {
64
64
  subscriber.next({ ...value, currentProduct: 'Complete' });
@@ -169,7 +169,7 @@ let ProductImporter = class ProductImporter {
169
169
  }
170
170
  this.allAssets = assets;
171
171
  }
172
- async doParseAndImport(input, updateProductSlug, mainLanguage, reqCtx, onProgress) {
172
+ async doParseAndImport(input, updateProductSlug, mainLanguage, updatingStrategy, reqCtx, onProgress) {
173
173
  const ctx = await this.getRequestContext(reqCtx, mainLanguage);
174
174
  await this.setMissingAssetHashes(ctx);
175
175
  let csvContent;
@@ -194,7 +194,7 @@ let ProductImporter = class ProductImporter {
194
194
  });
195
195
  if (parsed && parsed.results.length) {
196
196
  try {
197
- const importErrors = await this.importProducts(ctx, parsed.results, (progess) => {
197
+ const importErrors = await this.importProducts(ctx, parsed.results, updatingStrategy, (progess) => {
198
198
  onProgress({
199
199
  ...progess,
200
200
  processed: parsed.processed,
@@ -236,7 +236,7 @@ let ProductImporter = class ProductImporter {
236
236
  throw new core_1.InternalServerError('No RequestContext provided');
237
237
  }
238
238
  }
239
- async importProducts(ctx, rows, onProgress) {
239
+ async importProducts(ctx, rows, updatingStrategy, onProgress) {
240
240
  let errors = [];
241
241
  let imported = 0;
242
242
  const languageCode = ctx.languageCode;
@@ -294,6 +294,7 @@ let ProductImporter = class ProductImporter {
294
294
  'variants',
295
295
  'variants.facetValues',
296
296
  'assets',
297
+ 'featuredAsset',
297
298
  ],
298
299
  })
299
300
  : undefined;
@@ -322,10 +323,17 @@ let ProductImporter = class ProductImporter {
322
323
  previousNormalizedName: (0, normalize_string_1.normalizeString)(existingProductName),
323
324
  }
324
325
  : undefined;
326
+ if (existingProduct && updatingStrategy === 'replace') {
327
+ await this.fastImporter.removeAllAssetsFromProduct(existingProduct);
328
+ }
325
329
  const productData = {
326
- featuredAssetId: productAssets.length ? productAssets[0].id : undefined,
330
+ featuredAssetId: existingProduct?.featuredAsset
331
+ ? undefined
332
+ : productAssets.length
333
+ ? productAssets[0].id
334
+ : undefined,
327
335
  assetIds: productAssets.map((a) => a.id),
328
- // facetValueIds: await this.getFacetValueIds(ctx, product.facets, ctx.languageCode),
336
+ facetValueIds: updatingStrategy === 'replace' ? [] : undefined,
329
337
  translations: await Promise.all(product.translations.map(async (translation) => {
330
338
  return {
331
339
  languageCode: translation.languageCode,
@@ -430,6 +438,7 @@ let ProductImporter = class ProductImporter {
430
438
  sku: variant.sku,
431
439
  },
432
440
  withDeleted: true,
441
+ relations: ['facetValues', 'facetValues.facet', 'assets', 'featuredAsset'],
433
442
  });
434
443
  if (existingVariant?.deletedAt) {
435
444
  existingVariant.deletedAt = null;
@@ -441,10 +450,18 @@ let ProductImporter = class ProductImporter {
441
450
  console.error('Error while restoring the variant:', error);
442
451
  }
443
452
  }
453
+ if (existingVariant && updatingStrategy === 'replace') {
454
+ // Remove all assets from the product
455
+ await this.fastImporter.removeAllAssetsFromVariant(existingVariant);
456
+ }
444
457
  const variantData = {
445
458
  productId: createdProductId,
446
- // facetValueIds,
447
- featuredAssetId: variantAssets.length ? variantAssets[0].id : undefined,
459
+ facetValueIds: updatingStrategy === 'replace' ? [] : undefined,
460
+ featuredAssetId: existingVariant?.featuredAsset
461
+ ? undefined
462
+ : variantAssets.length
463
+ ? variantAssets[0].id
464
+ : undefined,
448
465
  assetIds: variantAssets.map((a) => a.id),
449
466
  sku: variant.sku,
450
467
  taxCategoryId: this.getMatchingTaxCategoryId(variant.taxCategory, taxCategories.items),
@@ -1,4 +1,4 @@
1
- import { Channel, ChannelService, ID, RequestContextService, StockMovementService, TransactionalConnection, TranslatableSaver } from '@vendure/core';
1
+ import { Channel, ChannelService, EntityHydrator, ID, Product, ProductVariant, RequestContextService, StockMovementService, TransactionalConnection, TranslatableSaver } from '@vendure/core';
2
2
  import { CreateFacetInput, CreateFacetValueInput, CreateProductInput, CreateProductOptionGroupInput, CreateProductOptionInput, CreateProductVariantInput } from '@vendure/common/lib/generated-types';
3
3
  export declare class ExtendedFastImporterService {
4
4
  private connection;
@@ -6,10 +6,11 @@ export declare class ExtendedFastImporterService {
6
6
  private stockMovementService;
7
7
  private translatableSaver;
8
8
  private requestContextService;
9
+ private entityHydrator;
9
10
  private defaultChannel;
10
11
  private importCtx;
11
12
  /** @internal */
12
- constructor(connection: TransactionalConnection, channelService: ChannelService, stockMovementService: StockMovementService, translatableSaver: TranslatableSaver, requestContextService: RequestContextService);
13
+ constructor(connection: TransactionalConnection, channelService: ChannelService, stockMovementService: StockMovementService, translatableSaver: TranslatableSaver, requestContextService: RequestContextService, entityHydrator: EntityHydrator);
13
14
  /**
14
15
  * @description
15
16
  * This should be called prior to any of the import methods, as it establishes the
@@ -22,6 +23,8 @@ export declare class ExtendedFastImporterService {
22
23
  updateProduct(id: ID, input: CreateProductInput & {
23
24
  id: ID;
24
25
  }): Promise<ID>;
26
+ removeAllAssetsFromProduct(product: Product): Promise<void>;
27
+ removeAllAssetsFromVariant(variant: ProductVariant): Promise<void>;
25
28
  createProduct(input: CreateProductInput): Promise<ID>;
26
29
  updateProductOptionGroup(input: CreateProductOptionGroupInput & {
27
30
  id: ID;
@@ -21,15 +21,17 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
21
21
  stockMovementService;
22
22
  translatableSaver;
23
23
  requestContextService;
24
+ entityHydrator;
24
25
  defaultChannel;
25
26
  importCtx;
26
27
  /** @internal */
27
- constructor(connection, channelService, stockMovementService, translatableSaver, requestContextService) {
28
+ constructor(connection, channelService, stockMovementService, translatableSaver, requestContextService, entityHydrator) {
28
29
  this.connection = connection;
29
30
  this.channelService = channelService;
30
31
  this.stockMovementService = stockMovementService;
31
32
  this.translatableSaver = translatableSaver;
32
33
  this.requestContextService = requestContextService;
34
+ this.entityHydrator = entityHydrator;
33
35
  }
34
36
  /**
35
37
  * @description
@@ -59,7 +61,11 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
59
61
  entityType: core_1.Product,
60
62
  translationType: core_1.ProductTranslation,
61
63
  beforeSave: async (p) => {
62
- p.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
64
+ await this.entityHydrator.hydrate(this.importCtx, p, { relations: ['channels'] });
65
+ p.channels = (0, unique_1.unique)([...p.channels, this.defaultChannel, this.importCtx.channel], 'id');
66
+ if (input.facetValueIds) {
67
+ p.facetValues = input.facetValueIds.map((id) => ({ id }));
68
+ }
63
69
  if (input.featuredAssetId) {
64
70
  p.featuredAsset = { id: input.featuredAssetId };
65
71
  }
@@ -80,6 +86,20 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
80
86
  }
81
87
  return product.id;
82
88
  }
89
+ async removeAllAssetsFromProduct(product) {
90
+ this.ensureInitialized();
91
+ await this.connection
92
+ .getRepository(this.importCtx, core_1.ProductAsset)
93
+ .delete({ productId: product.id });
94
+ product.featuredAsset = undefined;
95
+ }
96
+ async removeAllAssetsFromVariant(variant) {
97
+ this.ensureInitialized();
98
+ await this.connection
99
+ .getRepository(this.importCtx, core_1.ProductVariantAsset)
100
+ .delete({ productVariantId: variant.id });
101
+ variant.featuredAsset = undefined;
102
+ }
83
103
  async createProduct(input) {
84
104
  this.ensureInitialized();
85
105
  // https://github.com/vendure-ecommerce/vendure/issues/2053
@@ -197,8 +217,9 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
197
217
  input: { ...input, id: facetExists.id },
198
218
  entityType: core_1.Facet,
199
219
  translationType: core_1.FacetTranslation,
200
- beforeSave: (facet) => {
201
- facet.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
220
+ beforeSave: async (facet) => {
221
+ await this.entityHydrator.hydrate(this.importCtx, facet, { relations: ['channels'] });
222
+ facet.channels = (0, unique_1.unique)([...facet.channels, this.defaultChannel, this.importCtx.channel], 'id');
202
223
  },
203
224
  });
204
225
  return facet.id;
@@ -208,7 +229,7 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
208
229
  input,
209
230
  entityType: core_1.Facet,
210
231
  translationType: core_1.FacetTranslation,
211
- beforeSave: (facet) => {
232
+ beforeSave: async (facet) => {
212
233
  facet.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
213
234
  },
214
235
  });
@@ -228,9 +249,10 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
228
249
  input: { ...input, id: facetValueExists.id },
229
250
  entityType: core_1.FacetValue,
230
251
  translationType: core_1.FacetValueTranslation,
231
- beforeSave: (facetValue) => {
252
+ beforeSave: async (facetValue) => {
253
+ await this.entityHydrator.hydrate(this.importCtx, facetValue, { relations: ['channels'] });
232
254
  facetValue.facet = { id: input.facetId };
233
- facetValue.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
255
+ facetValue.channels = (0, unique_1.unique)([...facetValue.channels, this.defaultChannel, this.importCtx.channel], 'id');
234
256
  },
235
257
  });
236
258
  return updatedFacetValue.id;
@@ -240,7 +262,7 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
240
262
  input,
241
263
  entityType: core_1.FacetValue,
242
264
  translationType: core_1.FacetValueTranslation,
243
- beforeSave: (facetValue) => {
265
+ beforeSave: async (facetValue) => {
244
266
  facetValue.facet = { id: input.facetId };
245
267
  facetValue.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
246
268
  },
@@ -299,11 +321,15 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
299
321
  entityType: core_1.ProductVariant,
300
322
  translationType: core_1.ProductVariantTranslation,
301
323
  beforeSave: async (variant) => {
302
- variant.channels = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id');
324
+ await this.entityHydrator.hydrate(this.importCtx, variant, { relations: ['channels'] });
325
+ variant.channels = (0, unique_1.unique)([...variant.channels, this.defaultChannel, this.importCtx.channel], 'id');
303
326
  const { optionIds } = input;
304
327
  if (optionIds && optionIds.length) {
305
328
  variant.options = optionIds.map((id) => ({ id }));
306
329
  }
330
+ if (input.facetValueIds) {
331
+ variant.facetValues = input.facetValueIds.map((id) => ({ id }));
332
+ }
307
333
  variant.product = { id: input.productId };
308
334
  variant.taxCategory = { id: input.taxCategoryId };
309
335
  if (input.featuredAssetId) {
@@ -331,15 +357,32 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
331
357
  await this.stockMovementService.adjustProductVariantStock(this.importCtx, updatedVariant.id, input.stockOnHand ?? 0);
332
358
  const assignedChannelIds = (0, unique_1.unique)([this.defaultChannel, this.importCtx.channel], 'id').map((c) => c.id);
333
359
  for (const channelId of assignedChannelIds) {
334
- const variantPrice = new core_1.ProductVariantPrice({
335
- price: input.price,
336
- channelId,
337
- currencyCode: this.defaultChannel.defaultCurrencyCode,
338
- });
339
- variantPrice.variant = updatedVariant;
340
- await this.connection
360
+ const allPrices = await this.connection
341
361
  .getRepository(this.importCtx, core_1.ProductVariantPrice)
342
- .update({ id: updatedVariant.id }, variantPrice);
362
+ .find({
363
+ where: {
364
+ variant: { id: updatedVariant.id },
365
+ },
366
+ });
367
+ let existingVariantPrice = allPrices.find((p) => (0, core_1.idsAreEqual)(p.channelId, channelId) &&
368
+ p.currencyCode === this.importCtx.channel.defaultCurrencyCode);
369
+ if (!existingVariantPrice) {
370
+ const variantPrice = new core_1.ProductVariantPrice({
371
+ price: input.price,
372
+ channelId,
373
+ currencyCode: this.importCtx.channel.defaultCurrencyCode,
374
+ });
375
+ variantPrice.variant = updatedVariant;
376
+ await this.connection.rawConnection
377
+ .getRepository(core_1.ProductVariantPrice)
378
+ .save(variantPrice, { reload: false });
379
+ }
380
+ else {
381
+ existingVariantPrice.price = input.price;
382
+ await this.connection.rawConnection
383
+ .getRepository(core_1.ProductVariantPrice)
384
+ .save(existingVariantPrice, { reload: false });
385
+ }
343
386
  }
344
387
  return updatedVariant.id;
345
388
  }
@@ -392,11 +435,11 @@ let ExtendedFastImporterService = class ExtendedFastImporterService {
392
435
  const variantPrice = new core_1.ProductVariantPrice({
393
436
  price: input.price,
394
437
  channelId,
395
- currencyCode: this.defaultChannel.defaultCurrencyCode,
438
+ currencyCode: this.importCtx.channel.defaultCurrencyCode,
396
439
  });
397
440
  variantPrice.variant = createdVariant;
398
- await this.connection
399
- .getRepository(this.importCtx, core_1.ProductVariantPrice)
441
+ await this.connection.rawConnection
442
+ .getRepository(core_1.ProductVariantPrice)
400
443
  .save(variantPrice, { reload: false });
401
444
  }
402
445
  return createdVariant.id;
@@ -413,6 +456,7 @@ ExtendedFastImporterService = __decorate([
413
456
  core_1.ChannelService,
414
457
  core_1.StockMovementService,
415
458
  core_1.TranslatableSaver,
416
- core_1.RequestContextService])
459
+ core_1.RequestContextService,
460
+ core_1.EntityHydrator])
417
461
  ], ExtendedFastImporterService);
418
462
  exports.ExtendedFastImporterService = ExtendedFastImporterService;
@@ -1,7 +1,7 @@
1
1
  /// <reference types="Multer" />
2
2
  import { OnModuleInit } from '@nestjs/common';
3
3
  import { RequestContext, JobQueueService, SerializedRequestContext, SearchService, LanguageCode } from '@vendure/core';
4
- import { PluginInitOptions } from '../types';
4
+ import { PluginInitOptions, UpdatingStrategy } from '../types';
5
5
  import { ProductImporter } from '../providers/import-providers/product-importer';
6
6
  export declare class ProductImportService implements OnModuleInit {
7
7
  private options;
@@ -11,11 +11,12 @@ export declare class ProductImportService implements OnModuleInit {
11
11
  private productImportQueue;
12
12
  constructor(options: PluginInitOptions, jobQueueService: JobQueueService, productImporter: ProductImporter, searchService: SearchService);
13
13
  onModuleInit(): Promise<void>;
14
- processFile(ctx: RequestContext, file: Express.Multer.File, updateProductSlug: boolean, mainLanguage: LanguageCode): Promise<import("@vendure/core/dist/job-queue/subscribable-job").SubscribableJob<{
14
+ processFile(ctx: RequestContext, file: Express.Multer.File, updateProductSlug: boolean, mainLanguage: LanguageCode, updatingStrategy: UpdatingStrategy): Promise<import("@vendure/core/dist/job-queue/subscribable-job").SubscribableJob<{
15
15
  ctx: SerializedRequestContext;
16
16
  fileContent: string;
17
17
  updateProductSlug: boolean;
18
18
  mainLanguage: LanguageCode;
19
+ updatingStrategy: UpdatingStrategy;
19
20
  }>>;
20
21
  private triggerProductImport;
21
22
  }
@@ -39,7 +39,7 @@ let ProductImportService = class ProductImportService {
39
39
  let jobResult;
40
40
  return new Promise((resolve, reject) => {
41
41
  this.productImporter
42
- .parseAndImport(job.data.fileContent, ctx, job.data.updateProductSlug, job.data.mainLanguage)
42
+ .parseAndImport(job.data.fileContent, ctx, job.data.updateProductSlug, job.data.mainLanguage, job.data.updatingStrategy)
43
43
  .subscribe({
44
44
  next: (result) => {
45
45
  const processedCount = result.processed;
@@ -60,16 +60,17 @@ let ProductImportService = class ProductImportService {
60
60
  },
61
61
  });
62
62
  }
63
- async processFile(ctx, file, updateProductSlug, mainLanguage) {
63
+ async processFile(ctx, file, updateProductSlug, mainLanguage, updatingStrategy) {
64
64
  const fileContent = file.buffer.toString('utf-8');
65
- return this.triggerProductImport(ctx, fileContent, updateProductSlug, mainLanguage);
65
+ return this.triggerProductImport(ctx, fileContent, updateProductSlug, mainLanguage, updatingStrategy);
66
66
  }
67
- triggerProductImport(ctx, fileContent, updateProductSlug, mainLanguage) {
67
+ triggerProductImport(ctx, fileContent, updateProductSlug, mainLanguage, updatingStrategy) {
68
68
  return this.productImportQueue.add({
69
69
  ctx: ctx.serialize(),
70
70
  fileContent,
71
71
  updateProductSlug,
72
72
  mainLanguage,
73
+ updatingStrategy,
73
74
  });
74
75
  }
75
76
  };
package/dist/types.d.ts CHANGED
@@ -37,4 +37,5 @@ export type JsonAsset = {
37
37
  type ExportFields = Array<ProductFields | VariantFields>;
38
38
  type ProductFields = 'productId' | 'name' | 'slug' | 'description' | 'assets' | 'facets' | 'optionGroups';
39
39
  type VariantFields = 'sku' | 'optionValues' | 'price' | 'taxCategory' | 'stockOnHand' | 'trackInventory' | 'variantAssets' | 'variantFacets';
40
+ export type UpdatingStrategy = 'replace' | 'merge';
40
41
  export {};
@@ -13,7 +13,7 @@
13
13
 
14
14
  <div class="input-row" style="margin-top: 16px">
15
15
  <label style="margin-bottom: 8px; display: inline-block">{{
16
- 'product-export.dialog.customFieldsLabel' | translate
16
+ 'product-export.dialog.availableExportFieldsLabel' | translate
17
17
  }}</label>
18
18
  <div *ngFor="let field of availableExportFields">
19
19
  <input
@@ -3,6 +3,7 @@ import { NotificationService, LocalStorageService } from '@vendure/admin-ui/core
3
3
  import { TranslateService } from '@ngx-translate/core';
4
4
  import { AdminUiConfig } from '@vendure/common/lib/shared-types';
5
5
  import { Channel, LanguageCode } from '@vendure/core';
6
+ type UpdatingStrategy = 'replace' | 'merge';
6
7
  type ImportOptions = {
7
8
  updateProductSlug?: string;
8
9
  };
@@ -26,6 +27,7 @@ export declare class ProductImportComponent implements OnInit {
26
27
  updateProductSlug: boolean;
27
28
  selectedMainLanguage: LanguageCode | undefined;
28
29
  availableLanguages: LanguageCode[];
30
+ updatingStrategy: UpdatingStrategy;
29
31
  fileUploadElement: ElementRef<HTMLInputElement>;
30
32
  config: PluginInitOptions;
31
33
  appConfig: AdminUiConfig;
@@ -25,7 +25,11 @@
25
25
  </div>
26
26
  </vdr-form-field>
27
27
  </div>
28
- <div class="form-grid" *ngIf="selectedFile">
28
+ <div
29
+ class="form-grid"
30
+ *ngIf="selectedFile"
31
+ style="margin-top: 24px; border-top: 1px solid #cdcdcd"
32
+ >
29
33
  <div
30
34
  class="input-row"
31
35
  style="margin-top: 16px"
@@ -44,7 +48,7 @@
44
48
  </div>
45
49
 
46
50
  <div class="input-row" style="margin-top: 16px">
47
- <div>Välj huvudspråk för filen:</div>
51
+ <div>{{ 'product-import.card.select-main-language' | translate }}</div>
48
52
  <div *ngFor="let lang of availableLanguages">
49
53
  <label>
50
54
  <input
@@ -58,6 +62,49 @@
58
62
  {{ lang }}
59
63
  </label>
60
64
  </div>
65
+ <div
66
+ style="
67
+ margin-top: 8px;
68
+ padding-top: 8px;
69
+ font-style: italic;
70
+ font-size: 12px;
71
+ line-height: 1.4;
72
+ "
73
+ >
74
+ <clr-icon shape="info-circle" size="16" class="is-info"></clr-icon>
75
+ {{ 'product-import.card.main-language-info' | translate }}
76
+ </div>
77
+ </div>
78
+
79
+ <div class="input-row" style="margin-top: 16px">
80
+ <div>{{ 'product-import.card.updating-strategy' | translate }}</div>
81
+ <div>
82
+ <label>
83
+ <input
84
+ type="radio"
85
+ name="updatingStrategy"
86
+ value="merge"
87
+ [(ngModel)]="updatingStrategy"
88
+ style="width: fit-content"
89
+ />
90
+ {{ 'product-import.card.merge' | translate }}
91
+ </label>
92
+ </div>
93
+ <div>
94
+ <label>
95
+ <input
96
+ type="radio"
97
+ name="updatingStrategy"
98
+ value="replace"
99
+ [(ngModel)]="updatingStrategy"
100
+ style="width: fit-content"
101
+ />
102
+ {{ 'product-import.card.overwrite' | translate }}<br />
103
+ <span *ngIf="updatingStrategy === 'replace'" style="color: red; font-size: 12px">{{
104
+ 'product-import.card.overwrite-warning' | translate
105
+ }}</span>
106
+ </label>
107
+ </div>
61
108
  </div>
62
109
  </div>
63
110
  </vdr-card>
@@ -26,6 +26,7 @@ let ProductImportComponent = class ProductImportComponent {
26
26
  updateProductSlug = true;
27
27
  selectedMainLanguage;
28
28
  availableLanguages = [];
29
+ updatingStrategy = 'merge';
29
30
  fileUploadElement;
30
31
  config;
31
32
  appConfig;
@@ -90,6 +91,7 @@ let ProductImportComponent = class ProductImportComponent {
90
91
  this.selectedFile = null;
91
92
  this.validateFile = null;
92
93
  this.selectedMainLanguage = undefined;
94
+ this.updatingStrategy = 'merge';
93
95
  }
94
96
  async validateCsvStructure(file) {
95
97
  const baseColumns = ['sku'];
@@ -121,6 +123,15 @@ let ProductImportComponent = class ProductImportComponent {
121
123
  lines.splice(index, 1);
122
124
  }
123
125
  });
126
+ // Check if number of columns for each line is consistent and the same as headers
127
+ const headerLength = this.splitLines(lines[0]).length;
128
+ const inconsistentLines = lines.filter((line) => this.splitLines(line).length !== headerLength);
129
+ if (inconsistentLines.length > 0) {
130
+ console.log('Inconsistent number of columns in the CSV file.');
131
+ this.notificationService.error(this.translate.instant('product-import.notifications.inconsistent-columns'));
132
+ resolve({ isValid: false });
133
+ return;
134
+ }
124
135
  if (lines.length > 0) {
125
136
  const header = this.splitLines(lines[0]).map((col) => col.trim());
126
137
  console.log('CSV Header:', header);
@@ -282,11 +293,12 @@ let ProductImportComponent = class ProductImportComponent {
282
293
  });
283
294
  }
284
295
  async uploadFile() {
285
- if (this.selectedFile && this.selectedMainLanguage) {
296
+ if (this.selectedFile && this.selectedMainLanguage && this.updatingStrategy) {
286
297
  const formData = new FormData();
287
298
  formData.append('file', this.selectedFile);
288
299
  formData.append('updateProductSlug', this.updateProductSlug.toString());
289
300
  formData.append('mainLanguage', this.selectedMainLanguage);
301
+ formData.append('updatingStrategy', this.updatingStrategy);
290
302
  try {
291
303
  const res = await fetch(`${this.serverPath}/product-import/upload`, {
292
304
  method: 'POST',
@@ -7,6 +7,8 @@ import { endsWith, startsWith, size, uniq } from 'lodash'
7
7
  import { AdminUiConfig } from '@vendure/common/lib/shared-types'
8
8
  import { Channel, LanguageCode } from '@vendure/core'
9
9
 
10
+ type UpdatingStrategy = 'replace' | 'merge'
11
+
10
12
  type ImportOptions = {
11
13
  updateProductSlug?: string
12
14
  }
@@ -34,6 +36,7 @@ export class ProductImportComponent implements OnInit {
34
36
  updateProductSlug = true
35
37
  selectedMainLanguage: LanguageCode | undefined
36
38
  availableLanguages: LanguageCode[] = []
39
+ updatingStrategy: UpdatingStrategy = 'merge'
37
40
 
38
41
  @ViewChild('fileUpload', { static: false }) fileUploadElement: ElementRef<HTMLInputElement>
39
42
 
@@ -114,6 +117,7 @@ export class ProductImportComponent implements OnInit {
114
117
  this.selectedFile = null
115
118
  this.validateFile = null
116
119
  this.selectedMainLanguage = undefined
120
+ this.updatingStrategy = 'merge'
117
121
  }
118
122
 
119
123
  async validateCsvStructure(file: File): Promise<ValidateReturnType> {
@@ -151,6 +155,22 @@ export class ProductImportComponent implements OnInit {
151
155
  lines.splice(index, 1)
152
156
  }
153
157
  })
158
+
159
+ // Check if number of columns for each line is consistent and the same as headers
160
+ const headerLength = this.splitLines(lines[0]).length
161
+ const inconsistentLines = lines.filter(
162
+ (line) => this.splitLines(line).length !== headerLength,
163
+ )
164
+
165
+ if (inconsistentLines.length > 0) {
166
+ console.log('Inconsistent number of columns in the CSV file.')
167
+ this.notificationService.error(
168
+ this.translate.instant('product-import.notifications.inconsistent-columns'),
169
+ )
170
+ resolve({ isValid: false })
171
+ return
172
+ }
173
+
154
174
  if (lines.length > 0) {
155
175
  const header = this.splitLines(lines[0]).map((col) => col.trim())
156
176
  console.log('CSV Header:', header)
@@ -398,11 +418,12 @@ export class ProductImportComponent implements OnInit {
398
418
  }
399
419
 
400
420
  async uploadFile(): Promise<void> {
401
- if (this.selectedFile && this.selectedMainLanguage) {
421
+ if (this.selectedFile && this.selectedMainLanguage && this.updatingStrategy) {
402
422
  const formData = new FormData()
403
423
  formData.append('file', this.selectedFile)
404
424
  formData.append('updateProductSlug', this.updateProductSlug.toString())
405
425
  formData.append('mainLanguage', this.selectedMainLanguage)
426
+ formData.append('updatingStrategy', this.updatingStrategy)
406
427
 
407
428
  try {
408
429
  const res = await fetch(`${this.serverPath}/product-import/upload`, {
@@ -6,7 +6,13 @@
6
6
  "description": "Choose a CSV file to import products from",
7
7
  "button-select": "Select file",
8
8
  "button-upload": "Upload file",
9
- "update-product-slugs": "Update product slugs based on name"
9
+ "update-product-slugs": "Update product slugs based on name",
10
+ "select-main-language": "Select main language: ",
11
+ "updating-strategy": "Updating strategy for facets and assets: ",
12
+ "overwrite": "Overwrite",
13
+ "overwrite-warning": "Warning: This will remove existing facets and assets from products and variants.",
14
+ "merge": "Merge",
15
+ "main-language-info": "The main language is used as a fallback when translations are missing. It is recommended to select the language that is used the most in your system. The main language will be used to create products, the other languages will be used to fill in translations."
10
16
  },
11
17
  "notifications": {
12
18
  "invalid-file-type": "Invalid file type. Please upload a CSV file.",
@@ -23,7 +29,8 @@
23
29
  "missing-languages": "Your CSV file contains languages that are not available in the system. Please add the languages to the system or remove the languages that are not used and try again.",
24
30
  "mismatched-option-values": "Some product variants are missing product options. Please fill in all options: ",
25
31
  "missing-main-language-name": "Main language is missing for name. Please add the main language and try again.",
26
- "missing-translation-values": "Some fields are missing translations for the main language that exist in other languages. These will not be imported: "
32
+ "missing-translation-values": "Some fields are missing translations for the main language that exist in other languages. These will not be imported: ",
33
+ "inconsistent-columns": "The columns in the file are inconsistent. Please ensure all rows have the same number of columns."
27
34
  }
28
35
  },
29
36
  "product-export": {
@@ -38,7 +45,8 @@
38
45
  "exportAssetsAsOptions": {
39
46
  "url": "URL",
40
47
  "json": "JSON"
41
- }
48
+ },
49
+ "availableExportFieldsLabel": "Fields to export"
42
50
  }
43
51
  }
44
52
  }
@@ -6,7 +6,13 @@
6
6
  "description": "Välj en CSV-fil att importera produkter från",
7
7
  "button-select": "Välj fil",
8
8
  "button-upload": "Ladda upp fil",
9
- "update-product-slugs": "Uppdatera produkt-sluggar utifrån namn"
9
+ "update-product-slugs": "Uppdatera produkt-sluggar utifrån namn",
10
+ "select-main-language": "Välj huvudspråk: ",
11
+ "updating-strategy": "Uppdateringsstrategi för etiketter och filer: ",
12
+ "overwrite": "Skriv över",
13
+ "overwrite-warning": "Varning: Detta kommer ta bort befintliga etiketter och filer från produkter och varianter.",
14
+ "merge": "Sammanfoga",
15
+ "main-language-info": "Huvudspråket används som fallback när översättningar saknas. Det rekommenderas att välja det språk som används mest i din kanal. Huvudspråket kommer användas för att skapa upp produkter, de övriga språken kommer användas för att fylla i översättningar."
10
16
  },
11
17
  "notifications": {
12
18
  "invalid-file-type": "Felaktig filtyp. Vänligen ladda upp en CSV-fil.",
@@ -23,7 +29,8 @@
23
29
  "missing-languages": "Din CSV-fil innehåller språk som inte är tillgängliga i den här kanalen. Vänligen lägg till språken i kanalen eller ta bort de språk som inte används och försök igen.",
24
30
  "mismatched-option-values": "Antalet produktalternativ för vissa produktvarianter är felaktigt: ",
25
31
  "missing-main-language-name": "Huvudspråket saknas för namn. Vänligen lägg till huvudspråket och försök igen.",
26
- "missing-translation-values": "Det saknas översättningar för vissa fält för huvudspråket som existerar i andra språk. Dessa kommer inte importeras: "
32
+ "missing-translation-values": "Det saknas översättningar för vissa fält för huvudspråket som existerar i andra språk. Dessa kommer inte importeras: ",
33
+ "inconsistent-columns": "Kolumnerna i filen är inkonsekventa. Vänligen kontrollera att alla rader har samma antal kolumner."
27
34
  }
28
35
  },
29
36
  "product-export": {
@@ -38,7 +45,8 @@
38
45
  "exportAssetsAsOptions": {
39
46
  "url": "URL",
40
47
  "json": "JSON"
41
- }
48
+ },
49
+ "availableExportFieldsLabel": "Fält att exportera"
42
50
  }
43
51
  }
44
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@haus-tech/product-import-export-plugin",
3
- "version": "2.2.0-dev.0",
3
+ "version": "2.2.0-dev.2",
4
4
  "description": "A Vendure plugin for importing and exporting products using a CSV file",
5
5
  "author": "Haus Tech",
6
6
  "repository": {