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 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 (6 resources)</strong></summary>
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
- url: '={{$credentials.baseUrl}}/products?display=[id]&limit=1',
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
  };
@@ -0,0 +1,2 @@
1
+ export * from './credentials/PrestaShop8Api.credentials';
2
+ export * from './nodes/PrestaShop8/PrestaShop8.node';
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
- if (Array.isArray(processedData)) {
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 [key, value] = part.split('=', 2);
268
- if (key && value !== undefined) {
269
- filterParams[key.trim()] = value.trim();
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
- if (!format.requiresValue || filterValue) {
280
- filterParams[key] = format.template.replace('{v}', filterValue);
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
- const deleteResponse = {
328
- success: true,
329
- message: `${resource} with ID ${id} deleted successfully`,
330
- deletedId: id,
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
- * Capture complete request information for debugging
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
- const retryableCodes = [
50
- 'ETIMEDOUT',
51
- 'ECONNRESET',
52
- 'ECONNREFUSED',
53
- 'ECONNABORTED',
54
- 'ENOTFOUND',
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 true;
64
+ return !nonIdempotent;
65
65
  }
66
- // HTTP status: retry on 429 (rate-limit) and 5xx (server errors) only
67
- const status = (error === null || error === void 0 ? void 0 : error.httpCode) || ((_b = error === null || error === void 0 ? void 0 : error.response) === null || _b === void 0 ? void 0 : _b.status);
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
- * Capture complete request information for debugging
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': `Basic ${Buffer.from(credentials.apiKey + ':').toString('base64')}`,
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
- const response = await withRetry(retry, () => helpers.httpRequest(options));
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: error.httpCode || ((_a = error.response) === null || _a === void 0 ? void 0 : _a.status) || 500,
218
- message: ((_b = error.response) === null || _b === void 0 ? void 0 : _b.data) || '',
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: ((_c = error.response) === null || _c === void 0 ? void 0 : _c.headers) || {},
225
- statusCode: error.httpCode || ((_d = error.response) === null || _d === void 0 ? void 0 : _d.status) || 500,
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' && customFields) {
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
- else {
38
- const fieldList = fields.split(',').map(f => f.trim()).filter(f => f).join(',');
39
- return `[${fieldList}]`;
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
- const singleResourceKey = keys.find(key => (key === resource || key === resource.replace(/s$/, '')) &&
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
- * Escapes XML special characters
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 escapeXml(unsafe) {
65
- const str = unsafe !== null && unsafe !== undefined ? String(unsafe) : '';
66
- return str.replace(/[<>&'"]/g, function (c) {
67
- switch (c) {
68
- case '<': return '&lt;';
69
- case '>': return '&gt;';
70
- case '&': return '&amp;';
71
- case "'": return '&apos;';
72
- case '"': return '&quot;';
73
- default: return c;
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}"><![CDATA[${escapeXml(fieldValue.value)}]]></language>\n`;
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}><![CDATA[${escapeXml(fieldValues[0].value)}]]></${fieldName}>\n`;
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><![CDATA[${escapeXml(id)}]]></id>\n`;
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.6.1",
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
- "packageManager": "pnpm@7.18.0",
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.4",
87
+ "form-data": "^4.0.6",
90
88
  "follow-redirects": "^1.16.0"
91
89
  }
92
90
  }