UNPKG

6.76 kBJavaScriptView Raw
1const _ = require('lodash')
2const Bottleneck = require('bottleneck')
3const request = require('request-promise')
4const EventEmitter = require('events').EventEmitter
5
6const Broadcast = require('./broadcast')
7const Campaign = require('./campaign')
8const Company = require('./company')
9const Contact = require('./contact')
10const CRM = require('./crm')
11const Page = require('./page')
12const Deal = require('./deal')
13const Engagement = require('./engagement')
14const Email = require('./emails')
15const File = require('./file')
16const Form = require('./form')
17const Integrations = require('./integrations')
18const List = require('./list')
19const Owner = require('./owner')
20const OAuth = require('./oauth')
21const Pipeline = require('./pipeline')
22const Subscription = require('./subscription')
23const Timeline = require('./timeline')
24const Workflow = require('./workflow')
25
26const debug = require('debug')('hubspot:client')
27
28// define how long to wait API response before throwing a timeout error
29const API_TIMEOUT = 15000
30const MAX_USE_PERCENT_DEFAULT = 90
31
32const getLimiter = (options) =>
33 new Bottleneck(
34 Object.assign(
35 {
36 maxConcurrent: 2,
37 minTime: 1000 / 9,
38 },
39 options.limiter
40 )
41 )
42
43const 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
65class 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 // current timestamp in seconds
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 // defaults to OAuth2
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 ) // limit the number of concurrent requests
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 // don't check the api limit for the api call
173 if (this.auth) return resolve()
174 // don't check the api limit for the api call
175 if (/integrations\/v1\/limit|oauth/.test(params.url)) return resolve()
176 // don't check the api limit for the api call
177 if (!this.checkLimit) return resolve()
178 // if maxUsePercent set to 0, do not check for the API limit (use at your own risk)
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
214module.exports = Client
215
216// Allow use of default import syntax in TypeScript
217module.exports.default = Client