@cloudcommerce/cli 2.43.1 → 2.44.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 +98 -19
- package/package.json +3 -2
package/lib/ext/import-feed.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { extname } from 'node:path';
|
|
1
2
|
import {
|
|
2
3
|
argv, fs, echo, sleep,
|
|
3
4
|
} from 'zx';
|
|
4
5
|
import { XMLParser } from 'fast-xml-parser';
|
|
6
|
+
import { parse } from 'csv-parse/sync';
|
|
5
7
|
import { imageSize } from 'image-size';
|
|
6
8
|
import api from '@cloudcommerce/api';
|
|
7
9
|
|
|
@@ -14,13 +16,24 @@ const slugify = (name) => {
|
|
|
14
16
|
.replace(/[^a-z0-9_-]/ig, '');
|
|
15
17
|
};
|
|
16
18
|
const uploadPicture = async (downloadUrl, authHeaders) => {
|
|
17
|
-
const downloadRes = await fetch(downloadUrl
|
|
19
|
+
const downloadRes = await fetch(downloadUrl, {
|
|
20
|
+
method: 'GET',
|
|
21
|
+
redirect: 'follow',
|
|
22
|
+
headers: {
|
|
23
|
+
'User-Agent': 'Mozilla/5.0 (compatible; E-commerce Bot/1.0)',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
18
26
|
if (!downloadRes.ok) {
|
|
19
|
-
|
|
27
|
+
const msg = `Failed downloading ${downloadUrl} with status ${downloadRes.status}`;
|
|
28
|
+
const err = new Error(msg);
|
|
29
|
+
err.statusCode = downloadRes.status || 0;
|
|
30
|
+
throw err;
|
|
20
31
|
}
|
|
21
32
|
const contentType = downloadRes.headers.get('content-type');
|
|
22
33
|
if (!contentType) {
|
|
23
|
-
|
|
34
|
+
const err = new Error(`No mime type returned for ${downloadUrl}`);
|
|
35
|
+
err.statusCode = 0;
|
|
36
|
+
throw err;
|
|
24
37
|
}
|
|
25
38
|
const imageBuffer = await downloadRes.arrayBuffer();
|
|
26
39
|
const imageBlob = new Blob([imageBuffer], { type: contentType });
|
|
@@ -72,29 +85,83 @@ const uploadPicture = async (downloadUrl, authHeaders) => {
|
|
|
72
85
|
}
|
|
73
86
|
return picture;
|
|
74
87
|
};
|
|
88
|
+
const mapCSVToFeed = (item) => {
|
|
89
|
+
const basePrice = parseFloat(item['preco-cheio']?.replace(',', '.')) || 0;
|
|
90
|
+
const salePrice = parseFloat(item['preco-promocional']?.replace(',', '.')) || 0;
|
|
91
|
+
const isAvailable = item.ativo === 'S';
|
|
92
|
+
const quantity = Number(item['estoque-quantidade']) || 0;
|
|
93
|
+
return {
|
|
94
|
+
'g:id': item.sku,
|
|
95
|
+
'g:title': item.nome,
|
|
96
|
+
'g:description': item['descricao-completa'] || item['seo-tag-description'],
|
|
97
|
+
'g:image_link': item['imagem-1'],
|
|
98
|
+
'g:condition': item.usado === 'S' ? 'used' : 'new',
|
|
99
|
+
'g:shipping_weight': item['peso-em-kg']
|
|
100
|
+
? `${item['peso-em-kg']?.replace(',', '.')} kg`
|
|
101
|
+
: undefined,
|
|
102
|
+
'g:additional_image_link': [
|
|
103
|
+
item['imagem-2'],
|
|
104
|
+
item['imagem-3'],
|
|
105
|
+
item['imagem-4'],
|
|
106
|
+
item['imagem-5'],
|
|
107
|
+
].filter(Boolean),
|
|
108
|
+
'g:product_type': item['categoria-nome-nivel-1'],
|
|
109
|
+
'g:availability': (isAvailable && quantity > 0)
|
|
110
|
+
? 'in stock'
|
|
111
|
+
: 'out of stock',
|
|
112
|
+
'g:price': `${(basePrice || salePrice)} BRL`,
|
|
113
|
+
'g:sale_price': (salePrice > 0 && salePrice < basePrice)
|
|
114
|
+
? `${salePrice} BRL`
|
|
115
|
+
: undefined,
|
|
116
|
+
'g:brand': item.marca,
|
|
117
|
+
'g:gtin': item.gtin,
|
|
118
|
+
'g:mpn': item.mpn,
|
|
119
|
+
'available': isAvailable,
|
|
120
|
+
'visible': isAvailable,
|
|
121
|
+
'quantity': quantity,
|
|
122
|
+
};
|
|
123
|
+
};
|
|
75
124
|
const importFeed = async () => {
|
|
125
|
+
const {
|
|
126
|
+
ECOM_STORE_ID, ECOM_AUTHENTICATION_ID, ECOM_API_KEY, ECOM_ACCESS_TOKEN,
|
|
127
|
+
} = process.env;
|
|
76
128
|
const feedFilepath = typeof argv.feed === 'string' ? argv.feed : '';
|
|
77
129
|
if (!feedFilepath) {
|
|
78
130
|
await echo`You must specify XML file to import with --feed= argument`;
|
|
79
131
|
return process.exit(1);
|
|
80
132
|
}
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
133
|
+
const fileExtension = extname(feedFilepath).toLowerCase();
|
|
134
|
+
await echo`Importing ${fileExtension} file at ${feedFilepath}`;
|
|
135
|
+
await echo`Store ${ECOM_STORE_ID} with ${ECOM_AUTHENTICATION_ID}`;
|
|
136
|
+
let items = [];
|
|
137
|
+
if (fileExtension === '.xml') {
|
|
138
|
+
const parser = new XMLParser({
|
|
139
|
+
ignoreAttributes: false,
|
|
140
|
+
attributeNamePrefix: '',
|
|
141
|
+
});
|
|
142
|
+
const json = parser.parse(fs.readFileSync(argv.feed, 'utf8'));
|
|
143
|
+
items = json.rss?.channel?.item?.filter?.((item) => {
|
|
144
|
+
return item?.['g:id'] && item['g:title'];
|
|
145
|
+
});
|
|
146
|
+
} else if (fileExtension === '.tsv') {
|
|
147
|
+
const csvContent = fs.readFileSync(feedFilepath, 'utf8');
|
|
148
|
+
const csvRecords = parse(csvContent, {
|
|
149
|
+
columns: true,
|
|
150
|
+
skip_empty_lines: true,
|
|
151
|
+
delimiter: '\t',
|
|
152
|
+
});
|
|
153
|
+
items = csvRecords
|
|
154
|
+
.filter((record) => record.sku && record.nome)
|
|
155
|
+
.map(mapCSVToFeed);
|
|
156
|
+
}
|
|
89
157
|
if (!items?.[0] || typeof items?.[0] !== 'object') {
|
|
90
158
|
await echo`The XML file does not appear to be a valid RSS 2.0 product feed`;
|
|
91
159
|
return process.exit(1);
|
|
92
160
|
}
|
|
93
161
|
const ecomAuthHeaders = {
|
|
94
|
-
'X-Store-ID':
|
|
95
|
-
'X-My-ID':
|
|
162
|
+
'X-Store-ID': ECOM_STORE_ID,
|
|
163
|
+
'X-My-ID': ECOM_AUTHENTICATION_ID,
|
|
96
164
|
};
|
|
97
|
-
const { ECOM_ACCESS_TOKEN, ECOM_API_KEY } = process.env;
|
|
98
165
|
if (ECOM_ACCESS_TOKEN) {
|
|
99
166
|
ecomAuthHeaders['X-Access-Token'] = ECOM_ACCESS_TOKEN;
|
|
100
167
|
} else {
|
|
@@ -205,7 +272,11 @@ const importFeed = async () => {
|
|
|
205
272
|
}
|
|
206
273
|
}
|
|
207
274
|
const setStockAndPrices = (productOrVatiation, item) => {
|
|
208
|
-
|
|
275
|
+
if (typeof item.quantity === 'number') {
|
|
276
|
+
productOrVatiation.quantity = item.quantity;
|
|
277
|
+
} else {
|
|
278
|
+
productOrVatiation.quantity = item['g:availability'] === 'in stock' ? 100 : 0;
|
|
279
|
+
}
|
|
209
280
|
const price = parseFloat(item['g:price'] || item['g:sale_price'] || '');
|
|
210
281
|
if (price) {
|
|
211
282
|
const salePrice = parseFloat(item['g:sale_price'] || '');
|
|
@@ -272,13 +343,20 @@ const importFeed = async () => {
|
|
|
272
343
|
await echo`\n${new Date().toISOString()}`;
|
|
273
344
|
await echo`${(productItems.length - i)} products to import\n`;
|
|
274
345
|
await echo`SKU: ${sku}`;
|
|
346
|
+
let slug = item['g:link']
|
|
347
|
+
? new URL(item['g:link']).pathname.substring(1).toLowerCase()
|
|
348
|
+
: slugify(name);
|
|
349
|
+
if (!/[a-z0-9]/.test(slug.charAt(0))) {
|
|
350
|
+
slug = `r${slug}`;
|
|
351
|
+
}
|
|
275
352
|
const product = {
|
|
276
353
|
sku,
|
|
277
354
|
name,
|
|
278
|
-
slug
|
|
279
|
-
? new URL(item['g:link']).pathname.substring(1).toLowerCase()
|
|
280
|
-
: slugify(name),
|
|
355
|
+
slug,
|
|
281
356
|
condition: item['g:condition'],
|
|
357
|
+
available: item.available,
|
|
358
|
+
visible: item.visible,
|
|
359
|
+
quantity: item.quantity,
|
|
282
360
|
};
|
|
283
361
|
const description = item['g:description']?.trim();
|
|
284
362
|
if (description && description !== name) {
|
|
@@ -340,7 +418,8 @@ const importFeed = async () => {
|
|
|
340
418
|
// eslint-disable-next-line no-console
|
|
341
419
|
console.error(err);
|
|
342
420
|
if (err.statusCode < 500) {
|
|
343
|
-
|
|
421
|
+
retries = 4;
|
|
422
|
+
break;
|
|
344
423
|
}
|
|
345
424
|
retries += 1;
|
|
346
425
|
await sleep(4000);
|
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.44.0",
|
|
5
5
|
"description": "e-com.plus Cloud Commerce CLI tools",
|
|
6
6
|
"bin": {
|
|
7
7
|
"cloudcommerce": "./bin/run.mjs"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"homepage": "https://github.com/ecomplus/cloud-commerce/tree/main/packages/cli#readme",
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"@fastify/deepmerge": "^3.1.0",
|
|
34
|
+
"csv-parse": "^5.6.0",
|
|
34
35
|
"dotenv": "^16.5.0",
|
|
35
36
|
"fast-xml-parser": "^5.2.3",
|
|
36
37
|
"image-size": "^2.0.2",
|
|
@@ -38,7 +39,7 @@
|
|
|
38
39
|
"md5": "^2.3.0",
|
|
39
40
|
"typescript": "~5.8.3",
|
|
40
41
|
"zx": "^8.5.4",
|
|
41
|
-
"@cloudcommerce/api": "2.
|
|
42
|
+
"@cloudcommerce/api": "2.44.0"
|
|
42
43
|
},
|
|
43
44
|
"scripts": {
|
|
44
45
|
"build": "bash ../../scripts/build-lib.sh"
|