@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.
- package/CHANGELOG.md +17 -0
- package/README.md +110 -10
- package/package.json +1 -1
- package/src/constants.d.ts +1 -0
- package/src/constants.js +2 -1
- package/src/constants.js.map +1 -1
- package/src/dashboard/export-configuration-panel.tsx +1 -0
- package/src/dashboard/utils.ts +1 -0
- package/src/helpers/find-text-column.d.ts +8 -0
- package/src/helpers/find-text-column.js +32 -0
- package/src/helpers/find-text-column.js.map +1 -0
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/index.js.map +1 -1
- package/src/product-import-export.plugin.d.ts +11 -1
- package/src/product-import-export.plugin.js +35 -3
- package/src/product-import-export.plugin.js.map +1 -1
- package/src/providers/import-providers/import-parser.d.ts +9 -0
- package/src/providers/import-providers/import-parser.js +26 -8
- package/src/providers/import-providers/import-parser.js.map +1 -1
- package/src/providers/import-providers/product-importer.d.ts +58 -10
- package/src/providers/import-providers/product-importer.js +163 -94
- package/src/providers/import-providers/product-importer.js.map +1 -1
- package/src/services/channel-column-resolver.d.ts +24 -0
- package/src/services/channel-column-resolver.js +79 -0
- package/src/services/channel-column-resolver.js.map +1 -0
- package/src/services/export-relation-lookups.d.ts +26 -0
- package/src/services/export-relation-lookups.js +56 -0
- package/src/services/export-relation-lookups.js.map +1 -0
- package/src/services/extended-fast-importer.service.d.ts +16 -0
- package/src/services/extended-fast-importer.service.js +46 -21
- package/src/services/extended-fast-importer.service.js.map +1 -1
- package/src/services/import-row-groups.d.ts +17 -0
- package/src/services/import-row-groups.js +69 -0
- package/src/services/import-row-groups.js.map +1 -0
- package/src/services/product-export.service.d.ts +9 -3
- package/src/services/product-export.service.js +57 -10
- package/src/services/product-export.service.js.map +1 -1
- package/src/services/product-import.service.d.ts +1 -3
- package/src/services/product-import.service.js +10 -9
- package/src/services/product-import.service.js.map +1 -1
- package/src/services/relation-lookup-resolver.d.ts +52 -0
- package/src/services/relation-lookup-resolver.js +245 -0
- package/src/services/relation-lookup-resolver.js.map +1 -0
- package/src/services/self-relation-custom-fields.d.ts +7 -0
- package/src/services/self-relation-custom-fields.js +24 -0
- package/src/services/self-relation-custom-fields.js.map +1 -1
- package/src/strategies/import-row-validation-strategy.d.ts +71 -0
- package/src/strategies/import-row-validation-strategy.js +16 -0
- package/src/strategies/import-row-validation-strategy.js.map +1 -0
- package/src/types.d.ts +21 -1
- package/src/ui/components/export-dialog.component.ts +2 -0
- package/src/ui/components/product-import.component.ts +1 -0
- package/src/services/sku-relation-resolver.d.ts +0 -33
- package/src/services/sku-relation-resolver.js +0 -177
- 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
|
|
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
|
|
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
|
|
38
|
-
*
|
|
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
|
-
|
|
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,
|
|
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.
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
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:
|
|
218
|
-
// resolved once, here: a
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
const { cleanedCsv, customFieldTypes } = this.preprocessCsv(csvContent, updateProductSlug);
|
|
294
|
+
await this.setMissingAssetHashes(ctx);
|
|
225
295
|
this.customFieldTypes = customFieldTypes;
|
|
226
|
-
|
|
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
|
-
},
|
|
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,
|
|
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
|
-
|
|
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
|
|
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',
|
|
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',
|
|
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
|
|
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',
|
|
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',
|
|
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
|
|
752
|
-
|
|
753
|
-
const idFor = (
|
|
754
|
-
const id =
|
|
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
|
|
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,
|
|
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,
|
|
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,
|
|
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
|