aboutsummaryrefslogtreecommitdiffstats
path: root/packages/pipeline/src/data_sources/copper/index.ts
blob: 15df2fd7d053d758f8c984430df53c09c9960457 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
import { fetchAsync } from '@0x/utils';
import Bottleneck from 'bottleneck';

import {
    CopperActivityTypeCategory,
    CopperActivityTypeResponse,
    CopperCustomFieldResponse,
    CopperSearchResponse,
} from '../../parsers/copper';

const HTTP_OK_STATUS = 200;
const COPPER_URI = 'https://api.prosperworks.com/developer_api/v1';

const DEFAULT_PAGINATION_PARAMS = {
    page_size: 200,
    sort_by: 'date_modified',
    sort_direction: 'desc',
};

export type CopperSearchParams = CopperLeadSearchParams | CopperActivitySearchParams | CopperOpportunitySearchParams;
export interface CopperLeadSearchParams {
    page_number?: number;
}

export interface CopperActivitySearchParams {
    minimum_activity_date: number;
    page_number?: number;
}

export interface CopperOpportunitySearchParams {
    sort_by: string; // must override the default 'date_modified' for this endpoint
    page_number?: number;
}
export enum CopperEndpoint {
    Leads = '/leads/search',
    Opportunities = '/opportunities/search',
    Activities = '/activities/search',
}
const ONE_SECOND = 1000;

function httpErrorCheck(response: Response): void {
    if (response.status !== HTTP_OK_STATUS) {
        throw new Error(`HTTP error while scraping Copper: [${JSON.stringify(response)}]`);
    }
}
export class CopperSource {
    private readonly _accessToken: string;
    private readonly _userEmail: string;
    private readonly _defaultHeaders: any;
    private readonly _limiter: Bottleneck;

    constructor(maxConcurrentRequests: number, accessToken: string, userEmail: string) {
        this._accessToken = accessToken;
        this._userEmail = userEmail;
        this._defaultHeaders = {
            'Content-Type': 'application/json',
            'X-PW-AccessToken': this._accessToken,
            'X-PW-Application': 'developer_api',
            'X-PW-UserEmail': this._userEmail,
        };
        this._limiter = new Bottleneck({
            minTime: ONE_SECOND / maxConcurrentRequests,
            reservoir: 30,
            reservoirRefreshAmount: 30,
            reservoirRefreshInterval: maxConcurrentRequests,
        });
    }

    public async fetchNumberOfPagesAsync(endpoint: CopperEndpoint, searchParams?: CopperSearchParams): Promise<number> {
        const resp = await this._limiter.schedule(() =>
            fetchAsync(COPPER_URI + endpoint, {
                method: 'POST',
                body: JSON.stringify({ ...DEFAULT_PAGINATION_PARAMS, ...searchParams }),
                headers: this._defaultHeaders,
            }),
        );

        httpErrorCheck(resp);

        // total number of records that match the request parameters
        if (resp.headers.has('X-Pw-Total')) {
            const totalRecords: number = parseInt(resp.headers.get('X-Pw-Total') as string, 10); // tslint:disable-line:custom-no-magic-numbers
            return Math.ceil(totalRecords / DEFAULT_PAGINATION_PARAMS.page_size);
        } else {
            return 1;
        }
    }
    public async fetchSearchResultsAsync<T extends CopperSearchResponse>(
        endpoint: CopperEndpoint,
        searchParams?: CopperSearchParams,
    ): Promise<T[]> {
        const request = { ...DEFAULT_PAGINATION_PARAMS, ...searchParams };
        const response = await this._limiter.schedule(() =>
            fetchAsync(COPPER_URI + endpoint, {
                method: 'POST',
                body: JSON.stringify(request),
                headers: this._defaultHeaders,
            }),
        );
        httpErrorCheck(response);
        const json: T[] = await response.json();
        return json;
    }

    public async fetchActivityTypesAsync(): Promise<Map<CopperActivityTypeCategory, CopperActivityTypeResponse[]>> {
        const response = await this._limiter.schedule(() =>
            fetchAsync(`${COPPER_URI}/activity_types`, {
                method: 'GET',
                headers: this._defaultHeaders,
            }),
        );
        httpErrorCheck(response);
        return response.json();
    }

    public async fetchCustomFieldsAsync(): Promise<CopperCustomFieldResponse[]> {
        const response = await this._limiter.schedule(() =>
            fetchAsync(`${COPPER_URI}/custom_field_definitions`, {
                method: 'GET',
                headers: this._defaultHeaders,
            }),
        );
        httpErrorCheck(response);
        return response.json();
    }
}