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