mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
fix(*): store CSRF tokens in Request Client
This commit is contained in:
@@ -1,14 +1,15 @@
|
||||
import SASjs, { ServerType, SASjsConfig } from "@sasjs/adapter";
|
||||
import SASjs, { SASjsConfig } from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
import { ServerType } from "@sasjs/utils/types";
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: window.location.origin,
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
serverType: ServerType.SASViya,
|
||||
pathSAS9: "/SASStoredProcess/do",
|
||||
pathSASViya: "/SASJobExecution",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SasViya,
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
contextName: "SAS Job Execution compute context",
|
||||
useComputeApi: false
|
||||
};
|
||||
|
||||
@@ -17,7 +18,7 @@ const customConfig = {
|
||||
pathSAS9: "sas9",
|
||||
pathSASViya: "viya",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SAS9,
|
||||
serverType: ServerType.Sas9,
|
||||
debug: false
|
||||
};
|
||||
|
||||
@@ -39,11 +40,12 @@ export const basicTests = (
|
||||
},
|
||||
{
|
||||
title: "Multiple Log in attempts",
|
||||
description: "Should fail on first attempt and should log the user in on second attempt",
|
||||
description:
|
||||
"Should fail on first attempt and should log the user in on second attempt",
|
||||
test: async () => {
|
||||
await adapter.logOut()
|
||||
await adapter.logIn('invalid', 'invalid')
|
||||
return adapter.logIn(userName, password)
|
||||
await adapter.logOut();
|
||||
await adapter.logIn("invalid", "invalid");
|
||||
return adapter.logIn(userName, password);
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import {
|
||||
Context,
|
||||
CsrfToken,
|
||||
EditContextInput,
|
||||
ContextAllAttributes
|
||||
} from './types'
|
||||
import { makeRequest, isUrl } from './utils'
|
||||
import { Context, EditContextInput, ContextAllAttributes } from './types'
|
||||
import { isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/client'
|
||||
|
||||
export class ContextManager {
|
||||
private defaultComputeContexts = [
|
||||
@@ -28,8 +24,6 @@ export class ContextManager {
|
||||
'SAS Visual Forecasting launcher context'
|
||||
]
|
||||
|
||||
private csrfToken: CsrfToken | null = null
|
||||
|
||||
get getDefaultComputeContexts() {
|
||||
return this.defaultComputeContexts
|
||||
}
|
||||
@@ -37,28 +31,19 @@ export class ContextManager {
|
||||
return this.defaultLauncherContexts
|
||||
}
|
||||
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||
) {
|
||||
constructor(private serverUrl: string, private requestClient: RequestClient) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
public async getComputeContexts(accessToken?: string) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute contexts. ')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute contexts. ')
|
||||
})
|
||||
|
||||
const contextsList = contexts && contexts.items ? contexts.items : []
|
||||
|
||||
@@ -72,20 +57,14 @@ export class ContextManager {
|
||||
}
|
||||
|
||||
public async getLauncherContexts(accessToken?: string) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/launcher/contexts?limit=10000`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting launcher contexts. ')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/launcher/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting launcher contexts. ')
|
||||
})
|
||||
|
||||
const contextsList = contexts && contexts.items ? contexts.items : []
|
||||
|
||||
@@ -183,18 +162,15 @@ export class ContextManager {
|
||||
requestBody.environment = { autoExecLines }
|
||||
}
|
||||
|
||||
const createContextRequest: RequestInit = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(requestBody)
|
||||
}
|
||||
|
||||
const { result: context } = await this.request<Context>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
createContextRequest
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating compute context. ')
|
||||
})
|
||||
const { result: context } = await this.requestClient
|
||||
.post<Context>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
JSON.stringify(requestBody),
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating compute context. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -237,18 +213,15 @@ export class ContextManager {
|
||||
launchType
|
||||
}
|
||||
|
||||
const createContextRequest: RequestInit = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(requestBody)
|
||||
}
|
||||
|
||||
const { result: context } = await this.request<Context>(
|
||||
`${this.serverUrl}/launcher/contexts`,
|
||||
createContextRequest
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating launcher context. ')
|
||||
})
|
||||
const { result: context } = await this.requestClient
|
||||
.post<Context>(
|
||||
`${this.serverUrl}/launcher/contexts`,
|
||||
JSON.stringify(requestBody),
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating launcher context. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -267,14 +240,6 @@ export class ContextManager {
|
||||
true
|
||||
)
|
||||
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
let originalContext
|
||||
|
||||
originalContext = await this.getComputeContextByName(
|
||||
@@ -290,39 +255,33 @@ export class ContextManager {
|
||||
)
|
||||
}
|
||||
|
||||
const { result: context, etag } = await this.request<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
|
||||
{
|
||||
headers
|
||||
}
|
||||
).catch((err) => {
|
||||
if (err && err.status === 404) {
|
||||
throw new Error(
|
||||
`The context '${contextName}' was not found on this server.`
|
||||
)
|
||||
}
|
||||
const { result: context, etag } = await this.requestClient
|
||||
.get<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err && err.status === 404) {
|
||||
throw new Error(
|
||||
`The context '${contextName}' was not found on this server.`
|
||||
)
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
throw err
|
||||
})
|
||||
|
||||
// An If-Match header with the value of the last ETag for the context
|
||||
// is required to be able to update it
|
||||
// https://developer.sas.com/apis/rest/Compute/#update-a-context-definition
|
||||
headers['If-Match'] = etag
|
||||
|
||||
const updateContextRequest: RequestInit = {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
return await this.requestClient.put<Context>(
|
||||
`/compute/contexts/${context.id}`,
|
||||
JSON.stringify({
|
||||
...context,
|
||||
...editedContext,
|
||||
attributes: { ...context.attributes, ...editedContext.attributes }
|
||||
})
|
||||
}
|
||||
|
||||
return await this.request<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${context.id}`,
|
||||
updateContextRequest
|
||||
}),
|
||||
accessToken,
|
||||
{ 'If-Match': etag }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -330,20 +289,17 @@ export class ContextManager {
|
||||
contextName: string,
|
||||
accessToken?: string
|
||||
): Promise<Context> {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute context by name. ')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while getting compute context by name. '
|
||||
)
|
||||
})
|
||||
|
||||
if (!contexts || !(contexts.items && contexts.items.length)) {
|
||||
throw new Error(
|
||||
@@ -358,20 +314,16 @@ export class ContextManager {
|
||||
contextId: string,
|
||||
accessToken?: string
|
||||
): Promise<ContextAllAttributes> {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: context } = await this.request<ContextAllAttributes>(
|
||||
`${this.serverUrl}/compute/contexts/${contextId}`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute context by id. ')
|
||||
})
|
||||
const {
|
||||
result: context
|
||||
} = await this.requestClient
|
||||
.get<ContextAllAttributes>(
|
||||
`${this.serverUrl}/compute/contexts/${contextId}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute context by id. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -380,20 +332,14 @@ export class ContextManager {
|
||||
executeScript: Function,
|
||||
accessToken?: string
|
||||
) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while fetching compute contexts.')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while fetching compute contexts.')
|
||||
})
|
||||
|
||||
const contextsList = contexts.items || []
|
||||
const executableContexts: any[] = []
|
||||
@@ -470,14 +416,9 @@ export class ContextManager {
|
||||
|
||||
const context = await this.getComputeContextByName(contextName, accessToken)
|
||||
|
||||
const deleteContextRequest: RequestInit = {
|
||||
method: 'DELETE',
|
||||
headers
|
||||
}
|
||||
|
||||
return await this.request<Context>(
|
||||
return await this.requestClient.delete<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${context.id}`,
|
||||
deleteContextRequest
|
||||
accessToken
|
||||
)
|
||||
}
|
||||
|
||||
@@ -485,34 +426,6 @@ export class ContextManager {
|
||||
|
||||
// TODO: implement deleteLauncherContext method
|
||||
|
||||
private async request<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
) {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
}
|
||||
}
|
||||
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(token) => {
|
||||
this.csrfToken = token
|
||||
this.setCsrfToken(token)
|
||||
},
|
||||
contentType
|
||||
).catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while making request in Context Manager. '
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private validateContextName(name: string) {
|
||||
if (!name) throw new Error('Context name is required.')
|
||||
}
|
||||
|
||||
@@ -1,116 +1,63 @@
|
||||
import { needsRetry, isUrl } from './utils'
|
||||
import { CsrfToken } from './types/CsrfToken'
|
||||
import { isUrl } from './utils'
|
||||
import { UploadFile } from './types/UploadFile'
|
||||
import { ErrorResponse } from './types'
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { isLogInRequired } from './auth'
|
||||
|
||||
const requestRetryLimit = 5
|
||||
import { RequestClient } from './request/client'
|
||||
|
||||
export class FileUploader {
|
||||
private httpClient: AxiosInstance
|
||||
|
||||
constructor(
|
||||
private appLoc: string,
|
||||
serverUrl: string,
|
||||
private jobsPath: string,
|
||||
private setCsrfTokenWeb: any,
|
||||
private csrfToken: CsrfToken | null = null
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
this.httpClient = axios.create({ baseURL: serverUrl })
|
||||
}
|
||||
|
||||
private retryCount = 0
|
||||
|
||||
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (files?.length < 1)
|
||||
reject(new ErrorResponse('At least one file must be provided.'))
|
||||
if (!sasJob || sasJob === '')
|
||||
reject(new ErrorResponse('sasJob must be provided.'))
|
||||
if (files?.length < 1)
|
||||
return Promise.reject(
|
||||
new ErrorResponse('At least one file must be provided.')
|
||||
)
|
||||
if (!sasJob || sasJob === '')
|
||||
return Promise.reject(new ErrorResponse('sasJob must be provided.'))
|
||||
|
||||
let paramsString = ''
|
||||
let paramsString = ''
|
||||
|
||||
for (let param in params) {
|
||||
if (params.hasOwnProperty(param)) {
|
||||
paramsString += `&${param}=${params[param]}`
|
||||
}
|
||||
for (let param in params) {
|
||||
if (params.hasOwnProperty(param)) {
|
||||
paramsString += `&${param}=${params[param]}`
|
||||
}
|
||||
}
|
||||
|
||||
const program = this.appLoc
|
||||
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
const uploadUrl = `${this.jobsPath}/?${
|
||||
'_program=' + program
|
||||
}${paramsString}`
|
||||
const program = this.appLoc
|
||||
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
const uploadUrl = `${this.jobsPath}/?${
|
||||
'_program=' + program
|
||||
}${paramsString}`
|
||||
|
||||
const headers = {
|
||||
'cache-control': 'no-cache'
|
||||
}
|
||||
const formData = new FormData()
|
||||
|
||||
const formData = new FormData()
|
||||
for (let file of files) {
|
||||
formData.append('file', file.file, file.fileName)
|
||||
}
|
||||
|
||||
for (let file of files) {
|
||||
formData.append('file', file.file, file.fileName)
|
||||
}
|
||||
const csrfToken = this.requestClient.getCsrfToken('file')
|
||||
if (csrfToken) formData.append('_csrf', csrfToken.value)
|
||||
|
||||
if (this.csrfToken) formData.append('_csrf', this.csrfToken.value)
|
||||
const headers = {
|
||||
'cache-control': 'no-cache',
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
|
||||
this.httpClient
|
||||
.post(uploadUrl, formData, { responseType: 'text', headers })
|
||||
.then(async (response) => {
|
||||
if (response.status !== 200) {
|
||||
if (response.status === 403) {
|
||||
const tokenHeader = response.headers.get('X-CSRF-HEADER')
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers.get(tokenHeader)
|
||||
this.csrfToken = {
|
||||
headerName: tokenHeader,
|
||||
value: token || ''
|
||||
}
|
||||
|
||||
this.setCsrfTokenWeb(this.csrfToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.data
|
||||
})
|
||||
.then((responseText) => {
|
||||
if (isLogInRequired(responseText))
|
||||
reject(new ErrorResponse('You must be logged in to upload a file.'))
|
||||
|
||||
if (needsRetry(responseText)) {
|
||||
if (this.retryCount < requestRetryLimit) {
|
||||
this.retryCount++
|
||||
this.uploadFile(sasJob, files, params).then(
|
||||
(res: any) => resolve(res),
|
||||
(err: any) => reject(err)
|
||||
)
|
||||
} else {
|
||||
this.retryCount = 0
|
||||
reject(responseText)
|
||||
}
|
||||
} else {
|
||||
this.retryCount = 0
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(responseText))
|
||||
} catch (e) {
|
||||
reject(
|
||||
new ErrorResponse(
|
||||
'Error while parsing json from upload response.',
|
||||
e
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
reject(new ErrorResponse('Upload request failed.', err))
|
||||
})
|
||||
})
|
||||
return this.requestClient
|
||||
.post(uploadUrl, formData, undefined, headers)
|
||||
.then((res) => res.result)
|
||||
.catch((err: Error) => {
|
||||
return Promise.reject(
|
||||
new ErrorResponse('File upload request failed', err)
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
Context,
|
||||
ContextAllAttributes,
|
||||
Folder,
|
||||
CsrfToken,
|
||||
EditContextInput,
|
||||
JobDefinition,
|
||||
PollOptions
|
||||
@@ -16,35 +15,34 @@ import { SessionManager } from './SessionManager'
|
||||
import { ContextManager } from './ContextManager'
|
||||
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||
import { parseAndSubmitAuthorizeForm } from './auth'
|
||||
import { RequestClient } from './request/client'
|
||||
|
||||
/**
|
||||
* A client for interfacing with the SAS Viya REST API.
|
||||
*
|
||||
*/
|
||||
export class SASViyaApiClient {
|
||||
private httpClient: AxiosInstance
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private rootFolderName: string,
|
||||
private contextName: string,
|
||||
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
this.httpClient = axios.create({ baseURL: serverUrl })
|
||||
}
|
||||
|
||||
private csrfToken: CsrfToken | null = null
|
||||
private fileUploadCsrfToken: CsrfToken | null = null
|
||||
private _debug = false
|
||||
private sessionManager = new SessionManager(
|
||||
this.serverUrl,
|
||||
this.contextName,
|
||||
this.setCsrfToken
|
||||
this.requestClient
|
||||
)
|
||||
private contextManager = new ContextManager(
|
||||
this.serverUrl,
|
||||
this.requestClient
|
||||
)
|
||||
private contextManager = new ContextManager(this.serverUrl, this.setCsrfToken)
|
||||
private folderMap = new Map<string, Job[]>()
|
||||
|
||||
public get debug() {
|
||||
@@ -143,10 +141,9 @@ export class SASViyaApiClient {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.get<{ items: Context[] }>(
|
||||
`/compute/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
const { result: contexts } = await this.requestClient.get<{
|
||||
items: Context[]
|
||||
}>(`/compute/contexts?limit=10000`, accessToken)
|
||||
|
||||
const executionContext =
|
||||
contexts.items && contexts.items.length
|
||||
@@ -163,7 +160,7 @@ export class SASViyaApiClient {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
const { result: createdSession } = await this.post<Session>(
|
||||
const { result: createdSession } = await this.requestClient.post<Session>(
|
||||
`/compute/contexts/${executionContext.id}/sessions`,
|
||||
{},
|
||||
accessToken
|
||||
@@ -376,13 +373,15 @@ export class SASViyaApiClient {
|
||||
variables: jobVariables,
|
||||
arguments: jobArguments
|
||||
})
|
||||
const { result: postedJob, etag } = await this.post<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs`,
|
||||
jobRequestBody,
|
||||
accessToken
|
||||
).catch((err: any) => {
|
||||
throw err
|
||||
})
|
||||
const { result: postedJob, etag } = await this.requestClient
|
||||
.post<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs`,
|
||||
jobRequestBody,
|
||||
accessToken
|
||||
)
|
||||
.catch((err: any) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
if (!waitForResult) {
|
||||
return session
|
||||
@@ -404,12 +403,14 @@ export class SASViyaApiClient {
|
||||
pollOptions
|
||||
)
|
||||
|
||||
const { result: currentJob } = await this.get<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const { result: currentJob } = await this.requestClient
|
||||
.get<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
let jobResult
|
||||
let log
|
||||
@@ -417,10 +418,8 @@ export class SASViyaApiClient {
|
||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||
|
||||
if (debug && logLink) {
|
||||
log = await this.get<any>(
|
||||
`${logLink.href}/content?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
log = await this.requestClient
|
||||
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
|
||||
.then((res: any) =>
|
||||
res.result.items.map((i: any) => i.line).join('\n')
|
||||
)
|
||||
@@ -442,34 +441,30 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
if (resultLink) {
|
||||
jobResult = await this.get<any>(
|
||||
resultLink,
|
||||
accessToken,
|
||||
'text/plain'
|
||||
).catch(async (e) => {
|
||||
if (e && e.status === 404) {
|
||||
if (logLink) {
|
||||
log = await this.get<any>(
|
||||
`${logLink.href}/content?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.then((res: any) =>
|
||||
res.result.items.map((i: any) => i.line).join('\n')
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
jobResult = await this.requestClient
|
||||
.get<any>(resultLink, accessToken, 'text/plain')
|
||||
.catch(async (e) => {
|
||||
if (e && e.status === 404) {
|
||||
if (logLink) {
|
||||
log = await this.requestClient
|
||||
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
|
||||
.then((res: any) =>
|
||||
res.result.items.map((i: any) => i.line).join('\n')
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
log
|
||||
})
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
log
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: JSON.stringify(e)
|
||||
}
|
||||
})
|
||||
return {
|
||||
result: JSON.stringify(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await this.sessionManager
|
||||
@@ -559,7 +554,9 @@ export class SASViyaApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
const { result: createFolderResponse } = await this.post<Folder>(
|
||||
const {
|
||||
result: createFolderResponse
|
||||
} = await this.requestClient.post<Folder>(
|
||||
`/folders/folders?parentFolderUri=${parentFolderUri}`,
|
||||
JSON.stringify({
|
||||
name: folderName,
|
||||
@@ -599,7 +596,7 @@ export class SASViyaApiClient {
|
||||
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
||||
}
|
||||
|
||||
return await this.post<Job>(
|
||||
return await this.requestClient.post<Job>(
|
||||
`${this.serverUrl}/jobDefinitions/definitions?parentFolderUri=${parentFolderUri}`,
|
||||
JSON.stringify({
|
||||
name: jobName,
|
||||
@@ -763,7 +760,7 @@ export class SASViyaApiClient {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const deleteResponse = await this.delete(url, accessToken)
|
||||
const deleteResponse = await this.requestClient.delete(url, accessToken)
|
||||
|
||||
return deleteResponse.result
|
||||
}
|
||||
@@ -835,7 +832,9 @@ export class SASViyaApiClient {
|
||||
throw new Error(`URI of job definition was not found.`)
|
||||
}
|
||||
|
||||
const { result: jobDefinition } = await this.get<JobDefinition>(
|
||||
const {
|
||||
result: jobDefinition
|
||||
} = await this.requestClient.get<JobDefinition>(
|
||||
`${this.serverUrl}${jobDefinitionLink.href}`,
|
||||
accessToken
|
||||
)
|
||||
@@ -913,7 +912,7 @@ export class SASViyaApiClient {
|
||||
(l) => l.rel === 'getResource'
|
||||
)?.href
|
||||
|
||||
const { result: jobDefinition } = await this.get<Job>(
|
||||
const { result: jobDefinition } = await this.requestClient.get<Job>(
|
||||
`${this.serverUrl}${jobDefinitionLink}`,
|
||||
accessToken
|
||||
)
|
||||
@@ -948,12 +947,13 @@ export class SASViyaApiClient {
|
||||
jobDefinition,
|
||||
arguments: jobArguments
|
||||
})
|
||||
const { result: postedJob, etag } = await this.post<Job>(
|
||||
const { result: postedJob, etag } = await this.requestClient.post<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||
postJobRequestBody
|
||||
postJobRequestBody,
|
||||
accessToken
|
||||
)
|
||||
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
|
||||
const { result: currentJob } = await this.get<Job>(
|
||||
const { result: currentJob } = await this.requestClient.get<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||
accessToken
|
||||
)
|
||||
@@ -964,17 +964,16 @@ export class SASViyaApiClient {
|
||||
const resultLink = currentJob.results['_webout.json']
|
||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||
if (resultLink) {
|
||||
jobResult = await this.get<any>(
|
||||
jobResult = await this.requestClient.get<any>(
|
||||
`${this.serverUrl}${resultLink}/content`,
|
||||
accessToken,
|
||||
'text/plain'
|
||||
)
|
||||
}
|
||||
if (debug && logLink) {
|
||||
log = await this.get<any>(
|
||||
`${this.serverUrl}${logLink.href}/content`,
|
||||
accessToken
|
||||
).then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
|
||||
log = await this.requestClient
|
||||
.get<any>(`${this.serverUrl}${logLink.href}/content`, accessToken)
|
||||
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
|
||||
}
|
||||
if (jobStatus === 'failed') {
|
||||
return Promise.reject({ error: currentJob.error, log })
|
||||
@@ -991,11 +990,14 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
const url = '/folders/folders/@item?path=' + path
|
||||
const { result: folder } = await this.get<Folder>(`${url}`, accessToken)
|
||||
const { result: folder } = await this.requestClient.get<Folder>(
|
||||
`${url}`,
|
||||
accessToken
|
||||
)
|
||||
if (!folder) {
|
||||
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
|
||||
}
|
||||
const { result: members } = await this.get<{ items: any[] }>(
|
||||
const { result: members } = await this.requestClient.get<{ items: any[] }>(
|
||||
`/folders/folders/${folder.id}/members?limit=${folder.memberCount}`,
|
||||
accessToken
|
||||
)
|
||||
@@ -1033,7 +1035,7 @@ export class SASViyaApiClient {
|
||||
Promise.reject(`Job state link was not found.`)
|
||||
}
|
||||
|
||||
const { result: state } = await this.get<string>(
|
||||
const { result: state } = await this.requestClient.get<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||
accessToken,
|
||||
'text/plain'
|
||||
@@ -1054,7 +1056,7 @@ export class SASViyaApiClient {
|
||||
postedJobState === 'pending'
|
||||
) {
|
||||
if (stateLink) {
|
||||
const { result: jobState } = await this.get<string>(
|
||||
const { result: jobState } = await this.requestClient.get<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||
accessToken,
|
||||
'text/plain'
|
||||
@@ -1100,7 +1102,7 @@ export class SASViyaApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
const uploadResponse = await this.uploadFile(
|
||||
const uploadResponse = await this.requestClient.uploadFile(
|
||||
`${this.serverUrl}/files/files#rawUpload`,
|
||||
csv,
|
||||
accessToken
|
||||
@@ -1113,12 +1115,11 @@ export class SASViyaApiClient {
|
||||
|
||||
private async getFolderUri(folderPath: string, accessToken?: string) {
|
||||
const url = '/folders/folders/@item?path=' + folderPath
|
||||
const { result: folder } = await this.get<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
accessToken
|
||||
).catch(() => {
|
||||
return { result: null }
|
||||
})
|
||||
const { result: folder } = await this.requestClient
|
||||
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
|
||||
.catch(() => {
|
||||
return { result: null }
|
||||
})
|
||||
|
||||
if (!folder) return undefined
|
||||
return `/folders/folders/${folder.id}`
|
||||
@@ -1127,12 +1128,11 @@ export class SASViyaApiClient {
|
||||
private async getRecycleBinUri(accessToken: string) {
|
||||
const url = '/folders/folders/@myRecycleBin'
|
||||
|
||||
const { result: folder } = await this.get<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
accessToken
|
||||
).catch(() => {
|
||||
return { result: null }
|
||||
})
|
||||
const { result: folder } = await this.requestClient
|
||||
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
|
||||
.catch(() => {
|
||||
return { result: null }
|
||||
})
|
||||
|
||||
if (!folder) return undefined
|
||||
|
||||
@@ -1196,27 +1196,31 @@ export class SASViyaApiClient {
|
||||
const sourceFolderId = sourceFolderUri?.split('/').pop()
|
||||
const url = sourceFolderUri
|
||||
|
||||
const { result: folder } = await this.patch<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
JSON.stringify({
|
||||
id: sourceFolderId,
|
||||
name: targetFolderName,
|
||||
parentFolderUri: targetParentFolderUri
|
||||
}),
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
if (err.code && err.code === 'ENOTFOUND') {
|
||||
const notFoundError = {
|
||||
body: JSON.stringify({
|
||||
message: `Folder '${sourceFolder.split('/').pop()}' was not found.`
|
||||
})
|
||||
const { result: folder } = await this.requestClient
|
||||
.patch<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
JSON.stringify({
|
||||
id: sourceFolderId,
|
||||
name: targetFolderName,
|
||||
parentFolderUri: targetParentFolderUri
|
||||
}),
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err.code && err.code === 'ENOTFOUND') {
|
||||
const notFoundError = {
|
||||
body: JSON.stringify({
|
||||
message: `Folder '${sourceFolder
|
||||
.split('/')
|
||||
.pop()}' was not found.`
|
||||
})
|
||||
}
|
||||
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
throw err
|
||||
})
|
||||
|
||||
if (!folder) return undefined
|
||||
|
||||
@@ -1244,175 +1248,4 @@ export class SASViyaApiClient {
|
||||
|
||||
return movedFolder
|
||||
}
|
||||
|
||||
setCsrfTokenLocal = (csrfToken: CsrfToken) => {
|
||||
this.csrfToken = csrfToken
|
||||
this.setCsrfToken(csrfToken)
|
||||
}
|
||||
|
||||
setFileUploadCsrfToken = (csrfToken: CsrfToken) => {
|
||||
this.fileUploadCsrfToken = csrfToken
|
||||
}
|
||||
|
||||
private get<T>(
|
||||
url: string,
|
||||
accessToken?: string,
|
||||
contentType = 'application/json'
|
||||
) {
|
||||
const headers: any = {
|
||||
'Content-Type': contentType
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
headers,
|
||||
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||
withCredentials: true
|
||||
}
|
||||
if (contentType === 'text/plain') {
|
||||
requestConfig.headers.Accept = '*/*'
|
||||
requestConfig.transformResponse = undefined
|
||||
}
|
||||
|
||||
return this.httpClient.get<T>(url, requestConfig).then((response) => ({
|
||||
result: response.data,
|
||||
etag: response.headers['etag']
|
||||
}))
|
||||
}
|
||||
|
||||
private post<T>(
|
||||
url: string,
|
||||
data: any = {},
|
||||
accessToken?: string
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
if (this.csrfToken?.value) {
|
||||
headers[this.csrfToken.headerName] = this.csrfToken.value
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.post<T>(url, data, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
return {
|
||||
result: response.data as T,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
const response = e.response as AxiosResponse
|
||||
if (response.status === 403 || response.status === 449) {
|
||||
const tokenHeader = (response.headers[
|
||||
'x-csrf-header'
|
||||
] as string)?.toLowerCase()
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers[tokenHeader]
|
||||
this.setCsrfTokenLocal({
|
||||
headerName: tokenHeader,
|
||||
value: token || ''
|
||||
})
|
||||
return this.post<T>(url, data, accessToken)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
})
|
||||
}
|
||||
|
||||
private delete<T>(url: string, accessToken?: string) {
|
||||
const headers: any = {}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
if (this.csrfToken?.value) {
|
||||
headers[this.csrfToken.headerName] = this.csrfToken.value
|
||||
}
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
headers
|
||||
}
|
||||
|
||||
return this.httpClient.delete<T>(url, requestConfig).then((response) => ({
|
||||
result: response.data,
|
||||
etag: response.headers['etag']
|
||||
}))
|
||||
}
|
||||
|
||||
private patch<T>(url: string, data: any = {}, accessToken?: string) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
if (this.csrfToken?.value) {
|
||||
headers[this.csrfToken.headerName] = this.csrfToken.value
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.patch<T>(url, data, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
return {
|
||||
result: response.data as T,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private uploadFile(
|
||||
url: string,
|
||||
content: string,
|
||||
accessToken?: string
|
||||
): Promise<any> {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
if (this.fileUploadCsrfToken?.value) {
|
||||
headers[
|
||||
this.fileUploadCsrfToken.headerName
|
||||
] = this.fileUploadCsrfToken.value
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.post(url, content, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
return {
|
||||
result: response.data,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
const response = e.response as AxiosResponse
|
||||
if (response.status === 403 || response.status === 449) {
|
||||
const tokenHeader = (response.headers[
|
||||
'x-csrf-header'
|
||||
] as string)?.toLowerCase()
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers[tokenHeader]
|
||||
this.setFileUploadCsrfToken({
|
||||
headerName: tokenHeader,
|
||||
value: token || ''
|
||||
})
|
||||
return this.uploadFile(url, content, accessToken)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
39
src/SASjs.ts
39
src/SASjs.ts
@@ -24,6 +24,7 @@ import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
import { FileUploader } from './FileUploader'
|
||||
import { isLogInRequired, AuthManager } from './auth'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { RequestClient } from './request/client'
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: '',
|
||||
@@ -45,7 +46,6 @@ const requestRetryLimit = 5
|
||||
export default class SASjs {
|
||||
private sasjsConfig: SASjsConfig = new SASjsConfig()
|
||||
private jobsPath: string = ''
|
||||
private csrfTokenApi: CsrfToken | null = null
|
||||
private csrfTokenWeb: CsrfToken | null = null
|
||||
private retryCountWeb: number = 0
|
||||
private retryCountComputeApi: number = 0
|
||||
@@ -56,6 +56,7 @@ export default class SASjs {
|
||||
private sas9ApiClient: SAS9ApiClient | null = null
|
||||
private fileUploader: FileUploader | null = null
|
||||
private authManager: AuthManager | null = null
|
||||
private requestClient: RequestClient | null = null
|
||||
|
||||
constructor(config?: any) {
|
||||
this.sasjsConfig = {
|
||||
@@ -420,22 +421,6 @@ export default class SASjs {
|
||||
return this.authManager!.userName
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the _csrf token of the current session for the API approach.
|
||||
*
|
||||
*/
|
||||
public getCsrfApi() {
|
||||
return this.csrfTokenApi?.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the _csrf token of the current session for the WEB approach.
|
||||
*
|
||||
*/
|
||||
public getCsrfWeb() {
|
||||
return this.csrfTokenWeb?.value
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the SASjs configuration.
|
||||
* @param config - SASjs configuration.
|
||||
@@ -498,8 +483,7 @@ export default class SASjs {
|
||||
this.sasjsConfig.appLoc,
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.jobsPath,
|
||||
this.setCsrfTokenWeb,
|
||||
this.csrfTokenWeb
|
||||
this.requestClient!
|
||||
)
|
||||
|
||||
return fileUploader.uploadFile(sasJob, files, params)
|
||||
@@ -604,7 +588,7 @@ export default class SASjs {
|
||||
serverUrl,
|
||||
appLoc,
|
||||
this.sasjsConfig.contextName,
|
||||
this.setCsrfTokenApi
|
||||
this.requestClient!
|
||||
)
|
||||
sasApiClient.debug = this.sasjsConfig.debug
|
||||
} else if (this.sasjsConfig.serverType === ServerType.Sas9) {
|
||||
@@ -1150,14 +1134,6 @@ export default class SASjs {
|
||||
return sasjsWaitingRequest.requestPromise.promise
|
||||
}
|
||||
|
||||
private setCsrfTokenWeb = (csrfToken: CsrfToken) => {
|
||||
this.csrfTokenWeb = csrfToken
|
||||
}
|
||||
|
||||
private setCsrfTokenApi = (csrfToken: CsrfToken) => {
|
||||
this.csrfTokenApi = csrfToken
|
||||
}
|
||||
|
||||
private resendWaitingRequests = async () => {
|
||||
for (const sasjsWaitingRequest of this.sasjsWaitingRequests) {
|
||||
this.request(sasjsWaitingRequest.SASjob, sasjsWaitingRequest.data).then(
|
||||
@@ -1396,10 +1372,13 @@ export default class SASjs {
|
||||
this.sasjsConfig.serverUrl = this.sasjsConfig.serverUrl.slice(0, -1)
|
||||
}
|
||||
|
||||
this.requestClient = new RequestClient(this.sasjsConfig.serverUrl)
|
||||
|
||||
this.jobsPath =
|
||||
this.sasjsConfig.serverType === ServerType.SasViya
|
||||
? this.sasjsConfig.pathSASViya
|
||||
: this.sasjsConfig.pathSAS9
|
||||
|
||||
this.authManager = new AuthManager(
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.serverType!,
|
||||
@@ -1417,7 +1396,7 @@ export default class SASjs {
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.sasjsConfig.appLoc,
|
||||
this.sasjsConfig.contextName,
|
||||
this.setCsrfTokenApi
|
||||
this.requestClient
|
||||
)
|
||||
|
||||
this.sasViyaApiClient.debug = this.sasjsConfig.debug
|
||||
@@ -1432,7 +1411,7 @@ export default class SASjs {
|
||||
this.sasjsConfig.appLoc,
|
||||
this.sasjsConfig.serverUrl,
|
||||
this.jobsPath,
|
||||
this.setCsrfTokenWeb
|
||||
this.requestClient
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Session, Context, CsrfToken, SessionVariable } from './types'
|
||||
import { asyncForEach, makeRequest, isUrl } from './utils'
|
||||
import { asyncForEach, isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/client'
|
||||
|
||||
const MAX_SESSION_COUNT = 1
|
||||
const RETRY_LIMIT: number = 3
|
||||
@@ -14,7 +15,7 @@ export class SessionManager {
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private contextName: string,
|
||||
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
@@ -63,10 +64,8 @@ export class SessionManager {
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
|
||||
return await this.request<Session>(
|
||||
`${this.serverUrl}/compute/sessions/${id}`,
|
||||
deleteSessionRequest
|
||||
)
|
||||
return await this.requestClient
|
||||
.delete<Session>(`/compute/sessions/${id}`, accessToken)
|
||||
.then(() => {
|
||||
this.sessions = this.sessions.filter((s) => s.id !== id)
|
||||
})
|
||||
@@ -98,17 +97,20 @@ export class SessionManager {
|
||||
}
|
||||
|
||||
private async createAndWaitForSession(accessToken?: string) {
|
||||
const createSessionRequest = {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
|
||||
const { result: createdSession, etag } = await this.request<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
|
||||
createSessionRequest
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const {
|
||||
result: createdSession,
|
||||
etag
|
||||
} = await this.requestClient
|
||||
.post<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${
|
||||
this.currentContext!.id
|
||||
}/sessions`,
|
||||
{},
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
await this.waitForSession(createdSession, etag, accessToken)
|
||||
|
||||
@@ -119,13 +121,13 @@ export class SessionManager {
|
||||
|
||||
private async setCurrentContext(accessToken?: string) {
|
||||
if (!this.currentContext) {
|
||||
const { result: contexts } = await this.request<{
|
||||
items: Context[]
|
||||
}>(`${this.serverUrl}/compute/contexts?limit=10000`, {
|
||||
headers: this.getHeaders(accessToken)
|
||||
}).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{
|
||||
items: Context[]
|
||||
}>(`${this.serverUrl}/compute/contexts?limit=10000`, accessToken)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
const contextsList =
|
||||
contexts && contexts.items && contexts.items.length
|
||||
@@ -166,10 +168,7 @@ export class SessionManager {
|
||||
accessToken?: string
|
||||
) {
|
||||
let sessionState = session.state
|
||||
const headers: any = {
|
||||
...this.getHeaders(accessToken),
|
||||
'If-None-Match': etag
|
||||
}
|
||||
|
||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
||||
|
||||
return new Promise(async (resolve, _) => {
|
||||
@@ -185,12 +184,10 @@ export class SessionManager {
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
|
||||
const { result: state } = await this.requestSessionStatus<string>(
|
||||
const state = await this.getSessionState(
|
||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||
{
|
||||
headers
|
||||
},
|
||||
'text'
|
||||
etag!,
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
@@ -223,73 +220,33 @@ export class SessionManager {
|
||||
})
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
private async getSessionState(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
etag: string,
|
||||
accessToken?: string
|
||||
) {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
}
|
||||
}
|
||||
return await this.requestClient
|
||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||
.then((res) => res.result as string)
|
||||
.catch((err) => {
|
||||
if (err.status === INTERNAL_SAS_ERROR.status)
|
||||
return INTERNAL_SAS_ERROR.message
|
||||
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(token) => {
|
||||
this.csrfToken = token
|
||||
this.setCsrfToken(token)
|
||||
},
|
||||
contentType
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
private async requestSessionStatus<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
) {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
}
|
||||
}
|
||||
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(token) => {
|
||||
this.csrfToken = token
|
||||
this.setCsrfToken(token)
|
||||
},
|
||||
contentType
|
||||
).catch((err) => {
|
||||
if (err.status === INTERNAL_SAS_ERROR.status)
|
||||
return { result: INTERNAL_SAS_ERROR.message }
|
||||
|
||||
throw err
|
||||
})
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
async getVariable(sessionId: string, variable: string, accessToken?: string) {
|
||||
const getSessionVariable = {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
|
||||
return await this.request<SessionVariable>(
|
||||
`${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
|
||||
getSessionVariable
|
||||
).catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
`Error while fetching session variable '${variable}'.`
|
||||
return await this.requestClient
|
||||
.get<SessionVariable>(
|
||||
`${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
|
||||
accessToken
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
`Error while fetching session variable '${variable}'.`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
262
src/request/client.ts
Normal file
262
src/request/client.ts
Normal file
@@ -0,0 +1,262 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { CsrfToken } from '..'
|
||||
|
||||
export class RequestClient {
|
||||
private csrfToken: CsrfToken | undefined
|
||||
private fileUploadCsrfToken: CsrfToken | undefined
|
||||
private httpClient: AxiosInstance
|
||||
|
||||
constructor(baseUrl: string) {
|
||||
this.httpClient = axios.create({ baseURL: baseUrl })
|
||||
}
|
||||
|
||||
public getCsrfToken(type: 'general' | 'file' = 'general') {
|
||||
return type === 'file' ? this.fileUploadCsrfToken : this.csrfToken
|
||||
}
|
||||
|
||||
public async get<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined,
|
||||
contentType: string = 'application/json',
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, contentType),
|
||||
...overrideHeaders
|
||||
}
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
headers,
|
||||
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||
withCredentials: true
|
||||
}
|
||||
if (contentType === 'text/plain') {
|
||||
requestConfig.headers.Accept = '*/*'
|
||||
requestConfig.transformResponse = undefined
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.get<T>(url, requestConfig)
|
||||
return {
|
||||
result: response.data as T,
|
||||
etag: response.headers['etag']
|
||||
}
|
||||
} catch (e) {
|
||||
const response_1 = e.response as AxiosResponse
|
||||
if (response_1.status === 403 || response_1.status === 449) {
|
||||
this.parseAndSetCsrfToken(response_1)
|
||||
if (this.csrfToken) {
|
||||
return this.get<T>(url, accessToken, contentType, overrideHeaders)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public post<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, 'application/json'),
|
||||
...overrideHeaders
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.post<T>(url, data, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
return {
|
||||
result: response.data as T,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
const response = e.response as AxiosResponse
|
||||
if (response.status === 403 || response.status === 449) {
|
||||
this.parseAndSetCsrfToken(response)
|
||||
|
||||
if (this.csrfToken) {
|
||||
return this.post<T>(url, data, accessToken)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
})
|
||||
}
|
||||
|
||||
public async put<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, 'application/json'),
|
||||
...overrideHeaders
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.put<T>(url, data, {
|
||||
headers,
|
||||
withCredentials: true
|
||||
})
|
||||
return {
|
||||
result: response.data as T,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
} catch (e) {
|
||||
const response_1 = e.response as AxiosResponse
|
||||
if (response_1.status === 403 || response_1.status === 449) {
|
||||
this.parseAndSetCsrfToken(response_1)
|
||||
|
||||
if (this.csrfToken) {
|
||||
return this.put<T>(url, data, accessToken)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public async delete<T>(
|
||||
url: string,
|
||||
accessToken?: string
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = this.getHeaders(accessToken, 'application/json')
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
headers
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.delete<T>(url, requestConfig)
|
||||
return {
|
||||
result: response.data as T,
|
||||
etag: response.headers['etag']
|
||||
}
|
||||
} catch (e) {
|
||||
const response_1 = e.response as AxiosResponse
|
||||
if (response_1.status === 403 || response_1.status === 449) {
|
||||
this.parseAndSetCsrfToken(response_1)
|
||||
|
||||
if (this.csrfToken) {
|
||||
return this.delete<T>(url, accessToken)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public async patch<T>(
|
||||
url: string,
|
||||
data: any = {},
|
||||
accessToken?: string
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = this.getHeaders(accessToken, 'application/json')
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.patch<T>(url, data, {
|
||||
headers,
|
||||
withCredentials: true
|
||||
})
|
||||
return {
|
||||
result: response.data as T,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
} catch (e) {
|
||||
const response_1 = e.response as AxiosResponse
|
||||
if (response_1.status === 403 || response_1.status === 449) {
|
||||
this.parseAndSetCsrfToken(response_1)
|
||||
|
||||
if (this.csrfToken) {
|
||||
return this.patch<T>(url, accessToken)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadFile(
|
||||
url: string,
|
||||
content: string,
|
||||
accessToken?: string
|
||||
): Promise<any> {
|
||||
const headers = this.getHeaders(accessToken, 'application/json')
|
||||
|
||||
if (this.fileUploadCsrfToken?.value) {
|
||||
headers[
|
||||
this.fileUploadCsrfToken.headerName
|
||||
] = this.fileUploadCsrfToken.value
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.post(url, content, { headers })
|
||||
return {
|
||||
result: response.data,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
} catch (e) {
|
||||
const response_1 = e.response as AxiosResponse
|
||||
if (response_1.status === 403 || response_1.status === 449) {
|
||||
this.parseAndSetFileUploadCsrfToken(response_1)
|
||||
|
||||
if (this.fileUploadCsrfToken) {
|
||||
return this.uploadFile(url, content, accessToken)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
private getHeaders(accessToken: string | undefined, contentType: string) {
|
||||
const headers: any = {
|
||||
'Content-Type': contentType
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
if (this.csrfToken?.value) {
|
||||
headers[this.csrfToken.headerName] = this.csrfToken.value
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
private parseAndSetFileUploadCsrfToken(response: AxiosResponse) {
|
||||
const token = this.parseCsrfToken(response)
|
||||
|
||||
if (token) {
|
||||
this.fileUploadCsrfToken = token
|
||||
}
|
||||
}
|
||||
|
||||
private parseAndSetCsrfToken(response: AxiosResponse) {
|
||||
const token = this.parseCsrfToken(response)
|
||||
|
||||
if (token) {
|
||||
this.csrfToken = token
|
||||
}
|
||||
}
|
||||
|
||||
private parseCsrfToken(response: AxiosResponse): CsrfToken | undefined {
|
||||
const tokenHeader = (response.headers[
|
||||
'x-csrf-header'
|
||||
] as string)?.toLowerCase()
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers[tokenHeader]
|
||||
const csrfToken = {
|
||||
headerName: tokenHeader,
|
||||
value: token || ''
|
||||
}
|
||||
|
||||
return csrfToken
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user