n8n-nodes-hudu 1.4.1 → 1.4.3

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.
Files changed (30) hide show
  1. package/README.md +9 -3
  2. package/dist/nodes/Hudu/Hudu.node.js +2 -2
  3. package/dist/nodes/Hudu/Hudu.node.js.map +1 -1
  4. package/dist/nodes/Hudu/Hudu.node.ts +410 -413
  5. package/dist/nodes/Hudu/descriptions/asset_layout_fields.description.ts +450 -450
  6. package/dist/nodes/Hudu/descriptions/assets.description.ts +587 -587
  7. package/dist/nodes/Hudu/descriptions/magic_dash.description.js +9 -21
  8. package/dist/nodes/Hudu/descriptions/magic_dash.description.js.map +1 -1
  9. package/dist/nodes/Hudu/descriptions/magic_dash.description.ts +270 -282
  10. package/dist/nodes/Hudu/optionLoaders/asset_layouts/getAssetLayoutFields.ts +290 -290
  11. package/dist/nodes/Hudu/optionLoaders/index.ts +4 -4
  12. package/dist/nodes/Hudu/resources/assets/assets.handler.ts +345 -345
  13. package/dist/nodes/Hudu/resources/assets/assets.types.ts +43 -43
  14. package/dist/nodes/Hudu/resources/magic_dash/magic_dash.handler.js +8 -14
  15. package/dist/nodes/Hudu/resources/magic_dash/magic_dash.handler.js.map +1 -1
  16. package/dist/nodes/Hudu/resources/magic_dash/magic_dash.handler.ts +85 -85
  17. package/dist/nodes/Hudu/resources/magic_dash/magic_dash.types.ts +1 -1
  18. package/dist/nodes/Hudu/resources/public_photos/public_photos.handler.ts +153 -153
  19. package/dist/nodes/Hudu/utils/debugConfig.js +3 -3
  20. package/dist/nodes/Hudu/utils/debugConfig.js.map +1 -1
  21. package/dist/nodes/Hudu/utils/debugConfig.ts +3 -3
  22. package/dist/nodes/Hudu/utils/index.ts +24 -24
  23. package/dist/nodes/Hudu/utils/operations/magic_dash.js +4 -13
  24. package/dist/nodes/Hudu/utils/operations/magic_dash.js.map +1 -1
  25. package/dist/nodes/Hudu/utils/operations/magic_dash.ts +68 -85
  26. package/dist/nodes/Hudu/utils/operations/matchers.js +1 -11
  27. package/dist/nodes/Hudu/utils/operations/matchers.js.map +1 -1
  28. package/dist/nodes/Hudu/utils/operations/matchers.ts +7 -17
  29. package/dist/nodes/Hudu/utils/requestUtils.ts +490 -490
  30. package/package.json +1 -1
@@ -1,491 +1,491 @@
1
- /**
2
- * HTTP request utilities for Hudu API integration
3
- *
4
- * Provides functionality for:
5
- * - Creating and executing HTTP requests to Hudu API
6
- * - Request configuration and credential handling
7
- * - Response parsing and error handling
8
- * - Type-safe data conversion between n8n and API formats
9
- */
10
-
11
- import type {
12
- IDataObject,
13
- IExecuteFunctions,
14
- IHookFunctions,
15
- ILoadOptionsFunctions,
16
- IHttpRequestMethods,
17
- IHttpRequestOptions,
18
- ICredentialDataDecryptedObject,
19
- JsonObject,
20
- } from 'n8n-workflow';
21
- import { NodeApiError } from 'n8n-workflow';
22
- import { HUDU_API_CONSTANTS, RESOURCES_WITH_PAGE_SIZE } from './constants';
23
- import type { FilterMapping } from './types';
24
- import { applyPostFilters } from './filterUtils';
25
- import { DEBUG_CONFIG, debugLog } from './debugConfig';
26
-
27
- export interface IHuduRequestOptions {
28
- method: IHttpRequestMethods;
29
- endpoint: string;
30
- body?: IDataObject;
31
- qs?: IDataObject;
32
- paginate?: boolean;
33
- contentType?: 'application/json' | 'application/x-www-form-urlencoded' | 'multipart/form-data';
34
- }
35
-
36
- // Rate limiting constants
37
- const RATE_LIMIT_CONFIG = {
38
- MAX_RETRIES: 3,
39
- BASE_DELAY_MS: 1000, // Start with 1 second delay
40
- MAX_DELAY_MS: 10000, // Maximum delay of 10 seconds
41
- JITTER_MS: 500, // Add up to 500ms of random jitter
42
- } as const;
43
-
44
- // HTTP Status Code Messages
45
- const HTTP_STATUS_MESSAGES = {
46
- // Client Errors
47
- 400: 'Bad Request - The request was malformed or contains invalid parameters',
48
- 401: 'Unauthorized - Invalid or missing API credentials',
49
- 403: 'Forbidden - You do not have permission to access this resource',
50
- 404: 'Not Found - The requested resource does not exist',
51
- 422: 'Unprocessable Entity - The request was well-formed but contains semantic errors',
52
- 429: 'Rate Limited - Too many requests, please try again later',
53
-
54
- // Server Errors
55
- 500: 'Internal Server Error - An unexpected error occurred on the Hudu server',
56
- 502: 'Bad Gateway - Unable to reach the Hudu server',
57
- 503: 'Service Unavailable - The Hudu service is temporarily unavailable',
58
- 504: 'Gateway Timeout - The request timed out while waiting for Hudu server',
59
- } as const;
60
-
61
- /**
62
- * Calculate delay for exponential backoff with jitter
63
- */
64
- function calculateBackoffDelay(retryCount: number, retryAfter?: number): number {
65
- // If we have a retry-after header, use that as the base delay
66
- const baseDelay = retryAfter ? retryAfter * 1000 : RATE_LIMIT_CONFIG.BASE_DELAY_MS;
67
-
68
- // Calculate exponential backoff with jitter
69
- const exponentialDelay = baseDelay * (2 ** retryCount);
70
- const jitter = Math.random() * RATE_LIMIT_CONFIG.JITTER_MS;
71
-
72
- return Math.min(exponentialDelay + jitter, RATE_LIMIT_CONFIG.MAX_DELAY_MS);
73
- }
74
-
75
- /**
76
- * Sleep for specified milliseconds
77
- */
78
- function sleep(ms: number): Promise<void> {
79
- return new Promise(resolve => setTimeout(resolve, ms));
80
- }
81
-
82
- /**
83
- * Convert IDataObject to JsonObject safely
84
- */
85
- export function toJsonObject(obj: IDataObject): JsonObject {
86
- const result: JsonObject = {};
87
- for (const [key, value] of Object.entries(obj)) {
88
- if (value !== undefined) {
89
- result[key] = value as JsonObject[string];
90
- }
91
- }
92
- return result;
93
- }
94
-
95
- /**
96
- * Create an HTTP request configuration for Hudu API
97
- */
98
- export function createHuduRequest(
99
- credentials: ICredentialDataDecryptedObject,
100
- options: IHuduRequestOptions,
101
- ): IHttpRequestOptions {
102
- const { method, endpoint, body = {}, qs = {} } = options;
103
- let contentType = method === 'GET' ? 'application/x-www-form-urlencoded' : 'application/json';
104
-
105
- if (!credentials?.apiKey || !credentials?.baseUrl) {
106
- throw new Error('Missing API credentials. Please provide both the API key and base URL.');
107
- }
108
-
109
- const requestOptions: IHttpRequestOptions = {
110
- method,
111
- url: `${credentials.baseUrl}${HUDU_API_CONSTANTS.BASE_API_PATH}${endpoint}`,
112
- qs: toJsonObject(qs),
113
- headers: {
114
- 'x-api-key': credentials.apiKey as string,
115
- },
116
- };
117
-
118
- // Detect multipart
119
- if ((body as any)._isMultipart) {
120
- contentType = 'multipart/form-data';
121
- (requestOptions as any).formData = { ...body };
122
- delete (requestOptions as any).formData._isMultipart;
123
- } else if (Object.keys(body).length > 0) {
124
- if (contentType === 'application/json') {
125
- requestOptions.json = true;
126
- }
127
- requestOptions.body = toJsonObject(body);
128
- }
129
-
130
- if (requestOptions.headers) {
131
- requestOptions.headers['Content-Type'] = contentType;
132
- }
133
-
134
- return requestOptions;
135
- }
136
-
137
- /**
138
- * Get a descriptive error message for an HTTP status code
139
- */
140
- function getErrorMessage(statusCode: number, defaultMessage: string): string {
141
- return HTTP_STATUS_MESSAGES[statusCode as keyof typeof HTTP_STATUS_MESSAGES] || defaultMessage;
142
- }
143
-
144
- /**
145
- * Execute an HTTP request to Hudu API
146
- */
147
- export async function executeHuduRequest(
148
- this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
149
- requestOptions: IHttpRequestOptions,
150
- ): Promise<IDataObject | IDataObject[]> {
151
- const helpers = this.helpers;
152
- if (!helpers?.request) {
153
- throw new Error('Request helper not available');
154
- }
155
-
156
- let retryCount = 0;
157
-
158
- while (true) {
159
- try {
160
- if (DEBUG_CONFIG.API_REQUEST) {
161
- debugLog('[API_REQUEST] Hudu API Request:', {
162
- method: requestOptions.method,
163
- url: requestOptions.url,
164
- headers: requestOptions.headers,
165
- qs: requestOptions.qs,
166
- body: requestOptions.body,
167
- formData: (requestOptions as any).formData,
168
- });
169
- }
170
-
171
- // Get the raw response
172
- const rawResponse = await helpers.request(requestOptions);
173
-
174
- if (DEBUG_CONFIG.API_RESPONSE) {
175
- debugLog('[API_RESPONSE] Hudu API Response:', {
176
- statusCode: (rawResponse as any)?.statusCode,
177
- headers: (rawResponse as any)?.headers,
178
- body: rawResponse,
179
- });
180
- }
181
-
182
- // Parse the response if it's a string
183
- const response = typeof rawResponse === 'string' ? JSON.parse(rawResponse) : rawResponse;
184
-
185
- // Return empty array for null/undefined responses
186
- if (response === null || response === undefined) {
187
- return [];
188
- }
189
-
190
- return response;
191
- } catch (error) {
192
- // For DELETE requests, a 204 status code is a success, but n8n's request helper
193
- // may throw an error because there is no response body. We intercept this case.
194
- const err = error as IDataObject;
195
- const context = err.context as IDataObject | undefined;
196
- const responseContext = context?.response as IDataObject | undefined;
197
- const statusCode = err.statusCode ?? responseContext?.statusCode;
198
-
199
- if (requestOptions.method === 'DELETE' && statusCode === 204) {
200
- debugLog('[API_RESPONSE] Handled 204 No Content for DELETE request', {
201
- url: requestOptions.url,
202
- });
203
- return { success: true };
204
- }
205
-
206
- if (DEBUG_CONFIG.API_REQUEST) {
207
- debugLog('Hudu API Error', {
208
- error,
209
- message: error instanceof Error ? error.message : String(error),
210
- stack: error instanceof Error ? error.stack : undefined,
211
- retryCount,
212
- statusCode: error.statusCode,
213
- level: 'error',
214
- });
215
- }
216
-
217
- // Check if it's a rate limit error and we haven't exceeded max retries
218
- if (error.statusCode === 429 && retryCount < RATE_LIMIT_CONFIG.MAX_RETRIES) {
219
- // Get retry-after header if available (in seconds)
220
- const retryAfter = error.response?.headers?.['retry-after'];
221
- const retryAfterMs = retryAfter ? Number.parseInt(retryAfter, 10) : undefined;
222
-
223
- // Calculate delay with exponential backoff
224
- const delayMs = calculateBackoffDelay(retryCount, retryAfterMs);
225
-
226
- if (DEBUG_CONFIG.API_REQUEST) {
227
- debugLog('Rate Limited - Retrying', {
228
- retryCount,
229
- retryAfter: retryAfterMs,
230
- delayMs,
231
- });
232
- }
233
-
234
- // Wait before retrying
235
- await sleep(delayMs);
236
- retryCount++;
237
- continue;
238
- }
239
-
240
- // Format error with specific status code message
241
- const jsonError: JsonObject = {};
242
-
243
- const axiosError = error as {
244
- response?: {
245
- body?: any;
246
- headers?: any;
247
- };
248
- statusCode?: number;
249
- };
250
-
251
- // Preserve the original error message which may contain JSON details
252
- const originalMessage = error.message || 'Unknown error';
253
-
254
- // Get status code specific message as fallback
255
- const fallbackMessage = axiosError.statusCode ?
256
- getErrorMessage(axiosError.statusCode, originalMessage) :
257
- originalMessage;
258
-
259
- let errorDetails = '';
260
- if (axiosError.response?.body) {
261
- try {
262
- const body = typeof axiosError.response.body === 'string' ? JSON.parse(axiosError.response.body) : axiosError.response.body;
263
- if (body.error) {
264
- errorDetails = body.error;
265
- if (body.details && Array.isArray(body.details)) {
266
- errorDetails += `: ${body.details.join(', ')}`;
267
- }
268
- }
269
- } catch (e) {
270
- // Ignore parsing errors
271
- }
272
- }
273
-
274
- if (DEBUG_CONFIG.API_RESPONSE) {
275
- debugLog('[API_RESPONSE] Hudu API Error Response:', {
276
- statusCode: axiosError.statusCode,
277
- headers: axiosError.response?.headers,
278
- body: axiosError.response?.body,
279
- });
280
- }
281
-
282
- if (error instanceof Error) {
283
- // Preserve original message if it contains JSON details, otherwise use formatted message
284
- if (originalMessage.includes('{') && originalMessage.includes('}')) {
285
- jsonError.message = originalMessage;
286
- } else {
287
- jsonError.message = errorDetails ? `${fallbackMessage} - ${errorDetails}` : fallbackMessage;
288
- }
289
- jsonError.name = error.name;
290
- if (error.stack) {
291
- jsonError.stack = error.stack;
292
- }
293
- // Include any additional error details from the response
294
- if (axiosError.response?.body) {
295
- try {
296
- const errorBody = typeof axiosError.response.body === 'string' ?
297
- JSON.parse(axiosError.response.body) :
298
- axiosError.response.body;
299
- jsonError.details = errorBody;
300
- } catch {
301
- // If parsing fails, include raw error body
302
- jsonError.details = axiosError.response.body;
303
- }
304
- }
305
- } else if (typeof error === 'object' && error !== null) {
306
- const errorObj = toJsonObject(error as IDataObject);
307
- jsonError.message = errorDetails ? `${fallbackMessage} - ${errorDetails}` : fallbackMessage;
308
- // Include any additional error details from the response
309
- if (axiosError.response?.body) {
310
- try {
311
- const errorBody = typeof axiosError.response.body === 'string' ?
312
- JSON.parse(axiosError.response.body) :
313
- axiosError.response.body;
314
- jsonError.details = errorBody;
315
- } catch {
316
- // If parsing fails, include raw error body
317
- jsonError.details = axiosError.response.body;
318
- }
319
- }
320
- Object.assign(jsonError, errorObj);
321
- } else {
322
- jsonError.message = errorDetails ? `${fallbackMessage} - ${errorDetails}` : fallbackMessage;
323
- jsonError.error = String(error);
324
- }
325
-
326
- // Add status code to error object
327
- if (axiosError.statusCode) {
328
- jsonError.statusCode = axiosError.statusCode;
329
- }
330
-
331
- throw new NodeApiError(this.getNode(), jsonError);
332
- }
333
- }
334
- }
335
-
336
- /**
337
- * Parse Hudu API response based on expected format
338
- */
339
- export function parseHuduResponse(
340
- response: IDataObject | IDataObject[],
341
- resourceName?: string,
342
- ): IDataObject[] {
343
- // If response is empty or undefined, return empty array
344
- if (!response || (typeof response === 'object' && Object.keys(response).length === 0)) {
345
- return [];
346
- }
347
-
348
- // If response is already an array, return it if not empty
349
- if (Array.isArray(response)) {
350
- return response.filter(item => item && Object.keys(item).length > 0);
351
- }
352
-
353
- // If we have a resource name, the response should be an object with that key
354
- if (resourceName) {
355
- // Handle case where response is an object containing the array
356
- const data = response as IDataObject;
357
- if (data[resourceName] !== undefined) {
358
- const resourceData = data[resourceName];
359
- if (Array.isArray(resourceData)) {
360
- return resourceData.filter(item => item && Object.keys(item).length > 0) as IDataObject[];
361
- }
362
- // If it's a single item and not empty, wrap it in an array
363
- if (resourceData !== null && typeof resourceData === 'object' && Object.keys(resourceData).length > 0) {
364
- return [resourceData as IDataObject];
365
- }
366
- }
367
-
368
- // If we can't find the data in the expected format, return empty array
369
- return [];
370
- }
371
-
372
- // If no resource name and response is a non-empty object, wrap it in an array
373
- return Object.keys(response).length > 0 ? [response as IDataObject] : [];
374
- }
375
-
376
- /**
377
- * Make a request to Hudu API with proper error handling
378
- */
379
- export async function huduApiRequest(
380
- this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
381
- method: IHttpRequestMethods,
382
- endpoint: string,
383
- body: IDataObject = {},
384
- qs: IDataObject = {},
385
- resourceName?: string,
386
- ): Promise<IDataObject | IDataObject[]> {
387
- const credentials = await this.getCredentials('huduApi');
388
- const requestOptions = createHuduRequest(credentials, { method, endpoint, body, qs });
389
- const response = await executeHuduRequest.call(this, requestOptions);
390
- return resourceName ? parseHuduResponse(response, resourceName) : response;
391
- }
392
-
393
- /**
394
- * Handle paginated listings from Hudu API with proper error handling
395
- */
396
- export async function handleListing<T extends IDataObject>(
397
- this: IExecuteFunctions | ILoadOptionsFunctions,
398
- method: IHttpRequestMethods,
399
- endpoint: string,
400
- resourceName?: string,
401
- body: IDataObject = {},
402
- query: IDataObject = {},
403
- returnAll = false,
404
- limit = 0,
405
- postProcessFilters?: T,
406
- filterMapping?: FilterMapping<T>,
407
- ): Promise<IDataObject[]> {
408
- const results: IDataObject[] = [];
409
- let filteredResults: IDataObject[] = [];
410
- let hasMore = true;
411
- let page = 1;
412
-
413
- // Optimise page size if we have a specific limit less than default page size
414
- const pageSize = !returnAll && limit > 0 && limit < HUDU_API_CONSTANTS.PAGE_SIZE
415
- ? limit
416
- : HUDU_API_CONSTANTS.PAGE_SIZE;
417
-
418
- //debugLog('[handleListing] Start', { endpoint, resourceName, returnAll, limit, pageSize });
419
-
420
- // Check if this resource supports pagination
421
- const resourcePath = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
422
- const supportsPagination = RESOURCES_WITH_PAGE_SIZE.includes(resourcePath as any);
423
-
424
- // Keep fetching until we have enough filtered results or no more data
425
- while (hasMore) {
426
- // Only include pagination parameters if the resource supports them
427
- const queryParams = { ...query };
428
-
429
- if (supportsPagination) {
430
- queryParams.page = page;
431
- queryParams.page_size = pageSize;
432
- }
433
-
434
- const response = await huduApiRequest.call(this, method, endpoint, body, queryParams, resourceName);
435
- const batchResults = parseHuduResponse(response, resourceName);
436
-
437
- //debugLog('[handleListing] Page fetched', {
438
- // endpoint,
439
- // resourceName,
440
- // page,
441
- // supportsPagination,
442
- // batchCount: batchResults.length,
443
- // cumulativeCount: results.length + batchResults.length
444
- //});
445
-
446
- if (batchResults.length === 0) {
447
- hasMore = false;
448
- continue;
449
- }
450
-
451
- results.push(...batchResults);
452
-
453
- // Apply filters to all results we have so far
454
- if (postProcessFilters && filterMapping) {
455
- filteredResults = applyPostFilters(
456
- results,
457
- postProcessFilters,
458
- filterMapping as Record<string, (item: IDataObject, value: unknown) => boolean>,
459
- );
460
- } else {
461
- filteredResults = results;
462
- }
463
-
464
- // If resource doesn't support pagination, we get all data in one request
465
- if (!supportsPagination) {
466
- hasMore = false;
467
- } else {
468
- // Determine if we should continue fetching for resources with pagination
469
- if (!returnAll) {
470
- if (filteredResults.length >= limit) {
471
- hasMore = false;
472
- } else {
473
- // Continue if there might be more results
474
- hasMore = batchResults.length === pageSize;
475
- }
476
- } else {
477
- // If returning all, continue if there might be more results
478
- hasMore = batchResults.length === pageSize;
479
- }
480
-
481
- page++;
482
- }
483
- }
484
-
485
- // Slice to exact limit if we're not returning all
486
- if (!returnAll && limit && filteredResults.length > limit) {
487
- filteredResults = filteredResults.slice(0, limit);
488
- }
489
-
490
- return filteredResults;
1
+ /**
2
+ * HTTP request utilities for Hudu API integration
3
+ *
4
+ * Provides functionality for:
5
+ * - Creating and executing HTTP requests to Hudu API
6
+ * - Request configuration and credential handling
7
+ * - Response parsing and error handling
8
+ * - Type-safe data conversion between n8n and API formats
9
+ */
10
+
11
+ import type {
12
+ IDataObject,
13
+ IExecuteFunctions,
14
+ IHookFunctions,
15
+ ILoadOptionsFunctions,
16
+ IHttpRequestMethods,
17
+ IHttpRequestOptions,
18
+ ICredentialDataDecryptedObject,
19
+ JsonObject,
20
+ } from 'n8n-workflow';
21
+ import { NodeApiError } from 'n8n-workflow';
22
+ import { HUDU_API_CONSTANTS, RESOURCES_WITH_PAGE_SIZE } from './constants';
23
+ import type { FilterMapping } from './types';
24
+ import { applyPostFilters } from './filterUtils';
25
+ import { DEBUG_CONFIG, debugLog } from './debugConfig';
26
+
27
+ export interface IHuduRequestOptions {
28
+ method: IHttpRequestMethods;
29
+ endpoint: string;
30
+ body?: IDataObject;
31
+ qs?: IDataObject;
32
+ paginate?: boolean;
33
+ contentType?: 'application/json' | 'application/x-www-form-urlencoded' | 'multipart/form-data';
34
+ }
35
+
36
+ // Rate limiting constants
37
+ const RATE_LIMIT_CONFIG = {
38
+ MAX_RETRIES: 3,
39
+ BASE_DELAY_MS: 1000, // Start with 1 second delay
40
+ MAX_DELAY_MS: 10000, // Maximum delay of 10 seconds
41
+ JITTER_MS: 500, // Add up to 500ms of random jitter
42
+ } as const;
43
+
44
+ // HTTP Status Code Messages
45
+ const HTTP_STATUS_MESSAGES = {
46
+ // Client Errors
47
+ 400: 'Bad Request - The request was malformed or contains invalid parameters',
48
+ 401: 'Unauthorized - Invalid or missing API credentials',
49
+ 403: 'Forbidden - You do not have permission to access this resource',
50
+ 404: 'Not Found - The requested resource does not exist',
51
+ 422: 'Unprocessable Entity - The request was well-formed but contains semantic errors',
52
+ 429: 'Rate Limited - Too many requests, please try again later',
53
+
54
+ // Server Errors
55
+ 500: 'Internal Server Error - An unexpected error occurred on the Hudu server',
56
+ 502: 'Bad Gateway - Unable to reach the Hudu server',
57
+ 503: 'Service Unavailable - The Hudu service is temporarily unavailable',
58
+ 504: 'Gateway Timeout - The request timed out while waiting for Hudu server',
59
+ } as const;
60
+
61
+ /**
62
+ * Calculate delay for exponential backoff with jitter
63
+ */
64
+ function calculateBackoffDelay(retryCount: number, retryAfter?: number): number {
65
+ // If we have a retry-after header, use that as the base delay
66
+ const baseDelay = retryAfter ? retryAfter * 1000 : RATE_LIMIT_CONFIG.BASE_DELAY_MS;
67
+
68
+ // Calculate exponential backoff with jitter
69
+ const exponentialDelay = baseDelay * (2 ** retryCount);
70
+ const jitter = Math.random() * RATE_LIMIT_CONFIG.JITTER_MS;
71
+
72
+ return Math.min(exponentialDelay + jitter, RATE_LIMIT_CONFIG.MAX_DELAY_MS);
73
+ }
74
+
75
+ /**
76
+ * Sleep for specified milliseconds
77
+ */
78
+ function sleep(ms: number): Promise<void> {
79
+ return new Promise(resolve => setTimeout(resolve, ms));
80
+ }
81
+
82
+ /**
83
+ * Convert IDataObject to JsonObject safely
84
+ */
85
+ export function toJsonObject(obj: IDataObject): JsonObject {
86
+ const result: JsonObject = {};
87
+ for (const [key, value] of Object.entries(obj)) {
88
+ if (value !== undefined) {
89
+ result[key] = value as JsonObject[string];
90
+ }
91
+ }
92
+ return result;
93
+ }
94
+
95
+ /**
96
+ * Create an HTTP request configuration for Hudu API
97
+ */
98
+ export function createHuduRequest(
99
+ credentials: ICredentialDataDecryptedObject,
100
+ options: IHuduRequestOptions,
101
+ ): IHttpRequestOptions {
102
+ const { method, endpoint, body = {}, qs = {} } = options;
103
+ let contentType = method === 'GET' ? 'application/x-www-form-urlencoded' : 'application/json';
104
+
105
+ if (!credentials?.apiKey || !credentials?.baseUrl) {
106
+ throw new Error('Missing API credentials. Please provide both the API key and base URL.');
107
+ }
108
+
109
+ const requestOptions: IHttpRequestOptions = {
110
+ method,
111
+ url: `${credentials.baseUrl}${HUDU_API_CONSTANTS.BASE_API_PATH}${endpoint}`,
112
+ qs: toJsonObject(qs),
113
+ headers: {
114
+ 'x-api-key': credentials.apiKey as string,
115
+ },
116
+ };
117
+
118
+ // Detect multipart
119
+ if ((body as any)._isMultipart) {
120
+ contentType = 'multipart/form-data';
121
+ (requestOptions as any).formData = { ...body };
122
+ delete (requestOptions as any).formData._isMultipart;
123
+ } else if (Object.keys(body).length > 0) {
124
+ if (contentType === 'application/json') {
125
+ requestOptions.json = true;
126
+ }
127
+ requestOptions.body = toJsonObject(body);
128
+ }
129
+
130
+ if (requestOptions.headers) {
131
+ requestOptions.headers['Content-Type'] = contentType;
132
+ }
133
+
134
+ return requestOptions;
135
+ }
136
+
137
+ /**
138
+ * Get a descriptive error message for an HTTP status code
139
+ */
140
+ function getErrorMessage(statusCode: number, defaultMessage: string): string {
141
+ return HTTP_STATUS_MESSAGES[statusCode as keyof typeof HTTP_STATUS_MESSAGES] || defaultMessage;
142
+ }
143
+
144
+ /**
145
+ * Execute an HTTP request to Hudu API
146
+ */
147
+ export async function executeHuduRequest(
148
+ this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
149
+ requestOptions: IHttpRequestOptions,
150
+ ): Promise<IDataObject | IDataObject[]> {
151
+ const helpers = this.helpers;
152
+ if (!helpers?.request) {
153
+ throw new Error('Request helper not available');
154
+ }
155
+
156
+ let retryCount = 0;
157
+
158
+ while (true) {
159
+ try {
160
+ if (DEBUG_CONFIG.API_REQUEST) {
161
+ debugLog('[API_REQUEST] Hudu API Request:', {
162
+ method: requestOptions.method,
163
+ url: requestOptions.url,
164
+ headers: requestOptions.headers,
165
+ qs: requestOptions.qs,
166
+ body: requestOptions.body,
167
+ formData: (requestOptions as any).formData,
168
+ });
169
+ }
170
+
171
+ // Get the raw response
172
+ const rawResponse = await helpers.request(requestOptions);
173
+
174
+ if (DEBUG_CONFIG.API_RESPONSE) {
175
+ debugLog('[API_RESPONSE] Hudu API Response:', {
176
+ statusCode: (rawResponse as any)?.statusCode,
177
+ headers: (rawResponse as any)?.headers,
178
+ body: rawResponse,
179
+ });
180
+ }
181
+
182
+ // Parse the response if it's a string
183
+ const response = typeof rawResponse === 'string' ? JSON.parse(rawResponse) : rawResponse;
184
+
185
+ // Return empty array for null/undefined responses
186
+ if (response === null || response === undefined) {
187
+ return [];
188
+ }
189
+
190
+ return response;
191
+ } catch (error) {
192
+ // For DELETE requests, a 204 status code is a success, but n8n's request helper
193
+ // may throw an error because there is no response body. We intercept this case.
194
+ const err = error as IDataObject;
195
+ const context = err.context as IDataObject | undefined;
196
+ const responseContext = context?.response as IDataObject | undefined;
197
+ const statusCode = err.statusCode ?? responseContext?.statusCode;
198
+
199
+ if (requestOptions.method === 'DELETE' && statusCode === 204) {
200
+ debugLog('[API_RESPONSE] Handled 204 No Content for DELETE request', {
201
+ url: requestOptions.url,
202
+ });
203
+ return { success: true };
204
+ }
205
+
206
+ if (DEBUG_CONFIG.API_REQUEST) {
207
+ debugLog('Hudu API Error', {
208
+ error,
209
+ message: error instanceof Error ? error.message : String(error),
210
+ stack: error instanceof Error ? error.stack : undefined,
211
+ retryCount,
212
+ statusCode: error.statusCode,
213
+ level: 'error',
214
+ });
215
+ }
216
+
217
+ // Check if it's a rate limit error and we haven't exceeded max retries
218
+ if (error.statusCode === 429 && retryCount < RATE_LIMIT_CONFIG.MAX_RETRIES) {
219
+ // Get retry-after header if available (in seconds)
220
+ const retryAfter = error.response?.headers?.['retry-after'];
221
+ const retryAfterMs = retryAfter ? Number.parseInt(retryAfter, 10) : undefined;
222
+
223
+ // Calculate delay with exponential backoff
224
+ const delayMs = calculateBackoffDelay(retryCount, retryAfterMs);
225
+
226
+ if (DEBUG_CONFIG.API_REQUEST) {
227
+ debugLog('Rate Limited - Retrying', {
228
+ retryCount,
229
+ retryAfter: retryAfterMs,
230
+ delayMs,
231
+ });
232
+ }
233
+
234
+ // Wait before retrying
235
+ await sleep(delayMs);
236
+ retryCount++;
237
+ continue;
238
+ }
239
+
240
+ // Format error with specific status code message
241
+ const jsonError: JsonObject = {};
242
+
243
+ const axiosError = error as {
244
+ response?: {
245
+ body?: any;
246
+ headers?: any;
247
+ };
248
+ statusCode?: number;
249
+ };
250
+
251
+ // Preserve the original error message which may contain JSON details
252
+ const originalMessage = error.message || 'Unknown error';
253
+
254
+ // Get status code specific message as fallback
255
+ const fallbackMessage = axiosError.statusCode ?
256
+ getErrorMessage(axiosError.statusCode, originalMessage) :
257
+ originalMessage;
258
+
259
+ let errorDetails = '';
260
+ if (axiosError.response?.body) {
261
+ try {
262
+ const body = typeof axiosError.response.body === 'string' ? JSON.parse(axiosError.response.body) : axiosError.response.body;
263
+ if (body.error) {
264
+ errorDetails = body.error;
265
+ if (body.details && Array.isArray(body.details)) {
266
+ errorDetails += `: ${body.details.join(', ')}`;
267
+ }
268
+ }
269
+ } catch (e) {
270
+ // Ignore parsing errors
271
+ }
272
+ }
273
+
274
+ if (DEBUG_CONFIG.API_RESPONSE) {
275
+ debugLog('[API_RESPONSE] Hudu API Error Response:', {
276
+ statusCode: axiosError.statusCode,
277
+ headers: axiosError.response?.headers,
278
+ body: axiosError.response?.body,
279
+ });
280
+ }
281
+
282
+ if (error instanceof Error) {
283
+ // Preserve original message if it contains JSON details, otherwise use formatted message
284
+ if (originalMessage.includes('{') && originalMessage.includes('}')) {
285
+ jsonError.message = originalMessage;
286
+ } else {
287
+ jsonError.message = errorDetails ? `${fallbackMessage} - ${errorDetails}` : fallbackMessage;
288
+ }
289
+ jsonError.name = error.name;
290
+ if (error.stack) {
291
+ jsonError.stack = error.stack;
292
+ }
293
+ // Include any additional error details from the response
294
+ if (axiosError.response?.body) {
295
+ try {
296
+ const errorBody = typeof axiosError.response.body === 'string' ?
297
+ JSON.parse(axiosError.response.body) :
298
+ axiosError.response.body;
299
+ jsonError.details = errorBody;
300
+ } catch {
301
+ // If parsing fails, include raw error body
302
+ jsonError.details = axiosError.response.body;
303
+ }
304
+ }
305
+ } else if (typeof error === 'object' && error !== null) {
306
+ const errorObj = toJsonObject(error as IDataObject);
307
+ jsonError.message = errorDetails ? `${fallbackMessage} - ${errorDetails}` : fallbackMessage;
308
+ // Include any additional error details from the response
309
+ if (axiosError.response?.body) {
310
+ try {
311
+ const errorBody = typeof axiosError.response.body === 'string' ?
312
+ JSON.parse(axiosError.response.body) :
313
+ axiosError.response.body;
314
+ jsonError.details = errorBody;
315
+ } catch {
316
+ // If parsing fails, include raw error body
317
+ jsonError.details = axiosError.response.body;
318
+ }
319
+ }
320
+ Object.assign(jsonError, errorObj);
321
+ } else {
322
+ jsonError.message = errorDetails ? `${fallbackMessage} - ${errorDetails}` : fallbackMessage;
323
+ jsonError.error = String(error);
324
+ }
325
+
326
+ // Add status code to error object
327
+ if (axiosError.statusCode) {
328
+ jsonError.statusCode = axiosError.statusCode;
329
+ }
330
+
331
+ throw new NodeApiError(this.getNode(), jsonError);
332
+ }
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Parse Hudu API response based on expected format
338
+ */
339
+ export function parseHuduResponse(
340
+ response: IDataObject | IDataObject[],
341
+ resourceName?: string,
342
+ ): IDataObject[] {
343
+ // If response is empty or undefined, return empty array
344
+ if (!response || (typeof response === 'object' && Object.keys(response).length === 0)) {
345
+ return [];
346
+ }
347
+
348
+ // If response is already an array, return it if not empty
349
+ if (Array.isArray(response)) {
350
+ return response.filter(item => item && Object.keys(item).length > 0);
351
+ }
352
+
353
+ // If we have a resource name, the response should be an object with that key
354
+ if (resourceName) {
355
+ // Handle case where response is an object containing the array
356
+ const data = response as IDataObject;
357
+ if (data[resourceName] !== undefined) {
358
+ const resourceData = data[resourceName];
359
+ if (Array.isArray(resourceData)) {
360
+ return resourceData.filter(item => item && Object.keys(item).length > 0) as IDataObject[];
361
+ }
362
+ // If it's a single item and not empty, wrap it in an array
363
+ if (resourceData !== null && typeof resourceData === 'object' && Object.keys(resourceData).length > 0) {
364
+ return [resourceData as IDataObject];
365
+ }
366
+ }
367
+
368
+ // If we can't find the data in the expected format, return empty array
369
+ return [];
370
+ }
371
+
372
+ // If no resource name and response is a non-empty object, wrap it in an array
373
+ return Object.keys(response).length > 0 ? [response as IDataObject] : [];
374
+ }
375
+
376
+ /**
377
+ * Make a request to Hudu API with proper error handling
378
+ */
379
+ export async function huduApiRequest(
380
+ this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions,
381
+ method: IHttpRequestMethods,
382
+ endpoint: string,
383
+ body: IDataObject = {},
384
+ qs: IDataObject = {},
385
+ resourceName?: string,
386
+ ): Promise<IDataObject | IDataObject[]> {
387
+ const credentials = await this.getCredentials('huduApi');
388
+ const requestOptions = createHuduRequest(credentials, { method, endpoint, body, qs });
389
+ const response = await executeHuduRequest.call(this, requestOptions);
390
+ return resourceName ? parseHuduResponse(response, resourceName) : response;
391
+ }
392
+
393
+ /**
394
+ * Handle paginated listings from Hudu API with proper error handling
395
+ */
396
+ export async function handleListing<T extends IDataObject>(
397
+ this: IExecuteFunctions | ILoadOptionsFunctions,
398
+ method: IHttpRequestMethods,
399
+ endpoint: string,
400
+ resourceName?: string,
401
+ body: IDataObject = {},
402
+ query: IDataObject = {},
403
+ returnAll = false,
404
+ limit = 0,
405
+ postProcessFilters?: T,
406
+ filterMapping?: FilterMapping<T>,
407
+ ): Promise<IDataObject[]> {
408
+ const results: IDataObject[] = [];
409
+ let filteredResults: IDataObject[] = [];
410
+ let hasMore = true;
411
+ let page = 1;
412
+
413
+ // Optimise page size if we have a specific limit less than default page size
414
+ const pageSize = !returnAll && limit > 0 && limit < HUDU_API_CONSTANTS.PAGE_SIZE
415
+ ? limit
416
+ : HUDU_API_CONSTANTS.PAGE_SIZE;
417
+
418
+ //debugLog('[handleListing] Start', { endpoint, resourceName, returnAll, limit, pageSize });
419
+
420
+ // Check if this resource supports pagination
421
+ const resourcePath = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint;
422
+ const supportsPagination = RESOURCES_WITH_PAGE_SIZE.includes(resourcePath as any);
423
+
424
+ // Keep fetching until we have enough filtered results or no more data
425
+ while (hasMore) {
426
+ // Only include pagination parameters if the resource supports them
427
+ const queryParams = { ...query };
428
+
429
+ if (supportsPagination) {
430
+ queryParams.page = page;
431
+ queryParams.page_size = pageSize;
432
+ }
433
+
434
+ const response = await huduApiRequest.call(this, method, endpoint, body, queryParams, resourceName);
435
+ const batchResults = parseHuduResponse(response, resourceName);
436
+
437
+ //debugLog('[handleListing] Page fetched', {
438
+ // endpoint,
439
+ // resourceName,
440
+ // page,
441
+ // supportsPagination,
442
+ // batchCount: batchResults.length,
443
+ // cumulativeCount: results.length + batchResults.length
444
+ //});
445
+
446
+ if (batchResults.length === 0) {
447
+ hasMore = false;
448
+ continue;
449
+ }
450
+
451
+ results.push(...batchResults);
452
+
453
+ // Apply filters to all results we have so far
454
+ if (postProcessFilters && filterMapping) {
455
+ filteredResults = applyPostFilters(
456
+ results,
457
+ postProcessFilters,
458
+ filterMapping as Record<string, (item: IDataObject, value: unknown) => boolean>,
459
+ );
460
+ } else {
461
+ filteredResults = results;
462
+ }
463
+
464
+ // If resource doesn't support pagination, we get all data in one request
465
+ if (!supportsPagination) {
466
+ hasMore = false;
467
+ } else {
468
+ // Determine if we should continue fetching for resources with pagination
469
+ if (!returnAll) {
470
+ if (filteredResults.length >= limit) {
471
+ hasMore = false;
472
+ } else {
473
+ // Continue if there might be more results
474
+ hasMore = batchResults.length === pageSize;
475
+ }
476
+ } else {
477
+ // If returning all, continue if there might be more results
478
+ hasMore = batchResults.length === pageSize;
479
+ }
480
+
481
+ page++;
482
+ }
483
+ }
484
+
485
+ // Slice to exact limit if we're not returning all
486
+ if (!returnAll && limit && filteredResults.length > limit) {
487
+ filteredResults = filteredResults.slice(0, limit);
488
+ }
489
+
490
+ return filteredResults;
491
491
  }