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.
- package/README.md +9 -3
- package/dist/nodes/Hudu/Hudu.node.js +2 -2
- package/dist/nodes/Hudu/Hudu.node.js.map +1 -1
- package/dist/nodes/Hudu/Hudu.node.ts +410 -413
- package/dist/nodes/Hudu/descriptions/asset_layout_fields.description.ts +450 -450
- package/dist/nodes/Hudu/descriptions/assets.description.ts +587 -587
- package/dist/nodes/Hudu/descriptions/magic_dash.description.js +9 -21
- package/dist/nodes/Hudu/descriptions/magic_dash.description.js.map +1 -1
- package/dist/nodes/Hudu/descriptions/magic_dash.description.ts +270 -282
- package/dist/nodes/Hudu/optionLoaders/asset_layouts/getAssetLayoutFields.ts +290 -290
- package/dist/nodes/Hudu/optionLoaders/index.ts +4 -4
- package/dist/nodes/Hudu/resources/assets/assets.handler.ts +345 -345
- package/dist/nodes/Hudu/resources/assets/assets.types.ts +43 -43
- package/dist/nodes/Hudu/resources/magic_dash/magic_dash.handler.js +8 -14
- package/dist/nodes/Hudu/resources/magic_dash/magic_dash.handler.js.map +1 -1
- package/dist/nodes/Hudu/resources/magic_dash/magic_dash.handler.ts +85 -85
- package/dist/nodes/Hudu/resources/magic_dash/magic_dash.types.ts +1 -1
- package/dist/nodes/Hudu/resources/public_photos/public_photos.handler.ts +153 -153
- package/dist/nodes/Hudu/utils/debugConfig.js +3 -3
- package/dist/nodes/Hudu/utils/debugConfig.js.map +1 -1
- package/dist/nodes/Hudu/utils/debugConfig.ts +3 -3
- package/dist/nodes/Hudu/utils/index.ts +24 -24
- package/dist/nodes/Hudu/utils/operations/magic_dash.js +4 -13
- package/dist/nodes/Hudu/utils/operations/magic_dash.js.map +1 -1
- package/dist/nodes/Hudu/utils/operations/magic_dash.ts +68 -85
- package/dist/nodes/Hudu/utils/operations/matchers.js +1 -11
- package/dist/nodes/Hudu/utils/operations/matchers.js.map +1 -1
- package/dist/nodes/Hudu/utils/operations/matchers.ts +7 -17
- package/dist/nodes/Hudu/utils/requestUtils.ts +490 -490
- 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
|
}
|