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
3 changes: 1 addition & 2 deletions examples/custom-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ async function customWorkflowExample(): Promise<void> {
console.log('🚀 Linked API custom workflow example starting...');
const workflowId = await linkedapi.customWorkflow.execute({
actionType: 'st.searchPeople',
term: "John",
limit: 3,
filter: {
locations: ["San Francisco"],
Expand All @@ -31,7 +30,7 @@ async function customWorkflowExample(): Promise<void> {
const result = await linkedapi.customWorkflow.result(workflowId);

console.log('✅ Custom workflow executed successfully');
console.log('🔍 Result: ', JSON.stringify(result, null, 2));
console.log('🔍 Result: ', JSON.stringify(result.data, null, 2));
} catch (error) {
if (error instanceof LinkedApiError) {
console.error('🚨 Linked API Error:', error.message);
Expand Down
4 changes: 2 additions & 2 deletions examples/fetch-company.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ async function standardExample(linkedapi: LinkedApi): Promise<void> {
}

async function salesNavigatorExample(linkedapi: LinkedApi): Promise<void> {
const workflowId = await linkedapi.salesNavigatorFetchCompany.execute({
const workflowId = await linkedapi.nvFetchCompany.execute({
companyHashedUrl: 'https://www.linkedin.com/sales/company/1035',
retrieveEmployees: true,
retrieveDMs: true,
Expand All @@ -79,7 +79,7 @@ async function salesNavigatorExample(linkedapi: LinkedApi): Promise<void> {
});

console.log('🔍 Sales Navigator workflow started: ', workflowId);
const nvCompanyData = await linkedapi.salesNavigatorFetchCompany.result(workflowId);
const nvCompanyData = await linkedapi.nvFetchCompany.result(workflowId);
if (nvCompanyData.data) {
const nvCompany = nvCompanyData.data;
console.log('✅ Sales Navigator company page opened successfully');
Expand Down
4 changes: 2 additions & 2 deletions examples/fetch-person.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,9 +64,9 @@ async function salesNavigatorExample(linkedapi: LinkedApi): Promise<void> {
personHashedUrl: 'https://www.linkedin.com/in/abc123',
};

const workflowId = await linkedapi.salesNavigatorFetchPerson.execute(fetchParams);
const workflowId = await linkedapi.nvFetchPerson.execute(fetchParams);
console.log('🔍 Workflow started: ', workflowId);
const personResult = await linkedapi.salesNavigatorFetchPerson.result(workflowId);
const personResult = await linkedapi.nvFetchPerson.result(workflowId);
if (personResult.data) {
const person = personResult.data;
console.log('✅ Person page opened successfully');
Expand Down
25 changes: 11 additions & 14 deletions examples/messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ async function salesNavigatorSendMessage(linkedapi: LinkedApi, personUrl: string
subject: 'Let\'s connect!',
};

const workflowId = await linkedapi.salesNavigatorSendMessage.execute(nvMessageParams);
const workflowId = await linkedapi.nvSendMessage.execute(nvMessageParams);
console.log('🎯 Sales Navigator send message workflow started:', workflowId);

const nvMessageResult = await linkedapi.salesNavigatorSendMessage.result(workflowId);
const nvMessageResult = await linkedapi.nvSendMessage.result(workflowId);
if (nvMessageResult.errors.length > 0) {
console.error('🚨 Errors:', JSON.stringify(nvMessageResult.errors, null, 2));
} else {
Expand All @@ -100,10 +100,10 @@ async function salesNavigatorSyncConversation(linkedapi: LinkedApi, personUrl: s
personUrl: personUrl,
};

const workflowId = await linkedapi.salesNavigatorSyncConversation.execute(nvSyncParams);
const workflowId = await linkedapi.nvSyncConversation.execute(nvSyncParams);
console.log('🎯 Sales Navigator sync conversation workflow started:', workflowId);

const nvSyncResult = await linkedapi.salesNavigatorSyncConversation.result(workflowId);
const nvSyncResult = await linkedapi.nvSyncConversation.result(workflowId);
if (nvSyncResult.errors.length > 0) {
console.error('🚨 Errors:', JSON.stringify(nvSyncResult.errors, null, 2));
} else {
Expand All @@ -129,23 +129,20 @@ async function pollConversations(linkedapi: LinkedApi, standardPersonUrl: string
},
]);

if (!pollResponse.success) {
console.error('❌ Failed to poll conversations:', pollResponse.error?.message);
return;
}

console.log('✅ Conversations polled successfully');
console.log(`📊 Found ${pollResponse.result?.length || 0} conversations`);
console.log(`📊 Found ${pollResponse.data?.length || 0} conversations`);

pollResponse.result?.forEach((conversation, index) => {
console.log(`\n💬 Conversation ${index + 1}:`);
console.log(` 👤 Person: ${conversation.personUrl}`);
if (pollResponse.errors.length > 0) {
console.error('🚨 Errors:', JSON.stringify(pollResponse.errors, null, 2));
}
pollResponse.data?.forEach((conversation) => {
console.log(`\n💬 Conversation with ${conversation.personUrl}:`);
console.log(` 🔗 Type: ${conversation.type === 'st' ? 'Standard' : 'Sales Navigator'}`);
console.log(` 📬 Messages: ${conversation.messages.length}`);

if (conversation.messages.length > 0) {
console.log(' 📝 Recent messages:');
conversation.messages.slice(-3).forEach((message) => {
conversation.messages.slice(0, 5).forEach((message) => {
const senderIcon = message.sender === 'us' ? '👤' : '👋';
console.log(` ${senderIcon} ${message.sender.toUpperCase()}: "${message.text}"`);
console.log(` 🕐 ${message.time}`);
Expand Down
4 changes: 2 additions & 2 deletions examples/search-companies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ async function salesNavigatorExample(linkedapi: LinkedApi): Promise<void> {
};

console.log('\n🎯 Searching companies with Sales Navigator...');
const workflowId = await linkedapi.salesNavigatorSearchCompanies.execute(nvSearchParams);
const workflowId = await linkedapi.nvSearchCompanies.execute(nvSearchParams);
console.log('🔍 Sales Navigator workflow started:', workflowId);
const nvResults = await linkedapi.salesNavigatorSearchCompanies.result(workflowId);
const nvResults = await linkedapi.nvSearchCompanies.result(workflowId);

if (nvResults.data) {
const results = nvResults.data;
Expand Down
4 changes: 2 additions & 2 deletions examples/search-people.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ async function salesNavigatorExample(linkedapi: LinkedApi): Promise<void> {
};

console.log('\n🎯 Searching people with Sales Navigator...');
const workflowId = await linkedapi.salesNavigatorSearchPeople.execute(nvSearchParams);
const workflowId = await linkedapi.nvSearchPeople.execute(nvSearchParams);
console.log('🔍 Sales Navigator workflow started:', workflowId);
const nvResults = await linkedapi.salesNavigatorSearchPeople.result(workflowId);
const nvResults = await linkedapi.nvSearchPeople.result(workflowId);

if (nvResults.data) {
const results = nvResults.data;
Expand Down
14 changes: 5 additions & 9 deletions examples/statistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,22 +64,18 @@ async function getRecentUsageStats(linkedapi: LinkedApi): Promise<void> {
const endDate = new Date();
const startDate = new Date(endDate.getTime() - 7 * 24 * 60 * 60 * 1000);

const statsResponse = await linkedapi.getApiUsageStats({
const { data } = await linkedapi.getApiUsage({
start: startDate.toISOString(),
end: endDate.toISOString()
});

if (!statsResponse.success) {
console.error('❌ Failed to retrieve stats:', statsResponse.error?.message);
return;
}
console.log('✅ API Usage retrieved successfully');
console.log(`📈 Total actions executed: ${data?.length || 0}`);

console.log('✅ Usage statistics retrieved successfully');
console.log(`📈 Total actions executed: ${statsResponse.result?.length || 0}`);

if (statsResponse.result && statsResponse.result.length > 0) {
if (data && data.length > 0) {
console.log('\n📋 Recent actions:');
statsResponse.result.slice(-5).forEach((action) => {
data.slice(0, 10).forEach((action) => {
const status = action.success ? '✅' : '❌';
const date = new Date(action.time).toLocaleDateString();
const time = new Date(action.time).toLocaleTimeString();
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "linkedapi-node",
"version": "1.2.0",
"version": "1.2.1",
"description": "Official TypeScript SDK for Linked API",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
7 changes: 4 additions & 3 deletions src/core/linked-api-http-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,22 @@ import {
TLinkedApiResponse,
} from '../types';

export function buildLinkedApiHttpClient(config: TLinkedApiConfig): HttpClient {
return new LinkedApiHttpClient(config);
export function buildLinkedApiHttpClient(config: TLinkedApiConfig, client: string): HttpClient {
return new LinkedApiHttpClient(config, client);
}

class LinkedApiHttpClient extends HttpClient {
private readonly baseUrl: string;
private readonly headers: Record<string, string>;

constructor(config: TLinkedApiConfig) {
constructor(config: TLinkedApiConfig, client: string) {
super();
this.baseUrl = 'https://api.linkedapi.io';
this.headers = {
'Content-Type': 'application/json',
'linked-api-token': config.linkedApiToken,
'identification-token': config.identificationToken,
client: client,
};
}

Expand Down
83 changes: 24 additions & 59 deletions src/core/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,38 +5,37 @@ import {
LinkedApiWorkflowTimeoutError,
TLinkedApiErrorType,
TWorkflowCompletion,
TWorkflowDefinition,
TWorkflowResponse,
TWorkflowRunningStatus,
} from '../types';

import { pollWorkflowResult } from './poll-results';

export const OPERATION_NAME = {
fetchPerson: 'fetchPerson',
fetchCompany: 'fetchCompany',
salesNavigatorFetchCompany: 'salesNavigatorFetchCompany',
salesNavigatorFetchPerson: 'salesNavigatorFetchPerson',
fetchPost: 'fetchPost',
searchCompanies: 'searchCompanies',
salesNavigatorSearchCompanies: 'salesNavigatorSearchCompanies',
searchPeople: 'searchPeople',
salesNavigatorSearchPeople: 'salesNavigatorSearchPeople',
customWorkflow: 'customWorkflow',
sendMessage: 'sendMessage',
syncConversation: 'syncConversation',
salesNavigatorSendMessage: 'salesNavigatorSendMessage',
salesNavigatorSyncConversation: 'salesNavigatorSyncConversation',
sendConnectionRequest: 'sendConnectionRequest',
checkConnectionStatus: 'checkConnectionStatus',
sendConnectionRequest: 'sendConnectionRequest',
withdrawConnectionRequest: 'withdrawConnectionRequest',
retrievePendingRequests: 'retrievePendingRequests',
retrieveConnections: 'retrieveConnections',
removeConnection: 'removeConnection',
searchCompanies: 'searchCompanies',
searchPeople: 'searchPeople',
fetchPerson: 'fetchPerson',
fetchCompany: 'fetchCompany',
fetchPost: 'fetchPost',
reactToPost: 'reactToPost',
commentOnPost: 'commentOnPost',
retrieveSSI: 'retrieveSSI',
retrievePerformance: 'retrievePerformance',
customWorkflow: 'customWorkflow',
nvSendMessage: 'nvSendMessage',
nvSyncConversation: 'nvSyncConversation',
nvSearchCompanies: 'nvSearchCompanies',
nvSearchPeople: 'nvSearchPeople',
nvFetchCompany: 'nvFetchCompany',
nvFetchPerson: 'nvFetchPerson',
} as const;
export type TOperationName = (typeof OPERATION_NAME)[keyof typeof OPERATION_NAME];

Expand All @@ -45,52 +44,15 @@ export interface WaitForCompletionOptions {
timeout?: number;
}

export abstract class PredefinedOperation<TParams, TResult> {
export abstract class Operation<TParams, TResult> {
protected abstract readonly operationName: TOperationName;
protected abstract readonly mapper: BaseMapper<TParams, TResult>;

private readonly operation: CustomWorkflowOperation;

constructor(httpClient: HttpClient) {
this.operation = new CustomWorkflowOperation(httpClient);
}
constructor(private readonly httpClient: HttpClient) {}

public async execute(params: TParams): Promise<string> {
const request = this.mapper.mapRequest(params);
return this.operation.execute(request);
}

public async result(
workflowId: string,
options: WaitForCompletionOptions = {},
): Promise<TMappedResponse<TResult>> {
try {
const rawResult = await this.operation.result(workflowId, options);
return this.mapper.mapResponse(rawResult);
} catch (error) {
if (error instanceof LinkedApiError && error.type === 'workflowTimeout') {
throw new LinkedApiWorkflowTimeoutError(workflowId, this.operationName);
}
throw error;
}
}

public async status(
workflowId: string,
): Promise<TWorkflowRunningStatus | TMappedResponse<TResult>> {
const result = await this.operation.status(workflowId);
if (result === 'running') {
return result;
}
return this.mapper.mapResponse(result);
}
}

export class CustomWorkflowOperation {
constructor(private readonly httpClient: HttpClient) {}

public async execute(params: TWorkflowDefinition): Promise<string> {
const response = await this.httpClient.post<TWorkflowResponse>(`/workflows`, params);
const response = await this.httpClient.post<TWorkflowResponse>(`/workflows`, request);
if (response.error) {
throw new LinkedApiError(response.error.type as TLinkedApiErrorType, response.error.message);
}
Expand All @@ -103,23 +65,26 @@ export class CustomWorkflowOperation {
public async result(
workflowId: string,
options: WaitForCompletionOptions = {},
): Promise<TWorkflowCompletion> {
): Promise<TMappedResponse<TResult>> {
try {
return pollWorkflowResult(() => this.status(workflowId), options);
return pollWorkflowResult<TMappedResponse<TResult>>(() => this.status(workflowId), options);
} catch (error) {
if (error instanceof LinkedApiError && error.type === 'workflowTimeout') {
throw new LinkedApiWorkflowTimeoutError(workflowId, 'customWorkflow');
throw new LinkedApiWorkflowTimeoutError(workflowId, this.operationName);
}
throw error;
}
}

public async status(workflowId: string): Promise<TWorkflowRunningStatus | TWorkflowCompletion> {
public async status(
workflowId: string,
): Promise<TWorkflowRunningStatus | TMappedResponse<TResult>> {
const workflowResult = await this.getWorkflowResult(workflowId);
if (workflowResult.workflowStatus === 'running') {
return workflowResult.workflowStatus;
}
return this.getCompletion(workflowResult);
const result = this.getCompletion(workflowResult);
return this.mapper.mapResponse(result);
}

private async getWorkflowResult(workflowId: string): Promise<TWorkflowResponse> {
Expand Down
8 changes: 4 additions & 4 deletions src/core/poll-results.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { LinkedApiError, TWorkflowCompletion, TWorkflowRunningStatus } from '../types';
import { LinkedApiError, TWorkflowRunningStatus } from '../types';

import { WaitForCompletionOptions } from './operation';

export async function pollWorkflowResult(
workflowResultFn: () => Promise<TWorkflowRunningStatus | TWorkflowCompletion>,
export async function pollWorkflowResult<TResult>(
workflowResultFn: () => Promise<TWorkflowRunningStatus | TResult>,
options: WaitForCompletionOptions,
): Promise<TWorkflowCompletion> {
): Promise<TResult> {
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

const { pollInterval = 5000, timeout = 24 * 60 * 60 * 1000 } = options;
Expand Down
Loading