mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-13 15:10:06 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3556eb3903 | |||
| 600e561a45 | |||
| 6a161a05ef | |||
| 8db02012e5 | |||
| a01b1a9feb | |||
| e6ec51c7eb |
@@ -78,7 +78,16 @@ export class AuthManager {
|
|||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
if (this.serverType === ServerType.Sas9) {
|
if (this.serverType === ServerType.Sas9) {
|
||||||
await this.performCASSecurityCheck()
|
const casSecurityCheckResponse = await this.performCASSecurityCheck()
|
||||||
|
|
||||||
|
if (isPublicAccessDenied(casSecurityCheckResponse.result)) {
|
||||||
|
return {
|
||||||
|
isLoggedIn: false,
|
||||||
|
userName: this.userName || '',
|
||||||
|
userLongName: this.userLongName || '',
|
||||||
|
errorMessage: 'Public access has been denied.'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { userName, userLongName } = await this.fetchUserName()
|
const { userName, userLongName } = await this.fetchUserName()
|
||||||
@@ -149,7 +158,17 @@ export class AuthManager {
|
|||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
if (this.serverType === ServerType.Sas9) {
|
if (this.serverType === ServerType.Sas9) {
|
||||||
await this.performCASSecurityCheck()
|
const casSecurityCheckResponse = await this.performCASSecurityCheck()
|
||||||
|
if (isPublicAccessDenied(casSecurityCheckResponse.result)) {
|
||||||
|
isLoggedIn = false
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoggedIn,
|
||||||
|
userName: this.userName || '',
|
||||||
|
userLongName: this.userLongName || '',
|
||||||
|
errorMessage: 'Public access has been denied.'
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loginCallback()
|
this.loginCallback()
|
||||||
@@ -166,11 +185,15 @@ export class AuthManager {
|
|||||||
private async performCASSecurityCheck() {
|
private async performCASSecurityCheck() {
|
||||||
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
const casAuthenticationUrl = `${this.serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||||
|
|
||||||
await this.requestClient
|
return await this.requestClient
|
||||||
.get<string>(`/SASLogon/login?service=${casAuthenticationUrl}`, undefined)
|
.get<string>(`/SASLogon/login?service=${casAuthenticationUrl}`, undefined)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
// ignore if resource not found error
|
// ignore if resource not found error
|
||||||
if (!(err instanceof NotFoundError)) throw err
|
if (!(err instanceof NotFoundError)) throw err
|
||||||
|
|
||||||
|
return {
|
||||||
|
result: ''
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -387,3 +410,7 @@ const isLogInSuccess = (serverType: ServerType, response: any): boolean => {
|
|||||||
|
|
||||||
return /You have signed in/gm.test(response)
|
return /You have signed in/gm.test(response)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPublicAccessDenied = (response: any): boolean => {
|
||||||
|
return /Public access has been denied/gm.test(response)
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import axios from 'axios'
|
|||||||
import {
|
import {
|
||||||
mockedCurrentUserApi,
|
mockedCurrentUserApi,
|
||||||
mockLoginAuthoriseRequiredResponse,
|
mockLoginAuthoriseRequiredResponse,
|
||||||
|
mockLoginPublicAccessDeniedResponse,
|
||||||
mockLoginSuccessResponse
|
mockLoginSuccessResponse
|
||||||
} from './mockResponses'
|
} from './mockResponses'
|
||||||
import { serialize } from '../../utils'
|
import { serialize } from '../../utils'
|
||||||
@@ -213,6 +214,61 @@ describe('AuthManager', () => {
|
|||||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should post a login & a cas_security request to the SAS9 server when not logged in & get rejected due to public access denied', async () => {
|
||||||
|
const serverType = ServerType.Sas9
|
||||||
|
const authManager = new AuthManager(
|
||||||
|
serverUrl,
|
||||||
|
serverType,
|
||||||
|
requestClient,
|
||||||
|
authCallback
|
||||||
|
)
|
||||||
|
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
isLoggedIn: false,
|
||||||
|
userName: '',
|
||||||
|
userLongName: '',
|
||||||
|
loginForm: { name: 'test' }
|
||||||
|
})
|
||||||
|
)
|
||||||
|
mockedAxios.post.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({ data: mockLoginSuccessResponse })
|
||||||
|
)
|
||||||
|
mockedAxios.get.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({ data: mockLoginPublicAccessDeniedResponse })
|
||||||
|
)
|
||||||
|
|
||||||
|
const loginResponse = await authManager.logIn(userName, password)
|
||||||
|
|
||||||
|
expect(loginResponse.isLoggedIn).toBeFalse()
|
||||||
|
expect(loginResponse.userName).toEqual('')
|
||||||
|
expect(loginResponse.errorMessage).toEqual(
|
||||||
|
'Public access has been denied.'
|
||||||
|
)
|
||||||
|
|
||||||
|
const loginParams = serialize({
|
||||||
|
_service: 'default',
|
||||||
|
username: userName,
|
||||||
|
password,
|
||||||
|
name: 'test'
|
||||||
|
})
|
||||||
|
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||||
|
`/SASLogon/login`,
|
||||||
|
loginParams,
|
||||||
|
{
|
||||||
|
withCredentials: true,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
Accept: '*/*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
const casAuthenticationUrl = `${serverUrl}/SASStoredProcess/j_spring_cas_security_check`
|
||||||
|
expect(mockedAxios.get).toHaveBeenCalledWith(
|
||||||
|
`/SASLogon/login?service=${casAuthenticationUrl}`,
|
||||||
|
getHeadersJson
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('should return empty username if unable to logged in', async () => {
|
it('should return empty username if unable to logged in', async () => {
|
||||||
const authManager = new AuthManager(
|
const authManager = new AuthManager(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
@@ -422,6 +478,53 @@ describe('AuthManager', () => {
|
|||||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('should return error if public account access is denied', async () => {
|
||||||
|
const serverType = ServerType.Sas9
|
||||||
|
const authManager = new AuthManager(
|
||||||
|
serverUrl,
|
||||||
|
serverType,
|
||||||
|
requestClient,
|
||||||
|
authCallback
|
||||||
|
)
|
||||||
|
jest
|
||||||
|
.spyOn<any, any>(authManager, 'fetchUserName')
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
isLoggedIn: false,
|
||||||
|
userName: ''
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve({
|
||||||
|
isLoggedIn: true,
|
||||||
|
userName
|
||||||
|
})
|
||||||
|
)
|
||||||
|
mockedAxios.get.mockImplementation(() =>
|
||||||
|
Promise.resolve({ data: mockLoginPublicAccessDeniedResponse })
|
||||||
|
)
|
||||||
|
|
||||||
|
const loginResponse = await authManager.redirectedLogIn({})
|
||||||
|
|
||||||
|
expect(loginResponse.isLoggedIn).toBeFalse()
|
||||||
|
expect(loginResponse.userName).toEqual('')
|
||||||
|
expect(loginResponse.errorMessage).toEqual(
|
||||||
|
'Public access has been denied.'
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(openWebPageModule.openWebPage).toHaveBeenCalledWith(
|
||||||
|
`/SASLogon`,
|
||||||
|
'SASLogon',
|
||||||
|
{
|
||||||
|
width: 500,
|
||||||
|
height: 600
|
||||||
|
},
|
||||||
|
undefined
|
||||||
|
)
|
||||||
|
expect(authManager['fetchUserName']).toHaveBeenCalledTimes(1)
|
||||||
|
expect(verifySas9LoginModule.verifySas9Login).toHaveBeenCalledTimes(1)
|
||||||
|
})
|
||||||
|
|
||||||
it('should return empty username if user unable to re-login via pop up', async () => {
|
it('should return empty username if user unable to re-login via pop up', async () => {
|
||||||
const authManager = new AuthManager(
|
const authManager = new AuthManager(
|
||||||
serverUrl,
|
serverUrl,
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { SasAuthResponse } from '@sasjs/utils/types'
|
|||||||
|
|
||||||
export const mockLoginAuthoriseRequiredResponse = `<form id="application_authorization" action="/SASLogon/oauth/authorize" method="POST"><input type="hidden" name="X-Uaa-Csrf" value="2nfuxIn6WaOURWL7tzTXCe"/>`
|
export const mockLoginAuthoriseRequiredResponse = `<form id="application_authorization" action="/SASLogon/oauth/authorize" method="POST"><input type="hidden" name="X-Uaa-Csrf" value="2nfuxIn6WaOURWL7tzTXCe"/>`
|
||||||
export const mockLoginSuccessResponse = `You have signed in`
|
export const mockLoginSuccessResponse = `You have signed in`
|
||||||
|
export const mockLoginPublicAccessDeniedResponse = `Public access has been denied`
|
||||||
|
|
||||||
export const mockAuthResponse: SasAuthResponse = {
|
export const mockAuthResponse: SasAuthResponse = {
|
||||||
access_token: 'acc355',
|
access_token: 'acc355',
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
getValidJson,
|
getValidJson,
|
||||||
parseSasViyaDebugResponse,
|
parseSasViyaDebugResponse,
|
||||||
parseWeboutResponse
|
parseWeboutResponse,
|
||||||
|
SASJS_LOGS_SEPARATOR
|
||||||
} from '../utils'
|
} from '../utils'
|
||||||
import { UploadFile } from '../types/UploadFile'
|
import { UploadFile } from '../types/UploadFile'
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -187,6 +187,12 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
{ result: jsonResponse, log: res.log },
|
{ result: jsonResponse, log: res.log },
|
||||||
extraResponseAttributes
|
extraResponseAttributes
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (this.isPublicAccessDenied(jsonResponse))
|
||||||
|
reject(
|
||||||
|
new ErrorResponse('Public access has been denied', responseObject)
|
||||||
|
)
|
||||||
|
|
||||||
resolve(responseObject)
|
resolve(responseObject)
|
||||||
})
|
})
|
||||||
.catch(async (e: Error) => {
|
.catch(async (e: Error) => {
|
||||||
@@ -262,4 +268,8 @@ export class WebJobExecutor extends BaseJobExecutor {
|
|||||||
}
|
}
|
||||||
return uri
|
return uri
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private isPublicAccessDenied = (response: string): boolean => {
|
||||||
|
return /Public access has been denied/gm.test(response)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import { RequestClient } from './RequestClient'
|
import { RequestClient } from './RequestClient'
|
||||||
import { AxiosResponse } from 'axios'
|
import { AxiosResponse } from 'axios'
|
||||||
|
import { SASJS_LOGS_SEPARATOR } from '../utils'
|
||||||
|
|
||||||
export interface SasjsParsedResponse<T> {
|
interface SasjsParsedResponse<T> {
|
||||||
result: T
|
result: T
|
||||||
log: string
|
log: string
|
||||||
etag: string
|
etag: string
|
||||||
@@ -44,30 +45,13 @@ export class SasjsRequestClient extends RequestClient {
|
|||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (response.data.includes(SASJS_LOGS_SEPARATOR)) {
|
if (response.data.includes(SASJS_LOGS_SEPARATOR)) {
|
||||||
const { data } = response
|
const splittedResponse = response.data.split(SASJS_LOGS_SEPARATOR)
|
||||||
const splittedResponse: string[] = data.split(SASJS_LOGS_SEPARATOR)
|
|
||||||
|
|
||||||
webout = splittedResponse.splice(0, 1)[0]
|
webout = splittedResponse[0]
|
||||||
if (webout !== undefined) parsedResponse = webout
|
if (webout !== undefined) parsedResponse = webout
|
||||||
|
|
||||||
// log can contain nested logs
|
log = splittedResponse[1]
|
||||||
const logs = splittedResponse.splice(0, splittedResponse.length - 1)
|
printOutput = splittedResponse[2]
|
||||||
|
|
||||||
// tests if string ends with SASJS_LOGS_SEPARATOR
|
|
||||||
const endingWithLogSepRegExp = new RegExp(`${SASJS_LOGS_SEPARATOR}$`)
|
|
||||||
|
|
||||||
// at this point splittedResponse can contain only one item
|
|
||||||
const lastChunk = splittedResponse[0]
|
|
||||||
|
|
||||||
if (lastChunk) {
|
|
||||||
// if the last chunk doesn't end with SASJS_LOGS_SEPARATOR, then it is a printOutput
|
|
||||||
// else the last chunk is part of the log and has to be joined
|
|
||||||
if (!endingWithLogSepRegExp.test(data)) printOutput = lastChunk
|
|
||||||
else if (logs.length > 1) logs.push(lastChunk)
|
|
||||||
}
|
|
||||||
|
|
||||||
// join logs into single log with SASJS_LOGS_SEPARATOR
|
|
||||||
log = logs.join(SASJS_LOGS_SEPARATOR)
|
|
||||||
} else {
|
} else {
|
||||||
parsedResponse = response.data
|
parsedResponse = response.data
|
||||||
}
|
}
|
||||||
@@ -75,7 +59,7 @@ export class SasjsRequestClient extends RequestClient {
|
|||||||
|
|
||||||
const returnResult: SasjsParsedResponse<T> = {
|
const returnResult: SasjsParsedResponse<T> = {
|
||||||
result: parsedResponse as T,
|
result: parsedResponse as T,
|
||||||
log: log || '',
|
log,
|
||||||
etag,
|
etag,
|
||||||
status: response.status
|
status: response.status
|
||||||
}
|
}
|
||||||
@@ -85,6 +69,3 @@ export class SasjsRequestClient extends RequestClient {
|
|||||||
return returnResult
|
return returnResult
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SASJS_LOGS_SEPARATOR =
|
|
||||||
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
|
||||||
|
|||||||
@@ -1,172 +0,0 @@
|
|||||||
import {
|
|
||||||
SASJS_LOGS_SEPARATOR,
|
|
||||||
SasjsRequestClient,
|
|
||||||
SasjsParsedResponse
|
|
||||||
} from '../SasjsRequestClient'
|
|
||||||
import { AxiosResponse } from 'axios'
|
|
||||||
|
|
||||||
describe('SasjsRequestClient', () => {
|
|
||||||
const requestClient = new SasjsRequestClient('')
|
|
||||||
const etag = 'etag'
|
|
||||||
const status = 200
|
|
||||||
|
|
||||||
const webout = `hello`
|
|
||||||
const log = `1 The SAS System Tuesday, 25 July 2023 12:51:00
|
|
||||||
|
|
||||||
|
|
||||||
PROC MIGRATE will preserve current SAS file attributes and is
|
|
||||||
recommended for converting all your SAS libraries from any
|
|
||||||
SAS 8 release to SAS 9. For details and examples, please see
|
|
||||||
http://support.sas.com/rnd/migration/index.html
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
NOTE: SAS initialization used:
|
|
||||||
real time 0.01 seconds
|
|
||||||
cpu time 0.02 seconds
|
|
||||||
|
|
||||||
|
|
||||||
`
|
|
||||||
const printOutput = 'printOutPut'
|
|
||||||
|
|
||||||
describe('parseResponse', () => {})
|
|
||||||
|
|
||||||
it('should parse response with 1 log', () => {
|
|
||||||
const response: AxiosResponse<any> = {
|
|
||||||
data: `${webout}
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
${log}
|
|
||||||
${SASJS_LOGS_SEPARATOR}`,
|
|
||||||
status,
|
|
||||||
statusText: 'ok',
|
|
||||||
headers: { etag },
|
|
||||||
config: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
|
||||||
result: `${webout}
|
|
||||||
`,
|
|
||||||
log: `
|
|
||||||
${log}
|
|
||||||
`,
|
|
||||||
etag,
|
|
||||||
status
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(requestClient['parseResponse'](response)).toEqual(
|
|
||||||
expectedParsedResponse
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should parse response with 1 log and printOutput', () => {
|
|
||||||
const response: AxiosResponse<any> = {
|
|
||||||
data: `${webout}
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
${log}
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
${printOutput}`,
|
|
||||||
status,
|
|
||||||
statusText: 'ok',
|
|
||||||
headers: { etag },
|
|
||||||
config: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
|
||||||
result: `${webout}
|
|
||||||
`,
|
|
||||||
log: `
|
|
||||||
${log}
|
|
||||||
`,
|
|
||||||
etag,
|
|
||||||
status,
|
|
||||||
printOutput: `
|
|
||||||
${printOutput}`
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(requestClient['parseResponse'](response)).toEqual(
|
|
||||||
expectedParsedResponse
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should parse response with nested logs', () => {
|
|
||||||
const logWithNestedLog = `root log start
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
${log}
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
root log end`
|
|
||||||
|
|
||||||
const response: AxiosResponse<any> = {
|
|
||||||
data: `${webout}
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
${logWithNestedLog}
|
|
||||||
${SASJS_LOGS_SEPARATOR}`,
|
|
||||||
status,
|
|
||||||
statusText: 'ok',
|
|
||||||
headers: { etag },
|
|
||||||
config: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
|
||||||
result: `${webout}
|
|
||||||
`,
|
|
||||||
log: `
|
|
||||||
${logWithNestedLog}
|
|
||||||
`,
|
|
||||||
etag,
|
|
||||||
status
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(requestClient['parseResponse'](response)).toEqual(
|
|
||||||
expectedParsedResponse
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
it('should parse response with nested logs and printOutput', () => {
|
|
||||||
const logWithNestedLog = `root log start
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
${log}
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
log with indentation
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
${log}
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
some SAS code containing ${SASJS_LOGS_SEPARATOR}
|
|
||||||
root log end`
|
|
||||||
|
|
||||||
const response: AxiosResponse<any> = {
|
|
||||||
data: `${webout}
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
${logWithNestedLog}
|
|
||||||
${SASJS_LOGS_SEPARATOR}
|
|
||||||
${printOutput}`,
|
|
||||||
status,
|
|
||||||
statusText: 'ok',
|
|
||||||
headers: { etag },
|
|
||||||
config: {}
|
|
||||||
}
|
|
||||||
|
|
||||||
const expectedParsedResponse: SasjsParsedResponse<string> = {
|
|
||||||
result: `${webout}
|
|
||||||
`,
|
|
||||||
log: `
|
|
||||||
${logWithNestedLog}
|
|
||||||
`,
|
|
||||||
etag,
|
|
||||||
status,
|
|
||||||
printOutput: `
|
|
||||||
${printOutput}`
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(requestClient['parseResponse'](response)).toEqual(
|
|
||||||
expectedParsedResponse
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('SASJS_LOGS_SEPARATOR', () => {
|
|
||||||
it('SASJS_LOGS_SEPARATOR should be hardcoded', () => {
|
|
||||||
expect(SASJS_LOGS_SEPARATOR).toEqual(
|
|
||||||
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -6,6 +6,7 @@ export interface LoginResult {
|
|||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
userName: string
|
userName: string
|
||||||
userLongName: string
|
userLongName: string
|
||||||
|
errorMessage?: string
|
||||||
}
|
}
|
||||||
export interface LoginResultInternal {
|
export interface LoginResultInternal {
|
||||||
isLoggedIn: boolean
|
isLoggedIn: boolean
|
||||||
|
|||||||
2
src/utils/constants.ts
Normal file
2
src/utils/constants.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export const SASJS_LOGS_SEPARATOR =
|
||||||
|
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||||
@@ -2,6 +2,7 @@ export * from './appendExtraResponseAttributes'
|
|||||||
export * from './asyncForEach'
|
export * from './asyncForEach'
|
||||||
export * from './compareTimestamps'
|
export * from './compareTimestamps'
|
||||||
export * from './convertToCsv'
|
export * from './convertToCsv'
|
||||||
|
export * from './constants'
|
||||||
export * from './createAxiosInstance'
|
export * from './createAxiosInstance'
|
||||||
export * from './delay'
|
export * from './delay'
|
||||||
export * from './fetchLogByChunks'
|
export * from './fetchLogByChunks'
|
||||||
|
|||||||
Reference in New Issue
Block a user