mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a7b4a1de4 | ||
|
|
5a35237de5 | ||
|
|
5d77bbba8b | ||
|
|
eda021b6a5 | ||
|
|
259c479ef0 | ||
|
|
a962b8e7cf | ||
|
|
eb0e7247a6 | ||
| ccc77cb9d1 | |||
|
|
5cb5bbdb55 | ||
|
|
ac6cd7be82 | ||
|
|
63f5f4d03d | ||
|
|
a164fb7df9 | ||
|
|
336ba207cf | ||
|
|
3cfd45cc62 | ||
|
|
f7fb917282 | ||
|
|
a182037883 | ||
|
|
f9e79fb756 | ||
|
|
aaf0eef62b | ||
|
|
fafa0c3567 | ||
|
|
4a6845ad6a | ||
|
|
61d66c6f82 | ||
| 123fbc7235 | |||
|
|
eae8694a29 | ||
|
|
2b16be3aef | ||
|
|
d8d4da9c9a | ||
|
|
0b755b7304 | ||
|
|
0816b7b1f9 | ||
|
|
97d45e87ec | ||
|
|
57ef0647b5 | ||
|
|
2ee6c45d16 | ||
|
|
56b2ba026a | ||
|
|
8beda1ad6c | ||
|
|
b18b471549 | ||
| 93c9a34591 |
@@ -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
|
||||
|
||||
@@ -12,6 +12,8 @@ What code changes have been made to achieve the intent.
|
||||
|
||||
## Checks
|
||||
|
||||
No PR (that involves a non-trivial code change) should be merged, unless all four of the items below are confirmed! If an urgent fix is needed - use a tar file.
|
||||
|
||||
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
||||
- [ ] All unit tests are passing (`npm test`).
|
||||
- [ ] All `sasjs-cli` unit tests are passing (`npm test`).
|
||||
|
||||
@@ -189,6 +189,8 @@ In this setup, all requests are routed through the JES web app, at `YOURSERVER/S
|
||||
}
|
||||
```
|
||||
|
||||
Note - to use the web approach, the `useComputeApi` property must be `undefined` or `null`.
|
||||
|
||||
### Using the JES API
|
||||
Here we are running Jobs using the Job Execution Service except this time we are making the requests directly using the REST API instead of through the JES Web App. This is helpful when we need to call web services outside of a browser (eg with the SASjs CLI or other commandline tools). To save one network request, the adapter prefetches the JOB URIs and passes them in the `__job` parameter. Depending on your network bandwidth, it may or may not be faster than the JES Web approach.
|
||||
|
||||
|
||||
17198
package-lock.json
generated
17198
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
15
package.json
15
package.json
@@ -13,7 +13,7 @@
|
||||
"postpublish": "git clean -fd",
|
||||
"semantic-release": "semantic-release",
|
||||
"typedoc": "typedoc",
|
||||
"postinstall": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true"
|
||||
"prepare": "[ -d .git ] && git config core.hooksPath ./.git-hooks || true"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
@@ -43,28 +43,29 @@
|
||||
"@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",
|
||||
"node-polyfill-webpack-plugin": "^1.1.4",
|
||||
"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.22.0",
|
||||
"axios": "^0.21.1",
|
||||
"axios-cookiejar-support": "^1.0.1",
|
||||
"form-data": "^4.0.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,
|
||||
|
||||
@@ -25,11 +25,18 @@ import {
|
||||
import { formatDataForRequest } from './utils/formatDataForRequest'
|
||||
import { SessionManager } from './SessionManager'
|
||||
import { ContextManager } from './ContextManager'
|
||||
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import {
|
||||
timestampToYYYYMMDDHHMMSS,
|
||||
isAccessTokenExpiring,
|
||||
isRefreshTokenExpiring,
|
||||
Logger,
|
||||
LogLevel,
|
||||
SasAuthResponse,
|
||||
MacroVar,
|
||||
AuthConfig
|
||||
} from '@sasjs/utils'
|
||||
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { SasAuthResponse, MacroVar } from '@sasjs/utils/types'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import * as mime from 'mime'
|
||||
|
||||
@@ -130,14 +137,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 +273,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 +286,7 @@ export class SASViyaApiClient {
|
||||
jobPath: string,
|
||||
linesOfCode: string[],
|
||||
contextName: string,
|
||||
accessToken?: string,
|
||||
authConfig?: AuthConfig,
|
||||
data = null,
|
||||
debug: boolean = false,
|
||||
expectWebout = false,
|
||||
@@ -288,17 +295,18 @@ export class SASViyaApiClient {
|
||||
printPid = false,
|
||||
variables?: MacroVar
|
||||
): Promise<any> {
|
||||
let access_token = (authConfig || {}).access_token
|
||||
if (authConfig) {
|
||||
;({ access_token } = await this.getTokens(authConfig))
|
||||
}
|
||||
|
||||
const logger = process.logger || console
|
||||
|
||||
try {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) headers.Authorization = `Bearer ${accessToken}`
|
||||
|
||||
let executionSessionId: string
|
||||
|
||||
const session = await this.sessionManager
|
||||
.getSession(accessToken)
|
||||
.getSession(access_token)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting session. ')
|
||||
})
|
||||
@@ -307,7 +315,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. ')
|
||||
})
|
||||
@@ -339,7 +347,6 @@ export class SASViyaApiClient {
|
||||
if (debug) {
|
||||
jobArguments['_OMITTEXTLOG'] = false
|
||||
jobArguments['_OMITSESSIONRESULTS'] = false
|
||||
jobArguments['_DEBUG'] = 131
|
||||
}
|
||||
|
||||
let fileName
|
||||
@@ -362,11 +369,13 @@ export class SASViyaApiClient {
|
||||
|
||||
if (variables) jobVariables = { ...jobVariables, ...variables }
|
||||
|
||||
if (debug) jobVariables = { ...jobVariables, _DEBUG: 131 }
|
||||
|
||||
let files: any[] = []
|
||||
|
||||
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. ')
|
||||
})
|
||||
|
||||
@@ -396,7 +405,7 @@ export class SASViyaApiClient {
|
||||
.post<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs`,
|
||||
jobRequestBody,
|
||||
accessToken
|
||||
access_token
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while posting job. ')
|
||||
@@ -405,8 +414,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
|
||||
}'.`
|
||||
@@ -416,7 +425,7 @@ export class SASViyaApiClient {
|
||||
const jobStatus = await this.pollJobState(
|
||||
postedJob,
|
||||
etag,
|
||||
accessToken,
|
||||
authConfig,
|
||||
pollOptions
|
||||
).catch(async (err) => {
|
||||
const error = err?.response?.data
|
||||
@@ -429,7 +438,7 @@ export class SASViyaApiClient {
|
||||
const logCount = 1000000
|
||||
err.log = await fetchLogByChunks(
|
||||
this.requestClient,
|
||||
accessToken!,
|
||||
access_token!,
|
||||
sessionLogUrl,
|
||||
logCount
|
||||
)
|
||||
@@ -437,10 +446,14 @@ export class SASViyaApiClient {
|
||||
throw prefixMessage(err, 'Error while polling job status. ')
|
||||
})
|
||||
|
||||
if (authConfig) {
|
||||
;({ access_token } = await this.getTokens(authConfig))
|
||||
}
|
||||
|
||||
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. ')
|
||||
@@ -456,7 +469,7 @@ export class SASViyaApiClient {
|
||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||
log = await fetchLogByChunks(
|
||||
this.requestClient,
|
||||
accessToken!,
|
||||
access_token!,
|
||||
logUrl,
|
||||
logCount
|
||||
)
|
||||
@@ -476,7 +489,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) {
|
||||
@@ -484,7 +497,7 @@ export class SASViyaApiClient {
|
||||
const logCount = currentJob.logStatistics?.lineCount ?? 1000000
|
||||
log = await fetchLogByChunks(
|
||||
this.requestClient,
|
||||
accessToken!,
|
||||
access_token!,
|
||||
logUrl,
|
||||
logCount
|
||||
)
|
||||
@@ -503,7 +516,7 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
await this.sessionManager
|
||||
.clearSession(executionSessionId, accessToken)
|
||||
.clearSession(executionSessionId, access_token)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while clearing session. ')
|
||||
})
|
||||
@@ -515,7 +528,7 @@ export class SASViyaApiClient {
|
||||
jobPath,
|
||||
linesOfCode,
|
||||
contextName,
|
||||
accessToken,
|
||||
authConfig,
|
||||
data,
|
||||
debug,
|
||||
false,
|
||||
@@ -602,6 +615,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.')
|
||||
}
|
||||
@@ -609,7 +623,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.`
|
||||
)
|
||||
|
||||
@@ -621,7 +635,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(
|
||||
@@ -630,7 +644,7 @@ export class SASViyaApiClient {
|
||||
undefined,
|
||||
accessToken
|
||||
)
|
||||
console.log(
|
||||
logger.info(
|
||||
`Parent folder '${newFolderName}' has been successfully created.`
|
||||
)
|
||||
parentFolderUri = `/folders/folders/${parentFolder.id}`
|
||||
@@ -872,13 +886,18 @@ 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 || {}).access_token
|
||||
if (authConfig) {
|
||||
;({ access_token } = await this.getTokens(authConfig))
|
||||
}
|
||||
|
||||
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||
throw new Error(
|
||||
'Relative paths cannot be used without specifying a root folder name'
|
||||
@@ -892,7 +911,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. ')
|
||||
})
|
||||
|
||||
@@ -904,12 +923,6 @@ export class SASViyaApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
const headers: any = { 'Content-Type': 'application/json' }
|
||||
|
||||
if (!!accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const jobToExecute = jobFolder?.find((item) => item.name === jobName)
|
||||
|
||||
if (!jobToExecute) {
|
||||
@@ -930,7 +943,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. ')
|
||||
@@ -950,7 +963,7 @@ export class SASViyaApiClient {
|
||||
sasJob,
|
||||
linesToExecute,
|
||||
contextName,
|
||||
accessToken,
|
||||
authConfig,
|
||||
data,
|
||||
debug,
|
||||
expectWebout,
|
||||
@@ -974,8 +987,12 @@ export class SASViyaApiClient {
|
||||
contextName: string,
|
||||
debug: boolean,
|
||||
data?: any,
|
||||
accessToken?: string
|
||||
authConfig?: AuthConfig
|
||||
) {
|
||||
let access_token = (authConfig || {}).access_token
|
||||
if (authConfig) {
|
||||
;({ access_token } = await this.getTokens(authConfig))
|
||||
}
|
||||
if (isRelativePath(sasJob) && !this.rootFolderName) {
|
||||
throw new Error(
|
||||
'Relative paths cannot be used without specifying a root folder name.'
|
||||
@@ -988,7 +1005,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) {
|
||||
@@ -1001,7 +1018,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) {
|
||||
@@ -1013,7 +1030,7 @@ export class SASViyaApiClient {
|
||||
|
||||
const { result: jobDefinition } = await this.requestClient.get<Job>(
|
||||
`${this.serverUrl}${jobDefinitionLink}`,
|
||||
accessToken
|
||||
access_token
|
||||
)
|
||||
|
||||
const jobArguments: { [key: string]: any } = {
|
||||
@@ -1049,18 +1066,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
|
||||
@@ -1071,13 +1088,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') {
|
||||
@@ -1127,12 +1144,18 @@ 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 = (authConfig || {}).access_token
|
||||
if (authConfig) {
|
||||
;({ access_token } = await this.getTokens(authConfig))
|
||||
}
|
||||
|
||||
if (pollOptions) {
|
||||
POLL_INTERVAL = pollOptions.POLL_INTERVAL || POLL_INTERVAL
|
||||
@@ -1146,8 +1169,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) {
|
||||
@@ -1157,7 +1180,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
|
||||
@@ -1185,11 +1208,15 @@ export class SASViyaApiClient {
|
||||
postedJobState === 'pending' ||
|
||||
postedJobState === 'unavailable'
|
||||
) {
|
||||
if (authConfig) {
|
||||
;({ access_token } = await this.getTokens(authConfig))
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1218,8 +1245,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
|
||||
}
|
||||
@@ -1409,6 +1436,9 @@ export class SASViyaApiClient {
|
||||
accessToken
|
||||
)
|
||||
|
||||
if (!sourceFolderUri) {
|
||||
return undefined
|
||||
}
|
||||
const sourceFolderId = sourceFolderUri?.split('/').pop()
|
||||
|
||||
const { result: folder } = await this.requestClient
|
||||
@@ -1463,4 +1493,21 @@ export class SASViyaApiClient {
|
||||
|
||||
return movedFolder
|
||||
}
|
||||
|
||||
private async getTokens(authConfig: AuthConfig): Promise<AuthConfig> {
|
||||
const logger = process.logger || console
|
||||
let { access_token, refresh_token, client, secret } = authConfig
|
||||
if (
|
||||
isAccessTokenExpiring(access_token) ||
|
||||
isRefreshTokenExpiring(refresh_token)
|
||||
) {
|
||||
logger.info('Refreshing access and refresh tokens.')
|
||||
;({ access_token, refresh_token } = await this.refreshTokens(
|
||||
client,
|
||||
secret,
|
||||
refresh_token
|
||||
))
|
||||
}
|
||||
return { access_token, refresh_token, client, secret }
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -6,10 +6,6 @@ import { RequestClient } from './request/RequestClient'
|
||||
const MAX_SESSION_COUNT = 1
|
||||
const RETRY_LIMIT: number = 3
|
||||
let RETRY_COUNT: number = 0
|
||||
const INTERNAL_SAS_ERROR = {
|
||||
status: 304,
|
||||
message: 'Not Modified'
|
||||
}
|
||||
|
||||
export class SessionManager {
|
||||
constructor(
|
||||
@@ -158,11 +154,13 @@ 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')
|
||||
|
||||
return new Promise(async (resolve, _) => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
if (
|
||||
sessionState === 'pending' ||
|
||||
sessionState === 'running' ||
|
||||
@@ -170,7 +168,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
|
||||
}
|
||||
@@ -180,13 +178,13 @@ export class SessionManager {
|
||||
etag!,
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw err
|
||||
throw prefixMessage(err, 'Error while getting session state.')
|
||||
})
|
||||
|
||||
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
|
||||
@@ -194,13 +192,14 @@ export class SessionManager {
|
||||
|
||||
// There is an internal error present in SAS Viya 3.5
|
||||
// Retry to wait for a session status in such case of SAS internal error
|
||||
if (
|
||||
sessionState === INTERNAL_SAS_ERROR.message &&
|
||||
RETRY_COUNT < RETRY_LIMIT
|
||||
) {
|
||||
RETRY_COUNT++
|
||||
if (!sessionState) {
|
||||
if (RETRY_COUNT < RETRY_LIMIT) {
|
||||
RETRY_COUNT++
|
||||
|
||||
resolve(this.waitForSession(session, etag, accessToken))
|
||||
resolve(this.waitForSession(session, etag, accessToken))
|
||||
} else {
|
||||
reject('Could not get session state.')
|
||||
}
|
||||
}
|
||||
|
||||
resolve(sessionState)
|
||||
@@ -220,9 +219,6 @@ export class SessionManager {
|
||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||
.then((res) => res.result as string)
|
||||
.catch((err) => {
|
||||
if (err.status === INTERNAL_SAS_ERROR.status)
|
||||
return INTERNAL_SAS_ERROR.message
|
||||
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -8,8 +8,9 @@ import { generateFileUploadForm } from '../file/generateFileUploadForm'
|
||||
import { generateTableUploadForm } from '../file/generateTableUploadForm'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import { isRelativePath } from '../utils'
|
||||
import { isRelativePath, isValidJson } from '../utils'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
|
||||
export interface WaitingRequstPromise {
|
||||
promise: Promise<any> | null
|
||||
@@ -100,6 +101,19 @@ export class WebJobExecutor extends BaseJobExecutor {
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
resolve(jsonResponse)
|
||||
}
|
||||
if (this.serverType === ServerType.Sas9 && config.debug) {
|
||||
const jsonResponse = parseWeboutResponse(res.result as string)
|
||||
if (jsonResponse === '') {
|
||||
throw new Error(
|
||||
'Valid JSON could not be extracted from response.'
|
||||
)
|
||||
}
|
||||
|
||||
isValidJson(jsonResponse)
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
resolve(res.result)
|
||||
}
|
||||
isValidJson(res.result as string)
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
resolve(res.result)
|
||||
})
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { SAS9AuthError } from '../types/errors/SAS9AuthError'
|
||||
import { isValidJson } from '../utils'
|
||||
|
||||
export interface HttpClient {
|
||||
get<T>(
|
||||
@@ -63,6 +64,9 @@ export class RequestClient implements HttpClient {
|
||||
baseURL: baseUrl
|
||||
})
|
||||
}
|
||||
|
||||
this.httpClient.defaults.validateStatus = (status) =>
|
||||
status >= 200 && status < 305
|
||||
}
|
||||
|
||||
public getCsrfToken(type: 'general' | 'file' = 'general') {
|
||||
@@ -287,7 +291,8 @@ export class RequestClient implements HttpClient {
|
||||
})
|
||||
.then((res) => res.data)
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
const logger = process.logger || console
|
||||
logger.error(error)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -419,7 +424,13 @@ export class RequestClient implements HttpClient {
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
parsedResponse = JSON.parse(parseWeboutResponse(response.data))
|
||||
const weboutResponse = parseWeboutResponse(response.data)
|
||||
if (weboutResponse === '') {
|
||||
throw new Error('Valid JSON could not be extracted from response.')
|
||||
}
|
||||
|
||||
isValidJson(weboutResponse)
|
||||
parsedResponse = JSON.parse(weboutResponse)
|
||||
} catch {
|
||||
parsedResponse = response.data
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -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}.`
|
||||
|
||||
@@ -12,3 +12,4 @@ export * from './serialize'
|
||||
export * from './splitChunks'
|
||||
export * from './parseWeboutResponse'
|
||||
export * from './fetchLogByChunks'
|
||||
export * from './isValidJson'
|
||||
|
||||
11
src/utils/isValidJson.ts
Normal file
11
src/utils/isValidJson.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/**
|
||||
* Checks if string is in valid JSON format else throw error.
|
||||
* @param str - string to check.
|
||||
*/
|
||||
export const isValidJson = (str: string) => {
|
||||
try {
|
||||
JSON.parse(str)
|
||||
} catch (e) {
|
||||
throw new Error('Invalid JSON response.')
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
const path = require('path')
|
||||
const webpack = require('webpack')
|
||||
const terserPlugin = require('terser-webpack-plugin')
|
||||
const nodePolyfillPlugin = require('node-polyfill-webpack-plugin')
|
||||
|
||||
const defaultPlugins = [
|
||||
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
|
||||
@@ -37,7 +38,7 @@ const browserConfig = {
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.ts', '.js'],
|
||||
fallback: { https: false }
|
||||
fallback: { https: false, fs: false, readline: false }
|
||||
},
|
||||
output: {
|
||||
filename: 'index.js',
|
||||
@@ -49,7 +50,8 @@ const browserConfig = {
|
||||
...defaultPlugins,
|
||||
new webpack.ProvidePlugin({
|
||||
process: 'process/browser'
|
||||
})
|
||||
}),
|
||||
new nodePolyfillPlugin()
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user