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