@haus-tech/product-import-export-plugin 3.0.3 → 3.0.5
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/package.json +2 -2
- package/dist/api/plugin.controller.d.ts +0 -8
- package/dist/api/plugin.controller.js +0 -49
- package/dist/api/product-export.controller.d.ts +0 -14
- package/dist/api/product-export.controller.js +0 -117
- package/dist/api/product-import.controller.d.ts +0 -11
- package/dist/api/product-import.controller.js +0 -63
- package/dist/constants.d.ts +0 -2
- package/dist/constants.js +0 -5
- package/dist/helpers/generate-asset-hash.d.ts +0 -2
- package/dist/helpers/generate-asset-hash.js +0 -15
- package/dist/helpers/normalize-string.d.ts +0 -1
- package/dist/helpers/normalize-string.js +0 -16
- package/dist/helpers/remove-update-at-fields.d.ts +0 -5
- package/dist/helpers/remove-update-at-fields.js +0 -19
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -17
- package/dist/product-import-export.plugin.d.ts +0 -13
- package/dist/product-import-export.plugin.js +0 -153
- package/dist/providers/import-providers/asset-importer.d.ts +0 -27
- package/dist/providers/import-providers/asset-importer.js +0 -138
- package/dist/providers/import-providers/import-parser.d.ts +0 -137
- package/dist/providers/import-providers/import-parser.js +0 -600
- package/dist/providers/import-providers/product-importer.d.ts +0 -48
- package/dist/providers/import-providers/product-importer.js +0 -753
- package/dist/services/extended-fast-importer.service.d.ts +0 -44
- package/dist/services/extended-fast-importer.service.js +0 -462
- package/dist/services/product-export.service.d.ts +0 -23
- package/dist/services/product-export.service.js +0 -344
- package/dist/services/product-import.service.d.ts +0 -22
- package/dist/services/product-import.service.js +0 -84
- package/dist/types.d.ts +0 -41
- package/dist/types.js +0 -2
- package/dist/ui/components/export-dialog.component.d.ts +0 -43
- package/dist/ui/components/export-dialog.component.html +0 -78
- package/dist/ui/components/export-dialog.component.js +0 -108
- package/dist/ui/components/export-dialog.component.ts +0 -142
- package/dist/ui/components/product-import.component.d.ts +0 -57
- package/dist/ui/components/product-import.component.html +0 -123
- package/dist/ui/components/product-import.component.js +0 -413
- package/dist/ui/components/product-import.component.ts +0 -550
- package/dist/ui/product-export.service.d.ts +0 -14
- package/dist/ui/product-export.service.js +0 -107
- package/dist/ui/product-export.service.ts +0 -107
- package/dist/ui/providers.d.ts +0 -2
- package/dist/ui/providers.js +0 -49
- package/dist/ui/providers.ts +0 -57
- package/dist/ui/routes.d.ts +0 -31
- package/dist/ui/routes.js +0 -12
- package/dist/ui/routes.ts +0 -11
- package/dist/ui/translations/en.json +0 -52
- package/dist/ui/translations/sv.json +0 -52
|
@@ -1,753 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
-
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
-
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
-
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
-
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
-
};
|
|
8
|
-
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
-
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
-
};
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
exports.ProductImporter = void 0;
|
|
13
|
-
const common_1 = require("@nestjs/common");
|
|
14
|
-
const core_1 = require("@vendure/core");
|
|
15
|
-
const rxjs_1 = require("rxjs");
|
|
16
|
-
const extended_fast_importer_service_1 = require("../../services/extended-fast-importer.service");
|
|
17
|
-
const lodash_1 = require("lodash");
|
|
18
|
-
const normalize_string_1 = require("../../helpers/normalize-string");
|
|
19
|
-
const import_parser_1 = require("./import-parser");
|
|
20
|
-
const asset_importer_1 = require("./asset-importer");
|
|
21
|
-
const typeorm_1 = require("typeorm");
|
|
22
|
-
const generate_asset_hash_1 = require("../../helpers/generate-asset-hash");
|
|
23
|
-
const crypto_1 = require("crypto");
|
|
24
|
-
const remove_update_at_fields_1 = require("../../helpers/remove-update-at-fields");
|
|
25
|
-
let ProductImporter = class ProductImporter {
|
|
26
|
-
connection;
|
|
27
|
-
configService;
|
|
28
|
-
importParser;
|
|
29
|
-
assetImporter;
|
|
30
|
-
fastImporter;
|
|
31
|
-
translatableSaver;
|
|
32
|
-
productService;
|
|
33
|
-
channelService;
|
|
34
|
-
facetService;
|
|
35
|
-
facetValueService;
|
|
36
|
-
taxCategoryService;
|
|
37
|
-
assetService;
|
|
38
|
-
optionGroupService;
|
|
39
|
-
taxCategoryMatches = {};
|
|
40
|
-
facetMap = new Map();
|
|
41
|
-
facetValueMap = new Map();
|
|
42
|
-
customFieldTypes = {};
|
|
43
|
-
allAssets = [];
|
|
44
|
-
constructor(connection, configService, importParser, assetImporter, fastImporter, translatableSaver, productService, channelService, facetService, facetValueService, taxCategoryService, assetService, optionGroupService) {
|
|
45
|
-
this.connection = connection;
|
|
46
|
-
this.configService = configService;
|
|
47
|
-
this.importParser = importParser;
|
|
48
|
-
this.assetImporter = assetImporter;
|
|
49
|
-
this.fastImporter = fastImporter;
|
|
50
|
-
this.translatableSaver = translatableSaver;
|
|
51
|
-
this.productService = productService;
|
|
52
|
-
this.channelService = channelService;
|
|
53
|
-
this.facetService = facetService;
|
|
54
|
-
this.facetValueService = facetValueService;
|
|
55
|
-
this.taxCategoryService = taxCategoryService;
|
|
56
|
-
this.assetService = assetService;
|
|
57
|
-
this.optionGroupService = optionGroupService;
|
|
58
|
-
}
|
|
59
|
-
parseAndImport(input, ctx, updateProductSlug = true, mainLanguage, updatingStrategy) {
|
|
60
|
-
return new rxjs_1.Observable((subscriber) => {
|
|
61
|
-
const p = this.doParseAndImport(input, updateProductSlug, mainLanguage, updatingStrategy, ctx, (progress) => {
|
|
62
|
-
subscriber.next(progress);
|
|
63
|
-
}).then((value) => {
|
|
64
|
-
subscriber.next({ ...value, currentProduct: 'Complete' });
|
|
65
|
-
subscriber.complete();
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
extractProductIdsFromCSV(csvString) {
|
|
70
|
-
// Split the CSV string into lines
|
|
71
|
-
const lines = csvString.split('\n');
|
|
72
|
-
// Initialize an array to hold the product IDs
|
|
73
|
-
const productIds = [];
|
|
74
|
-
// Loop through each line in the CSV
|
|
75
|
-
lines.forEach((line, index) => {
|
|
76
|
-
if (index === 0)
|
|
77
|
-
return; // Skip header row
|
|
78
|
-
const columns = line.split(',');
|
|
79
|
-
const productId = columns[0].trim();
|
|
80
|
-
if (productId) {
|
|
81
|
-
productIds.push(parseInt(productId, 10));
|
|
82
|
-
}
|
|
83
|
-
else {
|
|
84
|
-
productIds.push(null); // Push null or undefined if productId is missing
|
|
85
|
-
}
|
|
86
|
-
});
|
|
87
|
-
return productIds;
|
|
88
|
-
}
|
|
89
|
-
preprocessCsv(input, updateProductSlug) {
|
|
90
|
-
const customFieldTypes = {};
|
|
91
|
-
const lines = input.split('\n');
|
|
92
|
-
// Filter out empty lines (lines that are either blank or just commas)
|
|
93
|
-
const filteredLines = lines.filter((line) => {
|
|
94
|
-
const trimmed = line.trim();
|
|
95
|
-
return trimmed !== '' && !/^,+$/.test(trimmed); // Keep lines that are not empty or comma-only
|
|
96
|
-
});
|
|
97
|
-
const splitLines = (text) => {
|
|
98
|
-
// Split text by commas, but not if comma is inside double quotes
|
|
99
|
-
const splittedText = text.split(/,(?=(?:(?:[^"]*"){2})*[^"]*$)/);
|
|
100
|
-
return splittedText;
|
|
101
|
-
};
|
|
102
|
-
const headers = splitLines(filteredLines[0]); // Use splitCsvLine to split headers correctly
|
|
103
|
-
const cleanedHeaders = headers.map((header) => {
|
|
104
|
-
if (header.startsWith('product:') || header.startsWith('variant:')) {
|
|
105
|
-
const parts = header.split(':');
|
|
106
|
-
if (parts.length === 3) {
|
|
107
|
-
const [prefix, fieldName, type] = parts;
|
|
108
|
-
const fullFieldName = `${prefix}:${fieldName}`;
|
|
109
|
-
customFieldTypes[fullFieldName] = type.trim();
|
|
110
|
-
return fullFieldName;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
return header;
|
|
114
|
-
});
|
|
115
|
-
// Recreate the CSV with cleaned headers
|
|
116
|
-
filteredLines[0] = cleanedHeaders.join(',');
|
|
117
|
-
// If the updateProductSlug flag is set, find all slug columns and set their values to an empty string.
|
|
118
|
-
if (updateProductSlug) {
|
|
119
|
-
const slugIndices = cleanedHeaders
|
|
120
|
-
.map((header, index) => (header.includes('slug') ? index : -1))
|
|
121
|
-
.filter((index) => index !== -1); // Get all indices of columns that contain 'slug'
|
|
122
|
-
if (slugIndices.length > 0) {
|
|
123
|
-
filteredLines.forEach((line, index) => {
|
|
124
|
-
if (index === 0)
|
|
125
|
-
return; // Skip header row
|
|
126
|
-
const columns = splitLines(line); // Use splitCsvLine to split the data line correctly
|
|
127
|
-
slugIndices.forEach((slugIndex) => {
|
|
128
|
-
columns[slugIndex] = ''; // Clear each slug column
|
|
129
|
-
});
|
|
130
|
-
filteredLines[index] = columns.join(','); // Reassemble the line
|
|
131
|
-
});
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
return {
|
|
135
|
-
cleanedCsv: filteredLines.join('\n'),
|
|
136
|
-
customFieldTypes,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
async streamToString(stream) {
|
|
140
|
-
const chunks = [];
|
|
141
|
-
return new Promise((resolve, reject) => {
|
|
142
|
-
stream.on('data', (chunk) => chunks.push(chunk));
|
|
143
|
-
stream.on('error', reject);
|
|
144
|
-
stream.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
|
-
async getAllAssets(ctx) {
|
|
148
|
-
let offset = 0;
|
|
149
|
-
const limit = 100;
|
|
150
|
-
const assets = [];
|
|
151
|
-
let totalItems = 0;
|
|
152
|
-
do {
|
|
153
|
-
const paginatedAssets = await this.assetService.findAll(ctx, { take: limit, skip: offset });
|
|
154
|
-
assets.push(...paginatedAssets.items);
|
|
155
|
-
totalItems = paginatedAssets.totalItems;
|
|
156
|
-
offset += limit;
|
|
157
|
-
} while (assets.length < totalItems);
|
|
158
|
-
return assets;
|
|
159
|
-
}
|
|
160
|
-
async setMissingAssetHashes(ctx) {
|
|
161
|
-
const assets = await this.getAllAssets(ctx);
|
|
162
|
-
const assetsWithoutHash = assets.filter((asset) => !asset.customFields.hash);
|
|
163
|
-
for (const asset of assetsWithoutHash) {
|
|
164
|
-
const hash = (0, generate_asset_hash_1.generateAssetHash)(asset);
|
|
165
|
-
if (asset.customFields.hash !== hash) {
|
|
166
|
-
asset.customFields.hash = hash;
|
|
167
|
-
await this.connection.getRepository(ctx, core_1.Asset).save(asset);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
this.allAssets = assets;
|
|
171
|
-
}
|
|
172
|
-
async doParseAndImport(input, updateProductSlug, mainLanguage, updatingStrategy, reqCtx, onProgress) {
|
|
173
|
-
const ctx = await this.getRequestContext(reqCtx, mainLanguage);
|
|
174
|
-
await this.setMissingAssetHashes(ctx);
|
|
175
|
-
let csvContent;
|
|
176
|
-
if (typeof input === 'string') {
|
|
177
|
-
csvContent = input;
|
|
178
|
-
}
|
|
179
|
-
else {
|
|
180
|
-
// If input is a Stream, you need to read it fully into memory
|
|
181
|
-
csvContent = await this.streamToString(input);
|
|
182
|
-
}
|
|
183
|
-
// Preprocess the CSV to extract types and clean headers
|
|
184
|
-
const { cleanedCsv, customFieldTypes } = this.preprocessCsv(csvContent, updateProductSlug);
|
|
185
|
-
this.customFieldTypes = customFieldTypes;
|
|
186
|
-
const parsed = (await this.importParser.parseProducts(cleanedCsv, ctx.languageCode));
|
|
187
|
-
const productIds = this.extractProductIdsFromCSV(cleanedCsv);
|
|
188
|
-
parsed.results.forEach((result, index) => {
|
|
189
|
-
const productId = productIds[index];
|
|
190
|
-
if (productId !== null) {
|
|
191
|
-
result.product.productId = productId;
|
|
192
|
-
}
|
|
193
|
-
// Handle the case where productId is null if necessary
|
|
194
|
-
});
|
|
195
|
-
if (parsed && parsed.results.length) {
|
|
196
|
-
try {
|
|
197
|
-
const importErrors = await this.importProducts(ctx, parsed.results, updatingStrategy, (progess) => {
|
|
198
|
-
onProgress({
|
|
199
|
-
...progess,
|
|
200
|
-
processed: parsed.processed,
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
return {
|
|
204
|
-
errors: parsed.errors.concat(importErrors),
|
|
205
|
-
imported: parsed.results.length,
|
|
206
|
-
processed: parsed.processed,
|
|
207
|
-
};
|
|
208
|
-
}
|
|
209
|
-
catch (err) {
|
|
210
|
-
core_1.Logger.error('Error while importing products:', 'ProductImporter', err?.stack);
|
|
211
|
-
return {
|
|
212
|
-
errors: [err.message],
|
|
213
|
-
imported: 0,
|
|
214
|
-
processed: parsed.processed,
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
else {
|
|
219
|
-
return {
|
|
220
|
-
errors: parsed.errors,
|
|
221
|
-
imported: 0,
|
|
222
|
-
processed: parsed.processed,
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
async getRequestContext(ctx, mainLanguage) {
|
|
227
|
-
if (ctx instanceof core_1.RequestContext) {
|
|
228
|
-
return new core_1.RequestContext({
|
|
229
|
-
apiType: 'admin',
|
|
230
|
-
isAuthorized: true,
|
|
231
|
-
authorizedAsOwnerOnly: false,
|
|
232
|
-
channel: ctx.channel,
|
|
233
|
-
languageCode: mainLanguage,
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
else {
|
|
237
|
-
throw new core_1.InternalServerError('No RequestContext provided');
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
async importProducts(ctx, rows, updatingStrategy, onProgress) {
|
|
241
|
-
let errors = [];
|
|
242
|
-
let imported = 0;
|
|
243
|
-
const languageCode = ctx.languageCode;
|
|
244
|
-
const taxCategories = await this.taxCategoryService.findAll(ctx);
|
|
245
|
-
// Create taxCategory if none exists
|
|
246
|
-
if (taxCategories.totalItems === 0) {
|
|
247
|
-
await this.taxCategoryService.create(ctx, {
|
|
248
|
-
name: 'Standard Tax',
|
|
249
|
-
isDefault: true,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
await this.fastImporter.initialize(ctx.channel);
|
|
253
|
-
for (const { product, variants } of rows) {
|
|
254
|
-
const productMainTranslation = this.getTranslationByCodeOrFirst(product.translations, ctx.languageCode);
|
|
255
|
-
const assetsToImport = (0, lodash_1.compact)(product.assetPaths.map((assetPath, idx) => {
|
|
256
|
-
const assetID = product.assetsJson?.[idx]?.id;
|
|
257
|
-
if (assetPath.startsWith('http://') || assetPath.startsWith('https://')) {
|
|
258
|
-
return {
|
|
259
|
-
url: assetPath,
|
|
260
|
-
name: product.assetsJson?.[idx]?.name || undefined,
|
|
261
|
-
id: assetID && this.allAssets.find((a) => a.id === assetID)
|
|
262
|
-
? product.assetsJson?.[idx]?.id
|
|
263
|
-
: undefined,
|
|
264
|
-
};
|
|
265
|
-
}
|
|
266
|
-
}));
|
|
267
|
-
const createProductAssets = await this.assetImporter.getAssets(assetsToImport, ctx);
|
|
268
|
-
const productAssets = createProductAssets.assets;
|
|
269
|
-
if (createProductAssets.errors.length) {
|
|
270
|
-
errors = errors.concat(createProductAssets.errors);
|
|
271
|
-
}
|
|
272
|
-
const customFields = await this.processCustomFieldValues(product.translations[0].customFields, this.configService.customFields.Product, ctx);
|
|
273
|
-
let checkExistingProduct;
|
|
274
|
-
if (product.productId) {
|
|
275
|
-
checkExistingProduct = await this.productService.findOne(ctx, product.productId);
|
|
276
|
-
}
|
|
277
|
-
if (!checkExistingProduct) {
|
|
278
|
-
const variantSkus = variants.map((v) => v.sku);
|
|
279
|
-
const foundVariants = await this.connection.getRepository(ctx, core_1.ProductVariant).findBy({
|
|
280
|
-
sku: (0, typeorm_1.In)(variantSkus),
|
|
281
|
-
});
|
|
282
|
-
if (foundVariants.length) {
|
|
283
|
-
checkExistingProduct = { id: foundVariants[0].productId };
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
const existingProduct = await this.getProductByIdWithRelations(ctx, checkExistingProduct?.id);
|
|
287
|
-
try {
|
|
288
|
-
if (existingProduct?.deletedAt) {
|
|
289
|
-
existingProduct.deletedAt = null;
|
|
290
|
-
try {
|
|
291
|
-
await this.connection.getRepository(ctx, core_1.Product).save([existingProduct]);
|
|
292
|
-
}
|
|
293
|
-
catch (error) {
|
|
294
|
-
errors.push(`Error while restoring the product: ${error}`);
|
|
295
|
-
core_1.Logger.error('Error while restoring the product:', 'ProductImporter', error?.stack);
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
catch (error) {
|
|
300
|
-
errors.push(`Error while restoring the product: ${error}`);
|
|
301
|
-
core_1.Logger.error('Error while restoring the product:', 'ProductImporter', error?.stack);
|
|
302
|
-
}
|
|
303
|
-
const existingProductName = existingProduct?.translations.find((t) => t.languageCode === languageCode)?.name;
|
|
304
|
-
const productNameHasChanged = existingProduct && existingProductName !== productMainTranslation.name
|
|
305
|
-
? {
|
|
306
|
-
name: productMainTranslation.name,
|
|
307
|
-
previousName: existingProductName,
|
|
308
|
-
normalizedName: (0, normalize_string_1.normalizeString)(productMainTranslation.name, '-', languageCode),
|
|
309
|
-
previousNormalizedName: (0, normalize_string_1.normalizeString)(existingProductName),
|
|
310
|
-
}
|
|
311
|
-
: undefined;
|
|
312
|
-
if (existingProduct && updatingStrategy === 'replace') {
|
|
313
|
-
await this.fastImporter.removeAllAssetsFromProduct(existingProduct);
|
|
314
|
-
}
|
|
315
|
-
const productData = {
|
|
316
|
-
featuredAssetId: existingProduct?.featuredAsset
|
|
317
|
-
? undefined
|
|
318
|
-
: productAssets.length
|
|
319
|
-
? productAssets[0].id
|
|
320
|
-
: undefined,
|
|
321
|
-
assetIds: productAssets.map((a) => a.id),
|
|
322
|
-
facetValueIds: updatingStrategy === 'replace' ? [] : undefined,
|
|
323
|
-
translations: await Promise.all(product.translations.map(async (translation) => {
|
|
324
|
-
return {
|
|
325
|
-
languageCode: translation.languageCode,
|
|
326
|
-
name: translation.name,
|
|
327
|
-
description: translation.description,
|
|
328
|
-
slug: translation.slug,
|
|
329
|
-
customFields: await this.processCustomFieldValues(translation.customFields, this.configService.customFields.Product, ctx),
|
|
330
|
-
};
|
|
331
|
-
})),
|
|
332
|
-
customFields,
|
|
333
|
-
};
|
|
334
|
-
const createdProductId = existingProduct
|
|
335
|
-
? await this.fastImporter.updateProduct(existingProduct.id, {
|
|
336
|
-
...productData,
|
|
337
|
-
id: existingProduct.id,
|
|
338
|
-
})
|
|
339
|
-
: await this.fastImporter.createProduct(productData);
|
|
340
|
-
for (const facet of product.facets) {
|
|
341
|
-
const facetMainTranslation = this.getTranslationByCodeOrFirst(facet.translations, ctx.languageCode);
|
|
342
|
-
const code = (0, normalize_string_1.normalizeString)(facetMainTranslation.facet, '-', facetMainTranslation.languageCode);
|
|
343
|
-
const valueCode = (0, normalize_string_1.normalizeString)(facetMainTranslation.value, '-', facetMainTranslation.languageCode);
|
|
344
|
-
const facetValueId = await this.createFacetAndValue(facet, code, valueCode);
|
|
345
|
-
await this.fastImporter.addFacetValueToProduct(createdProductId, facetValueId);
|
|
346
|
-
}
|
|
347
|
-
const optionsMap = {};
|
|
348
|
-
for (const [optionGroup, optionGroupIndex] of product.optionGroups.map((group, i) => [group, i])) {
|
|
349
|
-
const optionGroupMainTranslation = this.getTranslationByCodeOrFirst(optionGroup.translations, ctx.languageCode);
|
|
350
|
-
const code = (0, normalize_string_1.normalizeString)(`${productMainTranslation.name}-${optionGroupMainTranslation.name}`, '-', optionGroupMainTranslation.languageCode);
|
|
351
|
-
const productOptionsGroups = await this.optionGroupService.getOptionGroupsByProductId(ctx, createdProductId);
|
|
352
|
-
const previousCode = (0, normalize_string_1.normalizeString)(`${productNameHasChanged?.previousName}-${optionGroupMainTranslation.name}`, '-', optionGroupMainTranslation.languageCode);
|
|
353
|
-
const foundOptionGroup = productOptionsGroups.find((group) => group.code === previousCode);
|
|
354
|
-
let groupId = foundOptionGroup
|
|
355
|
-
? await this.fastImporter.updateProductOptionGroup({
|
|
356
|
-
id: foundOptionGroup.id,
|
|
357
|
-
code,
|
|
358
|
-
options: optionGroupMainTranslation.values.map((name) => ({})),
|
|
359
|
-
translations: optionGroup.translations
|
|
360
|
-
.map((translation) => {
|
|
361
|
-
return {
|
|
362
|
-
languageCode: translation.languageCode,
|
|
363
|
-
name: translation.name,
|
|
364
|
-
};
|
|
365
|
-
})
|
|
366
|
-
.filter((t) => t.name),
|
|
367
|
-
})
|
|
368
|
-
: await this.fastImporter.createProductOptionGroup({
|
|
369
|
-
code,
|
|
370
|
-
options: optionGroupMainTranslation.values.map((name) => ({})),
|
|
371
|
-
translations: optionGroup.translations
|
|
372
|
-
.map((translation) => {
|
|
373
|
-
return {
|
|
374
|
-
languageCode: translation.languageCode,
|
|
375
|
-
name: translation.name,
|
|
376
|
-
};
|
|
377
|
-
})
|
|
378
|
-
.filter((t) => t.name),
|
|
379
|
-
});
|
|
380
|
-
for (const [optionIndex, value] of optionGroupMainTranslation.values.map((val, index) => [index, val])) {
|
|
381
|
-
const createdOptionId = await this.fastImporter.createProductOption({
|
|
382
|
-
productOptionGroupId: groupId,
|
|
383
|
-
code: (0, normalize_string_1.normalizeString)(value, '-', optionGroupMainTranslation.languageCode),
|
|
384
|
-
translations: optionGroup.translations
|
|
385
|
-
.map((translation) => {
|
|
386
|
-
return {
|
|
387
|
-
languageCode: translation.languageCode,
|
|
388
|
-
name: translation.values[optionIndex],
|
|
389
|
-
};
|
|
390
|
-
})
|
|
391
|
-
.filter((t) => t.name),
|
|
392
|
-
});
|
|
393
|
-
optionsMap[`${optionGroupIndex}_${value}`] = createdOptionId;
|
|
394
|
-
}
|
|
395
|
-
await this.fastImporter.addOptionGroupToProduct(createdProductId, groupId);
|
|
396
|
-
}
|
|
397
|
-
for (const variant of variants) {
|
|
398
|
-
const variantMainTranslation = this.getTranslationByCodeOrFirst(variant.translations, ctx.languageCode);
|
|
399
|
-
const assetsToImport = (0, lodash_1.compact)(variant.assetPaths.map((assetPath, idx) => {
|
|
400
|
-
const assetID = variant.assetsJson?.[idx]?.id;
|
|
401
|
-
if (assetPath.startsWith('http://') || assetPath.startsWith('https://')) {
|
|
402
|
-
return {
|
|
403
|
-
url: assetPath,
|
|
404
|
-
name: variant.assetsJson?.[idx]?.name || undefined,
|
|
405
|
-
id: assetID && this.allAssets.find((a) => a.id === assetID)
|
|
406
|
-
? variant.assetsJson?.[idx]?.id
|
|
407
|
-
: undefined,
|
|
408
|
-
};
|
|
409
|
-
}
|
|
410
|
-
}));
|
|
411
|
-
const createVariantAssets = await this.assetImporter.getAssets(assetsToImport);
|
|
412
|
-
const variantAssets = createVariantAssets.assets;
|
|
413
|
-
if (createVariantAssets.errors.length) {
|
|
414
|
-
errors = errors.concat(createVariantAssets.errors);
|
|
415
|
-
}
|
|
416
|
-
let facetValueIds = [];
|
|
417
|
-
if (0 < variant.facets.length) {
|
|
418
|
-
facetValueIds = await this.getFacetValueIds(ctx, variant.facets, languageCode);
|
|
419
|
-
}
|
|
420
|
-
const variantCustomFields = await this.processCustomFieldValues(variantMainTranslation.customFields, this.configService.customFields.ProductVariant, ctx);
|
|
421
|
-
const optionIds = variantMainTranslation.optionValues.map((v, index) => optionsMap[`${index}_${v}`]);
|
|
422
|
-
const existingVariant = await this.connection.getRepository(ctx, core_1.ProductVariant).findOne({
|
|
423
|
-
where: {
|
|
424
|
-
sku: variant.sku,
|
|
425
|
-
},
|
|
426
|
-
withDeleted: true,
|
|
427
|
-
relations: ['facetValues', 'facetValues.facet', 'assets', 'featuredAsset'],
|
|
428
|
-
});
|
|
429
|
-
if (existingVariant?.deletedAt) {
|
|
430
|
-
existingVariant.deletedAt = null;
|
|
431
|
-
try {
|
|
432
|
-
await this.connection.getRepository(ctx, core_1.ProductVariant).save([existingVariant]);
|
|
433
|
-
}
|
|
434
|
-
catch (error) {
|
|
435
|
-
errors.push(`Error while restoring the variant: ${error}`);
|
|
436
|
-
core_1.Logger.error('Error while restoring the variant:', 'ProductImporter', error?.stack);
|
|
437
|
-
}
|
|
438
|
-
}
|
|
439
|
-
if (existingVariant && updatingStrategy === 'replace') {
|
|
440
|
-
// Remove all assets from the product
|
|
441
|
-
await this.fastImporter.removeAllAssetsFromVariant(existingVariant);
|
|
442
|
-
}
|
|
443
|
-
const variantData = {
|
|
444
|
-
productId: createdProductId,
|
|
445
|
-
facetValueIds: updatingStrategy === 'replace' ? [] : undefined,
|
|
446
|
-
featuredAssetId: existingVariant?.featuredAsset
|
|
447
|
-
? undefined
|
|
448
|
-
: variantAssets.length
|
|
449
|
-
? variantAssets[0].id
|
|
450
|
-
: undefined,
|
|
451
|
-
assetIds: variantAssets.map((a) => a.id),
|
|
452
|
-
sku: variant.sku,
|
|
453
|
-
taxCategoryId: this.getMatchingTaxCategoryId(variant.taxCategory, taxCategories.items),
|
|
454
|
-
stockOnHand: variant.stockOnHand,
|
|
455
|
-
trackInventory: variant.trackInventory,
|
|
456
|
-
optionIds,
|
|
457
|
-
translations: await Promise.all(variant.translations.map(async (translation) => {
|
|
458
|
-
const productTranslation = product.translations.find((t) => t.languageCode === translation.languageCode);
|
|
459
|
-
if (!productTranslation) {
|
|
460
|
-
throw new core_1.InternalServerError(`No translation '${translation.languageCode}' for product with slug '${productMainTranslation.slug}'`);
|
|
461
|
-
}
|
|
462
|
-
return {
|
|
463
|
-
languageCode: translation.languageCode,
|
|
464
|
-
name: [productTranslation.name, ...translation.optionValues].join(' '),
|
|
465
|
-
customFields: await this.processCustomFieldValues(translation.customFields, this.configService.customFields.ProductVariant, ctx),
|
|
466
|
-
};
|
|
467
|
-
})),
|
|
468
|
-
price: Math.round(variant.price * 100),
|
|
469
|
-
customFields: variantCustomFields,
|
|
470
|
-
};
|
|
471
|
-
const createdVariantId = existingVariant
|
|
472
|
-
? await this.fastImporter.updateProductVariant({ ...variantData, id: existingVariant.id })
|
|
473
|
-
: await this.fastImporter.createProductVariant(variantData);
|
|
474
|
-
for (const facet of variant.facets) {
|
|
475
|
-
const facetMainTranslation = this.getTranslationByCodeOrFirst(facet.translations, ctx.languageCode);
|
|
476
|
-
const code = (0, normalize_string_1.normalizeString)(facetMainTranslation.facet, '-', facetMainTranslation.languageCode);
|
|
477
|
-
const valueCode = (0, normalize_string_1.normalizeString)(facetMainTranslation.value, '-', facetMainTranslation.languageCode);
|
|
478
|
-
const facetValueId = await this.createFacetAndValue(facet, code, valueCode);
|
|
479
|
-
await this.fastImporter.addFacetValueToProductVariant(createdVariantId, facetValueId);
|
|
480
|
-
}
|
|
481
|
-
}
|
|
482
|
-
if (existingProduct) {
|
|
483
|
-
const productAfterUpdate = (await this.getProductByIdWithRelations(ctx, createdProductId)); // We know that the product exists, so we can safely cast it to Product
|
|
484
|
-
const existingProductHash = (0, crypto_1.createHash)('md5')
|
|
485
|
-
.update(JSON.stringify((0, remove_update_at_fields_1.removeUpdatedAtFields)(existingProduct)))
|
|
486
|
-
.digest('hex');
|
|
487
|
-
const productAfterUpdateHash = (0, crypto_1.createHash)('md5')
|
|
488
|
-
.update(JSON.stringify((0, remove_update_at_fields_1.removeUpdatedAtFields)(productAfterUpdate)))
|
|
489
|
-
.digest('hex');
|
|
490
|
-
// Update product updatedAt to existing product updatedAt
|
|
491
|
-
if (existingProductHash === productAfterUpdateHash) {
|
|
492
|
-
await this.connection.getRepository(ctx, core_1.Product).update(productAfterUpdate.id, {
|
|
493
|
-
updatedAt: existingProduct.updatedAt,
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
imported++;
|
|
498
|
-
onProgress({
|
|
499
|
-
processed: 0,
|
|
500
|
-
imported,
|
|
501
|
-
errors,
|
|
502
|
-
currentProduct: productMainTranslation.name,
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
return errors;
|
|
506
|
-
}
|
|
507
|
-
async getFacetValueIds(ctx, facets, languageCode) {
|
|
508
|
-
const facetValueIds = [];
|
|
509
|
-
for (const item of facets) {
|
|
510
|
-
const itemMainTranslation = this.getTranslationByCodeOrFirst(item.translations, languageCode);
|
|
511
|
-
const facetName = itemMainTranslation.facet;
|
|
512
|
-
const valueName = itemMainTranslation.value;
|
|
513
|
-
let facetEntity;
|
|
514
|
-
const cachedFacet = this.facetMap.get(facetName);
|
|
515
|
-
if (cachedFacet) {
|
|
516
|
-
facetEntity = cachedFacet;
|
|
517
|
-
}
|
|
518
|
-
else {
|
|
519
|
-
const existing = await this.facetService.findByCode(ctx, (0, normalize_string_1.normalizeString)(facetName, '-', languageCode), languageCode);
|
|
520
|
-
if (existing) {
|
|
521
|
-
facetEntity = existing;
|
|
522
|
-
}
|
|
523
|
-
else {
|
|
524
|
-
facetEntity = await this.facetService.create(ctx, {
|
|
525
|
-
isPrivate: false,
|
|
526
|
-
code: (0, normalize_string_1.normalizeString)(facetName, '-', languageCode),
|
|
527
|
-
translations: item.translations.map((translation) => {
|
|
528
|
-
return {
|
|
529
|
-
languageCode: translation.languageCode,
|
|
530
|
-
name: translation.facet,
|
|
531
|
-
};
|
|
532
|
-
}),
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
this.facetMap.set(facetName, facetEntity);
|
|
536
|
-
}
|
|
537
|
-
let facetValueEntity;
|
|
538
|
-
const facetValueMapKey = `${facetName}:${valueName}`;
|
|
539
|
-
const cachedFacetValue = this.facetValueMap.get(facetValueMapKey);
|
|
540
|
-
if (cachedFacetValue) {
|
|
541
|
-
facetValueEntity = cachedFacetValue;
|
|
542
|
-
}
|
|
543
|
-
else {
|
|
544
|
-
const existing = facetEntity.values.find((v) => (0, normalize_string_1.normalizeString)(v.name, '-') === (0, normalize_string_1.normalizeString)(valueName, '-', languageCode));
|
|
545
|
-
if (existing) {
|
|
546
|
-
facetValueEntity = existing;
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
facetValueEntity = await this.facetValueService.create(ctx, facetEntity, {
|
|
550
|
-
code: (0, normalize_string_1.normalizeString)(valueName, '-', languageCode),
|
|
551
|
-
translations: item.translations.map((translation) => {
|
|
552
|
-
return {
|
|
553
|
-
languageCode: translation.languageCode,
|
|
554
|
-
name: translation.value,
|
|
555
|
-
};
|
|
556
|
-
}),
|
|
557
|
-
});
|
|
558
|
-
}
|
|
559
|
-
this.facetValueMap.set(facetValueMapKey, facetValueEntity);
|
|
560
|
-
}
|
|
561
|
-
facetValueIds.push(facetValueEntity.id);
|
|
562
|
-
}
|
|
563
|
-
return facetValueIds;
|
|
564
|
-
}
|
|
565
|
-
async processCustomFieldValues(customFields, config, ctx) {
|
|
566
|
-
function parseBoolean(input) {
|
|
567
|
-
if (input == null) {
|
|
568
|
-
return false;
|
|
569
|
-
}
|
|
570
|
-
switch (input.toLowerCase()) {
|
|
571
|
-
case 'true':
|
|
572
|
-
case '1':
|
|
573
|
-
case 'yes':
|
|
574
|
-
return true;
|
|
575
|
-
default:
|
|
576
|
-
return false;
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
const processed = {};
|
|
580
|
-
for (const fieldDef of config) {
|
|
581
|
-
const foundCustomFieldType = (0, lodash_1.find)(this.customFieldTypes, (_type, key) => key.split(':')[1] === fieldDef.name);
|
|
582
|
-
const value = customFields[fieldDef.name];
|
|
583
|
-
if (fieldDef.list === true) {
|
|
584
|
-
processed[fieldDef.name] = value?.split('|').filter((val) => val.trim() !== '');
|
|
585
|
-
}
|
|
586
|
-
else if (fieldDef.type === 'boolean') {
|
|
587
|
-
processed[fieldDef.name] = parseBoolean(value);
|
|
588
|
-
}
|
|
589
|
-
else if (fieldDef.type === 'relation') {
|
|
590
|
-
if (foundCustomFieldType === 'asset') {
|
|
591
|
-
// Asset if of type json
|
|
592
|
-
if ((0, lodash_1.startsWith)(value, '{') || (0, lodash_1.startsWith)(value, '[')) {
|
|
593
|
-
if (typeof value === 'string') {
|
|
594
|
-
const assetValue = JSON.parse(value.replace(/'/g, '"').replace(/\s+/g, ' ').replace(/,\s*'/g, ", '"));
|
|
595
|
-
if (Array.isArray(assetValue)) {
|
|
596
|
-
assetValue.forEach(async (asset) => {
|
|
597
|
-
if (asset.url.startsWith('http://') || asset.url.startsWith('https://')) {
|
|
598
|
-
const createdAssets = await this.assetImporter.getAssets([], ctx);
|
|
599
|
-
processed[fieldDef.name] = createdAssets.assets[0]?.id;
|
|
600
|
-
}
|
|
601
|
-
const foundAsset = this.allAssets.find((a) => a.id === asset.id);
|
|
602
|
-
if (foundAsset && foundAsset.name !== asset.name) {
|
|
603
|
-
this.connection.getRepository(ctx, core_1.Asset).update(foundAsset.id, {
|
|
604
|
-
name: asset.name,
|
|
605
|
-
});
|
|
606
|
-
}
|
|
607
|
-
});
|
|
608
|
-
}
|
|
609
|
-
else {
|
|
610
|
-
if (assetValue.url.startsWith('http://') || assetValue.url.startsWith('https://')) {
|
|
611
|
-
const createdAssets = await this.assetImporter.getAssets([assetValue], ctx);
|
|
612
|
-
processed[fieldDef.name] = createdAssets.assets[0]?.id;
|
|
613
|
-
}
|
|
614
|
-
const foundAsset = this.allAssets.find((a) => a.id === assetValue.id);
|
|
615
|
-
if (foundAsset && foundAsset.name !== assetValue.name) {
|
|
616
|
-
this.connection.getRepository(ctx, core_1.Asset).update(foundAsset.id, {
|
|
617
|
-
name: assetValue.name,
|
|
618
|
-
});
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
// Asset if of type string
|
|
623
|
-
}
|
|
624
|
-
else {
|
|
625
|
-
if (value.startsWith('http://') || value.startsWith('https://')) {
|
|
626
|
-
// If value is an external URL, we need to import the asset
|
|
627
|
-
const createdAssets = await this.assetImporter.getAssets([{ url: value }], ctx);
|
|
628
|
-
processed[fieldDef.name] = createdAssets.assets[0]?.id;
|
|
629
|
-
}
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
else {
|
|
633
|
-
processed[fieldDef.name] = value ? value : undefined;
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
else {
|
|
637
|
-
if ((0, lodash_1.startsWith)(value, '{') || (0, lodash_1.startsWith)(value, '[')) {
|
|
638
|
-
if (typeof value === 'string') {
|
|
639
|
-
processed[fieldDef.name] = value.replace(/'/g, '"').replace(/\s+/g, ' ');
|
|
640
|
-
}
|
|
641
|
-
else {
|
|
642
|
-
processed[fieldDef.name] = JSON.stringify(value)
|
|
643
|
-
.replace(/'/g, '"')
|
|
644
|
-
.replace(/\s+/g, ' ')
|
|
645
|
-
.replace(/,\s*'/g, ", '");
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
else {
|
|
649
|
-
// Default to string
|
|
650
|
-
processed[fieldDef.name] = value ? value : undefined;
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
return processed;
|
|
655
|
-
}
|
|
656
|
-
/**
|
|
657
|
-
* Attempts to match a TaxCategory entity against the name supplied in the import table. If no matches
|
|
658
|
-
* are found, the first TaxCategory id is returned.
|
|
659
|
-
*/
|
|
660
|
-
getMatchingTaxCategoryId(name, taxCategories) {
|
|
661
|
-
if (this.taxCategoryMatches[name]) {
|
|
662
|
-
return this.taxCategoryMatches[name];
|
|
663
|
-
}
|
|
664
|
-
const regex = new RegExp(name, 'i');
|
|
665
|
-
const found = taxCategories.find((tc) => !!tc.name.match(regex));
|
|
666
|
-
const match = found ? found : taxCategories[0];
|
|
667
|
-
if (!match) {
|
|
668
|
-
throw new core_1.InternalServerError(`No TaxCategory found for name '${name}'`);
|
|
669
|
-
}
|
|
670
|
-
this.taxCategoryMatches[name] = match.id;
|
|
671
|
-
return match.id;
|
|
672
|
-
}
|
|
673
|
-
getTranslationByCodeOrFirst(translations, languageCode) {
|
|
674
|
-
let translation = translations.find((t) => t.languageCode === languageCode);
|
|
675
|
-
if (!translation) {
|
|
676
|
-
translation = translations[0];
|
|
677
|
-
}
|
|
678
|
-
return translation;
|
|
679
|
-
}
|
|
680
|
-
async createFacetAndValue(facet, facetCode, valueCode) {
|
|
681
|
-
const facetId = await this.fastImporter.createFacet({
|
|
682
|
-
code: facetCode,
|
|
683
|
-
isPrivate: false,
|
|
684
|
-
translations: facet.translations.map((translation) => {
|
|
685
|
-
return {
|
|
686
|
-
languageCode: translation.languageCode,
|
|
687
|
-
name: translation.facet,
|
|
688
|
-
value: translation.value,
|
|
689
|
-
};
|
|
690
|
-
}),
|
|
691
|
-
});
|
|
692
|
-
const facetValueId = await this.fastImporter.createFacetValue({
|
|
693
|
-
facetId,
|
|
694
|
-
code: valueCode,
|
|
695
|
-
translations: facet.translations.map((translation) => {
|
|
696
|
-
return {
|
|
697
|
-
languageCode: translation.languageCode,
|
|
698
|
-
name: translation.value,
|
|
699
|
-
};
|
|
700
|
-
}),
|
|
701
|
-
});
|
|
702
|
-
return facetValueId;
|
|
703
|
-
}
|
|
704
|
-
async getProductByIdWithRelations(ctx, id) {
|
|
705
|
-
if (!id) {
|
|
706
|
-
return undefined;
|
|
707
|
-
}
|
|
708
|
-
const product = (await this.connection.getRepository(ctx, core_1.Product).findOne({
|
|
709
|
-
where: { id },
|
|
710
|
-
withDeleted: true,
|
|
711
|
-
})) ?? undefined;
|
|
712
|
-
if (product) {
|
|
713
|
-
const [facetValues, optionGroups, variants, assets, featuredAsset] = await Promise.all([
|
|
714
|
-
this.connection
|
|
715
|
-
.getRepository(ctx, core_1.FacetValue)
|
|
716
|
-
.find({ where: { products: { id: product.id } }, relations: ['facet'] }),
|
|
717
|
-
this.connection
|
|
718
|
-
.getRepository(ctx, core_1.ProductOptionGroup)
|
|
719
|
-
.find({ where: { product: { id: product.id } }, relations: ['options'] }),
|
|
720
|
-
this.connection
|
|
721
|
-
.getRepository(ctx, core_1.ProductVariant)
|
|
722
|
-
.find({ where: { product: { id: product.id } }, relations: ['facetValues'] }),
|
|
723
|
-
this.connection.getRepository(ctx, core_1.ProductAsset).find({ where: { productId: product.id } }),
|
|
724
|
-
this.connection
|
|
725
|
-
.getRepository(ctx, core_1.Asset)
|
|
726
|
-
.findOne({ where: { id: product.featuredAssetId } }),
|
|
727
|
-
]);
|
|
728
|
-
product.facetValues = facetValues;
|
|
729
|
-
product.optionGroups = optionGroups;
|
|
730
|
-
product.variants = variants;
|
|
731
|
-
product.assets = assets;
|
|
732
|
-
featuredAsset && (product.featuredAsset = featuredAsset);
|
|
733
|
-
}
|
|
734
|
-
return product;
|
|
735
|
-
}
|
|
736
|
-
};
|
|
737
|
-
ProductImporter = __decorate([
|
|
738
|
-
(0, common_1.Injectable)(),
|
|
739
|
-
__metadata("design:paramtypes", [core_1.TransactionalConnection,
|
|
740
|
-
core_1.ConfigService,
|
|
741
|
-
import_parser_1.ImportParser,
|
|
742
|
-
asset_importer_1.ExtendedAssetImporter,
|
|
743
|
-
extended_fast_importer_service_1.ExtendedFastImporterService,
|
|
744
|
-
core_1.TranslatableSaver,
|
|
745
|
-
core_1.ProductService,
|
|
746
|
-
core_1.ChannelService,
|
|
747
|
-
core_1.FacetService,
|
|
748
|
-
core_1.FacetValueService,
|
|
749
|
-
core_1.TaxCategoryService,
|
|
750
|
-
core_1.AssetService,
|
|
751
|
-
core_1.ProductOptionGroupService])
|
|
752
|
-
], ProductImporter);
|
|
753
|
-
exports.ProductImporter = ProductImporter;
|