@haus-tech/product-import-export-plugin 3.6.12 → 3.6.14

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.
Files changed (56) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +110 -10
  3. package/package.json +1 -1
  4. package/src/constants.d.ts +1 -0
  5. package/src/constants.js +2 -1
  6. package/src/constants.js.map +1 -1
  7. package/src/dashboard/export-configuration-panel.tsx +1 -0
  8. package/src/dashboard/utils.ts +1 -0
  9. package/src/helpers/find-text-column.d.ts +8 -0
  10. package/src/helpers/find-text-column.js +32 -0
  11. package/src/helpers/find-text-column.js.map +1 -0
  12. package/src/index.d.ts +1 -0
  13. package/src/index.js +1 -0
  14. package/src/index.js.map +1 -1
  15. package/src/product-import-export.plugin.d.ts +11 -1
  16. package/src/product-import-export.plugin.js +35 -3
  17. package/src/product-import-export.plugin.js.map +1 -1
  18. package/src/providers/import-providers/import-parser.d.ts +9 -0
  19. package/src/providers/import-providers/import-parser.js +26 -8
  20. package/src/providers/import-providers/import-parser.js.map +1 -1
  21. package/src/providers/import-providers/product-importer.d.ts +58 -10
  22. package/src/providers/import-providers/product-importer.js +163 -94
  23. package/src/providers/import-providers/product-importer.js.map +1 -1
  24. package/src/services/channel-column-resolver.d.ts +24 -0
  25. package/src/services/channel-column-resolver.js +79 -0
  26. package/src/services/channel-column-resolver.js.map +1 -0
  27. package/src/services/export-relation-lookups.d.ts +26 -0
  28. package/src/services/export-relation-lookups.js +56 -0
  29. package/src/services/export-relation-lookups.js.map +1 -0
  30. package/src/services/extended-fast-importer.service.d.ts +16 -0
  31. package/src/services/extended-fast-importer.service.js +46 -21
  32. package/src/services/extended-fast-importer.service.js.map +1 -1
  33. package/src/services/import-row-groups.d.ts +17 -0
  34. package/src/services/import-row-groups.js +69 -0
  35. package/src/services/import-row-groups.js.map +1 -0
  36. package/src/services/product-export.service.d.ts +9 -3
  37. package/src/services/product-export.service.js +57 -10
  38. package/src/services/product-export.service.js.map +1 -1
  39. package/src/services/product-import.service.d.ts +1 -3
  40. package/src/services/product-import.service.js +10 -9
  41. package/src/services/product-import.service.js.map +1 -1
  42. package/src/services/relation-lookup-resolver.d.ts +52 -0
  43. package/src/services/relation-lookup-resolver.js +245 -0
  44. package/src/services/relation-lookup-resolver.js.map +1 -0
  45. package/src/services/self-relation-custom-fields.d.ts +7 -0
  46. package/src/services/self-relation-custom-fields.js +24 -0
  47. package/src/services/self-relation-custom-fields.js.map +1 -1
  48. package/src/strategies/import-row-validation-strategy.d.ts +71 -0
  49. package/src/strategies/import-row-validation-strategy.js +16 -0
  50. package/src/strategies/import-row-validation-strategy.js.map +1 -0
  51. package/src/types.d.ts +21 -1
  52. package/src/ui/components/export-dialog.component.ts +2 -0
  53. package/src/ui/components/product-import.component.ts +1 -0
  54. package/src/services/sku-relation-resolver.d.ts +0 -33
  55. package/src/services/sku-relation-resolver.js +0 -177
  56. package/src/services/sku-relation-resolver.js.map +0 -1
@@ -18,6 +18,7 @@ exports.ProductImporter = void 0;
18
18
  const common_1 = require("@nestjs/common");
19
19
  const core_1 = require("@vendure/core");
20
20
  const core_2 = require("@vendure/core");
21
+ const sync_1 = require("csv-parse/sync");
21
22
  const rxjs_1 = require("rxjs");
22
23
  const shared_utils_1 = require("@vendure/common/lib/shared-utils");
23
24
  const generated_types_1 = require("@vendure/common/lib/generated-types");
@@ -30,12 +31,20 @@ const typeorm_1 = require("typeorm");
30
31
  const generate_asset_hash_1 = require("../../helpers/generate-asset-hash");
31
32
  const crypto_1 = require("crypto");
32
33
  const remove_update_at_fields_1 = require("../../helpers/remove-update-at-fields");
33
- const sku_relation_resolver_1 = require("../../services/sku-relation-resolver");
34
+ const relation_lookup_resolver_1 = require("../../services/relation-lookup-resolver");
35
+ const import_row_validation_strategy_1 = require("../../strategies/import-row-validation-strategy");
36
+ const import_row_groups_1 = require("../../services/import-row-groups");
37
+ const channel_column_resolver_1 = require("../../services/channel-column-resolver");
34
38
  const import_csv_lines_1 = require("../../helpers/import-csv-lines");
35
- const NO_SKU_LOOKUP = { fields: new Set(), variantIdsBySku: new Map() };
39
+ const NO_RELATION_LOOKUPS = { fields: new Map(), idsByLookup: new Map() };
40
+ /** The row an error names (`Row 12: ...`), 0 for an error about the whole file. */
41
+ function errorRow(error) {
42
+ const match = /^Row (\d+):/.exec(error);
43
+ return match ? Number(match[1]) : 0;
44
+ }
36
45
  /**
37
- * Splits a typed custom field header (`product:field:type`, or with a fourth `:sku` segment that
38
- * SkuRelationResolver reads) into the `<owner>:<field>` key the parser checks and the type.
46
+ * Splits a typed custom field header (`product:field:type`, or with a fourth lookup segment that
47
+ * RelationLookupResolver reads) into the `<owner>:<field>` key the parser checks and the type.
39
48
  */
40
49
  function splitTypedCustomFieldHeader(header) {
41
50
  if (!header.startsWith('product:') && !header.startsWith('variant:')) {
@@ -63,17 +72,20 @@ let ProductImporter = class ProductImporter {
63
72
  assetService;
64
73
  optionGroupService;
65
74
  pluginOptions;
66
- skuRelationResolver;
75
+ relationLookupResolver;
76
+ rowValidationStrategy;
77
+ channelColumnResolver;
67
78
  taxCategoryMatches = {};
68
79
  facetMap = new Map();
69
80
  facetValueMap = new Map();
70
81
  customFieldTypes = {};
71
82
  allAssets = [];
72
83
  allAssetsById = new Map();
84
+ allAssetsBySource = new Map();
73
85
  facetIdCache = new Map();
74
86
  facetValueIdCache = new Map();
75
87
  slugStrategy;
76
- constructor(connection, configService, importParser, assetImporter, fastImporter, translatableSaver, productService, channelService, facetService, facetValueService, taxCategoryService, assetService, optionGroupService, pluginOptions, skuRelationResolver) {
88
+ constructor(connection, configService, importParser, assetImporter, fastImporter, translatableSaver, productService, channelService, facetService, facetValueService, taxCategoryService, assetService, optionGroupService, pluginOptions, relationLookupResolver, rowValidationStrategy, channelColumnResolver) {
77
89
  this.connection = connection;
78
90
  this.configService = configService;
79
91
  this.importParser = importParser;
@@ -88,7 +100,9 @@ let ProductImporter = class ProductImporter {
88
100
  this.assetService = assetService;
89
101
  this.optionGroupService = optionGroupService;
90
102
  this.pluginOptions = pluginOptions;
91
- this.skuRelationResolver = skuRelationResolver;
103
+ this.relationLookupResolver = relationLookupResolver;
104
+ this.rowValidationStrategy = rowValidationStrategy;
105
+ this.channelColumnResolver = channelColumnResolver;
92
106
  this.slugStrategy = this.configService.entityOptions.slugStrategy;
93
107
  }
94
108
  parseAndImport(input, ctx, updateProductSlug = true, mainLanguage, updatingStrategy) {
@@ -98,28 +112,31 @@ let ProductImporter = class ProductImporter {
98
112
  }).then((value) => {
99
113
  subscriber.next({ ...value, currentProduct: 'Complete' });
100
114
  subscriber.complete();
101
- });
115
+ }, (error) => subscriber.error(error));
102
116
  });
103
117
  }
118
+ /**
119
+ * Column 0 of every data record as a product id, null when empty or not a number. Index 0 is
120
+ * line 2, counted in records as the parser counts them, so a quoted cell that spans several
121
+ * physical lines does not shift the ids.
122
+ */
104
123
  extractProductIdsFromCSV(csvString) {
105
- // Split the CSV string into lines
106
- const lines = csvString.split('\n');
107
- // Initialize an array to hold the product IDs
108
- const productIds = [];
109
- // Loop through each line in the CSV
110
- lines.forEach((line, index) => {
111
- if (index === 0)
112
- return; // Skip header row
113
- const columns = line.split(',');
114
- const productId = columns[0].trim();
115
- if (productId) {
116
- productIds.push(parseInt(productId, 10));
117
- }
118
- else {
119
- productIds.push(null); // Push null or undefined if productId is missing
120
- }
124
+ let records;
125
+ try {
126
+ records = (0, sync_1.parse)(csvString, {
127
+ trim: true,
128
+ relax_column_count: true,
129
+ relax_quotes: true,
130
+ });
131
+ }
132
+ catch {
133
+ // The parser cannot read the file either, so there are no products to give ids to.
134
+ return [];
135
+ }
136
+ return records.slice(1).map((record) => {
137
+ const productId = parseInt(record[0] ?? '', 10);
138
+ return Number.isNaN(productId) ? null : productId;
121
139
  });
122
- return productIds;
123
140
  }
124
141
  /**
125
142
  * The problems in the file's header row, checked the way the import job checks it. Empty when
@@ -129,9 +146,57 @@ let ProductImporter = class ProductImporter {
129
146
  const headers = (0, import_csv_lines_1.importCsvHeaders)((0, import_csv_lines_1.importCsvLines)(csv)).map((header) => splitTypedCustomFieldHeader(header)?.fullFieldName ?? header);
130
147
  return this.importParser.validateHeader(headers);
131
148
  }
149
+ /**
150
+ * Everything that is checked before an import writes anything: relation lookups, then the parsed
151
+ * rows through the row validation strategy. The upload runs this before the file is stored, and
152
+ * the import job runs it again before its first write, since data can change while the job waits.
153
+ * With the default strategy and no `channels` column the file is parsed only when `parse` asks
154
+ * for it (the job does), so such an upload costs no more than the lookups. `uploader` is the
155
+ * context whose user needs rights in the channels the `channels` column names; the job passes
156
+ * the uploader's, since it imports with a context of its own.
157
+ */
158
+ async checkFile(ctx, csv, mainLanguage, updateProductSlug, { parse = false, uploader = ctx } = {}) {
159
+ const unchecked = { customFieldTypes: {}, channelsByToken: new Map() };
160
+ const relationLookups = await this.relationLookupResolver.resolve(ctx, csv);
161
+ if (!relationLookups.rowsChecked) {
162
+ return { errors: relationLookups.errors, relationLookups, ...unchecked };
163
+ }
164
+ // Only the default itself is skipped: a subclass of it has rules of its own.
165
+ const validatesRows = this.rowValidationStrategy.constructor !== import_row_validation_strategy_1.DefaultImportRowValidationStrategy;
166
+ const namesChannels = (0, import_csv_lines_1.importCsvHeaders)((0, import_csv_lines_1.importCsvLines)(csv)).includes(channel_column_resolver_1.CHANNELS_COLUMN);
167
+ if (!validatesRows && !namesChannels && !parse) {
168
+ return { errors: relationLookups.errors, relationLookups, ...unchecked };
169
+ }
170
+ const { cleanedCsv, customFieldTypes } = this.preprocessCsv(csv, updateProductSlug);
171
+ const parsed = (await this.importParser.parseProducts(cleanedCsv, mainLanguage));
172
+ const productIds = this.extractProductIdsFromCSV(cleanedCsv);
173
+ for (const result of parsed.results) {
174
+ // The id on the product's own first row, not on the line with the product's index.
175
+ const productId = result.product.line ? productIds[result.product.line - 2] : null;
176
+ if (productId != null) {
177
+ result.product.productId = productId;
178
+ }
179
+ }
180
+ const rowErrors = validatesRows
181
+ ? await this.rowValidationStrategy.validate(ctx, (0, import_row_groups_1.toImportRowGroups)(parsed.results, relationLookups, this.configService.customFields))
182
+ : [];
183
+ const channelColumn = await this.channelColumnResolver.resolve(ctx, uploader, parsed.results);
184
+ const errors = [
185
+ ...relationLookups.errors,
186
+ ...channelColumn.errors,
187
+ ...rowErrors.map(({ line, message }) => `Row ${line}: ${message}`),
188
+ ].sort((a, b) => errorRow(a) - errorRow(b));
189
+ return {
190
+ errors,
191
+ relationLookups,
192
+ parsed,
193
+ customFieldTypes,
194
+ channelsByToken: channelColumn.channelsByToken,
195
+ };
196
+ }
132
197
  preprocessCsv(input, updateProductSlug) {
133
198
  const customFieldTypes = {};
134
- // Without blank and comma-only lines, as SkuRelationResolver reads the file
199
+ // Without blank and comma-only lines, as RelationLookupResolver reads the file
135
200
  const filteredLines = (0, import_csv_lines_1.importCsvLines)(input);
136
201
  const headers = (0, import_csv_lines_1.splitCsvLine)(filteredLines[0]);
137
202
  const cleanedHeaders = headers.map((rawHeader) => {
@@ -199,13 +264,13 @@ let ProductImporter = class ProductImporter {
199
264
  }
200
265
  this.allAssets = assets;
201
266
  this.allAssetsById = new Map(assets.map((asset) => [asset.id, asset]));
267
+ this.allAssetsBySource = new Map(assets.map((asset) => [asset.source, asset]));
202
268
  }
203
269
  async doParseAndImport(input, updateProductSlug, mainLanguage, updatingStrategy, reqCtx, onProgress) {
204
270
  // Per-import caches to avoid stale cross-job state.
205
271
  this.facetIdCache.clear();
206
272
  this.facetValueIdCache.clear();
207
273
  const ctx = await this.getRequestContext(reqCtx, mainLanguage);
208
- await this.setMissingAssetHashes(ctx);
209
274
  let csvContent;
210
275
  if (typeof input === 'string') {
211
276
  csvContent = input;
@@ -214,32 +279,28 @@ let ProductImporter = class ProductImporter {
214
279
  // If input is a Stream, you need to read it fully into memory
215
280
  csvContent = await this.streamToString(input);
216
281
  }
217
- // Checked again here, not only on upload: variants can change while the job waits. SKUs are
218
- // resolved once, here: a variant deleted while the import runs is still linked to.
219
- const skuRelations = await this.skuRelationResolver.resolve(ctx, csvContent);
220
- if (skuRelations.errors.length > 0) {
221
- return { errors: skuRelations.errors, imported: 0, processed: 0 };
282
+ // Checked again here, before the first write, not only on upload: data can change while the
283
+ // job waits. Lookup values are resolved once, here: a row deleted while the import runs is
284
+ // still linked to.
285
+ const { errors, relationLookups, parsed, customFieldTypes, channelsByToken } = await this.checkFile(ctx, csvContent, mainLanguage, updateProductSlug, {
286
+ parse: true,
287
+ uploader: reqCtx,
288
+ });
289
+ if (errors.length > 0 || !parsed) {
290
+ // Never fail a job without saying why.
291
+ const reasons = errors.length > 0 ? errors : ['The import file could not be read'];
292
+ return { errors: reasons, imported: 0, processed: 0, rejected: true };
222
293
  }
223
- // Preprocess the CSV to extract types and clean headers
224
- const { cleanedCsv, customFieldTypes } = this.preprocessCsv(csvContent, updateProductSlug);
294
+ await this.setMissingAssetHashes(ctx);
225
295
  this.customFieldTypes = customFieldTypes;
226
- const parsed = (await this.importParser.parseProducts(cleanedCsv, ctx.languageCode));
227
- const productIds = this.extractProductIdsFromCSV(cleanedCsv);
228
- parsed.results.forEach((result, index) => {
229
- const productId = productIds[index];
230
- if (productId !== null) {
231
- result.product.productId = productId;
232
- }
233
- // Handle the case where productId is null if necessary
234
- });
235
- if (parsed && parsed.results.length) {
296
+ if (parsed.results.length) {
236
297
  try {
237
298
  const importErrors = await this.importProducts(ctx, parsed.results, updatingStrategy, (progess) => {
238
299
  onProgress({
239
300
  ...progess,
240
301
  processed: parsed.processed,
241
302
  });
242
- }, skuRelations);
303
+ }, relationLookups, channelsByToken);
243
304
  return {
244
305
  errors: parsed.errors.concat(importErrors),
245
306
  imported: parsed.results.length,
@@ -277,41 +338,31 @@ let ProductImporter = class ProductImporter {
277
338
  throw new core_2.InternalServerError('No RequestContext provided');
278
339
  }
279
340
  }
280
- async importProducts(ctx, rows, updatingStrategy, onProgress, skuLookup = NO_SKU_LOOKUP) {
341
+ async importProducts(ctx, rows, updatingStrategy, onProgress, relationLookups = NO_RELATION_LOOKUPS, channelsByToken = new Map()) {
281
342
  let errors = [];
282
343
  let imported = 0;
283
344
  const languageCode = ctx.languageCode;
284
- const taxCategories = await this.taxCategoryService.findAll(ctx);
285
- // Create taxCategory if none exists
345
+ let taxCategories = await this.taxCategoryService.findAll(ctx);
346
+ // Create taxCategory if none exists, and read the list again so the rows can match it
286
347
  if (taxCategories.totalItems === 0) {
287
348
  await this.taxCategoryService.create(ctx, {
288
349
  name: 'Standard Tax',
289
350
  isDefault: true,
290
351
  });
352
+ taxCategories = await this.taxCategoryService.findAll(ctx);
291
353
  }
292
354
  await this.fastImporter.initialize(ctx.channel);
293
355
  const shouldRestore = this.pluginOptions?.importOptions?.defaultOptions?.restoreSoftDeleted !== false;
294
356
  const sharedGroupMap = await this.buildSharedOptionGroupMap(ctx, rows);
295
357
  for (const { product, variants } of rows) {
358
+ this.fastImporter.setProductChannels((0, lodash_1.compact)(product.channelTokens.map((token) => channelsByToken.get(token))));
296
359
  const productMainTranslation = this.getTranslationByCodeOrFirst(product.translations, ctx.languageCode);
297
- const assetsToImport = (0, lodash_1.compact)(product.assetPaths.map((assetPath, idx) => {
298
- const assetID = product.assetsJson?.[idx]?.id;
299
- if (assetPath.startsWith('http://') || assetPath.startsWith('https://')) {
300
- return {
301
- url: assetPath,
302
- name: product.assetsJson?.[idx]?.name || undefined,
303
- id: assetID && this.allAssetsById.get(assetID)
304
- ? product.assetsJson?.[idx]?.id
305
- : undefined,
306
- };
307
- }
308
- }));
309
- const createProductAssets = await this.assetImporter.getAssets(assetsToImport, ctx);
360
+ const createProductAssets = await this.resolveAssets(product.assetPaths, product.assetsJson, ctx);
310
361
  const productAssets = createProductAssets.assets;
311
362
  if (createProductAssets.errors.length) {
312
363
  errors = errors.concat(createProductAssets.errors);
313
364
  }
314
- const customFields = await this.processCustomFieldValues(product.translations[0].customFields, this.configService.customFields.Product, ctx, 'product', skuLookup);
365
+ const customFields = await this.processCustomFieldValues(product.translations[0].customFields, this.configService.customFields.Product, ctx, 'product', relationLookups);
315
366
  let checkExistingProduct;
316
367
  if (product.productId) {
317
368
  checkExistingProduct = await this.productService.findOne(ctx, product.productId);
@@ -387,7 +438,7 @@ let ProductImporter = class ProductImporter {
387
438
  ? { description: translation.description }
388
439
  : {}),
389
440
  slug: existingTranslation?.slug || translation.slug,
390
- customFields: await this.processCustomFieldValues(translation.customFields, this.configService.customFields.Product, ctx, 'product', skuLookup),
441
+ customFields: await this.processCustomFieldValues(translation.customFields, this.configService.customFields.Product, ctx, 'product', relationLookups),
391
442
  };
392
443
  })),
393
444
  customFields,
@@ -495,19 +546,7 @@ let ProductImporter = class ProductImporter {
495
546
  }
496
547
  for (const variant of variants) {
497
548
  const variantMainTranslation = this.getTranslationByCodeOrFirst(variant.translations, ctx.languageCode);
498
- const assetsToImport = (0, lodash_1.compact)(variant.assetPaths.map((assetPath, idx) => {
499
- const assetID = variant.assetsJson?.[idx]?.id;
500
- if (assetPath.startsWith('http://') || assetPath.startsWith('https://')) {
501
- return {
502
- url: assetPath,
503
- name: variant.assetsJson?.[idx]?.name || undefined,
504
- id: assetID && this.allAssetsById.get(assetID)
505
- ? variant.assetsJson?.[idx]?.id
506
- : undefined,
507
- };
508
- }
509
- }));
510
- const createVariantAssets = await this.assetImporter.getAssets(assetsToImport);
549
+ const createVariantAssets = await this.resolveAssets(variant.assetPaths, variant.assetsJson);
511
550
  const variantAssets = createVariantAssets.assets;
512
551
  if (createVariantAssets.errors.length) {
513
552
  errors = errors.concat(createVariantAssets.errors);
@@ -516,7 +555,7 @@ let ProductImporter = class ProductImporter {
516
555
  // if (0 < variant.facets.length) {
517
556
  // facetValueIds = await this.getFacetValueIds(ctx, variant.facets, languageCode)
518
557
  // }
519
- const variantCustomFields = await this.processCustomFieldValues(variantMainTranslation.customFields, this.configService.customFields.ProductVariant, ctx, 'variant', skuLookup);
558
+ const variantCustomFields = await this.processCustomFieldValues(variantMainTranslation.customFields, this.configService.customFields.ProductVariant, ctx, 'variant', relationLookups);
520
559
  const optionIds = shouldUpdateOptions
521
560
  ? variantMainTranslation.optionValues.map((v, index) => optionsMap[`${index}_${v}`])
522
561
  : undefined;
@@ -577,7 +616,7 @@ let ProductImporter = class ProductImporter {
577
616
  return {
578
617
  languageCode: translation.languageCode,
579
618
  name: [productTranslation.name, ...translation.optionValues].join(' '),
580
- customFields: await this.processCustomFieldValues(translation.customFields, this.configService.customFields.ProductVariant, ctx, 'variant', skuLookup),
619
+ customFields: await this.processCustomFieldValues(translation.customFields, this.configService.customFields.ProductVariant, ctx, 'variant', relationLookups),
581
620
  };
582
621
  })),
583
622
  price: (0, lodash_1.isUndefined)(variant.price) ? undefined : Math.round(variant.price * 100),
@@ -748,24 +787,24 @@ let ProductImporter = class ProductImporter {
748
787
  }
749
788
  return out;
750
789
  }
751
- /** Swaps SKUs for variant ids; `SkuRelationResolver` has already checked each SKU. */
752
- variantIdsForSkus(variantIdsBySku, value, list) {
753
- const idFor = (sku) => {
754
- const id = variantIdsBySku.get(sku);
790
+ /** Swaps lookup values for ids; `RelationLookupResolver` has already checked each value. */
791
+ idsForLookupValues(idsByLookup, lookup, value, list) {
792
+ const idFor = (lookupValue) => {
793
+ const id = idsByLookup.get((0, relation_lookup_resolver_1.relationLookupKey)(lookup, lookupValue));
755
794
  if (id === undefined) {
756
- throw new core_2.InternalServerError(`No variant with SKU ${sku}`);
795
+ throw new core_2.InternalServerError(`No ${lookup.entityName} with ${lookup.column} ${lookupValue}`);
757
796
  }
758
797
  return String(id);
759
798
  };
760
799
  if (!list) {
761
800
  return idFor(value.trim());
762
801
  }
763
- return (0, sku_relation_resolver_1.splitSkuList)(value).map(idFor);
802
+ return (0, relation_lookup_resolver_1.splitLookupList)(value).map(idFor);
764
803
  }
765
804
  isValidRelationIdentifier(value) {
766
805
  return /^[A-Za-z0-9_-]+$/.test(value);
767
806
  }
768
- async processCustomFieldValues(customFields, config, ctx, owner, skuLookup = NO_SKU_LOOKUP) {
807
+ async processCustomFieldValues(customFields, config, ctx, owner, relationLookups = NO_RELATION_LOOKUPS) {
769
808
  function parseBoolean(input) {
770
809
  if (input == null) {
771
810
  return false;
@@ -843,6 +882,15 @@ let ProductImporter = class ProductImporter {
843
882
  if (fieldDef.type === 'relation') {
844
883
  const inputKey = (0, shared_utils_1.getGraphQlInputName)(fieldDef);
845
884
  const relDef = fieldDef;
885
+ const lookup = relationLookups.fields.get(`${owner}:${fieldDef.name}`);
886
+ if (lookup) {
887
+ const ids = this.idsForLookupValues(relationLookups.idsByLookup, lookup, value, fieldDef.list === true);
888
+ if (relDef.nullable === false && Array.isArray(ids) && ids.length === 0) {
889
+ throw new core_2.InternalServerError(`Required relation custom field "${fieldDef.name}" is missing or empty`);
890
+ }
891
+ processed[inputKey] = ids;
892
+ continue;
893
+ }
846
894
  if (relDef.entity === core_2.Asset) {
847
895
  let resolvedAssetRelation;
848
896
  if ((0, lodash_1.startsWith)(value, '{') || (0, lodash_1.startsWith)(value, '[')) {
@@ -901,14 +949,6 @@ let ProductImporter = class ProductImporter {
901
949
  }
902
950
  continue;
903
951
  }
904
- if (skuLookup.fields.has(`${owner}:${fieldDef.name}`)) {
905
- const variantIds = this.variantIdsForSkus(skuLookup.variantIdsBySku, value, fieldDef.list === true);
906
- if (relDef.nullable === false && Array.isArray(variantIds) && variantIds.length === 0) {
907
- throw new core_2.InternalServerError(`Required relation custom field "${fieldDef.name}" is missing or empty`);
908
- }
909
- processed[inputKey] = variantIds;
910
- continue;
911
- }
912
952
  if (fieldDef.list === true) {
913
953
  const relationIds = value
914
954
  .split('|')
@@ -1113,6 +1153,34 @@ let ProductImporter = class ProductImporter {
1113
1153
  this.facetValueIdCache.set(facetValueCacheKey, facetValueId);
1114
1154
  return facetValueId;
1115
1155
  }
1156
+ /**
1157
+ * Resolves the assets of an `assets` or `variantAssets` cell, in cell order. A URL goes through
1158
+ * the asset importer. Any other value names an existing asset the way the export writes it: by
1159
+ * the `id` of the JSON format or by its `source`. An existing asset is linked as it is, never
1160
+ * uploaded again, and a value that matches no asset is reported.
1161
+ */
1162
+ async resolveAssets(assetPaths, assetsJson, ctx) {
1163
+ const assets = [];
1164
+ const errors = [];
1165
+ for (const [idx, assetPath] of assetPaths.entries()) {
1166
+ const assetID = assetsJson?.[idx]?.id;
1167
+ const knownAsset = assetID ? this.allAssetsById.get(assetID) : undefined;
1168
+ if (assetPath.startsWith('http://') || assetPath.startsWith('https://')) {
1169
+ const imported = await this.assetImporter.getAssets([{ url: assetPath, name: assetsJson?.[idx]?.name || undefined, id: knownAsset?.id }], ctx);
1170
+ assets.push(...imported.assets);
1171
+ errors.push(...imported.errors);
1172
+ continue;
1173
+ }
1174
+ const existingAsset = knownAsset ?? this.allAssetsBySource.get(assetPath);
1175
+ if (existingAsset) {
1176
+ assets.push(existingAsset);
1177
+ }
1178
+ else {
1179
+ errors.push(`Could not find an existing asset with source "${assetPath}"`);
1180
+ }
1181
+ }
1182
+ return { assets, errors };
1183
+ }
1116
1184
  resolveFeaturedAssetId(importedAssets, existingFeaturedAsset, updatingStrategy) {
1117
1185
  const firstFeaturedCandidate = importedAssets.find((a) => a.type === generated_types_1.AssetType.IMAGE) ?? importedAssets[0];
1118
1186
  if (importedAssets.length > 0 && firstFeaturedCandidate) {
@@ -1169,6 +1237,7 @@ exports.ProductImporter = ProductImporter;
1169
1237
  exports.ProductImporter = ProductImporter = __decorate([
1170
1238
  (0, common_1.Injectable)(),
1171
1239
  __param(13, (0, common_1.Inject)(constants_1.PRODUCT_IMPORT_EXPORT_PLUGIN_OPTIONS)),
1240
+ __param(15, (0, common_1.Inject)(constants_1.IMPORT_ROW_VALIDATION_STRATEGY)),
1172
1241
  __metadata("design:paramtypes", [core_1.TransactionalConnection,
1173
1242
  core_1.ConfigService,
1174
1243
  import_parser_1.ImportParser,
@@ -1181,6 +1250,6 @@ exports.ProductImporter = ProductImporter = __decorate([
1181
1250
  core_1.FacetValueService,
1182
1251
  core_1.TaxCategoryService,
1183
1252
  core_1.AssetService,
1184
- core_1.ProductOptionGroupService, Object, sku_relation_resolver_1.SkuRelationResolver])
1253
+ core_1.ProductOptionGroupService, Object, relation_lookup_resolver_1.RelationLookupResolver, Object, channel_column_resolver_1.ChannelColumnResolver])
1185
1254
  ], ProductImporter);
1186
1255
  //# sourceMappingURL=product-importer.js.map