1 | const _ = require('lodash')
|
2 | const Bottleneck = require('bottleneck')
|
3 | const request = require('request-promise')
|
4 | const EventEmitter = require('events').EventEmitter
|
5 |
|
6 | const Broadcast = require('./broadcast')
|
7 | const Campaign = require('./campaign')
|
8 | const Company = require('./company')
|
9 | const Contact = require('./contact')
|
10 | const CRM = require('./crm')
|
11 | const Page = require('./page')
|
12 | const Deal = require('./deal')
|
13 | const Engagement = require('./engagement')
|
14 | const Email = require('./emails')
|
15 | const File = require('./file')
|
16 | const Form = require('./form')
|
17 | const Integrations = require('./integrations')
|
18 | const List = require('./list')
|
19 | const Owner = require('./owner')
|
20 | const OAuth = require('./oauth')
|
21 | const Pipeline = require('./pipeline')
|
22 | const Subscription = require('./subscription')
|
23 | const Timeline = require('./timeline')
|
24 | const Workflow = require('./workflow')
|
25 |
|
26 | const debug = require('debug')('hubspot:client')
|
27 |
|
28 |
|
29 | const API_TIMEOUT = 15000
|
30 | const MAX_USE_PERCENT_DEFAULT = 90
|
31 |
|
32 | const getLimiter = (options) =>
|
33 | new Bottleneck(
|
34 | Object.assign(
|
35 | {
|
36 | maxConcurrent: 2,
|
37 | minTime: 1000 / 9,
|
38 | },
|
39 | options.limiter
|
40 | )
|
41 | )
|
42 |
|
43 | const setInstances = (client) => {
|
44 | client.broadcasts = new Broadcast(client)
|
45 | client.campaigns = new Campaign(client)
|
46 | client.companies = new Company(client)
|
47 | client.contacts = new Contact(client)
|
48 | client.pages = new Page(client)
|
49 | client.deals = new Deal(client)
|
50 | client.emails = new Email(client)
|
51 | client.engagements = new Engagement(client)
|
52 | client.files = new File(client)
|
53 | client.forms = new Form(client)
|
54 | client.integrations = new Integrations(client)
|
55 | client.lists = new List(client)
|
56 | client.oauth = new OAuth(client)
|
57 | client.owners = new Owner(client)
|
58 | client.pipelines = new Pipeline(client)
|
59 | client.timelines = new Timeline(client)
|
60 | client.subscriptions = new Subscription(client)
|
61 | client.workflows = new Workflow(client)
|
62 | client.crm = new CRM(client)
|
63 | }
|
64 |
|
65 | class Client extends EventEmitter {
|
66 | constructor(options = {}) {
|
67 | super()
|
68 | this.qs = {}
|
69 | this.auth = undefined
|
70 | this.setAuth(options)
|
71 | this.setOAuth(options)
|
72 | this.maxUsePercent = typeof options.maxUsePercent !== 'undefined' ? options.maxUsePercent : MAX_USE_PERCENT_DEFAULT
|
73 | this.baseUrl = options.baseUrl || 'https://api.hubapi.com'
|
74 | this.apiTimeout = options.timeout || API_TIMEOUT
|
75 | this.apiCalls = 0
|
76 | this.on('apiCall', (params) => {
|
77 | debug('apiCall', _.pick(params, ['method', 'url']))
|
78 | this.apiCalls += 1
|
79 | })
|
80 | this.checkLimit = options.checkLimit !== undefined ? options.checkLimit : true
|
81 | this.limiter = getLimiter(options)
|
82 | setInstances(this)
|
83 | }
|
84 |
|
85 | requestStats() {
|
86 | return {
|
87 | running: this.limiter.running(),
|
88 | queued: this.limiter.queued(),
|
89 | }
|
90 | }
|
91 |
|
92 | setAccessToken(accessToken, expiresIn = 0, updatedAt = 0) {
|
93 | this.accessToken = accessToken
|
94 | this.accessTokenExpiresIn = expiresIn
|
95 |
|
96 | this.accessTokenUpdatedAt = updatedAt !== 0 ? updatedAt : Math.floor(Date.now() / 1000)
|
97 | this.auth = { bearer: accessToken }
|
98 | }
|
99 |
|
100 | refreshAccessToken() {
|
101 | return this.oauth.refreshAccessToken()
|
102 | }
|
103 |
|
104 | setOAuth(options = {}) {
|
105 | this.clientId = options.clientId
|
106 | this.clientSecret = options.clientSecret
|
107 | this.redirectUri = options.redirectUri
|
108 | this.refreshToken = options.refreshToken
|
109 | }
|
110 |
|
111 | setAuth(options = {}) {
|
112 | if (options.apiKey) {
|
113 | this.qs.hapikey = options.apiKey
|
114 | } else if (options.accessToken) {
|
115 | if (options.useOAuth1) {
|
116 | this.qs.access_token = options.accessToken
|
117 |
|
118 |
|
119 | } else {
|
120 | const currentTimestampInSeconds = Math.floor(Date.now() / 1000)
|
121 | const updatedAtTimestamp = _.get(options, 'updatedAtTimestamp') || currentTimestampInSeconds
|
122 | const expiresIn = _.get(options, 'expiresIn') || 21600
|
123 | this.setAccessToken(options.accessToken, expiresIn, updatedAtTimestamp)
|
124 | }
|
125 | }
|
126 | }
|
127 |
|
128 | _request(opts) {
|
129 | const params = _.cloneDeep(opts)
|
130 | if (this.auth) {
|
131 | params.auth = this.auth
|
132 | }
|
133 | params.json = true
|
134 | params.resolveWithFullResponse = true
|
135 |
|
136 | params.url = this.baseUrl + params.path
|
137 | delete params.path
|
138 | params.qs = Object.assign({}, this.qs, params.qs)
|
139 |
|
140 | params.qsStringifyOptions = {
|
141 | arrayFormat: 'repeat',
|
142 | }
|
143 |
|
144 | params.timeout = this.apiTimeout
|
145 |
|
146 | return this.checkApiLimit(params).then(() => {
|
147 | this.emit('apiCall', params)
|
148 | return this.limiter.schedule(() =>
|
149 | request(params)
|
150 | .then((res) => {
|
151 | this.updateApiLimit(res)
|
152 | return res
|
153 | })
|
154 | .then((res) => res.body)
|
155 | )
|
156 | })
|
157 | }
|
158 |
|
159 | updateApiLimit(res) {
|
160 | const { headers } = res
|
161 | if (this.usageLimit === undefined) {
|
162 | this.usageLimit = headers['x-hubspot-ratelimit-daily']
|
163 | }
|
164 | if (this.usageLimit !== undefined) {
|
165 | this.currentUsage = this.usageLimit - headers['x-hubspot-ratelimit-daily-remaining']
|
166 | }
|
167 | return Promise.resolve()
|
168 | }
|
169 |
|
170 | checkApiLimit(params) {
|
171 | return new Promise((resolve, reject) => {
|
172 |
|
173 | if (this.auth) return resolve()
|
174 |
|
175 | if (/integrations\/v1\/limit|oauth/.test(params.url)) return resolve()
|
176 |
|
177 | if (!this.checkLimit) return resolve()
|
178 |
|
179 | if (this.maxUsePercent === 0) return resolve()
|
180 |
|
181 | if (this.currentUsage !== undefined) {
|
182 | const usagePercent = (100.0 * this.currentUsage) / this.usageLimit
|
183 | debug('usagePercent', usagePercent, 'apiCalls', this.apiCalls)
|
184 | if (usagePercent > this.maxUsePercent) {
|
185 | const err = new Error('Too close to the API limit')
|
186 | err.usageLimit = this.usageLimit
|
187 | err.currentUsage = this.currentUsage
|
188 | err.usagePercent = usagePercent
|
189 | reject(err)
|
190 | }
|
191 | }
|
192 | resolve()
|
193 | })
|
194 | }
|
195 |
|
196 | getApiLimit() {
|
197 | this.limit = this.limit || {}
|
198 | const collectedAt = this.limit.collectedAt || 0
|
199 | const recencyMinutes = (Date.now() - collectedAt) / (60 * 1000)
|
200 | debug('recencyMinutes', recencyMinutes)
|
201 | if (recencyMinutes < 5) {
|
202 | return Promise.resolve(this.limit)
|
203 | }
|
204 | return this._request({
|
205 | method: 'GET',
|
206 | path: '/integrations/v1/limit/daily',
|
207 | }).then((results) => {
|
208 | this.limit = results.filter((r) => r.name === 'api-calls-daily')[0]
|
209 | return this.limit
|
210 | })
|
211 | }
|
212 | }
|
213 |
|
214 | module.exports = Client
|
215 |
|
216 |
|
217 | module.exports.default = Client
|