1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-11 01:14:36 +00:00

Compare commits

...

63 Commits

Author SHA1 Message Date
Krishna Acondy
fa531b34fd Merge pull request #120 from sasjs/session-manager-debug
fix(debug): propagate debug value from SASjs config
2020-10-03 17:41:35 +01:00
Krishna Acondy
354443c98b fix(debug): propagate debug value from SASjs config 2020-10-03 16:53:00 +01:00
Krishna Acondy
ee30ab195f Merge pull request #115 from sasjs/issue-114
chore(error-message): updated error message for forbidden request
2020-10-01 09:10:35 +01:00
Yury Shkoda
02c1712d22 chore(error-message): updated error message for forbidden request 2020-10-01 09:49:24 +03:00
Krishna Acondy
37def7a956 Merge pull request #111 from sasjs/dependabot/npm_and_yarn/isomorphic-fetch-3.0.0
chore(deps): bump isomorphic-fetch from 2.2.1 to 3.0.0
2020-09-29 20:03:06 +01:00
Krishna Acondy
653e3d05e0 Merge branch 'master' into dependabot/npm_and_yarn/isomorphic-fetch-3.0.0 2020-09-29 19:55:08 +01:00
Yury Shkoda
d8467c24b1 Merge pull request #112 from sasjs/cli-issue-73
feat(folder-management): made folder related methods public
2020-09-28 15:16:33 +03:00
Yury Shkoda
fc9056c1ac chore(folder-management): made 'moveFolder' method public and fixed 'createFolder' method 2020-09-28 14:59:27 +03:00
Yury Shkoda
9b1d295b82 feat(folder): made 'deleteFolder' method public 2020-09-26 11:41:18 +03:00
dependabot-preview[bot]
e2ea3f4ddc chore(deps): bump isomorphic-fetch from 2.2.1 to 3.0.0
Bumps [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) from 2.2.1 to 3.0.0.
- [Release notes](https://github.com/matthew-andrews/isomorphic-fetch/releases)
- [Commits](https://github.com/matthew-andrews/isomorphic-fetch/compare/v2.2.1...v3.0.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-25 23:51:17 +00:00
Allan Bowe
99d0b01a24 Update example.html 2020-09-24 23:00:37 +02:00
Yury Shkoda
131c672020 Merge pull request #110 from sasjs/cli-issue-105
fix(context): fixed 'ContextAllAttributes' interface
2020-09-24 17:10:14 +03:00
Yury Shkoda
338f2fb2dd Merge branch 'master' into cli-issue-105 2020-09-24 17:08:04 +03:00
Yury Shkoda
4552a9a856 fix(context): fixed 'ContextAllAttributes' interface 2020-09-24 16:51:50 +03:00
Yury Shkoda
daeb753f9e Merge pull request #109 from sasjs/cli-issue-105
feat(context): added getComputeContextById method
2020-09-24 16:10:02 +03:00
Yury Shkoda
f50a99d0b8 Merge branch 'master' into cli-issue-105 2020-09-24 16:08:09 +03:00
Yury Shkoda
e6d0d3efd5 docs(context): add docs for 'getComputeContextByName' method 2020-09-24 16:05:42 +03:00
Yury Shkoda
057460467c feat(context): added getComputeContextById method 2020-09-24 15:53:07 +03:00
Yury Shkoda
5aee9d955e Merge pull request #106 from sasjs/cli-issue-105
feat(context): made getContextByName function public
2020-09-24 08:38:47 +03:00
Yury Shkoda
7fb1da31e4 Merge branch 'master' into cli-issue-105 2020-09-24 08:35:28 +03:00
Allan Bowe
1aa92c0a69 Merge pull request #107 from sasjs/access-token-missed
Access token missed
2020-09-23 21:26:46 +02:00
Mihajlo Medjedovic
4c097a69fd style: lint 2020-09-23 20:43:41 +02:00
Mihajlo Medjedovic
2634933e84 fix: accessToken not passed in function calls 2020-09-23 20:41:31 +02:00
Yury Shkoda
d60c0850c2 docs(context): update docs related to getComputeContextByName function 2020-09-23 17:21:27 +03:00
Yury Shkoda
491bc3371c feat(context): made getContextByName function public 2020-09-23 16:38:21 +03:00
Krishna Acondy
c1bab07b08 Merge pull request #105 from sasjs/file-upload-csrf
fix(jes-job-execution): prevent file upload requests failing with invalid CSRF token
2020-09-23 08:04:18 +01:00
Krishna Acondy
95f3ebd51d chore(dx): add pull request template 2020-09-22 12:40:11 +01:00
Krishna Acondy
0e5b72b54f Merge branch 'master' into file-upload-csrf 2020-09-22 12:29:48 +01:00
Krishna Acondy
33ce592379 fix(*): fix build issue with iconv-loader 2020-09-22 10:51:44 +01:00
Krishna Acondy
9f6591d7e3 chore(sasjs-tests): fix failing tests, bump adapter version 2020-09-22 09:32:45 +01:00
Krishna Acondy
5343ca00d8 fix(file-upload): maintain separate CSRF token for file uploads 2020-09-22 09:26:16 +01:00
Krishna Acondy
f764f1f22f Merge pull request #94 from sasjs/sas-job-absolute-paths
feat(job-execution): support absolute paths to SAS jobs
2020-09-22 08:46:04 +01:00
Krishna Acondy
978af5037e Merge branch 'master' of https://github.com/sasjs/adapter into sas-job-absolute-paths 2020-09-22 08:38:13 +01:00
Krishna Acondy
39e88052c7 Merge pull request #101 from sasjs/dependabot/npm_and_yarn/semantic-release-17.1.2
chore(deps-dev): bump semantic-release from 17.1.1 to 17.1.2
2020-09-22 08:34:38 +01:00
dependabot-preview[bot]
889351caf1 chore(deps-dev): bump semantic-release from 17.1.1 to 17.1.2
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 17.1.1 to 17.1.2.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v17.1.1...v17.1.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-22 07:33:28 +00:00
Krishna Acondy
e6476dc230 Merge pull request #102 from sasjs/dependabot/npm_and_yarn/webpack-4.44.2
chore(deps-dev): bump webpack from 4.44.1 to 4.44.2
2020-09-22 08:31:20 +01:00
dependabot-preview[bot]
e7de45c94f chore(deps-dev): bump webpack from 4.44.1 to 4.44.2
Bumps [webpack](https://github.com/webpack/webpack) from 4.44.1 to 4.44.2.
- [Release notes](https://github.com/webpack/webpack/releases)
- [Commits](https://github.com/webpack/webpack/compare/v4.44.1...v4.44.2)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-22 07:30:22 +00:00
Krishna Acondy
2f822aba71 Merge pull request #103 from sasjs/dependabot/npm_and_yarn/types/jest-26.0.14
chore(deps-dev): bump @types/jest from 26.0.13 to 26.0.14
2020-09-22 08:28:16 +01:00
dependabot-preview[bot]
ba687bf8e2 chore(deps-dev): bump @types/jest from 26.0.13 to 26.0.14
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.13 to 26.0.14.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/jest)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-22 07:21:57 +00:00
Krishna Acondy
618cbe5a21 Merge pull request #104 from sasjs/dependabot/npm_and_yarn/ts-loader-8.0.4
chore(deps-dev): bump ts-loader from 8.0.3 to 8.0.4
2020-09-22 08:19:40 +01:00
Krishna Acondy
d723150b6d fix(job-execution): support relative paths with web approach 2020-09-21 19:45:23 +01:00
dependabot-preview[bot]
b1ad983ca5 chore(deps-dev): bump ts-loader from 8.0.3 to 8.0.4
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.0.3 to 8.0.4.
- [Release notes](https://github.com/TypeStrong/ts-loader/releases)
- [Changelog](https://github.com/TypeStrong/ts-loader/blob/master/CHANGELOG.md)
- [Commits](https://github.com/TypeStrong/ts-loader/compare/v8.0.3...8.0.4)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-09-21 18:19:59 +00:00
Krishna Acondy
4711d0510e Merge branch 'master' of https://github.com/sasjs/adapter into sas-job-absolute-paths 2020-09-21 08:47:19 +01:00
Krishna Acondy
93854c287f Merge pull request #100 from sasjs/descs
chore(doc): Update descriptions of SASjsConfig properties
2020-09-21 08:45:29 +01:00
Krishna Acondy
687a3047fd chore(*): remove trailing slashes 2020-09-21 08:38:03 +01:00
Krishna Acondy
c067c6e74d chore(*): remove log 2020-09-21 08:26:18 +01:00
Allan Bowe
04b44f40ba docs: updates to description of SASjsConfig items 2020-09-20 20:23:04 +02:00
Allan Bowe
ce2126bd34 docs: typedocs 2020-09-20 20:22:44 +02:00
Krishna Acondy
638efe8899 chore(build): add terser plugin for code optimization 2020-09-20 12:00:02 +01:00
Krishna Acondy
23353355e4 chore(build): update webpack config to minify built code 2020-09-20 11:59:37 +01:00
Krishna Acondy
1be64798c5 fix(job-execution): fetch log when webout is not available 2020-09-20 11:42:49 +01:00
Krishna Acondy
a92a458cf1 chore(types): export types from index 2020-09-20 11:42:29 +01:00
Krishna Acondy
703fdf9c02 fix(job-execution): fix program name 2020-09-20 10:35:32 +01:00
Krishna Acondy
bc239cf5d6 fix(job-execution): fix program path for absolute path jobs 2020-09-18 17:40:35 +01:00
Krishna Acondy
86780db478 fix(job-execution): fix program variable for absolute path jobs 2020-09-18 17:32:12 +01:00
Krishna Acondy
5d5afa20c7 chore(doc): update documentation 2020-09-18 16:49:54 +01:00
Krishna Acondy
d662c1a981 Merge branch 'master' into sas-job-absolute-paths 2020-09-18 16:45:13 +01:00
Krishna Acondy
f3abafd5ed Merge branch 'master' into sas-job-absolute-paths 2020-09-18 15:06:50 +01:00
Krishna Acondy
5076ea696c feat(job-execution): support absolute job paths for JES and web approaches 2020-09-18 09:24:33 +01:00
Krishna Acondy
3a60e6422c chore(sasjs-tests): fix failing test, add test for absolute path 2020-09-18 08:33:46 +01:00
Krishna Acondy
b90b5d5c03 Merge branch 'master' of https://github.com/sasjs/adapter into sas-job-absolute-paths 2020-09-18 08:25:21 +01:00
Krishna Acondy
d5a8140d4f feat(job-execution): populate folder map on demand 2020-09-17 09:35:43 +01:00
Krishna Acondy
5f5d84da87 feat(job-execution): support absolute paths to SAS jobs 2020-09-16 08:49:18 +01:00
90 changed files with 25314 additions and 1313 deletions

17
PULL_REQUEST_TEMPLATE.md Normal file
View File

@@ -0,0 +1,17 @@
## Issue
Link any related issue(s) in this section.
## Intent
What this PR intends to achieve.
## Implementation
What code changes have been made to achieve the intent.
## Checks
- [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] All unit tests are passing (`npm test`).
- [ ] All `sasjs-tests` are passing (instructions available [here](https://github.com/sasjs/adapter/blob/master/sasjs-tests/README.md)).

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -30,8 +30,8 @@
$('#chart-container').append('<canvas id="myChart" style="display: none;"></canvas>')
// make a request to a SAS service
var type = $("#cars")[0].options[$("#cars")[0].selectedIndex].value;
// request data from an endpoint under your appLoc
sasJs.request("/common/getdata", {
// request data from an endpoint under your appLoc (missing opening slash implies relative path)
sasJs.request("common/getdata", {
// send data as an array of objects - each object is one row
fromjs: [{ type: type }]
}).then((response) => {

2264
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,28 +37,28 @@
"license": "ISC",
"devDependencies": {
"@types/isomorphic-fetch": "0.0.35",
"@types/jest": "^26.0.13",
"@types/jest": "^26.0.14",
"cp": "^0.2.0",
"jest": "^25.5.4",
"path": "^0.12.7",
"rimraf": "^3.0.2",
"semantic-release": "^17.1.1",
"semantic-release": "^17.1.2",
"terser-webpack-plugin": "^4.2.2",
"ts-jest": "^25.5.1",
"ts-loader": "^8.0.3",
"ts-loader": "^8.0.4",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"typedoc": "^0.17.8",
"typedoc-neo-theme": "^1.0.10",
"typedoc-plugin-external-module-name": "^4.0.3",
"typescript": "^3.9.7",
"uglifyjs-webpack-plugin": "^2.2.0",
"webpack": "^4.44.1",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.12"
},
"main": "index.js",
"dependencies": {
"es6-promise": "^4.2.8",
"form-data": "^3.0.0",
"isomorphic-fetch": "^2.2.1"
"isomorphic-fetch": "^3.0.0"
}
}

View File

@@ -1357,9 +1357,9 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
},
"@sasjs/adapter": {
"version": "1.3.13",
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.3.13.tgz",
"integrity": "sha512-dWcDxgY3FB7Yx1I5dPpeQeyJDu4lezhIFrjn6lbdwRhV15aqOt4l9o9qZP+VbgOXqyi9gN0Y+p+vs2chBDFQqg==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.12.0.tgz",
"integrity": "sha512-0uGQH9ynomWzdBaEujEtcR38q6V7LCgG0mrb1Wellv6cC/IHD3j6WfeZZAgtiMPeOSJjbCDBOlVnzC2TlBqJFw==",
"requires": {
"es6-promise": "^4.2.8",
"form-data": "^3.0.0",

View File

@@ -4,7 +4,7 @@
"homepage": ".",
"private": true,
"dependencies": {
"@sasjs/adapter": "^1.3.13",
"@sasjs/adapter": "^1.12.0",
"@sasjs/test-framework": "^1.4.0",
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",

View File

@@ -13,7 +13,7 @@ const defaultConfig: SASjsConfig = {
};
const customConfig = {
serverUrl: "url",
serverUrl: "http://url.com",
pathSAS9: "sas9",
pathSASViya: "viya",
appLoc: "/Public/seedapp",

View File

@@ -47,6 +47,16 @@ const getLargeObjectData = () => {
export const sendArrTests = (adapter: SASjs): TestSuite => ({
name: "sendArr",
tests: [
{
title: "Absolute paths",
description: "Should work with absolute paths to SAS jobs",
test: () => {
return adapter.request("/Public/app/common/sendArr", stringData);
},
assertion: (res: any) => {
return res.table1[0][0] === stringData.table1[0].col1;
}
},
{
title: "Single string value",
description: "Should send an array with a single string value",
@@ -78,7 +88,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
return adapter.request("common/sendArr", data).catch((e) => e);
},
assertion: (error: any) => {
return !!error && !!error.MESSAGE;
return !!error && !!error.body && !!error.body.message;
}
},
{
@@ -175,7 +185,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
};
return adapter.request("common/sendObj", invalidData).catch((e) => e);
},
assertion: (error: any) => !!error && !!error.MESSAGE
assertion: (error: any) => !!error && !!error.body && !!error.body.message
},
{
title: "Single string value",
@@ -209,7 +219,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
.catch((e) => e);
},
assertion: (error: any) => {
return !!error && !!error.MESSAGE;
return !!error && !!error.body && !!error.body.message;
}
},
{

View File

@@ -3,20 +3,22 @@ import {
parseAndSubmitAuthorizeForm,
convertToCSV,
makeRequest,
isRelativePath,
isUri,
isUrl
} from './utils'
import * as NodeFormData from 'form-data'
import * as path from 'path'
import {
Job,
Session,
Context,
ContextAllAttributes,
Folder,
CsrfToken,
EditContextInput
EditContextInput,
ErrorResponse,
JobDefinition
} from './types'
import { JobDefinition } from './types/JobDefinition'
import { formatDataForRequest } from './utils/formatDataForRequest'
import { SessionManager } from './SessionManager'
@@ -29,35 +31,43 @@ export class SASViyaApiClient {
private serverUrl: string,
private rootFolderName: string,
private contextName: string,
private setCsrfToken: (csrfToken: CsrfToken) => void,
private rootFolderMap = new Map<string, Job[]>()
private setCsrfToken: (csrfToken: CsrfToken) => void
) {
if (!rootFolderName) {
throw new Error('Root folder must be provided.')
}
if (serverUrl) isUrl(serverUrl)
}
private csrfToken: CsrfToken | null = null
private rootFolder: Folder | null = null
private fileUploadCsrfToken: CsrfToken | null = null
private _debug = false
private sessionManager = new SessionManager(
this.serverUrl,
this.contextName,
this.setCsrfToken
)
private isForceDeploy: boolean = false
private folderMap = new Map<string, Job[]>()
public get debug() {
return this._debug
}
public set debug(value: boolean) {
this._debug = value
this.sessionManager.debug = value
}
/**
* Returns a map containing the directory structure in the currently set root folder.
* Returns a list of jobs in the currently set root folder.
*/
public async getAppLocMap() {
if (this.rootFolderMap.size) {
return this.rootFolderMap
public async getJobsInFolder(folderPath: string) {
const path = isRelativePath(folderPath)
? `${this.rootFolderName}/${folderPath}`
: folderPath
if (this.folderMap.get(path)) {
return this.folderMap.get(path)
}
this.populateRootFolderMap()
return this.rootFolderMap
await this.populateFolderMap(path)
return this.folderMap.get(path)
}
/**
@@ -140,7 +150,6 @@ export class SASViyaApiClient {
linesOfCode,
context.name,
accessToken,
false,
null,
true
).catch(() => null)
@@ -192,7 +201,7 @@ export class SASViyaApiClient {
}
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts`,
`${this.serverUrl}/compute/contexts?limit=10000`,
{ headers }
)
const executionContext =
@@ -224,16 +233,16 @@ export class SASViyaApiClient {
* @param launchContextName - the name of the launcher context used by the compute service.
* @param sharedAccountId - the ID of the account to run the servers for this context.
* @param autoExecLines - the lines of code to execute during session initialization.
* @param authorizedUsers - an optional list of authorized user IDs.
* @param accessToken - an access token for an authorized user.
* @param authorizedUsers - an optional list of authorized user IDs.
*/
public async createContext(
contextName: string,
launchContextName: string,
sharedAccountId: string,
autoExecLines: string[],
authorizedUsers: string[],
accessToken?: string
accessToken?: string,
authorizedUsers?: string[]
) {
if (!contextName) {
throw new Error('Context name is required.')
@@ -313,10 +322,22 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
const originalContext = await this.getContextByName(
let originalContext
originalContext = await this.getComputeContextByName(
contextName,
accessToken
)
).catch((_) => {})
// Try to find context by id, when context name has been changed.
if (!originalContext) {
originalContext = await this.getComputeContextById(
editedContext.id!,
accessToken
).catch((err) => {
throw err
})
}
const { result: context, etag } = await this.request<Context>(
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
@@ -372,7 +393,7 @@ export class SASViyaApiClient {
headers.Authorization = `Bearer ${accessToken}`
}
const context = await this.getContextByName(contextName, accessToken)
const context = await this.getComputeContextByName(contextName, accessToken)
const deleteContextRequest: RequestInit = {
method: 'DELETE',
@@ -387,27 +408,23 @@ export class SASViyaApiClient {
/**
* Executes code on the current SAS Viya server.
* @param fileName - a name for 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 contextName - the context to execute the code in.
* @param accessToken - an access token for an authorized user.
* @param sessionId - optional session ID to reuse.
* @param silent - optional flag to disable logging.
* @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).
*/
public async executeScript(
jobName: string,
jobPath: string,
linesOfCode: string[],
contextName: string,
accessToken?: string,
silent = false,
data = null,
debug = false,
expectWebout = false
): Promise<any> {
silent = !debug
try {
const headers: any = {
'Content-Type': 'application/json'
@@ -430,19 +447,27 @@ export class SASViyaApiClient {
_OMITTEXTLOG: true
}
if (debug) {
if (this.debug) {
jobArguments['_OMITTEXTLOG'] = false
jobArguments['_OMITSESSIONRESULTS'] = false
jobArguments['_DEBUG'] = 131
}
const fileName = `exec-${
jobName.includes('/') ? jobName.split('/')[1] : jobName
}`
let fileName
if (isRelativePath(jobPath)) {
fileName = `exec-${
jobPath.includes('/') ? jobPath.split('/')[1] : jobPath
}`
} else {
const jobPathParts = jobPath.split('/')
fileName = jobPathParts.pop()
}
let jobVariables: any = {
SYS_JES_JOB_URI: '',
_program: this.rootFolderName + '/' + jobName
_program: isRelativePath(jobPath)
? this.rootFolderName + '/' + jobPath
: jobPath
}
let files: any[] = []
@@ -482,7 +507,7 @@ export class SASViyaApiClient {
postJobRequest
)
if (!silent) {
if (this.debug) {
console.log(`Job has been submitted for '${fileName}'.`)
console.log(
`You can monitor the job progress at '${this.serverUrl}${
@@ -491,12 +516,7 @@ export class SASViyaApiClient {
)
}
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
silent
)
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
@@ -508,7 +528,7 @@ export class SASViyaApiClient {
const logLink = currentJob.links.find((l) => l.rel === 'log')
if (debug && logLink) {
if (this.debug && logLink) {
log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content?limit=10000`,
{
@@ -534,9 +554,30 @@ export class SASViyaApiClient {
`${this.serverUrl}${resultLink}`,
{ headers },
'text'
).catch((e) => ({
result: JSON.stringify(e)
}))
).catch(async (e) => {
if (e && e.status === 404) {
if (logLink) {
log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content?limit=10000`,
{
headers
}
).then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
return Promise.reject(
new ErrorResponse('Job execution failed', {
status: 500,
body: log
})
)
}
}
return {
result: JSON.stringify(e)
}
})
}
await this.sessionManager.clearSession(executionSessionId, accessToken)
@@ -545,13 +586,11 @@ export class SASViyaApiClient {
} catch (e) {
if (e && e.status === 404) {
return this.executeScript(
jobName,
jobPath,
linesOfCode,
contextName,
accessToken,
silent,
data,
debug
data
)
} else {
throw e
@@ -583,8 +622,6 @@ export class SASViyaApiClient {
if (!parentFolderUri && parentFolderPath) {
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
if (!parentFolderUri) {
if (isForced) this.isForceDeploy = true
console.log(
`Parent folder at path '${parentFolderPath}' is not present.`
)
@@ -610,37 +647,16 @@ export class SASViyaApiClient {
`Parent folder '${newFolderName}' has been successfully created.`
)
parentFolderUri = `/folders/folders/${parentFolder.id}`
} else if (isForced && accessToken && !this.isForceDeploy) {
this.isForceDeploy = true
} else if (isForced && accessToken) {
const folderPath = parentFolderPath + '/' + folderName
const folderUri = await this.getFolderUri(folderPath, accessToken)
await this.deleteFolder(parentFolderPath, accessToken)
const newParentFolderPath = parentFolderPath.substring(
0,
parentFolderPath.lastIndexOf('/')
)
const newFolderName = `${parentFolderPath.split('/').pop()}`
if (newParentFolderPath === '') {
throw new Error(`Root folder has to be present on the server.`)
if (folderUri) {
await this.deleteFolder(
parentFolderPath + '/' + folderName,
accessToken
)
}
console.log(
`Creating parent folder:\n'${newFolderName}' in '${newParentFolderPath}'`
)
const parentFolder = await this.createFolder(
newFolderName,
newParentFolderPath,
undefined,
accessToken
)
console.log(
`Parent folder '${newFolderName}' has been successfully created.`
)
parentFolderUri = `/folders/folders/${parentFolder.id}`
}
}
@@ -662,8 +678,11 @@ export class SASViyaApiClient {
createFolderRequest
)
// updates rootFolderMap with newly created folder.
await this.populateRootFolderMap(accessToken)
// update folder map with newly created folder.
await this.populateFolderMap(
`${parentFolderPath}/${folderName}`,
accessToken
)
return createFolderResponse
}
@@ -892,30 +911,51 @@ export class SASViyaApiClient {
data?: any,
accessToken?: string
) {
if (!this.rootFolder) {
await this.populateRootFolder(accessToken)
}
if (!this.rootFolder) {
throw new Error(`Root folder was not found.`)
}
if (!this.rootFolderMap.size) {
await this.populateRootFolderMap(accessToken)
}
if (!this.rootFolderMap.size) {
if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error(
`The job '${sasJob}' was not found in '${this.rootFolderName}'.`
'Relative paths cannot be used without specifying a root folder name'
)
}
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
await this.populateFolderMap(
`${this.rootFolderName}/${folderName}`,
accessToken
)
if (!this.folderMap.get(`${this.rootFolderName}/${folderName}`)) {
throw new Error(
`The folder '${folderName}' was not found at '${this.serverUrl}/${this.rootFolderName}'`
)
}
} else {
const folderPathParts = sasJob.split('/')
folderPathParts.pop()
const folderPath = folderPathParts.join('/')
await this.populateFolderMap(folderPath, accessToken)
}
const headers: any = { 'Content-Type': 'application/json' }
if (!!accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const folderName = sasJob.split('/')[0]
const jobName = sasJob.split('/')[1]
const jobFolder = this.rootFolderMap.get(folderName)
const jobToExecute = jobFolder?.find((item) => item.name === jobName)
let jobToExecute
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
const jobName = sasJob.split('/')[1]
const jobFolder = this.folderMap.get(
`${this.rootFolderName}/${folderName}`
)
jobToExecute = jobFolder?.find((item) => item.name === jobName)
} else {
const folderPathParts = sasJob.split('/')
const jobName = folderPathParts.pop()
const folderPath = folderPathParts.join('/')
const jobFolder = this.folderMap.get(folderPath)
jobToExecute = jobFolder?.find((item) => item.name === jobName)
}
if (!jobToExecute) {
throw new Error(`Job was not found.`)
@@ -949,16 +989,14 @@ export class SASViyaApiClient {
linesToExecute,
contextName,
accessToken,
true,
data,
debug,
true
)
}
/**
* Executes a job via the SAS Viya Job Execution API.
* @param sasJob - the relative path to the job.
* Executes a job via the SAS Viya Job Execution API
* @param sasJob - the relative or absolute path to the job.
* @param contextName - the name of the context where the job is to be executed.
* @param debug - sets the _debug flag in the job arguments.
* @param data - any data to be passed in as input to the job.
@@ -971,20 +1009,34 @@ export class SASViyaApiClient {
data?: any,
accessToken?: string
) {
if (!this.rootFolder) {
await this.populateRootFolder(accessToken)
if (isRelativePath(sasJob) && !this.rootFolderName) {
throw new Error(
'Relative paths cannot be used without specifying a root folder name.'
)
}
if (!this.rootFolder) {
throw new Error(`Root folder was not found.`)
}
if (!this.rootFolderMap.size) {
await this.populateRootFolderMap(accessToken)
}
if (!this.rootFolderMap.size) {
throw new Error(
`The job '${sasJob}' was not found in folder '${this.rootFolderName}'.`
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
await this.populateFolderMap(
`${this.rootFolderName}/${folderName}`,
accessToken
)
if (!this.folderMap.get(`${this.rootFolderName}/${folderName}`)) {
throw new Error(
`The folder '${folderName}' was not found at '${this.serverUrl}/${this.rootFolderName}'.`
)
}
} else {
const folderPathParts = sasJob.split('/')
folderPathParts.pop()
const folderPath = folderPathParts.join('/')
await this.populateFolderMap(folderPath, accessToken)
if (!this.folderMap.get(folderPath)) {
throw new Error(
`The folder '${folderPath}' was not found at '${this.serverUrl}'.`
)
}
}
let files: any[] = []
@@ -992,124 +1044,123 @@ export class SASViyaApiClient {
files = await this.uploadTables(data, accessToken)
}
const jobName = path.basename(sasJob)
const jobFolder = sasJob.replace(`/${jobName}`, '')
const allJobsInFolder = this.rootFolderMap.get(jobFolder.replace('/', ''))
if (allJobsInFolder) {
const jobSpec = allJobsInFolder.find((j: Job) => j.name === jobName)
if (!jobSpec) {
throw new Error('Job was not found.')
}
const jobDefinitionLink = jobSpec?.links.find(
(l) => l.rel === 'getResource'
)?.href
if (!jobDefinitionLink) {
throw new Error('Job definition URI was not found.')
}
const requestInfo: any = {
method: 'GET'
}
const headers: any = { 'Content-Type': 'application/json' }
if (!!accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
requestInfo.headers = headers
const { result: jobDefinition } = await this.request<Job>(
`${this.serverUrl}${jobDefinitionLink}`,
requestInfo
)
const jobArguments: { [key: string]: any } = {
_contextName: contextName,
_program: `${this.rootFolderName}/${sasJob}`,
_webin_file_count: files.length,
_OMITJSONLISTING: true,
_OMITJSONLOG: true,
_OMITSESSIONRESULTS: true,
_OMITTEXTLISTING: true,
_OMITTEXTLOG: true
}
if (debug) {
jobArguments['_OMITTEXTLOG'] = 'false'
jobArguments['_OMITSESSIONRESULTS'] = 'false'
jobArguments['_DEBUG'] = 131
}
files.forEach((fileInfo, index) => {
jobArguments[
`_webin_fileuri${index + 1}`
] = `/files/files/${fileInfo.file.id}`
jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName
})
const postJobRequest = {
method: 'POST',
headers,
body: JSON.stringify({
name: `exec-${jobName}`,
description: 'Powered by SASjs',
jobDefinition,
arguments: jobArguments
})
}
const { result: postedJob, etag } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequest
)
const jobStatus = await this.pollJobState(
postedJob,
etag,
accessToken,
true
)
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
{ headers }
)
let jobResult, log
if (jobStatus === 'failed') {
return Promise.reject(currentJob.error)
}
const resultLink = currentJob.results['_webout.json']
const logLink = currentJob.links.find((l) => l.rel === 'log')
if (resultLink) {
jobResult = await this.request<any>(
`${this.serverUrl}${resultLink}/content`,
{ headers },
'text'
)
}
if (debug && logLink) {
log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content`,
{
headers
}
).then((res: any) =>
res.result.items.map((i: any) => i.line).join('\n')
)
}
return { result: jobResult?.result, log }
let jobToExecute: Job | undefined
let jobName: string | undefined
let jobPath: string | undefined
if (isRelativePath(sasJob)) {
const folderName = sasJob.split('/')[0]
jobName = sasJob.split('/')[1]
jobPath = `${this.rootFolderName}/${folderName}`
const jobFolder = this.folderMap.get(jobPath)
jobToExecute = jobFolder?.find((item) => item.name === jobName)
} else {
throw new Error(
`The job '${sasJob}' was not found in folder '${this.rootFolderName}'.`
const folderPathParts = sasJob.split('/')
jobName = folderPathParts.pop()
jobPath = folderPathParts.join('/')
const jobFolder = this.folderMap.get(jobPath)
jobToExecute = jobFolder?.find((item) => item.name === jobName)
}
if (!jobToExecute) {
throw new Error(`The job ${sasJob} was not found.`)
}
const jobDefinitionLink = jobToExecute?.links.find(
(l) => l.rel === 'getResource'
)?.href
const requestInfo: any = {
method: 'GET'
}
const headers: any = { 'Content-Type': 'application/json' }
if (!!accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
requestInfo.headers = headers
const { result: jobDefinition } = await this.request<Job>(
`${this.serverUrl}${jobDefinitionLink}`,
requestInfo
)
const jobArguments: { [key: string]: any } = {
_contextName: contextName,
_program: `${jobPath}/${jobName}`,
_webin_file_count: files.length,
_OMITJSONLISTING: true,
_OMITJSONLOG: true,
_OMITSESSIONRESULTS: true,
_OMITTEXTLISTING: true,
_OMITTEXTLOG: true
}
if (debug) {
jobArguments['_OMITTEXTLOG'] = 'false'
jobArguments['_OMITSESSIONRESULTS'] = 'false'
jobArguments['_DEBUG'] = 131
}
files.forEach((fileInfo, index) => {
jobArguments[
`_webin_fileuri${index + 1}`
] = `/files/files/${fileInfo.file.id}`
jobArguments[`_webin_name${index + 1}`] = fileInfo.tableName
})
const postJobRequest = {
method: 'POST',
headers,
body: JSON.stringify({
name: `exec-${jobName}`,
description: 'Powered by SASjs',
jobDefinition,
arguments: jobArguments
})
}
const { result: postedJob, etag } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
postJobRequest
)
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
const { result: currentJob } = await this.request<Job>(
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
{ headers }
)
let jobResult
let log
if (jobStatus === 'failed') {
return Promise.reject(currentJob.error)
}
const resultLink = currentJob.results['_webout.json']
const logLink = currentJob.links.find((l) => l.rel === 'log')
if (resultLink) {
jobResult = await this.request<any>(
`${this.serverUrl}${resultLink}/content`,
{ headers },
'text'
)
}
if (debug && logLink) {
log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content`,
{
headers
}
).then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
}
return { result: jobResult?.result, log }
}
private async populateRootFolderMap(accessToken?: string) {
const allItems = new Map<string, Job[]>()
const url = '/folders/folders/@item?path=' + this.rootFolderName
private async populateFolderMap(folderPath: string, accessToken?: string) {
const path = isRelativePath(folderPath)
? `${this.rootFolderName}/${folderPath}`
: folderPath
if (this.folderMap.get(path)) {
return
}
const url = '/folders/folders/@item?path=' + path
const requestInfo: any = {
method: 'GET'
}
@@ -1121,9 +1172,7 @@ export class SASViyaApiClient {
requestInfo
)
if (!folder) {
throw new Error(
`Not able to populate root folder map, because folder '${this.rootFolderName}' does not exist.`
)
throw new Error(`The path ${path} does not exist on ${this.serverUrl}`)
}
const { result: members } = await this.request<{ items: any[] }>(
`${this.serverUrl}/folders/folders/${folder.id}/members`,
@@ -1131,62 +1180,13 @@ export class SASViyaApiClient {
)
const itemsAtRoot = members.items
allItems.set('', itemsAtRoot)
const subfolderRequests = members.items
.filter((i: any) => i.contentType === 'folder')
.map(async (member: any) => {
const subFolderUrl =
'/folders/folders/@item?path=' +
this.rootFolderName +
'/' +
member.name
const { result: memberDetail } = await this.request<Folder>(
`${this.serverUrl}${subFolderUrl}`,
requestInfo
)
const membersLink = memberDetail.links.find(
(l: any) => l.rel === 'members'
)
const { result: memberContents } = await this.request<{ items: any[] }>(
`${this.serverUrl}${membersLink!.href}`,
requestInfo
)
const itemsInFolder = memberContents.items as any[]
allItems.set(member.name, itemsInFolder)
return itemsInFolder
})
await Promise.all(subfolderRequests)
this.rootFolderMap = allItems
}
private async populateRootFolder(accessToken?: string) {
const url = '/folders/folders/@item?path=' + this.rootFolderName
const requestInfo: RequestInit = {
method: 'GET'
}
if (accessToken) {
requestInfo.headers = { Authorization: `Bearer ${accessToken}` }
}
let error
const rootFolder = await this.request<Folder>(
`${this.serverUrl}${url}`,
requestInfo
)
this.rootFolder = rootFolder?.result || null
if (error) {
throw new Error(JSON.stringify(error))
}
this.folderMap.set(path, itemsAtRoot)
}
private async pollJobState(
postedJob: any,
etag: string | null,
accessToken?: string,
silent = false
accessToken?: string
) {
const MAX_POLL_COUNT = 1000
const POLL_INTERVAL = 100
@@ -1225,7 +1225,7 @@ export class SASViyaApiClient {
postedJobState === 'pending'
) {
if (stateLink) {
if (!silent) {
if (this.debug) {
console.log('Polling job status... \n')
}
const { result: jobState } = await this.request<string>(
@@ -1237,7 +1237,7 @@ export class SASViyaApiClient {
)
postedJobState = jobState.trim()
if (!silent) {
if (this.debug) {
console.log(`Current state: ${postedJobState}\n`)
}
pollCount++
@@ -1253,49 +1253,6 @@ export class SASViyaApiClient {
})
}
private async waitForSession(
session: Session,
etag: string | null,
accessToken?: string,
silent = false
) {
let sessionState = session.state
let pollCount = 0
const headers: any = {
'Content-Type': 'application/json',
'If-None-Match': etag
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const stateLink = session.links.find((l: any) => l.rel === 'state')
return new Promise(async (resolve, _) => {
if (sessionState === 'pending') {
if (stateLink) {
if (!silent) {
console.log('Polling session status... \n')
}
const { result: state } = await this.request<string>(
`${this.serverUrl}${stateLink.href}?wait=30`,
{
headers
},
'text'
)
sessionState = state.trim()
if (!silent) {
console.log(`Current state: ${sessionState}\n`)
}
pollCount++
resolve(sessionState)
}
} else {
resolve(sessionState)
}
})
}
private async uploadTables(data: any, accessToken?: string) {
const uploadedFiles = []
const headers: any = {
@@ -1321,7 +1278,9 @@ export class SASViyaApiClient {
const uploadResponse = await this.request<any>(
`${this.serverUrl}/files/files#rawUpload`,
createFileRequest
createFileRequest,
'json',
'fileUpload'
)
uploadedFiles.push({ tableName, file: uploadResponse.result })
@@ -1370,7 +1329,13 @@ export class SASViyaApiClient {
return `/folders/folders/${folder.id}`
}
private async getContextByName(
/**
* Returns a JSON representation of a compute context.
* @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" }
* @param contextName - the name of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextByName(
contextName: string,
accessToken?: string
): Promise<Context> {
@@ -1385,9 +1350,7 @@ export class SASViyaApiClient {
const { result: contexts } = await this.request<{ items: Context[] }>(
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
{ headers }
).catch((err) => {
throw err
})
)
if (!contexts || !(contexts.items && contexts.items.length)) {
throw new Error(
@@ -1398,6 +1361,33 @@ export class SASViyaApiClient {
return contexts.items[0]
}
/**
* Returns a JSON representation of a compute context.
* @param contextId - an id of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextById(
contextId: string,
accessToken?: string
): Promise<ContextAllAttributes> {
const headers: any = {
'Content-Type': 'application/json'
}
if (accessToken) {
headers.Authorization = `Bearer ${accessToken}`
}
const { result: context } = await this.request<ContextAllAttributes>(
`${this.serverUrl}/compute/contexts/${contextId}`,
{ headers }
).catch((err) => {
throw err
})
return context
}
/**
* Moves a Viya folder to a new location. The folder may be renamed at the same time.
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder to be moved. Providing URI instead of path will save one extra request.
@@ -1441,6 +1431,16 @@ export class SASViyaApiClient {
`${this.serverUrl}${url}`,
requestInfo
).catch((err) => {
if (err.code && err.code === 'ENOTFOUND') {
const notFoundError = {
body: JSON.stringify({
message: `Folder '${sourceFolder.split('/').pop()}' was not found.`
})
}
throw notFoundError
}
throw err
})
@@ -1476,22 +1476,36 @@ export class SASViyaApiClient {
this.setCsrfToken(csrfToken)
}
setFileUploadCsrfToken = (csrfToken: CsrfToken) => {
this.fileUploadCsrfToken = csrfToken
}
private async request<T>(
url: string,
options: RequestInit,
contentType: 'text' | 'json' = 'json'
contentType: 'text' | 'json' = 'json',
type: 'fileUpload' | 'other' = 'other'
) {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value
const callback =
type === 'fileUpload'
? this.setFileUploadCsrfToken
: this.setCsrfTokenLocal
if (type === 'other') {
if (this.csrfToken) {
options.headers = {
...options.headers,
[this.csrfToken.headerName]: this.csrfToken.value
}
}
} else {
if (this.fileUploadCsrfToken) {
options.headers = {
...options.headers,
[this.fileUploadCsrfToken.headerName]: this.fileUploadCsrfToken.value
}
}
}
return await makeRequest<T>(
url,
options,
this.setCsrfTokenLocal,
contentType
)
return await makeRequest<T>(url, options, callback, contentType)
}
}

View File

@@ -21,7 +21,8 @@ import {
parseGeneratedCode,
parseWeboutResponse,
needsRetry,
asyncForEach
asyncForEach,
isRelativePath
} from './utils'
import {
SASjsConfig,
@@ -112,16 +113,16 @@ export default class SASjs {
* @param launchContextName - the name of the launcher context used by the compute service.
* @param sharedAccountId - the ID of the account to run the servers for this context as.
* @param autoExecLines - the lines of code to execute during session initialization.
* @param authorizedUsers - an optional list of authorized user IDs.
* @param accessToken - an access token for an authorized user.
* @param authorizedUsers - an optional list of authorized user IDs.
*/
public async createContext(
contextName: string,
launchContextName: string,
sharedAccountId: string,
autoExecLines: string[],
authorizedUsers: string[],
accessToken: string
accessToken: string,
authorizedUsers?: string[]
) {
this.isMethodSupported('createContext', ServerType.SASViya)
@@ -130,8 +131,8 @@ export default class SASjs {
launchContextName,
sharedAccountId,
autoExecLines,
authorizedUsers,
accessToken
accessToken,
authorizedUsers
)
}
@@ -166,6 +167,38 @@ export default class SASjs {
return await this.sasViyaApiClient!.deleteContext(contextName, accessToken)
}
/**
* Returns a JSON representation of a compute context.
* @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" }
* @param contextName - the name of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextByName(
contextName: string,
accessToken?: string
) {
this.isMethodSupported('getComputeContextByName', ServerType.SASViya)
return await this.sasViyaApiClient!.getComputeContextByName(
contextName,
accessToken
)
}
/**
* Returns a JSON representation of a compute context.
* @param contextId - an id of the context to return.
* @param accessToken - an access token for an authorized user.
*/
public async getComputeContextById(contextId: string, accessToken?: string) {
this.isMethodSupported('getComputeContextById', ServerType.SASViya)
return await this.sasViyaApiClient!.getComputeContextById(
contextId,
accessToken
)
}
public async createSession(contextName: string, accessToken: string) {
this.isMethodSupported('createSession', ServerType.SASViya)
@@ -176,9 +209,7 @@ export default class SASjs {
fileName: string,
linesOfCode: string[],
contextName: string,
accessToken?: string,
sessionId = '',
silent = false
accessToken?: string
) {
this.isMethodSupported('executeScriptSASViya', ServerType.SASViya)
@@ -187,9 +218,7 @@ export default class SASjs {
linesOfCode,
contextName,
accessToken,
silent,
null,
this.sasjsConfig.debug
null
)
}
@@ -210,8 +239,6 @@ export default class SASjs {
sasApiClient?: SASViyaApiClient,
isForced?: boolean
) {
this.isMethodSupported('createFolder', ServerType.SASViya)
if (sasApiClient)
return await sasApiClient.createFolder(
folderName,
@@ -228,6 +255,40 @@ export default class SASjs {
)
}
/**
* For performance (and in case of accidental error) the `deleteFolder` function does not actually delete the folder (and all its content and subfolder content). Instead the folder is simply moved to the recycle bin. Deletion time will be added to the folder name.
* @param folderPath - the full path (eg `/Public/example/deleteThis`) of the folder to be deleted.
* @param accessToken - an access token for authorizing the request.
*/
public async deleteFolder(folderPath: string, accessToken: string) {
this.isMethodSupported('deleteFolder', ServerType.SASViya)
return await this.sasViyaApiClient?.deleteFolder(folderPath, accessToken)
}
/**
* Moves folder to a new location. The folder may be renamed at the same time.
* @param sourceFolder - the full path (eg `/Public/example/myFolder`) or URI of the source folder to be moved. Providing URI instead of path will save one extra request.
* @param targetParentFolder - the full path or URI of the _parent_ folder to which the `sourceFolder` will be moved (eg `/Public/newDestination`). To move a folder, a user has to have write permissions in targetParentFolder. Providing URI instead of path will save one extra request.
* @param targetFolderName - the name of the "moved" folder. If left blank, the original folder name will be used (eg `myFolder` in `/Public/newDestination/myFolder` for the example above). Optional field.
* @param accessToken - an access token for authorizing the request.
*/
public async moveFolder(
sourceFolder: string,
targetParentFolder: string,
targetFolderName: string,
accessToken: string
) {
this.isMethodSupported('moveFolder', ServerType.SASViya)
return await this.sasViyaApiClient?.moveFolder(
sourceFolder,
targetParentFolder,
targetFolderName,
accessToken
)
}
public async createJobDefinition(
jobName: string,
code: string,
@@ -345,6 +406,9 @@ export default class SASjs {
*/
public setDebugState(value: boolean) {
this.sasjsConfig.debug = value
if (this.sasViyaApiClient) {
this.sasViyaApiClient.debug = value
}
}
/**
@@ -501,8 +565,6 @@ export default class SASjs {
...config
}
sasJob = sasJob.startsWith('/') ? sasJob.replace('/', '') : sasJob
if (config.serverType === ServerType.SASViya && config.contextName) {
if (config.useComputeApi) {
requestResponse = await this.executeJobViaComputeApi(
@@ -572,6 +634,7 @@ export default class SASjs {
this.sasjsConfig.contextName,
this.setCsrfTokenApi
)
sasApiClient.debug = this.sasjsConfig.debug
} else if (this.sasjsConfig.serverType === ServerType.SAS9) {
sasApiClient = new SAS9ApiClient(serverUrl)
}
@@ -794,11 +857,15 @@ export default class SASjs {
SASjob: sasJob,
data
}
const program = config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
const program = isRelativePath(sasJob)
? config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
: sasJob
const jobUri =
config.serverType === 'SASVIYA' ? await this.getJobUri(sasJob) : ''
config.serverType === ServerType.SASViya
? await this.getJobUri(sasJob)
: ''
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
jobUri.length > 0
? '__program=' + program + '&_job=' + jobUri
@@ -1091,21 +1158,26 @@ export default class SASjs {
private async getJobUri(sasJob: string) {
if (!this.sasViyaApiClient) return ''
const jobMap: any = await this.sasViyaApiClient.getAppLocMap()
let uri = ''
if (jobMap.size) {
const jobKey = sasJob.split('/')[0]
const jobName = sasJob.split('/')[1]
let folderPath
let jobName: string
if (isRelativePath(sasJob)) {
folderPath = sasJob.split('/')[0]
jobName = sasJob.split('/')[1]
} else {
const folderPathParts = sasJob.split('/')
jobName = folderPathParts.pop() || ''
folderPath = folderPathParts.join('/')
}
const locJobs = jobMap.get(jobKey)
if (locJobs) {
const job = locJobs.find(
(el: any) => el.name === jobName && el.contentType === 'jobDefinition'
)
if (job) {
uri = job.uri
}
const locJobs = await this.sasViyaApiClient.getJobsInFolder(folderPath)
if (locJobs) {
const job = locJobs.find(
(el: any) => el.name === jobName && el.contentType === 'jobDefinition'
)
if (job) {
uri = job.uri
}
}
return uri
@@ -1280,6 +1352,8 @@ export default class SASjs {
this.sasjsConfig.contextName,
this.setCsrfTokenApi
)
this.sasViyaApiClient.debug = this.sasjsConfig.debug
}
if (this.sasjsConfig.serverType === ServerType.SAS9) {
if (this.sas9ApiClient)

View File

@@ -15,6 +15,15 @@ export class SessionManager {
private sessions: Session[] = []
private currentContext: Context | null = null
private csrfToken: CsrfToken | null = null
private _debug: boolean = false
public get debug() {
return this._debug
}
public set debug(value: boolean) {
this._debug = value
}
async getSession(accessToken?: string) {
await this.createSessions(accessToken)
@@ -78,7 +87,7 @@ export class SessionManager {
if (!this.currentContext) {
const { result: contexts } = await this.request<{
items: Context[]
}>(`${this.serverUrl}/compute/contexts`, {
}>(`${this.serverUrl}/compute/contexts?limit=10000`, {
headers: this.getHeaders(accessToken)
})
@@ -115,8 +124,7 @@ export class SessionManager {
private async waitForSession(
session: Session,
etag: string | null,
accessToken?: string,
silent = false
accessToken?: string
) {
let sessionState = session.state
const headers: any = {
@@ -127,7 +135,7 @@ export class SessionManager {
return new Promise(async (resolve, _) => {
if (sessionState === 'pending') {
if (stateLink) {
if (!silent) {
if (this.debug) {
console.log('Polling session status... \n') // ?
}
const { result: state } = await this.request<string>(
@@ -139,7 +147,7 @@ export class SessionManager {
)
sessionState = state.trim()
if (!silent) {
if (this.debug) {
console.log(`Current state is '${sessionState}'\n`)
}
resolve(sessionState)

View File

@@ -14,4 +14,26 @@ export interface EditContextInput {
attributes?: any
authorizedUsers?: string[]
authorizeAllAuthenticatedUsers?: boolean
id?: string
}
export interface ContextAllAttributes {
attributes: {
reuseServerProcesses: boolean
runServerAs: string
}
modifiedTimeStamp: string
createdBy: string
creationTimeStamp: string
launchType: string
environment: {
autoExecLines: [string]
}
launchContext: {
contextName: string
}
modifiedBy: string
id: string
version: number
name: string
}

View File

@@ -1,8 +1,8 @@
import { ServerType } from './ServerType'
/**
* Specifies the configuration for the SASjs instance.
*
* Specifies the configuration for the SASjs instance - eg where and how to
* connect to SAS.
*/
export class SASjsConfig {
/**
@@ -11,11 +11,24 @@ export class SASjsConfig {
* streamed.
*/
serverUrl: string = ''
/**
* The location of the Stored Process Web Application. By default the adapter
* will use '/SASStoredProcess/do' on SAS 9.
*/
pathSAS9: string = ''
/**
* The location of the Job Execution Web Application. By default the adapter
* will use '/SASJobExecution' on SAS Viya.
*/
pathSASViya: string = ''
/**
* The appLoc is the parent folder under which the SAS services (STPs or Job
* Execution Services) are stored.
* Execution Services) are stored. We recommend that each app is stored in
* a dedicated parent folder (the appLoc) and the services are grouped inside
* subfolders within the appLoc - allowing functionality to be restricted
* according to those groups at backend.
* When using appLoc, the paths provided in the `request` function should be
* _without_ a leading slash (/).
*/
appLoc: string = ''
/**
@@ -26,6 +39,22 @@ export class SASjsConfig {
* Set to `true` to enable additional debugging.
*/
debug: boolean = true
/**
* The name of the compute context to use when calling the Viya APIs directly.
* Example value: 'SAS Job Execution compute context'
* If set to missing or empty, and useComputeApi is true, the adapter will use
* the JES APIs. If provided, the Job Code will be executed in pooled
* compute sessions on this named context.
*/
contextName: string = ''
/**
* Set to `false` to use the Job Execution Web Service. To enhance VIYA
* performance, set to `true` and provide a `contextName` on which to run
* the code. When running on a named context, the code executes under the
* user identity. When running as a Job Execution service, the code runs
* under the identity in the JES context. If no `contextName` is provided,
* and `useComputeApi` is `true`, then the service will run as a Job, except
* triggered using the APIs instead of the Job Execution Web Service broker.
*/
useComputeApi = false
}

View File

@@ -1,7 +1,10 @@
export * from './Context'
export * from './CsrfToken'
export * from './ErrorResponse'
export * from './Folder'
export * from './Job'
export * from './JobDefinition'
export * from './JobResult'
export * from './Link'
export * from './SASjsConfig'
export * from './SASjsRequest'
@@ -9,4 +12,3 @@ export * from './SASjsWaitingRequest'
export * from './ServerType'
export * from './Session'
export * from './UploadFile'
export * from './ErrorResponse'

View File

@@ -4,6 +4,9 @@ export * from './convertToCsv'
export * from './isAuthorizeFormRequired'
export * from './isLoginRequired'
export * from './isLoginSuccess'
export * from './isRelativePath'
export * from './isUri'
export * from './isUrl'
export * from './makeRequest'
export * from './needsRetry'
export * from './parseAndSubmitAuthorizeForm'
@@ -13,5 +16,3 @@ export * from './parseSasViyaLog'
export * from './serialize'
export * from './splitChunks'
export * from './parseWeboutResponse'
export * from './isUri'
export * from './isUrl'

View File

@@ -0,0 +1,2 @@
export const isRelativePath = (uri: string): boolean =>
!!uri && !uri.startsWith('/')

View File

@@ -48,7 +48,7 @@ export async function makeRequest<T>(
try {
body = JSON.parse(body)
body.message = `Forbidden. Check your permissions and user groups. ${
body.message = `Forbidden. Check your permissions and user groups, and also the scopes granted when registering your CLIENT_ID. ${
body.message || ''
}`

View File

@@ -1,49 +1,56 @@
const path = require("path");
const webpack = require("webpack");
const UglifyJsPlugin = require("uglifyjs-webpack-plugin");
const path = require('path')
const webpack = require('webpack')
const terserPlugin = require('terser-webpack-plugin')
const browserConfig = {
entry: "./src/index.ts",
devtool: "inline-source-map",
mode: "development",
entry: './src/index.ts',
devtool: 'inline-source-map',
mode: 'production',
optimization: {
minimize: false,
minimizer: [
new terserPlugin({
cache: true,
parallel: true,
sourceMap: true,
terserOptions: {}
})
]
},
module: {
rules: [
{
test: /\.ts?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [".ts", ".js"],
extensions: ['.ts', '.js']
},
output: {
filename: "index.js",
path: path.resolve(__dirname, "build"),
libraryTarget: "umd",
library: "SASjs",
filename: 'index.js',
path: path.resolve(__dirname, 'build'),
libraryTarget: 'umd',
library: 'SASjs'
},
plugins: [
new webpack.ContextReplacementPlugin(/moment[\/\\]locale$/, /en/),
new webpack.SourceMapDevToolPlugin({
filename: null,
exclude: [/node_modules/],
test: /\.ts($|\?)/i,
}),
],
};
test: /\.ts($|\?)/i
})
]
}
const nodeConfig = {
...browserConfig,
target: "node",
target: 'node',
output: {
...browserConfig.output,
path: path.resolve(__dirname, "build", "node"),
},
};
path: path.resolve(__dirname, 'build', 'node')
}
}
module.exports = [browserConfig, nodeConfig];
module.exports = [browserConfig, nodeConfig]