mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 09:24:35 +00:00
Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
79bb27524c | ||
|
|
9651b7adb4 | ||
|
|
59e5bec731 | ||
|
|
182e66216f | ||
|
|
2408fd091e | ||
|
|
0e38a24664 | ||
|
|
aa643d1782 | ||
|
|
cdc91e9cda | ||
|
|
3f0590e0fe | ||
|
|
5efb294ff2 | ||
|
|
011e2d83dc | ||
|
|
e36b511530 | ||
|
|
b6a2a85d1d | ||
|
|
f1cceeb5e6 | ||
|
|
6fee2548fd | ||
|
|
91005066cf | ||
|
|
e1f17ef47d | ||
|
|
8a40071c35 | ||
|
|
430957eb3d | ||
|
|
25874be679 | ||
|
|
ed8440434f | ||
|
|
0f9884c1b6 | ||
|
|
d126a05347 | ||
|
|
3e26bbbbba | ||
|
|
982cc8f7a0 | ||
|
|
d1770698e0 | ||
|
|
b78e8617c4 | ||
|
|
3ce9ca0986 | ||
|
|
04d17c3680 | ||
|
|
d26e15f91c | ||
|
|
83c46091b3 | ||
|
|
d640d7c040 | ||
|
|
c934eb2b08 | ||
|
|
24dd5e32ad | ||
|
|
a23103b2c3 | ||
|
|
35aa4235e4 | ||
|
|
e9be1cf99a | ||
|
|
c7b0821081 | ||
|
|
4a4618dd32 | ||
|
|
d223e83c60 | ||
|
|
d1f1a20126 | ||
|
|
4b89e3762f | ||
|
|
bc110288de | ||
|
|
e94e16b52c | ||
|
|
76aacee016 | ||
|
|
1a3bd5d1f5 | ||
|
|
3f6e89d716 | ||
|
|
7279c23fe2 | ||
|
|
80707d77d9 | ||
|
|
3f796b300d |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,2 +1,2 @@
|
||||
node_modules
|
||||
build
|
||||
build
|
||||
@@ -14,4 +14,5 @@ What code changes have been made to achieve the intent.
|
||||
|
||||
- [ ] Code is formatted correctly (`npm run lint:fix`).
|
||||
- [ ] All unit tests are passing (`npm test`).
|
||||
- [ ] All `sasjs-cli` 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)).
|
||||
|
||||
2660
package-lock.json
generated
2660
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -2,7 +2,7 @@
|
||||
"name": "@sasjs/adapter",
|
||||
"description": "JavaScript adapter for SAS",
|
||||
"scripts": {
|
||||
"build": "rimraf build && webpack",
|
||||
"build": "rimraf build && rimraf node && mkdir node && cp -r src/* node && webpack && rimraf build/src && rimraf node",
|
||||
"package:lib": "npm run build && cp ./package.json build && cd build && npm version \"5.0.0\" && npm pack",
|
||||
"publish:lib": "npm run build && cd build && npm publish",
|
||||
"lint:fix": "npx prettier --write 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
||||
@@ -37,15 +37,15 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/isomorphic-fetch": "0.0.35",
|
||||
"@types/jest": "^26.0.14",
|
||||
"@types/jest": "^26.0.15",
|
||||
"cp": "^0.2.0",
|
||||
"jest": "^25.5.4",
|
||||
"path": "^0.12.7",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^17.1.2",
|
||||
"terser-webpack-plugin": "^4.2.2",
|
||||
"semantic-release": "^17.3.0",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"ts-jest": "^25.5.1",
|
||||
"ts-loader": "^8.0.4",
|
||||
"ts-loader": "^8.0.11",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.17.8",
|
||||
@@ -53,7 +53,7 @@
|
||||
"typedoc-plugin-external-module-name": "^4.0.3",
|
||||
"typescript": "^3.9.7",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.3.12"
|
||||
"webpack-cli": "^4.2.0"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
|
||||
6
sasjs-tests/package-lock.json
generated
6
sasjs-tests/package-lock.json
generated
@@ -1357,9 +1357,9 @@
|
||||
"integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw=="
|
||||
},
|
||||
"@sasjs/adapter": {
|
||||
"version": "1.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.12.0.tgz",
|
||||
"integrity": "sha512-0uGQH9ynomWzdBaEujEtcR38q6V7LCgG0mrb1Wellv6cC/IHD3j6WfeZZAgtiMPeOSJjbCDBOlVnzC2TlBqJFw==",
|
||||
"version": "1.18.3",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-1.18.3.tgz",
|
||||
"integrity": "sha512-wzDFJRyt2dXFeQP+JzqRGunYUbujrAbU/Jc4IWg5URsCkGAzwsNl/4G0xJVbqOTy1MvoZ431rfCnvhkUlg7D3Q==",
|
||||
"requires": {
|
||||
"es6-promise": "^4.2.8",
|
||||
"form-data": "^3.0.0",
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "^1.12.0",
|
||||
"@sasjs/adapter": "^1.18.2",
|
||||
"@sasjs/test-framework": "^1.4.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -3,12 +3,12 @@ import { TestSuite } from "@sasjs/test-framework";
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: window.location.origin,
|
||||
pathSAS9: "/SASStoredProcess/do",
|
||||
pathSASViya: "/SASJobExecution",
|
||||
appLoc: "/Public/seedapp",
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
serverType: ServerType.SASViya,
|
||||
debug: true,
|
||||
contextName: "SAS Job Execution compute context",
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: false
|
||||
};
|
||||
|
||||
@@ -37,6 +37,17 @@ export const basicTests = (
|
||||
assertion: (response: any) =>
|
||||
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",
|
||||
description:
|
||||
@@ -46,6 +57,7 @@ export const basicTests = (
|
||||
},
|
||||
assertion: (sasjsInstance: SASjs) => {
|
||||
const sasjsConfig = sasjsInstance.getSasjsConfig();
|
||||
|
||||
return (
|
||||
sasjsConfig.serverUrl === defaultConfig.serverUrl &&
|
||||
sasjsConfig.pathSAS9 === defaultConfig.pathSAS9 &&
|
||||
|
||||
@@ -88,7 +88,7 @@ export const sendArrTests = (adapter: SASjs): TestSuite => ({
|
||||
return adapter.request("common/sendArr", data).catch((e) => e);
|
||||
},
|
||||
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);
|
||||
},
|
||||
assertion: (error: any) => !!error && !!error.body && !!error.body.message
|
||||
assertion: (error: any) => !!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: "Single string value",
|
||||
@@ -219,7 +219,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
.catch((e) => e);
|
||||
},
|
||||
assertion: (error: any) => {
|
||||
return !!error && !!error.body && !!error.body.message;
|
||||
return !!error && !!error.error && !!error.error.message;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,22 +23,24 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
||||
},
|
||||
{
|
||||
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 () => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
adapter
|
||||
.request("common/makeErr", data)
|
||||
.request("common/makeErr", data, {debug: true})
|
||||
.then((res) => {
|
||||
//no action here, this request must throw error
|
||||
})
|
||||
.catch((err) => {
|
||||
let sasRequests = adapter.getSasRequests();
|
||||
let makeErrRequest =
|
||||
let makeErrRequest: any =
|
||||
sasRequests.find((req) =>
|
||||
req.serviceLink.includes("makeErr")
|
||||
) || null;
|
||||
|
||||
resolve(!!makeErrRequest);
|
||||
if (!makeErrRequest) resolve(false)
|
||||
|
||||
resolve(!!(makeErrRequest.logFile && makeErrRequest.logFile.length > 0));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { isLogInRequired, needsRetry, isUrl } from './utils'
|
||||
import { CsrfToken } from './types/CsrfToken'
|
||||
import { UploadFile } from './types/UploadFile'
|
||||
import { ErrorResponse } from './types'
|
||||
|
||||
const requestRetryLimit = 5
|
||||
|
||||
@@ -18,29 +19,31 @@ export class FileUploader {
|
||||
private retryCount = 0
|
||||
|
||||
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) => {
|
||||
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()
|
||||
|
||||
for (let file of files) {
|
||||
@@ -76,7 +79,7 @@ export class FileUploader {
|
||||
})
|
||||
.then((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 (this.retryCount < requestRetryLimit) {
|
||||
@@ -95,10 +98,18 @@ export class FileUploader {
|
||||
try {
|
||||
resolve(JSON.parse(responseText))
|
||||
} 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))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -164,9 +164,9 @@ export class SASViyaApiClient {
|
||||
for (const promise of promises) results.push(await promise())
|
||||
|
||||
results.forEach((result: any, index: number) => {
|
||||
if (result && result.body && result.body.details) {
|
||||
if (result && result.error && result.error.details) {
|
||||
try {
|
||||
const resultParsed = JSON.parse(result.body.details)
|
||||
const resultParsed = result.error.details
|
||||
|
||||
if (resultParsed && resultParsed.body) {
|
||||
let sysUserId = ''
|
||||
@@ -426,10 +426,10 @@ export class SASViyaApiClient {
|
||||
* @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 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).
|
||||
* @param waitForResult - when set to true, function will return the session
|
||||
*/
|
||||
public async executeScript(
|
||||
jobPath: string,
|
||||
@@ -437,6 +437,7 @@ export class SASViyaApiClient {
|
||||
contextName: string,
|
||||
accessToken?: string,
|
||||
data = null,
|
||||
debug: boolean = false,
|
||||
expectWebout = false,
|
||||
waitForResult = true
|
||||
): Promise<any> {
|
||||
@@ -467,7 +468,7 @@ export class SASViyaApiClient {
|
||||
_OMITTEXTLOG: true
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
if (debug) {
|
||||
jobArguments['_OMITTEXTLOG'] = false
|
||||
jobArguments['_OMITSESSIONRESULTS'] = false
|
||||
jobArguments['_DEBUG'] = 131
|
||||
@@ -535,7 +536,7 @@ export class SASViyaApiClient {
|
||||
return session
|
||||
}
|
||||
|
||||
if (this.debug) {
|
||||
if (debug) {
|
||||
console.log(`Job has been submitted for '${fileName}'.`)
|
||||
console.log(
|
||||
`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')
|
||||
|
||||
if (this.debug && logLink) {
|
||||
if (debug && logLink) {
|
||||
log = await this.request<any>(
|
||||
`${this.serverUrl}${logLink.href}/content?limit=10000`,
|
||||
{
|
||||
@@ -574,7 +575,7 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
if (jobStatus === 'failed' || jobStatus === 'error') {
|
||||
return Promise.reject({ error: currentJob.error, log })
|
||||
return Promise.reject({ job: currentJob, log })
|
||||
}
|
||||
|
||||
let resultLink
|
||||
@@ -606,12 +607,10 @@ export class SASViyaApiClient {
|
||||
throw err
|
||||
})
|
||||
|
||||
return Promise.reject(
|
||||
new ErrorResponse('Job execution failed', {
|
||||
status: 500,
|
||||
body: log
|
||||
})
|
||||
)
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
log: log
|
||||
})
|
||||
}
|
||||
}
|
||||
return {
|
||||
@@ -635,6 +634,7 @@ export class SASViyaApiClient {
|
||||
contextName,
|
||||
accessToken,
|
||||
data,
|
||||
debug,
|
||||
false,
|
||||
true
|
||||
)
|
||||
@@ -955,6 +955,7 @@ export class SASViyaApiClient {
|
||||
public async executeComputeJob(
|
||||
sasJob: string,
|
||||
contextName: string,
|
||||
debug?: boolean,
|
||||
data?: any,
|
||||
accessToken?: string,
|
||||
waitForResult = true,
|
||||
@@ -1041,6 +1042,7 @@ export class SASViyaApiClient {
|
||||
contextName,
|
||||
accessToken,
|
||||
data,
|
||||
debug,
|
||||
expectWebout,
|
||||
waitForResult
|
||||
)
|
||||
@@ -1114,7 +1116,7 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
if (!jobToExecute) {
|
||||
throw new Error(`The job ${sasJob} was not found.`)
|
||||
throw new Error(`Job was not found.`)
|
||||
}
|
||||
const jobDefinitionLink = jobToExecute?.links.find(
|
||||
(l) => l.rel === 'getResource'
|
||||
|
||||
124
src/SASjs.ts
124
src/SASjs.ts
@@ -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.
|
||||
* @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 responseText = await loginResponse.text()
|
||||
const isLoggedIn = /<button.+onClick.+logout/gm.test(responseText)
|
||||
let loginForm: any = null
|
||||
|
||||
if (!isLoggedIn) {
|
||||
loginForm = await this.getLoginForm(responseText)
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
isLoggedIn,
|
||||
userName: this.userName
|
||||
userName: this.userName,
|
||||
loginForm
|
||||
})
|
||||
}
|
||||
|
||||
@@ -440,7 +469,7 @@ export default class SASjs {
|
||||
|
||||
this.userName = loginParams.username
|
||||
|
||||
const { isLoggedIn } = await this.checkSession()
|
||||
const { isLoggedIn, loginForm } = await this.checkSession()
|
||||
if (isLoggedIn) {
|
||||
this.resendWaitingRequests()
|
||||
|
||||
@@ -450,15 +479,13 @@ export default class SASjs {
|
||||
})
|
||||
}
|
||||
|
||||
const loginForm = await this.getLoginForm()
|
||||
|
||||
for (const key in loginForm) {
|
||||
loginParams[key] = loginForm[key]
|
||||
}
|
||||
const loginParamsStr = serialize(loginParams)
|
||||
|
||||
return fetch(this.loginUrl, {
|
||||
method: 'post',
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
referrerPolicy: 'same-origin',
|
||||
body: loginParamsStr,
|
||||
@@ -707,6 +734,7 @@ export default class SASjs {
|
||||
return this.sasViyaApiClient?.executeComputeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken,
|
||||
!!waitForResult,
|
||||
@@ -739,6 +767,7 @@ export default class SASjs {
|
||||
?.executeComputeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken,
|
||||
waitForResult,
|
||||
@@ -780,11 +809,23 @@ export default class SASjs {
|
||||
} else {
|
||||
this.retryCountComputeApi = 0
|
||||
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 (loginRequiredCallback) loginRequiredCallback(true)
|
||||
sasjsWaitingRequest.requestPromise.resolve = resolve
|
||||
@@ -792,10 +833,8 @@ export default class SASjs {
|
||||
sasjsWaitingRequest.config = config
|
||||
this.sasjsWaitingRequests.push(sasjsWaitingRequest)
|
||||
} 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
|
||||
})
|
||||
.catch(async (e) => {
|
||||
if (needsRetry(JSON.stringify(e))) {
|
||||
.catch(async (response) => {
|
||||
if (needsRetry(JSON.stringify(response))) {
|
||||
if (this.retryCountJeseApi < requestRetryLimit) {
|
||||
let retryResponse = await this.executeJobViaJesApi(
|
||||
sasJob,
|
||||
@@ -875,12 +914,24 @@ export default class SASjs {
|
||||
} else {
|
||||
this.retryCountJeseApi = 0
|
||||
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 {
|
||||
reject(
|
||||
new ErrorResponse(
|
||||
'Job WEB execution failed',
|
||||
'Job WEB execution failed.',
|
||||
this.parseSAS9ErrorResponse(responseText)
|
||||
)
|
||||
)
|
||||
@@ -1082,7 +1133,7 @@ export default class SASjs {
|
||||
} catch (e) {
|
||||
reject(
|
||||
new ErrorResponse(
|
||||
'Job WEB debug response parsing failed',
|
||||
'Job WEB debug response parsing failed.',
|
||||
{ response: resText, exception: e }
|
||||
)
|
||||
)
|
||||
@@ -1091,7 +1142,7 @@ export default class SASjs {
|
||||
(err: any) => {
|
||||
reject(
|
||||
new ErrorResponse(
|
||||
'Job WEB debug response parsing failed',
|
||||
'Job WEB debug response parsing failed.',
|
||||
err
|
||||
)
|
||||
)
|
||||
@@ -1100,19 +1151,34 @@ export default class SASjs {
|
||||
} catch (e) {
|
||||
reject(
|
||||
new ErrorResponse(
|
||||
'Job WEB debug response parsing failed',
|
||||
'Job WEB debug response parsing failed.',
|
||||
{ response: responseText, exception: e }
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
const parsedJson = JSON.parse(responseText)
|
||||
resolve(parsedJson)
|
||||
} catch (e) {
|
||||
reject(
|
||||
new ErrorResponse('Job WEB response parsing failed', {
|
||||
new ErrorResponse('Job WEB response parsing failed.', {
|
||||
response: responseText,
|
||||
exception: e
|
||||
})
|
||||
@@ -1123,7 +1189,7 @@ export default class SASjs {
|
||||
}
|
||||
})
|
||||
.catch((e: Error) => {
|
||||
reject(new ErrorResponse('Job WEB request failed', e))
|
||||
reject(new ErrorResponse('Job WEB request failed.', e))
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -1448,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(
|
||||
parentFolder: string,
|
||||
membersJson: any[],
|
||||
|
||||
137
src/test/FileUploader.spec.ts
Normal file
137
src/test/FileUploader.spec.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseGeneratedCode } from './index'
|
||||
import { parseGeneratedCode } from '../../utils/index'
|
||||
|
||||
it('should parse generated code', async (done) => {
|
||||
expect(sampleResponse).toBeTruthy()
|
||||
@@ -1,4 +1,4 @@
|
||||
import { parseSourceCode } from './index'
|
||||
import { parseSourceCode } from '../../utils/index'
|
||||
|
||||
it('should parse SAS9 source code', async (done) => {
|
||||
expect(sampleResponse).toBeTruthy()
|
||||
@@ -1,17 +1,19 @@
|
||||
export class ErrorResponse {
|
||||
body: ErrorBody
|
||||
error: ErrorBody
|
||||
|
||||
constructor(message: string, details?: any) {
|
||||
let detailsString = ''
|
||||
let raw
|
||||
constructor(message: string, details?: any, raw?: any) {
|
||||
let detailsString = details
|
||||
|
||||
try {
|
||||
detailsString = JSON.stringify(details)
|
||||
} catch {
|
||||
raw = details
|
||||
if (typeof details !== 'object') {
|
||||
try {
|
||||
detailsString = JSON.parse(details)
|
||||
} catch {
|
||||
raw = details
|
||||
detailsString = ''
|
||||
}
|
||||
}
|
||||
|
||||
this.body = {
|
||||
this.error = {
|
||||
message,
|
||||
details: detailsString,
|
||||
raw
|
||||
|
||||
@@ -47,6 +47,7 @@ const browserConfig = {
|
||||
const nodeConfig = {
|
||||
...browserConfig,
|
||||
target: 'node',
|
||||
entry: './node/index.ts',
|
||||
output: {
|
||||
...browserConfig.output,
|
||||
path: path.resolve(__dirname, 'build', 'node')
|
||||
|
||||
Reference in New Issue
Block a user