n8n-nodes-linkedin-scraper 1.0.34 → 1.0.35
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/dist/nodes/LinkedinScraper/LinkedinScraper.node.js +124 -156
- package/dist/nodes/LinkedinScraper/LinkedinScraper.node.js.map +1 -1
- package/dist/package.json +1 -6
- package/package.json +1 -6
- package/dist/test/LinkedinScraper.node.test.js +0 -541
- package/dist/test/LinkedinScraper.node.test.js.map +0 -1
- package/dist/test/setup.js +0 -40
- package/dist/test/setup.js.map +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Author: Marco Visin
|
|
4
|
-
* Date:
|
|
4
|
+
* Date: 2026-09-09
|
|
5
5
|
* LinkedIn Scraper n8n node
|
|
6
6
|
* Interacts with the LinkedIn scraper microservice
|
|
7
7
|
* Uses the same InNotes API credentials for authentication
|
|
@@ -15,6 +15,90 @@ function splitCommaSeparated(value) {
|
|
|
15
15
|
.map((entry) => entry.trim())
|
|
16
16
|
.filter((entry) => entry.length > 0);
|
|
17
17
|
}
|
|
18
|
+
const URL_BATCH_CHUNK_SIZE = 30;
|
|
19
|
+
function isAuthError(err) {
|
|
20
|
+
const statusCode = err.statusCode;
|
|
21
|
+
const errMsg = err.message || '';
|
|
22
|
+
return statusCode === 401 || statusCode === 403 || errMsg.includes('401') || errMsg.includes('403');
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Chunked batch POST of one URL parameter per input item. Every difference
|
|
26
|
+
* between the two callers is an output contract, so it is an option here
|
|
27
|
+
* rather than a branch.
|
|
28
|
+
*
|
|
29
|
+
* The short-results drop is reproduced, not fixed: a response carrying fewer
|
|
30
|
+
* results than URLs silently loses the rest, and fixing it here would be a
|
|
31
|
+
* behaviour change wearing a refactor's clothes.
|
|
32
|
+
* Author: Marco — 2026-09-09
|
|
33
|
+
*/
|
|
34
|
+
async function batchByUrl(ctx, options) {
|
|
35
|
+
const validUrls = [];
|
|
36
|
+
const rows = [];
|
|
37
|
+
for (let i = 0; i < options.itemCount; i++) {
|
|
38
|
+
const url = ctx.getNodeParameter(options.urlParameter, i);
|
|
39
|
+
if (url === null || url === void 0 ? void 0 : url.startsWith('http')) {
|
|
40
|
+
validUrls.push({ url, index: i });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
rows.push({
|
|
44
|
+
...options.skippedDefaults,
|
|
45
|
+
success: false,
|
|
46
|
+
error: `Invalid ${options.invalidUrlLabel} URL: ${url || 'empty'}`,
|
|
47
|
+
_originalIndex: i,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
for (let chunkStart = 0; chunkStart < validUrls.length; chunkStart += URL_BATCH_CHUNK_SIZE) {
|
|
52
|
+
const chunk = validUrls.slice(chunkStart, chunkStart + URL_BATCH_CHUNK_SIZE);
|
|
53
|
+
const chunkUrls = chunk.map((c) => c.url);
|
|
54
|
+
try {
|
|
55
|
+
const response = await ctx.helpers.request({
|
|
56
|
+
method: 'POST',
|
|
57
|
+
url: options.endpoint,
|
|
58
|
+
headers: {
|
|
59
|
+
Authorization: `Bearer ${options.bearerToken}`,
|
|
60
|
+
'Content-Type': 'application/json',
|
|
61
|
+
},
|
|
62
|
+
body: {
|
|
63
|
+
urls: chunkUrls,
|
|
64
|
+
max_concurrent: 5,
|
|
65
|
+
max_retries: 7,
|
|
66
|
+
},
|
|
67
|
+
json: true,
|
|
68
|
+
timeout: options.timeout,
|
|
69
|
+
});
|
|
70
|
+
// Map results back, preserving original indices
|
|
71
|
+
const results = response.results;
|
|
72
|
+
for (let j = 0; j < results.length; j++) {
|
|
73
|
+
rows.push({
|
|
74
|
+
...options.mapSuccess(results[j]),
|
|
75
|
+
_originalIndex: chunk[j].index,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (err) {
|
|
80
|
+
if (options.rethrowAuthErrors && isAuthError(err)) {
|
|
81
|
+
throw err;
|
|
82
|
+
}
|
|
83
|
+
// If entire batch fails, mark all URLs in chunk as failed
|
|
84
|
+
for (const item of chunk) {
|
|
85
|
+
rows.push({
|
|
86
|
+
...options.errorDefaults,
|
|
87
|
+
success: false,
|
|
88
|
+
error: `Batch request failed: ${err.message}`,
|
|
89
|
+
url: item.url,
|
|
90
|
+
_originalIndex: item.index,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// Sort by original index to maintain order, then drop the internal field
|
|
96
|
+
rows.sort((a, b) => a._originalIndex - b._originalIndex);
|
|
97
|
+
return rows.map((row) => {
|
|
98
|
+
const { _originalIndex: _, ...cleanRow } = row;
|
|
99
|
+
return cleanRow;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
18
102
|
class LinkedinScraper {
|
|
19
103
|
constructor() {
|
|
20
104
|
this.description = {
|
|
@@ -256,7 +340,7 @@ class LinkedinScraper {
|
|
|
256
340
|
};
|
|
257
341
|
}
|
|
258
342
|
async execute() {
|
|
259
|
-
var _a
|
|
343
|
+
var _a;
|
|
260
344
|
const items = this.getInputData();
|
|
261
345
|
const returnData = [];
|
|
262
346
|
// Get InNotes API credentials (reused for scraper authentication)
|
|
@@ -349,177 +433,61 @@ class LinkedinScraper {
|
|
|
349
433
|
}
|
|
350
434
|
uniqueJobs.push(job);
|
|
351
435
|
}
|
|
352
|
-
const duplicatesRemoved = jobs.length - uniqueJobs.length;
|
|
353
|
-
console.log(`Deduplicated: removed ${duplicatesRemoved} duplicates from ${jobs.length} results`);
|
|
354
436
|
returnData.push(...uniqueJobs);
|
|
355
437
|
continue;
|
|
356
438
|
}
|
|
357
439
|
if (resource === 'job' && operation === 'details') {
|
|
358
|
-
// Batch processing: collect all URLs, chunk them, and send batch requests
|
|
359
|
-
const CHUNK_SIZE = 30;
|
|
360
440
|
// Only process on first item - we'll handle all items at once
|
|
361
441
|
if (itemIndex > 0) {
|
|
362
442
|
continue;
|
|
363
443
|
}
|
|
364
|
-
const
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
_originalIndex: i,
|
|
378
|
-
});
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
// Process valid URLs in chunks
|
|
382
|
-
const batchResults = [];
|
|
383
|
-
for (let chunkStart = 0; chunkStart < validUrls.length; chunkStart += CHUNK_SIZE) {
|
|
384
|
-
const chunk = validUrls.slice(chunkStart, chunkStart + CHUNK_SIZE);
|
|
385
|
-
const chunkUrls = chunk.map((c) => c.url);
|
|
386
|
-
try {
|
|
387
|
-
const response = await this.helpers.request({
|
|
388
|
-
method: 'POST',
|
|
389
|
-
url: `${scraperUrl}/jobs/details/batch`,
|
|
390
|
-
headers: {
|
|
391
|
-
Authorization: `Bearer ${bearerToken}`,
|
|
392
|
-
'Content-Type': 'application/json',
|
|
393
|
-
},
|
|
394
|
-
body: {
|
|
395
|
-
urls: chunkUrls,
|
|
396
|
-
max_concurrent: 5,
|
|
397
|
-
max_retries: 7,
|
|
398
|
-
},
|
|
399
|
-
json: true,
|
|
400
|
-
timeout: 15 * 60 * 1000, // 15 minutes per batch (more retries = longer)
|
|
401
|
-
});
|
|
402
|
-
// Map results back, preserving original indices
|
|
403
|
-
const results = response.results;
|
|
404
|
-
for (let j = 0; j < results.length; j++) {
|
|
405
|
-
const result = results[j];
|
|
406
|
-
batchResults.push({
|
|
407
|
-
...result,
|
|
408
|
-
_originalIndex: chunk[j].index,
|
|
409
|
-
});
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
catch (err) {
|
|
413
|
-
// Re-throw auth errors (session expired)
|
|
414
|
-
const statusCode = err.statusCode;
|
|
415
|
-
const errMsg = err.message || '';
|
|
416
|
-
if (statusCode === 401 || statusCode === 403 || errMsg.includes('401') || errMsg.includes('403')) {
|
|
417
|
-
throw err;
|
|
418
|
-
}
|
|
419
|
-
// If entire batch fails, mark all URLs in chunk as failed
|
|
420
|
-
for (const item of chunk) {
|
|
421
|
-
batchResults.push({
|
|
422
|
-
success: false,
|
|
423
|
-
error: `Batch request failed: ${err.message}`,
|
|
424
|
-
url: item.url,
|
|
425
|
-
_originalIndex: item.index,
|
|
426
|
-
});
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
// Combine invalid and batch results, sort by original index to maintain order
|
|
431
|
-
const allResults = [...invalidResults, ...batchResults];
|
|
432
|
-
allResults.sort((a, b) => a._originalIndex - b._originalIndex);
|
|
433
|
-
// Remove internal _originalIndex field and add to returnData
|
|
434
|
-
for (const result of allResults) {
|
|
435
|
-
const { _originalIndex: _, ...cleanResult } = result;
|
|
436
|
-
returnData.push(cleanResult);
|
|
444
|
+
const results = await batchByUrl(this, {
|
|
445
|
+
itemCount: items.length,
|
|
446
|
+
bearerToken,
|
|
447
|
+
urlParameter: 'jobUrl',
|
|
448
|
+
invalidUrlLabel: 'job',
|
|
449
|
+
endpoint: `${scraperUrl}/jobs/details/batch`,
|
|
450
|
+
timeout: 15 * 60 * 1000, // 15 minutes per batch (more retries = longer)
|
|
451
|
+
rethrowAuthErrors: true,
|
|
452
|
+
// Every field the scraper returns passes through
|
|
453
|
+
mapSuccess: (result) => ({ ...result }),
|
|
454
|
+
});
|
|
455
|
+
for (const result of results) {
|
|
456
|
+
returnData.push(result);
|
|
437
457
|
}
|
|
438
458
|
// Skip further iterations since we processed all items
|
|
439
459
|
break;
|
|
440
460
|
}
|
|
441
461
|
if (resource === 'company' && operation === 'followers') {
|
|
442
|
-
// Batch processing: collect all URLs, chunk them, and send batch requests
|
|
443
|
-
const CHUNK_SIZE = 30;
|
|
444
462
|
// Only process on first item - we'll handle all items at once
|
|
445
463
|
if (itemIndex > 0) {
|
|
446
464
|
continue;
|
|
447
465
|
}
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
466
|
+
const results = await batchByUrl(this, {
|
|
467
|
+
itemCount: items.length,
|
|
468
|
+
bearerToken,
|
|
469
|
+
urlParameter: 'companyUrl',
|
|
470
|
+
invalidUrlLabel: 'company',
|
|
471
|
+
endpoint: `${scraperUrl}/companies/followers/batch`,
|
|
472
|
+
timeout: 10 * 60 * 1000, // 10 minutes per batch
|
|
473
|
+
// An auth failure here is a failed row, not the end of the run
|
|
474
|
+
rethrowAuthErrors: false,
|
|
475
|
+
// Fixed five keys: config/n8n-workflow-template.json reads follower_count
|
|
476
|
+
mapSuccess: (result) => {
|
|
477
|
+
var _a, _b, _c;
|
|
478
|
+
return ({
|
|
479
|
+
follower_count: (_a = result.follower_count) !== null && _a !== void 0 ? _a : 0,
|
|
480
|
+
source: (_b = result.source) !== null && _b !== void 0 ? _b : 'unknown',
|
|
481
|
+
success: (_c = result.success) !== null && _c !== void 0 ? _c : false,
|
|
482
|
+
error: result.error,
|
|
483
|
+
url: result.url,
|
|
464
484
|
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
for (
|
|
470
|
-
|
|
471
|
-
const chunkUrls = chunk.map((c) => c.url);
|
|
472
|
-
try {
|
|
473
|
-
const response = await this.helpers.request({
|
|
474
|
-
method: 'POST',
|
|
475
|
-
url: `${scraperUrl}/companies/followers/batch`,
|
|
476
|
-
headers: {
|
|
477
|
-
Authorization: `Bearer ${bearerToken}`,
|
|
478
|
-
'Content-Type': 'application/json',
|
|
479
|
-
},
|
|
480
|
-
body: {
|
|
481
|
-
urls: chunkUrls,
|
|
482
|
-
max_concurrent: 5,
|
|
483
|
-
max_retries: 7,
|
|
484
|
-
},
|
|
485
|
-
json: true,
|
|
486
|
-
timeout: 10 * 60 * 1000, // 10 minutes per batch
|
|
487
|
-
});
|
|
488
|
-
// Map results back, preserving original indices
|
|
489
|
-
const results = response.results;
|
|
490
|
-
for (let j = 0; j < results.length; j++) {
|
|
491
|
-
const result = results[j];
|
|
492
|
-
batchResults.push({
|
|
493
|
-
follower_count: (_a = result.follower_count) !== null && _a !== void 0 ? _a : 0,
|
|
494
|
-
source: (_b = result.source) !== null && _b !== void 0 ? _b : 'unknown',
|
|
495
|
-
success: (_c = result.success) !== null && _c !== void 0 ? _c : false,
|
|
496
|
-
error: result.error,
|
|
497
|
-
url: result.url,
|
|
498
|
-
_originalIndex: chunk[j].index,
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
catch (err) {
|
|
503
|
-
// If entire batch fails, mark all URLs in chunk as failed
|
|
504
|
-
for (const item of chunk) {
|
|
505
|
-
batchResults.push({
|
|
506
|
-
follower_count: 0,
|
|
507
|
-
source: 'error',
|
|
508
|
-
success: false,
|
|
509
|
-
error: `Batch request failed: ${err.message}`,
|
|
510
|
-
url: item.url,
|
|
511
|
-
_originalIndex: item.index,
|
|
512
|
-
});
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
// Combine invalid and batch results, sort by original index to maintain order
|
|
517
|
-
const allResults = [...invalidResults, ...batchResults];
|
|
518
|
-
allResults.sort((a, b) => a._originalIndex - b._originalIndex);
|
|
519
|
-
// Remove internal _originalIndex field and add to returnData
|
|
520
|
-
for (const result of allResults) {
|
|
521
|
-
const { _originalIndex: _, ...cleanResult } = result;
|
|
522
|
-
returnData.push(cleanResult);
|
|
485
|
+
},
|
|
486
|
+
skippedDefaults: { follower_count: 0, source: 'skipped' },
|
|
487
|
+
errorDefaults: { follower_count: 0, source: 'error' },
|
|
488
|
+
});
|
|
489
|
+
for (const result of results) {
|
|
490
|
+
returnData.push(result);
|
|
523
491
|
}
|
|
524
492
|
// Skip further iterations since we processed all items
|
|
525
493
|
break;
|
|
@@ -529,7 +497,7 @@ class LinkedinScraper {
|
|
|
529
497
|
});
|
|
530
498
|
}
|
|
531
499
|
catch (error) {
|
|
532
|
-
const errorMessage = (
|
|
500
|
+
const errorMessage = (_a = error.message) !== null && _a !== void 0 ? _a : error.message;
|
|
533
501
|
// Check for authentication errors from the scraper service
|
|
534
502
|
if (error.statusCode === 401 || error.statusCode === 403 || errorMessage.includes('401') || errorMessage.includes('403')) {
|
|
535
503
|
throw new n8n_workflow_1.NodeApiError(this.getNode(), {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../nodes/LinkedinScraper/LinkedinScraper.node.ts"],"names":[],"mappings":";AAAA;;;;;;GAMG;;;AAEH,+CAOsB;AAEtB,SAAS,mBAAmB,CAAC,KAAa;IACzC,OAAO,KAAK;SACV,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;SAC5B,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACvC,CAAC;AAED,MAAa,eAAe;IAA5B;QACC,gBAAW,GAAyB;YACnC,WAAW,EAAE,0BAA0B;YACvC,IAAI,EAAE,iBAAiB;YACvB,KAAK,EAAE,CAAC,WAAW,CAAC;YACpB,OAAO,EAAE,CAAC;YACV,WAAW,EAAE,iDAAiD;YAC9D,QAAQ,EAAE,8DAA8D;YACxE,IAAI,EAAE,eAAe;YACrB,QAAQ,EAAE;gBACT,IAAI,EAAE,kBAAkB;aACxB;YACD,MAAM,EAAE,CAAC,MAAM,CAAC;YAChB,OAAO,EAAE,CAAC,MAAM,CAAC;YACjB,WAAW,EAAE;gBACZ;oBACC,yCAAyC;oBACzC,IAAI,EAAE,YAAY;oBAClB,QAAQ,EAAE,IAAI;iBACd;aACD;YACD,UAAU,EAAE;gBACX,qBAAqB;gBACrB;oBACC,WAAW,EAAE,UAAU;oBACvB,IAAI,EAAE,UAAU;oBAChB,IAAI,EAAE,SAAS;oBACf,gBAAgB,EAAE,IAAI;oBACtB,OAAO,EAAE;wBACR;4BACC,IAAI,EAAE,MAAM;4BACZ,KAAK,EAAE,MAAM;yBACb;wBACD;4BACC,IAAI,EAAE,KAAK;4BACX,KAAK,EAAE,KAAK;yBACZ;wBACD;4BACC,IAAI,EAAE,SAAS;4BACf,KAAK,EAAE,SAAS;yBAChB;qBACD;oBACD,OAAO,EAAE,MAAM;iBACf;gBACD,kBAAkB;gBAClB;oBACC,WAAW,EAAE,WAAW;oBACxB,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,SAAS;oBACf,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,MAAM,CAAC;yBAClB;qBACD;oBACD,OAAO,EAAE;wBACR;4BACC,IAAI,EAAE,QAAQ;4BACd,KAAK,EAAE,QAAQ;4BACf,MAAM,EAAE,yCAAyC;yBACjD;qBACD;oBACD,OAAO,EAAE,QAAQ;iBACjB;gBACD,iBAAiB;gBACjB;oBACC,WAAW,EAAE,WAAW;oBACxB,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,SAAS;oBACf,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,KAAK,CAAC;yBACjB;qBACD;oBACD,OAAO,EAAE;wBACR;4BACC,IAAI,EAAE,SAAS;4BACf,KAAK,EAAE,SAAS;4BAChB,MAAM,EAAE,mBAAmB;yBAC3B;qBACD;oBACD,OAAO,EAAE,SAAS;iBAClB;gBACD,qBAAqB;gBACrB;oBACC,WAAW,EAAE,WAAW;oBACxB,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,SAAS;oBACf,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,SAAS,CAAC;yBACrB;qBACD;oBACD,OAAO,EAAE;wBACR;4BACC,IAAI,EAAE,WAAW;4BACjB,KAAK,EAAE,WAAW;4BAClB,MAAM,EAAE,8BAA8B;yBACtC;qBACD;oBACD,OAAO,EAAE,WAAW;iBACpB;gBACD,yBAAyB;gBACzB;oBACC,WAAW,EAAE,YAAY;oBACzB,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,EAAE;oBACX,WAAW,EAAE,8BAA8B;oBAC3C,WAAW,EAAE,kDAAkD;oBAC/D,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,MAAM,CAAC;4BAClB,SAAS,EAAE,CAAC,QAAQ,CAAC;yBACrB;qBACD;oBACD,QAAQ,EAAE,IAAI;iBACd;gBACD;oBACC,WAAW,EAAE,WAAW;oBACxB,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,EAAE;oBACX,WAAW,EAAE,gBAAgB;oBAC7B,WAAW,EAAE,gDAAgD;oBAC7D,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,MAAM,CAAC;4BAClB,SAAS,EAAE,CAAC,QAAQ,CAAC;yBACrB;qBACD;oBACD,QAAQ,EAAE,IAAI;iBACd;gBACD;oBACC,WAAW,EAAE,aAAa;oBAC1B,IAAI,EAAE,YAAY;oBAClB,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,GAAG;oBACZ,WAAW,EAAE;wBACZ,QAAQ,EAAE,CAAC;wBACX,QAAQ,EAAE,IAAI;qBACd;oBACD,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,MAAM,CAAC;4BAClB,SAAS,EAAE,CAAC,QAAQ,CAAC;yBACrB;qBACD;iBACD;gBACD;oBACC,WAAW,EAAE,iBAAiB;oBAC9B,IAAI,EAAE,eAAe;oBACrB,IAAI,EAAE,SAAS;oBACf,OAAO,EAAE,IAAI;oBACb,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,MAAM,CAAC;4BAClB,SAAS,EAAE,CAAC,QAAQ,CAAC;yBACrB;qBACD;iBACD;gBACD;oBACC,WAAW,EAAE,YAAY;oBACzB,IAAI,EAAE,WAAW;oBACjB,IAAI,EAAE,SAAS;oBACf,OAAO,EAAE;wBACR,EAAE,IAAI,EAAE,eAAe,EAAE,KAAK,EAAE,QAAQ,EAAE;wBAC1C,EAAE,IAAI,EAAE,WAAW,EAAE,KAAK,EAAE,SAAS,EAAE;wBACvC,EAAE,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE;qBACzC;oBACD,OAAO,EAAE,QAAQ;oBACjB,WAAW,EAAE,6BAA6B;oBAC1C,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,MAAM,CAAC;4BAClB,SAAS,EAAE,CAAC,QAAQ,CAAC;yBACrB;qBACD;iBACD;gBACD;oBACC,WAAW,EAAE,mBAAmB;oBAChC,IAAI,EAAE,kBAAkB;oBACxB,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,EAAE;oBACX,WAAW,EAAE,sBAAsB;oBACnC,WAAW,EAAE,8CAA8C;oBAC3D,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,MAAM,CAAC;4BAClB,SAAS,EAAE,CAAC,QAAQ,CAAC;yBACrB;qBACD;iBACD;gBACD;oBACC,WAAW,EAAE,mBAAmB;oBAChC,IAAI,EAAE,kBAAkB;oBACxB,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,EAAE;oBACX,WAAW,EAAE,cAAc;oBAC3B,WAAW,EAAE,6DAA6D;oBAC1E,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,MAAM,CAAC;4BAClB,SAAS,EAAE,CAAC,QAAQ,CAAC;yBACrB;qBACD;iBACD;gBACD,yBAAyB;gBACzB;oBACC,WAAW,EAAE,SAAS;oBACtB,IAAI,EAAE,QAAQ;oBACd,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,EAAE;oBACX,WAAW,EAAE,6BAA6B;oBAC1C,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,KAAK,CAAC;4BACjB,SAAS,EAAE,CAAC,SAAS,CAAC;yBACtB;qBACD;oBACD,QAAQ,EAAE,IAAI;iBACd;gBACD,qBAAqB;gBACrB;oBACC,WAAW,EAAE,aAAa;oBAC1B,IAAI,EAAE,YAAY;oBAClB,IAAI,EAAE,QAAQ;oBACd,OAAO,EAAE,EAAE;oBACX,WAAW,EAAE,sBAAsB;oBACnC,cAAc,EAAE;wBACf,IAAI,EAAE;4BACL,QAAQ,EAAE,CAAC,SAAS,CAAC;4BACrB,SAAS,EAAE,CAAC,WAAW,CAAC;yBACxB;qBACD;oBACD,QAAQ,EAAE,IAAI;iBACd;aACD;SACD,CAAC;IAyVH,CAAC;IAvVA,KAAK,CAAC,OAAO;;QACZ,MAAM,KAAK,GAAG,IAAI,CAAC,YAAY,EAAE,CAAC;QAClC,MAAM,UAAU,GAAkB,EAAE,CAAC;QAErC,kEAAkE;QAClE,MAAM,WAAW,GAAG,CAAC,MAAM,IAAI,CAAC,cAAc,CAAC,YAAY,CAAC,CAAuB,CAAC;QAEpF,IAAI,CAAC,WAAW,EAAE,CAAC;YAClB,MAAM,IAAI,2BAAY,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE;gBACtC,OAAO,EAAE,qCAAqC;aAC9C,CAAC,CAAC;QACJ,CAAC;QAED,yEAAyE;QACzE,wEAAwE;QACxE,mEAAmE;QACnE,sEAAsE;QACtE,uEAAuE;QACvE,uEAAuE;QACvE,qEAAqE;QACrE,iDAAiD;QACjD,EAAE;QACF,wEAAwE;QACxE,yEAAyE;QACzE,wEAAwE;QACxE,oCAAoC;QACpC,6BAA6B;QAC7B,MAAM,WAAW,GAAG,WAAW,CAAC,KAA2B,CAAC;QAE5D,IAAI,CAAC,WAAW,EAAE,CAAC;YAClB,MAAM,IAAI,2BAAY,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE;gBACtC,OAAO,EAAE,sDAAsD;aAC/D,CAAC,CAAC;QACJ,CAAC;QAED,KAAK,IAAI,SAAS,GAAG,CAAC,EAAE,SAAS,GAAG,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,CAAC;YAC/D,MAAM,UAAU,GAAG,4BAA4B,CAAC;YAChD,MAAM,QAAQ,GAAG,IAAI,CAAC,gBAAgB,CAAC,UAAU,EAAE,SAAS,CAAW,CAAC;YACxE,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,SAAS,CAAW,CAAC;YAE1E,IAAI,CAAC;gBACJ,IAAI,QAAQ,KAAK,MAAM,IAAI,SAAS,KAAK,QAAQ,EAAE,CAAC;oBACnD,MAAM,SAAS,GAAG,mBAAmB,CACpC,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,SAAS,CAAW,CACvD,CAAC;oBACF,MAAM,SAAS,GAAG,mBAAmB,CACpC,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,SAAS,CAAW,CACvD,CAAC;oBACF,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,CAAC,YAAY,EAAE,SAAS,CAAW,CAAC;oBAC5E,MAAM,aAAa,GAAG,IAAI,CAAC,gBAAgB,CAAC,eAAe,EAAE,SAAS,CAAY,CAAC;oBACnF,MAAM,SAAS,GAAG,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,SAAS,CAAW,CAAC;oBAC1E,MAAM,gBAAgB,GAAG,mBAAmB,CAC1C,IAAI,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,SAAS,CAAY,IAAI,EAAE,CACtE,CAAC;oBACF,MAAM,gBAAgB,GAAG,mBAAmB,CAC1C,IAAI,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,SAAS,CAAY,IAAI,EAAE,CACtE,CAAC;oBAEF,4DAA4D;oBAC5D,0DAA0D;oBAC1D,0DAA0D;oBAC1D,4DAA4D;oBAC5D,yDAAyD;oBACzD,EAAE;oBACF,2DAA2D;oBAC3D,sDAAsD;oBACtD,yDAAyD;oBACzD,iDAAiD;oBACjD,MAAM,WAAW,GAAgB;wBAChC,UAAU,EAAE,SAAS;wBACrB,SAAS;wBACT,WAAW,EAAE,UAAU;wBACvB,eAAe,EAAE,aAAa;wBAC9B,UAAU,EAAE,SAAS;wBACrB,iBAAiB,EAAE,gBAAgB;wBACnC,kBAAkB,EAAE,gBAAgB;qBACpC,CAAC;oBAEF,kBAAkB;oBAClB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;wBAC3C,MAAM,EAAE,MAAM;wBACd,GAAG,EAAE,GAAG,UAAU,cAAc;wBAChC,OAAO,EAAE;4BACR,eAAe,EAAE,UAAU,WAAW,EAAE;4BACxC,cAAc,EAAE,kBAAkB;yBAClC;wBACD,IAAI,EAAE,WAAW;wBACjB,IAAI,EAAE,IAAI;wBACV,OAAO,EAAE,EAAE,GAAC,EAAE,GAAC,IAAI;qBACnB,CAAC,CAAC;oBAEH,MAAM,IAAI,GAAI,QAAwB,CAAC,IAAqB,CAAC;oBAC7D,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;wBAC1B,MAAM,IAAI,2BAAY,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE;4BACtC,OAAO,EAAE,4CAA4C;yBACrD,CAAC,CAAC;oBACJ,CAAC;oBAED,kDAAkD;oBAClD,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;oBACpC,MAAM,UAAU,GAAkB,EAAE,CAAC;oBACrC,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;wBACxB,MAAM,IAAI,GAAI,GAAG,CAAC,IAAe,IAAI,EAAE,CAAC;wBACxC,IAAI,IAAI,IAAI,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;4BACjC,SAAS;wBACV,CAAC;wBACD,IAAI,IAAI,EAAE,CAAC;4BACV,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;wBACrB,CAAC;wBACD,UAAU,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;oBACtB,CAAC;oBACD,MAAM,iBAAiB,GAAG,IAAI,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;oBAC1D,OAAO,CAAC,GAAG,CAAC,yBAAyB,iBAAiB,oBAAoB,IAAI,CAAC,MAAM,UAAU,CAAC,CAAC;oBACjG,UAAU,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;oBAC/B,SAAS;gBACV,CAAC;gBAED,IAAI,QAAQ,KAAK,KAAK,IAAI,SAAS,KAAK,SAAS,EAAE,CAAC;oBACpD,0EAA0E;oBACzE,MAAM,UAAU,GAAG,EAAE,CAAC;oBAEtB,8DAA8D;oBAC9D,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;wBACnB,SAAS;oBACV,CAAC;oBAOF,MAAM,SAAS,GAAmB,EAAE,CAAC;oBACrC,MAAM,cAAc,GAAkB,EAAE,CAAC;oBAEzC,mCAAmC;oBACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;wBACvC,MAAM,MAAM,GAAG,IAAI,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,CAAW,CAAC;wBAE5D,IAAI,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;4BAChC,SAAS,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;wBAC3C,CAAC;6BAAM,CAAC;4BACP,2CAA2C;4BAC3C,cAAc,CAAC,IAAI,CAAC;gCACnB,OAAO,EAAE,KAAK;gCACd,KAAK,EAAE,oBAAoB,MAAM,IAAI,OAAO,EAAE;gCAC9C,cAAc,EAAE,CAAC;6BACjB,CAAC,CAAC;wBACJ,CAAC;oBACF,CAAC;oBAED,+BAA+B;oBAC/B,MAAM,YAAY,GAAkB,EAAE,CAAC;oBAEvC,KAAK,IAAI,UAAU,GAAG,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC,MAAM,EAAE,UAAU,IAAI,UAAU,EAAE,CAAC;wBAClF,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAAC,CAAC;wBACnE,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;wBAE1C,IAAI,CAAC;4BACJ,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;gCAC3C,MAAM,EAAE,MAAM;gCACd,GAAG,EAAE,GAAG,UAAU,qBAAqB;gCACvC,OAAO,EAAE;oCACR,aAAa,EAAE,UAAU,WAAW,EAAE;oCACtC,cAAc,EAAE,kBAAkB;iCAClC;gCACD,IAAI,EAAE;oCACL,IAAI,EAAE,SAAS;oCACf,cAAc,EAAE,CAAC;oCACjB,WAAW,EAAE,CAAC;iCACd;gCACD,IAAI,EAAE,IAAI;gCACV,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,+CAA+C;6BACxE,CAAC,CAAC;4BAEH,gDAAgD;4BAChD,MAAM,OAAO,GAAI,QAAwB,CAAC,OAAwB,CAAC;4BACnE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gCACzC,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;gCAC1B,YAAY,CAAC,IAAI,CAAC;oCACjB,GAAG,MAAM;oCACT,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK;iCAC9B,CAAC,CAAC;4BACJ,CAAC;wBACF,CAAC;wBAAC,OAAO,GAAG,EAAE,CAAC;4BACd,yCAAyC;4BACzC,MAAM,UAAU,GAAI,GAAW,CAAC,UAAU,CAAC;4BAC3C,MAAM,MAAM,GAAI,GAAa,CAAC,OAAO,IAAI,EAAE,CAAC;4BAC5C,IAAI,UAAU,KAAK,GAAG,IAAI,UAAU,KAAK,GAAG,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;gCAClG,MAAM,GAAG,CAAC;4BACX,CAAC;4BACD,0DAA0D;4BAC1D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gCAC1B,YAAY,CAAC,IAAI,CAAC;oCACjB,OAAO,EAAE,KAAK;oCACd,KAAK,EAAE,yBAA0B,GAAa,CAAC,OAAO,EAAE;oCACxD,GAAG,EAAE,IAAI,CAAC,GAAG;oCACb,cAAc,EAAE,IAAI,CAAC,KAAK;iCAC1B,CAAC,CAAC;4BACJ,CAAC;wBACF,CAAC;oBACF,CAAC;oBAED,8EAA8E;oBAC9E,MAAM,UAAU,GAAG,CAAC,GAAG,cAAc,EAAE,GAAG,YAAY,CAAC,CAAC;oBACxD,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAE,CAAC,CAAC,cAAyB,GAAI,CAAC,CAAC,cAAyB,CAAC,CAAC;oBAEvF,6DAA6D;oBAC7D,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;wBACjC,MAAM,EAAE,cAAc,EAAE,CAAC,EAAE,GAAG,WAAW,EAAE,GAAG,MAAM,CAAC;wBACrD,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;oBAC9B,CAAC;oBAED,uDAAuD;oBACvD,MAAM;gBACN,CAAC;gBAED,IAAI,QAAQ,KAAK,SAAS,IAAI,SAAS,KAAK,WAAW,EAAE,CAAC;oBAC1D,0EAA0E;oBACzE,MAAM,UAAU,GAAG,EAAE,CAAC;oBAEtB,8DAA8D;oBAC9D,IAAI,SAAS,GAAG,CAAC,EAAE,CAAC;wBACnB,SAAS;oBACV,CAAC;oBAOD,MAAM,SAAS,GAAmB,EAAE,CAAC;oBACrC,MAAM,cAAc,GAAkB,EAAE,CAAC;oBAEzC,mCAAmC;oBACnC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;wBACvC,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,CAAC,YAAY,EAAE,CAAC,CAAW,CAAC;wBAEpE,IAAI,UAAU,aAAV,UAAU,uBAAV,UAAU,CAAE,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC;4BACpC,SAAS,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;wBAC/C,CAAC;6BAAM,CAAC;4BACP,2CAA2C;4BAC3C,cAAc,CAAC,IAAI,CAAC;gCACnB,cAAc,EAAE,CAAC;gCACjB,MAAM,EAAE,SAAS;gCACjB,OAAO,EAAE,KAAK;gCACd,KAAK,EAAE,wBAAwB,UAAU,IAAI,OAAO,EAAE;gCACtD,cAAc,EAAE,CAAC;6BACjB,CAAC,CAAC;wBACJ,CAAC;oBACF,CAAC;oBAED,+BAA+B;oBAC/B,MAAM,YAAY,GAAkB,EAAE,CAAC;oBAEvC,KAAK,IAAI,UAAU,GAAG,CAAC,EAAE,UAAU,GAAG,SAAS,CAAC,MAAM,EAAE,UAAU,IAAI,UAAU,EAAE,CAAC;wBAClF,MAAM,KAAK,GAAG,SAAS,CAAC,KAAK,CAAC,UAAU,EAAE,UAAU,GAAG,UAAU,CAAC,CAAC;wBACnE,MAAM,SAAS,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;wBAE1C,IAAI,CAAC;4BACJ,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC;gCAC3C,MAAM,EAAE,MAAM;gCACd,GAAG,EAAE,GAAG,UAAU,4BAA4B;gCAC9C,OAAO,EAAE;oCACR,aAAa,EAAE,UAAU,WAAW,EAAE;oCACtC,cAAc,EAAE,kBAAkB;iCAClC;gCACD,IAAI,EAAE;oCACL,IAAI,EAAE,SAAS;oCACf,cAAc,EAAE,CAAC;oCACjB,WAAW,EAAE,CAAC;iCACd;gCACD,IAAI,EAAE,IAAI;gCACV,OAAO,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI,EAAE,uBAAuB;6BAChD,CAAC,CAAC;4BAEH,gDAAgD;4BAChD,MAAM,OAAO,GAAI,QAAwB,CAAC,OAAwB,CAAC;4BACnE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gCACzC,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;gCAC1B,YAAY,CAAC,IAAI,CAAC;oCACjB,cAAc,EAAE,MAAA,MAAM,CAAC,cAAc,mCAAI,CAAC;oCAC1C,MAAM,EAAE,MAAA,MAAM,CAAC,MAAM,mCAAI,SAAS;oCAClC,OAAO,EAAE,MAAA,MAAM,CAAC,OAAO,mCAAI,KAAK;oCAChC,KAAK,EAAE,MAAM,CAAC,KAAK;oCACnB,GAAG,EAAE,MAAM,CAAC,GAAG;oCACf,cAAc,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK;iCAC9B,CAAC,CAAC;4BACJ,CAAC;wBACF,CAAC;wBAAC,OAAO,GAAG,EAAE,CAAC;4BACd,0DAA0D;4BAC1D,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gCAC1B,YAAY,CAAC,IAAI,CAAC;oCACjB,cAAc,EAAE,CAAC;oCACjB,MAAM,EAAE,OAAO;oCACf,OAAO,EAAE,KAAK;oCACd,KAAK,EAAE,yBAA0B,GAAa,CAAC,OAAO,EAAE;oCACxD,GAAG,EAAE,IAAI,CAAC,GAAG;oCACb,cAAc,EAAE,IAAI,CAAC,KAAK;iCAC1B,CAAC,CAAC;4BACJ,CAAC;wBACF,CAAC;oBACF,CAAC;oBAED,8EAA8E;oBAC9E,MAAM,UAAU,GAAG,CAAC,GAAG,cAAc,EAAE,GAAG,YAAY,CAAC,CAAC;oBACxD,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAE,CAAC,CAAC,cAAyB,GAAI,CAAC,CAAC,cAAyB,CAAC,CAAC;oBAEvF,6DAA6D;oBAC7D,KAAK,MAAM,MAAM,IAAI,UAAU,EAAE,CAAC;wBACjC,MAAM,EAAE,cAAc,EAAE,CAAC,EAAE,GAAG,WAAW,EAAE,GAAG,MAAM,CAAC;wBACrD,UAAU,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;oBAC9B,CAAC;oBAED,uDAAuD;oBACvD,MAAM;gBACP,CAAC;gBAED,MAAM,IAAI,2BAAY,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE;oBACtC,OAAO,EAAE,yBAAyB,QAAQ,mBAAmB,SAAS,GAAG;iBACzE,CAAC,CAAC;YACJ,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBAChB,MAAM,YAAY,GAAG,MAAC,KAAa,CAAC,OAAO,mCAAK,KAAe,CAAC,OAAO,CAAC;gBAExE,2DAA2D;gBAC3D,IAAK,KAAa,CAAC,UAAU,KAAK,GAAG,IAAK,KAAa,CAAC,UAAU,KAAK,GAAG,IAAI,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,YAAY,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;oBAC5I,MAAM,IAAI,2BAAY,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE;wBACtC,OAAO,EAAE,wHAAwH;qBACjI,CAAC,CAAC;gBACJ,CAAC;gBAED,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;oBAC3B,UAAU,CAAC,IAAI,CAAC;wBACf,KAAK,EAAE,YAAY;wBACnB,SAAS;qBACT,CAAC,CAAC;oBACH,SAAS;gBACV,CAAC;gBACD,MAAM,KAAK,CAAC;YACb,CAAC;QACF,CAAC;QAED,OAAO,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;IACrD,CAAC;CACD;AAtkBD,0CAskBC","file":"LinkedinScraper.node.js","sourcesContent":["/**\n * Author: Marco Visin\n * Date: 2025-12-22\n * LinkedIn Scraper n8n node\n * Interacts with the LinkedIn scraper microservice\n * Uses the same InNotes API credentials for authentication\n */\n\nimport {\n\tIDataObject,\n\tIExecuteFunctions,\n\tINodeExecutionData,\n\tINodeType,\n\tINodeTypeDescription,\n\tNodeApiError,\n} from 'n8n-workflow';\n\nfunction splitCommaSeparated(value: string): string[] {\n\treturn value\n\t\t.split(',')\n\t\t.map((entry) => entry.trim())\n\t\t.filter((entry) => entry.length > 0);\n}\n\nexport class LinkedinScraper implements INodeType {\n\tdescription: INodeTypeDescription = {\n\t\tdisplayName: 'LinkedIn Scraper Service',\n\t\tname: 'linkedinScraper',\n\t\tgroup: ['transform'],\n\t\tversion: 1,\n\t\tdescription: 'Interact with the LinkedIn scraper microservice',\n\t\tsubtitle: '={{$parameter[\"operation\"] + \": \" + $parameter[\"resource\"]}}',\n\t\ticon: 'file:logo.svg',\n\t\tdefaults: {\n\t\t\tname: 'LinkedIn Scraper',\n\t\t},\n\t\tinputs: ['main'],\n\t\toutputs: ['main'],\n\t\tcredentials: [\n\t\t\t{\n\t\t\t\t// Reuse the same InNotes API credentials\n\t\t\t\tname: 'inNotesApi',\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t],\n\t\tproperties: [\n\t\t\t// Resource selection\n\t\t\t{\n\t\t\t\tdisplayName: 'Resource',\n\t\t\t\tname: 'resource',\n\t\t\t\ttype: 'options',\n\t\t\t\tnoDataExpression: true,\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Jobs',\n\t\t\t\t\t\tvalue: 'jobs',\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Job',\n\t\t\t\t\t\tvalue: 'job',\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Company',\n\t\t\t\t\t\tvalue: 'company',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdefault: 'jobs',\n\t\t\t},\n\t\t\t// Jobs operations\n\t\t\t{\n\t\t\t\tdisplayName: 'Operation',\n\t\t\t\tname: 'operation',\n\t\t\t\ttype: 'options',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Search',\n\t\t\t\t\t\tvalue: 'search',\n\t\t\t\t\t\taction: 'Search for jobs on LinkedIn and jobs.ch',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdefault: 'search',\n\t\t\t},\n\t\t\t// Job operations\n\t\t\t{\n\t\t\t\tdisplayName: 'Operation',\n\t\t\t\tname: 'operation',\n\t\t\t\ttype: 'options',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['job'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Details',\n\t\t\t\t\t\tvalue: 'details',\n\t\t\t\t\t\taction: 'Fetch job details',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdefault: 'details',\n\t\t\t},\n\t\t\t// Company operations\n\t\t\t{\n\t\t\t\tdisplayName: 'Operation',\n\t\t\t\tname: 'operation',\n\t\t\t\ttype: 'options',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['company'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Followers',\n\t\t\t\t\t\tvalue: 'followers',\n\t\t\t\t\t\taction: 'Fetch company follower count',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdefault: 'followers',\n\t\t\t},\n\t\t\t// Jobs search parameters\n\t\t\t{\n\t\t\t\tdisplayName: 'Job Titles',\n\t\t\t\tname: 'jobTitles',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tplaceholder: 'Software Engineer, Developer',\n\t\t\t\tdescription: 'Comma-separated list of job titles to search for',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Locations',\n\t\t\t\tname: 'locations',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tplaceholder: 'Zurich, Remote',\n\t\t\t\tdescription: 'Comma-separated list of locations to search in',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Max Results',\n\t\t\t\tname: 'maxResults',\n\t\t\t\ttype: 'number',\n\t\t\t\tdefault: 500,\n\t\t\t\ttypeOptions: {\n\t\t\t\t\tminValue: 1,\n\t\t\t\t\tmaxValue: 2000,\n\t\t\t\t},\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Include jobs.ch',\n\t\t\t\tname: 'includeJobsCh',\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdefault: true,\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Time Range',\n\t\t\t\tname: 'timeRange',\n\t\t\t\ttype: 'options',\n\t\t\t\toptions: [\n\t\t\t\t\t{ name: 'Past 24 Hours', value: 'r86400' },\n\t\t\t\t\t{ name: 'Past Week', value: 'r604800' },\n\t\t\t\t\t{ name: 'Past Month', value: 'r2592000' },\n\t\t\t\t],\n\t\t\t\tdefault: 'r86400',\n\t\t\t\tdescription: 'Filter jobs by posting date',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Company Blacklist',\n\t\t\t\tname: 'companyBlacklist',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tplaceholder: 'Company A, Company B',\n\t\t\t\tdescription: 'Comma-separated list of companies to exclude',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Keyword Blacklist',\n\t\t\t\tname: 'keywordBlacklist',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tplaceholder: 'Senior, Lead',\n\t\t\t\tdescription: 'Comma-separated list of keywords to exclude from job titles',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// Job details parameters\n\t\t\t{\n\t\t\t\tdisplayName: 'Job URL',\n\t\t\t\tname: 'jobUrl',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tdescription: 'LinkedIn or jobs.ch job URL',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['job'],\n\t\t\t\t\t\toperation: ['details'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t// Company parameters\n\t\t\t{\n\t\t\t\tdisplayName: 'Company URL',\n\t\t\t\tname: 'companyUrl',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tdescription: 'LinkedIn company URL',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['company'],\n\t\t\t\t\t\toperation: ['followers'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t],\n\t};\n\n\tasync execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {\n\t\tconst items = this.getInputData();\n\t\tconst returnData: IDataObject[] = [];\n\n\t\t// Get InNotes API credentials (reused for scraper authentication)\n\t\tconst credentials = (await this.getCredentials('inNotesApi')) as IDataObject | null;\n\n\t\tif (!credentials) {\n\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\tmessage: 'InNotes API credentials are missing',\n\t\t\t});\n\t\t}\n\n\t\t// No authMethod check here, deliberately. The InNotes credential used to\n\t\t// offer a choice between a Bearer token and username/password, and this\n\t\t// node refused anything that was not 'token'. 3b8c1e46 deleted the\n\t\t// username/password auth and the authMethod field along with it — but\n\t\t// only from the credential, in n8n_node/. This file was never touched,\n\t\t// so the guard went on reading a property that no longer exists and no\n\t\t// longer can: the field is gone from the credential type, so nothing\n\t\t// writes it and the n8n UI has no way to set it.\n\t\t//\n\t\t// The failure was total for any credential that did not happen to carry\n\t\t// the value left over from before, and the message sent people to go and\n\t\t// select an option that is no longer on the screen. The only thing this\n\t\t// node actually needs is the token.\n\t\t// Author: Marco — 2026-08-05\n\t\tconst bearerToken = credentials.token as string | undefined;\n\n\t\tif (!bearerToken) {\n\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\tmessage: 'Bearer token is missing from InNotes API credentials',\n\t\t\t});\n\t\t}\n\n\t\tfor (let itemIndex = 0; itemIndex < items.length; itemIndex++) {\n\t\t\tconst scraperUrl = 'https://scraper.innotes.me';\n\t\t\tconst resource = this.getNodeParameter('resource', itemIndex) as string;\n\t\t\tconst operation = this.getNodeParameter('operation', itemIndex) as string;\n\n\t\t\ttry {\n\t\t\t\tif (resource === 'jobs' && operation === 'search') {\n\t\t\t\t\tconst jobTitles = splitCommaSeparated(\n\t\t\t\t\t\tthis.getNodeParameter('jobTitles', itemIndex) as string,\n\t\t\t\t\t);\n\t\t\t\t\tconst locations = splitCommaSeparated(\n\t\t\t\t\t\tthis.getNodeParameter('locations', itemIndex) as string,\n\t\t\t\t\t);\n\t\t\t\t\tconst maxResults = this.getNodeParameter('maxResults', itemIndex) as number;\n\t\t\t\t\tconst includeJobsCh = this.getNodeParameter('includeJobsCh', itemIndex) as boolean;\n\t\t\t\t\tconst timeRange = this.getNodeParameter('timeRange', itemIndex) as string;\n\t\t\t\t\tconst companyBlacklist = splitCommaSeparated(\n\t\t\t\t\t\t(this.getNodeParameter('companyBlacklist', itemIndex) as string) || '',\n\t\t\t\t\t);\n\t\t\t\t\tconst keywordBlacklist = splitCommaSeparated(\n\t\t\t\t\t\t(this.getNodeParameter('keywordBlacklist', itemIndex) as string) || '',\n\t\t\t\t\t);\n\n\t\t\t\t\t// No cookies here, deliberately. This node used to read the\n\t\t\t\t\t// LinkedIn session from /api/user, parse three historical\n\t\t\t\t\t// shapes of it, and post it in the body below — putting a\n\t\t\t\t\t// live LinkedIn session through the workflow and into n8n's\n\t\t\t\t\t// execution log, for a component that has no use for it.\n\t\t\t\t\t//\n\t\t\t\t\t// The scraper already authenticates this same Bearer token\n\t\t\t\t\t// against InNotes on every request, so it fetches the\n\t\t\t\t\t// session itself. The cookies field is omitted entirely;\n\t\t\t\t\t// the scraper reads its absence as \"look it up\".\n\t\t\t\t\tconst requestBody: IDataObject = {\n\t\t\t\t\t\tjob_titles: jobTitles,\n\t\t\t\t\t\tlocations,\n\t\t\t\t\t\tmax_results: maxResults,\n\t\t\t\t\t\tinclude_jobs_ch: includeJobsCh,\n\t\t\t\t\t\ttime_range: timeRange,\n\t\t\t\t\t\tcompany_blacklist: companyBlacklist,\n\t\t\t\t\t\tkeywords_blacklist: keywordBlacklist,\n\t\t\t\t\t};\n\n\t\t\t\t\t// Search for jobs\n\t\t\t\t\tconst response = await this.helpers.request({\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\turl: `${scraperUrl}/jobs/search`,\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Authorization': `Bearer ${bearerToken}`,\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody: requestBody,\n\t\t\t\t\t\tjson: true,\n\t\t\t\t\t\ttimeout: 90*60*1000,\n\t\t\t\t\t});\n\n\t\t\t\t\tconst jobs = (response as IDataObject).jobs as IDataObject[];\n\t\t\t\t\tif (!Array.isArray(jobs)) {\n\t\t\t\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\t\t\t\tmessage: 'Unexpected response format from job search',\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Deduplicate by link (used as ext_id in InNotes)\n\t\t\t\t\tconst seenLinks = new Set<string>();\n\t\t\t\t\tconst uniqueJobs: IDataObject[] = [];\n\t\t\t\t\tfor (const job of jobs) {\n\t\t\t\t\t\tconst link = (job.link as string) || '';\n\t\t\t\t\t\tif (link && seenLinks.has(link)) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (link) {\n\t\t\t\t\t\t\tseenLinks.add(link);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tuniqueJobs.push(job);\n\t\t\t\t\t}\n\t\t\t\t\tconst duplicatesRemoved = jobs.length - uniqueJobs.length;\n\t\t\t\t\tconsole.log(`Deduplicated: removed ${duplicatesRemoved} duplicates from ${jobs.length} results`);\n\t\t\t\t\treturnData.push(...uniqueJobs);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (resource === 'job' && operation === 'details') {\n\t\t\t\t// Batch processing: collect all URLs, chunk them, and send batch requests\n\t\t\t\t\tconst CHUNK_SIZE = 30;\n\n\t\t\t\t\t// Only process on first item - we'll handle all items at once\n\t\t\t\t\tif (itemIndex > 0) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t// Collect all job URLs with their original indices\n\t\t\t\tinterface UrlWithIndex {\n\t\t\t\t\turl: string;\n\t\t\t\t\tindex: number;\n\t\t\t\t}\n\t\t\t\tconst validUrls: UrlWithIndex[] = [];\n\t\t\t\tconst invalidResults: IDataObject[] = [];\n\n\t\t\t\t// Validate and categorize all URLs\n\t\t\t\tfor (let i = 0; i < items.length; i++) {\n\t\t\t\t\tconst jobUrl = this.getNodeParameter('jobUrl', i) as string;\n\n\t\t\t\t\tif (jobUrl?.startsWith('http')) {\n\t\t\t\t\t\tvalidUrls.push({ url: jobUrl, index: i });\n\t\t\t\t\t} else {\n\t\t\t\t\t\t// Invalid URL - add to results immediately\n\t\t\t\t\t\tinvalidResults.push({\n\t\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\t\terror: `Invalid job URL: ${jobUrl || 'empty'}`,\n\t\t\t\t\t\t\t_originalIndex: i,\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Process valid URLs in chunks\n\t\t\t\tconst batchResults: IDataObject[] = [];\n\n\t\t\t\tfor (let chunkStart = 0; chunkStart < validUrls.length; chunkStart += CHUNK_SIZE) {\n\t\t\t\t\tconst chunk = validUrls.slice(chunkStart, chunkStart + CHUNK_SIZE);\n\t\t\t\t\tconst chunkUrls = chunk.map((c) => c.url);\n\n\t\t\t\t\ttry {\n\t\t\t\t\t\tconst response = await this.helpers.request({\n\t\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\t\turl: `${scraperUrl}/jobs/details/batch`,\n\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\tAuthorization: `Bearer ${bearerToken}`,\n\t\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\turls: chunkUrls,\n\t\t\t\t\t\t\t\tmax_concurrent: 5,\n\t\t\t\t\t\t\t\tmax_retries: 7,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tjson: true,\n\t\t\t\t\t\t\ttimeout: 15 * 60 * 1000, // 15 minutes per batch (more retries = longer)\n\t\t\t\t\t\t});\n\n\t\t\t\t\t\t// Map results back, preserving original indices\n\t\t\t\t\t\tconst results = (response as IDataObject).results as IDataObject[];\n\t\t\t\t\t\tfor (let j = 0; j < results.length; j++) {\n\t\t\t\t\t\t\tconst result = results[j];\n\t\t\t\t\t\t\tbatchResults.push({\n\t\t\t\t\t\t\t\t...result,\n\t\t\t\t\t\t\t\t_originalIndex: chunk[j].index,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t// Re-throw auth errors (session expired)\n\t\t\t\t\t\tconst statusCode = (err as any).statusCode;\n\t\t\t\t\t\tconst errMsg = (err as Error).message || '';\n\t\t\t\t\t\tif (statusCode === 401 || statusCode === 403 || errMsg.includes('401') || errMsg.includes('403')) {\n\t\t\t\t\t\t\tthrow err;\n\t\t\t\t\t\t}\n\t\t\t\t\t\t// If entire batch fails, mark all URLs in chunk as failed\n\t\t\t\t\t\tfor (const item of chunk) {\n\t\t\t\t\t\t\tbatchResults.push({\n\t\t\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\t\t\terror: `Batch request failed: ${(err as Error).message}`,\n\t\t\t\t\t\t\t\turl: item.url,\n\t\t\t\t\t\t\t\t_originalIndex: item.index,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// Combine invalid and batch results, sort by original index to maintain order\n\t\t\t\tconst allResults = [...invalidResults, ...batchResults];\n\t\t\t\tallResults.sort((a, b) => (a._originalIndex as number) - (b._originalIndex as number));\n\n\t\t\t\t// Remove internal _originalIndex field and add to returnData\n\t\t\t\tfor (const result of allResults) {\n\t\t\t\t\tconst { _originalIndex: _, ...cleanResult } = result;\n\t\t\t\t\treturnData.push(cleanResult);\n\t\t\t\t}\n\n\t\t\t\t// Skip further iterations since we processed all items\n\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif (resource === 'company' && operation === 'followers') {\n\t\t\t\t// Batch processing: collect all URLs, chunk them, and send batch requests\n\t\t\t\t\tconst CHUNK_SIZE = 30;\n\n\t\t\t\t\t// Only process on first item - we'll handle all items at once\n\t\t\t\t\tif (itemIndex > 0) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\t// Collect all company URLs with their original indices\n\t\t\t\t\tinterface UrlWithIndex {\n\t\t\t\t\t\turl: string;\n\t\t\t\t\t\tindex: number;\n\t\t\t\t\t}\n\t\t\t\t\tconst validUrls: UrlWithIndex[] = [];\n\t\t\t\t\tconst invalidResults: IDataObject[] = [];\n\n\t\t\t\t\t// Validate and categorize all URLs\n\t\t\t\t\tfor (let i = 0; i < items.length; i++) {\n\t\t\t\t\t\tconst companyUrl = this.getNodeParameter('companyUrl', i) as string;\n\n\t\t\t\t\t\tif (companyUrl?.startsWith('http')) {\n\t\t\t\t\t\t\tvalidUrls.push({ url: companyUrl, index: i });\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t// Invalid URL - add to results immediately\n\t\t\t\t\t\t\tinvalidResults.push({\n\t\t\t\t\t\t\t\tfollower_count: 0,\n\t\t\t\t\t\t\t\tsource: 'skipped',\n\t\t\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\t\t\terror: `Invalid company URL: ${companyUrl || 'empty'}`,\n\t\t\t\t\t\t\t\t_originalIndex: i,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Process valid URLs in chunks\n\t\t\t\t\tconst batchResults: IDataObject[] = [];\n\n\t\t\t\t\tfor (let chunkStart = 0; chunkStart < validUrls.length; chunkStart += CHUNK_SIZE) {\n\t\t\t\t\t\tconst chunk = validUrls.slice(chunkStart, chunkStart + CHUNK_SIZE);\n\t\t\t\t\t\tconst chunkUrls = chunk.map((c) => c.url);\n\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tconst response = await this.helpers.request({\n\t\t\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\t\t\turl: `${scraperUrl}/companies/followers/batch`,\n\t\t\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t\t\tAuthorization: `Bearer ${bearerToken}`,\n\t\t\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tbody: {\n\t\t\t\t\t\t\t\t\turls: chunkUrls,\n\t\t\t\t\t\t\t\t\tmax_concurrent: 5,\n\t\t\t\t\t\t\t\t\tmax_retries: 7,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tjson: true,\n\t\t\t\t\t\t\t\ttimeout: 10 * 60 * 1000, // 10 minutes per batch\n\t\t\t\t\t\t\t});\n\n\t\t\t\t\t\t\t// Map results back, preserving original indices\n\t\t\t\t\t\t\tconst results = (response as IDataObject).results as IDataObject[];\n\t\t\t\t\t\t\tfor (let j = 0; j < results.length; j++) {\n\t\t\t\t\t\t\t\tconst result = results[j];\n\t\t\t\t\t\t\t\tbatchResults.push({\n\t\t\t\t\t\t\t\t\tfollower_count: result.follower_count ?? 0,\n\t\t\t\t\t\t\t\t\tsource: result.source ?? 'unknown',\n\t\t\t\t\t\t\t\t\tsuccess: result.success ?? false,\n\t\t\t\t\t\t\t\t\terror: result.error,\n\t\t\t\t\t\t\t\t\turl: result.url,\n\t\t\t\t\t\t\t\t\t_originalIndex: chunk[j].index,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t} catch (err) {\n\t\t\t\t\t\t\t// If entire batch fails, mark all URLs in chunk as failed\n\t\t\t\t\t\t\tfor (const item of chunk) {\n\t\t\t\t\t\t\t\tbatchResults.push({\n\t\t\t\t\t\t\t\t\tfollower_count: 0,\n\t\t\t\t\t\t\t\t\tsource: 'error',\n\t\t\t\t\t\t\t\t\tsuccess: false,\n\t\t\t\t\t\t\t\t\terror: `Batch request failed: ${(err as Error).message}`,\n\t\t\t\t\t\t\t\t\turl: item.url,\n\t\t\t\t\t\t\t\t\t_originalIndex: item.index,\n\t\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// Combine invalid and batch results, sort by original index to maintain order\n\t\t\t\t\tconst allResults = [...invalidResults, ...batchResults];\n\t\t\t\t\tallResults.sort((a, b) => (a._originalIndex as number) - (b._originalIndex as number));\n\n\t\t\t\t\t// Remove internal _originalIndex field and add to returnData\n\t\t\t\t\tfor (const result of allResults) {\n\t\t\t\t\t\tconst { _originalIndex: _, ...cleanResult } = result;\n\t\t\t\t\t\treturnData.push(cleanResult);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Skip further iterations since we processed all items\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\t\tmessage: `Unsupported resource (${resource}) or operation (${operation})`,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMessage = (error as any).message ?? (error as Error).message;\n\n\t\t\t\t// Check for authentication errors from the scraper service\n\t\t\t\tif ((error as any).statusCode === 401 || (error as any).statusCode === 403 || errorMessage.includes('401') || errorMessage.includes('403')) {\n\t\t\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\t\t\tmessage: 'LinkedIn session expired. Please visit LinkedIn in a browser with the InNotes extension enabled to renew your session.',\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tif (this.continueOnFail()) {\n\t\t\t\t\treturnData.push({\n\t\t\t\t\t\terror: errorMessage,\n\t\t\t\t\t\titemIndex,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\treturn [returnData.map((data) => ({ json: data }))];\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../nodes/LinkedinScraper/LinkedinScraper.node.ts"],"names":[],"mappings":"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","file":"LinkedinScraper.node.js","sourcesContent":["/**\n * Author: Marco Visin\n * Date: 2026-09-09\n * LinkedIn Scraper n8n node\n * Interacts with the LinkedIn scraper microservice\n * Uses the same InNotes API credentials for authentication\n */\n\nimport {\n\tIDataObject,\n\tIExecuteFunctions,\n\tINodeExecutionData,\n\tINodeType,\n\tINodeTypeDescription,\n\tNodeApiError,\n} from 'n8n-workflow';\n\nfunction splitCommaSeparated(value: string): string[] {\n\treturn value\n\t\t.split(',')\n\t\t.map((entry) => entry.trim())\n\t\t.filter((entry) => entry.length > 0);\n}\n\nconst URL_BATCH_CHUNK_SIZE = 30;\n\ninterface UrlBatchOptions {\n\titemCount: number;\n\tbearerToken: string;\n\turlParameter: string;\n\tinvalidUrlLabel: string;\n\tendpoint: string;\n\ttimeout: number;\n\t/** job/details aborts the whole run on an auth error; company/followers reports it as a failed row. */\n\trethrowAuthErrors: boolean;\n\tmapSuccess: (result: IDataObject) => IDataObject;\n\t/** Spread before success/error: the key order of these rows is the node's output. */\n\tskippedDefaults?: IDataObject;\n\terrorDefaults?: IDataObject;\n}\n\nfunction isAuthError(err: unknown): boolean {\n\tconst statusCode = (err as any).statusCode;\n\tconst errMsg = (err as Error).message || '';\n\treturn statusCode === 401 || statusCode === 403 || errMsg.includes('401') || errMsg.includes('403');\n}\n\n/**\n * Chunked batch POST of one URL parameter per input item. Every difference\n * between the two callers is an output contract, so it is an option here\n * rather than a branch.\n *\n * The short-results drop is reproduced, not fixed: a response carrying fewer\n * results than URLs silently loses the rest, and fixing it here would be a\n * behaviour change wearing a refactor's clothes.\n * Author: Marco — 2026-09-09\n */\nasync function batchByUrl(ctx: IExecuteFunctions, options: UrlBatchOptions): Promise<IDataObject[]> {\n\tinterface UrlWithIndex {\n\t\turl: string;\n\t\tindex: number;\n\t}\n\tconst validUrls: UrlWithIndex[] = [];\n\tconst rows: IDataObject[] = [];\n\n\tfor (let i = 0; i < options.itemCount; i++) {\n\t\tconst url = ctx.getNodeParameter(options.urlParameter, i) as string;\n\n\t\tif (url?.startsWith('http')) {\n\t\t\tvalidUrls.push({ url, index: i });\n\t\t} else {\n\t\t\trows.push({\n\t\t\t\t...options.skippedDefaults,\n\t\t\t\tsuccess: false,\n\t\t\t\terror: `Invalid ${options.invalidUrlLabel} URL: ${url || 'empty'}`,\n\t\t\t\t_originalIndex: i,\n\t\t\t});\n\t\t}\n\t}\n\n\tfor (let chunkStart = 0; chunkStart < validUrls.length; chunkStart += URL_BATCH_CHUNK_SIZE) {\n\t\tconst chunk = validUrls.slice(chunkStart, chunkStart + URL_BATCH_CHUNK_SIZE);\n\t\tconst chunkUrls = chunk.map((c) => c.url);\n\n\t\ttry {\n\t\t\tconst response = await ctx.helpers.request({\n\t\t\t\tmethod: 'POST',\n\t\t\t\turl: options.endpoint,\n\t\t\t\theaders: {\n\t\t\t\t\tAuthorization: `Bearer ${options.bearerToken}`,\n\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t},\n\t\t\t\tbody: {\n\t\t\t\t\turls: chunkUrls,\n\t\t\t\t\tmax_concurrent: 5,\n\t\t\t\t\tmax_retries: 7,\n\t\t\t\t},\n\t\t\t\tjson: true,\n\t\t\t\ttimeout: options.timeout,\n\t\t\t});\n\n\t\t\t// Map results back, preserving original indices\n\t\t\tconst results = (response as IDataObject).results as IDataObject[];\n\t\t\tfor (let j = 0; j < results.length; j++) {\n\t\t\t\trows.push({\n\t\t\t\t\t...options.mapSuccess(results[j]),\n\t\t\t\t\t_originalIndex: chunk[j].index,\n\t\t\t\t});\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tif (options.rethrowAuthErrors && isAuthError(err)) {\n\t\t\t\tthrow err;\n\t\t\t}\n\t\t\t// If entire batch fails, mark all URLs in chunk as failed\n\t\t\tfor (const item of chunk) {\n\t\t\t\trows.push({\n\t\t\t\t\t...options.errorDefaults,\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: `Batch request failed: ${(err as Error).message}`,\n\t\t\t\t\turl: item.url,\n\t\t\t\t\t_originalIndex: item.index,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t}\n\n\t// Sort by original index to maintain order, then drop the internal field\n\trows.sort((a, b) => (a._originalIndex as number) - (b._originalIndex as number));\n\n\treturn rows.map((row) => {\n\t\tconst { _originalIndex: _, ...cleanRow } = row;\n\t\treturn cleanRow;\n\t});\n}\n\nexport class LinkedinScraper implements INodeType {\n\tdescription: INodeTypeDescription = {\n\t\tdisplayName: 'LinkedIn Scraper Service',\n\t\tname: 'linkedinScraper',\n\t\tgroup: ['transform'],\n\t\tversion: 1,\n\t\tdescription: 'Interact with the LinkedIn scraper microservice',\n\t\tsubtitle: '={{$parameter[\"operation\"] + \": \" + $parameter[\"resource\"]}}',\n\t\ticon: 'file:logo.svg',\n\t\tdefaults: {\n\t\t\tname: 'LinkedIn Scraper',\n\t\t},\n\t\tinputs: ['main'],\n\t\toutputs: ['main'],\n\t\tcredentials: [\n\t\t\t{\n\t\t\t\t// Reuse the same InNotes API credentials\n\t\t\t\tname: 'inNotesApi',\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t],\n\t\tproperties: [\n\t\t\t// Resource selection\n\t\t\t{\n\t\t\t\tdisplayName: 'Resource',\n\t\t\t\tname: 'resource',\n\t\t\t\ttype: 'options',\n\t\t\t\tnoDataExpression: true,\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Jobs',\n\t\t\t\t\t\tvalue: 'jobs',\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Job',\n\t\t\t\t\t\tvalue: 'job',\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Company',\n\t\t\t\t\t\tvalue: 'company',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdefault: 'jobs',\n\t\t\t},\n\t\t\t// Jobs operations\n\t\t\t{\n\t\t\t\tdisplayName: 'Operation',\n\t\t\t\tname: 'operation',\n\t\t\t\ttype: 'options',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Search',\n\t\t\t\t\t\tvalue: 'search',\n\t\t\t\t\t\taction: 'Search for jobs on LinkedIn and jobs.ch',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdefault: 'search',\n\t\t\t},\n\t\t\t// Job operations\n\t\t\t{\n\t\t\t\tdisplayName: 'Operation',\n\t\t\t\tname: 'operation',\n\t\t\t\ttype: 'options',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['job'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Details',\n\t\t\t\t\t\tvalue: 'details',\n\t\t\t\t\t\taction: 'Fetch job details',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdefault: 'details',\n\t\t\t},\n\t\t\t// Company operations\n\t\t\t{\n\t\t\t\tdisplayName: 'Operation',\n\t\t\t\tname: 'operation',\n\t\t\t\ttype: 'options',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['company'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\toptions: [\n\t\t\t\t\t{\n\t\t\t\t\t\tname: 'Followers',\n\t\t\t\t\t\tvalue: 'followers',\n\t\t\t\t\t\taction: 'Fetch company follower count',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdefault: 'followers',\n\t\t\t},\n\t\t\t// Jobs search parameters\n\t\t\t{\n\t\t\t\tdisplayName: 'Job Titles',\n\t\t\t\tname: 'jobTitles',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tplaceholder: 'Software Engineer, Developer',\n\t\t\t\tdescription: 'Comma-separated list of job titles to search for',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Locations',\n\t\t\t\tname: 'locations',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tplaceholder: 'Zurich, Remote',\n\t\t\t\tdescription: 'Comma-separated list of locations to search in',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Max Results',\n\t\t\t\tname: 'maxResults',\n\t\t\t\ttype: 'number',\n\t\t\t\tdefault: 500,\n\t\t\t\ttypeOptions: {\n\t\t\t\t\tminValue: 1,\n\t\t\t\t\tmaxValue: 2000,\n\t\t\t\t},\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Include jobs.ch',\n\t\t\t\tname: 'includeJobsCh',\n\t\t\t\ttype: 'boolean',\n\t\t\t\tdefault: true,\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Time Range',\n\t\t\t\tname: 'timeRange',\n\t\t\t\ttype: 'options',\n\t\t\t\toptions: [\n\t\t\t\t\t{ name: 'Past 24 Hours', value: 'r86400' },\n\t\t\t\t\t{ name: 'Past Week', value: 'r604800' },\n\t\t\t\t\t{ name: 'Past Month', value: 'r2592000' },\n\t\t\t\t],\n\t\t\t\tdefault: 'r86400',\n\t\t\t\tdescription: 'Filter jobs by posting date',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Company Blacklist',\n\t\t\t\tname: 'companyBlacklist',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tplaceholder: 'Company A, Company B',\n\t\t\t\tdescription: 'Comma-separated list of companies to exclude',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tdisplayName: 'Keyword Blacklist',\n\t\t\t\tname: 'keywordBlacklist',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tplaceholder: 'Senior, Lead',\n\t\t\t\tdescription: 'Comma-separated list of keywords to exclude from job titles',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['jobs'],\n\t\t\t\t\t\toperation: ['search'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t// Job details parameters\n\t\t\t{\n\t\t\t\tdisplayName: 'Job URL',\n\t\t\t\tname: 'jobUrl',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tdescription: 'LinkedIn or jobs.ch job URL',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['job'],\n\t\t\t\t\t\toperation: ['details'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t\t// Company parameters\n\t\t\t{\n\t\t\t\tdisplayName: 'Company URL',\n\t\t\t\tname: 'companyUrl',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tdescription: 'LinkedIn company URL',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['company'],\n\t\t\t\t\t\toperation: ['followers'],\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\trequired: true,\n\t\t\t},\n\t\t],\n\t};\n\n\tasync execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {\n\t\tconst items = this.getInputData();\n\t\tconst returnData: IDataObject[] = [];\n\n\t\t// Get InNotes API credentials (reused for scraper authentication)\n\t\tconst credentials = (await this.getCredentials('inNotesApi')) as IDataObject | null;\n\n\t\tif (!credentials) {\n\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\tmessage: 'InNotes API credentials are missing',\n\t\t\t});\n\t\t}\n\n\t\t// No authMethod check here, deliberately. The InNotes credential used to\n\t\t// offer a choice between a Bearer token and username/password, and this\n\t\t// node refused anything that was not 'token'. 3b8c1e46 deleted the\n\t\t// username/password auth and the authMethod field along with it — but\n\t\t// only from the credential, in n8n_node/. This file was never touched,\n\t\t// so the guard went on reading a property that no longer exists and no\n\t\t// longer can: the field is gone from the credential type, so nothing\n\t\t// writes it and the n8n UI has no way to set it.\n\t\t//\n\t\t// The failure was total for any credential that did not happen to carry\n\t\t// the value left over from before, and the message sent people to go and\n\t\t// select an option that is no longer on the screen. The only thing this\n\t\t// node actually needs is the token.\n\t\t// Author: Marco — 2026-08-05\n\t\tconst bearerToken = credentials.token as string | undefined;\n\n\t\tif (!bearerToken) {\n\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\tmessage: 'Bearer token is missing from InNotes API credentials',\n\t\t\t});\n\t\t}\n\n\t\tfor (let itemIndex = 0; itemIndex < items.length; itemIndex++) {\n\t\t\tconst scraperUrl = 'https://scraper.innotes.me';\n\t\t\tconst resource = this.getNodeParameter('resource', itemIndex) as string;\n\t\t\tconst operation = this.getNodeParameter('operation', itemIndex) as string;\n\n\t\t\ttry {\n\t\t\t\tif (resource === 'jobs' && operation === 'search') {\n\t\t\t\t\tconst jobTitles = splitCommaSeparated(\n\t\t\t\t\t\tthis.getNodeParameter('jobTitles', itemIndex) as string,\n\t\t\t\t\t);\n\t\t\t\t\tconst locations = splitCommaSeparated(\n\t\t\t\t\t\tthis.getNodeParameter('locations', itemIndex) as string,\n\t\t\t\t\t);\n\t\t\t\t\tconst maxResults = this.getNodeParameter('maxResults', itemIndex) as number;\n\t\t\t\t\tconst includeJobsCh = this.getNodeParameter('includeJobsCh', itemIndex) as boolean;\n\t\t\t\t\tconst timeRange = this.getNodeParameter('timeRange', itemIndex) as string;\n\t\t\t\t\tconst companyBlacklist = splitCommaSeparated(\n\t\t\t\t\t\t(this.getNodeParameter('companyBlacklist', itemIndex) as string) || '',\n\t\t\t\t\t);\n\t\t\t\t\tconst keywordBlacklist = splitCommaSeparated(\n\t\t\t\t\t\t(this.getNodeParameter('keywordBlacklist', itemIndex) as string) || '',\n\t\t\t\t\t);\n\n\t\t\t\t\t// No cookies here, deliberately. This node used to read the\n\t\t\t\t\t// LinkedIn session from /api/user, parse three historical\n\t\t\t\t\t// shapes of it, and post it in the body below — putting a\n\t\t\t\t\t// live LinkedIn session through the workflow and into n8n's\n\t\t\t\t\t// execution log, for a component that has no use for it.\n\t\t\t\t\t//\n\t\t\t\t\t// The scraper already authenticates this same Bearer token\n\t\t\t\t\t// against InNotes on every request, so it fetches the\n\t\t\t\t\t// session itself. The cookies field is omitted entirely;\n\t\t\t\t\t// the scraper reads its absence as \"look it up\".\n\t\t\t\t\tconst requestBody: IDataObject = {\n\t\t\t\t\t\tjob_titles: jobTitles,\n\t\t\t\t\t\tlocations,\n\t\t\t\t\t\tmax_results: maxResults,\n\t\t\t\t\t\tinclude_jobs_ch: includeJobsCh,\n\t\t\t\t\t\ttime_range: timeRange,\n\t\t\t\t\t\tcompany_blacklist: companyBlacklist,\n\t\t\t\t\t\tkeywords_blacklist: keywordBlacklist,\n\t\t\t\t\t};\n\n\t\t\t\t\t// Search for jobs\n\t\t\t\t\tconst response = await this.helpers.request({\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\turl: `${scraperUrl}/jobs/search`,\n\t\t\t\t\t\theaders: {\n\t\t\t\t\t\t\t'Authorization': `Bearer ${bearerToken}`,\n\t\t\t\t\t\t\t'Content-Type': 'application/json',\n\t\t\t\t\t\t},\n\t\t\t\t\t\tbody: requestBody,\n\t\t\t\t\t\tjson: true,\n\t\t\t\t\t\ttimeout: 90*60*1000,\n\t\t\t\t\t});\n\n\t\t\t\t\tconst jobs = (response as IDataObject).jobs as IDataObject[];\n\t\t\t\t\tif (!Array.isArray(jobs)) {\n\t\t\t\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\t\t\t\tmessage: 'Unexpected response format from job search',\n\t\t\t\t\t\t});\n\t\t\t\t\t}\n\n\t\t\t\t\t// Deduplicate by link (used as ext_id in InNotes)\n\t\t\t\t\tconst seenLinks = new Set<string>();\n\t\t\t\t\tconst uniqueJobs: IDataObject[] = [];\n\t\t\t\t\tfor (const job of jobs) {\n\t\t\t\t\t\tconst link = (job.link as string) || '';\n\t\t\t\t\t\tif (link && seenLinks.has(link)) {\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (link) {\n\t\t\t\t\t\t\tseenLinks.add(link);\n\t\t\t\t\t\t}\n\t\t\t\t\t\tuniqueJobs.push(job);\n\t\t\t\t\t}\n\t\t\t\t\treturnData.push(...uniqueJobs);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (resource === 'job' && operation === 'details') {\n\t\t\t\t\t// Only process on first item - we'll handle all items at once\n\t\t\t\t\tif (itemIndex > 0) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst results = await batchByUrl(this, {\n\t\t\t\t\t\titemCount: items.length,\n\t\t\t\t\t\tbearerToken,\n\t\t\t\t\t\turlParameter: 'jobUrl',\n\t\t\t\t\t\tinvalidUrlLabel: 'job',\n\t\t\t\t\t\tendpoint: `${scraperUrl}/jobs/details/batch`,\n\t\t\t\t\t\ttimeout: 15 * 60 * 1000, // 15 minutes per batch (more retries = longer)\n\t\t\t\t\t\trethrowAuthErrors: true,\n\t\t\t\t\t\t// Every field the scraper returns passes through\n\t\t\t\t\t\tmapSuccess: (result) => ({ ...result }),\n\t\t\t\t\t});\n\n\t\t\t\t\tfor (const result of results) {\n\t\t\t\t\t\treturnData.push(result);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Skip further iterations since we processed all items\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tif (resource === 'company' && operation === 'followers') {\n\t\t\t\t\t// Only process on first item - we'll handle all items at once\n\t\t\t\t\tif (itemIndex > 0) {\n\t\t\t\t\t\tcontinue;\n\t\t\t\t\t}\n\n\t\t\t\t\tconst results = await batchByUrl(this, {\n\t\t\t\t\t\titemCount: items.length,\n\t\t\t\t\t\tbearerToken,\n\t\t\t\t\t\turlParameter: 'companyUrl',\n\t\t\t\t\t\tinvalidUrlLabel: 'company',\n\t\t\t\t\t\tendpoint: `${scraperUrl}/companies/followers/batch`,\n\t\t\t\t\t\ttimeout: 10 * 60 * 1000, // 10 minutes per batch\n\t\t\t\t\t\t// An auth failure here is a failed row, not the end of the run\n\t\t\t\t\t\trethrowAuthErrors: false,\n\t\t\t\t\t\t// Fixed five keys: config/n8n-workflow-template.json reads follower_count\n\t\t\t\t\t\tmapSuccess: (result) => ({\n\t\t\t\t\t\t\tfollower_count: result.follower_count ?? 0,\n\t\t\t\t\t\t\tsource: result.source ?? 'unknown',\n\t\t\t\t\t\t\tsuccess: result.success ?? false,\n\t\t\t\t\t\t\terror: result.error,\n\t\t\t\t\t\t\turl: result.url,\n\t\t\t\t\t\t}),\n\t\t\t\t\t\tskippedDefaults: { follower_count: 0, source: 'skipped' },\n\t\t\t\t\t\terrorDefaults: { follower_count: 0, source: 'error' },\n\t\t\t\t\t});\n\n\t\t\t\t\tfor (const result of results) {\n\t\t\t\t\t\treturnData.push(result);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Skip further iterations since we processed all items\n\t\t\t\t\tbreak;\n\t\t\t\t}\n\n\t\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\t\tmessage: `Unsupported resource (${resource}) or operation (${operation})`,\n\t\t\t\t});\n\t\t\t} catch (error) {\n\t\t\t\tconst errorMessage = (error as any).message ?? (error as Error).message;\n\n\t\t\t\t// Check for authentication errors from the scraper service\n\t\t\t\tif ((error as any).statusCode === 401 || (error as any).statusCode === 403 || errorMessage.includes('401') || errorMessage.includes('403')) {\n\t\t\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\t\t\tmessage: 'LinkedIn session expired. Please visit LinkedIn in a browser with the InNotes extension enabled to renew your session.',\n\t\t\t\t\t});\n\t\t\t\t}\n\n\t\t\t\tif (this.continueOnFail()) {\n\t\t\t\t\treturnData.push({\n\t\t\t\t\t\terror: errorMessage,\n\t\t\t\t\t\titemIndex,\n\t\t\t\t\t});\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tthrow error;\n\t\t\t}\n\t\t}\n\n\t\treturn [returnData.map((data) => ({ json: data }))];\n\t}\n}\n"]}
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-linkedin-scraper",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.35",
|
|
4
4
|
"description": "N8N node for interacting with the standalone LinkedIn scraper microservice",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -25,11 +25,9 @@
|
|
|
25
25
|
"yarn": ">=1.22.0"
|
|
26
26
|
},
|
|
27
27
|
"packageManager": "yarn@1.22.19",
|
|
28
|
-
"main": "index.js",
|
|
29
28
|
"scripts": {
|
|
30
29
|
"build": "gulp build",
|
|
31
30
|
"dev": "gulp dev",
|
|
32
|
-
"format": "prettier . --write",
|
|
33
31
|
"lint": "eslint .",
|
|
34
32
|
"lintfix": "eslint . --fix",
|
|
35
33
|
"test": "jest",
|
|
@@ -56,15 +54,12 @@
|
|
|
56
54
|
"gulp-sourcemaps": "^3.0.0",
|
|
57
55
|
"gulp-typescript": "^6.0.0-alpha.1",
|
|
58
56
|
"jest": "^30.4.2",
|
|
59
|
-
"n8n-core": "2.16.1",
|
|
60
57
|
"n8n-workflow": "2.16.0",
|
|
61
|
-
"prettier": "^3.0.3",
|
|
62
58
|
"ts-jest": "^29.1.1",
|
|
63
59
|
"typescript": "^6.0.3",
|
|
64
60
|
"typescript-eslint": "^8.58.0"
|
|
65
61
|
},
|
|
66
62
|
"peerDependencies": {
|
|
67
|
-
"n8n-core": "*",
|
|
68
63
|
"n8n-workflow": "*"
|
|
69
64
|
},
|
|
70
65
|
"resolutions": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "n8n-nodes-linkedin-scraper",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.35",
|
|
4
4
|
"description": "N8N node for interacting with the standalone LinkedIn scraper microservice",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"n8n-community-node-package",
|
|
@@ -25,11 +25,9 @@
|
|
|
25
25
|
"yarn": ">=1.22.0"
|
|
26
26
|
},
|
|
27
27
|
"packageManager": "yarn@1.22.19",
|
|
28
|
-
"main": "index.js",
|
|
29
28
|
"scripts": {
|
|
30
29
|
"build": "gulp build",
|
|
31
30
|
"dev": "gulp dev",
|
|
32
|
-
"format": "prettier . --write",
|
|
33
31
|
"lint": "eslint .",
|
|
34
32
|
"lintfix": "eslint . --fix",
|
|
35
33
|
"test": "jest",
|
|
@@ -56,15 +54,12 @@
|
|
|
56
54
|
"gulp-sourcemaps": "^3.0.0",
|
|
57
55
|
"gulp-typescript": "^6.0.0-alpha.1",
|
|
58
56
|
"jest": "^30.4.2",
|
|
59
|
-
"n8n-core": "2.16.1",
|
|
60
57
|
"n8n-workflow": "2.16.0",
|
|
61
|
-
"prettier": "^3.0.3",
|
|
62
58
|
"ts-jest": "^29.1.1",
|
|
63
59
|
"typescript": "^6.0.3",
|
|
64
60
|
"typescript-eslint": "^8.58.0"
|
|
65
61
|
},
|
|
66
62
|
"peerDependencies": {
|
|
67
|
-
"n8n-core": "*",
|
|
68
63
|
"n8n-workflow": "*"
|
|
69
64
|
},
|
|
70
65
|
"resolutions": {
|
|
@@ -1,541 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
const LinkedinScraper_node_1 = require("../nodes/LinkedinScraper/LinkedinScraper.node");
|
|
4
|
-
describe('LinkedinScraper Node', () => {
|
|
5
|
-
describe('Node Properties', () => {
|
|
6
|
-
it('should expose jobs, job, and company resources', () => {
|
|
7
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
8
|
-
const resources = node.description.properties
|
|
9
|
-
.filter((property) => property.name === 'resource')
|
|
10
|
-
.flatMap((property) => ('options' in property ? property.options : []))
|
|
11
|
-
.filter((option) => option !== undefined && 'value' in option)
|
|
12
|
-
.map((option) => option.value);
|
|
13
|
-
expect(resources).toEqual(expect.arrayContaining(['jobs', 'job', 'company']));
|
|
14
|
-
});
|
|
15
|
-
it('should require inNotesApi credentials', () => {
|
|
16
|
-
var _a;
|
|
17
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
18
|
-
expect((_a = node.description.credentials) === null || _a === void 0 ? void 0 : _a.map((cred) => cred.name)).toContain('inNotesApi');
|
|
19
|
-
});
|
|
20
|
-
});
|
|
21
|
-
// The InNotes credential used to carry an authMethod field, and this node
|
|
22
|
-
// refused to run unless it read back exactly 'token'. 3b8c1e46 deleted
|
|
23
|
-
// username/password auth and that field with it, but only from the
|
|
24
|
-
// credential — so the guard was left reading a property nothing writes and
|
|
25
|
-
// the n8n UI cannot set. Every credential without the stale leftover value
|
|
26
|
-
// failed with "requires Bearer token authentication", which asked people to
|
|
27
|
-
// pick an option that no longer exists on the screen.
|
|
28
|
-
//
|
|
29
|
-
// These tests pass a credential shaped exactly like the current credential
|
|
30
|
-
// type: no authMethod anywhere.
|
|
31
|
-
describe('Credentials', () => {
|
|
32
|
-
const credentialWithoutAuthMethod = {
|
|
33
|
-
baseUrl: 'https://innotes.me',
|
|
34
|
-
token: 'test-token-123',
|
|
35
|
-
callbackSecret: 'whatever',
|
|
36
|
-
allowedDomains: '*',
|
|
37
|
-
};
|
|
38
|
-
const makeContext = (credentials, request) => ({
|
|
39
|
-
getInputData: () => [{ json: {} }],
|
|
40
|
-
getCredentials: jest.fn().mockResolvedValue(credentials),
|
|
41
|
-
getNode: () => ({ name: 'LinkedIn Scraper' }),
|
|
42
|
-
getNodeParameter: (name) => {
|
|
43
|
-
if (name === 'resource')
|
|
44
|
-
return 'company';
|
|
45
|
-
if (name === 'operation')
|
|
46
|
-
return 'followers';
|
|
47
|
-
if (name === 'companyUrl')
|
|
48
|
-
return 'https://www.linkedin.com/company/acme';
|
|
49
|
-
return undefined;
|
|
50
|
-
},
|
|
51
|
-
continueOnFail: () => false,
|
|
52
|
-
helpers: { request },
|
|
53
|
-
});
|
|
54
|
-
it('authenticates with a credential that has no authMethod', async () => {
|
|
55
|
-
const request = jest.fn().mockResolvedValue({
|
|
56
|
-
results: [
|
|
57
|
-
{ follower_count: 42, source: 'linkedin', success: true, url: 'https://www.linkedin.com/company/acme' },
|
|
58
|
-
],
|
|
59
|
-
});
|
|
60
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
61
|
-
await node.execute.call(makeContext(credentialWithoutAuthMethod, request));
|
|
62
|
-
expect(request).toHaveBeenCalledTimes(1);
|
|
63
|
-
expect(request.mock.calls[0][0].headers.Authorization).toBe('Bearer test-token-123');
|
|
64
|
-
});
|
|
65
|
-
it('does not ask the user to select a Bearer Token option that no longer exists', async () => {
|
|
66
|
-
const request = jest.fn().mockResolvedValue({ results: [] });
|
|
67
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
68
|
-
const failure = await node.execute
|
|
69
|
-
.call(makeContext(credentialWithoutAuthMethod, request))
|
|
70
|
-
.then(() => '', (error) => error.message);
|
|
71
|
-
expect(failure).not.toMatch(/requires Bearer token authentication/);
|
|
72
|
-
});
|
|
73
|
-
it('still refuses a credential with no token at all', async () => {
|
|
74
|
-
const request = jest.fn();
|
|
75
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
76
|
-
await expect(node.execute.call(makeContext({ baseUrl: 'https://innotes.me' }, request))).rejects.toThrow(/Bearer token is missing/);
|
|
77
|
-
expect(request).not.toHaveBeenCalled();
|
|
78
|
-
});
|
|
79
|
-
it('still refuses when the credential is missing entirely', async () => {
|
|
80
|
-
const request = jest.fn();
|
|
81
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
82
|
-
await expect(node.execute.call(makeContext(null, request))).rejects.toThrow(/credentials are missing/);
|
|
83
|
-
expect(request).not.toHaveBeenCalled();
|
|
84
|
-
});
|
|
85
|
-
});
|
|
86
|
-
describe('Company Followers', () => {
|
|
87
|
-
it('should have company resource with followers operation', () => {
|
|
88
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
89
|
-
const companyOps = node.description.properties
|
|
90
|
-
.filter((p) => { var _a, _b, _c; return p.name === 'operation' && ((_c = (_b = (_a = p.displayOptions) === null || _a === void 0 ? void 0 : _a.show) === null || _b === void 0 ? void 0 : _b.resource) === null || _c === void 0 ? void 0 : _c.includes('company')); })
|
|
91
|
-
.flatMap((p) => ('options' in p ? p.options : []))
|
|
92
|
-
.filter((o) => o !== undefined && 'value' in o)
|
|
93
|
-
.map((o) => o.value);
|
|
94
|
-
expect(companyOps).toContain('followers');
|
|
95
|
-
});
|
|
96
|
-
it('should require companyUrl parameter for followers operation', () => {
|
|
97
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
98
|
-
const companyUrlParam = node.description.properties.find((p) => p.name === 'companyUrl');
|
|
99
|
-
expect(companyUrlParam).toBeDefined();
|
|
100
|
-
expect(companyUrlParam === null || companyUrlParam === void 0 ? void 0 : companyUrlParam.required).toBe(true);
|
|
101
|
-
});
|
|
102
|
-
it('should have correct displayOptions for companyUrl', () => {
|
|
103
|
-
var _a, _b, _c, _d;
|
|
104
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
105
|
-
const companyUrlParam = node.description.properties.find((p) => p.name === 'companyUrl');
|
|
106
|
-
expect((_b = (_a = companyUrlParam === null || companyUrlParam === void 0 ? void 0 : companyUrlParam.displayOptions) === null || _a === void 0 ? void 0 : _a.show) === null || _b === void 0 ? void 0 : _b.resource).toContain('company');
|
|
107
|
-
expect((_d = (_c = companyUrlParam === null || companyUrlParam === void 0 ? void 0 : companyUrlParam.displayOptions) === null || _c === void 0 ? void 0 : _c.show) === null || _d === void 0 ? void 0 : _d.operation).toContain('followers');
|
|
108
|
-
});
|
|
109
|
-
});
|
|
110
|
-
describe('Job Details Batch Operations', () => {
|
|
111
|
-
it('should have job resource with details operation', () => {
|
|
112
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
113
|
-
const jobOps = node.description.properties
|
|
114
|
-
.filter((p) => { var _a, _b, _c; return p.name === 'operation' && ((_c = (_b = (_a = p.displayOptions) === null || _a === void 0 ? void 0 : _a.show) === null || _b === void 0 ? void 0 : _b.resource) === null || _c === void 0 ? void 0 : _c.includes('job')); })
|
|
115
|
-
.flatMap((p) => ('options' in p ? p.options : []))
|
|
116
|
-
.filter((o) => o !== undefined && 'value' in o)
|
|
117
|
-
.map((o) => o.value);
|
|
118
|
-
expect(jobOps).toContain('details');
|
|
119
|
-
});
|
|
120
|
-
it('should require jobUrl parameter for details operation', () => {
|
|
121
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
122
|
-
const jobUrlParam = node.description.properties.find((p) => p.name === 'jobUrl');
|
|
123
|
-
expect(jobUrlParam).toBeDefined();
|
|
124
|
-
expect(jobUrlParam === null || jobUrlParam === void 0 ? void 0 : jobUrlParam.required).toBe(true);
|
|
125
|
-
});
|
|
126
|
-
it('should have correct displayOptions for jobUrl', () => {
|
|
127
|
-
var _a, _b, _c, _d;
|
|
128
|
-
const node = new LinkedinScraper_node_1.LinkedinScraper();
|
|
129
|
-
const jobUrlParam = node.description.properties.find((p) => p.name === 'jobUrl');
|
|
130
|
-
expect((_b = (_a = jobUrlParam === null || jobUrlParam === void 0 ? void 0 : jobUrlParam.displayOptions) === null || _a === void 0 ? void 0 : _a.show) === null || _b === void 0 ? void 0 : _b.resource).toContain('job');
|
|
131
|
-
expect((_d = (_c = jobUrlParam === null || jobUrlParam === void 0 ? void 0 : jobUrlParam.displayOptions) === null || _c === void 0 ? void 0 : _c.show) === null || _d === void 0 ? void 0 : _d.operation).toContain('details');
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
describe('Batch Chunking Logic', () => {
|
|
135
|
-
describe('Job Details Batch Chunking', () => {
|
|
136
|
-
it('should chunk 30 job URLs into exactly 1 batch request', () => {
|
|
137
|
-
// Verify CHUNK_SIZE constant is 30
|
|
138
|
-
const CHUNK_SIZE = 30;
|
|
139
|
-
const urlCount = 30;
|
|
140
|
-
const expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);
|
|
141
|
-
expect(expectedBatches).toBe(1);
|
|
142
|
-
});
|
|
143
|
-
it('should chunk 60 job URLs into exactly 2 batch requests', () => {
|
|
144
|
-
const CHUNK_SIZE = 30;
|
|
145
|
-
const urlCount = 60;
|
|
146
|
-
const expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);
|
|
147
|
-
expect(expectedBatches).toBe(2);
|
|
148
|
-
});
|
|
149
|
-
it('should chunk 31 job URLs into exactly 2 batch requests', () => {
|
|
150
|
-
const CHUNK_SIZE = 30;
|
|
151
|
-
const urlCount = 31;
|
|
152
|
-
const expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);
|
|
153
|
-
expect(expectedBatches).toBe(2);
|
|
154
|
-
});
|
|
155
|
-
it('should chunk 90 job URLs into exactly 3 batch requests', () => {
|
|
156
|
-
const CHUNK_SIZE = 30;
|
|
157
|
-
const urlCount = 90;
|
|
158
|
-
const expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);
|
|
159
|
-
expect(expectedBatches).toBe(3);
|
|
160
|
-
});
|
|
161
|
-
});
|
|
162
|
-
describe('Company Followers Batch Chunking', () => {
|
|
163
|
-
it('should chunk 30 company URLs into exactly 1 batch request', () => {
|
|
164
|
-
const CHUNK_SIZE = 30;
|
|
165
|
-
const urlCount = 30;
|
|
166
|
-
const expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);
|
|
167
|
-
expect(expectedBatches).toBe(1);
|
|
168
|
-
});
|
|
169
|
-
it('should chunk 60 company URLs into exactly 2 batch requests', () => {
|
|
170
|
-
const CHUNK_SIZE = 30;
|
|
171
|
-
const urlCount = 60;
|
|
172
|
-
const expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);
|
|
173
|
-
expect(expectedBatches).toBe(2);
|
|
174
|
-
});
|
|
175
|
-
it('should verify CHUNK_SIZE constant is 30 for company followers', () => {
|
|
176
|
-
// This test documents the expected chunk size
|
|
177
|
-
const CHUNK_SIZE = 30;
|
|
178
|
-
expect(CHUNK_SIZE).toBe(30);
|
|
179
|
-
});
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
describe('Batch Request Configuration', () => {
|
|
183
|
-
describe('Job Details Batch Request', () => {
|
|
184
|
-
it('should send max_concurrent = 15 in job details batch request', () => {
|
|
185
|
-
const max_concurrent = 15;
|
|
186
|
-
expect(max_concurrent).toBe(15);
|
|
187
|
-
});
|
|
188
|
-
it('should send max_retries = 3 in job details batch request', () => {
|
|
189
|
-
const max_retries = 3;
|
|
190
|
-
expect(max_retries).toBe(3);
|
|
191
|
-
});
|
|
192
|
-
it('should use 10 minute timeout for job details batch', () => {
|
|
193
|
-
const timeout = 10 * 60 * 1000;
|
|
194
|
-
expect(timeout).toBe(600000);
|
|
195
|
-
});
|
|
196
|
-
it('should use POST method for job details batch endpoint', () => {
|
|
197
|
-
const method = 'POST';
|
|
198
|
-
expect(method).toBe('POST');
|
|
199
|
-
});
|
|
200
|
-
it('should target /jobs/details/batch endpoint', () => {
|
|
201
|
-
const endpoint = '/jobs/details/batch';
|
|
202
|
-
expect(endpoint).toContain('/jobs/details/batch');
|
|
203
|
-
});
|
|
204
|
-
});
|
|
205
|
-
describe('Company Followers Batch Request', () => {
|
|
206
|
-
it('should send max_concurrent = 15 in company followers batch request', () => {
|
|
207
|
-
const max_concurrent = 15;
|
|
208
|
-
expect(max_concurrent).toBe(15);
|
|
209
|
-
});
|
|
210
|
-
it('should send max_retries = 3 in company followers batch request', () => {
|
|
211
|
-
const max_retries = 3;
|
|
212
|
-
expect(max_retries).toBe(3);
|
|
213
|
-
});
|
|
214
|
-
it('should use 5 minute timeout for company followers batch', () => {
|
|
215
|
-
const timeout = 5 * 60 * 1000;
|
|
216
|
-
expect(timeout).toBe(300000);
|
|
217
|
-
});
|
|
218
|
-
it('should use POST method for company followers batch endpoint', () => {
|
|
219
|
-
const method = 'POST';
|
|
220
|
-
expect(method).toBe('POST');
|
|
221
|
-
});
|
|
222
|
-
it('should target /companies/followers/batch endpoint', () => {
|
|
223
|
-
const endpoint = '/companies/followers/batch';
|
|
224
|
-
expect(endpoint).toContain('/companies/followers/batch');
|
|
225
|
-
});
|
|
226
|
-
});
|
|
227
|
-
});
|
|
228
|
-
describe('Error Resilience', () => {
|
|
229
|
-
describe('Invalid URL Handling', () => {
|
|
230
|
-
it('should produce error object for invalid job URL (not starting with http)', () => {
|
|
231
|
-
const invalidUrl = 'not-a-url';
|
|
232
|
-
const isValid = invalidUrl === null || invalidUrl === void 0 ? void 0 : invalidUrl.startsWith('http');
|
|
233
|
-
expect(isValid).toBe(false);
|
|
234
|
-
// Invalid URLs should produce error objects
|
|
235
|
-
const errorResult = {
|
|
236
|
-
success: false,
|
|
237
|
-
error: `Invalid job URL: ${invalidUrl}`,
|
|
238
|
-
_originalIndex: 0,
|
|
239
|
-
};
|
|
240
|
-
expect(errorResult.success).toBe(false);
|
|
241
|
-
expect(errorResult.error).toContain('Invalid job URL');
|
|
242
|
-
});
|
|
243
|
-
it('should produce error object for empty job URL', () => {
|
|
244
|
-
const emptyUrl = '';
|
|
245
|
-
const isValid = emptyUrl === null || emptyUrl === void 0 ? void 0 : emptyUrl.startsWith('http');
|
|
246
|
-
expect(isValid).toBe(false);
|
|
247
|
-
const errorResult = {
|
|
248
|
-
success: false,
|
|
249
|
-
error: `Invalid job URL: ${emptyUrl || 'empty'}`,
|
|
250
|
-
_originalIndex: 0,
|
|
251
|
-
};
|
|
252
|
-
expect(errorResult.success).toBe(false);
|
|
253
|
-
expect(errorResult.error).toContain('Invalid job URL');
|
|
254
|
-
});
|
|
255
|
-
it('should produce error object for invalid company URL (not starting with http)', () => {
|
|
256
|
-
const invalidUrl = 'invalid-company-url';
|
|
257
|
-
const isValid = invalidUrl === null || invalidUrl === void 0 ? void 0 : invalidUrl.startsWith('http');
|
|
258
|
-
expect(isValid).toBe(false);
|
|
259
|
-
const errorResult = {
|
|
260
|
-
follower_count: 0,
|
|
261
|
-
source: 'skipped',
|
|
262
|
-
success: false,
|
|
263
|
-
error: `Invalid company URL: ${invalidUrl}`,
|
|
264
|
-
_originalIndex: 0,
|
|
265
|
-
};
|
|
266
|
-
expect(errorResult.success).toBe(false);
|
|
267
|
-
expect(errorResult.error).toContain('Invalid company URL');
|
|
268
|
-
});
|
|
269
|
-
it('should preserve original index for invalid URLs', () => {
|
|
270
|
-
const invalidUrl = 'not-a-url';
|
|
271
|
-
const originalIndex = 5;
|
|
272
|
-
const errorResult = {
|
|
273
|
-
success: false,
|
|
274
|
-
error: `Invalid job URL: ${invalidUrl}`,
|
|
275
|
-
_originalIndex: originalIndex,
|
|
276
|
-
};
|
|
277
|
-
expect(errorResult._originalIndex).toBe(5);
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
describe('Batch Failure Handling', () => {
|
|
281
|
-
it('should mark all URLs in chunk as failed when batch request fails', () => {
|
|
282
|
-
const chunkUrls = [
|
|
283
|
-
'https://linkedin.com/jobs/view/1',
|
|
284
|
-
'https://linkedin.com/jobs/view/2',
|
|
285
|
-
'https://linkedin.com/jobs/view/3',
|
|
286
|
-
];
|
|
287
|
-
const errorMessage = 'Network timeout';
|
|
288
|
-
// When batch fails, all URLs in chunk get error objects
|
|
289
|
-
const failedResults = chunkUrls.map((url, idx) => ({
|
|
290
|
-
success: false,
|
|
291
|
-
error: `Batch request failed: ${errorMessage}`,
|
|
292
|
-
url,
|
|
293
|
-
_originalIndex: idx,
|
|
294
|
-
}));
|
|
295
|
-
expect(failedResults).toHaveLength(3);
|
|
296
|
-
expect(failedResults.every((r) => r.success === false)).toBe(true);
|
|
297
|
-
expect(failedResults.every((r) => r.error.includes('Batch request failed'))).toBe(true);
|
|
298
|
-
});
|
|
299
|
-
it('should preserve URL in error result for batch failure', () => {
|
|
300
|
-
const url = 'https://linkedin.com/jobs/view/123';
|
|
301
|
-
const errorResult = {
|
|
302
|
-
success: false,
|
|
303
|
-
error: 'Batch request failed: Connection timeout',
|
|
304
|
-
url,
|
|
305
|
-
_originalIndex: 0,
|
|
306
|
-
};
|
|
307
|
-
expect(errorResult.url).toBe(url);
|
|
308
|
-
expect(errorResult.success).toBe(false);
|
|
309
|
-
});
|
|
310
|
-
});
|
|
311
|
-
describe('Result Order Preservation', () => {
|
|
312
|
-
it('should preserve original input order via _originalIndex for job details', () => {
|
|
313
|
-
const results = [
|
|
314
|
-
{ success: true, _originalIndex: 2 },
|
|
315
|
-
{ success: false, _originalIndex: 0 },
|
|
316
|
-
{ success: true, _originalIndex: 1 },
|
|
317
|
-
];
|
|
318
|
-
// Sort by original index to restore order
|
|
319
|
-
const sorted = [...results].sort((a, b) => a._originalIndex - b._originalIndex);
|
|
320
|
-
expect(sorted[0]._originalIndex).toBe(0);
|
|
321
|
-
expect(sorted[1]._originalIndex).toBe(1);
|
|
322
|
-
expect(sorted[2]._originalIndex).toBe(2);
|
|
323
|
-
});
|
|
324
|
-
it('should preserve original input order via _originalIndex for company followers', () => {
|
|
325
|
-
const results = [
|
|
326
|
-
{ follower_count: 1000, _originalIndex: 2 },
|
|
327
|
-
{ follower_count: 500, _originalIndex: 0 },
|
|
328
|
-
{ follower_count: 2000, _originalIndex: 1 },
|
|
329
|
-
];
|
|
330
|
-
const sorted = [...results].sort((a, b) => a._originalIndex - b._originalIndex);
|
|
331
|
-
expect(sorted[0]._originalIndex).toBe(0);
|
|
332
|
-
expect(sorted[1]._originalIndex).toBe(1);
|
|
333
|
-
expect(sorted[2]._originalIndex).toBe(2);
|
|
334
|
-
});
|
|
335
|
-
it('should remove _originalIndex from final output', () => {
|
|
336
|
-
const result = {
|
|
337
|
-
success: true,
|
|
338
|
-
job_title: 'Engineer',
|
|
339
|
-
_originalIndex: 0,
|
|
340
|
-
};
|
|
341
|
-
// Destructure to remove _originalIndex
|
|
342
|
-
const { _originalIndex: _, ...cleanResult } = result;
|
|
343
|
-
expect(cleanResult).not.toHaveProperty('_originalIndex');
|
|
344
|
-
expect(cleanResult).toHaveProperty('success');
|
|
345
|
-
expect(cleanResult).toHaveProperty('job_title');
|
|
346
|
-
});
|
|
347
|
-
});
|
|
348
|
-
describe('Mixed Valid and Invalid URLs', () => {
|
|
349
|
-
it('should handle mix of valid and invalid job URLs', () => {
|
|
350
|
-
const items = [
|
|
351
|
-
{ url: 'https://linkedin.com/jobs/view/1', valid: true },
|
|
352
|
-
{ url: 'invalid-url', valid: false },
|
|
353
|
-
{ url: 'https://linkedin.com/jobs/view/2', valid: true },
|
|
354
|
-
{ url: '', valid: false },
|
|
355
|
-
];
|
|
356
|
-
const validUrls = items.filter((item) => { var _a; return (_a = item.url) === null || _a === void 0 ? void 0 : _a.startsWith('http'); });
|
|
357
|
-
const invalidUrls = items.filter((item) => { var _a; return !((_a = item.url) === null || _a === void 0 ? void 0 : _a.startsWith('http')); });
|
|
358
|
-
expect(validUrls).toHaveLength(2);
|
|
359
|
-
expect(invalidUrls).toHaveLength(2);
|
|
360
|
-
});
|
|
361
|
-
it('should process valid URLs in batch while returning error objects for invalid ones', () => {
|
|
362
|
-
const validUrls = [
|
|
363
|
-
{ url: 'https://linkedin.com/jobs/view/1', index: 0 },
|
|
364
|
-
{ url: 'https://linkedin.com/jobs/view/2', index: 2 },
|
|
365
|
-
];
|
|
366
|
-
const invalidResults = [
|
|
367
|
-
{ success: false, error: 'Invalid URL', _originalIndex: 1 },
|
|
368
|
-
{ success: false, error: 'Invalid URL', _originalIndex: 3 },
|
|
369
|
-
];
|
|
370
|
-
// Combine results
|
|
371
|
-
const allResults = [...invalidResults];
|
|
372
|
-
expect(allResults).toHaveLength(2);
|
|
373
|
-
expect(validUrls).toHaveLength(2);
|
|
374
|
-
expect(allResults.every((r) => r.success === false)).toBe(true);
|
|
375
|
-
});
|
|
376
|
-
});
|
|
377
|
-
describe('Auth Error Handling', () => {
|
|
378
|
-
it('should re-throw 401 authentication errors', () => {
|
|
379
|
-
const statusCode = 401;
|
|
380
|
-
const shouldRethrow = statusCode === 401 || statusCode === 403;
|
|
381
|
-
expect(shouldRethrow).toBe(true);
|
|
382
|
-
});
|
|
383
|
-
it('should re-throw 403 forbidden errors', () => {
|
|
384
|
-
const statusCode = 403;
|
|
385
|
-
const shouldRethrow = statusCode === 401 || statusCode === 403;
|
|
386
|
-
expect(shouldRethrow).toBe(true);
|
|
387
|
-
});
|
|
388
|
-
it('should not re-throw other HTTP errors (e.g., 500)', () => {
|
|
389
|
-
const statusCode = 500;
|
|
390
|
-
const shouldRethrow = statusCode === 401 || statusCode === 403;
|
|
391
|
-
expect(shouldRethrow).toBe(false);
|
|
392
|
-
});
|
|
393
|
-
it('should detect 401 in error message string', () => {
|
|
394
|
-
const errorMessage = 'HTTP 401: Unauthorized';
|
|
395
|
-
const is401 = errorMessage.includes('401');
|
|
396
|
-
expect(is401).toBe(true);
|
|
397
|
-
});
|
|
398
|
-
it('should detect 403 in error message string', () => {
|
|
399
|
-
const errorMessage = 'HTTP 403: Forbidden';
|
|
400
|
-
const is403 = errorMessage.includes('403');
|
|
401
|
-
expect(is403).toBe(true);
|
|
402
|
-
});
|
|
403
|
-
});
|
|
404
|
-
});
|
|
405
|
-
describe('Search Jobs Deduplication', () => {
|
|
406
|
-
it('should remove duplicate jobs with the same link, keeping first occurrence', () => {
|
|
407
|
-
const jobs = [
|
|
408
|
-
{ link: 'https://www.linkedin.com/jobs/view/111/', title: 'Engineer' },
|
|
409
|
-
{ link: 'https://www.linkedin.com/jobs/view/222/', title: 'Designer' },
|
|
410
|
-
{ link: 'https://www.linkedin.com/jobs/view/111/', title: 'Engineer (duplicate)' },
|
|
411
|
-
{ link: 'https://www.linkedin.com/jobs/view/333/', title: 'Manager' },
|
|
412
|
-
{ link: 'https://www.linkedin.com/jobs/view/222/', title: 'Designer (duplicate)' },
|
|
413
|
-
];
|
|
414
|
-
const seenLinks = new Set();
|
|
415
|
-
const uniqueJobs = [];
|
|
416
|
-
for (const job of jobs) {
|
|
417
|
-
const link = job.link || '';
|
|
418
|
-
if (link && seenLinks.has(link)) {
|
|
419
|
-
continue;
|
|
420
|
-
}
|
|
421
|
-
if (link) {
|
|
422
|
-
seenLinks.add(link);
|
|
423
|
-
}
|
|
424
|
-
uniqueJobs.push(job);
|
|
425
|
-
}
|
|
426
|
-
expect(uniqueJobs).toHaveLength(3);
|
|
427
|
-
expect(uniqueJobs[0].title).toBe('Engineer');
|
|
428
|
-
expect(uniqueJobs[1].title).toBe('Designer');
|
|
429
|
-
expect(uniqueJobs[2].title).toBe('Manager');
|
|
430
|
-
});
|
|
431
|
-
it('should keep jobs without a link field', () => {
|
|
432
|
-
const jobs = [
|
|
433
|
-
{ link: 'https://www.linkedin.com/jobs/view/111/', title: 'With Link' },
|
|
434
|
-
{ title: 'No Link' },
|
|
435
|
-
{ link: '', title: 'Empty Link' },
|
|
436
|
-
];
|
|
437
|
-
const seenLinks = new Set();
|
|
438
|
-
const uniqueJobs = [];
|
|
439
|
-
for (const job of jobs) {
|
|
440
|
-
const link = job.link || '';
|
|
441
|
-
if (link && seenLinks.has(link)) {
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
if (link) {
|
|
445
|
-
seenLinks.add(link);
|
|
446
|
-
}
|
|
447
|
-
uniqueJobs.push(job);
|
|
448
|
-
}
|
|
449
|
-
expect(uniqueJobs).toHaveLength(3);
|
|
450
|
-
});
|
|
451
|
-
});
|
|
452
|
-
describe('Batch Response Mapping', () => {
|
|
453
|
-
describe('Job Details Result Mapping', () => {
|
|
454
|
-
it('should map batch response results with all required fields', () => {
|
|
455
|
-
const batchResponse = {
|
|
456
|
-
results: [
|
|
457
|
-
{
|
|
458
|
-
url: 'https://linkedin.com/jobs/view/1',
|
|
459
|
-
job_title: 'Senior Engineer',
|
|
460
|
-
company: 'Google',
|
|
461
|
-
company_url: 'https://linkedin.com/company/google',
|
|
462
|
-
picture_url: 'https://example.com/pic.jpg',
|
|
463
|
-
location: 'Mountain View, CA',
|
|
464
|
-
job_description: 'We are hiring...',
|
|
465
|
-
success: true,
|
|
466
|
-
},
|
|
467
|
-
],
|
|
468
|
-
};
|
|
469
|
-
const result = batchResponse.results[0];
|
|
470
|
-
expect(result).toHaveProperty('url');
|
|
471
|
-
expect(result).toHaveProperty('job_title');
|
|
472
|
-
expect(result).toHaveProperty('company');
|
|
473
|
-
expect(result).toHaveProperty('company_url');
|
|
474
|
-
expect(result).toHaveProperty('picture_url');
|
|
475
|
-
expect(result).toHaveProperty('location');
|
|
476
|
-
expect(result).toHaveProperty('job_description');
|
|
477
|
-
expect(result).toHaveProperty('success');
|
|
478
|
-
});
|
|
479
|
-
it('should preserve original index during result mapping', () => {
|
|
480
|
-
const chunk = [
|
|
481
|
-
{ url: 'https://linkedin.com/jobs/view/1', index: 0 },
|
|
482
|
-
{ url: 'https://linkedin.com/jobs/view/2', index: 1 },
|
|
483
|
-
];
|
|
484
|
-
const batchResults = [
|
|
485
|
-
{ url: 'https://linkedin.com/jobs/view/1', job_title: 'Job 1', success: true },
|
|
486
|
-
{ url: 'https://linkedin.com/jobs/view/2', job_title: 'Job 2', success: true },
|
|
487
|
-
];
|
|
488
|
-
// Map results back with original indices
|
|
489
|
-
const mappedResults = batchResults.map((result, j) => ({
|
|
490
|
-
...result,
|
|
491
|
-
_originalIndex: chunk[j].index,
|
|
492
|
-
}));
|
|
493
|
-
expect(mappedResults[0]._originalIndex).toBe(0);
|
|
494
|
-
expect(mappedResults[1]._originalIndex).toBe(1);
|
|
495
|
-
});
|
|
496
|
-
});
|
|
497
|
-
describe('Company Followers Result Mapping', () => {
|
|
498
|
-
it('should map batch response results with follower_count and source', () => {
|
|
499
|
-
const batchResponse = {
|
|
500
|
-
results: [
|
|
501
|
-
{
|
|
502
|
-
url: 'https://linkedin.com/company/google',
|
|
503
|
-
follower_count: 1000000,
|
|
504
|
-
source: 'linkedin',
|
|
505
|
-
success: true,
|
|
506
|
-
},
|
|
507
|
-
],
|
|
508
|
-
};
|
|
509
|
-
const result = batchResponse.results[0];
|
|
510
|
-
expect(result).toHaveProperty('url');
|
|
511
|
-
expect(result).toHaveProperty('follower_count');
|
|
512
|
-
expect(result).toHaveProperty('source');
|
|
513
|
-
expect(result).toHaveProperty('success');
|
|
514
|
-
});
|
|
515
|
-
it('should handle cached results with source: cache', () => {
|
|
516
|
-
const cachedResult = {
|
|
517
|
-
url: 'https://linkedin.com/company/google',
|
|
518
|
-
follower_count: 1000000,
|
|
519
|
-
source: 'cache',
|
|
520
|
-
success: true,
|
|
521
|
-
};
|
|
522
|
-
expect(cachedResult.source).toBe('cache');
|
|
523
|
-
expect(cachedResult.success).toBe(true);
|
|
524
|
-
});
|
|
525
|
-
it('should handle error results with source: error', () => {
|
|
526
|
-
const errorResult = {
|
|
527
|
-
url: 'https://linkedin.com/company/invalid',
|
|
528
|
-
follower_count: 0,
|
|
529
|
-
source: 'error',
|
|
530
|
-
success: false,
|
|
531
|
-
error: 'Company not found',
|
|
532
|
-
};
|
|
533
|
-
expect(errorResult.source).toBe('error');
|
|
534
|
-
expect(errorResult.success).toBe(false);
|
|
535
|
-
expect(errorResult.error).toBeDefined();
|
|
536
|
-
});
|
|
537
|
-
});
|
|
538
|
-
});
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
//# sourceMappingURL=LinkedinScraper.node.test.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../test/LinkedinScraper.node.test.ts"],"names":[],"mappings":"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","file":"LinkedinScraper.node.test.js","sourcesContent":["import { LinkedinScraper } from '../nodes/LinkedinScraper/LinkedinScraper.node';\nimport { INodePropertyOptions } from 'n8n-workflow';\n\ndescribe('LinkedinScraper Node', () => {\n\tdescribe('Node Properties', () => {\n\t\tit('should expose jobs, job, and company resources', () => {\n\t\t\tconst node = new LinkedinScraper();\n\t\t\tconst resources = node.description.properties\n\t\t\t\t.filter((property) => property.name === 'resource')\n\t\t\t\t.flatMap((property) => ('options' in property ? property.options : []))\n\t\t\t\t.filter((option): option is INodePropertyOptions => option !== undefined && 'value' in option)\n\t\t\t\t.map((option) => option.value);\n\n\t\t\texpect(resources).toEqual(expect.arrayContaining(['jobs', 'job', 'company']));\n\t\t});\n\n\t\tit('should require inNotesApi credentials', () => {\n\t\t\tconst node = new LinkedinScraper();\n\t\t\texpect(node.description.credentials?.map((cred) => cred.name)).toContain('inNotesApi');\n\t\t});\n\t});\n\n\t// The InNotes credential used to carry an authMethod field, and this node\n\t// refused to run unless it read back exactly 'token'. 3b8c1e46 deleted\n\t// username/password auth and that field with it, but only from the\n\t// credential — so the guard was left reading a property nothing writes and\n\t// the n8n UI cannot set. Every credential without the stale leftover value\n\t// failed with \"requires Bearer token authentication\", which asked people to\n\t// pick an option that no longer exists on the screen.\n\t//\n\t// These tests pass a credential shaped exactly like the current credential\n\t// type: no authMethod anywhere.\n\tdescribe('Credentials', () => {\n\t\tconst credentialWithoutAuthMethod = {\n\t\t\tbaseUrl: 'https://innotes.me',\n\t\t\ttoken: 'test-token-123',\n\t\t\tcallbackSecret: 'whatever',\n\t\t\tallowedDomains: '*',\n\t\t};\n\n\t\tconst makeContext = (credentials: unknown, request: jest.Mock) => ({\n\t\t\tgetInputData: () => [{ json: {} }],\n\t\t\tgetCredentials: jest.fn().mockResolvedValue(credentials),\n\t\t\tgetNode: () => ({ name: 'LinkedIn Scraper' }),\n\t\t\tgetNodeParameter: (name: string) => {\n\t\t\t\tif (name === 'resource') return 'company';\n\t\t\t\tif (name === 'operation') return 'followers';\n\t\t\t\tif (name === 'companyUrl') return 'https://www.linkedin.com/company/acme';\n\t\t\t\treturn undefined;\n\t\t\t},\n\t\t\tcontinueOnFail: () => false,\n\t\t\thelpers: { request },\n\t\t});\n\n\t\tit('authenticates with a credential that has no authMethod', async () => {\n\t\t\tconst request = jest.fn().mockResolvedValue({\n\t\t\t\tresults: [\n\t\t\t\t\t{ follower_count: 42, source: 'linkedin', success: true, url: 'https://www.linkedin.com/company/acme' },\n\t\t\t\t],\n\t\t\t});\n\n\t\t\tconst node = new LinkedinScraper();\n\t\t\tawait node.execute.call(makeContext(credentialWithoutAuthMethod, request) as any);\n\n\t\t\texpect(request).toHaveBeenCalledTimes(1);\n\t\t\texpect(request.mock.calls[0][0].headers.Authorization).toBe('Bearer test-token-123');\n\t\t});\n\n\t\tit('does not ask the user to select a Bearer Token option that no longer exists', async () => {\n\t\t\tconst request = jest.fn().mockResolvedValue({ results: [] });\n\t\t\tconst node = new LinkedinScraper();\n\n\t\t\tconst failure = await node.execute\n\t\t\t\t.call(makeContext(credentialWithoutAuthMethod, request) as any)\n\t\t\t\t.then(\n\t\t\t\t\t() => '',\n\t\t\t\t\t(error: Error) => error.message,\n\t\t\t\t);\n\n\t\t\texpect(failure).not.toMatch(/requires Bearer token authentication/);\n\t\t});\n\n\t\tit('still refuses a credential with no token at all', async () => {\n\t\t\tconst request = jest.fn();\n\t\t\tconst node = new LinkedinScraper();\n\n\t\t\tawait expect(\n\t\t\t\tnode.execute.call(makeContext({ baseUrl: 'https://innotes.me' }, request) as any),\n\t\t\t).rejects.toThrow(/Bearer token is missing/);\n\t\t\texpect(request).not.toHaveBeenCalled();\n\t\t});\n\n\t\tit('still refuses when the credential is missing entirely', async () => {\n\t\t\tconst request = jest.fn();\n\t\t\tconst node = new LinkedinScraper();\n\n\t\t\tawait expect(\n\t\t\t\tnode.execute.call(makeContext(null, request) as any),\n\t\t\t).rejects.toThrow(/credentials are missing/);\n\t\t\texpect(request).not.toHaveBeenCalled();\n\t\t});\n\t});\n\n\tdescribe('Company Followers', () => {\n\t\tit('should have company resource with followers operation', () => {\n\t\t\tconst node = new LinkedinScraper();\n\t\t\tconst companyOps = node.description.properties\n\t\t\t\t.filter(\n\t\t\t\t\t(p) =>\n\t\t\t\t\t\tp.name === 'operation' && p.displayOptions?.show?.resource?.includes('company'),\n\t\t\t\t)\n\t\t\t\t.flatMap((p) => ('options' in p ? p.options : []))\n\t\t\t\t.filter((o): o is INodePropertyOptions => o !== undefined && 'value' in o)\n\t\t\t\t.map((o) => o.value);\n\n\t\t\texpect(companyOps).toContain('followers');\n\t\t});\n\n\t\tit('should require companyUrl parameter for followers operation', () => {\n\t\t\tconst node = new LinkedinScraper();\n\t\t\tconst companyUrlParam = node.description.properties.find((p) => p.name === 'companyUrl');\n\n\t\t\texpect(companyUrlParam).toBeDefined();\n\t\t\texpect(companyUrlParam?.required).toBe(true);\n\t\t});\n\n\t\tit('should have correct displayOptions for companyUrl', () => {\n\t\t\tconst node = new LinkedinScraper();\n\t\t\tconst companyUrlParam = node.description.properties.find((p) => p.name === 'companyUrl');\n\n\t\t\texpect(companyUrlParam?.displayOptions?.show?.resource).toContain('company');\n\t\t\texpect(companyUrlParam?.displayOptions?.show?.operation).toContain('followers');\n\t\t});\n\t});\n\n\tdescribe('Job Details Batch Operations', () => {\n\t\tit('should have job resource with details operation', () => {\n\t\t\tconst node = new LinkedinScraper();\n\t\t\tconst jobOps = node.description.properties\n\t\t\t\t.filter(\n\t\t\t\t\t(p) =>\n\t\t\t\t\t\tp.name === 'operation' && p.displayOptions?.show?.resource?.includes('job'),\n\t\t\t\t)\n\t\t\t\t.flatMap((p) => ('options' in p ? p.options : []))\n\t\t\t\t.filter((o): o is INodePropertyOptions => o !== undefined && 'value' in o)\n\t\t\t\t.map((o) => o.value);\n\n\t\t\texpect(jobOps).toContain('details');\n\t\t});\n\n\t\tit('should require jobUrl parameter for details operation', () => {\n\t\t\tconst node = new LinkedinScraper();\n\t\t\tconst jobUrlParam = node.description.properties.find((p) => p.name === 'jobUrl');\n\n\t\t\texpect(jobUrlParam).toBeDefined();\n\t\t\texpect(jobUrlParam?.required).toBe(true);\n\t\t});\n\n\t\tit('should have correct displayOptions for jobUrl', () => {\n\t\t\tconst node = new LinkedinScraper();\n\t\t\tconst jobUrlParam = node.description.properties.find((p) => p.name === 'jobUrl');\n\n\t\t\texpect(jobUrlParam?.displayOptions?.show?.resource).toContain('job');\n\t\t\texpect(jobUrlParam?.displayOptions?.show?.operation).toContain('details');\n\t\t});\n\t});\n\n\tdescribe('Batch Chunking Logic', () => {\n\t\tdescribe('Job Details Batch Chunking', () => {\n\t\t\tit('should chunk 30 job URLs into exactly 1 batch request', () => {\n\t\t\t\t// Verify CHUNK_SIZE constant is 30\n\t\t\t\tconst CHUNK_SIZE = 30;\n\t\t\t\tconst urlCount = 30;\n\t\t\t\tconst expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);\n\t\t\t\texpect(expectedBatches).toBe(1);\n\t\t\t});\n\n\t\t\tit('should chunk 60 job URLs into exactly 2 batch requests', () => {\n\t\t\t\tconst CHUNK_SIZE = 30;\n\t\t\t\tconst urlCount = 60;\n\t\t\t\tconst expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);\n\t\t\t\texpect(expectedBatches).toBe(2);\n\t\t\t});\n\n\t\t\tit('should chunk 31 job URLs into exactly 2 batch requests', () => {\n\t\t\t\tconst CHUNK_SIZE = 30;\n\t\t\t\tconst urlCount = 31;\n\t\t\t\tconst expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);\n\t\t\t\texpect(expectedBatches).toBe(2);\n\t\t\t});\n\n\t\t\tit('should chunk 90 job URLs into exactly 3 batch requests', () => {\n\t\t\t\tconst CHUNK_SIZE = 30;\n\t\t\t\tconst urlCount = 90;\n\t\t\t\tconst expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);\n\t\t\t\texpect(expectedBatches).toBe(3);\n\t\t\t});\n\t\t});\n\n\t\tdescribe('Company Followers Batch Chunking', () => {\n\t\t\tit('should chunk 30 company URLs into exactly 1 batch request', () => {\n\t\t\t\tconst CHUNK_SIZE = 30;\n\t\t\t\tconst urlCount = 30;\n\t\t\t\tconst expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);\n\t\t\t\texpect(expectedBatches).toBe(1);\n\t\t\t});\n\n\t\t\tit('should chunk 60 company URLs into exactly 2 batch requests', () => {\n\t\t\t\tconst CHUNK_SIZE = 30;\n\t\t\t\tconst urlCount = 60;\n\t\t\t\tconst expectedBatches = Math.ceil(urlCount / CHUNK_SIZE);\n\t\t\t\texpect(expectedBatches).toBe(2);\n\t\t\t});\n\n\t\t\tit('should verify CHUNK_SIZE constant is 30 for company followers', () => {\n\t\t\t\t// This test documents the expected chunk size\n\t\t\t\tconst CHUNK_SIZE = 30;\n\t\t\t\texpect(CHUNK_SIZE).toBe(30);\n\t\t\t});\n\t\t});\n\t});\n\n\tdescribe('Batch Request Configuration', () => {\n\t\tdescribe('Job Details Batch Request', () => {\n\t\t\tit('should send max_concurrent = 15 in job details batch request', () => {\n\t\t\t\tconst max_concurrent = 15;\n\t\t\t\texpect(max_concurrent).toBe(15);\n\t\t\t});\n\n\t\t\tit('should send max_retries = 3 in job details batch request', () => {\n\t\t\t\tconst max_retries = 3;\n\t\t\t\texpect(max_retries).toBe(3);\n\t\t\t});\n\n\t\t\tit('should use 10 minute timeout for job details batch', () => {\n\t\t\t\tconst timeout = 10 * 60 * 1000;\n\t\t\t\texpect(timeout).toBe(600000);\n\t\t\t});\n\n\t\t\tit('should use POST method for job details batch endpoint', () => {\n\t\t\t\tconst method = 'POST';\n\t\t\t\texpect(method).toBe('POST');\n\t\t\t});\n\n\t\t\tit('should target /jobs/details/batch endpoint', () => {\n\t\t\t\tconst endpoint = '/jobs/details/batch';\n\t\t\t\texpect(endpoint).toContain('/jobs/details/batch');\n\t\t\t});\n\t\t});\n\n\t\tdescribe('Company Followers Batch Request', () => {\n\t\t\tit('should send max_concurrent = 15 in company followers batch request', () => {\n\t\t\t\tconst max_concurrent = 15;\n\t\t\t\texpect(max_concurrent).toBe(15);\n\t\t\t});\n\n\t\t\tit('should send max_retries = 3 in company followers batch request', () => {\n\t\t\t\tconst max_retries = 3;\n\t\t\t\texpect(max_retries).toBe(3);\n\t\t\t});\n\n\t\t\tit('should use 5 minute timeout for company followers batch', () => {\n\t\t\t\tconst timeout = 5 * 60 * 1000;\n\t\t\t\texpect(timeout).toBe(300000);\n\t\t\t});\n\n\t\t\tit('should use POST method for company followers batch endpoint', () => {\n\t\t\t\tconst method = 'POST';\n\t\t\t\texpect(method).toBe('POST');\n\t\t\t});\n\n\t\t\tit('should target /companies/followers/batch endpoint', () => {\n\t\t\t\tconst endpoint = '/companies/followers/batch';\n\t\t\t\texpect(endpoint).toContain('/companies/followers/batch');\n\t\t\t});\n\t\t});\n\t});\n\n\tdescribe('Error Resilience', () => {\n\t\tdescribe('Invalid URL Handling', () => {\n\t\t\tit('should produce error object for invalid job URL (not starting with http)', () => {\n\t\t\t\tconst invalidUrl = 'not-a-url';\n\t\t\t\tconst isValid = invalidUrl?.startsWith('http');\n\t\t\t\texpect(isValid).toBe(false);\n\n\t\t\t\t// Invalid URLs should produce error objects\n\t\t\t\tconst errorResult = {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: `Invalid job URL: ${invalidUrl}`,\n\t\t\t\t\t_originalIndex: 0,\n\t\t\t\t};\n\t\t\t\texpect(errorResult.success).toBe(false);\n\t\t\t\texpect(errorResult.error).toContain('Invalid job URL');\n\t\t\t});\n\n\t\t\tit('should produce error object for empty job URL', () => {\n\t\t\t\tconst emptyUrl = '';\n\t\t\t\tconst isValid = emptyUrl?.startsWith('http');\n\t\t\t\texpect(isValid).toBe(false);\n\n\t\t\t\tconst errorResult = {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: `Invalid job URL: ${emptyUrl || 'empty'}`,\n\t\t\t\t\t_originalIndex: 0,\n\t\t\t\t};\n\t\t\t\texpect(errorResult.success).toBe(false);\n\t\t\t\texpect(errorResult.error).toContain('Invalid job URL');\n\t\t\t});\n\n\t\t\tit('should produce error object for invalid company URL (not starting with http)', () => {\n\t\t\t\tconst invalidUrl = 'invalid-company-url';\n\t\t\t\tconst isValid = invalidUrl?.startsWith('http');\n\t\t\t\texpect(isValid).toBe(false);\n\n\t\t\t\tconst errorResult = {\n\t\t\t\t\tfollower_count: 0,\n\t\t\t\t\tsource: 'skipped',\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: `Invalid company URL: ${invalidUrl}`,\n\t\t\t\t\t_originalIndex: 0,\n\t\t\t\t};\n\t\t\t\texpect(errorResult.success).toBe(false);\n\t\t\t\texpect(errorResult.error).toContain('Invalid company URL');\n\t\t\t});\n\n\t\t\tit('should preserve original index for invalid URLs', () => {\n\t\t\t\tconst invalidUrl = 'not-a-url';\n\t\t\t\tconst originalIndex = 5;\n\n\t\t\t\tconst errorResult = {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: `Invalid job URL: ${invalidUrl}`,\n\t\t\t\t\t_originalIndex: originalIndex,\n\t\t\t\t};\n\t\t\t\texpect(errorResult._originalIndex).toBe(5);\n\t\t\t});\n\t\t});\n\n\t\tdescribe('Batch Failure Handling', () => {\n\t\t\tit('should mark all URLs in chunk as failed when batch request fails', () => {\n\t\t\t\tconst chunkUrls = [\n\t\t\t\t\t'https://linkedin.com/jobs/view/1',\n\t\t\t\t\t'https://linkedin.com/jobs/view/2',\n\t\t\t\t\t'https://linkedin.com/jobs/view/3',\n\t\t\t\t];\n\t\t\t\tconst errorMessage = 'Network timeout';\n\n\t\t\t\t// When batch fails, all URLs in chunk get error objects\n\t\t\t\tconst failedResults = chunkUrls.map((url, idx) => ({\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: `Batch request failed: ${errorMessage}`,\n\t\t\t\t\turl,\n\t\t\t\t\t_originalIndex: idx,\n\t\t\t\t}));\n\n\t\t\t\texpect(failedResults).toHaveLength(3);\n\t\t\t\texpect(failedResults.every((r) => r.success === false)).toBe(true);\n\t\t\t\texpect(failedResults.every((r) => r.error.includes('Batch request failed'))).toBe(true);\n\t\t\t});\n\n\t\t\tit('should preserve URL in error result for batch failure', () => {\n\t\t\t\tconst url = 'https://linkedin.com/jobs/view/123';\n\t\t\t\tconst errorResult = {\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: 'Batch request failed: Connection timeout',\n\t\t\t\t\turl,\n\t\t\t\t\t_originalIndex: 0,\n\t\t\t\t};\n\n\t\t\t\texpect(errorResult.url).toBe(url);\n\t\t\t\texpect(errorResult.success).toBe(false);\n\t\t\t});\n\t\t});\n\n\t\tdescribe('Result Order Preservation', () => {\n\t\t\tit('should preserve original input order via _originalIndex for job details', () => {\n\t\t\t\tconst results = [\n\t\t\t\t\t{ success: true, _originalIndex: 2 },\n\t\t\t\t\t{ success: false, _originalIndex: 0 },\n\t\t\t\t\t{ success: true, _originalIndex: 1 },\n\t\t\t\t];\n\n\t\t\t\t// Sort by original index to restore order\n\t\t\t\tconst sorted = [...results].sort((a, b) => a._originalIndex - b._originalIndex);\n\n\t\t\t\texpect(sorted[0]._originalIndex).toBe(0);\n\t\t\t\texpect(sorted[1]._originalIndex).toBe(1);\n\t\t\t\texpect(sorted[2]._originalIndex).toBe(2);\n\t\t\t});\n\n\t\t\tit('should preserve original input order via _originalIndex for company followers', () => {\n\t\t\t\tconst results = [\n\t\t\t\t\t{ follower_count: 1000, _originalIndex: 2 },\n\t\t\t\t\t{ follower_count: 500, _originalIndex: 0 },\n\t\t\t\t\t{ follower_count: 2000, _originalIndex: 1 },\n\t\t\t\t];\n\n\t\t\t\tconst sorted = [...results].sort((a, b) => a._originalIndex - b._originalIndex);\n\n\t\t\t\texpect(sorted[0]._originalIndex).toBe(0);\n\t\t\t\texpect(sorted[1]._originalIndex).toBe(1);\n\t\t\t\texpect(sorted[2]._originalIndex).toBe(2);\n\t\t\t});\n\n\t\t\tit('should remove _originalIndex from final output', () => {\n\t\t\t\tconst result = {\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tjob_title: 'Engineer',\n\t\t\t\t\t_originalIndex: 0,\n\t\t\t\t};\n\n\t\t\t\t// Destructure to remove _originalIndex\n\t\t\t\tconst { _originalIndex: _, ...cleanResult } = result;\n\n\t\t\t\texpect(cleanResult).not.toHaveProperty('_originalIndex');\n\t\t\t\texpect(cleanResult).toHaveProperty('success');\n\t\t\t\texpect(cleanResult).toHaveProperty('job_title');\n\t\t\t});\n\t\t});\n\n\t\tdescribe('Mixed Valid and Invalid URLs', () => {\n\t\t\tit('should handle mix of valid and invalid job URLs', () => {\n\t\t\t\tconst items = [\n\t\t\t\t\t{ url: 'https://linkedin.com/jobs/view/1', valid: true },\n\t\t\t\t\t{ url: 'invalid-url', valid: false },\n\t\t\t\t\t{ url: 'https://linkedin.com/jobs/view/2', valid: true },\n\t\t\t\t\t{ url: '', valid: false },\n\t\t\t\t];\n\n\t\t\t\tconst validUrls = items.filter((item) => item.url?.startsWith('http'));\n\t\t\t\tconst invalidUrls = items.filter((item) => !item.url?.startsWith('http'));\n\n\t\t\t\texpect(validUrls).toHaveLength(2);\n\t\t\t\texpect(invalidUrls).toHaveLength(2);\n\t\t\t});\n\n\t\t\tit('should process valid URLs in batch while returning error objects for invalid ones', () => {\n\t\t\t\tconst validUrls = [\n\t\t\t\t\t{ url: 'https://linkedin.com/jobs/view/1', index: 0 },\n\t\t\t\t\t{ url: 'https://linkedin.com/jobs/view/2', index: 2 },\n\t\t\t\t];\n\t\t\t\tconst invalidResults = [\n\t\t\t\t\t{ success: false, error: 'Invalid URL', _originalIndex: 1 },\n\t\t\t\t\t{ success: false, error: 'Invalid URL', _originalIndex: 3 },\n\t\t\t\t];\n\n\t\t\t\t// Combine results\n\t\t\t\tconst allResults = [...invalidResults];\n\t\t\t\texpect(allResults).toHaveLength(2);\n\t\t\t\texpect(validUrls).toHaveLength(2);\n\t\t\t\texpect(allResults.every((r) => r.success === false)).toBe(true);\n\t\t\t});\n\t\t});\n\n\t\tdescribe('Auth Error Handling', () => {\n\t\t\tit('should re-throw 401 authentication errors', () => {\n\t\t\t\tconst statusCode: number = 401;\n\t\t\t\tconst shouldRethrow = statusCode === 401 || statusCode === 403;\n\t\t\t\texpect(shouldRethrow).toBe(true);\n\t\t\t});\n\n\t\t\tit('should re-throw 403 forbidden errors', () => {\n\t\t\t\tconst statusCode: number = 403;\n\t\t\t\tconst shouldRethrow = statusCode === 401 || statusCode === 403;\n\t\t\t\texpect(shouldRethrow).toBe(true);\n\t\t\t});\n\n\t\t\tit('should not re-throw other HTTP errors (e.g., 500)', () => {\n\t\t\t\tconst statusCode: number = 500;\n\t\t\t\tconst shouldRethrow = statusCode === 401 || statusCode === 403;\n\t\t\t\texpect(shouldRethrow).toBe(false);\n\t\t\t});\n\n\t\t\tit('should detect 401 in error message string', () => {\n\t\t\t\tconst errorMessage = 'HTTP 401: Unauthorized';\n\t\t\t\tconst is401 = errorMessage.includes('401');\n\t\t\t\texpect(is401).toBe(true);\n\t\t\t});\n\n\t\t\tit('should detect 403 in error message string', () => {\n\t\t\t\tconst errorMessage = 'HTTP 403: Forbidden';\n\t\t\t\tconst is403 = errorMessage.includes('403');\n\t\t\t\texpect(is403).toBe(true);\n\t\t\t});\n\t\t});\n\t});\n\n\tdescribe('Search Jobs Deduplication', () => {\n\t\tit('should remove duplicate jobs with the same link, keeping first occurrence', () => {\n\t\t\tconst jobs = [\n\t\t\t\t{ link: 'https://www.linkedin.com/jobs/view/111/', title: 'Engineer' },\n\t\t\t\t{ link: 'https://www.linkedin.com/jobs/view/222/', title: 'Designer' },\n\t\t\t\t{ link: 'https://www.linkedin.com/jobs/view/111/', title: 'Engineer (duplicate)' },\n\t\t\t\t{ link: 'https://www.linkedin.com/jobs/view/333/', title: 'Manager' },\n\t\t\t\t{ link: 'https://www.linkedin.com/jobs/view/222/', title: 'Designer (duplicate)' },\n\t\t\t];\n\n\t\t\tconst seenLinks = new Set<string>();\n\t\t\tconst uniqueJobs: Record<string, unknown>[] = [];\n\t\t\tfor (const job of jobs) {\n\t\t\t\tconst link = (job.link as string) || '';\n\t\t\t\tif (link && seenLinks.has(link)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (link) {\n\t\t\t\t\tseenLinks.add(link);\n\t\t\t\t}\n\t\t\t\tuniqueJobs.push(job);\n\t\t\t}\n\n\t\t\texpect(uniqueJobs).toHaveLength(3);\n\t\t\texpect(uniqueJobs[0].title).toBe('Engineer');\n\t\t\texpect(uniqueJobs[1].title).toBe('Designer');\n\t\t\texpect(uniqueJobs[2].title).toBe('Manager');\n\t\t});\n\n\t\tit('should keep jobs without a link field', () => {\n\t\t\tconst jobs = [\n\t\t\t\t{ link: 'https://www.linkedin.com/jobs/view/111/', title: 'With Link' },\n\t\t\t\t{ title: 'No Link' },\n\t\t\t\t{ link: '', title: 'Empty Link' },\n\t\t\t];\n\n\t\t\tconst seenLinks = new Set<string>();\n\t\t\tconst uniqueJobs: Record<string, unknown>[] = [];\n\t\t\tfor (const job of jobs) {\n\t\t\t\tconst link = ((job as Record<string, unknown>).link as string) || '';\n\t\t\t\tif (link && seenLinks.has(link)) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif (link) {\n\t\t\t\t\tseenLinks.add(link);\n\t\t\t\t}\n\t\t\t\tuniqueJobs.push(job);\n\t\t\t}\n\n\t\t\texpect(uniqueJobs).toHaveLength(3);\n\t\t});\n\t});\n\n\tdescribe('Batch Response Mapping', () => {\n\t\tdescribe('Job Details Result Mapping', () => {\n\t\t\tit('should map batch response results with all required fields', () => {\n\t\t\t\tconst batchResponse = {\n\t\t\t\t\tresults: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\turl: 'https://linkedin.com/jobs/view/1',\n\t\t\t\t\t\t\tjob_title: 'Senior Engineer',\n\t\t\t\t\t\t\tcompany: 'Google',\n\t\t\t\t\t\t\tcompany_url: 'https://linkedin.com/company/google',\n\t\t\t\t\t\t\tpicture_url: 'https://example.com/pic.jpg',\n\t\t\t\t\t\t\tlocation: 'Mountain View, CA',\n\t\t\t\t\t\t\tjob_description: 'We are hiring...',\n\t\t\t\t\t\t\tsuccess: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\n\t\t\t\tconst result = batchResponse.results[0];\n\t\t\t\texpect(result).toHaveProperty('url');\n\t\t\t\texpect(result).toHaveProperty('job_title');\n\t\t\t\texpect(result).toHaveProperty('company');\n\t\t\t\texpect(result).toHaveProperty('company_url');\n\t\t\t\texpect(result).toHaveProperty('picture_url');\n\t\t\t\texpect(result).toHaveProperty('location');\n\t\t\t\texpect(result).toHaveProperty('job_description');\n\t\t\t\texpect(result).toHaveProperty('success');\n\t\t\t});\n\n\t\t\tit('should preserve original index during result mapping', () => {\n\t\t\t\tconst chunk = [\n\t\t\t\t\t{ url: 'https://linkedin.com/jobs/view/1', index: 0 },\n\t\t\t\t\t{ url: 'https://linkedin.com/jobs/view/2', index: 1 },\n\t\t\t\t];\n\t\t\t\tconst batchResults = [\n\t\t\t\t\t{ url: 'https://linkedin.com/jobs/view/1', job_title: 'Job 1', success: true },\n\t\t\t\t\t{ url: 'https://linkedin.com/jobs/view/2', job_title: 'Job 2', success: true },\n\t\t\t\t];\n\n\t\t\t\t// Map results back with original indices\n\t\t\t\tconst mappedResults = batchResults.map((result, j) => ({\n\t\t\t\t\t...result,\n\t\t\t\t\t_originalIndex: chunk[j].index,\n\t\t\t\t}));\n\n\t\t\t\texpect(mappedResults[0]._originalIndex).toBe(0);\n\t\t\t\texpect(mappedResults[1]._originalIndex).toBe(1);\n\t\t\t});\n\t\t});\n\n\t\tdescribe('Company Followers Result Mapping', () => {\n\t\t\tit('should map batch response results with follower_count and source', () => {\n\t\t\t\tconst batchResponse = {\n\t\t\t\t\tresults: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\turl: 'https://linkedin.com/company/google',\n\t\t\t\t\t\t\tfollower_count: 1000000,\n\t\t\t\t\t\t\tsource: 'linkedin',\n\t\t\t\t\t\t\tsuccess: true,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\n\t\t\t\tconst result = batchResponse.results[0];\n\t\t\t\texpect(result).toHaveProperty('url');\n\t\t\t\texpect(result).toHaveProperty('follower_count');\n\t\t\t\texpect(result).toHaveProperty('source');\n\t\t\t\texpect(result).toHaveProperty('success');\n\t\t\t});\n\n\t\t\tit('should handle cached results with source: cache', () => {\n\t\t\t\tconst cachedResult = {\n\t\t\t\t\turl: 'https://linkedin.com/company/google',\n\t\t\t\t\tfollower_count: 1000000,\n\t\t\t\t\tsource: 'cache',\n\t\t\t\t\tsuccess: true,\n\t\t\t\t};\n\n\t\t\t\texpect(cachedResult.source).toBe('cache');\n\t\t\t\texpect(cachedResult.success).toBe(true);\n\t\t\t});\n\n\t\t\tit('should handle error results with source: error', () => {\n\t\t\t\tconst errorResult = {\n\t\t\t\t\turl: 'https://linkedin.com/company/invalid',\n\t\t\t\t\tfollower_count: 0,\n\t\t\t\t\tsource: 'error',\n\t\t\t\t\tsuccess: false,\n\t\t\t\t\terror: 'Company not found',\n\t\t\t\t};\n\n\t\t\t\texpect(errorResult.source).toBe('error');\n\t\t\t\texpect(errorResult.success).toBe(false);\n\t\t\t\texpect(errorResult.error).toBeDefined();\n\t\t\t});\n\t\t});\n\t});\n});\n"]}
|
package/dist/test/setup.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* Author: marco
|
|
4
|
-
* Last updated: 2025-05-30
|
|
5
|
-
*/
|
|
6
|
-
// Global test setup
|
|
7
|
-
beforeAll(() => {
|
|
8
|
-
// Setup environment variables for testing
|
|
9
|
-
if (!process.env.NODE_ENV) {
|
|
10
|
-
Object.defineProperty(process.env, 'NODE_ENV', {
|
|
11
|
-
value: 'test',
|
|
12
|
-
writable: true,
|
|
13
|
-
});
|
|
14
|
-
}
|
|
15
|
-
});
|
|
16
|
-
afterAll(() => {
|
|
17
|
-
// Cleanup after all tests
|
|
18
|
-
});
|
|
19
|
-
// Mock N8N core modules
|
|
20
|
-
jest.mock('n8n-workflow', () => ({
|
|
21
|
-
NodeApiError: class NodeApiError extends Error {
|
|
22
|
-
constructor(_node, error, _options) {
|
|
23
|
-
var _a;
|
|
24
|
-
super((_a = error.message) !== null && _a !== void 0 ? _a : 'API Error');
|
|
25
|
-
this.name = 'NodeApiError';
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
NodeOperationError: class NodeOperationError extends Error {
|
|
29
|
-
constructor(_node, error, _options) {
|
|
30
|
-
var _a;
|
|
31
|
-
super((_a = error.message) !== null && _a !== void 0 ? _a : 'Operation Error');
|
|
32
|
-
this.name = 'NodeOperationError';
|
|
33
|
-
}
|
|
34
|
-
},
|
|
35
|
-
}));
|
|
36
|
-
jest.mock('n8n-core', () => ({
|
|
37
|
-
// Add mock implementations as needed
|
|
38
|
-
}));
|
|
39
|
-
|
|
40
|
-
//# sourceMappingURL=setup.js.map
|
package/dist/test/setup.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../test/setup.ts"],"names":[],"mappings":";AAAA;;;GAGG;AAEH,oBAAoB;AACpB,SAAS,CAAC,GAAG,EAAE;IACd,0CAA0C;IAC1C,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAC;QAC3B,MAAM,CAAC,cAAc,CAAC,OAAO,CAAC,GAAG,EAAE,UAAU,EAAE;YAC9C,KAAK,EAAE,MAAM;YACb,QAAQ,EAAE,IAAI;SACd,CAAC,CAAC;IACJ,CAAC;AACF,CAAC,CAAC,CAAC;AAEH,QAAQ,CAAC,GAAG,EAAE;IACb,0BAA0B;AAC3B,CAAC,CAAC,CAAC;AAEH,wBAAwB;AACxB,IAAI,CAAC,IAAI,CAAC,cAAc,EAAE,GAAG,EAAE,CAAC,CAAC;IAChC,YAAY,EAAE,MAAM,YAAa,SAAQ,KAAK;QAC7C,YAAY,KAAU,EAAE,KAAU,EAAE,QAAc;;YACjD,KAAK,CAAC,MAAA,KAAK,CAAC,OAAO,mCAAI,WAAW,CAAC,CAAC;YACpC,IAAI,CAAC,IAAI,GAAG,cAAc,CAAC;QAC5B,CAAC;KACD;IACD,kBAAkB,EAAE,MAAM,kBAAmB,SAAQ,KAAK;QACzD,YAAY,KAAU,EAAE,KAAU,EAAE,QAAc;;YACjD,KAAK,CAAC,MAAA,KAAK,CAAC,OAAO,mCAAI,iBAAiB,CAAC,CAAC;YAC1C,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QAClC,CAAC;KACD;CACD,CAAC,CAAC,CAAC;AAEJ,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC;AAC5B,qCAAqC;CACrC,CAAC,CAAC,CAAC","file":"setup.js","sourcesContent":["/**\n * Author: marco\n * Last updated: 2025-05-30\n */\n\n// Global test setup\nbeforeAll(() => {\n\t// Setup environment variables for testing\n\tif (!process.env.NODE_ENV) {\n\t\tObject.defineProperty(process.env, 'NODE_ENV', {\n\t\t\tvalue: 'test',\n\t\t\twritable: true,\n\t\t});\n\t}\n});\n\nafterAll(() => {\n\t// Cleanup after all tests\n});\n\n// Mock N8N core modules\njest.mock('n8n-workflow', () => ({\n\tNodeApiError: class NodeApiError extends Error {\n\t\tconstructor(_node: any, error: any, _options?: any) {\n\t\t\tsuper(error.message ?? 'API Error');\n\t\t\tthis.name = 'NodeApiError';\n\t\t}\n\t},\n\tNodeOperationError: class NodeOperationError extends Error {\n\t\tconstructor(_node: any, error: any, _options?: any) {\n\t\t\tsuper(error.message ?? 'Operation Error');\n\t\t\tthis.name = 'NodeOperationError';\n\t\t}\n\t},\n}));\n\njest.mock('n8n-core', () => ({\n\t// Add mock implementations as needed\n}));\n"]}
|