Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6,508 changes: 0 additions & 6,508 deletions package-lock.json

This file was deleted.

28 changes: 15 additions & 13 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,36 +13,38 @@
"test": "jest --silent --logHeapUsage --coverage",
"dev": "node --env-file=.env -r esbuild-register ./src/launch.ts -e .ts",
"dev:secrets": "doppler setup --no-interactive && doppler secrets download --no-file --format env > .env && touch .env.overrides && cat .env.overrides >> .env",
"dev:consumer": "node --env-file=.env -r esbuild-register ./src/launch-sqs-consumer.ts",
"audit:critical": "npm audit --audit-level=critical",
"clean": "rm -rf dist node_modules"
},
"dependencies": {
"aws-lambda": "1.0.7",
"axios": "1.7.7",
"axios": "1.7.9",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"date-fns": "4.1.0",
"envalid": "8.0.0",
"reflect-metadata": "0.2.2",
"ts-jest": "29.2.5",
"winston": "3.15.0"
"winston": "3.17.0"
},
"devDependencies": {
"@eslint/eslintrc": "3.1.0",
"@eslint/js": "9.13.0",
"@types/aws-lambda": "8.10.145",
"@types/jest": "29.5.13",
"@types/node": "22.7.8",
"@typescript-eslint/eslint-plugin": "8.11.0",
"@typescript-eslint/parser": "8.11.0",
"@eslint/eslintrc": "3.2.0",
"@eslint/js": "9.16.0",
"@types/aws-lambda": "8.10.146",
"@types/jest": "29.5.14",
"@types/node": "22.10.1",
"@typescript-eslint/eslint-plugin": "8.18.0",
"@typescript-eslint/parser": "8.18.0",
"esbuild-register": "3.6.0",
"eslint": "9.13.0",
"eslint": "9.16.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-prettier": "5.2.1",
"jest-mock-extended": "4.0.0-beta1",
"prettier": "3.3.3",
"jest-mock-extended": "3.0.7",
"prettier": "3.4.2",
"prettier-plugin-sort-imports": "1.8.6",
"typescript": "5.6.3"
"sqs-consumer": "11.2.0",
"typescript": "5.7.2"
},
"engines": {
"node": "20.17.0"
Expand Down
14 changes: 7 additions & 7 deletions src/contracts/linkedin-profile.request.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { IsIn, IsInt, IsNumber, IsString, IsUrl, Min } from 'class-validator';
import { IsIn, IsInt, IsNumber, IsString, IsUrl } from 'class-validator';

export class LinkedinProfileRequest {
@IsString()
public messageId!: string;

@IsString()
public importId!: string;

@IsString()
public contextId!: string;

Expand All @@ -14,13 +17,10 @@ export class LinkedinProfileRequest {
@IsInt()
public profileId!: number;

// Profile parameters
@IsString()
public profileApiToken!: string;
public linkedinApiToken!: string;

@IsUrl()
public linkedinUrl!: string;

@IsInt()
@Min(1)
public attempt!: number;
public linkedinProfileUrl!: string;
}
4 changes: 2 additions & 2 deletions src/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,10 @@ describe('Linkedin lambda handler', () => {

it('should throw an error if message is not a valid LinkedinProfileRequest', async () => {
const request = createMockedLinkedinProfileRequest();
request.profileApiToken = undefined as unknown as string; // missing required field
request.linkedinApiToken = undefined as unknown as string; // missing required field
const event = createMockedSqsSEvent(request);
const expectedErrorString =
'[LinkedinProfileRequestMapper] Validation failed: ["property: profileApiToken errors: profileApiToken must be a string"]';
'[LinkedinProfileRequestMapper] Validation failed: ["property: linkedinApiToken errors: linkedinApiToken must be a string"]';

await expect(handler(event, {} as Context, () => {})).rejects.toThrow(new Error(expectedErrorString));
});
Expand Down
14 changes: 10 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,18 @@ export const handler: Handler = async (event: SQSEvent): Promise<LinkedinProfile
try {
const linkedinProfileRequest = LinkedinProfileRequestMapper.toDomain(event);
Environment.setupEnvironment(linkedinProfileRequest);
logger.info(`⌛️ [handler] Starting Linkedin profile request for linkedinUrl: ${linkedinProfileRequest.linkedinUrl}`, linkedinProfileRequest);
logger.info(
`⌛️ [handler] Starting Linkedin profile request for linkedinProfileUrl: ${linkedinProfileRequest.linkedinProfileUrl}`,
linkedinProfileRequest
);

const { linkedinProfile, isEmptyProfile } = await new LinkedinProfileService().getLinkedinProfile(linkedinProfileRequest.profileApiToken);
const { linkedinProfile, isEmptyProfile } = await new LinkedinProfileService().getLinkedinProfile(linkedinProfileRequest.linkedinApiToken);

if (isEmptyProfile) {
logger.warn(`👻 [handler] Linkedin profile is not synced for linkedinUrl: ${linkedinProfileRequest.linkedinUrl}`, linkedinProfileRequest);
logger.warn(
`👻 [handler] Linkedin profile is not synced for linkedinProfileUrl: ${linkedinProfileRequest.linkedinProfileUrl}`,
linkedinProfileRequest
);
// TODO: send response to SQS queue again if no max retries
return undefined;
}
Expand All @@ -27,7 +33,7 @@ export const handler: Handler = async (event: SQSEvent): Promise<LinkedinProfile

return linkedinProfileResponse;
} catch (error) {
logger.error(`❌ [handler] Error processing Linkedin profile request: ${error}`, error);
logger.error(`❌ [handler] Error processing Linkedin profile request`, { error, event });
throw error;
}
};
60 changes: 60 additions & 0 deletions src/launch-sqs-consumer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Message, SQSClient } from '@aws-sdk/client-sqs';
import { Context, SQSEvent, SQSRecord } from 'aws-lambda';
import { Consumer } from 'sqs-consumer';

import { handler } from './index';
import { logger } from './util/logger';

/**
* Test the SQS consumer locally using LocalStack queues
*/
const sqsClient = new SQSClient({ region: 'eu-west-1', endpoint: 'http://localhost:4566' });

const queueUrl = 'http://localhost:4566/000000000000/linkedin-api-import-profile-local.fifo';

const consumer = Consumer.create({
queueUrl,
handleMessage: async (message: Message) => {
logger.info('👉 Message received:', message);

try {
const sqsRecord = {
messageId: message.MessageId!,
receiptHandle: message.ReceiptHandle!,
body: message.Body!,
attributes: message.Attributes || {},
messageAttributes: message.MessageAttributes || {},
md5OfBody: message.MD5OfBody || '',
eventSource: 'aws:sqs',
eventSourceARN: `arn:aws:sqs:eu-west-1:000000000000:linkedin-api-import-profile-local.fifo`,
awsRegion: 'eu-west-1'
} as unknown as SQSRecord;

const event = { Records: [sqsRecord] } as SQSEvent;

const response = await handler(event, {} as Context, () => {
logger.info('✅ Executed handler');
});

return response;
} catch (error) {
logger.error('❌ Error handling message:', error);
}
},
sqs: sqsClient
});

consumer.on('error', (err: Error) => {
logger.error('❌ Consumer error:', err.message);
});

consumer.on('processing_error', (err: Error) => {
logger.error('❌ Processing error:', err.message);
});

consumer.on('empty', () => {
logger.info('🥱 Waiting for messages...');
});

logger.info(`🔄 Starting SQS consumer for ${queueUrl}...`);
consumer.start();
4 changes: 2 additions & 2 deletions src/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { logger } from './util/logger';

// 👉 Fake manual execution without AWS: For local debug
void (async (): Promise<void> => {
const profileApiToken = process.env['LOCAL_PROFILE_API_TOKEN'];
const request = createMockedLinkedinProfileRequest({ profileApiToken });
const linkedinApiToken = process.env['LOCAL_LINKEDIN_API_TOKEN'];
const request = createMockedLinkedinProfileRequest({ linkedinApiToken });
const fakeEvent = createMockedSqsSEvent(request);

logger.info('👉 [launch.ts] Debugging lambda handler ...');
Expand Down
8 changes: 4 additions & 4 deletions src/mappers/linkedin-profile.request.mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ export class LinkedinProfileRequestMapper {

const request = new LinkedinProfileRequest();
request.messageId = message.messageId;
request.importId = messageBody.importId;
request.contextId = messageBody.contextId;
request.env = messageBody.env;
request.profileId = messageBody.profileId;
request.profileApiToken = messageBody.profileApiToken;
request.linkedinUrl = messageBody.linkedinUrl;
request.attempt = messageBody.attempt;
request.profileId = +messageBody.profileId;
request.linkedinApiToken = messageBody.linkedinApiToken;
request.linkedinProfileUrl = messageBody.linkedinProfileUrl;

this.validate(request);

Expand Down
6 changes: 3 additions & 3 deletions src/test/mocks/linkedin-profile.mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { LinkedinProfile } from '../../domain/linkedin-profile';
export const createMockedLinkedinProfileRequest = (customValues: Partial<LinkedinProfileRequest> = {}): LinkedinProfileRequest => {
const request = new LinkedinProfileRequest();
request.messageId = customValues.messageId ?? '059f36b4-87a3-44ab-83d2-661975830a7d';
request.importId = customValues.importId ?? '1';
request.contextId = customValues.contextId ?? '1234';
request.env = customValues.env ?? 'local';
request.profileId = customValues.profileId ?? 123;
request.profileApiToken = customValues.profileApiToken ?? 'fake-token';
request.linkedinUrl = customValues.linkedinUrl ?? 'https://www.linkedin.com/in/username';
request.attempt = customValues.attempt ?? 1;
request.linkedinApiToken = customValues.linkedinApiToken ?? 'fake-token';
request.linkedinProfileUrl = customValues.linkedinProfileUrl ?? 'https://www.linkedin.com/in/username';
return request;
};

Expand Down
6 changes: 3 additions & 3 deletions src/util/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@ import { setContextLogger } from './logger';

export class Environment {
public readonly LOGGER_CONSOLE: boolean;
public readonly LOCAL_PROFILE_API_TOKEN?: string;
public readonly LOCAL_LINKEDIN_API_TOKEN?: string;

private constructor(_envName: EnvironmentType) {
const env = cleanEnv(process.env, {
LOGGER_CONSOLE: bool({ default: false }),
LOCAL_PROFILE_API_TOKEN: str({ default: undefined })
LOCAL_LINKEDIN_API_TOKEN: str({ default: undefined })
});

this.LOGGER_CONSOLE = env.LOGGER_CONSOLE;
this.LOCAL_PROFILE_API_TOKEN = env.LOCAL_PROFILE_API_TOKEN ?? undefined;
this.LOCAL_LINKEDIN_API_TOKEN = env.LOCAL_LINKEDIN_API_TOKEN ?? undefined;
}

public static setupEnvironment(request: LinkedinProfileRequest): Environment {
Expand Down
Loading
Loading