n8n-nodes-script-runner 1.7.0 → 1.9.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.
|
@@ -6,71 +6,156 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.LinkedInFetcher = void 0;
|
|
7
7
|
const n8n_workflow_1 = require("n8n-workflow");
|
|
8
8
|
const axios_1 = __importDefault(require("axios"));
|
|
9
|
-
function
|
|
10
|
-
return Math.floor(Math.random() * (
|
|
9
|
+
function randomDelayMs(minSeconds = 45, maxSeconds = 90) {
|
|
10
|
+
return Math.floor(Math.random() * (maxSeconds - minSeconds + 1) + minSeconds) * 1000;
|
|
11
11
|
}
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
async function fetchLinkedIn(accountId, url, totalLimit, apiKey, minDelaySec, maxDelaySec, startCursor = null, collected = [], seenIds = new Set(), consecutiveEmptyPages = 0, pageCount = 0) {
|
|
13
|
+
const MAX_PAGES = 25;
|
|
14
|
+
const MAX_CONSECUTIVE_EMPTY = 1;
|
|
15
|
+
const PAGE_SIZE = 100;
|
|
16
|
+
// Store API metadata for return values
|
|
17
|
+
let apiEstimatedTotal = null;
|
|
18
|
+
let apiNextCursor = null;
|
|
19
|
+
if (collected.length >= totalLimit || pageCount >= MAX_PAGES) {
|
|
20
|
+
this.logger?.info?.(`Limit reached: ${collected.length} items | ${pageCount} pages`);
|
|
21
|
+
return {
|
|
22
|
+
items: collected,
|
|
23
|
+
nextCursor: apiNextCursor,
|
|
24
|
+
lastFailedCursor: null,
|
|
25
|
+
estimatedTotal: apiEstimatedTotal,
|
|
26
|
+
error: null,
|
|
27
|
+
totalFetched: collected.length,
|
|
28
|
+
};
|
|
19
29
|
}
|
|
20
30
|
try {
|
|
31
|
+
const queryParams = new URLSearchParams({
|
|
32
|
+
limit: PAGE_SIZE.toString(),
|
|
33
|
+
account_id: accountId,
|
|
34
|
+
});
|
|
21
35
|
const config = {
|
|
22
36
|
method: 'POST',
|
|
23
|
-
url: `https://api15.unipile.com:14554/api/v1/linkedin/search
|
|
37
|
+
url: `https://api15.unipile.com:14554/api/v1/linkedin/search?${queryParams.toString()}`,
|
|
24
38
|
headers: {
|
|
25
39
|
'X-API-KEY': apiKey,
|
|
26
|
-
'
|
|
27
|
-
'Content-Type': 'application/json'
|
|
40
|
+
'Accept': 'application/json',
|
|
41
|
+
'Content-Type': 'application/json',
|
|
28
42
|
},
|
|
29
|
-
data: { url }
|
|
43
|
+
data: startCursor ? { cursor: startCursor } : { url },
|
|
44
|
+
timeout: 60000, // 60 seconds
|
|
30
45
|
};
|
|
46
|
+
this.logger.info(`Fetching page ${pageCount + 1} | cursor: ${startCursor ?? 'initial'} | ` +
|
|
47
|
+
`collected: ${collected.length}/${totalLimit}`);
|
|
31
48
|
const response = await (0, axios_1.default)(config);
|
|
32
49
|
const data = response.data;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
50
|
+
const beforeLength = collected.length;
|
|
51
|
+
// Deduplicate and collect
|
|
52
|
+
if (Array.isArray(data.items)) {
|
|
53
|
+
for (const item of data.items) {
|
|
54
|
+
if (item?.id && typeof item.id === 'string' && !seenIds.has(item.id)) {
|
|
55
|
+
seenIds.add(item.id);
|
|
56
|
+
collected.push(item);
|
|
57
|
+
if (collected.length >= totalLimit)
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
36
61
|
}
|
|
37
|
-
if (
|
|
38
|
-
|
|
62
|
+
// Early return if page_count is zero (no more pages available)
|
|
63
|
+
if (data.paging?.page_count === 0) {
|
|
64
|
+
const estimatedTotal = data.paging?.total_count ?? null;
|
|
65
|
+
if (estimatedTotal !== null)
|
|
66
|
+
apiEstimatedTotal = estimatedTotal;
|
|
67
|
+
this.logger.info(`page_count is 0, no more pages. Total collected: ${collected.length}`);
|
|
68
|
+
return {
|
|
69
|
+
items: collected,
|
|
70
|
+
nextCursor: null,
|
|
71
|
+
lastFailedCursor: null,
|
|
72
|
+
estimatedTotal: apiEstimatedTotal ?? estimatedTotal,
|
|
73
|
+
error: null,
|
|
74
|
+
totalFetched: collected.length,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const addedThisPage = collected.length - beforeLength;
|
|
78
|
+
const nextCursor = data.cursor ?? null;
|
|
79
|
+
const estimatedTotal = data.paging?.total_count ?? null;
|
|
80
|
+
// Store for final return
|
|
81
|
+
if (estimatedTotal !== null)
|
|
82
|
+
apiEstimatedTotal = estimatedTotal;
|
|
83
|
+
if (nextCursor !== null)
|
|
84
|
+
apiNextCursor = nextCursor;
|
|
85
|
+
if (!nextCursor) {
|
|
86
|
+
this.logger.info(`No more cursor. Total collected: ${collected.length}`);
|
|
87
|
+
return {
|
|
88
|
+
items: collected,
|
|
89
|
+
nextCursor: apiNextCursor,
|
|
90
|
+
lastFailedCursor: null,
|
|
91
|
+
estimatedTotal: apiEstimatedTotal ?? estimatedTotal,
|
|
92
|
+
error: null,
|
|
93
|
+
totalFetched: collected.length,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (addedThisPage === 0) {
|
|
97
|
+
consecutiveEmptyPages++;
|
|
98
|
+
if (consecutiveEmptyPages >= MAX_CONSECUTIVE_EMPTY) {
|
|
99
|
+
this.logger.warn(`Stopping: ${consecutiveEmptyPages} consecutive empty pages`);
|
|
100
|
+
return {
|
|
101
|
+
items: collected,
|
|
102
|
+
nextCursor: null,
|
|
103
|
+
lastFailedCursor: startCursor,
|
|
104
|
+
estimatedTotal,
|
|
105
|
+
error: { message: 'Too many consecutive empty pages from API' },
|
|
106
|
+
totalFetched: collected.length,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
39
109
|
}
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
110
|
+
else {
|
|
111
|
+
consecutiveEmptyPages = 0;
|
|
112
|
+
}
|
|
113
|
+
// Continue only if needed
|
|
114
|
+
if (collected.length < totalLimit) {
|
|
115
|
+
const delayMs = randomDelayMs(minDelaySec, maxDelaySec);
|
|
116
|
+
this.logger.info(`Added ${addedThisPage} new items → total ${collected.length}/${totalLimit}. ` +
|
|
117
|
+
`Waiting ~${Math.round(delayMs / 1000)} seconds...`);
|
|
118
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
119
|
+
return fetchLinkedIn.call(this, accountId, url, totalLimit, apiKey, minDelaySec, maxDelaySec, nextCursor, collected, seenIds, consecutiveEmptyPages, pageCount + 1);
|
|
48
120
|
}
|
|
49
121
|
}
|
|
50
|
-
catch (
|
|
51
|
-
|
|
52
|
-
|
|
122
|
+
catch (err) {
|
|
123
|
+
const error = err;
|
|
124
|
+
const status = error?.response?.status ?? null;
|
|
125
|
+
const message = error.message || 'Unknown fetch error';
|
|
126
|
+
this.logger.warn(`Page fetch failed (cursor: ${startCursor ?? 'initial'}): ${message} (status: ${status})`);
|
|
53
127
|
return {
|
|
54
|
-
items:
|
|
55
|
-
|
|
128
|
+
items: collected,
|
|
129
|
+
nextCursor: null,
|
|
130
|
+
lastFailedCursor: startCursor,
|
|
131
|
+
estimatedTotal: null,
|
|
56
132
|
error: {
|
|
57
|
-
code: error
|
|
58
|
-
status
|
|
59
|
-
message
|
|
60
|
-
}
|
|
133
|
+
code: error?.code ?? null,
|
|
134
|
+
status,
|
|
135
|
+
message,
|
|
136
|
+
},
|
|
137
|
+
totalFetched: collected.length,
|
|
61
138
|
};
|
|
62
139
|
}
|
|
63
|
-
|
|
140
|
+
// Fallback (should rarely reach here)
|
|
141
|
+
return {
|
|
142
|
+
items: collected,
|
|
143
|
+
nextCursor: apiNextCursor,
|
|
144
|
+
lastFailedCursor: null,
|
|
145
|
+
estimatedTotal: apiEstimatedTotal,
|
|
146
|
+
error: null,
|
|
147
|
+
totalFetched: collected.length,
|
|
148
|
+
};
|
|
64
149
|
}
|
|
65
150
|
class LinkedInFetcher {
|
|
66
151
|
constructor() {
|
|
67
152
|
this.description = {
|
|
68
|
-
displayName: 'LinkedIn Fetcher',
|
|
153
|
+
displayName: 'LinkedIn Fetcher (Unipile)',
|
|
69
154
|
name: 'linkedInFetcher',
|
|
70
155
|
icon: 'file:linkedin.svg',
|
|
71
156
|
group: ['transform'],
|
|
72
157
|
version: 1,
|
|
73
|
-
description: 'Fetch LinkedIn
|
|
158
|
+
description: 'Fetch people / company results from LinkedIn via Unipile API (max 2500 default)',
|
|
74
159
|
defaults: {
|
|
75
160
|
name: 'LinkedIn Fetcher',
|
|
76
161
|
},
|
|
@@ -82,60 +167,55 @@ class LinkedInFetcher {
|
|
|
82
167
|
name: 'accountId',
|
|
83
168
|
type: 'string',
|
|
84
169
|
default: '',
|
|
85
|
-
placeholder: '
|
|
86
|
-
description: 'The LinkedIn account ID to use for the API',
|
|
170
|
+
placeholder: 'e.g. x4SmGz5ASq6pD66Gn0rPxA',
|
|
87
171
|
required: true,
|
|
88
172
|
},
|
|
89
173
|
{
|
|
90
|
-
displayName: 'URL',
|
|
174
|
+
displayName: 'LinkedIn URL',
|
|
91
175
|
name: 'url',
|
|
92
176
|
type: 'string',
|
|
93
177
|
default: '',
|
|
94
|
-
placeholder: '
|
|
95
|
-
description: '
|
|
178
|
+
placeholder: 'https://www.linkedin.com/sales/search/people?...',
|
|
179
|
+
description: 'Full LinkedIn search, profile, or company page URL',
|
|
96
180
|
required: true,
|
|
97
181
|
},
|
|
98
182
|
{
|
|
99
|
-
displayName: '
|
|
183
|
+
displayName: 'Max Items to Fetch',
|
|
100
184
|
name: 'total',
|
|
101
185
|
type: 'number',
|
|
102
|
-
default:
|
|
103
|
-
|
|
186
|
+
default: 2500,
|
|
187
|
+
typeOptions: { minValue: 1, maxValue: 10000 },
|
|
188
|
+
description: 'Maximum number of records to collect (after deduplication)',
|
|
104
189
|
required: true,
|
|
105
190
|
},
|
|
106
191
|
{
|
|
107
192
|
displayName: 'API Key',
|
|
108
193
|
name: 'apiKey',
|
|
109
194
|
type: 'string',
|
|
110
|
-
typeOptions: {
|
|
111
|
-
password: true,
|
|
112
|
-
},
|
|
195
|
+
typeOptions: { password: true },
|
|
113
196
|
default: '',
|
|
114
|
-
placeholder: 'Enter API key',
|
|
115
|
-
description: 'The Unipile API key for authentication',
|
|
116
197
|
required: true,
|
|
117
198
|
},
|
|
118
199
|
{
|
|
119
|
-
displayName: '
|
|
200
|
+
displayName: 'Resume From Cursor',
|
|
120
201
|
name: 'startCursor',
|
|
121
202
|
type: 'string',
|
|
122
203
|
default: '',
|
|
123
|
-
placeholder: '
|
|
124
|
-
description: 'Optional cursor to resume from a previous fetch',
|
|
204
|
+
placeholder: '(optional) Paste cursor from previous run',
|
|
125
205
|
},
|
|
126
206
|
{
|
|
127
|
-
displayName: 'Min Delay (seconds)',
|
|
207
|
+
displayName: 'Min Delay Between Requests (seconds)',
|
|
128
208
|
name: 'minDelay',
|
|
129
209
|
type: 'number',
|
|
130
210
|
default: 45,
|
|
131
|
-
|
|
211
|
+
typeOptions: { minValue: 10 },
|
|
132
212
|
},
|
|
133
213
|
{
|
|
134
|
-
displayName: 'Max Delay (seconds)',
|
|
214
|
+
displayName: 'Max Delay Between Requests (seconds)',
|
|
135
215
|
name: 'maxDelay',
|
|
136
216
|
type: 'number',
|
|
137
217
|
default: 90,
|
|
138
|
-
|
|
218
|
+
typeOptions: { minValue: 30 },
|
|
139
219
|
},
|
|
140
220
|
],
|
|
141
221
|
};
|
|
@@ -146,42 +226,43 @@ class LinkedInFetcher {
|
|
|
146
226
|
for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
147
227
|
try {
|
|
148
228
|
const accountId = this.getNodeParameter('accountId', itemIndex);
|
|
149
|
-
const url = this.getNodeParameter('url', itemIndex);
|
|
229
|
+
const url = this.getNodeParameter('url', itemIndex)?.trim();
|
|
150
230
|
const total = this.getNodeParameter('total', itemIndex);
|
|
151
231
|
const apiKey = this.getNodeParameter('apiKey', itemIndex);
|
|
152
|
-
const
|
|
232
|
+
const startCursorRaw = this.getNodeParameter('startCursor', itemIndex, '');
|
|
233
|
+
let startCursor = startCursorRaw ? startCursorRaw.trim() : null;
|
|
153
234
|
const minDelay = this.getNodeParameter('minDelay', itemIndex);
|
|
154
235
|
const maxDelay = this.getNodeParameter('maxDelay', itemIndex);
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
// Final output includes last failed cursor and error info
|
|
236
|
+
if (!url) {
|
|
237
|
+
throw new Error('LinkedIn URL is required');
|
|
238
|
+
}
|
|
239
|
+
if (startCursor === '' || startCursor === 'null')
|
|
240
|
+
startCursor = null;
|
|
241
|
+
const result = await fetchLinkedIn.call(this, accountId, url, total, apiKey, minDelay, maxDelay, startCursor);
|
|
162
242
|
returnData.push({
|
|
163
243
|
json: {
|
|
164
|
-
result:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
244
|
+
result: result.items,
|
|
245
|
+
nextCursor: result.nextCursor,
|
|
246
|
+
lastFailedCursor: result.lastFailedCursor,
|
|
247
|
+
estimatedTotal: result.estimatedTotal,
|
|
248
|
+
error: result.error,
|
|
249
|
+
totalFetched: result.totalFetched,
|
|
250
|
+
requestedLimit: total,
|
|
251
|
+
pagesUsed: Math.ceil(result.totalFetched / 100),
|
|
168
252
|
},
|
|
169
|
-
pairedItem: itemIndex,
|
|
253
|
+
pairedItem: { item: itemIndex },
|
|
170
254
|
});
|
|
171
255
|
}
|
|
172
|
-
catch (
|
|
256
|
+
catch (err) {
|
|
257
|
+
const error = err;
|
|
173
258
|
if (this.continueOnFail()) {
|
|
174
259
|
returnData.push({
|
|
175
|
-
json: {
|
|
176
|
-
|
|
177
|
-
},
|
|
178
|
-
pairedItem: itemIndex,
|
|
260
|
+
json: { error: error.message || 'Execution failed' },
|
|
261
|
+
pairedItem: { item: itemIndex },
|
|
179
262
|
});
|
|
180
263
|
continue;
|
|
181
264
|
}
|
|
182
|
-
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, {
|
|
183
|
-
itemIndex,
|
|
184
|
-
});
|
|
265
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), error, { itemIndex });
|
|
185
266
|
}
|
|
186
267
|
}
|
|
187
268
|
return [returnData];
|