Skip to content

Commit ee383b7

Browse files
committed
feat(error): add typed errors
1 parent f36d3df commit ee383b7

File tree

2 files changed

+125
-46
lines changed

2 files changed

+125
-46
lines changed

src/index.ts

Lines changed: 85 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,56 @@ class Token {
5353
}
5454
}
5555

56+
/**
57+
* Invalid client configuration
58+
*/
59+
class ConfigError extends Error {
60+
constructor(message: string) {
61+
super(message)
62+
this.name = 'ConfigError'
63+
}
64+
}
65+
66+
/**
67+
* Invalid input data
68+
*/
69+
class InputError extends Error {
70+
constructor(message: string) {
71+
super(message)
72+
this.name = 'InputError'
73+
}
74+
}
75+
76+
/**
77+
* Authentication error
78+
*/
79+
class AuthError extends Error {
80+
constructor(message: string) {
81+
super(message)
82+
this.name = 'AuthError'
83+
}
84+
}
85+
86+
/**
87+
* HTTP error (syntax issue or incorrect parameter)
88+
*/
89+
class RequestError extends Error {
90+
constructor(message: string) {
91+
super(message)
92+
this.name = 'RequestError'
93+
}
94+
}
95+
96+
/**
97+
* Network error
98+
*/
99+
class NetworkError extends Error {
100+
constructor(message: string) {
101+
super(message)
102+
this.name = 'NetworkError'
103+
}
104+
}
105+
56106
class NetatmoClient {
57107
clientId: string
58108
clientSecret: string
@@ -76,7 +126,7 @@ class NetatmoClient {
76126
*/
77127
constructor(clientId: string, clientSecret: string, scope: string, requestConfig: AxiosRequestConfig = {}) {
78128
if (!clientId || !clientSecret) {
79-
throw new Error('Client id and client secret must be provided, see https://dev.netatmo.com/apidocumentation')
129+
throw new ConfigError('Client id and client secret must be provided, see https://dev.netatmo.com/apidocumentation')
80130
}
81131
this.clientId = clientId
82132
this.clientSecret = clientSecret
@@ -110,7 +160,7 @@ class NetatmoClient {
110160
if (refreshToken) {
111161
return this.authenticateByRefreshToken(refreshToken)
112162
}
113-
throw new Error('Refresh token is missing')
163+
throw new InputError('Refresh token is missing')
114164
}
115165

116166
/**
@@ -125,7 +175,7 @@ class NetatmoClient {
125175
this.redirectUrl = redirectUrl
126176
}
127177
if (!this.redirectUrl) {
128-
throw new Error('Redirect url must be provided')
178+
throw new InputError('Redirect url must be provided')
129179
}
130180
this.state = statePrefix + Math.random() * 10000000000000000
131181
const query = querystring.stringify({
@@ -147,10 +197,10 @@ class NetatmoClient {
147197
*/
148198
async authenticateByAuthorizationCode(authorizationCode: string, redirectUrl: string, state: string): Promise<Token> {
149199
if (!authorizationCode || !redirectUrl) {
150-
throw new Error('Authorization code and redirect url must be provided')
200+
throw new InputError('Authorization code and redirect url must be provided')
151201
}
152202
if (this.state !== state) {
153-
throw new Error('State is not identical as the one provided during authorize url request')
203+
throw new InputError('State is not identical as the one provided during authorize url request')
154204
}
155205
this.authorizationCode = authorizationCode
156206
this.redirectUrl = redirectUrl
@@ -173,7 +223,7 @@ class NetatmoClient {
173223
*/
174224
async authenticateByRefreshToken(refreshToken: string): Promise<Token> {
175225
if (!refreshToken) {
176-
throw new Error('Refresh token must be provided')
226+
throw new InputError('Refresh token must be provided')
177227
}
178228
this.refreshToken = refreshToken
179229
const authentication: NetatmoToken = await this.request(HTTP_POST, PATH_AUTH, null, {
@@ -193,7 +243,7 @@ class NetatmoClient {
193243
*/
194244
setToken(netatmoAuthentication: NetatmoToken): Token {
195245
if (!netatmoAuthentication.access_token || !netatmoAuthentication.refresh_token || !netatmoAuthentication.expires_in) {
196-
throw new Error('Invalid Netatmo token')
246+
throw new InputError('Invalid Netatmo token')
197247
}
198248
this.accessToken = netatmoAuthentication.access_token
199249
this.refreshToken = netatmoAuthentication.refresh_token
@@ -247,7 +297,7 @@ class NetatmoClient {
247297

248298
if (path !== PATH_AUTH) {
249299
if (!this.accessToken) {
250-
throw new Error('Access token must be provided')
300+
throw new AuthError('Access token must be provided')
251301
}
252302
config.headers!.Authorization = `Bearer ${this.accessToken}`
253303
}
@@ -265,23 +315,26 @@ class NetatmoClient {
265315
return await this.request(method, path, params, data, true)
266316
}
267317
if (error.response.data.error_description) {
268-
// bad request error
269-
throw new Error(`HTTP request ${path} failed: ${error.response.data.error_description} (${error.response.status})`)
318+
// bad request error (Oauth2 RFC syntax)
319+
throw new AuthError(`HTTP request ${path} failed: ${error.response.data.error_description} (${error.response.status})`)
270320
}
271321
if (error.response.data.error && error.response.data.error.message) {
272322
// standard error
273-
throw new Error(`HTTP request ${path} failed: ${error.response.data.error.message} (${error.response.status})`)
323+
throw new RequestError(`HTTP request ${path} failed: ${error.response.data.error.message} (${error.response.status})`)
274324
}
275325
if (error.response.data.error) {
276326
// other error
277-
throw new Error(`HTTP request ${path} failed: ${JSON.stringify(error.response.data.error)} (${error.response.status})`)
327+
throw new RequestError(`HTTP request ${path} failed: ${JSON.stringify(error.response.data.error)} (${error.response.status})`)
278328
}
279329
}
330+
if (error.code && ['ENOTFOUND', 'ECONNRESET', 'ECONNREFUSED', 'ECONNABORTED'].includes(error.code)) {
331+
throw new NetworkError(`HTTP request ${path} failed: ${error.message}`)
332+
}
280333
}
281334
if (error instanceof Error) {
282-
throw new Error(`HTTP request ${path} failed: ${error.message}`)
335+
throw new RequestError(`HTTP request ${path} failed: ${error.message}`)
283336
}
284-
throw new Error(`HTTP request ${path} failed: ${error}`)
337+
throw new RequestError(`HTTP request ${path} failed: ${error}`)
285338
}
286339
}
287340

@@ -309,7 +362,7 @@ class NetatmoClient {
309362
*/
310363
async getEventsUntil(homeId: string, eventId: string): Promise<EventsList> {
311364
if (!homeId || !eventId) {
312-
throw new Error('Home id and event id must be provided')
365+
throw new InputError('Home id and event id must be provided')
313366
}
314367
const params = {
315368
home_id: homeId,
@@ -328,7 +381,7 @@ class NetatmoClient {
328381
*/
329382
async getLastEventOf(homeId: string, personId: string, offset: number): Promise<EventsList> {
330383
if (!homeId || !personId) {
331-
throw new Error('Home id and person id must be provided')
384+
throw new InputError('Home id and person id must be provided')
332385
}
333386
const params = {
334387
home_id: homeId,
@@ -348,7 +401,7 @@ class NetatmoClient {
348401
*/
349402
async getNextEvents(homeId: string, eventId: string, size: number): Promise<EventsList> {
350403
if (!homeId || !eventId) {
351-
throw new Error('Home id and event id must be provided')
404+
throw new InputError('Home id and event id must be provided')
352405
}
353406
const params = {
354407
home_id: homeId,
@@ -367,7 +420,7 @@ class NetatmoClient {
367420
*/
368421
async getCameraPicture(imageId: string, key: string): Promise<CameraImage> {
369422
if (!imageId || !key) {
370-
throw new Error('Image id and key must be provided')
423+
throw new InputError('Image id and key must be provided')
371424
}
372425
const params = {
373426
image_id: imageId,
@@ -389,7 +442,7 @@ class NetatmoClient {
389442
*/
390443
async getPublicData(latNE: number, lonNE: number, latSW: number, lonSW: number, requiredData: string, filter = false): Promise<PublicData[]> {
391444
if (!latNE || !lonNE || !latSW || !lonSW) {
392-
throw new Error('Latitude and Longitude must be provided')
445+
throw new InputError('Latitude and Longitude must be provided')
393446
}
394447
const params = {
395448
lat_ne: latNE,
@@ -433,7 +486,7 @@ class NetatmoClient {
433486
*/
434487
async getMeasure(deviceId: string, moduleId: string, scale: string, type: string, dateBegin: number, dateEnd: number, limit: number, optimize: boolean, realTime: boolean): Promise<Measure[]> {
435488
if (!deviceId || !scale || !type) {
436-
throw new Error('Device id, scale and type must be provided')
489+
throw new InputError('Device id, scale and type must be provided')
437490
}
438491
const params = {
439492
device_id: deviceId,
@@ -450,4 +503,15 @@ class NetatmoClient {
450503
}
451504
}
452505

453-
export { NetatmoClient, SCOPE_BASIC_CAMERA, SCOPE_FULL_CAMERA, SCOPE_FULL, SCOPE_BASIC_WEATHER }
506+
export {
507+
NetatmoClient,
508+
ConfigError,
509+
InputError,
510+
AuthError,
511+
RequestError,
512+
NetworkError,
513+
SCOPE_BASIC_CAMERA,
514+
SCOPE_FULL_CAMERA,
515+
SCOPE_FULL,
516+
SCOPE_BASIC_WEATHER,
517+
}

0 commit comments

Comments
 (0)