1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-16 00:20: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:
Krishna Acondy
2021-06-30 11:10:34 +01:00
committed by GitHub
18 changed files with 36575 additions and 946 deletions

View File

@@ -6,7 +6,7 @@ GREEN="\033[1;32m"
# temporary file which holds the message). # temporary file which holds the message).
commit_message=$(cat "$1") 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" echo "${GREEN} ✔ Commit message meets Conventional Commit standards"
exit 0 exit 0
fi fi

15148
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -43,32 +43,33 @@
"@types/tough-cookie": "^4.0.0", "@types/tough-cookie": "^4.0.0",
"cp": "^0.2.0", "cp": "^0.2.0",
"dotenv": "^10.0.0", "dotenv": "^10.0.0",
"jest": "^27.0.4", "jest": "^27.0.6",
"jest-extended": "^0.11.5", "jest-extended": "^0.11.5",
"mime": "^2.5.2", "mime": "^2.5.2",
"path": "^0.12.7", "path": "^0.12.7",
"process": "^0.11.10", "process": "^0.11.10",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semantic-release": "^17.4.4", "semantic-release": "^17.4.4",
"terser-webpack-plugin": "^5.1.3", "terser-webpack-plugin": "^5.1.4",
"ts-jest": "^27.0.3", "ts-jest": "^27.0.3",
"ts-loader": "^9.2.2", "ts-loader": "^9.2.2",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",
"typedoc": "^0.20.36", "typedoc": "^0.21.2",
"typedoc-neo-theme": "^1.1.1", "typedoc-neo-theme": "^1.1.1",
"typedoc-plugin-external-module-name": "^4.0.6", "typedoc-plugin-external-module-name": "^4.0.6",
"typescript": "^4.3.2", "typescript": "^4.3.4",
"webpack": "^5.38.1", "webpack": "^5.41.1",
"webpack-cli": "^4.7.2" "webpack-cli": "^4.7.2"
}, },
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@sasjs/utils": "^2.20.1", "@sasjs/utils": "^2.21.0",
"axios": "^0.21.1", "axios": "^0.21.1",
"axios-cookiejar-support": "^1.0.1", "axios-cookiejar-support": "^1.0.1",
"form-data": "^4.0.0", "form-data": "^4.0.0",
"https": "^1.0.0", "https": "^1.0.0",
"jwt-decode": "^3.1.2",
"tough-cookie": "^4.0.0", "tough-cookie": "^4.0.0",
"url": "^0.11.0" "url": "^0.11.0"
} }

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,7 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
'/Public/app/common/sendArr', '/Public/app/common/sendArr',
data, data,
{}, {},
'', undefined,
true true
) )
}, },

View File

@@ -2,6 +2,7 @@ import { Context, EditContextInput, ContextAllAttributes } from './types'
import { isUrl } from './utils' import { isUrl } from './utils'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
import { AuthConfig } from '@sasjs/utils/types'
export class ContextManager { export class ContextManager {
private defaultComputeContexts = [ private defaultComputeContexts = [
@@ -328,12 +329,12 @@ export class ContextManager {
public async getExecutableContexts( public async getExecutableContexts(
executeScript: Function, executeScript: Function,
accessToken?: string authConfig?: AuthConfig
) { ) {
const { result: contexts } = await this.requestClient const { result: contexts } = await this.requestClient
.get<{ items: Context[] }>( .get<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?limit=10000`, `${this.serverUrl}/compute/contexts?limit=10000`,
accessToken authConfig?.access_token
) )
.catch((err) => { .catch((err) => {
throw prefixMessage(err, 'Error while fetching compute contexts.') throw prefixMessage(err, 'Error while fetching compute contexts.')
@@ -350,7 +351,7 @@ export class ContextManager {
`test-${context.name}`, `test-${context.name}`,
linesOfCode, linesOfCode,
context.name, context.name,
accessToken, authConfig,
null, null,
false, false,
true, true,

View File

@@ -3,7 +3,9 @@ import {
isRelativePath, isRelativePath,
isUri, isUri,
isUrl, isUrl,
fetchLogByChunks fetchLogByChunks,
isAccessTokenExpiring,
isRefreshTokenExpiring
} from './utils' } from './utils'
import * as NodeFormData from 'form-data' import * as NodeFormData from 'form-data'
import { import {
@@ -29,7 +31,7 @@ import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
import { Logger, LogLevel } from '@sasjs/utils/logger' import { Logger, LogLevel } from '@sasjs/utils/logger'
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired' import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
import { RequestClient } from './request/RequestClient' 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 { prefixMessage } from '@sasjs/utils/error'
import * as mime from 'mime' 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. * 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) const bindedExecuteScript = this.executeScript.bind(this)
return await this.contextManager.getExecutableContexts( return await this.contextManager.getExecutableContexts(
bindedExecuteScript, bindedExecuteScript,
accessToken authConfig
) )
} }
@@ -266,7 +268,7 @@ export class SASViyaApiClient {
* @param jobPath - the path to the file being submitted for execution. * @param jobPath - the path to the file being submitted for execution.
* @param linesOfCode - an array of code lines to execute. * @param linesOfCode - an array of code lines to execute.
* @param contextName - the context to execute the code in. * @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 data - execution data.
* @param debug - when set to true, the log will be returned. * @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). * @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, jobPath: string,
linesOfCode: string[], linesOfCode: string[],
contextName: string, contextName: string,
accessToken?: string, authConfig?: AuthConfig,
data = null, data = null,
debug: boolean = false, debug: boolean = false,
expectWebout = false, expectWebout = false,
@@ -288,17 +290,20 @@ export class SASViyaApiClient {
printPid = false, printPid = false,
variables?: MacroVar variables?: MacroVar
): Promise<any> { ): Promise<any> {
const { access_token } = authConfig || {}
const logger = process.logger || console
try { try {
const headers: any = { const headers: any = {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
if (accessToken) headers.Authorization = `Bearer ${accessToken}` if (access_token) headers.Authorization = `Bearer ${access_token}`
let executionSessionId: string let executionSessionId: string
const session = await this.sessionManager const session = await this.sessionManager
.getSession(accessToken) .getSession(access_token)
.catch((err) => { .catch((err) => {
throw prefixMessage(err, 'Error while getting session. ') throw prefixMessage(err, 'Error while getting session. ')
}) })
@@ -307,7 +312,7 @@ export class SASViyaApiClient {
if (printPid) { if (printPid) {
const { result: jobIdVariable } = await this.sessionManager const { result: jobIdVariable } = await this.sessionManager
.getVariable(executionSessionId, 'SYSJOBID', accessToken) .getVariable(executionSessionId, 'SYSJOBID', access_token)
.catch((err) => { .catch((err) => {
throw prefixMessage(err, 'Error while getting session variable. ') throw prefixMessage(err, 'Error while getting session variable. ')
}) })
@@ -367,7 +372,7 @@ export class SASViyaApiClient {
if (data) { if (data) {
if (JSON.stringify(data).includes(';')) { 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. ') throw prefixMessage(err, 'Error while uploading tables. ')
}) })
@@ -397,7 +402,7 @@ export class SASViyaApiClient {
.post<Job>( .post<Job>(
`/compute/sessions/${executionSessionId}/jobs`, `/compute/sessions/${executionSessionId}/jobs`,
jobRequestBody, jobRequestBody,
accessToken access_token
) )
.catch((err) => { .catch((err) => {
throw prefixMessage(err, 'Error while posting job. ') throw prefixMessage(err, 'Error while posting job. ')
@@ -406,8 +411,8 @@ export class SASViyaApiClient {
if (!waitForResult) return session if (!waitForResult) return session
if (debug) { if (debug) {
console.log(`Job has been submitted for '${fileName}'.`) logger.info(`Job has been submitted for '${fileName}'.`)
console.log( logger.info(
`You can monitor the job progress at '${this.serverUrl}${ `You can monitor the job progress at '${this.serverUrl}${
postedJob.links.find((l: any) => l.rel === 'state')!.href postedJob.links.find((l: any) => l.rel === 'state')!.href
}'.` }'.`
@@ -417,7 +422,7 @@ export class SASViyaApiClient {
const jobStatus = await this.pollJobState( const jobStatus = await this.pollJobState(
postedJob, postedJob,
etag, etag,
accessToken, authConfig,
pollOptions pollOptions
).catch(async (err) => { ).catch(async (err) => {
const error = err?.response?.data const error = err?.response?.data
@@ -430,7 +435,7 @@ export class SASViyaApiClient {
const logCount = 1000000 const logCount = 1000000
err.log = await fetchLogByChunks( err.log = await fetchLogByChunks(
this.requestClient, this.requestClient,
accessToken!, access_token!,
sessionLogUrl, sessionLogUrl,
logCount logCount
) )
@@ -441,7 +446,7 @@ export class SASViyaApiClient {
const { result: currentJob } = await this.requestClient const { result: currentJob } = await this.requestClient
.get<Job>( .get<Job>(
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`, `/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
accessToken access_token
) )
.catch((err) => { .catch((err) => {
throw prefixMessage(err, 'Error while getting job. ') throw prefixMessage(err, 'Error while getting job. ')
@@ -457,7 +462,7 @@ export class SASViyaApiClient {
const logCount = currentJob.logStatistics?.lineCount ?? 1000000 const logCount = currentJob.logStatistics?.lineCount ?? 1000000
log = await fetchLogByChunks( log = await fetchLogByChunks(
this.requestClient, this.requestClient,
accessToken!, access_token!,
logUrl, logUrl,
logCount logCount
) )
@@ -477,7 +482,7 @@ export class SASViyaApiClient {
if (resultLink) { if (resultLink) {
jobResult = await this.requestClient jobResult = await this.requestClient
.get<any>(resultLink, accessToken, 'text/plain') .get<any>(resultLink, access_token, 'text/plain')
.catch(async (e) => { .catch(async (e) => {
if (e instanceof NotFoundError) { if (e instanceof NotFoundError) {
if (logLink) { if (logLink) {
@@ -485,7 +490,7 @@ export class SASViyaApiClient {
const logCount = currentJob.logStatistics?.lineCount ?? 1000000 const logCount = currentJob.logStatistics?.lineCount ?? 1000000
log = await fetchLogByChunks( log = await fetchLogByChunks(
this.requestClient, this.requestClient,
accessToken!, access_token!,
logUrl, logUrl,
logCount logCount
) )
@@ -504,7 +509,7 @@ export class SASViyaApiClient {
} }
await this.sessionManager await this.sessionManager
.clearSession(executionSessionId, accessToken) .clearSession(executionSessionId, access_token)
.catch((err) => { .catch((err) => {
throw prefixMessage(err, 'Error while clearing session. ') throw prefixMessage(err, 'Error while clearing session. ')
}) })
@@ -516,7 +521,7 @@ export class SASViyaApiClient {
jobPath, jobPath,
linesOfCode, linesOfCode,
contextName, contextName,
accessToken, authConfig,
data, data,
debug, debug,
false, false,
@@ -603,6 +608,7 @@ export class SASViyaApiClient {
accessToken?: string, accessToken?: string,
isForced?: boolean isForced?: boolean
): Promise<Folder> { ): Promise<Folder> {
const logger = process.logger || console
if (!parentFolderPath && !parentFolderUri) { if (!parentFolderPath && !parentFolderUri) {
throw new Error('Path or URI of the parent folder is required.') throw new Error('Path or URI of the parent folder is required.')
} }
@@ -610,7 +616,7 @@ export class SASViyaApiClient {
if (!parentFolderUri && parentFolderPath) { if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken) parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
if (!parentFolderUri) { if (!parentFolderUri) {
console.log( logger.info(
`Parent folder at path '${parentFolderPath}' is not present.` `Parent folder at path '${parentFolderPath}' is not present.`
) )
@@ -622,7 +628,7 @@ export class SASViyaApiClient {
if (newParentFolderPath === '') { if (newParentFolderPath === '') {
throw new Error('Root folder has to be present on the server.') throw new Error('Root folder has to be present on the server.')
} }
console.log( logger.info(
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'` `Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
) )
const parentFolder = await this.createFolder( const parentFolder = await this.createFolder(
@@ -631,7 +637,7 @@ export class SASViyaApiClient {
undefined, undefined,
accessToken accessToken
) )
console.log( logger.info(
`Parent folder '${newFolderName}' has been successfully created.` `Parent folder '${newFolderName}' has been successfully created.`
) )
parentFolderUri = `/folders/folders/${parentFolder.id}` parentFolderUri = `/folders/folders/${parentFolder.id}`
@@ -873,13 +879,15 @@ export class SASViyaApiClient {
contextName: string, contextName: string,
debug?: boolean, debug?: boolean,
data?: any, data?: any,
accessToken?: string, authConfig?: AuthConfig,
waitForResult = true, waitForResult = true,
expectWebout = false, expectWebout = false,
pollOptions?: PollOptions, pollOptions?: PollOptions,
printPid = false, printPid = false,
variables?: MacroVar variables?: MacroVar
) { ) {
let { access_token } = authConfig || {}
if (isRelativePath(sasJob) && !this.rootFolderName) { if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error( throw new Error(
'Relative paths cannot be used without specifying a root folder name' 'Relative paths cannot be used without specifying a root folder name'
@@ -893,7 +901,7 @@ export class SASViyaApiClient {
? `${this.rootFolderName}/${folderPath}` ? `${this.rootFolderName}/${folderPath}`
: 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. ') throw prefixMessage(err, 'Error while populating folder map. ')
}) })
@@ -907,8 +915,8 @@ export class SASViyaApiClient {
const headers: any = { 'Content-Type': 'application/json' } const headers: any = { 'Content-Type': 'application/json' }
if (!!accessToken) { if (!!access_token) {
headers.Authorization = `Bearer ${accessToken}` headers.Authorization = `Bearer ${access_token}`
} }
const jobToExecute = jobFolder?.find((item) => item.name === jobName) const jobToExecute = jobFolder?.find((item) => item.name === jobName)
@@ -931,7 +939,7 @@ export class SASViyaApiClient {
const { result: jobDefinition } = await this.requestClient const { result: jobDefinition } = await this.requestClient
.get<JobDefinition>( .get<JobDefinition>(
`${this.serverUrl}${jobDefinitionLink.href}`, `${this.serverUrl}${jobDefinitionLink.href}`,
accessToken access_token
) )
.catch((err) => { .catch((err) => {
throw prefixMessage(err, 'Error while getting job definition. ') throw prefixMessage(err, 'Error while getting job definition. ')
@@ -951,7 +959,7 @@ export class SASViyaApiClient {
sasJob, sasJob,
linesToExecute, linesToExecute,
contextName, contextName,
accessToken, authConfig,
data, data,
debug, debug,
expectWebout, expectWebout,
@@ -975,8 +983,9 @@ export class SASViyaApiClient {
contextName: string, contextName: string,
debug: boolean, debug: boolean,
data?: any, data?: any,
accessToken?: string authConfig?: AuthConfig
) { ) {
let { access_token } = authConfig || {}
if (isRelativePath(sasJob) && !this.rootFolderName) { if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error( throw new Error(
'Relative paths cannot be used without specifying a root folder name.' 'Relative paths cannot be used without specifying a root folder name.'
@@ -989,7 +998,7 @@ export class SASViyaApiClient {
const fullFolderPath = isRelativePath(sasJob) const fullFolderPath = isRelativePath(sasJob)
? `${this.rootFolderName}/${folderPath}` ? `${this.rootFolderName}/${folderPath}`
: folderPath : folderPath
await this.populateFolderMap(fullFolderPath, accessToken) await this.populateFolderMap(fullFolderPath, access_token)
const jobFolder = this.folderMap.get(fullFolderPath) const jobFolder = this.folderMap.get(fullFolderPath)
if (!jobFolder) { if (!jobFolder) {
@@ -1002,7 +1011,7 @@ export class SASViyaApiClient {
let files: any[] = [] let files: any[] = []
if (data && Object.keys(data).length) { if (data && Object.keys(data).length) {
files = await this.uploadTables(data, accessToken) files = await this.uploadTables(data, access_token)
} }
if (!jobToExecute) { if (!jobToExecute) {
@@ -1014,7 +1023,7 @@ export class SASViyaApiClient {
const { result: jobDefinition } = await this.requestClient.get<Job>( const { result: jobDefinition } = await this.requestClient.get<Job>(
`${this.serverUrl}${jobDefinitionLink}`, `${this.serverUrl}${jobDefinitionLink}`,
accessToken access_token
) )
const jobArguments: { [key: string]: any } = { const jobArguments: { [key: string]: any } = {
@@ -1050,18 +1059,18 @@ export class SASViyaApiClient {
const { result: postedJob, etag } = await this.requestClient.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 access_token
) )
const jobStatus = await this.pollJobState( const jobStatus = await this.pollJobState(
postedJob, postedJob,
etag, etag,
accessToken authConfig
).catch((err) => { ).catch((err) => {
throw prefixMessage(err, 'Error while polling job status. ') throw prefixMessage(err, 'Error while polling job status. ')
}) })
const { result: currentJob } = await this.requestClient.get<Job>( const { result: currentJob } = await this.requestClient.get<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`, `${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
accessToken access_token
) )
let jobResult let jobResult
@@ -1072,13 +1081,13 @@ export class SASViyaApiClient {
if (resultLink) { if (resultLink) {
jobResult = await this.requestClient.get<any>( jobResult = await this.requestClient.get<any>(
`${this.serverUrl}${resultLink}/content`, `${this.serverUrl}${resultLink}/content`,
accessToken, access_token,
'text/plain' 'text/plain'
) )
} }
if (debug && logLink) { if (debug && logLink) {
log = await this.requestClient 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')) .then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
} }
if (jobStatus === 'failed') { if (jobStatus === 'failed') {
@@ -1128,12 +1137,30 @@ export class SASViyaApiClient {
private async pollJobState( private async pollJobState(
postedJob: any, postedJob: any,
etag: string | null, etag: string | null,
accessToken?: string, authConfig?: AuthConfig,
pollOptions?: PollOptions pollOptions?: PollOptions
) { ) {
const logger = process.logger || console
let POLL_INTERVAL = 300 let POLL_INTERVAL = 300
let MAX_POLL_COUNT = 1000 let MAX_POLL_COUNT = 1000
let MAX_ERROR_COUNT = 5 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) { if (pollOptions) {
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
@@ -1147,8 +1174,8 @@ export class SASViyaApiClient {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'If-None-Match': etag 'If-None-Match': etag
} }
if (accessToken) { if (access_token) {
headers.Authorization = `Bearer ${accessToken}` headers.Authorization = `Bearer ${access_token}`
} }
const stateLink = postedJob.links.find((l: any) => l.rel === 'state') const stateLink = postedJob.links.find((l: any) => l.rel === 'state')
if (!stateLink) { if (!stateLink) {
@@ -1158,7 +1185,7 @@ export class SASViyaApiClient {
const { result: state } = await this.requestClient const { result: state } = await this.requestClient
.get<string>( .get<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`, `${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
accessToken, access_token,
'text/plain', 'text/plain',
{}, {},
this.debug this.debug
@@ -1186,11 +1213,27 @@ export class SASViyaApiClient {
postedJobState === 'pending' || postedJobState === 'pending' ||
postedJobState === 'unavailable' 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) { if (stateLink) {
const { result: jobState } = await this.requestClient const { result: jobState } = await this.requestClient
.get<string>( .get<string>(
`${this.serverUrl}${stateLink.href}?_action=wait&wait=300`, `${this.serverUrl}${stateLink.href}?_action=wait&wait=300`,
accessToken, access_token,
'text/plain', 'text/plain',
{}, {},
this.debug this.debug
@@ -1219,8 +1262,8 @@ export class SASViyaApiClient {
} }
if (this.debug && printedState !== postedJobState) { if (this.debug && printedState !== postedJobState) {
console.log('Polling job status...') logger.info('Polling job status...')
console.log(`Current job state: ${postedJobState}`) logger.info(`Current job state: ${postedJobState}`)
printedState = postedJobState printedState = postedJobState
} }
@@ -1410,6 +1453,9 @@ export class SASViyaApiClient {
accessToken accessToken
) )
if (!sourceFolderUri) {
return undefined
}
const sourceFolderId = sourceFolderUri?.split('/').pop() const sourceFolderId = sourceFolderUri?.split('/').pop()
const { result: folder } = await this.requestClient const { result: folder } = await this.requestClient

View File

@@ -4,7 +4,7 @@ import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient' import { SAS9ApiClient } from './SAS9ApiClient'
import { FileUploader } from './FileUploader' import { FileUploader } from './FileUploader'
import { AuthManager } from './auth' 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 { RequestClient } from './request/RequestClient'
import { import {
JobExecutor, JobExecutor,
@@ -103,12 +103,12 @@ export default class SASjs {
/** /**
* Gets executable compute contexts. * 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) 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 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 linesOfCode - lines of sas code from the file to run.
* @param contextName - context name on which code will be run on the server. * @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 * @param debug - (optional) if true, global debug config will be overriden
*/ */
public async executeScriptSASViya( public async executeScriptSASViya(
fileName: string, fileName: string,
linesOfCode: string[], linesOfCode: string[],
contextName: string, contextName: string,
accessToken?: string, authConfig?: AuthConfig,
debug?: boolean debug?: boolean
) { ) {
this.isMethodSupported('executeScriptSASViya', ServerType.SasViya) this.isMethodSupported('executeScriptSASViya', ServerType.SasViya)
@@ -261,7 +261,7 @@ export default class SASjs {
fileName, fileName,
linesOfCode, linesOfCode,
contextName, contextName,
accessToken, authConfig,
null, null,
debug ? debug : this.sasjsConfig.debug debug ? debug : this.sasjsConfig.debug
) )
@@ -579,7 +579,7 @@ export default class SASjs {
data: { [key: string]: any } | null, data: { [key: string]: any } | null,
config: { [key: string]: any } = {}, config: { [key: string]: any } = {},
loginRequiredCallback?: () => any, loginRequiredCallback?: () => any,
accessToken?: string, authConfig?: AuthConfig,
extraResponseAttributes: ExtraResponseAttributes[] = [] extraResponseAttributes: ExtraResponseAttributes[] = []
) { ) {
config = { config = {
@@ -601,7 +601,7 @@ export default class SASjs {
data, data,
config, config,
loginRequiredCallback, loginRequiredCallback,
accessToken authConfig
) )
} else { } else {
return await this.jesJobExecutor!.execute( return await this.jesJobExecutor!.execute(
@@ -609,7 +609,7 @@ export default class SASjs {
data, data,
config, config,
loginRequiredCallback, loginRequiredCallback,
accessToken, authConfig,
extraResponseAttributes extraResponseAttributes
) )
} }
@@ -625,7 +625,7 @@ export default class SASjs {
data, data,
config, config,
loginRequiredCallback, loginRequiredCallback,
accessToken, authConfig,
extraResponseAttributes extraResponseAttributes
) )
} }
@@ -776,7 +776,7 @@ export default class SASjs {
* @param config - provide any changes to the config here, for instance to * @param config - provide any changes to the config here, for instance to
* enable/disable `debug`. Any change provided will override the global config, * enable/disable `debug`. Any change provided will override the global config,
* for that particular function call. * 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. * 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 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 }. * @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, sasJob: string,
data: any, data: any,
config: any = {}, config: any = {},
accessToken?: string, authConfig?: AuthConfig,
waitForResult?: boolean, waitForResult?: boolean,
pollOptions?: PollOptions, pollOptions?: PollOptions,
printPid = false, printPid = false,
@@ -810,7 +810,7 @@ export default class SASjs {
config.contextName, config.contextName,
config.debug, config.debug,
data, data,
accessToken, authConfig,
!!waitForResult, !!waitForResult,
false, false,
pollOptions, pollOptions,

View File

@@ -158,6 +158,8 @@ export class SessionManager {
etag: string | null, etag: string | null,
accessToken?: string accessToken?: string
) { ) {
const logger = process.logger || console
let sessionState = session.state let sessionState = session.state
const stateLink = session.links.find((l: any) => l.rel === 'state') const stateLink = session.links.find((l: any) => l.rel === 'state')
@@ -170,7 +172,7 @@ export class SessionManager {
) { ) {
if (stateLink) { if (stateLink) {
if (this.debug && !this.printedSessionState.printed) { if (this.debug && !this.printedSessionState.printed) {
console.log('Polling session status...') logger.info('Polling session status...')
this.printedSessionState.printed = true this.printedSessionState.printed = true
} }
@@ -186,7 +188,7 @@ export class SessionManager {
sessionState = state.trim() sessionState = state.trim()
if (this.debug && this.printedSessionState.state !== sessionState) { 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.state = sessionState
this.printedSessionState.printed = false this.printedSessionState.printed = false

View File

@@ -1,5 +1,4 @@
import { ServerType } from '@sasjs/utils/types' import { ServerType } from '@sasjs/utils/types'
import { isAuthorizeFormRequired } from '.'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { serialize } from '../utils' import { serialize } from '../utils'

View File

@@ -1,4 +1,4 @@
import { ServerType } from '@sasjs/utils/types' import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { SASViyaApiClient } from '../SASViyaApiClient' import { SASViyaApiClient } from '../SASViyaApiClient'
import { import {
ErrorResponse, ErrorResponse,
@@ -17,7 +17,7 @@ export class ComputeJobExecutor extends BaseJobExecutor {
data: any, data: any,
config: any, config: any,
loginRequiredCallback?: any, loginRequiredCallback?: any,
accessToken?: string authConfig?: AuthConfig
) { ) {
const loginCallback = loginRequiredCallback || (() => Promise.resolve()) const loginCallback = loginRequiredCallback || (() => Promise.resolve())
const waitForResult = true const waitForResult = true
@@ -30,7 +30,7 @@ export class ComputeJobExecutor extends BaseJobExecutor {
config.contextName, config.contextName,
config.debug, config.debug,
data, data,
accessToken, authConfig,
waitForResult, waitForResult,
expectWebout expectWebout
) )

View File

@@ -1,4 +1,4 @@
import { ServerType } from '@sasjs/utils/types' import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { SASViyaApiClient } from '../SASViyaApiClient' import { SASViyaApiClient } from '../SASViyaApiClient'
import { import {
ErrorResponse, ErrorResponse,
@@ -18,20 +18,14 @@ export class JesJobExecutor extends BaseJobExecutor {
data: any, data: any,
config: any, config: any,
loginRequiredCallback?: any, loginRequiredCallback?: any,
accessToken?: string, authConfig?: AuthConfig,
extraResponseAttributes: ExtraResponseAttributes[] = [] extraResponseAttributes: ExtraResponseAttributes[] = []
) { ) {
const loginCallback = loginRequiredCallback || (() => Promise.resolve()) const loginCallback = loginRequiredCallback || (() => Promise.resolve())
const requestPromise = new Promise((resolve, reject) => { const requestPromise = new Promise((resolve, reject) => {
this.sasViyaApiClient this.sasViyaApiClient
?.executeJob( ?.executeJob(sasJob, config.contextName, config.debug, data, authConfig)
sasJob,
config.contextName,
config.debug,
data,
accessToken
)
.then((response: any) => { .then((response: any) => {
this.appendRequest(response, sasJob, config.debug) this.appendRequest(response, sasJob, config.debug)
@@ -69,7 +63,7 @@ export class JesJobExecutor extends BaseJobExecutor {
data, data,
config, config,
loginRequiredCallback, loginRequiredCallback,
accessToken, authConfig,
extraResponseAttributes extraResponseAttributes
).then( ).then(
(res: any) => { (res: any) => {

View File

@@ -1,4 +1,4 @@
import { ServerType } from '@sasjs/utils/types' import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { SASjsRequest } from '../types' import { SASjsRequest } from '../types'
import { ExtraResponseAttributes } from '@sasjs/utils/types' import { ExtraResponseAttributes } from '@sasjs/utils/types'
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils' import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
@@ -11,7 +11,7 @@ export interface JobExecutor {
data: any, data: any,
config: any, config: any,
loginRequiredCallback?: any, loginRequiredCallback?: any,
accessToken?: string, authConfig?: AuthConfig,
extraResponseAttributes?: ExtraResponseAttributes[] extraResponseAttributes?: ExtraResponseAttributes[]
) => Promise<any> ) => Promise<any>
resendWaitingRequests: () => Promise<void> resendWaitingRequests: () => Promise<void>
@@ -30,7 +30,7 @@ export abstract class BaseJobExecutor implements JobExecutor {
data: any, data: any,
config: any, config: any,
loginRequiredCallback?: any, loginRequiredCallback?: any,
accessToken?: string | undefined, authConfig?: AuthConfig | undefined,
extraResponseAttributes?: ExtraResponseAttributes[] extraResponseAttributes?: ExtraResponseAttributes[]
): Promise<any> ): Promise<any>

View File

@@ -287,7 +287,8 @@ export class RequestClient implements HttpClient {
}) })
.then((res) => res.data) .then((res) => res.data)
.catch((error) => { .catch((error) => {
console.log(error) const logger = process.logger || console
logger.error(error)
}) })
} }

5
src/types/Process.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare namespace NodeJS {
export interface Process {
logger?: import('@sasjs/utils/logger').Logger
}
}

35
src/utils/auth.ts Normal file
View 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
}

View File

@@ -15,12 +15,14 @@ export const fetchLogByChunks = async (
logUrl: string, logUrl: string,
logCount: number logCount: number
): Promise<string> => { ): Promise<string> => {
const logger = process.logger || console
let log: string = '' let log: string = ''
const loglimit = logCount < 10000 ? logCount : 10000 const loglimit = logCount < 10000 ? logCount : 10000
let start = 0 let start = 0
do { do {
console.log( logger.info(
`Fetching logs from line no: ${start + 1} to ${ `Fetching logs from line no: ${start + 1} to ${
start + loglimit start + loglimit
} of ${logCount}.` } of ${logCount}.`

View File

@@ -1,4 +1,5 @@
export * from './asyncForEach' export * from './asyncForEach'
export * from './auth'
export * from './compareTimestamps' export * from './compareTimestamps'
export * from './convertToCsv' export * from './convertToCsv'
export * from './isRelativePath' export * from './isRelativePath'