Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(js client): improve things (one axios client, keepAlive, baseUrl… #47

Merged
merged 3 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 68 additions & 60 deletions scheduler/clients/javascript/lib/baseClient.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
import axios, { type AxiosInstance, type AxiosResponse } from 'axios'
import { config } from '.'
import axios, {
AxiosRequestConfig,
type AxiosInstance,
type AxiosResponse,
} from 'axios'
import { ICredentials } from './helpers/credentials'

/**
* Base client for the API
* This client is used to centralize configuration needed for calling the API
* It shouldn't be exposed to the end user
*/
export abstract class NettuBaseClient {
private readonly credentials: ICredentials
private readonly axiosClient: AxiosInstance

constructor(credentials: ICredentials) {
this.credentials = credentials
this.axiosClient = axios.create({
headers: this.credentials.createAuthHeaders(),
validateStatus: () => true, // allow all status codes without throwing error
paramsSerializer: {
indexes: null, // Force to stringify arrays like value1,value2 instead of value1[0],value1[1]
},
})
constructor(private readonly axiosClient: AxiosInstance) {
this.axiosClient = axiosClient
}

protected async get<T>(
path: string,
params: Record<string, unknown> = {}
): Promise<APIResponse<T>> {
const res = await this.axiosClient.get(`${config.baseUrl}${path}`, {
const res = await this.axiosClient.get(path, {
params,
})
return new APIResponse(res)
}

protected async delete<T>(path: string): Promise<APIResponse<T>> {
const res = await this.axiosClient.delete(`${config.baseUrl}${path}`)
const res = await this.axiosClient.delete(path)
return new APIResponse(res)
}

Expand All @@ -43,7 +37,7 @@ export abstract class NettuBaseClient {
const res = await this.axiosClient({
method: 'DELETE',
data,
url: `${config.baseUrl}${path}`,
url: path,
})
return new APIResponse(res)
}
Expand All @@ -52,12 +46,12 @@ export abstract class NettuBaseClient {
path: string,
data: unknown
): Promise<APIResponse<T>> {
const res = await this.axiosClient.post(`${config.baseUrl}${path}`, data)
const res = await this.axiosClient.post(path, data)
return new APIResponse(res)
}

protected async put<T>(path: string, data: unknown): Promise<APIResponse<T>> {
const res = await this.axiosClient.put(`${config.baseUrl}${path}`, data)
const res = await this.axiosClient.put(path, data)
return new APIResponse(res)
}
}
Expand All @@ -78,56 +72,70 @@ export class APIResponse<T> {
}

/**
* Credentials for the API for end users (usually frontend)
* Create an Axios instance for the frontend
*
* Compared to the backend, this function is not async
* And the frontend cannot keep the connection alive
*
* @param args specify base URL for the API
* @param credentials credentials for the API
* @returns an Axios instance
*/
export class UserCreds implements ICredentials {
private readonly nettuAccount: string
private readonly token?: string

constructor(nettuAccount: string, token?: string) {
this.nettuAccount = nettuAccount
this.token = token
export const createAxiosInstanceFrontend = (

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you document? What's the difference between this and createAxiosInstanceBackend?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated 👍

The main difference is that one allow keepAlive, which needs the function to be async as it needs to import a Node package.

Technically, it could be only one function, but I think it makes it easier for the frontend to still have a function that isn't async. That way, it can be defined directly as a variable in a file (export ....) on the frontend

args: {
baseUrl: string
},
credentials: ICredentials
): AxiosInstance => {
const config: AxiosRequestConfig = {
baseURL: args.baseUrl,
headers: credentials.createAuthHeaders(),
validateStatus: () => true, // allow all status codes without throwing error
paramsSerializer: {
indexes: null, // Force to stringify arrays like value1,value2 instead of value1[0],value1[1]
},
}

createAuthHeaders() {
const creds: Record<string, string> = {
'nettu-account': this.nettuAccount,
}
if (this.token) {
creds.authorization = `Bearer ${this.token}`
}

return Object.freeze(creds)
}
return axios.create(config)
}

/**
* Credentials for the API for admins (usually backend)
* Create an Axios instance for the backend
*
* On the backend (NodeJS), it is possible to keep the connection alive
* @param args specify base URL and if the connection should be kept alive
* @param credentials credentials for the API
* @returns Promise of an Axios instance
*/
export class AccountCreds implements ICredentials {
private readonly apiKey: string

constructor(apiKey: string) {
this.apiKey = apiKey
export const createAxiosInstanceBackend = async (
args: {
baseUrl: string
keepAlive: boolean
},
credentials: ICredentials
): Promise<AxiosInstance> => {
const config: AxiosRequestConfig = {
baseURL: args.baseUrl,
headers: credentials.createAuthHeaders(),
validateStatus: () => true, // allow all status codes without throwing error
paramsSerializer: {
indexes: null, // Force to stringify arrays like value1,value2 instead of value1[0],value1[1]
},
}

createAuthHeaders() {
return Object.freeze({
'x-api-key': this.apiKey,
})
}
}

export interface ICredentials {
createAuthHeaders(): object
}

export class EmptyCreds implements ICredentials {
createAuthHeaders() {
return Object.freeze({})
// If keepAlive is true, and if we are in NodeJS
// create an agent to keep the connection alive
if (args.keepAlive && typeof module !== 'undefined' && module.exports) {
if (args.baseUrl.startsWith('https')) {
// This is a dynamic import to avoid loading the https module in the browser
const https = await import('https')
config.httpsAgent = new https.Agent({ keepAlive: true })
} else {
// This is a dynamic import to avoid loading the http module in the browser
const http = await import('http')
config.httpAgent = new http.Agent({ keepAlive: true })
}
}
}

export interface ICredentials {
createAuthHeaders(): object
return axios.create(config)
}
88 changes: 88 additions & 0 deletions scheduler/clients/javascript/lib/helpers/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/**
* Partial credentials to be used for the client
*/
export type PartialCredentials = {
/**
* API key (admin)
*/
apiKey?: string
/**
* Nettu account id (admin)
*/
nettuAccount?: string

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nettu or Nettei?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the moment nettu is still used everywhere
I think I'll just make one big PR renaming everything to nittei

/**
* Token (user)
*/
token?: string
}

/**
* Create credentials for the client (admin or user)
* @param creds partial credentials
* @returns credentials
*/
export const createCreds = (creds?: PartialCredentials): ICredentials => {
if (creds?.apiKey) {
return new AccountCreds(creds.apiKey)
}
if (creds?.nettuAccount) {
return new UserCreds(creds?.nettuAccount, creds?.token)
}
// throw new Error("No api key or nettu account provided to nettu client.");
return new EmptyCreds()
}

/**
* Credentials for the API for end users (usually frontend)
*/
export class UserCreds implements ICredentials {
private readonly nettuAccount: string
private readonly token?: string

constructor(nettuAccount: string, token?: string) {
this.nettuAccount = nettuAccount
this.token = token
}

createAuthHeaders() {
const creds: Record<string, string> = {
'nettu-account': this.nettuAccount,
}
if (this.token) {
creds.authorization = `Bearer ${this.token}`
}

return Object.freeze(creds)
}
}

/**
* Credentials for the API for admins (usually backend)
*/
export class AccountCreds implements ICredentials {
private readonly apiKey: string

constructor(apiKey: string) {
this.apiKey = apiKey
}

createAuthHeaders() {
return Object.freeze({
'x-api-key': this.apiKey,
})
}
}

export interface ICredentials {
createAuthHeaders(): object
}

export class EmptyCreds implements ICredentials {
createAuthHeaders() {
return Object.freeze({})
}
}

export interface ICredentials {
createAuthHeaders(): object
}
Loading