UNPKG

@cityssm/avanti-api

Version:

An unofficial wrapper around the Avanti API.

297 lines (257 loc) 9.27 kB
import Debug from 'debug' import { DEBUG_NAMESPACE } from './debug.config.js' import type { AccessTokenResponse, AvantiApiConfiguration, AvantiApiEndpoint, AvantiApiError, AvantiApiOptions, AvantiApiResponse, GetEmployeeEarningCodesEarningCode, GetEmployeeJobDataResponse, GetEmployeePersonalInfoResponse, GetEmployeesRequest, GetEmployeesResponse, GetTimeEntriesRequest, GetTimeEntriesTimeEntry, GetTimeEntryTemplatesRequest, GetTimeEntryTemplatesTimeEntryTemplate } from './types.js' import { objectToUrlSearchParameters } from './utilities.js' const debug = Debug(`${DEBUG_NAMESPACE}:index`) export const _defaultLatestASSP = false export class AvantiApi { readonly #apiConfiguration: AvantiApiConfiguration readonly #accessTokenUrl: string #accessToken: AccessTokenResponse | undefined #accessTokenTimeMillis = 0 constructor(configuration: AvantiApiConfiguration) { this.#apiConfiguration = configuration this.#accessTokenUrl = (this.#apiConfiguration.latestASSP ?? _defaultLatestASSP) ? 'https://auth.myavanti.ca/connect/token' : `https://myavanti.ca/${this.#apiConfiguration.tenant}-api/connect/token` } #getEndpointUrl(apiEndpoint: AvantiApiEndpoint): string { return (this.#apiConfiguration.latestASSP ?? _defaultLatestASSP) ? `https://${this.#apiConfiguration.tenant}.myavanti.ca/api${apiEndpoint}` : `https://myavanti.ca/${this.#apiConfiguration.tenant}-api${apiEndpoint}` } async #refreshAccessToken(): Promise<AccessTokenResponse> { this.#accessTokenTimeMillis = Date.now() const requestObject = Object.assign( { grant_type: 'password', device_id: this.#apiConfiguration.device_id ?? `node-avanti-api-${Date.now().toString()}` }, { client_id: this.#apiConfiguration.client_id, client_secret: this.#apiConfiguration.client_secret, username: this.#apiConfiguration.username, password: this.#apiConfiguration.password, company: this.#apiConfiguration.company } ) const request = objectToUrlSearchParameters( requestObject as unknown as Record<string, string> ) debug(`Access token request: ${this.#accessTokenUrl}`) const response = await fetch(this.#accessTokenUrl, { method: 'post', headers: { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded', 'x-tenant': this.#apiConfiguration.tenant }, body: request }) this.#accessToken = (await response.json()) as AccessTokenResponse if ((this.#accessToken?.access_token ?? '') === '') { debug('Error retrieving access token.') } debug(`Access Token: ${this.#accessToken.access_token}`) return this.#accessToken } /** * Requests an API endpoint excluded from the included functions. * @param apiEndpoint - ex. '/v1/Employees' * @param apiOptions - API Options * @returns API Response */ async callApi<T>( apiEndpoint: AvantiApiEndpoint, apiOptions: AvantiApiOptions ): Promise<AvantiApiResponse<T>> { // eslint-disable-next-line @typescript-eslint/naming-convention let access_token = this.#accessToken?.access_token ?? '' if ( access_token === '' || this.#accessTokenTimeMillis + (this.#accessToken?.expires_in ?? 0) <= Date.now() ) { const accessTokenResponse = await this.#refreshAccessToken() access_token = accessTokenResponse.access_token } let requestUrl = this.#getEndpointUrl(apiEndpoint) debug(`API request: ${requestUrl}`) if (apiOptions.method === 'get') { requestUrl += `?${objectToUrlSearchParameters( apiOptions.getParameters ?? {} ).toString()}` } const fetchOptions: RequestInit = { method: apiOptions.method, headers: { Authorization: `Bearer ${access_token}` } } if (apiOptions.method === 'post') { fetchOptions.body = JSON.stringify(apiOptions.bodyParameters) ;(fetchOptions.headers as HeadersInit)['Content-Type'] = 'application/json' } const response = await fetch(requestUrl, fetchOptions) // eslint-disable-next-line @typescript-eslint/init-declarations let parsingError: Error | undefined try { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment const json = (await response.json()) ?? {} if ( typeof json === 'object' && (Object.hasOwn(json as object, 'status') || Object.hasOwn(json as object, 'instance')) ) { return { success: false, error: json as AvantiApiError } } return { success: true, response: json } } catch (error) { debug(response) parsingError = error as Error } return { success: false, error: { title: 'callApi() error', status: 600, detail: parsingError === undefined ? undefined : parsingError.name, error: parsingError, instance: apiEndpoint } } } /** * List Employees: * /v1/Employees * @param parameters - Request parameters * @returns See https://avanti.stoplight.io/docs/avanti-api/62932b8f232fb-list-employees */ async getEmployees( parameters: GetEmployeesRequest ): Promise<AvantiApiResponse<GetEmployeesResponse>> { return (await this.callApi('/v1/Employees', { bodyParameters: parameters, method: 'post' })) as AvantiApiResponse<GetEmployeesResponse> } /** * List Employee Earning Codes: * /v1/EmployeeEarningCodes * @param employeeNumber - Employee number * @returns See https://avanti.stoplight.io/docs/avanti-api/204d3078d2230-list-employee-earning-codes */ async getEmployeeEarningCodes( employeeNumber?: string ): Promise<AvantiApiResponse<GetEmployeeEarningCodesEarningCode[]>> { return (await this.callApi('/v1/EmployeeEarningCodes', { getParameters: { empNo: employeeNumber }, method: 'get' })) as AvantiApiResponse<GetEmployeeEarningCodesEarningCode[]> } /** * Get Employee Job Data: * /v1/EmployeeJobData * @param employeeNumber - Employee number * @returns See https://avanti.stoplight.io/docs/avanti-api/a44b4af6f1abd-get-employee-job-data */ async getEmployeeJobData( employeeNumber: string ): Promise<AvantiApiResponse<GetEmployeeJobDataResponse>> { return (await this.callApi('/v1/EmployeeJobData', { getParameters: { empNo: employeeNumber }, method: 'get' })) as AvantiApiResponse<GetEmployeeJobDataResponse> } /** * Get Employee Personal Info: * /v1/PersonalInfo * @param employeeNumber - Employee number * @returns See https://avanti.stoplight.io/docs/avanti-api/f851b988b5cf6-get-employee-personal-info */ async getEmployeePersonalInfo( employeeNumber: string ): Promise<AvantiApiResponse<GetEmployeePersonalInfoResponse>> { return (await this.callApi('/v1/PersonalInfo', { getParameters: { empNo: employeeNumber }, method: 'get' })) as AvantiApiResponse<GetEmployeePersonalInfoResponse> } /** * List Time Entries: * /v1/TimeManagement/{viewId}/{templateId} * @param viewId - View Id * @param templateId - Template Id * @param parameters - Request parameters * @returns See https://avanti.stoplight.io/docs/avanti-api/4952dd2917595-list-time-entries */ async getTimeEntries( viewId: string, templateId: string, parameters: GetTimeEntriesRequest ): Promise<AvantiApiResponse<GetTimeEntriesTimeEntry[]>> { return (await this.callApi(`/v1/TimeManagement/${viewId}/${templateId}`, { getParameters: parameters, method: 'get' })) as AvantiApiResponse<GetTimeEntriesTimeEntry[]> } /** * List Time Entry Templates: * /v1/TimeManagement/Templates * @param parameters - Request parameters * @returns See https://avanti.stoplight.io/docs/avanti-api/faa0ddb0eb18d-list-time-entry-templates */ async getTimeEntryTemplates( parameters: GetTimeEntryTemplatesRequest ): Promise<AvantiApiResponse<GetTimeEntryTemplatesTimeEntryTemplate[]>> { return (await this.callApi('/v1/TimeManagement/Templates', { getParameters: parameters, method: 'get' })) as AvantiApiResponse<GetTimeEntryTemplatesTimeEntryTemplate[]> } /** * Get Report Data: * /v1/Reporter/{reportId} * @param reportId - The ID of the report you would like to get the data for. * @returns See https://avanti.stoplight.io/docs/avanti-api/ed0485a9c98bb-get-report-data */ async getReport(reportId: string): Promise<AvantiApiResponse<object[]>> { return (await this.callApi(`/v1/Reporter/${reportId}`, { method: 'get' })) as AvantiApiResponse<object[]> } } export * as lookups from './lookups.js' export type * as types from './types.js'