@cloudcommerce/cli 2.38.0 → 2.39.0
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/lib/ext/import-feed.js +125 -4
- package/package.json +4 -3
package/lib/ext/import-feed.js
CHANGED
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
argv, fs, echo, sleep,
|
|
3
3
|
} from 'zx';
|
|
4
4
|
import { XMLParser } from 'fast-xml-parser';
|
|
5
|
+
import { imageSize } from 'image-size';
|
|
5
6
|
import api from '@cloudcommerce/api';
|
|
6
7
|
|
|
7
8
|
const clearAccents = (str) => {
|
|
@@ -12,6 +13,65 @@ const slugify = (name) => {
|
|
|
12
13
|
.replace(/[\s\n/]/g, '-')
|
|
13
14
|
.replace(/[^a-z0-9_-]/ig, '');
|
|
14
15
|
};
|
|
16
|
+
const uploadPicture = async (downloadUrl, authHeaders) => {
|
|
17
|
+
const downloadRes = await fetch(downloadUrl);
|
|
18
|
+
if (!downloadRes.ok) {
|
|
19
|
+
throw new Error(`Failed downloading ${downloadUrl} with status ${downloadRes.status}`);
|
|
20
|
+
}
|
|
21
|
+
const contentType = downloadRes.headers.get('content-type');
|
|
22
|
+
if (!contentType) {
|
|
23
|
+
throw new Error(`No mime type returned for ${downloadUrl}`);
|
|
24
|
+
}
|
|
25
|
+
const imageBuffer = await downloadRes.arrayBuffer();
|
|
26
|
+
const imageBlob = new Blob([imageBuffer], { type: contentType });
|
|
27
|
+
const formData = new FormData();
|
|
28
|
+
const fileName = downloadUrl.split('/').pop() || 'image.jpg';
|
|
29
|
+
formData.append('file', imageBlob, fileName);
|
|
30
|
+
const uploadRes = await fetch('https://ecomplus.app/api/storage/upload.json', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
body: formData,
|
|
33
|
+
headers: authHeaders,
|
|
34
|
+
});
|
|
35
|
+
if (!uploadRes.ok) {
|
|
36
|
+
const err = new Error(`Failed uploading ${downloadUrl} with ${uploadRes.status}`);
|
|
37
|
+
err.statusCode = uploadRes.status;
|
|
38
|
+
throw err;
|
|
39
|
+
}
|
|
40
|
+
const data = await uploadRes.json();
|
|
41
|
+
const { width, height } = imageSize(Buffer.from(imageBuffer));
|
|
42
|
+
const { picture } = data;
|
|
43
|
+
if (width && height) {
|
|
44
|
+
picture.zoom.size = `${width}x${height}`;
|
|
45
|
+
Object.keys(picture).forEach((thumb) => {
|
|
46
|
+
if (thumb === 'zoom' || !picture[thumb]) return;
|
|
47
|
+
const px = parseInt(picture[thumb].size, 10);
|
|
48
|
+
if (px) {
|
|
49
|
+
if (px >= Math.max(width, height)) {
|
|
50
|
+
picture[thumb].size = picture.zoom.size;
|
|
51
|
+
} else {
|
|
52
|
+
picture[thumb].size = width > height
|
|
53
|
+
? px + 'x' + Math.round((height * px) / width)
|
|
54
|
+
: Math.round((width * px) / height) + 'x' + px;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
delete picture[thumb].size;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
} else {
|
|
61
|
+
Object.keys(picture).forEach((thumb) => {
|
|
62
|
+
if (!picture[thumb]) return;
|
|
63
|
+
delete picture[thumb].size;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (fileName) {
|
|
67
|
+
const alt = fileName.replace(/\.[^.]+$/, '');
|
|
68
|
+
Object.keys(picture).forEach((thumb) => {
|
|
69
|
+
if (!picture[thumb]) return;
|
|
70
|
+
picture[thumb].alt = alt;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return picture;
|
|
74
|
+
};
|
|
15
75
|
const importFeed = async () => {
|
|
16
76
|
const feedFilepath = typeof argv.feed === 'string' ? argv.feed : '';
|
|
17
77
|
if (!feedFilepath) {
|
|
@@ -30,6 +90,20 @@ const importFeed = async () => {
|
|
|
30
90
|
await echo`The XML file does not appear to be a valid RSS 2.0 product feed`;
|
|
31
91
|
return process.exit(1);
|
|
32
92
|
}
|
|
93
|
+
const ecomAuthHeaders = {
|
|
94
|
+
'X-Store-ID': process.env.ECOM_STORE_ID,
|
|
95
|
+
'X-My-ID': process.env.ECOM_AUTHENTICATION_ID,
|
|
96
|
+
};
|
|
97
|
+
const { ECOM_ACCESS_TOKEN, ECOM_API_KEY } = process.env;
|
|
98
|
+
if (ECOM_ACCESS_TOKEN) {
|
|
99
|
+
ecomAuthHeaders['X-Access-Token'] = ECOM_ACCESS_TOKEN;
|
|
100
|
+
} else {
|
|
101
|
+
const { data } = await api.post('authenticate', {
|
|
102
|
+
_id: process.env.ECOM_AUTHENTICATION_ID,
|
|
103
|
+
api_key: ECOM_API_KEY,
|
|
104
|
+
});
|
|
105
|
+
ecomAuthHeaders['X-Access-Token'] = data.access_token;
|
|
106
|
+
}
|
|
33
107
|
const categoryNames = [];
|
|
34
108
|
const brandNames = [];
|
|
35
109
|
items.forEach(async (item) => {
|
|
@@ -126,6 +200,7 @@ const importFeed = async () => {
|
|
|
126
200
|
for (let ii = 0; ii < productItems.length; ii++) {
|
|
127
201
|
if (result.find(({ sku }) => sku === productItems[ii]['g:id'])) {
|
|
128
202
|
productItems.splice(ii, 1);
|
|
203
|
+
ii -= 1;
|
|
129
204
|
}
|
|
130
205
|
}
|
|
131
206
|
}
|
|
@@ -172,19 +247,36 @@ const importFeed = async () => {
|
|
|
172
247
|
});
|
|
173
248
|
if (Object.keys(specifications).length) {
|
|
174
249
|
productOrVatiation.specifications = specifications;
|
|
250
|
+
} else if (!isProductItem) {
|
|
251
|
+
productOrVatiation.specifications = {
|
|
252
|
+
size: [{ text: 'Único' }],
|
|
253
|
+
};
|
|
175
254
|
}
|
|
176
255
|
};
|
|
177
|
-
|
|
256
|
+
const listItemRemoteImages = (item) => {
|
|
257
|
+
const remoteImages = Array.isArray(item['g:additional_image_link'])
|
|
258
|
+
? item['g:additional_image_link']
|
|
259
|
+
: [];
|
|
260
|
+
if (item['g:image_link']) {
|
|
261
|
+
remoteImages.unshift(item['g:image_link']);
|
|
262
|
+
}
|
|
263
|
+
if (typeof item['g:additional_image_link'] === 'string') {
|
|
264
|
+
remoteImages.push(item['g:additional_image_link']);
|
|
265
|
+
}
|
|
266
|
+
return remoteImages;
|
|
267
|
+
};
|
|
178
268
|
for (let i = 0; i < productItems.length; i++) {
|
|
179
269
|
const item = productItems[i];
|
|
180
270
|
const { 'g:id': sku, 'g:title': name } = item;
|
|
181
271
|
if (!sku || !name) continue;
|
|
272
|
+
await echo`\n${new Date().toISOString()}`;
|
|
273
|
+
await echo`${(productItems.length - i)} products to import\n`;
|
|
182
274
|
await echo`SKU: ${sku}`;
|
|
183
275
|
const product = {
|
|
184
276
|
sku,
|
|
185
277
|
name,
|
|
186
278
|
slug: item['g:link']
|
|
187
|
-
? new URL(item['g:link']).pathname.substring(1)
|
|
279
|
+
? new URL(item['g:link']).pathname.substring(1).toLowerCase()
|
|
188
280
|
: slugify(name),
|
|
189
281
|
condition: item['g:condition'],
|
|
190
282
|
};
|
|
@@ -203,6 +295,7 @@ const importFeed = async () => {
|
|
|
203
295
|
product.brands = [brand];
|
|
204
296
|
}
|
|
205
297
|
setStockAndPrices(product, item);
|
|
298
|
+
const remoteImages = listItemRemoteImages(item);
|
|
206
299
|
const variationItems = items.filter((_item) => {
|
|
207
300
|
return _item['g:item_group_id'] === sku;
|
|
208
301
|
});
|
|
@@ -222,13 +315,41 @@ const importFeed = async () => {
|
|
|
222
315
|
totalQuantity += variation.quantity || 0;
|
|
223
316
|
setSpecifications(variation, variationItem);
|
|
224
317
|
product.variations.push(variation);
|
|
318
|
+
const variationRemoteImages = listItemRemoteImages(variationItem);
|
|
319
|
+
variationRemoteImages.forEach((imageUrl) => {
|
|
320
|
+
if (!remoteImages.includes(imageUrl)) remoteImages.push(imageUrl);
|
|
321
|
+
});
|
|
225
322
|
});
|
|
226
323
|
product.quantity = totalQuantity;
|
|
227
324
|
} else {
|
|
228
325
|
setSpecifications(product, item, true);
|
|
229
326
|
}
|
|
230
|
-
|
|
231
|
-
|
|
327
|
+
if (remoteImages.length) {
|
|
328
|
+
product.pictures = [];
|
|
329
|
+
}
|
|
330
|
+
for (let ii = 0; ii < remoteImages.length; ii++) {
|
|
331
|
+
const imageUrl = remoteImages[ii];
|
|
332
|
+
if (imageUrl) {
|
|
333
|
+
let retries = 0;
|
|
334
|
+
while (retries < 4) {
|
|
335
|
+
try {
|
|
336
|
+
const picture = await uploadPicture(imageUrl, ecomAuthHeaders);
|
|
337
|
+
product.pictures?.push(picture);
|
|
338
|
+
break;
|
|
339
|
+
} catch (err) {
|
|
340
|
+
// eslint-disable-next-line no-console
|
|
341
|
+
console.error(err);
|
|
342
|
+
if (err.statusCode < 500) {
|
|
343
|
+
throw err;
|
|
344
|
+
}
|
|
345
|
+
retries += 1;
|
|
346
|
+
await sleep(4000);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
await echo`${JSON.stringify(product, null, 2)}\n`;
|
|
352
|
+
await api.post('products', product);
|
|
232
353
|
}
|
|
233
354
|
return echo``;
|
|
234
355
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudcommerce/cli",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.39.0",
|
|
5
5
|
"description": "e-com.plus Cloud Commerce CLI tools",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cloudcommerce": "./bin/run.mjs"
|
|
@@ -33,11 +33,12 @@
|
|
|
33
33
|
"@fastify/deepmerge": "^2.0.2",
|
|
34
34
|
"dotenv": "^16.4.7",
|
|
35
35
|
"fast-xml-parser": "^5.0.8",
|
|
36
|
+
"image-size": "^1.2.0",
|
|
36
37
|
"libsodium-wrappers": "^0.7.15",
|
|
37
38
|
"md5": "^2.3.0",
|
|
38
39
|
"typescript": "~5.8.2",
|
|
39
|
-
"zx": "^8.
|
|
40
|
-
"@cloudcommerce/api": "2.
|
|
40
|
+
"zx": "^8.4.0",
|
|
41
|
+
"@cloudcommerce/api": "2.39.0"
|
|
41
42
|
},
|
|
42
43
|
"scripts": {
|
|
43
44
|
"build": "bash ../../scripts/build-lib.sh"
|