generated from bscotch/typescript-template
-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Draft base BravoClient class, including a method for general Fa…
…vro API requests and a wrapper class for returned results.
- Loading branch information
1 parent
2f0ddae
commit 2d90d6b
Showing
5 changed files
with
155 additions
and
18 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ build | |
sandbox | ||
sand\ box | ||
debug.log | ||
.env |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1 @@ | ||
/** | ||
* @file This is the entrypoint for your project. | ||
* If used as a node module, when someone runs | ||
* `import stuff from 'your-module'` (typescript) | ||
* or `const stuff = require('your-module')` (javascript) | ||
* whatever is exported here is what they'll get. | ||
* For small projects you could put all your code right in this file. | ||
*/ | ||
|
||
export * from './lib/sample-module.js'; | ||
export default undefined; | ||
export * from './lib/BravoClient.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
import { assertBravoClaim, BravoError } from './errors.js'; | ||
import fetch, { Response } from 'node-fetch'; | ||
import { URL } from 'url'; | ||
|
||
type FavroApiMethod = 'get' | 'post' | 'put' | 'delete'; | ||
|
||
export class FavroResponse { | ||
private _response: Response; | ||
|
||
constructor(response: Response) { | ||
this._response = response; | ||
} | ||
|
||
get status() { | ||
return this._response.status; | ||
} | ||
|
||
get succeeded() { | ||
return this.status <= 399 && this.status >= 200; | ||
} | ||
|
||
get failed() { | ||
return this.succeeded; | ||
} | ||
|
||
get requestsRemaining() { | ||
return Number(this._response.headers.get('X-RateLimit-Remaining')); | ||
} | ||
|
||
get limitResetsAt() { | ||
return new Date(this._response.headers.get('X-RateLimit-Reset')!); | ||
} | ||
} | ||
|
||
export class BravoClient { | ||
static readonly baseUrl = 'https://favro.com/api/v1'; | ||
|
||
private _token!: string; | ||
private _organizationId!: string; | ||
/** | ||
* Authentication requires the user's identifer (their email address) | ||
*/ | ||
private _userEmail!: string; | ||
/** | ||
* The response header X-RateLimit-Remaining informs how many | ||
* requests we can make before being blocked. Use this to ensure | ||
* we don't frequently hit those! X-RateLimit-Reset is the time | ||
* when the limit will be reset. | ||
*/ | ||
private _requestsRemaining?: number; | ||
private _limitResetsAt?: Date; | ||
/** | ||
* Favro responses include the header X-Favro-Backend-Identifier, | ||
* which is used to route to the same server. Required for paging. | ||
*/ | ||
private _backendId?: string; | ||
|
||
constructor(options: { | ||
token?: string; | ||
organizationId?: string; | ||
userEmail?: string; | ||
}) { | ||
for (const [optionsName, envName] of [ | ||
['token', 'FAVRO_TOKEN'], | ||
['organizationId', 'FAVRO_ORGANIZATION_ID'], | ||
['userEmail', 'FAVRO_USER_EMAIL'], | ||
] as const) { | ||
const value = options?.[optionsName] || process.env[envName]; | ||
assertBravoClaim(value, `A Favro ${optionsName} is required.`); | ||
this[`_${optionsName}`] = value; | ||
} | ||
} | ||
|
||
private get authHeader() { | ||
const encodedCredentials = BravoClient.toBase64( | ||
`${this._userEmail}:${this._token}`, | ||
); | ||
return { | ||
Authorization: `Basic ${encodedCredentials}`, | ||
}; | ||
} | ||
|
||
/** | ||
* General API request function against Favro's HTTP API {@link https://favro.com/developer/}. | ||
* Defaults to a GET request. Default headers are automatically handled. | ||
* | ||
* @param url Relative to the base URL {@link https://favro.com/api/v1} | ||
*/ | ||
async request( | ||
url: string, | ||
options?: { | ||
method?: FavroApiMethod | Capitalize<FavroApiMethod>; | ||
query?: Record<string, string>; | ||
body?: any; | ||
headers?: Record<string, string>; | ||
/** | ||
* BravoClients use the last-received Backend ID by default, | ||
* but you can override this if necessary. | ||
*/ | ||
backendId?: string; | ||
}, | ||
) { | ||
const method = options?.method || 'get'; | ||
if (['get', 'delete'].includes(method) && options?.body) { | ||
throw new BravoError(`HTTP Bodies not allowed for ${method} method`); | ||
} | ||
// Ensure initial slash | ||
url = url.startsWith('/') ? url : `/${url}`; | ||
url = `${BravoClient.baseUrl}${url}`; | ||
const fullUrl = new URL(url); | ||
if (options?.query) { | ||
for (const param of Object.keys(options.query)) { | ||
fullUrl.searchParams.append(param, options.query[param]); | ||
} | ||
} | ||
const res = new FavroResponse( | ||
await fetch(fullUrl.toString(), { | ||
method, | ||
headers: { | ||
...options?.headers, | ||
...this.authHeader, | ||
'User-Agent': `BravoClient <https://github.com/bscotch/favro-sdk>`, | ||
organizationId: this._organizationId, | ||
'X-Favro-Backend-Identifier': options?.backendId || this._backendId!, | ||
}, | ||
body: options?.body, | ||
}), | ||
); | ||
this._limitResetsAt = res.limitResetsAt; | ||
this._requestsRemaining = res.requestsRemaining; | ||
return res; | ||
} | ||
|
||
static toBase64(string: string) { | ||
return Buffer.from(string).toString('base64'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
export class BravoError extends Error { | ||
constructor(message: string) { | ||
super(message); | ||
this.name = 'BravoError'; | ||
Error.captureStackTrace(this, this.constructor); | ||
} | ||
} | ||
|
||
export function assertBravoClaim( | ||
claim: any, | ||
message = 'Assertion failed', | ||
): asserts claim { | ||
if (!claim) { | ||
throw new BravoError(message); | ||
} | ||
} |
This file was deleted.
Oops, something went wrong.