mirror of
https://github.com/sasjs/adapter.git
synced 2026-01-03 10:40:06 +00:00
Merge pull request #432 from sasjs/job-refresh-tokens
fix(job-execution): refresh access token if it has expired during job status checks
This commit is contained in:
@@ -6,7 +6,7 @@ GREEN="\033[1;32m"
|
||||
# temporary file which holds the message).
|
||||
commit_message=$(cat "$1")
|
||||
|
||||
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-]+\))?!?: .+$") then
|
||||
if (echo "$commit_message" | grep -Eq "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\([a-z0-9 \-\*]+\))?!?: .+$") then
|
||||
echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
15148
package-lock.json
generated
15148
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -43,32 +43,33 @@
|
||||
"@types/tough-cookie": "^4.0.0",
|
||||
"cp": "^0.2.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"jest": "^27.0.4",
|
||||
"jest": "^27.0.6",
|
||||
"jest-extended": "^0.11.5",
|
||||
"mime": "^2.5.2",
|
||||
"path": "^0.12.7",
|
||||
"process": "^0.11.10",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^17.4.4",
|
||||
"terser-webpack-plugin": "^5.1.3",
|
||||
"terser-webpack-plugin": "^5.1.4",
|
||||
"ts-jest": "^27.0.3",
|
||||
"ts-loader": "^9.2.2",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.20.36",
|
||||
"typedoc": "^0.21.2",
|
||||
"typedoc-neo-theme": "^1.1.1",
|
||||
"typedoc-plugin-external-module-name": "^4.0.6",
|
||||
"typescript": "^4.3.2",
|
||||
"webpack": "^5.38.1",
|
||||
"typescript": "^4.3.4",
|
||||
"webpack": "^5.41.1",
|
||||
"webpack-cli": "^4.7.2"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^2.20.1",
|
||||
"@sasjs/utils": "^2.21.0",
|
||||
"axios": "^0.21.1",
|
||||
"axios-cookiejar-support": "^1.0.1",
|
||||
"form-data": "^4.0.0",
|
||||
"https": "^1.0.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"tough-cookie": "^4.0.0",
|
||||
"url": "^0.11.0"
|
||||
}
|
||||
|
||||
22100
sasjs-tests/package-lock.json
generated
22100
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -25,7 +25,7 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
'/Public/app/common/sendArr',
|
||||
data,
|
||||
{},
|
||||
'',
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
},
|
||||
|
||||
@@ -2,6 +2,7 @@ import { Context, EditContextInput, ContextAllAttributes } from './types'
|
||||
import { isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { AuthConfig } from '@sasjs/utils/types'
|
||||
|
||||
export class ContextManager {
|
||||
private defaultComputeContexts = [
|
||||
@@ -328,12 +329,12 @@ export class ContextManager {
|
||||
|
||||
public async getExecutableContexts(
|
||||
executeScript: Function,
|
||||
accessToken?: string
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
accessToken
|
||||
authConfig?.access_token
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while fetching compute contexts.')
|
||||
@@ -350,7 +351,7 @@ export class ContextManager {
|
||||
`test-${context.name}`,
|
||||
linesOfCode,
|
||||
context.name,
|
||||
accessToken,
|
||||
authConfig,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
|
||||
@@ -3,7 +3,9 @@ import {
|
||||
isRelativePath,
|
||||
isUri,
|
||||
isUrl,
|
||||
fetchLogByChunks
|
||||
fetchLogByChunks,
|
||||
isAccessTokenExpiring,
|
||||
isRefreshTokenExpiring
|
||||
} from './utils'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import {
|
||||
@@ -29,7 +31,7 @@ import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { SasAuthResponse, MacroVar } from '@sasjs/utils/types'
|
||||
import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import * as mime from 'mime'
|
||||
|
||||
@@ -130,14 +132,14 @@ export class SASViyaApiClient {
|
||||
|
||||
/**
|
||||
* Returns all compute contexts on this server that the user has access to.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
|
||||
*/
|
||||
public async getExecutableContexts(accessToken?: string) {
|
||||
public async getExecutableContexts(authConfig?: AuthConfig) {
|
||||
const bindedExecuteScript = this.executeScript.bind(this)
|
||||
|
||||
return await this.contextManager.getExecutableContexts(
|
||||
bindedExecuteScript,
|
||||
accessToken
|
||||
authConfig
|
||||
)
|
||||
}
|
||||
|
||||
@@ -266,7 +268,7 @@ export class SASViyaApiClient {
|
||||
* @param jobPath - the path to the file being submitted for execution.
|
||||
* @param linesOfCode - an array of code lines to execute.
|
||||
* @param contextName - the context to execute the code in.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
* @param authConfig - an object containing an access token, refresh token, client ID and secret.
|
||||
* @param data - execution data.
|
||||
* @param debug - when set to true, the log will be returned.
|
||||
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
|
||||
@@ -279,7 +281,7 @@ export class SASViyaApiClient {
|
||||
jobPath: string,
|
||||
linesOfCode: string[],
|
||||
contextName: string,
|
||||
accessToken?: string,
|
||||
authConfig?: AuthConfig,
|
||||
data = null,
|
||||
debug: boolean = false,
|
||||
expectWebout = false,
|
||||
@@ -288,17 +290,20 @@ export class SASViyaApiClient {
|
||||
printPid = false,
|
||||
variables?: MacroVar
|
||||
): Promise<any> {
|
||||
const { access_token } = authConfig || {}
|
||||
const logger = process.logger || console
|
||||
|
||||
try {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
||||
if (access_token) headers.Authorization = `Bearer ${access_token}`
|
||||
|
||||
let executionSessionId: string
|
||||
|
||||
const session = await this.sessionManager
|
||||
.getSession(accessToken)
|
||||
.getSession(access_token)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting session. ')
|
||||
})
|
||||
@@ -307,7 +312,7 @@ export class SASViyaApiClient {
|
||||
|
||||
if (printPid) {
|
||||
const { result: jobIdVariable } = await this.sessionManager
|
||||
.getVariable(executionSessionId, 'SYSJOBID', accessToken)
|
||||
.getVariable(executionSessionId, 'SYSJOBID', access_token)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting session variable. ')
|
||||
})
|
||||
@@ -367,7 +372,7 @@ export class SASViyaApiClient {
|
||||
|
||||
if (data) {
|
||||
if (JSON.stringify(data).includes(';')) {
|
||||
files = await this.uploadTables(data, accessToken).catch((err) => {
|
||||
files = await this.uploadTables(data, access_token).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while uploading tables. ')
|
||||
})
|
||||
|
||||
@@ -397,7 +402,7 @@ export class SASViyaApiClient {
|
||||
.post<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs`,
|
||||
jobRequestBody,
|
||||
accessToken
|
||||
access_token
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while posting job. ')
|
||||
@@ -406,8 +411,8 @@ export class SASViyaApiClient {
|
||||
if (!waitForResult) return session
|
||||
|
||||
if (debug) {
|
||||
console.log(`Job has been submitted for '${fileName}'.`)
|
||||
console.log(
|
||||
logger.info(`Job has been submitted for '${fileName}'.`)
|
||||
logger.info(
|
||||
`You can monitor the job progress at '${this.serverUrl}${
|
||||
postedJob.links.find((l: any) => l.rel === 'state')!.href
|
||||
}'.`
|
||||
@@ -417,7 +422,7 @@ export class SASViyaApiClient {
|
||||
const jobStatus = await this.pollJobState(
|
||||
postedJob,
|
||||
etag,
|
||||
accessToken,
|
||||
authConfig,
|
||||
pollOptions
|
||||
).catch(async (err) => {
|
||||
const error = err?.response?.data
|
||||
@@ -430,7 +435,7 @@ export class SASViyaApiClient {
|
||||
const logCount = 1000000
|
||||
err.log = await fetchLogByChunks(
|
||||
this.requestClient,
|
||||
accessToken!,
|
||||
access_token!,
|
||||
sessionLogUrl,
|
||||
logCount
|
||||
)
|
||||
@@ -441,7 +446,7 @@ export class SASViyaApiClient {
|
||||
const { result: currentJob } = await this.requestClient
|
||||
.get<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||
accessToken
|
||||
access_token
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting job. ')
|
||||
@@ -457,7 +462,7 @@ export class SASViyaApiClient {
|
||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||
log = await fetchLogByChunks(
|
||||
this.requestClient,
|
||||
accessToken!,
|
||||
access_token!,
|
||||
logUrl,
|
||||
logCount
|
||||
)
|
||||
@@ -477,7 +482,7 @@ export class SASViyaApiClient {
|
||||
|
||||
if (resultLink) {
|
||||
jobResult = await this.requestClient
|
||||
.get<any>(resultLink, accessToken, 'text/plain')
|
||||
.get<any>(resultLink, access_token, 'text/plain')
|
||||
.catch(async (e) => {
|
||||
if (e instanceof NotFoundError) {
|
||||
if (logLink) {
|
||||
@@ -485,7 +490,7 @@ export class SASViyaApiClient {
|
||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||
log = await fetchLogByChunks(
|
||||
this.requestClient,
|
||||
accessToken!,
|
||||
access_token!,
|
||||
logUrl,
|
||||
logCount
|
||||
)
|
||||
@@ -504,7 +509,7 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
await this.sessionManager
|
||||
.clearSession(executionSessionId, accessToken)
|
||||
.clearSession(executionSessionId, access_token)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while clearing session. ')
|
||||
})
|
||||
@@ -516,7 +521,7 @@ export class SASViyaApiClient {
|
||||
jobPath,
|
||||
linesOfCode,
|
||||
contextName,
|
||||
accessToken,
|
||||
authConfig,
|
||||
data,
|
||||
debug,
|
||||
false,
|
||||
@@ -603,6 +608,7 @@ export class SASViyaApiClient {
|
||||
accessToken?: string,
|
||||
isForced?: boolean
|
||||
): Promise<Folder> {
|
||||
const logger = process.logger || console
|
||||
if (!parentFolderPath && !parentFolderUri) {
|
||||
throw new Error('Path or URI of the parent folder is required.')
|
||||
}
|
||||
@@ -610,7 +616,7 @@ export class SASViyaApiClient {
|
||||
if (!parentFolderUri && parentFolderPath) {
|
||||
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
||||
if (!parentFolderUri) {
|
||||
console.log(
|
||||
logger.info(
|
||||
`Parent folder at path '${parentFolderPath}' is not present.`
|
||||
)
|
||||
|
||||
@@ -622,7 +628,7 @@ export class SASViyaApiClient {
|
||||
if (newParentFolderPath === '') {
|
||||
throw new Error('Root folder has to be present on the server.')
|
||||
}
|
||||
console.log(
|
||||
logger.info(
|
||||
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
|
||||
)
|
||||
const parentFolder = await this.createFolder(
|
||||
@@ -631,7 +637,7 @@ export class SASViyaApiClient {
|
||||
undefined,
|
||||
accessToken
|
||||
)
|
||||
console.log(
|
||||
logger.info(
|
||||
`Parent folder '${newFolderName}' has been successfully created.`
|
||||
)
|
||||
parentFolderUri = `/folders/folders/${parentFolder.id}`
|
||||
@@ -873,13 +879,15 @@ export class SASViyaApiClient {
|
||||
contextName: string,
|
||||
debug?: boolean,
|
||||
data?: any,
|
||||
accessToken?: string,
|
||||
authConfig?: AuthConfig,
|
||||
waitForResult = true,
|
||||
expectWebout = false,
|
||||
pollOptions?: PollOptions,
|
||||
printPid = false,
|
||||
variables?: MacroVar
|
||||
) {
|
||||
let { access_token } = authConfig || {}
|
||||
|
||||
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||
throw new Error(
|
||||
'Relative paths cannot be used without specifying a root folder name'
|
||||
@@ -893,7 +901,7 @@ export class SASViyaApiClient {
|
||||
? `${this.rootFolderName}/${folderPath}`
|
||||
: folderPath
|
||||
|
||||
await this.populateFolderMap(fullFolderPath, accessToken).catch((err) => {
|
||||
await this.populateFolderMap(fullFolderPath, access_token).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while populating folder map. ')
|
||||
})
|
||||
|
||||
@@ -907,8 +915,8 @@ export class SASViyaApiClient {
|
||||
|
||||
const headers: any = { 'Content-Type': 'application/json' }
|
||||
|
||||
if (!!accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
if (!!access_token) {
|
||||
headers.Authorization = `Bearer ${access_token}`
|
||||
}
|
||||
|
||||
const jobToExecute = jobFolder?.find((item) => item.name === jobName)
|
||||
@@ -931,7 +939,7 @@ export class SASViyaApiClient {
|
||||
const { result: jobDefinition } = await this.requestClient
|
||||
.get<JobDefinition>(
|
||||
`${this.serverUrl}${jobDefinitionLink.href}`,
|
||||
accessToken
|
||||
access_token
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting job definition. ')
|
||||
@@ -951,7 +959,7 @@ export class SASViyaApiClient {
|
||||
sasJob,
|
||||
linesToExecute,
|
||||
contextName,
|
||||
accessToken,
|
||||
authConfig,
|
||||
data,
|
||||
debug,
|
||||
expectWebout,
|
||||
@@ -975,8 +983,9 @@ export class SASViyaApiClient {
|
||||
contextName: string,
|
||||
debug: boolean,
|
||||
data?: any,
|
||||
accessToken?: string
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
let { access_token } = authConfig || {}
|
||||
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||
throw new Error(
|
||||
'Relative paths cannot be used without specifying a root folder name.'
|
||||
@@ -989,7 +998,7 @@ export class SASViyaApiClient {
|
||||
const fullFolderPath = isRelativePath(sasJob)
|
||||
? `${this.rootFolderName}/${folderPath}`
|
||||
: folderPath
|
||||
await this.populateFolderMap(fullFolderPath, accessToken)
|
||||
await this.populateFolderMap(fullFolderPath, access_token)
|
||||
|
||||
const jobFolder = this.folderMap.get(fullFolderPath)
|
||||
if (!jobFolder) {
|
||||
@@ -1002,7 +1011,7 @@ export class SASViyaApiClient {
|
||||
|
||||
let files: any[] = []
|
||||
if (data && Object.keys(data).length) {
|
||||
files = await this.uploadTables(data, accessToken)
|
||||
files = await this.uploadTables(data, access_token)
|
||||
}
|
||||
|
||||
if (!jobToExecute) {
|
||||
@@ -1014,7 +1023,7 @@ export class SASViyaApiClient {
|
||||
|
||||
const { result: jobDefinition } = await this.requestClient.get<Job>(
|
||||
`${this.serverUrl}${jobDefinitionLink}`,
|
||||
accessToken
|
||||
access_token
|
||||
)
|
||||
|
||||
const jobArguments: { [key: string]: any } = {
|
||||
@@ -1050,18 +1059,18 @@ export class SASViyaApiClient {
|
||||
const { result: postedJob, etag } = await this.requestClient.post<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||
postJobRequestBody,
|
||||
accessToken
|
||||
access_token
|
||||
)
|
||||
const jobStatus = await this.pollJobState(
|
||||
postedJob,
|
||||
etag,
|
||||
accessToken
|
||||
authConfig
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while polling job status. ')
|
||||
})
|
||||
const { result: currentJob } = await this.requestClient.get<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||
accessToken
|
||||
access_token
|
||||
)
|
||||
|
||||
let jobResult
|
||||
@@ -1072,13 +1081,13 @@ export class SASViyaApiClient {
|
||||
if (resultLink) {
|
||||
jobResult = await this.requestClient.get<any>(
|
||||
`${this.serverUrl}${resultLink}/content`,
|
||||
accessToken,
|
||||
access_token,
|
||||
'text/plain'
|
||||
)
|
||||
}
|
||||
if (debug && logLink) {
|
||||
log = await this.requestClient
|
||||
.get<any>(`${this.serverUrl}${logLink.href}/content`, accessToken)
|
||||
.get<any>(`${this.serverUrl}${logLink.href}/content`, access_token)
|
||||
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
|
||||
}
|
||||
if (jobStatus === 'failed') {
|
||||
@@ -1128,12 +1137,30 @@ export class SASViyaApiClient {
|
||||
private async pollJobState(
|
||||
postedJob: any,
|
||||
etag: string | null,
|
||||
accessToken?: string,
|
||||
authConfig?: AuthConfig,
|
||||
pollOptions?: PollOptions
|
||||
) {
|
||||
const logger = process.logger || console
|
||||
|
||||
let POLL_INTERVAL = 300
|
||||
let MAX_POLL_COUNT = 1000
|
||||
let MAX_ERROR_COUNT = 5
|
||||
let { access_token, refresh_token, client, secret } = authConfig || {}
|
||||
if (access_token && refresh_token) {
|
||||
if (
|
||||
client &&
|
||||
secret &&
|
||||
refresh_token &&
|
||||
(isAccessTokenExpiring(access_token) ||
|
||||
isRefreshTokenExpiring(refresh_token))
|
||||
) {
|
||||
;({ access_token, refresh_token } = await this.refreshTokens(
|
||||
client,
|
||||
secret,
|
||||
refresh_token
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if (pollOptions) {
|
||||
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
|
||||
@@ -1147,8 +1174,8 @@ export class SASViyaApiClient {
|
||||
'Content-Type': 'application/json',
|
||||
'If-None-Match': etag
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
if (access_token) {
|
||||
headers.Authorization = `Bearer ${access_token}`
|
||||
}
|
||||
const stateLink = postedJob.links.find((l: any) => l.rel === 'state')
|
||||
if (!stateLink) {
|
||||
@@ -1158,7 +1185,7 @@ export class SASViyaApiClient {
|
||||
const { result: state } = await this.requestClient
|
||||
.get<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
|
||||
accessToken,
|
||||
access_token,
|
||||
'text/plain',
|
||||
{},
|
||||
this.debug
|
||||
@@ -1186,11 +1213,27 @@ export class SASViyaApiClient {
|
||||
postedJobState === 'pending' ||
|
||||
postedJobState === 'unavailable'
|
||||
) {
|
||||
if (access_token && refresh_token) {
|
||||
if (
|
||||
client &&
|
||||
secret &&
|
||||
refresh_token &&
|
||||
(isAccessTokenExpiring(access_token) ||
|
||||
isRefreshTokenExpiring(refresh_token))
|
||||
) {
|
||||
;({ access_token, refresh_token } = await this.refreshTokens(
|
||||
client,
|
||||
secret,
|
||||
refresh_token
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
if (stateLink) {
|
||||
const { result: jobState } = await this.requestClient
|
||||
.get<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
|
||||
accessToken,
|
||||
access_token,
|
||||
'text/plain',
|
||||
{},
|
||||
this.debug
|
||||
@@ -1219,8 +1262,8 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
if (this.debug && printedState !== postedJobState) {
|
||||
console.log('Polling job status...')
|
||||
console.log(`Current job state: ${postedJobState}`)
|
||||
logger.info('Polling job status...')
|
||||
logger.info(`Current job state: ${postedJobState}`)
|
||||
|
||||
printedState = postedJobState
|
||||
}
|
||||
@@ -1410,6 +1453,9 @@ export class SASViyaApiClient {
|
||||
accessToken
|
||||
)
|
||||
|
||||
if (!sourceFolderUri) {
|
||||
return undefined
|
||||
}
|
||||
const sourceFolderId = sourceFolderUri?.split('/').pop()
|
||||
|
||||
const { result: folder } = await this.requestClient
|
||||
|
||||
28
src/SASjs.ts
28
src/SASjs.ts
@@ -4,7 +4,7 @@ import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { SAS9ApiClient } from './SAS9ApiClient'
|
||||
import { FileUploader } from './FileUploader'
|
||||
import { AuthManager } from './auth'
|
||||
import { ServerType, MacroVar } from '@sasjs/utils/types'
|
||||
import { ServerType, MacroVar, AuthConfig } from '@sasjs/utils/types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import {
|
||||
JobExecutor,
|
||||
@@ -103,12 +103,12 @@ export default class SASjs {
|
||||
|
||||
/**
|
||||
* Gets executable compute contexts.
|
||||
* @param accessToken - an access token for an authorized user.
|
||||
* @param authConfig - an access token, refresh token, client and secret for an authorized user.
|
||||
*/
|
||||
public async getExecutableContexts(accessToken: string) {
|
||||
public async getExecutableContexts(authConfig: AuthConfig) {
|
||||
this.isMethodSupported('getExecutableContexts', ServerType.SasViya)
|
||||
|
||||
return await this.sasViyaApiClient!.getExecutableContexts(accessToken)
|
||||
return await this.sasViyaApiClient!.getExecutableContexts(authConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -240,14 +240,14 @@ export default class SASjs {
|
||||
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
|
||||
* @param linesOfCode - lines of sas code from the file to run.
|
||||
* @param contextName - context name on which code will be run on the server.
|
||||
* @param accessToken - (optional) the access token for authorizing the request.
|
||||
* @param authConfig - (optional) the access token, refresh token, client and secret for authorizing the request.
|
||||
* @param debug - (optional) if true, global debug config will be overriden
|
||||
*/
|
||||
public async executeScriptSASViya(
|
||||
fileName: string,
|
||||
linesOfCode: string[],
|
||||
contextName: string,
|
||||
accessToken?: string,
|
||||
authConfig?: AuthConfig,
|
||||
debug?: boolean
|
||||
) {
|
||||
this.isMethodSupported('executeScriptSASViya', ServerType.SasViya)
|
||||
@@ -261,7 +261,7 @@ export default class SASjs {
|
||||
fileName,
|
||||
linesOfCode,
|
||||
contextName,
|
||||
accessToken,
|
||||
authConfig,
|
||||
null,
|
||||
debug ? debug : this.sasjsConfig.debug
|
||||
)
|
||||
@@ -579,7 +579,7 @@ export default class SASjs {
|
||||
data: { [key: string]: any } | null,
|
||||
config: { [key: string]: any } = {},
|
||||
loginRequiredCallback?: () => any,
|
||||
accessToken?: string,
|
||||
authConfig?: AuthConfig,
|
||||
extraResponseAttributes: ExtraResponseAttributes[] = []
|
||||
) {
|
||||
config = {
|
||||
@@ -601,7 +601,7 @@ export default class SASjs {
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
accessToken
|
||||
authConfig
|
||||
)
|
||||
} else {
|
||||
return await this.jesJobExecutor!.execute(
|
||||
@@ -609,7 +609,7 @@ export default class SASjs {
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
accessToken,
|
||||
authConfig,
|
||||
extraResponseAttributes
|
||||
)
|
||||
}
|
||||
@@ -625,7 +625,7 @@ export default class SASjs {
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
accessToken,
|
||||
authConfig,
|
||||
extraResponseAttributes
|
||||
)
|
||||
}
|
||||
@@ -776,7 +776,7 @@ export default class SASjs {
|
||||
* @param config - provide any changes to the config here, for instance to
|
||||
* enable/disable `debug`. Any change provided will override the global config,
|
||||
* for that particular function call.
|
||||
* @param accessToken - a valid access token that is authorised to execute compute jobs.
|
||||
* @param authConfig - a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
|
||||
* The access token is not required when the user is authenticated via the browser.
|
||||
* @param waitForResult - a boolean that indicates whether the function needs to wait for execution to complete.
|
||||
* @param pollOptions - an object that represents poll interval(milliseconds) and maximum amount of attempts. Object example: { MAX_POLL_COUNT: 24 * 60 * 60, POLL_INTERVAL: 1000 }.
|
||||
@@ -787,7 +787,7 @@ export default class SASjs {
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any = {},
|
||||
accessToken?: string,
|
||||
authConfig?: AuthConfig,
|
||||
waitForResult?: boolean,
|
||||
pollOptions?: PollOptions,
|
||||
printPid = false,
|
||||
@@ -810,7 +810,7 @@ export default class SASjs {
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken,
|
||||
authConfig,
|
||||
!!waitForResult,
|
||||
false,
|
||||
pollOptions,
|
||||
|
||||
@@ -158,6 +158,8 @@ export class SessionManager {
|
||||
etag: string | null,
|
||||
accessToken?: string
|
||||
) {
|
||||
const logger = process.logger || console
|
||||
|
||||
let sessionState = session.state
|
||||
|
||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
||||
@@ -170,7 +172,7 @@ export class SessionManager {
|
||||
) {
|
||||
if (stateLink) {
|
||||
if (this.debug && !this.printedSessionState.printed) {
|
||||
console.log('Polling session status...')
|
||||
logger.info('Polling session status...')
|
||||
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
@@ -186,7 +188,7 @@ export class SessionManager {
|
||||
sessionState = state.trim()
|
||||
|
||||
if (this.debug && this.printedSessionState.state !== sessionState) {
|
||||
console.log(`Current session state is '${sessionState}'`)
|
||||
logger.info(`Current session state is '${sessionState}'`)
|
||||
|
||||
this.printedSessionState.state = sessionState
|
||||
this.printedSessionState.printed = false
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { isAuthorizeFormRequired } from '.'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { serialize } from '../utils'
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import {
|
||||
ErrorResponse,
|
||||
@@ -17,7 +17,7 @@ export class ComputeJobExecutor extends BaseJobExecutor {
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
const waitForResult = true
|
||||
@@ -30,7 +30,7 @@ export class ComputeJobExecutor extends BaseJobExecutor {
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken,
|
||||
authConfig,
|
||||
waitForResult,
|
||||
expectWebout
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import {
|
||||
ErrorResponse,
|
||||
@@ -18,20 +18,14 @@ export class JesJobExecutor extends BaseJobExecutor {
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string,
|
||||
authConfig?: AuthConfig,
|
||||
extraResponseAttributes: ExtraResponseAttributes[] = []
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
|
||||
const requestPromise = new Promise((resolve, reject) => {
|
||||
this.sasViyaApiClient
|
||||
?.executeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken
|
||||
)
|
||||
?.executeJob(sasJob, config.contextName, config.debug, data, authConfig)
|
||||
.then((response: any) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
|
||||
@@ -69,7 +63,7 @@ export class JesJobExecutor extends BaseJobExecutor {
|
||||
data,
|
||||
config,
|
||||
loginRequiredCallback,
|
||||
accessToken,
|
||||
authConfig,
|
||||
extraResponseAttributes
|
||||
).then(
|
||||
(res: any) => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { AuthConfig, ServerType } from '@sasjs/utils/types'
|
||||
import { SASjsRequest } from '../types'
|
||||
import { ExtraResponseAttributes } from '@sasjs/utils/types'
|
||||
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
|
||||
@@ -11,7 +11,7 @@ export interface JobExecutor {
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string,
|
||||
authConfig?: AuthConfig,
|
||||
extraResponseAttributes?: ExtraResponseAttributes[]
|
||||
) => Promise<any>
|
||||
resendWaitingRequests: () => Promise<void>
|
||||
@@ -30,7 +30,7 @@ export abstract class BaseJobExecutor implements JobExecutor {
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string | undefined,
|
||||
authConfig?: AuthConfig | undefined,
|
||||
extraResponseAttributes?: ExtraResponseAttributes[]
|
||||
): Promise<any>
|
||||
|
||||
|
||||
@@ -287,7 +287,8 @@ export class RequestClient implements HttpClient {
|
||||
})
|
||||
.then((res) => res.data)
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
const logger = process.logger || console
|
||||
logger.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
5
src/types/Process.d.ts
vendored
Normal file
5
src/types/Process.d.ts
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
declare namespace NodeJS {
|
||||
export interface Process {
|
||||
logger?: import('@sasjs/utils/logger').Logger
|
||||
}
|
||||
}
|
||||
35
src/utils/auth.ts
Normal file
35
src/utils/auth.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import jwtDecode from 'jwt-decode'
|
||||
|
||||
/**
|
||||
* Checks if the Access Token is expired or is expiring in 1 hour. A default Access Token
|
||||
* lasts 12 hours. If the Access Token expires, the Refresh Token is used to fetch a new
|
||||
* Access Token. In the case that the Refresh Token is expired, 1 hour is enough to let
|
||||
* most jobs finish.
|
||||
* @param {string} token- token string that will be evaluated
|
||||
*/
|
||||
export function isAccessTokenExpiring(token: string): boolean {
|
||||
if (!token) {
|
||||
return true
|
||||
}
|
||||
const payload = jwtDecode<{ exp: number }>(token)
|
||||
const timeToLive = payload.exp - new Date().valueOf() / 1000
|
||||
|
||||
return timeToLive <= 60 * 60 // 1 hour
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the Refresh Token is expired or expiring in 30 secs. A default Refresh Token
|
||||
* lasts 30 days. Once the Refresh Token expires, the user must re-authenticate (provide
|
||||
* credentials in a browser to obtain an authorisation code). 30 seconds is enough time
|
||||
* to make a request for a final Access Token.
|
||||
* @param {string} token- token string that will be evaluated
|
||||
*/
|
||||
export function isRefreshTokenExpiring(token?: string): boolean {
|
||||
if (!token) {
|
||||
return true
|
||||
}
|
||||
const payload = jwtDecode<{ exp: number }>(token)
|
||||
const timeToLive = payload.exp - new Date().valueOf() / 1000
|
||||
|
||||
return timeToLive <= 30 // 30 seconds
|
||||
}
|
||||
@@ -15,12 +15,14 @@ export const fetchLogByChunks = async (
|
||||
logUrl: string,
|
||||
logCount: number
|
||||
): Promise<string> => {
|
||||
const logger = process.logger || console
|
||||
|
||||
let log: string = ''
|
||||
|
||||
const loglimit = logCount < 10000 ? logCount : 10000
|
||||
let start = 0
|
||||
do {
|
||||
console.log(
|
||||
logger.info(
|
||||
`Fetching logs from line no: ${start + 1} to ${
|
||||
start + loglimit
|
||||
} of ${logCount}.`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './asyncForEach'
|
||||
export * from './auth'
|
||||
export * from './compareTimestamps'
|
||||
export * from './convertToCsv'
|
||||
export * from './isRelativePath'
|
||||
|
||||
Reference in New Issue
Block a user