n8n-nodes-prestashop8 2.6.1 → 2.8.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/README.md +4 -3
- package/dist/credentials/PrestaShop8Api.credentials.js +3 -8
- package/dist/index.d.ts +2 -0
- package/dist/index.js +18 -0
- package/dist/nodes/PrestaShop8/PrestaShop8.node.d.ts +0 -1
- package/dist/nodes/PrestaShop8/PrestaShop8.node.description.js +3 -53
- package/dist/nodes/PrestaShop8/PrestaShop8.node.js +53 -21
- package/dist/nodes/PrestaShop8/helpers/http.d.ts +15 -2
- package/dist/nodes/PrestaShop8/helpers/http.js +51 -27
- package/dist/nodes/PrestaShop8/loadOptions.d.ts +0 -1
- package/dist/nodes/PrestaShop8/loadOptions.js +0 -67
- package/dist/nodes/PrestaShop8/resourceSchemas.js +5 -0
- package/dist/nodes/PrestaShop8/types.js +11 -0
- package/dist/nodes/PrestaShop8/utils/response.js +13 -7
- package/dist/nodes/PrestaShop8/utils/xml.js +21 -16
- package/package.json +4 -6
package/README.md
CHANGED
|
@@ -30,6 +30,7 @@ A comprehensive n8n community node for PrestaShop 8 integration with automatic X
|
|
|
30
30
|
- ✅ **25+ resources supported**: products, customers, orders, stocks...
|
|
31
31
|
- ✅ **Advanced filtering** with 10 search operators
|
|
32
32
|
- ✅ **Raw mode** for debugging and advanced use cases
|
|
33
|
+
- ✅ **Retry on error** to automatically recover from transient failures (timeouts, connection drops)
|
|
33
34
|
|
|
34
35
|
## 🚀 Quick Start
|
|
35
36
|
|
|
@@ -81,14 +82,13 @@ npm install n8n-nodes-prestashop8
|
|
|
81
82
|
### 📊 Supported Resources
|
|
82
83
|
|
|
83
84
|
<details>
|
|
84
|
-
<summary><strong>👥 CRM & Customers (
|
|
85
|
+
<summary><strong>👥 CRM & Customers (5 resources)</strong></summary>
|
|
85
86
|
|
|
86
87
|
- `customers` - Store customers
|
|
87
88
|
- `addresses` - Shipping/billing addresses
|
|
88
89
|
- `groups` - Customer groups and pricing
|
|
89
90
|
- `customer_threads` - Customer service conversations
|
|
90
91
|
- `customer_messages` - Individual messages
|
|
91
|
-
- `guests` - Non-registered visitors
|
|
92
92
|
</details>
|
|
93
93
|
|
|
94
94
|
<details>
|
|
@@ -137,6 +137,7 @@ npm install n8n-nodes-prestashop8
|
|
|
137
137
|
- **Sorting**: `[price_ASC]`, `[date_add_DESC]`
|
|
138
138
|
- **Fields**: `full`, `minimal`, or custom
|
|
139
139
|
- **Debug**: URL, headers, timeout
|
|
140
|
+
- **Retry on error**: automatically retry a call that fails on a transient error — network timeout, connection drop, 5xx server error or 429 rate-limit (never on 4xx). Configurable max attempts and fixed delay between attempts; the retry budget is reset for each failing call. Each attempt is logged to the n8n server logs.
|
|
140
141
|
|
|
141
142
|
## 🎯 Usage Examples
|
|
142
143
|
|
|
@@ -169,7 +170,7 @@ Cron → PrestaShop 8 Node → Calculate KPIs → Email Report
|
|
|
169
170
|
### Common Problems
|
|
170
171
|
- **401 Unauthorized** → Check API key and permissions
|
|
171
172
|
- **404 Not Found** → Verify base URL and Webservices enabled
|
|
172
|
-
- **Timeout** → Increase timeout in debug options
|
|
173
|
+
- **Timeout** → Increase timeout in debug options, or enable **Retry on error** to auto-recover from transient timeouts
|
|
173
174
|
|
|
174
175
|
### Get Help
|
|
175
176
|
- 🐞 **[GitHub Issues](https://github.com/PPCM/n8n-nodes-prestashop8/issues)** - Bugs and questions
|
|
@@ -27,13 +27,6 @@ class PrestaShop8Api {
|
|
|
27
27
|
description: 'PrestaShop API key generated in back office (Advanced Parameters > Web Service)',
|
|
28
28
|
required: true,
|
|
29
29
|
},
|
|
30
|
-
{
|
|
31
|
-
displayName: 'Test Connection',
|
|
32
|
-
name: 'testConnection',
|
|
33
|
-
type: 'boolean',
|
|
34
|
-
default: false,
|
|
35
|
-
description: 'Automatically test the connection when saving credentials',
|
|
36
|
-
},
|
|
37
30
|
];
|
|
38
31
|
this.authenticate = {
|
|
39
32
|
type: 'generic',
|
|
@@ -46,7 +39,9 @@ class PrestaShop8Api {
|
|
|
46
39
|
};
|
|
47
40
|
this.test = {
|
|
48
41
|
request: {
|
|
49
|
-
|
|
42
|
+
// Normalize the base URL like the node does (strip trailing slash,
|
|
43
|
+
// ensure the /api suffix) so the test matches runtime behavior
|
|
44
|
+
url: '={{ ($credentials.baseUrl.trim().replace(new RegExp("/+$"), "") + "/api").replace(new RegExp("/api/api$"), "/api") }}/products?display=[id]&limit=1',
|
|
50
45
|
method: 'GET',
|
|
51
46
|
},
|
|
52
47
|
};
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./credentials/PrestaShop8Api.credentials"), exports);
|
|
18
|
+
__exportStar(require("./nodes/PrestaShop8/PrestaShop8.node"), exports);
|
|
@@ -4,7 +4,6 @@ export declare class PrestaShop8 implements INodeType {
|
|
|
4
4
|
methods: {
|
|
5
5
|
loadOptions: {
|
|
6
6
|
getOperations(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
7
|
-
getRequiredFields(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
8
7
|
getAvailableFields(this: import("n8n-workflow").ILoadOptionsFunctions): Promise<import("n8n-workflow").INodePropertyOptions[]>;
|
|
9
8
|
};
|
|
10
9
|
};
|
|
@@ -121,6 +121,7 @@ function buildStandardFieldValues() {
|
|
|
121
121
|
type: 'options',
|
|
122
122
|
typeOptions: {
|
|
123
123
|
loadOptionsMethod: 'getAvailableFields',
|
|
124
|
+
loadOptionsDependsOn: ['resource'],
|
|
124
125
|
},
|
|
125
126
|
default: '',
|
|
126
127
|
required: true,
|
|
@@ -197,59 +198,6 @@ exports.PrestaShop8Description = {
|
|
|
197
198
|
required: true,
|
|
198
199
|
description: 'PrestaShop resource type to work with',
|
|
199
200
|
},
|
|
200
|
-
// Integrated documentation
|
|
201
|
-
{
|
|
202
|
-
displayName: '📚 Documentation',
|
|
203
|
-
name: 'documentation',
|
|
204
|
-
type: 'notice',
|
|
205
|
-
default: '',
|
|
206
|
-
displayOptions: {
|
|
207
|
-
show: {
|
|
208
|
-
'/': [
|
|
209
|
-
{
|
|
210
|
-
_cnd: {
|
|
211
|
-
eq: 'doc',
|
|
212
|
-
},
|
|
213
|
-
},
|
|
214
|
-
],
|
|
215
|
-
},
|
|
216
|
-
},
|
|
217
|
-
typeOptions: {
|
|
218
|
-
theme: 'info',
|
|
219
|
-
},
|
|
220
|
-
options: [
|
|
221
|
-
{
|
|
222
|
-
name: '🚀 Quick Start Guide',
|
|
223
|
-
value: 'quickstart',
|
|
224
|
-
description: 'Configuration and first steps with PrestaShop 8',
|
|
225
|
-
},
|
|
226
|
-
{
|
|
227
|
-
name: '🔑 API Authentication',
|
|
228
|
-
value: 'auth',
|
|
229
|
-
description: 'PrestaShop API key configuration',
|
|
230
|
-
},
|
|
231
|
-
{
|
|
232
|
-
name: '🔄 XML/JSON Conversion',
|
|
233
|
-
value: 'conversion',
|
|
234
|
-
description: 'How automatic simplification works',
|
|
235
|
-
},
|
|
236
|
-
{
|
|
237
|
-
name: '🔍 Search and Filters',
|
|
238
|
-
value: 'filters',
|
|
239
|
-
description: 'Using advanced PrestaShop filters',
|
|
240
|
-
},
|
|
241
|
-
{
|
|
242
|
-
name: '⚡ Raw Mode',
|
|
243
|
-
value: 'rawmode',
|
|
244
|
-
description: 'Using raw data mode',
|
|
245
|
-
},
|
|
246
|
-
{
|
|
247
|
-
name: '📝 Practical Examples',
|
|
248
|
-
value: 'examples',
|
|
249
|
-
description: 'Common use cases and code examples',
|
|
250
|
-
},
|
|
251
|
-
],
|
|
252
|
-
},
|
|
253
201
|
// Operation selection
|
|
254
202
|
{
|
|
255
203
|
displayName: 'Operation',
|
|
@@ -263,6 +211,7 @@ exports.PrestaShop8Description = {
|
|
|
263
211
|
},
|
|
264
212
|
typeOptions: {
|
|
265
213
|
loadOptionsMethod: 'getOperations',
|
|
214
|
+
loadOptionsDependsOn: ['resource'],
|
|
266
215
|
},
|
|
267
216
|
default: 'list',
|
|
268
217
|
required: true,
|
|
@@ -400,6 +349,7 @@ exports.PrestaShop8Description = {
|
|
|
400
349
|
type: 'options',
|
|
401
350
|
typeOptions: {
|
|
402
351
|
loadOptionsMethod: 'getAvailableFields',
|
|
352
|
+
loadOptionsDependsOn: ['resource'],
|
|
403
353
|
},
|
|
404
354
|
default: '',
|
|
405
355
|
description: 'Select a field from the PrestaShop schema.',
|
|
@@ -43,16 +43,30 @@ function collectRequiredFields(executeFunctions, resource, itemIndex) {
|
|
|
43
43
|
/**
|
|
44
44
|
* Process response data into appropriate format with type conversion
|
|
45
45
|
*/
|
|
46
|
+
function convertPayloadTypes(data, resource) {
|
|
47
|
+
if (!data) {
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
if (Array.isArray(data)) {
|
|
51
|
+
return (0, resourceSchemas_1.convertResourceArray)(data, resource);
|
|
52
|
+
}
|
|
53
|
+
if (typeof data === 'object') {
|
|
54
|
+
// Unwrap debug wrapper (Show Request URL / Show Request Info) before converting
|
|
55
|
+
if ('data' in data && ('requestUrl' in data || 'requestInfo' in data)) {
|
|
56
|
+
return { ...data, data: convertPayloadTypes(data.data, resource) };
|
|
57
|
+
}
|
|
58
|
+
// Unwrap response wrapper (Include Response Headers) before converting
|
|
59
|
+
if ('body' in data && 'headers' in data && 'statusCode' in data) {
|
|
60
|
+
return { ...data, body: convertPayloadTypes(data.body, resource) };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return (0, resourceSchemas_1.convertResourceTypes)(data, resource);
|
|
64
|
+
}
|
|
46
65
|
function processResponseData(responseData, returnData, itemIndex, resource, convertTypes = true) {
|
|
47
66
|
// Convert types if enabled and we have data
|
|
48
67
|
let processedData = responseData;
|
|
49
68
|
if (convertTypes && processedData) {
|
|
50
|
-
|
|
51
|
-
processedData = (0, resourceSchemas_1.convertResourceArray)(processedData, resource);
|
|
52
|
-
}
|
|
53
|
-
else {
|
|
54
|
-
processedData = (0, resourceSchemas_1.convertResourceTypes)(processedData, resource);
|
|
55
|
-
}
|
|
69
|
+
processedData = convertPayloadTypes(processedData, resource);
|
|
56
70
|
}
|
|
57
71
|
if (Array.isArray(processedData)) {
|
|
58
72
|
processedData.forEach((item) => {
|
|
@@ -157,7 +171,7 @@ class PrestaShop8 {
|
|
|
157
171
|
if (displayValue !== null) {
|
|
158
172
|
urlParams.display = displayValue;
|
|
159
173
|
}
|
|
160
|
-
requestUrl = (0, utils_1.buildUrlWithFilters)(`${credentials.baseUrl}/${resource}/${id}`, urlParams, rawMode);
|
|
174
|
+
requestUrl = (0, utils_1.buildUrlWithFilters)(`${credentials.baseUrl}/${resource}/${encodeURIComponent(id)}`, urlParams, rawMode);
|
|
161
175
|
const options = (0, http_1.buildHttpOptions)('GET', requestUrl, credentials, rawMode, timeout);
|
|
162
176
|
const { response, debugInfo, url, responseHeaders, statusCode } = await (0, http_1.executeHttpRequest)(this.helpers, options, credentials, rawMode, operation, resource, neverError, undefined, opts.retry);
|
|
163
177
|
requestUrl = url;
|
|
@@ -228,7 +242,7 @@ class PrestaShop8 {
|
|
|
228
242
|
}
|
|
229
243
|
// Build XML using new format
|
|
230
244
|
body = (0, utils_1.buildUpdateXml)(resource, id, fieldsToUpdate);
|
|
231
|
-
const options = (0, http_1.buildHttpOptions)('PATCH', `${credentials.baseUrl}/${resource}/${id}`, credentials, rawMode, timeout, body);
|
|
245
|
+
const options = (0, http_1.buildHttpOptions)('PATCH', `${credentials.baseUrl}/${resource}/${encodeURIComponent(id)}`, credentials, rawMode, timeout, body);
|
|
232
246
|
const { response, debugInfo, url, responseHeaders, statusCode } = await (0, http_1.executeHttpRequest)(this.helpers, options, credentials, rawMode, operation, resource, neverError, body, opts.retry);
|
|
233
247
|
requestUrl = url;
|
|
234
248
|
requestDebugInfo = debugInfo;
|
|
@@ -261,12 +275,17 @@ class PrestaShop8 {
|
|
|
261
275
|
// Add custom filter directly to URL without any interpretation
|
|
262
276
|
// User writes exactly what they want: date=1, filter[name]=test, etc.
|
|
263
277
|
const customFilter = filter.customFilterExpression.trim();
|
|
264
|
-
// Parse the custom filter to extract key=value pairs for URL construction
|
|
278
|
+
// Parse the custom filter to extract key=value pairs for URL construction.
|
|
279
|
+
// Split on the FIRST '=' only: values may legitimately contain '='.
|
|
265
280
|
const parts = customFilter.split('&');
|
|
266
281
|
for (const part of parts) {
|
|
267
|
-
const
|
|
268
|
-
if (
|
|
269
|
-
|
|
282
|
+
const eqIndex = part.indexOf('=');
|
|
283
|
+
if (eqIndex > 0) {
|
|
284
|
+
const key = part.slice(0, eqIndex).trim();
|
|
285
|
+
const value = part.slice(eqIndex + 1).trim();
|
|
286
|
+
if (key) {
|
|
287
|
+
filterParams[key] = value;
|
|
288
|
+
}
|
|
270
289
|
}
|
|
271
290
|
}
|
|
272
291
|
}
|
|
@@ -276,9 +295,12 @@ class PrestaShop8 {
|
|
|
276
295
|
const filterValue = filter.value !== null && filter.value !== undefined ? String(filter.value).trim() : '';
|
|
277
296
|
const format = http_1.FILTER_OPERATOR_FORMATS[filter.operator];
|
|
278
297
|
if (format) {
|
|
279
|
-
|
|
280
|
-
|
|
298
|
+
// Guard: operators that require a value must not be silently dropped
|
|
299
|
+
// when the value is empty, otherwise PrestaShop returns all records.
|
|
300
|
+
if (format.requiresValue && !filterValue) {
|
|
301
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Filter operator "${filter.operator}" on field "${filter.field}" requires a value. Use the "IS_EMPTY" operator to match empty fields.`);
|
|
281
302
|
}
|
|
303
|
+
filterParams[key] = format.template.replace('{v}', filterValue);
|
|
282
304
|
}
|
|
283
305
|
else if (filterValue) {
|
|
284
306
|
filterParams[key] = `[${filterValue}]`;
|
|
@@ -320,15 +342,25 @@ class PrestaShop8 {
|
|
|
320
342
|
if (!id) {
|
|
321
343
|
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'ID required for this operation');
|
|
322
344
|
}
|
|
323
|
-
const options = (0, http_1.buildHttpOptions)('DELETE', `${credentials.baseUrl}/${resource}/${id}`, credentials, rawMode, timeout);
|
|
324
|
-
const { debugInfo, url, responseHeaders, statusCode } = await (0, http_1.executeHttpRequest)(this.helpers, options, credentials, rawMode, operation, resource, neverError, undefined, opts.retry);
|
|
345
|
+
const options = (0, http_1.buildHttpOptions)('DELETE', `${credentials.baseUrl}/${resource}/${encodeURIComponent(id)}`, credentials, rawMode, timeout);
|
|
346
|
+
const { response, debugInfo, url, responseHeaders, statusCode } = await (0, http_1.executeHttpRequest)(this.helpers, options, credentials, rawMode, operation, resource, neverError, undefined, opts.retry);
|
|
325
347
|
requestUrl = url;
|
|
326
348
|
requestDebugInfo = debugInfo;
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
349
|
+
// With neverError enabled a failed delete returns normally with an
|
|
350
|
+
// error status code: report the failure instead of a fake success
|
|
351
|
+
const deleteFailed = typeof statusCode === 'number' && statusCode >= 400;
|
|
352
|
+
const deleteResponse = deleteFailed
|
|
353
|
+
? {
|
|
354
|
+
success: false,
|
|
355
|
+
status: statusCode,
|
|
356
|
+
message: (response === null || response === void 0 ? void 0 : response.message) || `Failed to delete ${resource} with ID ${id}`,
|
|
357
|
+
deletedId: id,
|
|
358
|
+
}
|
|
359
|
+
: {
|
|
360
|
+
success: true,
|
|
361
|
+
message: `${resource} with ID ${id} deleted successfully`,
|
|
362
|
+
deletedId: id,
|
|
363
|
+
};
|
|
332
364
|
responseData = (0, http_1.wrapResponse)(deleteResponse, includeResponseHeaders, responseHeaders, statusCode);
|
|
333
365
|
break;
|
|
334
366
|
}
|
|
@@ -31,6 +31,12 @@ export interface RetryOptions {
|
|
|
31
31
|
retryDelay: number;
|
|
32
32
|
/** Optional hook called before each retry wait, with the upcoming retry number (1-based) and the error. */
|
|
33
33
|
onRetry?: (attempt: number, error: any) => void;
|
|
34
|
+
/**
|
|
35
|
+
* Set for non-idempotent calls (POST/create): ambiguous network errors
|
|
36
|
+
* (timeout, reset) are then NOT retried because the server may already
|
|
37
|
+
* have processed the request, which would create duplicate records.
|
|
38
|
+
*/
|
|
39
|
+
nonIdempotent?: boolean;
|
|
34
40
|
}
|
|
35
41
|
/**
|
|
36
42
|
* Pause execution for the given number of milliseconds (used to throttle calls).
|
|
@@ -41,7 +47,7 @@ export declare function sleep(ms: number): Promise<void>;
|
|
|
41
47
|
* Retries on network/timeout errors, 5xx server errors and 429 rate-limit.
|
|
42
48
|
* Never retries on 4xx (invalid key, not found, invalid XML, etc.).
|
|
43
49
|
*/
|
|
44
|
-
export declare function isRetryableError(error: any): boolean;
|
|
50
|
+
export declare function isRetryableError(error: any, nonIdempotent?: boolean): boolean;
|
|
45
51
|
/**
|
|
46
52
|
* Build a short human-readable reason from an error, for retry logging.
|
|
47
53
|
* Prefers the network code, then the HTTP status, then the message.
|
|
@@ -62,7 +68,14 @@ export declare function getOperationOptions(executeFunctions: IExecuteFunctions,
|
|
|
62
68
|
*/
|
|
63
69
|
export declare function buildHttpOptions(method: IHttpRequestMethods, url: string, credentials: IPrestaShopCredentials, rawMode: boolean, timeout: number, body?: string): IHttpRequestOptions;
|
|
64
70
|
/**
|
|
65
|
-
*
|
|
71
|
+
* Mask a secret for debug output: keep the first 4 characters to help
|
|
72
|
+
* identify which key was used, never expose the full value.
|
|
73
|
+
*/
|
|
74
|
+
export declare function maskSecret(secret: unknown): string;
|
|
75
|
+
/**
|
|
76
|
+
* Capture complete request information for debugging.
|
|
77
|
+
* The API key is the Basic auth username, so both the Authorization header
|
|
78
|
+
* and the username must be masked to avoid persisting the key in execution data.
|
|
66
79
|
*/
|
|
67
80
|
export declare function captureRequestDebugInfo(options: any, credentials: any, rawMode: boolean, operation: string, resource: string, body?: string): any;
|
|
68
81
|
/**
|
|
@@ -7,6 +7,7 @@ exports.describeError = describeError;
|
|
|
7
7
|
exports.withRetry = withRetry;
|
|
8
8
|
exports.getOperationOptions = getOperationOptions;
|
|
9
9
|
exports.buildHttpOptions = buildHttpOptions;
|
|
10
|
+
exports.maskSecret = maskSecret;
|
|
10
11
|
exports.captureRequestDebugInfo = captureRequestDebugInfo;
|
|
11
12
|
exports.wrapResponse = wrapResponse;
|
|
12
13
|
exports.executeHttpRequest = executeHttpRequest;
|
|
@@ -42,29 +43,29 @@ function sleep(ms) {
|
|
|
42
43
|
* Retries on network/timeout errors, 5xx server errors and 429 rate-limit.
|
|
43
44
|
* Never retries on 4xx (invalid key, not found, invalid XML, etc.).
|
|
44
45
|
*/
|
|
45
|
-
function isRetryableError(error) {
|
|
46
|
-
var _a, _b;
|
|
46
|
+
function isRetryableError(error, nonIdempotent = false) {
|
|
47
|
+
var _a, _b, _c;
|
|
47
48
|
// Network / timeout errors expose a code on the error or its cause
|
|
48
49
|
const code = (error === null || error === void 0 ? void 0 : error.code) || ((_a = error === null || error === void 0 ? void 0 : error.cause) === null || _a === void 0 ? void 0 : _a.code);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
'EAI_AGAIN',
|
|
56
|
-
'EPIPE',
|
|
57
|
-
];
|
|
58
|
-
if (code && retryableCodes.includes(code)) {
|
|
50
|
+
// Errors raised before the request could reach the server: always safe to retry
|
|
51
|
+
const preSendCodes = ['ECONNREFUSED', 'ENOTFOUND', 'EAI_AGAIN'];
|
|
52
|
+
// Errors where the request may already have been processed by the server:
|
|
53
|
+
// unsafe to retry for non-idempotent calls (duplicate records)
|
|
54
|
+
const ambiguousCodes = ['ETIMEDOUT', 'ECONNRESET', 'ECONNABORTED', 'EPIPE'];
|
|
55
|
+
if (code && preSendCodes.includes(code)) {
|
|
59
56
|
return true;
|
|
60
57
|
}
|
|
58
|
+
if (code && ambiguousCodes.includes(code)) {
|
|
59
|
+
return !nonIdempotent;
|
|
60
|
+
}
|
|
61
61
|
// "socket hang up" and timeout messages have no stable code
|
|
62
62
|
const message = String((error === null || error === void 0 ? void 0 : error.message) || '').toLowerCase();
|
|
63
63
|
if (message.includes('socket hang up') || message.includes('timeout')) {
|
|
64
|
-
return
|
|
64
|
+
return !nonIdempotent;
|
|
65
65
|
}
|
|
66
|
-
// HTTP status: retry on 429 (rate-limit) and 5xx (server errors) only
|
|
67
|
-
|
|
66
|
+
// HTTP status: retry on 429 (rate-limit) and 5xx (server errors) only.
|
|
67
|
+
// n8n's NodeApiError exposes httpCode as a string, so coerce to number.
|
|
68
|
+
const status = Number((_b = error === null || error === void 0 ? void 0 : error.httpCode) !== null && _b !== void 0 ? _b : (_c = error === null || error === void 0 ? void 0 : error.response) === null || _c === void 0 ? void 0 : _c.status);
|
|
68
69
|
if (status === 429 || (status >= 500 && status <= 599)) {
|
|
69
70
|
return true;
|
|
70
71
|
}
|
|
@@ -100,7 +101,7 @@ async function withRetry(retry, fn) {
|
|
|
100
101
|
return await fn();
|
|
101
102
|
}
|
|
102
103
|
catch (error) {
|
|
103
|
-
if (attempt >= maxRetries || !isRetryableError(error)) {
|
|
104
|
+
if (attempt >= maxRetries || !isRetryableError(error, retry.nonIdempotent)) {
|
|
104
105
|
throw error;
|
|
105
106
|
}
|
|
106
107
|
// attempt is 0-based; report the upcoming retry number (1-based)
|
|
@@ -155,7 +156,20 @@ function buildHttpOptions(method, url, credentials, rawMode, timeout, body) {
|
|
|
155
156
|
};
|
|
156
157
|
}
|
|
157
158
|
/**
|
|
158
|
-
*
|
|
159
|
+
* Mask a secret for debug output: keep the first 4 characters to help
|
|
160
|
+
* identify which key was used, never expose the full value.
|
|
161
|
+
*/
|
|
162
|
+
function maskSecret(secret) {
|
|
163
|
+
const str = typeof secret === 'string' ? secret : '';
|
|
164
|
+
if (!str) {
|
|
165
|
+
return '';
|
|
166
|
+
}
|
|
167
|
+
return str.length > 8 ? `${str.slice(0, 4)}***` : '***';
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Capture complete request information for debugging.
|
|
171
|
+
* The API key is the Basic auth username, so both the Authorization header
|
|
172
|
+
* and the username must be masked to avoid persisting the key in execution data.
|
|
159
173
|
*/
|
|
160
174
|
function captureRequestDebugInfo(options, credentials, rawMode, operation, resource, body) {
|
|
161
175
|
return {
|
|
@@ -163,12 +177,12 @@ function captureRequestDebugInfo(options, credentials, rawMode, operation, resou
|
|
|
163
177
|
url: options.url,
|
|
164
178
|
headers: {
|
|
165
179
|
...options.headers,
|
|
166
|
-
'Authorization':
|
|
180
|
+
'Authorization': 'Basic [REDACTED]',
|
|
167
181
|
'User-Agent': 'n8n-prestashop8-node/1.0.0',
|
|
168
182
|
},
|
|
169
183
|
authentication: {
|
|
170
184
|
type: 'Basic Auth',
|
|
171
|
-
username: credentials.apiKey,
|
|
185
|
+
username: maskSecret(credentials.apiKey),
|
|
172
186
|
password: '[HIDDEN]',
|
|
173
187
|
baseUrl: credentials.baseUrl,
|
|
174
188
|
},
|
|
@@ -202,27 +216,37 @@ async function executeHttpRequest(helpers, options, credentials, rawMode, operat
|
|
|
202
216
|
const requestUrl = options.url;
|
|
203
217
|
const debugInfo = captureRequestDebugInfo(options, credentials, rawMode, operation, resource, body);
|
|
204
218
|
try {
|
|
205
|
-
|
|
219
|
+
// POST (create) is not idempotent: ambiguous network errors must not be retried
|
|
220
|
+
const retryOptions = { ...retry, nonIdempotent: options.method === 'POST' };
|
|
221
|
+
// Request the full response so real headers and status code can be surfaced
|
|
222
|
+
const rawResponse = await withRetry(retryOptions, () => helpers.httpRequest({ ...options, returnFullResponse: true }));
|
|
223
|
+
// Older n8n helpers may ignore returnFullResponse and return the body directly
|
|
224
|
+
const isFullResponse = rawResponse !== null &&
|
|
225
|
+
typeof rawResponse === 'object' &&
|
|
226
|
+
'body' in rawResponse &&
|
|
227
|
+
('statusCode' in rawResponse || 'headers' in rawResponse);
|
|
206
228
|
return {
|
|
207
|
-
response,
|
|
229
|
+
response: isFullResponse ? rawResponse.body : rawResponse,
|
|
208
230
|
debugInfo,
|
|
209
231
|
url: requestUrl,
|
|
210
|
-
responseHeaders: {},
|
|
211
|
-
statusCode: 200,
|
|
232
|
+
responseHeaders: (isFullResponse && rawResponse.headers) || {},
|
|
233
|
+
statusCode: (isFullResponse && rawResponse.statusCode) || 200,
|
|
212
234
|
};
|
|
213
235
|
}
|
|
214
236
|
catch (error) {
|
|
215
237
|
if (neverError) {
|
|
238
|
+
// n8n's NodeApiError exposes httpCode as a string: coerce to number
|
|
239
|
+
const errorStatus = Number((_a = error.httpCode) !== null && _a !== void 0 ? _a : (_b = error.response) === null || _b === void 0 ? void 0 : _b.status) || 500;
|
|
216
240
|
const errorResponse = {
|
|
217
|
-
status:
|
|
218
|
-
message: ((
|
|
241
|
+
status: errorStatus,
|
|
242
|
+
message: ((_c = error.response) === null || _c === void 0 ? void 0 : _c.data) || '',
|
|
219
243
|
};
|
|
220
244
|
return {
|
|
221
245
|
response: errorResponse,
|
|
222
246
|
debugInfo,
|
|
223
247
|
url: requestUrl,
|
|
224
|
-
responseHeaders: ((
|
|
225
|
-
statusCode:
|
|
248
|
+
responseHeaders: ((_d = error.response) === null || _d === void 0 ? void 0 : _d.headers) || {},
|
|
249
|
+
statusCode: errorStatus,
|
|
226
250
|
};
|
|
227
251
|
}
|
|
228
252
|
throw error;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow';
|
|
2
2
|
export declare const loadOptionsMethods: {
|
|
3
3
|
getOperations(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
4
|
-
getRequiredFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
5
4
|
getAvailableFields(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
6
5
|
};
|
|
@@ -1,37 +1,4 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
-
if (k2 === undefined) k2 = k;
|
|
4
|
-
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
-
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
-
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
-
}
|
|
8
|
-
Object.defineProperty(o, k2, desc);
|
|
9
|
-
}) : (function(o, m, k, k2) {
|
|
10
|
-
if (k2 === undefined) k2 = k;
|
|
11
|
-
o[k2] = m[k];
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
3
|
exports.loadOptionsMethods = void 0;
|
|
37
4
|
const types_1 = require("./types");
|
|
@@ -89,40 +56,6 @@ exports.loadOptionsMethods = {
|
|
|
89
56
|
}
|
|
90
57
|
return operations;
|
|
91
58
|
},
|
|
92
|
-
// Load required fields for CREATE operation based on resource
|
|
93
|
-
async getRequiredFields() {
|
|
94
|
-
try {
|
|
95
|
-
const resource = this.getCurrentNodeParameter('resource');
|
|
96
|
-
if (!resource) {
|
|
97
|
-
return [{
|
|
98
|
-
name: 'Custom Field',
|
|
99
|
-
value: '__custom__',
|
|
100
|
-
description: 'Enter a custom field name',
|
|
101
|
-
}];
|
|
102
|
-
}
|
|
103
|
-
const { REQUIRED_FIELDS_BY_RESOURCE } = await Promise.resolve().then(() => __importStar(require('./utils')));
|
|
104
|
-
const requiredFields = REQUIRED_FIELDS_BY_RESOURCE[resource] || [];
|
|
105
|
-
if (requiredFields.length === 0) {
|
|
106
|
-
return [{
|
|
107
|
-
name: 'No specific required fields',
|
|
108
|
-
value: 'none',
|
|
109
|
-
description: 'This resource has no specific required fields defined',
|
|
110
|
-
}];
|
|
111
|
-
}
|
|
112
|
-
return requiredFields.map((field) => ({
|
|
113
|
-
name: field,
|
|
114
|
-
value: field,
|
|
115
|
-
description: `Required field: ${field}`,
|
|
116
|
-
}));
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return [{
|
|
120
|
-
name: 'Custom Field',
|
|
121
|
-
value: '__custom__',
|
|
122
|
-
description: 'Enter a custom field name',
|
|
123
|
-
}];
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
59
|
// Load available fields from resource schemas for autocomplete
|
|
127
60
|
async getAvailableFields() {
|
|
128
61
|
try {
|
|
@@ -3979,6 +3979,11 @@ function convertFieldValue(value, fieldInfo) {
|
|
|
3979
3979
|
if (value === null || value === undefined || value === '') {
|
|
3980
3980
|
return value;
|
|
3981
3981
|
}
|
|
3982
|
+
// Multilang fields arrive as arrays of {id, value} on multi-language shops;
|
|
3983
|
+
// never stringify objects/arrays or the data would become "[object Object]"
|
|
3984
|
+
if (typeof value === 'object') {
|
|
3985
|
+
return value;
|
|
3986
|
+
}
|
|
3982
3987
|
switch (fieldInfo.type) {
|
|
3983
3988
|
case 'number':
|
|
3984
3989
|
const num = Number(value);
|
|
@@ -226,6 +226,17 @@ exports.PRESTASHOP_RESOURCES = {
|
|
|
226
226
|
supportsGetById: true,
|
|
227
227
|
supportsSearch: true,
|
|
228
228
|
},
|
|
229
|
+
order_invoices: {
|
|
230
|
+
name: 'order_invoices',
|
|
231
|
+
displayName: 'Order Invoices',
|
|
232
|
+
description: 'Order invoices (order_invoices)',
|
|
233
|
+
supportsCreate: true,
|
|
234
|
+
supportsUpdate: true,
|
|
235
|
+
supportsDelete: true,
|
|
236
|
+
supportsList: true,
|
|
237
|
+
supportsGetById: true,
|
|
238
|
+
supportsSearch: true,
|
|
239
|
+
},
|
|
229
240
|
carts: {
|
|
230
241
|
name: 'carts',
|
|
231
242
|
displayName: 'Carts',
|
|
@@ -6,6 +6,7 @@ exports.processResponseForMode = processResponseForMode;
|
|
|
6
6
|
exports.simplifyPrestashopResponse = simplifyPrestashopResponse;
|
|
7
7
|
exports.buildPrestashopXml = buildPrestashopXml;
|
|
8
8
|
const fieldMappings_1 = require("../fieldMappings");
|
|
9
|
+
const xml_1 = require("./xml");
|
|
9
10
|
/**
|
|
10
11
|
* Normalize sort parameter to include _DESC by default
|
|
11
12
|
*/
|
|
@@ -29,15 +30,17 @@ function processDisplayParameter(displayValue, resource, customFields) {
|
|
|
29
30
|
if (displayValue === 'minimal') {
|
|
30
31
|
return null;
|
|
31
32
|
}
|
|
32
|
-
else if (displayValue === 'custom'
|
|
33
|
-
const fields = customFields.trim();
|
|
33
|
+
else if (displayValue === 'custom') {
|
|
34
|
+
const fields = (customFields || '').trim();
|
|
34
35
|
if (fields.startsWith('[') && fields.endsWith(']')) {
|
|
35
36
|
return fields;
|
|
36
37
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
const fieldList = fields.split(',').map(f => f.trim()).filter(f => f).join(',');
|
|
39
|
+
if (!fieldList) {
|
|
40
|
+
// 'display=custom' is not a valid PrestaShop value: fail with a clear message
|
|
41
|
+
throw new Error('Display "Custom Fields" requires at least one field name in Custom Fields');
|
|
40
42
|
}
|
|
43
|
+
return `[${fieldList}]`;
|
|
41
44
|
}
|
|
42
45
|
else if (displayValue === 'full') {
|
|
43
46
|
return 'full';
|
|
@@ -60,8 +63,11 @@ function processResponseForMode(rawData, resource) {
|
|
|
60
63
|
if (resourceKey && Array.isArray(simplified[resourceKey])) {
|
|
61
64
|
return simplified[resourceKey];
|
|
62
65
|
}
|
|
63
|
-
// For single item responses (get by ID), unwrap the resource level
|
|
64
|
-
|
|
66
|
+
// For single item responses (get by ID), unwrap the resource level.
|
|
67
|
+
// Irregular plurals (categories -> category, addresses -> address, ...)
|
|
68
|
+
// must use the singular map: a naive 's' strip never matches them.
|
|
69
|
+
const singular = xml_1.SINGULAR_RESOURCE_MAP[resource] || (resource.endsWith('s') ? resource.slice(0, -1) : resource);
|
|
70
|
+
const singleResourceKey = keys.find(key => (key === resource || key === singular) &&
|
|
65
71
|
typeof simplified[key] === 'object' &&
|
|
66
72
|
!Array.isArray(simplified[key]));
|
|
67
73
|
if (singleResourceKey && simplified[singleResourceKey]) {
|
|
@@ -59,20 +59,24 @@ function getSingularResource(resource) {
|
|
|
59
59
|
return exports.SINGULAR_RESOURCE_MAP[resource] || (resource.endsWith('s') ? resource.slice(0, -1) : resource);
|
|
60
60
|
}
|
|
61
61
|
/**
|
|
62
|
-
*
|
|
62
|
+
* Wraps a value in a CDATA section without altering it.
|
|
63
|
+
* Content inside CDATA must NOT be XML-escaped (it would be double-encoded
|
|
64
|
+
* and stored escaped in PrestaShop). Any ']]>' occurrence is split across
|
|
65
|
+
* two CDATA sections to prevent breaking out of the section.
|
|
63
66
|
*/
|
|
64
|
-
function
|
|
65
|
-
const str =
|
|
66
|
-
return str.replace(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
}
|
|
67
|
+
function cdata(value) {
|
|
68
|
+
const str = value !== null && value !== undefined ? String(value) : '';
|
|
69
|
+
return `<![CDATA[${str.replace(/\]\]>/g, ']]]]><![CDATA[>')}]]>`;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validates a user-provided field name so it can be used as an XML tag name.
|
|
73
|
+
* Prevents injection of extra elements via crafted field names.
|
|
74
|
+
*/
|
|
75
|
+
function assertValidXmlFieldName(name) {
|
|
76
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(name)) {
|
|
77
|
+
throw new Error(`Invalid field name "${name}": only letters, digits, "_", "-" and "." are allowed`);
|
|
78
|
+
}
|
|
79
|
+
return name;
|
|
76
80
|
}
|
|
77
81
|
/**
|
|
78
82
|
* Builds PrestaShop XML structure for key-value pairs (shared by Create and Update)
|
|
@@ -100,17 +104,18 @@ function buildFieldsXml(fields) {
|
|
|
100
104
|
}
|
|
101
105
|
let xml = '';
|
|
102
106
|
for (const [fieldName, fieldValues] of Object.entries(fieldGroups)) {
|
|
107
|
+
assertValidXmlFieldName(fieldName);
|
|
103
108
|
if (fieldValues.some(f => f.langId)) {
|
|
104
109
|
xml += ` <${fieldName}>\n`;
|
|
105
110
|
for (const fieldValue of fieldValues) {
|
|
106
111
|
if (fieldValue.langId) {
|
|
107
|
-
xml += ` <language id="${fieldValue.langId}"
|
|
112
|
+
xml += ` <language id="${fieldValue.langId}">${cdata(fieldValue.value)}</language>\n`;
|
|
108
113
|
}
|
|
109
114
|
}
|
|
110
115
|
xml += ` </${fieldName}>\n`;
|
|
111
116
|
}
|
|
112
117
|
else {
|
|
113
|
-
xml += ` <${fieldName}
|
|
118
|
+
xml += ` <${fieldName}>${cdata(fieldValues[0].value)}</${fieldName}>\n`;
|
|
114
119
|
}
|
|
115
120
|
}
|
|
116
121
|
return xml;
|
|
@@ -137,7 +142,7 @@ function buildUpdateXml(resource, id, fields) {
|
|
|
137
142
|
xml += '<prestashop xmlns:xlink="http://www.w3.org/1999/xlink">\n';
|
|
138
143
|
xml += ` <${singularResource}>\n`;
|
|
139
144
|
xml += buildFieldsXml(fields);
|
|
140
|
-
xml += ` <id
|
|
145
|
+
xml += ` <id>${cdata(id)}</id>\n`;
|
|
141
146
|
xml += ` </${singularResource}>\n`;
|
|
142
147
|
xml += '</prestashop>';
|
|
143
148
|
return xml;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-prestashop8",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.8.0",
|
|
4
4
|
"description": "Nœud n8n personnalisé pour PrestaShop 8 avec support CRUD complet et conversion XML/JSON automatique",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -23,11 +23,9 @@
|
|
|
23
23
|
"url": "https://github.com/PPCM/n8n-nodes-prestashop8/issues"
|
|
24
24
|
},
|
|
25
25
|
"engines": {
|
|
26
|
-
"node": ">=16.10"
|
|
27
|
-
"pnpm": ">=7.18"
|
|
26
|
+
"node": ">=16.10"
|
|
28
27
|
},
|
|
29
|
-
"
|
|
30
|
-
"main": "index.js",
|
|
28
|
+
"main": "dist/index.js",
|
|
31
29
|
"scripts": {
|
|
32
30
|
"build": "tsc && gulp build:icons",
|
|
33
31
|
"dev": "tsc --watch",
|
|
@@ -86,7 +84,7 @@
|
|
|
86
84
|
"brace-expansion": "^1.1.13",
|
|
87
85
|
"uuid": "^14.0.0",
|
|
88
86
|
"axios": "^1.16.0",
|
|
89
|
-
"form-data": "^4.0.
|
|
87
|
+
"form-data": "^4.0.6",
|
|
90
88
|
"follow-redirects": "^1.16.0"
|
|
91
89
|
}
|
|
92
90
|
}
|