1
0
mirror of https://github.com/sasjs/adapter.git synced 2026-01-08 21:10:05 +00:00

Compare commits

...

50 Commits

Author SHA1 Message Date
Krishna Acondy
3f0590e0fe Merge pull request #158 from sasjs/issue157
Issue157
2020-11-25 11:14:39 +00:00
Mihajlo Medjedovic
5efb294ff2 fix: tests updated 2020-11-25 12:03:07 +01:00
Mihajlo Medjedovic
011e2d83dc fix: computeApi debug override 2020-11-24 15:19:19 +01:00
Mihajlo Medjedovic
e36b511530 test: make error and capture log, fixed 2020-11-24 13:02:17 +01:00
Mihajlo Medjedovic
b6a2a85d1d fix: compute api log not appended to sasjs requests 2020-11-24 11:59:17 +01:00
Krishna Acondy
f1cceeb5e6 Merge pull request #156 from sasjs/fix
fix(job execute): details for failing job
2020-11-24 07:46:32 +00:00
Krishna Acondy
6fee2548fd fix(job-failure): return original job object when a job has failed 2020-11-24 07:44:11 +00:00
Saad Jutt
91005066cf fix(job execute): details for failing job 2020-11-24 04:16:18 +05:00
Krishna Acondy
e1f17ef47d Merge pull request #127 from sasjs/dependabot/npm_and_yarn/terser-webpack-plugin-4.2.3
chore(deps-dev): bump terser-webpack-plugin from 4.2.2 to 4.2.3
2020-11-20 09:07:14 +00:00
dependabot-preview[bot]
8a40071c35 chore(deps-dev): bump terser-webpack-plugin from 4.2.2 to 4.2.3
Bumps [terser-webpack-plugin](https://github.com/webpack-contrib/terser-webpack-plugin) from 4.2.2 to 4.2.3.
- [Release notes](https://github.com/webpack-contrib/terser-webpack-plugin/releases)
- [Changelog](https://github.com/webpack-contrib/terser-webpack-plugin/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack-contrib/terser-webpack-plugin/compare/v4.2.2...v4.2.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 09:01:25 +00:00
Krishna Acondy
430957eb3d Merge pull request #132 from sasjs/dependabot/npm_and_yarn/npm-user-validate-1.0.1
chore(deps): [security] bump npm-user-validate from 1.0.0 to 1.0.1
2020-11-20 08:58:57 +00:00
dependabot-preview[bot]
25874be679 chore(deps): [security] bump npm-user-validate from 1.0.0 to 1.0.1
Bumps [npm-user-validate](https://github.com/npm/npm-user-validate) from 1.0.0 to 1.0.1. **This update includes a security fix.**
- [Release notes](https://github.com/npm/npm-user-validate/releases)
- [Commits](https://github.com/npm/npm-user-validate/compare/v1.0.0...v1.0.1)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:57:20 +00:00
Krishna Acondy
ed8440434f Merge pull request #135 from sasjs/dependabot/npm_and_yarn/types/jest-26.0.15
chore(deps-dev): bump @types/jest from 26.0.14 to 26.0.15
2020-11-20 08:55:42 +00:00
dependabot-preview[bot]
0f9884c1b6 chore(deps-dev): bump @types/jest from 26.0.14 to 26.0.15
Bumps [@types/jest](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/jest) from 26.0.14 to 26.0.15.
- [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-11-20 08:46:00 +00:00
Krishna Acondy
d126a05347 Merge pull request #146 from sasjs/dependabot/npm_and_yarn/webpack-cli-4.2.0
chore(deps-dev): bump webpack-cli from 3.3.12 to 4.2.0
2020-11-20 08:43:50 +00:00
dependabot-preview[bot]
3e26bbbbba chore(deps-dev): bump webpack-cli from 3.3.12 to 4.2.0
Bumps [webpack-cli](https://github.com/webpack/webpack-cli) from 3.3.12 to 4.2.0.
- [Release notes](https://github.com/webpack/webpack-cli/releases)
- [Changelog](https://github.com/webpack/webpack-cli/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-cli/compare/v3.3.12...webpack-cli@4.2.0)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:34:49 +00:00
Krishna Acondy
982cc8f7a0 Merge pull request #149 from sasjs/dependabot/npm_and_yarn/ts-loader-8.0.11
chore(deps-dev): bump ts-loader from 8.0.4 to 8.0.11
2020-11-20 08:32:39 +00:00
dependabot-preview[bot]
d1770698e0 chore(deps-dev): bump ts-loader from 8.0.4 to 8.0.11
Bumps [ts-loader](https://github.com/TypeStrong/ts-loader) from 8.0.4 to 8.0.11.
- [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/8.0.4...v8.0.11)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:29:41 +00:00
Krishna Acondy
b78e8617c4 Merge pull request #151 from sasjs/dependabot/npm_and_yarn/semantic-release-17.2.3
[security] chore(deps-dev): bump semantic-release from 17.1.2 to 17.2.3
2020-11-20 08:27:32 +00:00
dependabot-preview[bot]
3ce9ca0986 chore(deps-dev): bump semantic-release from 17.1.2 to 17.2.3
Bumps [semantic-release](https://github.com/semantic-release/semantic-release) from 17.1.2 to 17.2.3.
- [Release notes](https://github.com/semantic-release/semantic-release/releases)
- [Commits](https://github.com/semantic-release/semantic-release/compare/v17.1.2...v17.2.3)

Signed-off-by: dependabot-preview[bot] <support@dependabot.com>
2020-11-20 08:19:07 +00:00
Yury Shkoda
04d17c3680 Merge pull request #153 from sasjs/context-issue
fix(context): fixed log parsing
2020-11-19 16:53:48 +03:00
Yury Shkoda
d26e15f91c Merge branch 'master' into context-issue 2020-11-19 16:43:30 +03:00
Yury Shkoda
83c46091b3 fix(context): fixed log parsing 2020-11-19 16:39:47 +03:00
Krishna Acondy
d640d7c040 Merge pull request #152 from sasjs/issue117
fix: viya login issue
2020-11-18 17:16:26 +00:00
Mihajlo Medjedovic
c934eb2b08 test: added test for multiple login attempts 2020-11-18 14:13:13 +01:00
Mihajlo Medjedovic
24dd5e32ad style: lint 2020-11-18 13:10:29 +01:00
Mihajlo Medjedovic
a23103b2c3 fix: viya login issue 2020-11-18 13:09:49 +01:00
Yury Shkoda
35aa4235e4 Merge pull request #148 from sasjs/issue113
feat: service not found error handling
2020-11-16 09:54:03 +03:00
Mihajlo Medjedovic
e9be1cf99a fix: service not found error handling 2020-11-12 16:03:32 +01:00
Mihajlo Medjedovic
c7b0821081 style: lint 2020-11-09 18:24:10 +01:00
Mihajlo Medjedovic
4a4618dd32 feat: service not found error handling for SAS9 2020-11-09 18:19:39 +01:00
Krishna Acondy
d223e83c60 Merge pull request #142 from sasjs/issue138
fix(file-uploader): handle errors during file upload
2020-11-02 14:54:58 +00:00
Krishna Acondy
d1f1a20126 chore(file-uploader): move uploader to describe scope 2020-11-02 09:29:33 +00:00
Krishna Acondy
4b89e3762f chore(file-uploader): remove duplication 2020-11-02 08:54:26 +00:00
Krishna Acondy
bc110288de chore(file-uploader): improve mocking of fetch, add tests for all error scenarios 2020-11-02 08:51:27 +00:00
Krishna Acondy
e94e16b52c chore(*): fix linting errors 2020-11-02 07:55:48 +00:00
Krishna Acondy
76aacee016 Merge branch 'master' into issue138 2020-11-02 07:42:39 +00:00
Mihajlo Medjedovic
1a3bd5d1f5 chore: lint 2020-10-30 16:13:03 +01:00
Mihajlo Medjedovic
3f6e89d716 fix: file uploader error handling and tests 2020-10-30 16:11:50 +01:00
Yury Shkoda
361ec84638 Merge pull request #141 from sasjs/fetch-log-feat
chore(log): made 'fetchLogFileContent' method public
2020-10-30 11:28:07 +03:00
Yury Shkoda
35cc1e4f62 chore(fetchLogFileContent): made accessToken optional 2020-10-30 11:26:43 +03:00
Yury Shkoda
64a976e888 doc: updated docs 2020-10-30 10:36:53 +03:00
Yury Shkoda
7e2cb8491f feat(log): made 'fetchLogFileContent' method public 2020-10-30 10:36:04 +03:00
Krishna Acondy
2cdab7522d Merge pull request #139 from sasjs/location-issue
fix(location): added handle cases when 'location' is not defined
2020-10-29 08:07:11 +00:00
Yury Shkoda
a07eabc408 fix(location): added handle cases when 'location' is not defined 2020-10-29 10:07:30 +03:00
Mihajlo Medjedovic
7279c23fe2 fix: FIleUploader added catch 2020-10-27 14:50:05 +01:00
Mihajlo Medjedovic
80707d77d9 gitfe Merge branches 'errorResponse' and 'master' of github.com:sasjs/adapter 2020-10-27 14:40:41 +01:00
Yury Shkoda
d5920c5885 Merge pull request #134 from sasjs/executeComputeJob
fix(executeComputeJob): added fix for cases when code was not provided
2020-10-21 11:55:43 +03:00
Yury Shkoda
6a3a6b4485 fix(executeComputeJob): added fix for cases when code was not provided 2020-10-21 11:45:21 +03:00
Mihajlo Medjedovic
3f796b300d fix: ErrorResponse body changed to error 2020-10-07 11:15:00 +02:00
44 changed files with 5590 additions and 1686 deletions

View File

@@ -14,4 +14,5 @@ What code changes have been made to achieve the intent.
- [ ] Code is formatted correctly (`npm run lint:fix`). - [ ] Code is formatted correctly (`npm run lint:fix`).
- [ ] All unit tests are passing (`npm test`). - [ ] All unit tests are passing (`npm test`).
- [ ] All `sasjs-tests` 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)). - [ ] 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

2579
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,15 +37,15 @@
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@types/isomorphic-fetch": "0.0.35", "@types/isomorphic-fetch": "0.0.35",
"@types/jest": "^26.0.14", "@types/jest": "^26.0.15",
"cp": "^0.2.0", "cp": "^0.2.0",
"jest": "^25.5.4", "jest": "^25.5.4",
"path": "^0.12.7", "path": "^0.12.7",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"semantic-release": "^17.1.2", "semantic-release": "^17.2.3",
"terser-webpack-plugin": "^4.2.2", "terser-webpack-plugin": "^4.2.3",
"ts-jest": "^25.5.1", "ts-jest": "^25.5.1",
"ts-loader": "^8.0.4", "ts-loader": "^8.0.11",
"tslint": "^6.1.3", "tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0", "tslint-config-prettier": "^1.18.0",
"typedoc": "^0.17.8", "typedoc": "^0.17.8",
@@ -53,7 +53,7 @@
"typedoc-plugin-external-module-name": "^4.0.3", "typedoc-plugin-external-module-name": "^4.0.3",
"typescript": "^3.9.7", "typescript": "^3.9.7",
"webpack": "^4.44.2", "webpack": "^4.44.2",
"webpack-cli": "^3.3.12" "webpack-cli": "^4.2.0"
}, },
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {

View File

@@ -1357,9 +1357,9 @@
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==" "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
}, },
"@sasjs/adapter": { "@sasjs/adapter": {
"version": "1.12.0", "version": "1.18.3",
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.12.0.tgz", "resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.18.3.tgz",
"integrity": "sha512-0uGQH9ynomWzdBaEujEtcR38q6V7LCgG0mrb1Wellv6cC/IHD3j6WfeZZAgtiMPeOSJjbCDBOlVnzC2TlBqJFw==", "integrity": "sha512-wzDFJRyt2dXFeQP+JzqRGunYUbujrAbU/Jc4IWg5URsCkGAzwsNl/4G0xJVbqOTy1MvoZ431rfCnvhkUlg7D3Q==",
"requires": { "requires": {
"es6-promise": "^4.2.8", "es6-promise": "^4.2.8",
"form-data": "^3.0.0", "form-data": "^3.0.0",

View File

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

View File

@@ -1,9 +0,0 @@
import React from "react";
import { render } from "@testing-library/react";
import App from "./App";
test("renders learn react link", () => {
const { getByText } = render(<App />);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

View File

@@ -3,12 +3,12 @@ import { TestSuite } from "@sasjs/test-framework";
const defaultConfig: SASjsConfig = { const defaultConfig: SASjsConfig = {
serverUrl: window.location.origin, serverUrl: window.location.origin,
pathSAS9: "/SASStoredProcess/do", pathSAS9: '/SASStoredProcess/do',
pathSASViya: "/SASJobExecution", pathSASViya: '/SASJobExecution',
appLoc: "/Public/seedapp", appLoc: '/Public/seedapp',
serverType: ServerType.SASViya, serverType: ServerType.SASViya,
debug: true, debug: false,
contextName: "SAS Job Execution compute context", contextName: 'SAS Job Execution compute context',
useComputeApi: false useComputeApi: false
}; };
@@ -37,6 +37,17 @@ export const basicTests = (
assertion: (response: any) => assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName response && response.isLoggedIn && response.userName === userName
}, },
{
title: "Multiple Log in attempts",
description: "Should fail on first attempt and should log the user in on second attempt",
test: async () => {
await adapter.logOut()
await adapter.logIn('invalid', 'invalid')
return adapter.logIn(userName, password)
},
assertion: (response: any) =>
response && response.isLoggedIn && response.userName === userName
},
{ {
title: "Default config", title: "Default config",
description: description:
@@ -46,6 +57,7 @@ export const basicTests = (
}, },
assertion: (sasjsInstance: SASjs) => { assertion: (sasjsInstance: SASjs) => {
const sasjsConfig = sasjsInstance.getSasjsConfig(); const sasjsConfig = sasjsInstance.getSasjsConfig();
return ( return (
sasjsConfig.serverUrl === defaultConfig.serverUrl && sasjsConfig.serverUrl === defaultConfig.serverUrl &&
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 && sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&

View File

@@ -88,7 +88,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
return adapter.request("common/sendArr", data).catch((e) => e); return adapter.request("common/sendArr", data).catch((e) => e);
}, },
assertion: (error: any) => { assertion: (error: any) => {
return !!error && !!error.body && !!error.body.message; return !!error && !!error.error && !!error.error.message;
} }
}, },
{ {
@@ -185,7 +185,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
}; };
return adapter.request("common/sendObj", invalidData).catch((e) => e); return adapter.request("common/sendObj", invalidData).catch((e) => e);
}, },
assertion: (error: any) => !!error && !!error.body && !!error.body.message assertion: (error: any) => !!error && !!error.error && !!error.error.message
}, },
{ {
title: "Single string value", title: "Single string value",
@@ -219,7 +219,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
.catch((e) => e); .catch((e) => e);
}, },
assertion: (error: any) => { assertion: (error: any) => {
return !!error && !!error.body && !!error.body.message; return !!error && !!error.error && !!error.error.message;
} }
}, },
{ {

View File

@@ -23,22 +23,24 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
}, },
{ {
title: "Make error and capture log", title: "Make error and capture log",
description: "Should make an error and capture log", description: "Should make an error and capture log, in the same time it is testing if debug override is working",
test: async () => { test: async () => {
return new Promise(async (resolve, reject) => { return new Promise(async (resolve, reject) => {
adapter adapter
.request("common/makeErr", data) .request("common/makeErr", data, {debug: true})
.then((res) => { .then((res) => {
//no action here, this request must throw error //no action here, this request must throw error
}) })
.catch((err) => { .catch((err) => {
let sasRequests = adapter.getSasRequests(); let sasRequests = adapter.getSasRequests();
let makeErrRequest = let makeErrRequest: any =
sasRequests.find((req) => sasRequests.find((req) =>
req.serviceLink.includes("makeErr") req.serviceLink.includes("makeErr")
) || null; ) || null;
resolve(!!makeErrRequest); if (!makeErrRequest) resolve(false)
resolve(!!(makeErrRequest.logFile && makeErrRequest.logFile.length > 0));
}); });
}); });
}, },

View File

@@ -1,6 +1,7 @@
import { isLogInRequired, needsRetry, isUrl } from './utils' import { isLogInRequired, needsRetry, isUrl } from './utils'
import { CsrfToken } from './types/CsrfToken' import { CsrfToken } from './types/CsrfToken'
import { UploadFile } from './types/UploadFile' import { UploadFile } from './types/UploadFile'
import { ErrorResponse } from './types'
const requestRetryLimit = 5 const requestRetryLimit = 5
@@ -18,29 +19,31 @@ export class FileUploader {
private retryCount = 0 private retryCount = 0
public uploadFile(sasJob: string, files: UploadFile[], params: any) { public uploadFile(sasJob: string, files: UploadFile[], params: any) {
if (files?.length < 1)
throw new Error('At least one file must be provided.')
let paramsString = ''
for (let param in params) {
if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}`
}
}
const program = this.appLoc
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const headers = {
'cache-control': 'no-cache'
}
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (files?.length < 1)
reject(new ErrorResponse('At least one file must be provided.'))
if (!sasJob || sasJob === '')
reject(new ErrorResponse('sasJob must be provided.'))
let paramsString = ''
for (let param in params) {
if (params.hasOwnProperty(param)) {
paramsString += `&${param}=${params[param]}`
}
}
const program = this.appLoc
? this.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
: sasJob
const uploadUrl = `${this.serverUrl}${this.jobsPath}/?${
'_program=' + program
}${paramsString}`
const headers = {
'cache-control': 'no-cache'
}
const formData = new FormData() const formData = new FormData()
for (let file of files) { for (let file of files) {
@@ -76,7 +79,7 @@ export class FileUploader {
}) })
.then((responseText) => { .then((responseText) => {
if (isLogInRequired(responseText)) if (isLogInRequired(responseText))
reject('You must be logged in to upload a file') reject(new ErrorResponse('You must be logged in to upload a file.'))
if (needsRetry(responseText)) { if (needsRetry(responseText)) {
if (this.retryCount < requestRetryLimit) { if (this.retryCount < requestRetryLimit) {
@@ -95,10 +98,18 @@ export class FileUploader {
try { try {
resolve(JSON.parse(responseText)) resolve(JSON.parse(responseText))
} catch (e) { } catch (e) {
reject(e) reject(
new ErrorResponse(
'Error while parsing json from upload response.',
e
)
)
} }
} }
}) })
.catch((err: any) => {
reject(new ErrorResponse('Upload request failed.', err))
})
}) })
} }
} }

View File

@@ -164,9 +164,9 @@ export class SASViyaApiClient {
for (const promise of promises) results.push(await promise()) for (const promise of promises) results.push(await promise())
results.forEach((result: any, index: number) => { results.forEach((result: any, index: number) => {
if (result && result.body && result.body.details) { if (result && result.error && result.error.details) {
try { try {
const resultParsed = JSON.parse(result.body.details) const resultParsed = result.error.details
if (resultParsed && resultParsed.body) { if (resultParsed && resultParsed.body) {
let sysUserId = '' let sysUserId = ''
@@ -426,10 +426,10 @@ export class SASViyaApiClient {
* @param linesOfCode - an array of code lines to execute. * @param linesOfCode - an array of code lines to execute.
* @param contextName - the context to execute the code in. * @param contextName - the context to execute the code in.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorized user.
* @param sessionId - optional session ID to reuse.
* @param data - execution data. * @param data - execution data.
* @param debug - when set to true, the log will be returned. * @param debug - when set to true, the log will be returned.
* @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code). * @param expectWebout - when set to true, the automatic _webout fileref will be checked for content, and that content returned. This fileref is used when the Job contains a SASjs web request (as opposed to executing arbitrary SAS code).
* @param waitForResult - when set to true, function will return the session
*/ */
public async executeScript( public async executeScript(
jobPath: string, jobPath: string,
@@ -437,6 +437,7 @@ export class SASViyaApiClient {
contextName: string, contextName: string,
accessToken?: string, accessToken?: string,
data = null, data = null,
debug: boolean = false,
expectWebout = false, expectWebout = false,
waitForResult = true waitForResult = true
): Promise<any> { ): Promise<any> {
@@ -467,7 +468,7 @@ export class SASViyaApiClient {
_OMITTEXTLOG: true _OMITTEXTLOG: true
} }
if (this.debug) { if (debug) {
jobArguments['_OMITTEXTLOG'] = false jobArguments['_OMITTEXTLOG'] = false
jobArguments['_OMITSESSIONRESULTS'] = false jobArguments['_OMITSESSIONRESULTS'] = false
jobArguments['_DEBUG'] = 131 jobArguments['_DEBUG'] = 131
@@ -535,7 +536,7 @@ export class SASViyaApiClient {
return session return session
} }
if (this.debug) { if (debug) {
console.log(`Job has been submitted for '${fileName}'.`) console.log(`Job has been submitted for '${fileName}'.`)
console.log( console.log(
`You can monitor the job progress at '${this.serverUrl}${ `You can monitor the job progress at '${this.serverUrl}${
@@ -558,7 +559,7 @@ export class SASViyaApiClient {
const logLink = currentJob.links.find((l) => l.rel === 'log') const logLink = currentJob.links.find((l) => l.rel === 'log')
if (this.debug && logLink) { if (debug && logLink) {
log = await this.request<any>( log = await this.request<any>(
`${this.serverUrl}${logLink.href}/content?limit=10000`, `${this.serverUrl}${logLink.href}/content?limit=10000`,
{ {
@@ -574,7 +575,7 @@ export class SASViyaApiClient {
} }
if (jobStatus === 'failed' || jobStatus === 'error') { if (jobStatus === 'failed' || jobStatus === 'error') {
return Promise.reject({ error: currentJob.error, log }) return Promise.reject({ job: currentJob, log })
} }
let resultLink let resultLink
@@ -606,12 +607,10 @@ export class SASViyaApiClient {
throw err throw err
}) })
return Promise.reject( return Promise.reject({
new ErrorResponse('Job execution failed', { status: 500,
status: 500, log: log
body: log })
})
)
} }
} }
return { return {
@@ -635,6 +634,7 @@ export class SASViyaApiClient {
contextName, contextName,
accessToken, accessToken,
data, data,
debug,
false, false,
true true
) )
@@ -955,6 +955,7 @@ export class SASViyaApiClient {
public async executeComputeJob( public async executeComputeJob(
sasJob: string, sasJob: string,
contextName: string, contextName: string,
debug?: boolean,
data?: any, data?: any,
accessToken?: string, accessToken?: string,
waitForResult = true, waitForResult = true,
@@ -1032,6 +1033,8 @@ export class SASViyaApiClient {
jobToExecute.code = code jobToExecute.code = code
} }
if (!code) code = ''
const linesToExecute = code.replace(/\r\n/g, '\n').split('\n') const linesToExecute = code.replace(/\r\n/g, '\n').split('\n')
return await this.executeScript( return await this.executeScript(
sasJob, sasJob,
@@ -1039,6 +1042,7 @@ export class SASViyaApiClient {
contextName, contextName,
accessToken, accessToken,
data, data,
debug,
expectWebout, expectWebout,
waitForResult waitForResult
) )
@@ -1112,7 +1116,7 @@ export class SASViyaApiClient {
} }
if (!jobToExecute) { if (!jobToExecute) {
throw new Error(`The job ${sasJob} was not found.`) throw new Error(`Job was not found.`)
} }
const jobDefinitionLink = jobToExecute?.links.find( const jobDefinitionLink = jobToExecute?.links.find(
(l) => l.rel === 'getResource' (l) => l.rel === 'getResource'

View File

@@ -411,6 +411,29 @@ export default class SASjs {
} }
} }
private async getLoginForm(response: any) {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
const matches = pattern.exec(response)
const formInputs: any = {}
if (matches && matches.length) {
this.setLoginUrl(matches)
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
if (inputs) {
inputs.forEach((inputStr: string) => {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
if (valueMatch && valueMatch.length) {
formInputs[valueMatch[1]] = valueMatch[2]
}
})
}
}
return Object.keys(formInputs).length ? formInputs : null
}
/** /**
* Checks whether a session is active, or login is required. * Checks whether a session is active, or login is required.
* @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`. * @returns - a promise which resolves with an object containing two values - a boolean `isLoggedIn`, and a string `userName`.
@@ -419,10 +442,16 @@ export default class SASjs {
const loginResponse = await fetch(this.loginUrl.replace('.do', '')) const loginResponse = await fetch(this.loginUrl.replace('.do', ''))
const responseText = await loginResponse.text() const responseText = await loginResponse.text()
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText) const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
let loginForm: any = null
if (!isLoggedIn) {
loginForm = await this.getLoginForm(responseText)
}
return Promise.resolve({ return Promise.resolve({
isLoggedIn, isLoggedIn,
userName: this.userName userName: this.userName,
loginForm
}) })
} }
@@ -440,7 +469,7 @@ export default class SASjs {
this.userName = loginParams.username this.userName = loginParams.username
const { isLoggedIn } = await this.checkSession() const { isLoggedIn, loginForm } = await this.checkSession()
if (isLoggedIn) { if (isLoggedIn) {
this.resendWaitingRequests() this.resendWaitingRequests()
@@ -450,15 +479,13 @@ export default class SASjs {
}) })
} }
const loginForm = await this.getLoginForm()
for (const key in loginForm) { for (const key in loginForm) {
loginParams[key] = loginForm[key] loginParams[key] = loginForm[key]
} }
const loginParamsStr = serialize(loginParams) const loginParamsStr = serialize(loginParams)
return fetch(this.loginUrl, { return fetch(this.loginUrl, {
method: 'post', method: 'POST',
credentials: 'include', credentials: 'include',
referrerPolicy: 'same-origin', referrerPolicy: 'same-origin',
body: loginParamsStr, body: loginParamsStr,
@@ -707,6 +734,7 @@ export default class SASjs {
return this.sasViyaApiClient?.executeComputeJob( return this.sasViyaApiClient?.executeComputeJob(
sasJob, sasJob,
config.contextName, config.contextName,
config.debug,
data, data,
accessToken, accessToken,
!!waitForResult, !!waitForResult,
@@ -739,6 +767,7 @@ export default class SASjs {
?.executeComputeJob( ?.executeComputeJob(
sasJob, sasJob,
config.contextName, config.contextName,
config.debug,
data, data,
accessToken, accessToken,
waitForResult, waitForResult,
@@ -780,11 +809,23 @@ export default class SASjs {
} else { } else {
this.retryCountComputeApi = 0 this.retryCountComputeApi = 0
reject( reject(
new ErrorResponse('Compute API retry requests limit reached') new ErrorResponse('Compute API retry requests limit reached.')
) )
} }
} }
if (response?.log) {
this.appendSasjsRequest(response.log, sasJob, null)
}
if (error.toString().includes('Job was not found')) {
reject(
new ErrorResponse('Service not found on the server.', {
sasJob: sasJob
})
)
}
if (error && error.status === 401) { if (error && error.status === 401) {
if (loginRequiredCallback) loginRequiredCallback(true) if (loginRequiredCallback) loginRequiredCallback(true)
sasjsWaitingRequest.requestPromise.resolve = resolve sasjsWaitingRequest.requestPromise.resolve = resolve
@@ -792,10 +833,8 @@ export default class SASjs {
sasjsWaitingRequest.config = config sasjsWaitingRequest.config = config
this.sasjsWaitingRequests.push(sasjsWaitingRequest) this.sasjsWaitingRequests.push(sasjsWaitingRequest)
} else { } else {
reject(new ErrorResponse('Job execution failed', error)) reject(new ErrorResponse('Job execution failed.', error))
} }
this.appendSasjsRequest(response.log, sasJob, null)
}) })
} }
) )
@@ -858,8 +897,8 @@ export default class SASjs {
return responseJson return responseJson
}) })
.catch(async (e) => { .catch(async (response) => {
if (needsRetry(JSON.stringify(e))) { if (needsRetry(JSON.stringify(response))) {
if (this.retryCountJeseApi < requestRetryLimit) { if (this.retryCountJeseApi < requestRetryLimit) {
let retryResponse = await this.executeJobViaJesApi( let retryResponse = await this.executeJobViaJesApi(
sasJob, sasJob,
@@ -875,12 +914,24 @@ export default class SASjs {
} else { } else {
this.retryCountJeseApi = 0 this.retryCountJeseApi = 0
reject( reject(
new ErrorResponse('Jes API retry requests limit reached') new ErrorResponse('Jes API retry requests limit reached.')
) )
} }
} }
reject(new ErrorResponse('Job execution failed', e)) if (response?.log) {
this.appendSasjsRequest(response.log, sasJob, null)
}
if (response.toString().includes('Job was not found')) {
reject(
new ErrorResponse('Service not found on the server.', {
sasJob: sasJob
})
)
}
reject(new ErrorResponse('Job execution failed.', response))
}) })
) )
} }
@@ -1064,7 +1115,7 @@ export default class SASjs {
} else { } else {
reject( reject(
new ErrorResponse( new ErrorResponse(
'Job WEB execution failed', 'Job WEB execution failed.',
this.parseSAS9ErrorResponse(responseText) this.parseSAS9ErrorResponse(responseText)
) )
) )
@@ -1082,7 +1133,7 @@ export default class SASjs {
} catch (e) { } catch (e) {
reject( reject(
new ErrorResponse( new ErrorResponse(
'Job WEB debug response parsing failed', 'Job WEB debug response parsing failed.',
{ response: resText, exception: e } { response: resText, exception: e }
) )
) )
@@ -1091,7 +1142,7 @@ export default class SASjs {
(err: any) => { (err: any) => {
reject( reject(
new ErrorResponse( new ErrorResponse(
'Job WEB debug response parsing failed', 'Job WEB debug response parsing failed.',
err err
) )
) )
@@ -1100,19 +1151,34 @@ export default class SASjs {
} catch (e) { } catch (e) {
reject( reject(
new ErrorResponse( new ErrorResponse(
'Job WEB debug response parsing failed', 'Job WEB debug response parsing failed.',
{ response: responseText, exception: e } { response: responseText, exception: e }
) )
) )
} }
} else { } else {
this.updateUsername(responseText) this.updateUsername(responseText)
if (
responseText.includes(
'The requested URL /SASStoredProcess/do/ was not found on this server.'
) ||
responseText.includes('Stored process not found')
) {
reject(
new ErrorResponse(
'Service not found on the server.',
{ service: sasJob },
responseText
)
)
}
try { try {
const parsedJson = JSON.parse(responseText) const parsedJson = JSON.parse(responseText)
resolve(parsedJson) resolve(parsedJson)
} catch (e) { } catch (e) {
reject( reject(
new ErrorResponse('Job WEB response parsing failed', { new ErrorResponse('Job WEB response parsing failed.', {
response: responseText, response: responseText,
exception: e exception: e
}) })
@@ -1123,7 +1189,7 @@ export default class SASjs {
} }
}) })
.catch((e: Error) => { .catch((e: Error) => {
reject(new ErrorResponse('Job WEB request failed', e)) reject(new ErrorResponse('Job WEB request failed.', e))
}) })
} }
) )
@@ -1264,10 +1330,20 @@ export default class SASjs {
} }
} }
private fetchLogFileContent(logLink: string) { /**
* Fetches content of the log file
* @param logLink - url of the log file.
* @param accessToken - an access token for an authorized user.
*/
public fetchLogFileContent(logLink: string, accessToken?: string) {
const headers: any = { 'Content-Type': 'application/json' }
if (accessToken) headers.Authorization = 'Bearer ' + accessToken
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
fetch(logLink, { fetch(logLink, {
method: 'GET' method: 'GET',
headers
}) })
.then((response: any) => response.text()) .then((response: any) => response.text())
.then((response: any) => resolve(response)) .then((response: any) => resolve(response))
@@ -1365,11 +1441,15 @@ export default class SASjs {
this.sasjsConfig.serverUrl === undefined || this.sasjsConfig.serverUrl === undefined ||
this.sasjsConfig.serverUrl === '' this.sasjsConfig.serverUrl === ''
) { ) {
let url = `${location.protocol}//${location.hostname}` if (typeof location !== 'undefined') {
if (location.port) { let url = `${location.protocol}//${location.hostname}`
url = `${url}:${location.port}`
if (location.port) url = `${url}:${location.port}`
this.sasjsConfig.serverUrl = url
} else {
this.sasjsConfig.serverUrl = ''
} }
this.sasjsConfig.serverUrl = url
} }
if (this.sasjsConfig.serverUrl.slice(-1) === '/') { if (this.sasjsConfig.serverUrl.slice(-1) === '/') {
@@ -1434,26 +1514,6 @@ export default class SASjs {
} }
} }
private async getLoginForm() {
const pattern: RegExp = /<form.+action="(.*Logon[^"]*).*>/
const response = await fetch(this.loginUrl).then((r) => r.text())
const matches = pattern.exec(response)
const formInputs: any = {}
if (matches && matches.length) {
this.setLoginUrl(matches)
const inputs = response.match(/<input.*"hidden"[^>]*>/g)
if (inputs) {
inputs.forEach((inputStr: string) => {
const valueMatch = inputStr.match(/name="([^"]*)"\svalue="([^"]*)/)
if (valueMatch && valueMatch.length) {
formInputs[valueMatch[1]] = valueMatch[2]
}
})
}
}
return Object.keys(formInputs).length ? formInputs : null
}
private async createFoldersAndServices( private async createFoldersAndServices(
parentFolder: string, parentFolder: string,
membersJson: any[], membersJson: any[],

View File

@@ -0,0 +1,137 @@
import { FileUploader } from '../FileUploader'
import { UploadFile } from '../types'
const sampleResponse = `{
"SYSUSERID": "cas",
"_DEBUG":" ",
"SYS_JES_JOB_URI": "/jobExecution/jobs/000-000-000-000",
"_PROGRAM" : "/Public/app/editors/loadfile",
"SYSCC" : "0",
"SYSJOBID" : "117382",
"SYSWARNINGTEXT" : ""
}`
const prepareFilesAndParams = () => {
const files: UploadFile[] = [
{
file: new File([''], 'testfile'),
fileName: 'testfile'
}
]
const params = { table: 'libtable' }
return { files, params }
}
describe('FileUploader', () => {
let originalFetch: any
const fileUploader = new FileUploader(
'/sample/apploc',
'https://sample.server.com',
'/jobs/path',
null,
null
)
beforeAll(() => {
originalFetch = (global as any).fetch
})
beforeEach(() => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve(sampleResponse)
})
)
})
afterAll(() => {
;(global as any).fetch = originalFetch
})
it('should upload successfully', async (done) => {
const sasJob = 'test/upload'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).then((res: any) => {
expect(JSON.stringify(res)).toEqual(
JSON.stringify(JSON.parse(sampleResponse))
)
done()
})
})
it('should an error when no files are provided', async (done) => {
const sasJob = 'test/upload'
const files: UploadFile[] = []
const params = { table: 'libtable' }
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('At least one file must be provided.')
done()
})
})
it('should throw an error when no sasJob is provided', async (done) => {
const sasJob = ''
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('sasJob must be provided.')
done()
})
})
it('should throw an error when login is required', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve('<form action="Logon">')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual(
'You must be logged in to upload a file.'
)
done()
})
})
it('should throw an error when invalid JSON is returned by the server', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.resolve('{invalid: "json"')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual(
'Error while parsing json from upload response.'
)
done()
})
})
it('should throw an error when the server request fails', async (done) => {
;(global as any).fetch = jest.fn().mockImplementation(() =>
Promise.resolve({
text: () => Promise.reject('{message: "Server error"}')
})
)
const sasJob = 'test'
const { files, params } = prepareFilesAndParams()
fileUploader.uploadFile(sasJob, files, params).catch((err: any) => {
expect(err.error.message).toEqual('Upload request failed.')
done()
})
})
})

View File

@@ -1,4 +1,4 @@
import { parseGeneratedCode } from './index' import { parseGeneratedCode } from '../../utils/index'
it('should parse generated code', async (done) => { it('should parse generated code', async (done) => {
expect(sampleResponse).toBeTruthy() expect(sampleResponse).toBeTruthy()

View File

@@ -1,4 +1,4 @@
import { parseSourceCode } from './index' import { parseSourceCode } from '../../utils/index'
it('should parse SAS9 source code', async (done) => { it('should parse SAS9 source code', async (done) => {
expect(sampleResponse).toBeTruthy() expect(sampleResponse).toBeTruthy()

View File

@@ -1,17 +1,19 @@
export class ErrorResponse { export class ErrorResponse {
body: ErrorBody error: ErrorBody
constructor(message: string, details?: any) { constructor(message: string, details?: any, raw?: any) {
let detailsString = '' let detailsString = details
let raw
try { if (typeof details !== 'object') {
detailsString = JSON.stringify(details) try {
} catch { detailsString = JSON.parse(details)
raw = details } catch {
raw = details
detailsString = ''
}
} }
this.body = { this.error = {
message, message,
details: detailsString, details: detailsString,
raw raw