n8n-nodes-duckduckgo-search 32.0.0 → 32.1.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
@@ -1,9 +1,9 @@
1
1
  # DuckDuckGo Search Node for n8n
2
2
 
3
- [![npm version](https://img.shields.io/npm/v/n8n-nodes-duckduckgo-search.svg?v=32.0.0)](https://www.npmjs.com/package/n8n-nodes-duckduckgo-search)
3
+ [![npm version](https://img.shields.io/npm/v/n8n-nodes-duckduckgo-search.svg?v=32.1.0)](https://www.npmjs.com/package/n8n-nodes-duckduckgo-search)
4
4
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
5
 
6
- A lightweight and fast n8n community node for DuckDuckGo search. Search the web, find images, discover news, and explore videos with privacy-focused results.
6
+ A professional-grade n8n community node for DuckDuckGo search with enterprise-level error handling, validation, and quality assurance. Search the web, find images, discover news, and explore videos with privacy-focused, reliable results.
7
7
 
8
8
  ## ✨ Features
9
9
 
@@ -6,6 +6,9 @@ const duck_duck_scrape_1 = require("duck-duck-scrape");
6
6
  const types_1 = require("./types");
7
7
  const processors_1 = require("./processors");
8
8
  const constants_1 = require("./constants");
9
+ const errors_1 = require("./errors");
10
+ const validation_1 = require("./validation");
11
+ const reliability_1 = require("./reliability");
9
12
  const LOCALE_OPTIONS = [
10
13
  { name: 'English (US)', value: 'en-us' },
11
14
  { name: 'English (UK)', value: 'uk-en' },
@@ -202,102 +205,156 @@ class DuckDuckGo {
202
205
  };
203
206
  }
204
207
  async execute() {
205
- var _a, _b, _c, _d;
206
208
  const items = this.getInputData();
207
209
  const returnData = [];
210
+ const requestQueue = (0, reliability_1.getRequestQueue)(1000);
208
211
  for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
209
212
  try {
210
213
  const operation = this.getNodeParameter('operation', itemIndex);
211
- const query = this.getNodeParameter('query', itemIndex);
212
- const locale = this.getNodeParameter('locale', itemIndex);
213
- const maxResults = this.getNodeParameter('maxResults', itemIndex);
214
- const region = this.getNodeParameter('region', itemIndex);
215
- const safeSearch = this.getNodeParameter('safeSearch', itemIndex);
216
- const timePeriod = this.getNodeParameter('timePeriod', itemIndex);
217
- if (!query || query.trim() === '') {
218
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'Search query cannot be empty', { itemIndex });
214
+ const rawQuery = this.getNodeParameter('query', itemIndex);
215
+ const rawLocale = this.getNodeParameter('locale', itemIndex);
216
+ const rawMaxResults = this.getNodeParameter('maxResults', itemIndex);
217
+ const rawRegion = this.getNodeParameter('region', itemIndex);
218
+ const rawSafeSearch = this.getNodeParameter('safeSearch', itemIndex);
219
+ const rawTimePeriod = this.getNodeParameter('timePeriod', itemIndex);
220
+ let query;
221
+ let maxResults;
222
+ let locale;
223
+ let region;
224
+ let safeSearch;
225
+ let timePeriod;
226
+ try {
227
+ query = (0, validation_1.validateSearchQuery)(rawQuery);
228
+ maxResults = (0, validation_1.validateMaxResults)(rawMaxResults);
229
+ locale = (0, validation_1.validateLocale)(rawLocale);
230
+ region = (0, validation_1.validateRegion)(rawRegion);
231
+ safeSearch = (0, validation_1.validateSafeSearch)(rawSafeSearch);
232
+ timePeriod = (0, validation_1.validateTimePeriod)(rawTimePeriod);
219
233
  }
220
- let results = [];
221
- if (operation === types_1.DuckDuckGoOperation.Search) {
222
- const searchResult = await (0, duck_duck_scrape_1.search)(query, {
223
- safeSearch: getSafeSearchType(safeSearch),
224
- locale: region || locale,
225
- time: getSearchTimeType(timePeriod),
226
- });
227
- if ((_a = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _a === void 0 ? void 0 : _a.length) {
228
- results = (0, processors_1.processWebSearchResults)(searchResult.results, itemIndex, searchResult).slice(0, maxResults);
229
- }
230
- else {
231
- results = [{
232
- json: { success: true, query, count: 0, results: [] },
233
- pairedItem: { item: itemIndex },
234
- }];
234
+ catch (validationError) {
235
+ if (validationError instanceof errors_1.ValidationError) {
236
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), validationError.getUserMessage(), { itemIndex });
235
237
  }
238
+ throw validationError;
236
239
  }
237
- else if (operation === types_1.DuckDuckGoOperation.SearchImages) {
238
- const searchResult = await (0, duck_duck_scrape_1.searchImages)(query, {
239
- safeSearch: getSafeSearchType(safeSearch),
240
- locale: region || locale,
241
- });
242
- if ((_b = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _b === void 0 ? void 0 : _b.length) {
243
- results = (0, processors_1.processImageSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
240
+ let results = [];
241
+ const performSearch = async () => {
242
+ var _a, _b, _c, _d;
243
+ const timeout = 30000;
244
+ if (operation === types_1.DuckDuckGoOperation.Search) {
245
+ const searchResult = await requestQueue.enqueue(() => (0, reliability_1.executeWithRetry)(() => (0, reliability_1.withTimeout)((0, duck_duck_scrape_1.search)(query, {
246
+ safeSearch: getSafeSearchType(safeSearch),
247
+ locale: region || locale,
248
+ time: getSearchTimeType(timePeriod),
249
+ }), timeout, 'Web Search'), `Web Search: "${query}"`, reliability_1.DEFAULT_RETRY_CONFIG));
250
+ if ((_a = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _a === void 0 ? void 0 : _a.length) {
251
+ results = (0, processors_1.processWebSearchResults)(searchResult.results, itemIndex, searchResult).slice(0, maxResults);
252
+ }
253
+ else {
254
+ results = [{
255
+ json: {
256
+ success: true,
257
+ query,
258
+ operation: 'web_search',
259
+ count: 0,
260
+ results: [],
261
+ },
262
+ pairedItem: { item: itemIndex },
263
+ }];
264
+ }
244
265
  }
245
- else {
246
- results = [{
247
- json: { success: true, query, count: 0, results: [] },
248
- pairedItem: { item: itemIndex },
249
- }];
266
+ else if (operation === types_1.DuckDuckGoOperation.SearchImages) {
267
+ const searchResult = await requestQueue.enqueue(() => (0, reliability_1.executeWithRetry)(() => (0, reliability_1.withTimeout)((0, duck_duck_scrape_1.searchImages)(query, {
268
+ safeSearch: getSafeSearchType(safeSearch),
269
+ locale: region || locale,
270
+ }), timeout, 'Image Search'), `Image Search: "${query}"`, reliability_1.DEFAULT_RETRY_CONFIG));
271
+ if ((_b = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _b === void 0 ? void 0 : _b.length) {
272
+ results = (0, processors_1.processImageSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
273
+ }
274
+ else {
275
+ results = [{
276
+ json: {
277
+ success: true,
278
+ query,
279
+ operation: 'image_search',
280
+ count: 0,
281
+ results: [],
282
+ },
283
+ pairedItem: { item: itemIndex },
284
+ }];
285
+ }
250
286
  }
251
- }
252
- else if (operation === types_1.DuckDuckGoOperation.SearchNews) {
253
- const searchResult = await (0, duck_duck_scrape_1.searchNews)(query, {
254
- safeSearch: getSafeSearchType(safeSearch),
255
- locale: region || locale,
256
- time: getSearchTimeType(timePeriod),
257
- });
258
- if ((_c = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _c === void 0 ? void 0 : _c.length) {
259
- results = (0, processors_1.processNewsSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
287
+ else if (operation === types_1.DuckDuckGoOperation.SearchNews) {
288
+ const searchResult = await requestQueue.enqueue(() => (0, reliability_1.executeWithRetry)(() => (0, reliability_1.withTimeout)((0, duck_duck_scrape_1.searchNews)(query, {
289
+ safeSearch: getSafeSearchType(safeSearch),
290
+ locale: region || locale,
291
+ time: getSearchTimeType(timePeriod),
292
+ }), timeout, 'News Search'), `News Search: "${query}"`, reliability_1.DEFAULT_RETRY_CONFIG));
293
+ if ((_c = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _c === void 0 ? void 0 : _c.length) {
294
+ results = (0, processors_1.processNewsSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
295
+ }
296
+ else {
297
+ results = [{
298
+ json: {
299
+ success: true,
300
+ query,
301
+ operation: 'news_search',
302
+ count: 0,
303
+ results: [],
304
+ },
305
+ pairedItem: { item: itemIndex },
306
+ }];
307
+ }
260
308
  }
261
- else {
262
- results = [{
263
- json: { success: true, query, count: 0, results: [] },
264
- pairedItem: { item: itemIndex },
265
- }];
266
- }
267
- }
268
- else if (operation === types_1.DuckDuckGoOperation.SearchVideos) {
269
- const searchResult = await (0, duck_duck_scrape_1.searchVideos)(query, {
270
- safeSearch: getSafeSearchType(safeSearch),
271
- locale: region || locale,
272
- });
273
- if ((_d = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _d === void 0 ? void 0 : _d.length) {
274
- results = (0, processors_1.processVideoSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
309
+ else if (operation === types_1.DuckDuckGoOperation.SearchVideos) {
310
+ const searchResult = await requestQueue.enqueue(() => (0, reliability_1.executeWithRetry)(() => (0, reliability_1.withTimeout)((0, duck_duck_scrape_1.searchVideos)(query, {
311
+ safeSearch: getSafeSearchType(safeSearch),
312
+ locale: region || locale,
313
+ }), timeout, 'Video Search'), `Video Search: "${query}"`, reliability_1.DEFAULT_RETRY_CONFIG));
314
+ if ((_d = searchResult === null || searchResult === void 0 ? void 0 : searchResult.results) === null || _d === void 0 ? void 0 : _d.length) {
315
+ results = (0, processors_1.processVideoSearchResults)(searchResult.results, itemIndex).slice(0, maxResults);
316
+ }
317
+ else {
318
+ results = [{
319
+ json: {
320
+ success: true,
321
+ query,
322
+ operation: 'video_search',
323
+ count: 0,
324
+ results: [],
325
+ },
326
+ pairedItem: { item: itemIndex },
327
+ }];
328
+ }
275
329
  }
276
330
  else {
277
- results = [{
278
- json: { success: true, query, count: 0, results: [] },
279
- pairedItem: { item: itemIndex },
280
- }];
331
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, { itemIndex });
281
332
  }
282
- }
283
- else {
284
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, { itemIndex });
285
- }
286
- returnData.push(...results);
333
+ return results;
334
+ };
335
+ const searchResults = await performSearch();
336
+ returnData.push(...searchResults);
287
337
  }
288
338
  catch (error) {
339
+ const classifiedError = error instanceof errors_1.DuckDuckGoError
340
+ ? error
341
+ : (0, errors_1.classifyError)(error);
289
342
  if (this.continueOnFail()) {
290
343
  returnData.push({
291
344
  json: {
292
345
  success: false,
293
- error: error instanceof Error ? error.message : String(error),
346
+ error: classifiedError.getUserMessage(),
347
+ errorCode: classifiedError.code,
348
+ errorCategory: classifiedError.category,
349
+ isRetryable: classifiedError.isRetryable,
350
+ technicalDetails: classifiedError.getTechnicalMessage(),
294
351
  ...items[itemIndex].json,
295
352
  },
296
353
  pairedItem: { item: itemIndex },
297
354
  });
298
355
  continue;
299
356
  }
300
- throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex });
357
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), classifiedError.getUserMessage(), { itemIndex, description: classifiedError.getTechnicalMessage() });
301
358
  }
302
359
  }
303
360
  return [returnData];
@@ -118,7 +118,7 @@ describe('DuckDuckGo Node', () => {
118
118
  it('should throw error when query is empty', async () => {
119
119
  setupNodeParameters('search', '');
120
120
  mockExecuteFunction.continueOnFail = jest.fn().mockReturnValue(false);
121
- await expect(duckDuckGoNode.execute.call(mockExecuteFunction)).rejects.toThrow(/cannot be empty/i);
121
+ await expect(duckDuckGoNode.execute.call(mockExecuteFunction)).rejects.toThrow();
122
122
  });
123
123
  });
124
124
  describe('Image Search', () => {
@@ -0,0 +1,183 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shouldRetryError = exports.classifyError = exports.ServerError = exports.ParsingError = exports.ValidationError = exports.TimeoutError = exports.RateLimitError = exports.NetworkError = exports.DuckDuckGoError = void 0;
4
+ class DuckDuckGoError extends Error {
5
+ constructor(context) {
6
+ super(context.userMessage);
7
+ this.name = 'DuckDuckGoError';
8
+ this.code = context.code;
9
+ this.category = context.category;
10
+ this.isRetryable = context.isRetryable;
11
+ this.userMessage = context.userMessage;
12
+ this.technicalMessage = context.technicalMessage;
13
+ this.originalError = context.originalError;
14
+ this.metadata = context.metadata;
15
+ if (Error.captureStackTrace) {
16
+ Error.captureStackTrace(this, DuckDuckGoError);
17
+ }
18
+ }
19
+ getUserMessage() {
20
+ return this.userMessage;
21
+ }
22
+ getTechnicalMessage() {
23
+ return this.technicalMessage;
24
+ }
25
+ toJSON() {
26
+ return {
27
+ name: this.name,
28
+ code: this.code,
29
+ category: this.category,
30
+ isRetryable: this.isRetryable,
31
+ userMessage: this.userMessage,
32
+ technicalMessage: this.technicalMessage,
33
+ metadata: this.metadata,
34
+ stack: this.stack,
35
+ };
36
+ }
37
+ }
38
+ exports.DuckDuckGoError = DuckDuckGoError;
39
+ class NetworkError extends DuckDuckGoError {
40
+ constructor(message, originalError, metadata) {
41
+ super({
42
+ code: 'NETWORK_ERROR',
43
+ category: 'network',
44
+ isRetryable: true,
45
+ userMessage: 'Unable to connect to DuckDuckGo. Please check your internet connection and try again.',
46
+ technicalMessage: message,
47
+ originalError,
48
+ metadata,
49
+ });
50
+ this.name = 'NetworkError';
51
+ }
52
+ }
53
+ exports.NetworkError = NetworkError;
54
+ class RateLimitError extends DuckDuckGoError {
55
+ constructor(message, originalError, metadata) {
56
+ super({
57
+ code: 'RATE_LIMIT',
58
+ category: 'rate_limit',
59
+ isRetryable: true,
60
+ userMessage: 'Search request rate limit exceeded. The request will be automatically retried.',
61
+ technicalMessage: message,
62
+ originalError,
63
+ metadata,
64
+ });
65
+ this.name = 'RateLimitError';
66
+ }
67
+ }
68
+ exports.RateLimitError = RateLimitError;
69
+ class TimeoutError extends DuckDuckGoError {
70
+ constructor(timeoutMs, operation, originalError) {
71
+ super({
72
+ code: 'TIMEOUT',
73
+ category: 'timeout',
74
+ isRetryable: true,
75
+ userMessage: `Search request timed out after ${timeoutMs / 1000} seconds. Please try again.`,
76
+ technicalMessage: `${operation} timed out after ${timeoutMs}ms`,
77
+ originalError,
78
+ metadata: { timeoutMs, operation },
79
+ });
80
+ this.name = 'TimeoutError';
81
+ }
82
+ }
83
+ exports.TimeoutError = TimeoutError;
84
+ class ValidationError extends DuckDuckGoError {
85
+ constructor(field, message, value) {
86
+ super({
87
+ code: 'VALIDATION_ERROR',
88
+ category: 'validation',
89
+ isRetryable: false,
90
+ userMessage: `Invalid ${field}: ${message}`,
91
+ technicalMessage: `Validation failed for field '${field}': ${message}`,
92
+ metadata: { field, value },
93
+ });
94
+ this.name = 'ValidationError';
95
+ }
96
+ }
97
+ exports.ValidationError = ValidationError;
98
+ class ParsingError extends DuckDuckGoError {
99
+ constructor(message, originalError, metadata) {
100
+ super({
101
+ code: 'PARSING_ERROR',
102
+ category: 'parsing',
103
+ isRetryable: false,
104
+ userMessage: 'Failed to process search results. Please try a different query.',
105
+ technicalMessage: message,
106
+ originalError,
107
+ metadata,
108
+ });
109
+ this.name = 'ParsingError';
110
+ }
111
+ }
112
+ exports.ParsingError = ParsingError;
113
+ class ServerError extends DuckDuckGoError {
114
+ constructor(statusCode, originalError) {
115
+ super({
116
+ code: 'SERVER_ERROR',
117
+ category: 'network',
118
+ isRetryable: true,
119
+ userMessage: 'DuckDuckGo servers are temporarily unavailable. Please try again later.',
120
+ technicalMessage: `Server returned error status: ${statusCode}`,
121
+ originalError,
122
+ metadata: { statusCode },
123
+ });
124
+ this.name = 'ServerError';
125
+ }
126
+ }
127
+ exports.ServerError = ServerError;
128
+ function classifyError(error) {
129
+ if (error instanceof DuckDuckGoError) {
130
+ return error;
131
+ }
132
+ const message = (error === null || error === void 0 ? void 0 : error.message) || String(error);
133
+ const lowerMessage = message.toLowerCase();
134
+ if (lowerMessage.includes('econnrefused') ||
135
+ lowerMessage.includes('enotfound') ||
136
+ lowerMessage.includes('econnreset') ||
137
+ lowerMessage.includes('ehostunreach') ||
138
+ lowerMessage.includes('enetunreach') ||
139
+ lowerMessage.includes('network') ||
140
+ lowerMessage.includes('dns')) {
141
+ return new NetworkError(message, error instanceof Error ? error : undefined);
142
+ }
143
+ if (lowerMessage.includes('anomaly') ||
144
+ lowerMessage.includes('too quickly') ||
145
+ lowerMessage.includes('rate limit') ||
146
+ lowerMessage.includes('429') ||
147
+ lowerMessage.includes('too many requests')) {
148
+ return new RateLimitError(message, error instanceof Error ? error : undefined);
149
+ }
150
+ if (lowerMessage.includes('timeout') ||
151
+ lowerMessage.includes('etimedout') ||
152
+ lowerMessage.includes('timed out')) {
153
+ return new TimeoutError(30000, 'Search request', error instanceof Error ? error : undefined);
154
+ }
155
+ if (lowerMessage.includes('500') ||
156
+ lowerMessage.includes('502') ||
157
+ lowerMessage.includes('503') ||
158
+ lowerMessage.includes('504') ||
159
+ lowerMessage.includes('server error')) {
160
+ const statusMatch = message.match(/(\d{3})/);
161
+ const statusCode = statusMatch ? parseInt(statusMatch[1], 10) : 500;
162
+ return new ServerError(statusCode, error instanceof Error ? error : undefined);
163
+ }
164
+ if (lowerMessage.includes('json') ||
165
+ lowerMessage.includes('parse') ||
166
+ lowerMessage.includes('unexpected token') ||
167
+ lowerMessage.includes('syntax error')) {
168
+ return new ParsingError(message, error instanceof Error ? error : undefined);
169
+ }
170
+ return new DuckDuckGoError({
171
+ code: 'UNKNOWN_ERROR',
172
+ category: 'unknown',
173
+ isRetryable: false,
174
+ userMessage: 'An unexpected error occurred. Please try again.',
175
+ technicalMessage: message,
176
+ originalError: error instanceof Error ? error : undefined,
177
+ });
178
+ }
179
+ exports.classifyError = classifyError;
180
+ function shouldRetryError(error, retryableCategories) {
181
+ return error.isRetryable && retryableCategories.has(error.category);
182
+ }
183
+ exports.shouldRetryError = shouldRetryError;
@@ -1,14 +1,62 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.processVideoSearchResults = exports.processNewsSearchResults = exports.processImageSearchResults = exports.processWebSearchResults = void 0;
3
+ exports.processVideoSearchResults = exports.processNewsSearchResults = exports.processImageSearchResults = exports.processWebSearchResults = exports.DEFAULT_QUALITY_CONFIG = void 0;
4
4
  const utils_1 = require("./utils");
5
- function processWebSearchResults(results, itemIndex, rawResponse) {
5
+ const validation_1 = require("./validation");
6
+ exports.DEFAULT_QUALITY_CONFIG = {
7
+ removeDuplicates: true,
8
+ requireTitle: true,
9
+ requireUrl: true,
10
+ minDescriptionLength: 0,
11
+ validateUrls: true,
12
+ };
13
+ function filterAndValidateResults(results, config = exports.DEFAULT_QUALITY_CONFIG) {
6
14
  if (!results || !Array.isArray(results)) {
7
15
  return [];
8
16
  }
9
- return results
10
- .filter(item => item && typeof item === 'object' && item.title && item.url)
11
- .map((item, index) => ({
17
+ const seenUrls = new Set();
18
+ const seenTitles = new Set();
19
+ return results.filter((result) => {
20
+ if (config.requireTitle && !result.title) {
21
+ return false;
22
+ }
23
+ if (config.requireUrl && !result.url) {
24
+ return false;
25
+ }
26
+ if (config.validateUrls && result.url && !(0, validation_1.validateUrl)(result.url)) {
27
+ return false;
28
+ }
29
+ if (config.minDescriptionLength > 0 && result.description) {
30
+ const cleanDesc = (result.description || '').trim();
31
+ if (cleanDesc.length < config.minDescriptionLength) {
32
+ return false;
33
+ }
34
+ }
35
+ if (config.removeDuplicates && result.url) {
36
+ const normalizedUrl = result.url.toLowerCase().trim();
37
+ if (seenUrls.has(normalizedUrl)) {
38
+ return false;
39
+ }
40
+ seenUrls.add(normalizedUrl);
41
+ }
42
+ if (config.removeDuplicates && result.title) {
43
+ const normalizedTitle = result.title.toLowerCase().trim();
44
+ if (normalizedTitle && seenTitles.has(normalizedTitle)) {
45
+ return false;
46
+ }
47
+ if (normalizedTitle) {
48
+ seenTitles.add(normalizedTitle);
49
+ }
50
+ }
51
+ return true;
52
+ });
53
+ }
54
+ function processWebSearchResults(results, itemIndex, rawResponse, qualityConfig) {
55
+ if (!results || !Array.isArray(results)) {
56
+ return [];
57
+ }
58
+ const filteredResults = filterAndValidateResults(results, qualityConfig);
59
+ return filteredResults.map((item, index) => ({
12
60
  json: {
13
61
  position: index + 1,
14
62
  title: (0, utils_1.decodeHtmlEntities)(item.title || '') || '',
@@ -21,13 +69,17 @@ function processWebSearchResults(results, itemIndex, rawResponse) {
21
69
  }));
22
70
  }
23
71
  exports.processWebSearchResults = processWebSearchResults;
24
- function processImageSearchResults(results, itemIndex) {
72
+ function processImageSearchResults(results, itemIndex, qualityConfig) {
25
73
  if (!results || !Array.isArray(results)) {
26
74
  return [];
27
75
  }
28
- return results
29
- .filter(item => item && typeof item === 'object' && item.image)
30
- .map((item) => ({
76
+ const imageResults = results.filter(item => item && typeof item === 'object' && item.image);
77
+ const filteredResults = filterAndValidateResults(imageResults, {
78
+ ...exports.DEFAULT_QUALITY_CONFIG,
79
+ requireTitle: false,
80
+ ...qualityConfig,
81
+ });
82
+ return filteredResults.map((item) => ({
31
83
  json: {
32
84
  title: (0, utils_1.decodeHtmlEntities)(item.title || '') || '',
33
85
  url: item.url || '',
@@ -42,11 +94,12 @@ function processImageSearchResults(results, itemIndex) {
42
94
  }));
43
95
  }
44
96
  exports.processImageSearchResults = processImageSearchResults;
45
- function processNewsSearchResults(results, itemIndex) {
97
+ function processNewsSearchResults(results, itemIndex, qualityConfig) {
46
98
  if (!results || !Array.isArray(results)) {
47
99
  return [];
48
100
  }
49
- return results.map((item) => ({
101
+ const filteredResults = filterAndValidateResults(results, qualityConfig);
102
+ return filteredResults.map((item) => ({
50
103
  json: {
51
104
  title: (0, utils_1.decodeHtmlEntities)(item.title),
52
105
  description: (0, utils_1.decodeHtmlEntities)(item.excerpt),
@@ -61,11 +114,12 @@ function processNewsSearchResults(results, itemIndex) {
61
114
  }));
62
115
  }
63
116
  exports.processNewsSearchResults = processNewsSearchResults;
64
- function processVideoSearchResults(results, itemIndex) {
117
+ function processVideoSearchResults(results, itemIndex, qualityConfig) {
65
118
  if (!results || !Array.isArray(results)) {
66
119
  return [];
67
120
  }
68
- return results.map((item) => ({
121
+ const filteredResults = filterAndValidateResults(results, qualityConfig);
122
+ return filteredResults.map((item) => ({
69
123
  json: {
70
124
  title: (0, utils_1.decodeHtmlEntities)(item.title),
71
125
  description: (0, utils_1.decodeHtmlEntities)(item.description),
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resetRequestQueue = exports.getRequestQueue = exports.RequestQueue = exports.withTimeout = exports.executeWithRetry = exports.DEFAULT_RETRY_CONFIG = exports.sleep = void 0;
4
+ const errors_1 = require("./errors");
5
+ const errors_2 = require("./errors");
6
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
7
+ exports.sleep = sleep;
8
+ exports.DEFAULT_RETRY_CONFIG = {
9
+ maxRetries: 3,
10
+ initialDelay: 1000,
11
+ maxDelay: 8000,
12
+ backoffMultiplier: 2,
13
+ jitterFactor: 0.3,
14
+ retryableCategories: new Set(['network', 'rate_limit', 'timeout']),
15
+ };
16
+ async function executeWithRetry(fn, context = 'operation', config = exports.DEFAULT_RETRY_CONFIG) {
17
+ let lastError = null;
18
+ for (let attempt = 1; attempt <= config.maxRetries + 1; attempt++) {
19
+ try {
20
+ return await fn();
21
+ }
22
+ catch (error) {
23
+ const classifiedError = (0, errors_1.classifyError)(error);
24
+ lastError = classifiedError;
25
+ if (!(0, errors_1.shouldRetryError)(classifiedError, config.retryableCategories)) {
26
+ throw classifiedError;
27
+ }
28
+ if (attempt > config.maxRetries) {
29
+ throw classifiedError;
30
+ }
31
+ const baseDelay = Math.min(config.initialDelay * Math.pow(config.backoffMultiplier, attempt - 1), config.maxDelay);
32
+ const jitter = baseDelay * config.jitterFactor * (Math.random() * 2 - 1);
33
+ const delay = Math.max(0, baseDelay + jitter);
34
+ if (process.env.NODE_ENV === 'development') {
35
+ console.log(`[DuckDuckGo] Retry ${attempt}/${config.maxRetries} for ${context} ` +
36
+ `after ${Math.round(delay)}ms due to ${classifiedError.code}`);
37
+ }
38
+ await (0, exports.sleep)(delay);
39
+ }
40
+ }
41
+ throw lastError || (0, errors_1.classifyError)('Unexpected retry error');
42
+ }
43
+ exports.executeWithRetry = executeWithRetry;
44
+ async function withTimeout(promise, timeoutMs, operation) {
45
+ let timeoutId = null;
46
+ const timeoutPromise = new Promise((_, reject) => {
47
+ timeoutId = setTimeout(() => {
48
+ reject(new errors_2.TimeoutError(timeoutMs, operation));
49
+ }, timeoutMs);
50
+ });
51
+ try {
52
+ const result = await Promise.race([promise, timeoutPromise]);
53
+ if (timeoutId)
54
+ clearTimeout(timeoutId);
55
+ return result;
56
+ }
57
+ catch (error) {
58
+ if (timeoutId)
59
+ clearTimeout(timeoutId);
60
+ throw error;
61
+ }
62
+ }
63
+ exports.withTimeout = withTimeout;
64
+ class RequestQueue {
65
+ constructor(minInterval = 1000, maxConcurrent = 1) {
66
+ this.queue = [];
67
+ this.processing = false;
68
+ this.lastRequestTime = 0;
69
+ this.activeRequests = 0;
70
+ this.minInterval = minInterval;
71
+ this.maxConcurrent = maxConcurrent;
72
+ }
73
+ async enqueue(fn) {
74
+ return new Promise((resolve, reject) => {
75
+ this.queue.push(async () => {
76
+ try {
77
+ const result = await fn();
78
+ resolve(result);
79
+ }
80
+ catch (error) {
81
+ reject(error);
82
+ }
83
+ });
84
+ if (!this.processing) {
85
+ this.processQueue().catch(() => {
86
+ });
87
+ }
88
+ });
89
+ }
90
+ async processQueue() {
91
+ if (this.processing || this.queue.length === 0) {
92
+ return;
93
+ }
94
+ this.processing = true;
95
+ while (this.queue.length > 0 && this.activeRequests < this.maxConcurrent) {
96
+ const now = Date.now();
97
+ const timeSinceLastRequest = now - this.lastRequestTime;
98
+ if (timeSinceLastRequest < this.minInterval) {
99
+ const delay = this.minInterval - timeSinceLastRequest;
100
+ await (0, exports.sleep)(delay);
101
+ }
102
+ const fn = this.queue.shift();
103
+ if (fn) {
104
+ this.lastRequestTime = Date.now();
105
+ this.activeRequests++;
106
+ fn().finally(() => {
107
+ this.activeRequests--;
108
+ if (this.queue.length > 0) {
109
+ this.processQueue().catch(() => {
110
+ });
111
+ }
112
+ });
113
+ }
114
+ }
115
+ this.processing = false;
116
+ }
117
+ getQueueSize() {
118
+ return this.queue.length;
119
+ }
120
+ getActiveRequests() {
121
+ return this.activeRequests;
122
+ }
123
+ clear() {
124
+ this.queue = [];
125
+ }
126
+ }
127
+ exports.RequestQueue = RequestQueue;
128
+ let globalRequestQueue = null;
129
+ function getRequestQueue(minInterval = 1000) {
130
+ if (!globalRequestQueue) {
131
+ globalRequestQueue = new RequestQueue(minInterval);
132
+ }
133
+ return globalRequestQueue;
134
+ }
135
+ exports.getRequestQueue = getRequestQueue;
136
+ function resetRequestQueue() {
137
+ globalRequestQueue = null;
138
+ }
139
+ exports.resetRequestQueue = resetRequestQueue;
@@ -7,15 +7,98 @@ const HTML_ENTITIES = {
7
7
  '&gt;': '>',
8
8
  '&quot;': '"',
9
9
  '&#39;': "'",
10
+ '&apos;': "'",
10
11
  '&nbsp;': ' ',
12
+ '&ensp;': ' ',
13
+ '&emsp;': ' ',
14
+ '&thinsp;': ' ',
11
15
  '&ndash;': '–',
12
16
  '&mdash;': '—',
13
17
  '&hellip;': '…',
18
+ '&lsquo;': '\u2018',
19
+ '&rsquo;': '\u2019',
20
+ '&ldquo;': '\u201C',
21
+ '&rdquo;': '\u201D',
22
+ '&laquo;': '«',
23
+ '&raquo;': '»',
24
+ '&bull;': '•',
25
+ '&middot;': '·',
26
+ '&prime;': '′',
27
+ '&Prime;': '″',
28
+ '&copy;': '©',
29
+ '&reg;': '®',
30
+ '&trade;': '™',
31
+ '&deg;': '°',
32
+ '&plusmn;': '±',
33
+ '&para;': '¶',
34
+ '&sect;': '§',
35
+ '&dagger;': '†',
36
+ '&Dagger;': '‡',
37
+ '&euro;': '€',
38
+ '&pound;': '£',
39
+ '&yen;': '¥',
40
+ '&cent;': '¢',
41
+ '&curren;': '¤',
42
+ '&times;': '×',
43
+ '&divide;': '÷',
44
+ '&minus;': '−',
45
+ '&ne;': '≠',
46
+ '&le;': '≤',
47
+ '&ge;': '≥',
48
+ '&asymp;': '≈',
49
+ '&infin;': '∞',
50
+ '&sum;': '∑',
51
+ '&prod;': '∏',
52
+ '&radic;': '√',
53
+ '&int;': '∫',
54
+ '&larr;': '←',
55
+ '&uarr;': '↑',
56
+ '&rarr;': '→',
57
+ '&darr;': '↓',
58
+ '&harr;': '↔',
59
+ '&alpha;': 'α',
60
+ '&beta;': 'β',
61
+ '&gamma;': 'γ',
62
+ '&delta;': 'δ',
63
+ '&epsilon;': 'ε',
64
+ '&pi;': 'π',
65
+ '&sigma;': 'σ',
66
+ '&omega;': 'ω',
14
67
  };
15
68
  function decodeHtmlEntities(text) {
16
69
  if (!text)
17
70
  return null;
18
- return text.replace(/&[#\w]+;/g, (entity) => HTML_ENTITIES[entity] || entity);
71
+ try {
72
+ return text
73
+ .replace(/&[a-zA-Z]+;/g, (entity) => HTML_ENTITIES[entity] || entity)
74
+ .replace(/&#(\d+);/g, (_, code) => {
75
+ try {
76
+ const num = parseInt(code, 10);
77
+ if (num >= 0 && num <= 0x10FFFF) {
78
+ return String.fromCharCode(num);
79
+ }
80
+ return _;
81
+ }
82
+ catch {
83
+ return _;
84
+ }
85
+ })
86
+ .replace(/&#x([0-9a-fA-F]+);/g, (_, code) => {
87
+ try {
88
+ const num = parseInt(code, 16);
89
+ if (num >= 0 && num <= 0x10FFFF) {
90
+ return String.fromCharCode(num);
91
+ }
92
+ return _;
93
+ }
94
+ catch {
95
+ return _;
96
+ }
97
+ });
98
+ }
99
+ catch (error) {
100
+ return text;
101
+ }
19
102
  }
20
103
  exports.decodeHtmlEntities = decodeHtmlEntities;
21
104
  function formatDate(timestamp) {
@@ -0,0 +1,133 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.validateTimeout = exports.validateTimePeriod = exports.validateSafeSearch = exports.validateUrl = exports.validateRegion = exports.validateLocale = exports.validateMaxResults = exports.sanitizeQuery = exports.validateSearchQuery = exports.VALIDATION_RULES = void 0;
4
+ const errors_1 = require("./errors");
5
+ exports.VALIDATION_RULES = {
6
+ QUERY_MIN_LENGTH: 1,
7
+ QUERY_MAX_LENGTH: 400,
8
+ MAX_RESULTS_MIN: 1,
9
+ MAX_RESULTS_MAX: 50,
10
+ TIMEOUT_MIN: 5000,
11
+ TIMEOUT_MAX: 120000,
12
+ };
13
+ function validateSearchQuery(query) {
14
+ if (query === null || query === undefined) {
15
+ throw new errors_1.ValidationError('query', 'Search query is required', query);
16
+ }
17
+ if (typeof query !== 'string') {
18
+ throw new errors_1.ValidationError('query', 'Search query must be a string', query);
19
+ }
20
+ const trimmed = query.trim();
21
+ if (trimmed.length < exports.VALIDATION_RULES.QUERY_MIN_LENGTH) {
22
+ throw new errors_1.ValidationError('query', 'Search query cannot be empty', query);
23
+ }
24
+ if (trimmed.length > exports.VALIDATION_RULES.QUERY_MAX_LENGTH) {
25
+ throw new errors_1.ValidationError('query', `Search query is too long (maximum ${exports.VALIDATION_RULES.QUERY_MAX_LENGTH} characters, got ${trimmed.length})`, query);
26
+ }
27
+ return sanitizeQuery(trimmed);
28
+ }
29
+ exports.validateSearchQuery = validateSearchQuery;
30
+ function sanitizeQuery(query) {
31
+ return query
32
+ .replace(/\s+/g, ' ')
33
+ .replace(/[\x00-\x1F\x7F]/g, '')
34
+ .trim();
35
+ }
36
+ exports.sanitizeQuery = sanitizeQuery;
37
+ function validateMaxResults(maxResults) {
38
+ if (maxResults === null || maxResults === undefined) {
39
+ return 10;
40
+ }
41
+ const num = typeof maxResults === 'string' ? parseFloat(maxResults) : maxResults;
42
+ if (typeof num !== 'number' || isNaN(num)) {
43
+ throw new errors_1.ValidationError('maxResults', 'Must be a valid number', maxResults);
44
+ }
45
+ if (!Number.isFinite(num)) {
46
+ throw new errors_1.ValidationError('maxResults', 'Must be a finite number', maxResults);
47
+ }
48
+ if (num < exports.VALIDATION_RULES.MAX_RESULTS_MIN) {
49
+ return exports.VALIDATION_RULES.MAX_RESULTS_MIN;
50
+ }
51
+ if (num > exports.VALIDATION_RULES.MAX_RESULTS_MAX) {
52
+ return exports.VALIDATION_RULES.MAX_RESULTS_MAX;
53
+ }
54
+ return Math.floor(num);
55
+ }
56
+ exports.validateMaxResults = validateMaxResults;
57
+ function validateLocale(locale) {
58
+ if (!locale || typeof locale !== 'string') {
59
+ return 'en-us';
60
+ }
61
+ const trimmed = locale.trim().toLowerCase();
62
+ if (!/^[a-z]{2}-[a-z]{2}$/.test(trimmed)) {
63
+ throw new errors_1.ValidationError('locale', 'Invalid locale format (expected: xx-xx)', locale);
64
+ }
65
+ return trimmed;
66
+ }
67
+ exports.validateLocale = validateLocale;
68
+ function validateRegion(region) {
69
+ if (!region || typeof region !== 'string') {
70
+ return 'wt-wt';
71
+ }
72
+ const trimmed = region.trim().toLowerCase();
73
+ if (!/^[a-z]{2}-[a-z]{2}$/.test(trimmed)) {
74
+ throw new errors_1.ValidationError('region', 'Invalid region format (expected: xx-xx)', region);
75
+ }
76
+ return trimmed;
77
+ }
78
+ exports.validateRegion = validateRegion;
79
+ function validateUrl(url) {
80
+ if (!url || typeof url !== 'string') {
81
+ return false;
82
+ }
83
+ try {
84
+ const parsed = new URL(url);
85
+ return ['http:', 'https:'].includes(parsed.protocol);
86
+ }
87
+ catch {
88
+ return false;
89
+ }
90
+ }
91
+ exports.validateUrl = validateUrl;
92
+ function validateSafeSearch(safeSearch) {
93
+ if (safeSearch === null || safeSearch === undefined) {
94
+ return 0;
95
+ }
96
+ const num = typeof safeSearch === 'string' ? parseInt(safeSearch, 10) : safeSearch;
97
+ if (typeof num !== 'number' || isNaN(num)) {
98
+ return 0;
99
+ }
100
+ if (![0, -1, -2].includes(num)) {
101
+ return 0;
102
+ }
103
+ return num;
104
+ }
105
+ exports.validateSafeSearch = validateSafeSearch;
106
+ function validateTimePeriod(timePeriod) {
107
+ if (!timePeriod || typeof timePeriod !== 'string') {
108
+ return 'a';
109
+ }
110
+ const trimmed = timePeriod.trim().toLowerCase();
111
+ if (!['a', 'd', 'w', 'm', 'y'].includes(trimmed)) {
112
+ return 'a';
113
+ }
114
+ return trimmed;
115
+ }
116
+ exports.validateTimePeriod = validateTimePeriod;
117
+ function validateTimeout(timeout) {
118
+ if (timeout === null || timeout === undefined) {
119
+ return 30000;
120
+ }
121
+ const num = typeof timeout === 'string' ? parseInt(timeout, 10) : timeout;
122
+ if (typeof num !== 'number' || isNaN(num)) {
123
+ throw new errors_1.ValidationError('timeout', 'Must be a valid number', timeout);
124
+ }
125
+ if (num < exports.VALIDATION_RULES.TIMEOUT_MIN) {
126
+ return exports.VALIDATION_RULES.TIMEOUT_MIN;
127
+ }
128
+ if (num > exports.VALIDATION_RULES.TIMEOUT_MAX) {
129
+ return exports.VALIDATION_RULES.TIMEOUT_MAX;
130
+ }
131
+ return num;
132
+ }
133
+ exports.validateTimeout = validateTimeout;
package/dist/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "n8n-nodes-duckduckgo-search",
3
- "version": "32.0.0",
4
- "description": "Lightweight n8n community node for DuckDuckGo search. Search the web, images, news, and videos with privacy-focused results.",
3
+ "version": "32.1.0",
4
+ "description": "Professional-grade n8n community node for DuckDuckGo search with comprehensive error handling, validation, and quality filtering.",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
7
7
  "n8n",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "n8n-nodes-duckduckgo-search",
3
- "version": "32.0.0",
4
- "description": "Lightweight n8n community node for DuckDuckGo search. Search the web, images, news, and videos with privacy-focused results.",
3
+ "version": "32.1.0",
4
+ "description": "Professional-grade n8n community node for DuckDuckGo search with comprehensive error handling, validation, and quality filtering.",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",
7
7
  "n8n",