n8n-nodes-linkedin-scraper 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # LinkedIn Scraper Microservice Node
2
+
3
+ A standalone n8n community node that connects to the LinkedIn scraper microservice packaged in this repository. It exposes login, job search, job detail, and company follower endpoints so any official n8n installation can orchestrate scraping workflows without custom Docker images.
4
+
5
+ ## Features
6
+
7
+ - **Session Management** – Initiate a login request and reuse the returned cookies across subsequent calls.
8
+ - **Job Search Automation** – Trigger combined LinkedIn and jobs.ch job searches with blacklist filters and pagination controls.
9
+ - **Job Detail Retrieval** – Fetch job descriptions with optional authenticated cookies for private postings.
10
+ - **Company Insights** – Collect LinkedIn company follower counts to enrich CRM records.
11
+
12
+ ## Installation
13
+
14
+ 1. Clone or download the `n8n_linkedin_scraper_node` directory.
15
+ 2. Install dependencies:
16
+ ```bash
17
+ yarn install
18
+ ```
19
+ 3. Build the node:
20
+ ```bash
21
+ yarn build
22
+ ```
23
+ 4. Copy the contents of the generated `dist/` folder to your n8n custom nodes directory (e.g. `~/.n8n/custom/`).
24
+ 5. Restart n8n and enable Community Nodes if required.
25
+
26
+ ## Configuration
27
+
28
+ 1. Create credentials in n8n using **LinkedIn Scraper API**.
29
+ 2. Provide the microservice base URL along with HTTP basic authentication if enabled.
30
+ 3. Store your LinkedIn email and password as encrypted credential properties.
31
+
32
+ ## Usage
33
+
34
+ The node exposes four resources:
35
+
36
+ - **Session → Login**: Returns cookies that can be stored for reuse inside the workflow.
37
+ - **Jobs → Search**: Accepts comma separated job titles and locations, plus optional company and keyword blacklists.
38
+ - **Job → Details**: Retrieves job metadata, optionally using session cookies from the login step.
39
+ - **Company → Followers**: Returns the follower count for a LinkedIn company profile.
40
+
41
+ Chain the Session → Login operation before other calls when authenticated cookies are required. Use n8n expressions to pass the cookie array between nodes.
42
+
43
+ ## Development
44
+
45
+ ```bash
46
+ yarn install
47
+ yarn lint
48
+ yarn test
49
+ yarn build
50
+ ```
51
+
52
+ ## Packaging
53
+
54
+ ```bash
55
+ yarn build
56
+ yarn pack
57
+ ```
58
+
59
+ The resulting archive can be published to npm or installed directly into another n8n deployment.
@@ -0,0 +1,15 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-labelledby="title desc">
2
+ <title id="title">LinkedIn Scraper Icon</title>
3
+ <desc id="desc">Stylised InNotes hexagon with LinkedIn glyph</desc>
4
+ <defs>
5
+ <linearGradient id="grad" x1="0%" x2="100%" y1="0%" y2="100%">
6
+ <stop offset="0%" stop-color="#3b82f6" />
7
+ <stop offset="100%" stop-color="#1d4ed8" />
8
+ </linearGradient>
9
+ </defs>
10
+ <rect width="128" height="128" rx="24" fill="#0f172a" />
11
+ <path d="M32 36l32-18 32 18v36l-32 18-32-18z" fill="url(#grad)" />
12
+ <rect x="38" y="50" width="16" height="36" rx="4" fill="#e2e8f0" />
13
+ <circle cx="46" cy="41" r="8" fill="#e2e8f0" />
14
+ <path d="M71 50h14c11 0 19 6 19 18v18h-16V72c0-6-3-9-8-9-5 0-8 3-8 9v14H56V50h15z" fill="#e2e8f0" />
15
+ </svg>
@@ -0,0 +1,493 @@
1
+ "use strict";
2
+ /**
3
+ * Author: Marco Visin
4
+ * Date: 2025-12-22
5
+ * LinkedIn Scraper n8n node
6
+ * Interacts with the LinkedIn scraper microservice
7
+ * Uses the same InNotes API credentials for authentication
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.LinkedinScraper = void 0;
11
+ const n8n_workflow_1 = require("n8n-workflow");
12
+ function splitCommaSeparated(value) {
13
+ return value
14
+ .split(',')
15
+ .map((entry) => entry.trim())
16
+ .filter((entry) => entry.length > 0);
17
+ }
18
+ class LinkedinScraper {
19
+ constructor() {
20
+ this.description = {
21
+ displayName: 'LinkedIn Scraper Service',
22
+ name: 'linkedinScraper',
23
+ group: ['transform'],
24
+ version: 1,
25
+ description: 'Interact with the LinkedIn scraper microservice',
26
+ subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
27
+ icon: 'file:logo.svg',
28
+ defaults: {
29
+ name: 'LinkedIn Scraper',
30
+ },
31
+ inputs: ["main" /* NodeConnectionType.Main */],
32
+ outputs: ["main" /* NodeConnectionType.Main */],
33
+ credentials: [
34
+ {
35
+ // Reuse the same InNotes API credentials
36
+ name: 'inNotesApi',
37
+ required: true,
38
+ },
39
+ ],
40
+ properties: [
41
+ // Scraper service URL
42
+ {
43
+ displayName: 'Scraper Service URL',
44
+ name: 'scraperUrl',
45
+ type: 'string',
46
+ default: 'https://scraper.innotes.me',
47
+ description: 'Base URL of the LinkedIn scraper service',
48
+ required: true,
49
+ },
50
+ // Resource selection
51
+ {
52
+ displayName: 'Resource',
53
+ name: 'resource',
54
+ type: 'options',
55
+ noDataExpression: true,
56
+ options: [
57
+ {
58
+ name: 'Session',
59
+ value: 'session',
60
+ },
61
+ {
62
+ name: 'Jobs',
63
+ value: 'jobs',
64
+ },
65
+ {
66
+ name: 'Job',
67
+ value: 'job',
68
+ },
69
+ {
70
+ name: 'Company',
71
+ value: 'company',
72
+ },
73
+ ],
74
+ default: 'jobs',
75
+ },
76
+ // Session operations
77
+ {
78
+ displayName: 'Operation',
79
+ name: 'operation',
80
+ type: 'options',
81
+ displayOptions: {
82
+ show: {
83
+ resource: ['session'],
84
+ },
85
+ },
86
+ options: [
87
+ {
88
+ name: 'Login',
89
+ value: 'login',
90
+ action: 'Login to LinkedIn and fetch cookies',
91
+ },
92
+ ],
93
+ default: 'login',
94
+ },
95
+ // Jobs operations
96
+ {
97
+ displayName: 'Operation',
98
+ name: 'operation',
99
+ type: 'options',
100
+ displayOptions: {
101
+ show: {
102
+ resource: ['jobs'],
103
+ },
104
+ },
105
+ options: [
106
+ {
107
+ name: 'Search',
108
+ value: 'search',
109
+ action: 'Search for jobs on LinkedIn and jobs.ch',
110
+ },
111
+ ],
112
+ default: 'search',
113
+ },
114
+ // Job operations
115
+ {
116
+ displayName: 'Operation',
117
+ name: 'operation',
118
+ type: 'options',
119
+ displayOptions: {
120
+ show: {
121
+ resource: ['job'],
122
+ },
123
+ },
124
+ options: [
125
+ {
126
+ name: 'Details',
127
+ value: 'details',
128
+ action: 'Fetch job details',
129
+ },
130
+ ],
131
+ default: 'details',
132
+ },
133
+ // Company operations
134
+ {
135
+ displayName: 'Operation',
136
+ name: 'operation',
137
+ type: 'options',
138
+ displayOptions: {
139
+ show: {
140
+ resource: ['company'],
141
+ },
142
+ },
143
+ options: [
144
+ {
145
+ name: 'Followers',
146
+ value: 'followers',
147
+ action: 'Fetch company follower count',
148
+ },
149
+ ],
150
+ default: 'followers',
151
+ },
152
+ // LinkedIn credentials for login
153
+ {
154
+ displayName: 'LinkedIn Email',
155
+ name: 'linkedinEmail',
156
+ type: 'string',
157
+ default: '',
158
+ placeholder: 'user@example.com',
159
+ description: 'LinkedIn account email',
160
+ displayOptions: {
161
+ show: {
162
+ resource: ['session'],
163
+ operation: ['login'],
164
+ },
165
+ },
166
+ required: true,
167
+ },
168
+ {
169
+ displayName: 'LinkedIn Password',
170
+ name: 'linkedinPassword',
171
+ type: 'string',
172
+ typeOptions: {
173
+ password: true,
174
+ },
175
+ default: '',
176
+ description: 'LinkedIn account password',
177
+ displayOptions: {
178
+ show: {
179
+ resource: ['session'],
180
+ operation: ['login'],
181
+ },
182
+ },
183
+ required: true,
184
+ },
185
+ // Jobs search parameters
186
+ {
187
+ displayName: 'Job Titles',
188
+ name: 'jobTitles',
189
+ type: 'string',
190
+ default: '',
191
+ placeholder: 'Software Engineer, Developer',
192
+ description: 'Comma-separated list of job titles to search for',
193
+ displayOptions: {
194
+ show: {
195
+ resource: ['jobs'],
196
+ operation: ['search'],
197
+ },
198
+ },
199
+ required: true,
200
+ },
201
+ {
202
+ displayName: 'Locations',
203
+ name: 'locations',
204
+ type: 'string',
205
+ default: '',
206
+ placeholder: 'Zurich, Remote',
207
+ description: 'Comma-separated list of locations to search in',
208
+ displayOptions: {
209
+ show: {
210
+ resource: ['jobs'],
211
+ operation: ['search'],
212
+ },
213
+ },
214
+ required: true,
215
+ },
216
+ {
217
+ displayName: 'LinkedIn Email',
218
+ name: 'linkedinEmailSearch',
219
+ type: 'string',
220
+ default: '',
221
+ placeholder: 'user@example.com',
222
+ description: 'LinkedIn account email for authentication',
223
+ displayOptions: {
224
+ show: {
225
+ resource: ['jobs'],
226
+ operation: ['search'],
227
+ },
228
+ },
229
+ required: true,
230
+ },
231
+ {
232
+ displayName: 'LinkedIn Password',
233
+ name: 'linkedinPasswordSearch',
234
+ type: 'string',
235
+ typeOptions: {
236
+ password: true,
237
+ },
238
+ default: '',
239
+ description: 'LinkedIn account password',
240
+ displayOptions: {
241
+ show: {
242
+ resource: ['jobs'],
243
+ operation: ['search'],
244
+ },
245
+ },
246
+ required: true,
247
+ },
248
+ {
249
+ displayName: 'Max Results',
250
+ name: 'maxResults',
251
+ type: 'number',
252
+ default: 500,
253
+ typeOptions: {
254
+ minValue: 1,
255
+ maxValue: 2000,
256
+ },
257
+ displayOptions: {
258
+ show: {
259
+ resource: ['jobs'],
260
+ operation: ['search'],
261
+ },
262
+ },
263
+ },
264
+ {
265
+ displayName: 'Include jobs.ch',
266
+ name: 'includeJobsCh',
267
+ type: 'boolean',
268
+ default: true,
269
+ displayOptions: {
270
+ show: {
271
+ resource: ['jobs'],
272
+ operation: ['search'],
273
+ },
274
+ },
275
+ },
276
+ {
277
+ displayName: 'Company Blacklist',
278
+ name: 'companyBlacklist',
279
+ type: 'string',
280
+ default: '',
281
+ placeholder: 'Company A, Company B',
282
+ description: 'Comma-separated list of companies to exclude',
283
+ displayOptions: {
284
+ show: {
285
+ resource: ['jobs'],
286
+ operation: ['search'],
287
+ },
288
+ },
289
+ },
290
+ {
291
+ displayName: 'Keyword Blacklist',
292
+ name: 'keywordBlacklist',
293
+ type: 'string',
294
+ default: '',
295
+ placeholder: 'Senior, Lead',
296
+ description: 'Comma-separated list of keywords to exclude from job titles',
297
+ displayOptions: {
298
+ show: {
299
+ resource: ['jobs'],
300
+ operation: ['search'],
301
+ },
302
+ },
303
+ },
304
+ // Job details parameters
305
+ {
306
+ displayName: 'Job URL',
307
+ name: 'jobUrl',
308
+ type: 'string',
309
+ default: '',
310
+ description: 'LinkedIn or jobs.ch job URL',
311
+ displayOptions: {
312
+ show: {
313
+ resource: ['job'],
314
+ operation: ['details'],
315
+ },
316
+ },
317
+ required: true,
318
+ },
319
+ // Company parameters
320
+ {
321
+ displayName: 'Company URL',
322
+ name: 'companyUrl',
323
+ type: 'string',
324
+ default: '',
325
+ description: 'LinkedIn company URL',
326
+ displayOptions: {
327
+ show: {
328
+ resource: ['company'],
329
+ operation: ['followers'],
330
+ },
331
+ },
332
+ required: true,
333
+ },
334
+ ],
335
+ };
336
+ }
337
+ async execute() {
338
+ const items = this.getInputData();
339
+ const returnData = [];
340
+ // Get InNotes API credentials (reused for scraper authentication)
341
+ const credentials = (await this.getCredentials('inNotesApi'));
342
+ if (!credentials) {
343
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {
344
+ message: 'InNotes API credentials are missing',
345
+ });
346
+ }
347
+ // Get the Bearer token from credentials
348
+ const authMethod = credentials.authMethod;
349
+ let bearerToken;
350
+ if (authMethod === 'token') {
351
+ bearerToken = credentials.token;
352
+ }
353
+ else {
354
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {
355
+ message: 'LinkedIn Scraper requires Bearer token authentication. Please update your InNotes API credentials to use Bearer Token.',
356
+ });
357
+ }
358
+ if (!bearerToken) {
359
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {
360
+ message: 'Bearer token is missing from InNotes API credentials',
361
+ });
362
+ }
363
+ for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
364
+ const scraperUrl = this.getNodeParameter('scraperUrl', itemIndex).replace(/\/+$/, '');
365
+ const resource = this.getNodeParameter('resource', itemIndex);
366
+ const operation = this.getNodeParameter('operation', itemIndex);
367
+ try {
368
+ if (resource === 'session' && operation === 'login') {
369
+ const email = this.getNodeParameter('linkedinEmail', itemIndex);
370
+ const password = this.getNodeParameter('linkedinPassword', itemIndex);
371
+ const response = await this.helpers.request({
372
+ method: 'POST',
373
+ url: `${scraperUrl}/linkedin/login`,
374
+ headers: {
375
+ 'Authorization': `Bearer ${bearerToken}`,
376
+ 'Content-Type': 'application/json',
377
+ },
378
+ body: {
379
+ email,
380
+ password,
381
+ },
382
+ json: true,
383
+ });
384
+ returnData.push(response);
385
+ continue;
386
+ }
387
+ if (resource === 'jobs' && operation === 'search') {
388
+ const jobTitles = splitCommaSeparated(this.getNodeParameter('jobTitles', itemIndex));
389
+ const locations = splitCommaSeparated(this.getNodeParameter('locations', itemIndex));
390
+ const email = this.getNodeParameter('linkedinEmailSearch', itemIndex);
391
+ const password = this.getNodeParameter('linkedinPasswordSearch', itemIndex);
392
+ const maxResults = this.getNodeParameter('maxResults', itemIndex);
393
+ const includeJobsCh = this.getNodeParameter('includeJobsCh', itemIndex);
394
+ const companyBlacklist = splitCommaSeparated(this.getNodeParameter('companyBlacklist', itemIndex) || '');
395
+ const keywordBlacklist = splitCommaSeparated(this.getNodeParameter('keywordBlacklist', itemIndex) || '');
396
+ // First login to get cookies
397
+ const loginResponse = await this.helpers.request({
398
+ method: 'POST',
399
+ url: `${scraperUrl}/linkedin/login`,
400
+ headers: {
401
+ 'Authorization': `Bearer ${bearerToken}`,
402
+ 'Content-Type': 'application/json',
403
+ },
404
+ body: {
405
+ email,
406
+ password,
407
+ },
408
+ json: true,
409
+ });
410
+ const cookies = loginResponse.cookies;
411
+ // Then search for jobs
412
+ const response = await this.helpers.request({
413
+ method: 'POST',
414
+ url: `${scraperUrl}/jobs/search`,
415
+ headers: {
416
+ 'Authorization': `Bearer ${bearerToken}`,
417
+ 'Content-Type': 'application/json',
418
+ },
419
+ body: {
420
+ job_titles: jobTitles,
421
+ locations,
422
+ max_results: maxResults,
423
+ include_jobs_ch: includeJobsCh,
424
+ company_blacklist: companyBlacklist,
425
+ keywords_blacklist: keywordBlacklist,
426
+ cookies,
427
+ },
428
+ json: true,
429
+ });
430
+ const jobs = response.jobs;
431
+ if (!Array.isArray(jobs)) {
432
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {
433
+ message: 'Unexpected response format from job search',
434
+ });
435
+ }
436
+ returnData.push(...jobs);
437
+ continue;
438
+ }
439
+ if (resource === 'job' && operation === 'details') {
440
+ const jobUrl = this.getNodeParameter('jobUrl', itemIndex);
441
+ const response = await this.helpers.request({
442
+ method: 'POST',
443
+ url: `${scraperUrl}/jobs/details`,
444
+ headers: {
445
+ 'Authorization': `Bearer ${bearerToken}`,
446
+ 'Content-Type': 'application/json',
447
+ },
448
+ body: {
449
+ url: jobUrl,
450
+ },
451
+ json: true,
452
+ });
453
+ returnData.push(response);
454
+ continue;
455
+ }
456
+ if (resource === 'company' && operation === 'followers') {
457
+ const companyUrl = this.getNodeParameter('companyUrl', itemIndex);
458
+ const response = await this.helpers.request({
459
+ method: 'POST',
460
+ url: `${scraperUrl}/companies/followers`,
461
+ headers: {
462
+ 'Authorization': `Bearer ${bearerToken}`,
463
+ 'Content-Type': 'application/json',
464
+ },
465
+ body: {
466
+ url: companyUrl,
467
+ },
468
+ json: true,
469
+ });
470
+ returnData.push(response);
471
+ continue;
472
+ }
473
+ throw new n8n_workflow_1.NodeApiError(this.getNode(), {
474
+ message: `Unsupported resource (${resource}) or operation (${operation})`,
475
+ });
476
+ }
477
+ catch (error) {
478
+ if (this.continueOnFail()) {
479
+ returnData.push({
480
+ error: error.message,
481
+ itemIndex,
482
+ });
483
+ continue;
484
+ }
485
+ throw error;
486
+ }
487
+ }
488
+ return [returnData.map((data) => ({ json: data }))];
489
+ }
490
+ }
491
+ exports.LinkedinScraper = LinkedinScraper;
492
+
493
+ //# sourceMappingURL=LinkedinScraper.node.js.map
@@ -0,0 +1 @@
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: 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\tNodeConnectionType,\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: [NodeConnectionType.Main],\n\t\toutputs: [NodeConnectionType.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// Scraper service URL\n\t\t\t{\n\t\t\t\tdisplayName: 'Scraper Service URL',\n\t\t\t\tname: 'scraperUrl',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: 'https://scraper.innotes.me',\n\t\t\t\tdescription: 'Base URL of the LinkedIn scraper service',\n\t\t\t\trequired: true,\n\t\t\t},\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: 'Session',\n\t\t\t\t\t\tvalue: 'session',\n\t\t\t\t\t},\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// Session 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: ['session'],\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: 'Login',\n\t\t\t\t\t\tvalue: 'login',\n\t\t\t\t\t\taction: 'Login to LinkedIn and fetch cookies',\n\t\t\t\t\t},\n\t\t\t\t],\n\t\t\t\tdefault: 'login',\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// LinkedIn credentials for login\n\t\t\t{\n\t\t\t\tdisplayName: 'LinkedIn Email',\n\t\t\t\tname: 'linkedinEmail',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tplaceholder: 'user@example.com',\n\t\t\t\tdescription: 'LinkedIn account email',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['session'],\n\t\t\t\t\t\toperation: ['login'],\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: 'LinkedIn Password',\n\t\t\t\tname: 'linkedinPassword',\n\t\t\t\ttype: 'string',\n\t\t\t\ttypeOptions: {\n\t\t\t\t\tpassword: true,\n\t\t\t\t},\n\t\t\t\tdefault: '',\n\t\t\t\tdescription: 'LinkedIn account password',\n\t\t\t\tdisplayOptions: {\n\t\t\t\t\tshow: {\n\t\t\t\t\t\tresource: ['session'],\n\t\t\t\t\t\toperation: ['login'],\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// 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: 'LinkedIn Email',\n\t\t\t\tname: 'linkedinEmailSearch',\n\t\t\t\ttype: 'string',\n\t\t\t\tdefault: '',\n\t\t\t\tplaceholder: 'user@example.com',\n\t\t\t\tdescription: 'LinkedIn account email for authentication',\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: 'LinkedIn Password',\n\t\t\t\tname: 'linkedinPasswordSearch',\n\t\t\t\ttype: 'string',\n\t\t\t\ttypeOptions: {\n\t\t\t\t\tpassword: true,\n\t\t\t\t},\n\t\t\t\tdefault: '',\n\t\t\t\tdescription: 'LinkedIn account password',\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: '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// Get the Bearer token from credentials\n\t\tconst authMethod = credentials.authMethod as string;\n\t\tlet bearerToken: string | undefined;\n\n\t\tif (authMethod === 'token') {\n\t\t\tbearerToken = credentials.token as string;\n\t\t} else {\n\t\t\tthrow new NodeApiError(this.getNode(), {\n\t\t\t\tmessage: 'LinkedIn Scraper requires Bearer token authentication. Please update your InNotes API credentials to use Bearer Token.',\n\t\t\t});\n\t\t}\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 = (this.getNodeParameter('scraperUrl', itemIndex) as string).replace(/\\/+$/, '');\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 === 'session' && operation === 'login') {\n\t\t\t\t\tconst email = this.getNodeParameter('linkedinEmail', itemIndex) as string;\n\t\t\t\t\tconst password = this.getNodeParameter('linkedinPassword', itemIndex) as string;\n\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}/linkedin/login`,\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: {\n\t\t\t\t\t\t\temail,\n\t\t\t\t\t\t\tpassword,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tjson: true,\n\t\t\t\t\t});\n\t\t\t\t\treturnData.push(response as IDataObject);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\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 email = this.getNodeParameter('linkedinEmailSearch', itemIndex) as string;\n\t\t\t\t\tconst password = this.getNodeParameter('linkedinPasswordSearch', itemIndex) as string;\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 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// First login to get cookies\n\t\t\t\t\tconst loginResponse = await this.helpers.request({\n\t\t\t\t\t\tmethod: 'POST',\n\t\t\t\t\t\turl: `${scraperUrl}/linkedin/login`,\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: {\n\t\t\t\t\t\t\temail,\n\t\t\t\t\t\t\tpassword,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tjson: true,\n\t\t\t\t\t});\n\n\t\t\t\t\tconst cookies = (loginResponse as IDataObject).cookies as IDataObject[];\n\n\t\t\t\t\t// Then 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: {\n\t\t\t\t\t\t\tjob_titles: jobTitles,\n\t\t\t\t\t\t\tlocations,\n\t\t\t\t\t\t\tmax_results: maxResults,\n\t\t\t\t\t\t\tinclude_jobs_ch: includeJobsCh,\n\t\t\t\t\t\t\tcompany_blacklist: companyBlacklist,\n\t\t\t\t\t\t\tkeywords_blacklist: keywordBlacklist,\n\t\t\t\t\t\t\tcookies,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tjson: true,\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\t\t\t\t\treturnData.push(...jobs);\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\tconst jobUrl = this.getNodeParameter('jobUrl', itemIndex) as string;\n\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/details`,\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: {\n\t\t\t\t\t\t\turl: jobUrl,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tjson: true,\n\t\t\t\t\t});\n\t\t\t\t\treturnData.push(response as IDataObject);\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\n\t\t\t\tif (resource === 'company' && operation === 'followers') {\n\t\t\t\t\tconst companyUrl = this.getNodeParameter('companyUrl', itemIndex) as string;\n\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}/companies/followers`,\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: {\n\t\t\t\t\t\t\turl: companyUrl,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tjson: true,\n\t\t\t\t\t});\n\t\t\t\t\treturnData.push(response as IDataObject);\n\t\t\t\t\tcontinue;\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\tif (this.continueOnFail()) {\n\t\t\t\t\treturnData.push({\n\t\t\t\t\t\terror: (error as Error).message,\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"]}
@@ -0,0 +1,15 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128" role="img" aria-labelledby="title desc">
2
+ <title id="title">LinkedIn Scraper Icon</title>
3
+ <desc id="desc">Stylised InNotes hexagon with LinkedIn glyph</desc>
4
+ <defs>
5
+ <linearGradient id="grad" x1="0%" x2="100%" y1="0%" y2="100%">
6
+ <stop offset="0%" stop-color="#3b82f6" />
7
+ <stop offset="100%" stop-color="#1d4ed8" />
8
+ </linearGradient>
9
+ </defs>
10
+ <rect width="128" height="128" rx="24" fill="#0f172a" />
11
+ <path d="M32 36l32-18 32 18v36l-32 18-32-18z" fill="url(#grad)" />
12
+ <rect x="38" y="50" width="16" height="36" rx="4" fill="#e2e8f0" />
13
+ <circle cx="46" cy="41" r="8" fill="#e2e8f0" />
14
+ <path d="M71 50h14c11 0 19 6 19 18v18h-16V72c0-6-3-9-8-9-5 0-8 3-8 9v14H56V50h15z" fill="#e2e8f0" />
15
+ </svg>
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "n8n-nodes-linkedin-scraper",
3
+ "version": "1.0.0",
4
+ "description": "N8N node for interacting with the standalone LinkedIn scraper microservice",
5
+ "keywords": [
6
+ "n8n-community-node-package",
7
+ "LinkedIn",
8
+ "scraper",
9
+ "jobs",
10
+ "automation"
11
+ ],
12
+ "license": "MIT",
13
+ "homepage": "https://github.com/mrc527/InNotes/tree/main/n8n_linkedin_scraper_node",
14
+ "author": {
15
+ "name": "Marco Visin",
16
+ "email": "marco@visin.ch"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/mrc527/InNotes.git",
21
+ "directory": "n8n_linkedin_scraper_node"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.10",
25
+ "yarn": ">=1.22.0"
26
+ },
27
+ "packageManager": "yarn@1.22.19",
28
+ "main": "index.js",
29
+ "scripts": {
30
+ "build": "gulp build",
31
+ "dev": "gulp dev",
32
+ "format": "prettier . --write",
33
+ "lint": "eslint . --ext .ts",
34
+ "lintfix": "eslint . --ext .ts --fix",
35
+ "test": "jest",
36
+ "test:watch": "jest --watch",
37
+ "test:coverage": "jest --coverage",
38
+ "prepublishOnly": "yarn build && yarn lint"
39
+ },
40
+ "files": [
41
+ "dist"
42
+ ],
43
+ "n8n": {
44
+ "n8nNodesApiVersion": 1,
45
+ "nodes": [
46
+ "dist/nodes/LinkedinScraper/LinkedinScraper.node.js"
47
+ ]
48
+ },
49
+ "devDependencies": {
50
+ "@types/jest": "^29.5.5",
51
+ "@types/node": "^18.16.16",
52
+ "@typescript-eslint/eslint-plugin": "^6.6.0",
53
+ "@typescript-eslint/parser": "^6.6.0",
54
+ "eslint": "^8.48.0",
55
+ "eslint-plugin-n8n-nodes-base": "^1.16.1",
56
+ "gulp": "^4.0.2",
57
+ "gulp-clean": "^0.4.0",
58
+ "gulp-sourcemaps": "^3.0.0",
59
+ "gulp-typescript": "^6.0.0-alpha.1",
60
+ "jest": "^29.6.4",
61
+ "n8n-core": "*",
62
+ "n8n-workflow": "*",
63
+ "prettier": "^3.0.3",
64
+ "ts-jest": "^29.1.1",
65
+ "typescript": "^5.2.2"
66
+ },
67
+ "peerDependencies": {
68
+ "n8n-core": "*",
69
+ "n8n-workflow": "*"
70
+ }
71
+ }
@@ -0,0 +1,27 @@
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
+ it('should expose login, job, and company resources', () => {
6
+ const node = new LinkedinScraper_node_1.LinkedinScraper();
7
+ const resources = node.description.properties
8
+ .filter((property) => property.name === 'resource')
9
+ .flatMap((property) => ('options' in property ? property.options : []))
10
+ .filter((option) => option !== undefined && 'value' in option)
11
+ .map((option) => option.value);
12
+ expect(resources).toEqual(expect.arrayContaining(['session', 'jobs', 'job', 'company']));
13
+ });
14
+ it('should require inNotesApi credentials', () => {
15
+ var _a;
16
+ const node = new LinkedinScraper_node_1.LinkedinScraper();
17
+ expect((_a = node.description.credentials) === null || _a === void 0 ? void 0 : _a.map((cred) => cred.name)).toContain('inNotesApi');
18
+ });
19
+ it('should have scraperUrl parameter', () => {
20
+ const node = new LinkedinScraper_node_1.LinkedinScraper();
21
+ const scraperUrlParam = node.description.properties.find((p) => p.name === 'scraperUrl');
22
+ expect(scraperUrlParam).toBeDefined();
23
+ expect(scraperUrlParam === null || scraperUrlParam === void 0 ? void 0 : scraperUrlParam.default).toBe('https://scraper.innotes.me');
24
+ });
25
+ });
26
+
27
+ //# sourceMappingURL=LinkedinScraper.node.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../test/LinkedinScraper.node.test.ts"],"names":[],"mappings":";;AAAA,wFAAgF;AAGhF,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACrC,EAAE,CAAC,iDAAiD,EAAE,GAAG,EAAE;QAC1D,MAAM,IAAI,GAAG,IAAI,sCAAe,EAAE,CAAC;QACnC,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,CAAC,UAAU;aAC3C,MAAM,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,UAAU,CAAC;aAClD,OAAO,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,SAAS,IAAI,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;aACtE,MAAM,CAAC,CAAC,MAAM,EAAkC,EAAE,CAAC,MAAM,KAAK,SAAS,IAAI,OAAO,IAAI,MAAM,CAAC;aAC7F,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAEhC,MAAM,CAAC,SAAS,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC;IAC1F,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,GAAG,EAAE;;QAChD,MAAM,IAAI,GAAG,IAAI,sCAAe,EAAE,CAAC;QACnC,MAAM,CAAC,MAAA,IAAI,CAAC,WAAW,CAAC,WAAW,0CAAE,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,YAAY,CAAC,CAAC;IACxF,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC3C,MAAM,IAAI,GAAG,IAAI,sCAAe,EAAE,CAAC;QACnC,MAAM,eAAe,GAAG,IAAI,CAAC,WAAW,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC;QACzF,MAAM,CAAC,eAAe,CAAC,CAAC,WAAW,EAAE,CAAC;QACtC,MAAM,CAAC,eAAe,aAAf,eAAe,uBAAf,eAAe,CAAE,OAAO,CAAC,CAAC,IAAI,CAAC,4BAA4B,CAAC,CAAC;IACrE,CAAC,CAAC,CAAC;AACJ,CAAC,CAAC,CAAC","file":"LinkedinScraper.node.test.js","sourcesContent":["import { LinkedinScraper } from '../nodes/LinkedinScraper/LinkedinScraper.node';\nimport { INodePropertyOptions } from 'n8n-workflow';\n\ndescribe('LinkedinScraper Node', () => {\n\tit('should expose login, job, and company resources', () => {\n\t\tconst node = new LinkedinScraper();\n\t\tconst resources = node.description.properties\n\t\t\t.filter((property) => property.name === 'resource')\n\t\t\t.flatMap((property) => ('options' in property ? property.options : []))\n\t\t\t.filter((option): option is INodePropertyOptions => option !== undefined && 'value' in option)\n\t\t\t.map((option) => option.value);\n\n\t\texpect(resources).toEqual(expect.arrayContaining(['session', 'jobs', 'job', 'company']));\n\t});\n\n\tit('should require inNotesApi credentials', () => {\n\t\tconst node = new LinkedinScraper();\n\t\texpect(node.description.credentials?.map((cred) => cred.name)).toContain('inNotesApi');\n\t});\n\n\tit('should have scraperUrl parameter', () => {\n\t\tconst node = new LinkedinScraper();\n\t\tconst scraperUrlParam = node.description.properties.find((p) => p.name === 'scraperUrl');\n\t\texpect(scraperUrlParam).toBeDefined();\n\t\texpect(scraperUrlParam?.default).toBe('https://scraper.innotes.me');\n\t});\n});\n"]}
@@ -0,0 +1,38 @@
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
+ super(error.message || 'API Error');
24
+ this.name = 'NodeApiError';
25
+ }
26
+ },
27
+ NodeOperationError: class NodeOperationError extends Error {
28
+ constructor(_node, error, _options) {
29
+ super(error.message || 'Operation Error');
30
+ this.name = 'NodeOperationError';
31
+ }
32
+ },
33
+ }));
34
+ jest.mock('n8n-core', () => ({
35
+ // Add mock implementations as needed
36
+ }));
37
+
38
+ //# sourceMappingURL=setup.js.map
@@ -0,0 +1 @@
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,KAAK,CAAC,OAAO,IAAI,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,KAAK,CAAC,OAAO,IAAI,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"]}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "n8n-nodes-linkedin-scraper",
3
+ "version": "1.0.0",
4
+ "description": "N8N node for interacting with the standalone LinkedIn scraper microservice",
5
+ "keywords": [
6
+ "n8n-community-node-package",
7
+ "LinkedIn",
8
+ "scraper",
9
+ "jobs",
10
+ "automation"
11
+ ],
12
+ "license": "MIT",
13
+ "homepage": "https://github.com/mrc527/InNotes/tree/main/n8n_linkedin_scraper_node",
14
+ "author": {
15
+ "name": "Marco Visin",
16
+ "email": "marco@visin.ch"
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://github.com/mrc527/InNotes.git",
21
+ "directory": "n8n_linkedin_scraper_node"
22
+ },
23
+ "engines": {
24
+ "node": ">=18.10",
25
+ "yarn": ">=1.22.0"
26
+ },
27
+ "packageManager": "yarn@1.22.19",
28
+ "main": "index.js",
29
+ "scripts": {
30
+ "build": "gulp build",
31
+ "dev": "gulp dev",
32
+ "format": "prettier . --write",
33
+ "lint": "eslint . --ext .ts",
34
+ "lintfix": "eslint . --ext .ts --fix",
35
+ "test": "jest",
36
+ "test:watch": "jest --watch",
37
+ "test:coverage": "jest --coverage",
38
+ "prepublishOnly": "yarn build && yarn lint"
39
+ },
40
+ "files": [
41
+ "dist"
42
+ ],
43
+ "n8n": {
44
+ "n8nNodesApiVersion": 1,
45
+ "nodes": [
46
+ "dist/nodes/LinkedinScraper/LinkedinScraper.node.js"
47
+ ]
48
+ },
49
+ "devDependencies": {
50
+ "@types/jest": "^29.5.5",
51
+ "@types/node": "^18.16.16",
52
+ "@typescript-eslint/eslint-plugin": "^6.6.0",
53
+ "@typescript-eslint/parser": "^6.6.0",
54
+ "eslint": "^8.48.0",
55
+ "eslint-plugin-n8n-nodes-base": "^1.16.1",
56
+ "gulp": "^4.0.2",
57
+ "gulp-clean": "^0.4.0",
58
+ "gulp-sourcemaps": "^3.0.0",
59
+ "gulp-typescript": "^6.0.0-alpha.1",
60
+ "jest": "^29.6.4",
61
+ "n8n-core": "*",
62
+ "n8n-workflow": "*",
63
+ "prettier": "^3.0.3",
64
+ "ts-jest": "^29.1.1",
65
+ "typescript": "^5.2.2"
66
+ },
67
+ "peerDependencies": {
68
+ "n8n-core": "*",
69
+ "n8n-workflow": "*"
70
+ }
71
+ }