mirror of
https://github.com/sasjs/adapter.git
synced 2025-12-11 01:14:36 +00:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1d31ec4dcc | ||
|
|
4407ed68ae | ||
|
|
b3359f2138 | ||
|
|
cff9104adf | ||
|
|
b73f821c19 | ||
|
|
dd3c1a7375 | ||
|
|
c10b5368af | ||
|
|
2df66765c0 | ||
|
|
ed65545571 | ||
|
|
b4ae486520 | ||
|
|
bb7b3d0b84 | ||
|
|
65b34aa015 | ||
|
|
b3c9303def | ||
|
|
19dfda9b6a | ||
|
|
ce2abb448f | ||
|
|
413c3b8098 | ||
|
|
269514a44f | ||
|
|
5fc334dd8b | ||
|
|
1b251f1cea | ||
|
|
70d3e25c7f | ||
|
|
5e9b33e346 | ||
|
|
eb52ec7532 | ||
|
|
f46c4bf3ca | ||
|
|
d8176912cf | ||
|
|
4c54ade2d3 | ||
|
|
52c41dfb3a | ||
|
|
36f0aa7411 | ||
|
|
a10e4ec264 | ||
|
|
851b6bc273 | ||
|
|
23f8d31b1b | ||
|
|
a5683bcd07 | ||
|
|
c7be71c781 | ||
|
|
594f274323 | ||
|
|
ba1ed5e732 | ||
|
|
613cc6b9ef | ||
|
|
8edb00f869 | ||
|
|
60a1f84604 | ||
|
|
36cfaee5db | ||
|
|
cb607c93ca | ||
|
|
03b5e1d824 | ||
|
|
2ea49a425f | ||
|
|
3c894c4147 | ||
|
|
23d151c919 | ||
|
|
6d1c4ff81a | ||
|
|
0eba6bdcf4 | ||
|
|
d7ecaf5932 | ||
|
|
e0d85f458b | ||
|
|
3a9cd46e6e | ||
|
|
301edab8ad | ||
|
|
aed39c2ec4 | ||
|
|
e31774ae9d | ||
|
|
00f09179a8 | ||
|
|
4196901e01 | ||
|
|
bf35dd072a | ||
|
|
75e3fd018d | ||
|
|
965dfff7c6 | ||
|
|
ff64dd22ad | ||
|
|
e7cceab065 | ||
|
|
f789b8f7a2 | ||
|
|
a2906abf71 | ||
|
|
1555afe771 | ||
|
|
a2832f1e1a | ||
|
|
56f34508fa |
2
.github/workflows/npmpublish.yml
vendored
2
.github/workflows/npmpublish.yml
vendored
@@ -25,3 +25,5 @@ jobs:
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
- name: Send Slack message
|
||||
run: curl -X POST --data-urlencode "payload={\"channel\":\"#sasjs\", \"username\":\"GitHub CI\", \"text\":\"New version of @sasjs/adapter has been released! \n Please deploy and run `dctests` with new adapter to make sure everything is still in place.\", \"icon_emoji\":\":rocket:\"}" ${{ secrets.SLACK_WEBHOOK }}
|
||||
|
||||
13
cypress.json
13
cypress.json
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"defaultCommandTimeout": 10000,
|
||||
"chromeWebSecurity": false,
|
||||
"screenshotOnRunFailure": false,
|
||||
"env": {
|
||||
"serverUrl": "",
|
||||
"appLoc": "/Public/app",
|
||||
"serverType": "SAS9",
|
||||
"debug": false,
|
||||
"username": "",
|
||||
"password": ""
|
||||
}
|
||||
}
|
||||
188
jest.config.js
188
jest.config.js
@@ -1,10 +1,184 @@
|
||||
// For a detailed explanation regarding each configuration property, visit:
|
||||
// https://jestjs.io/docs/en/configuration.html
|
||||
|
||||
module.exports = {
|
||||
roots: ["<rootDir>/src"],
|
||||
testMatch: [
|
||||
"**/__tests__/**/*.+(ts|tsx|js)",
|
||||
"**/?(*.)+(spec|test).+(ts|tsx|js)"
|
||||
],
|
||||
testTimeout: 90000,
|
||||
// All imported modules in your tests should be mocked automatically
|
||||
// automock: false,
|
||||
|
||||
// Stop running tests after `n` failures
|
||||
// bail: 1,
|
||||
|
||||
// Respect "browser" field in package.json when resolving modules
|
||||
// browser: false,
|
||||
|
||||
// The directory where Jest should store its cached dependency information
|
||||
// cacheDirectory: "/private/var/folders/7y/nmqg1srj29q6210rs9dfsdzc0000gn/T/jest_dx",
|
||||
|
||||
// Automatically clear mock calls and instances between every test
|
||||
clearMocks: true,
|
||||
|
||||
// Indicates whether the coverage information should be collected while executing the test
|
||||
// collectCoverage: false,
|
||||
|
||||
// An array of glob patterns indicating a set of files for which coverage information should be collected
|
||||
// collectCoverageFrom: undefined,
|
||||
|
||||
// The directory where Jest should output its coverage files
|
||||
coverageDirectory: 'coverage',
|
||||
|
||||
// An array of regexp pattern strings used to skip coverage collection
|
||||
// coveragePathIgnorePatterns: [
|
||||
// "/node_modules/"
|
||||
// ],
|
||||
|
||||
// A list of reporter names that Jest uses when writing coverage reports
|
||||
// coverageReporters: [
|
||||
// "json",
|
||||
// "text",
|
||||
// "lcov",
|
||||
// "clover"
|
||||
// ],
|
||||
|
||||
// An object that configures minimum threshold enforcement for coverage results
|
||||
// coverageThreshold: undefined,
|
||||
|
||||
// A path to a custom dependency extractor
|
||||
// dependencyExtractor: undefined,
|
||||
|
||||
// Make calling deprecated APIs throw helpful error messages
|
||||
// errorOnDeprecated: false,
|
||||
|
||||
// Force coverage collection from ignored files using an array of glob patterns
|
||||
// forceCoverageMatch: [],
|
||||
|
||||
// A path to a module which exports an async function that is triggered once before all test suites
|
||||
// globalSetup: undefined,
|
||||
|
||||
// A path to a module which exports an async function that is triggered once after all test suites
|
||||
// globalTeardown: undefined,
|
||||
|
||||
// A set of global variables that need to be available in all test environments
|
||||
// globals: {},
|
||||
|
||||
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
|
||||
// maxWorkers: "50%",
|
||||
|
||||
// An array of directory names to be searched recursively up from the requiring module's location
|
||||
// moduleDirectories: [
|
||||
// "node_modules"
|
||||
// ],
|
||||
|
||||
// An array of file extensions your modules use
|
||||
// moduleFileExtensions: [
|
||||
// "js",
|
||||
// "json",
|
||||
// "jsx",
|
||||
// "ts",
|
||||
// "tsx",
|
||||
// "node"
|
||||
// ],
|
||||
|
||||
// A map from regular expressions to module names that allow to stub out resources with a single module
|
||||
moduleNameMapper: {},
|
||||
|
||||
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
|
||||
// modulePathIgnorePatterns: [],
|
||||
|
||||
// Activates notifications for test results
|
||||
// notify: false,
|
||||
|
||||
// An enum that specifies notification mode. Requires { notify: true }
|
||||
// notifyMode: "failure-change",
|
||||
|
||||
// A preset that is used as a base for Jest's configuration
|
||||
preset: 'ts-jest/presets/js-with-ts',
|
||||
|
||||
// Run tests from one or more projects
|
||||
// projects: undefined,
|
||||
|
||||
// Use this configuration option to add custom reporters to Jest
|
||||
// reporters: undefined,
|
||||
|
||||
// Automatically reset mock state between every test
|
||||
// resetMocks: false,
|
||||
|
||||
// Reset the module registry before running each individual test
|
||||
// resetModules: false,
|
||||
|
||||
// A path to a custom resolver
|
||||
// resolver: undefined,
|
||||
|
||||
// Automatically restore mock state between every test
|
||||
// restoreMocks: false,
|
||||
|
||||
// The root directory that Jest should scan for tests and modules within
|
||||
// rootDir: undefined,
|
||||
|
||||
// A list of paths to directories that Jest should use to search for files in
|
||||
// roots: [
|
||||
// "<rootDir>"
|
||||
// ],
|
||||
|
||||
// Allows you to use a custom runner instead of Jest's default test runner
|
||||
// runner: "jest-runner",
|
||||
|
||||
// The paths to modules that run some code to configure or set up the testing environment before each test
|
||||
setupFiles: [],
|
||||
|
||||
// A list of paths to modules that run some code to configure or set up the testing framework before each test
|
||||
setupFilesAfterEnv: ['jest-extended'],
|
||||
|
||||
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
|
||||
// snapshotSerializers: [],
|
||||
|
||||
// Options that will be passed to the testEnvironment
|
||||
// testEnvironmentOptions: {},
|
||||
|
||||
// Adds a location field to test results
|
||||
// testLocationInResults: false,
|
||||
|
||||
// The glob patterns Jest uses to detect test files
|
||||
testMatch: ['**/*spec.[j|t]s?(x)'],
|
||||
|
||||
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
|
||||
testPathIgnorePatterns: ['/node_modules/', '/build'],
|
||||
|
||||
// The regexp pattern or array of patterns that Jest uses to detect test files
|
||||
// testRegex: [],
|
||||
|
||||
// This option allows the use of a custom results processor
|
||||
// testResultsProcessor: undefined,
|
||||
|
||||
// This option allows use of a custom test runner
|
||||
// testRunner: "jasmine2",
|
||||
|
||||
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
|
||||
// testURL: "http://localhost",
|
||||
|
||||
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
|
||||
// timers: "real",
|
||||
|
||||
// A map from regular expressions to paths to transformers
|
||||
transform: {
|
||||
"^.+\\.(ts|tsx)$": "ts-jest"
|
||||
'^.+\\.ts?$': 'ts-jest'
|
||||
}
|
||||
};
|
||||
|
||||
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
|
||||
// transformIgnorePatterns: [
|
||||
// '**/test/**/*.ts?(x)',
|
||||
// '**/?(*.)+(spec|test).ts?(x)'
|
||||
// ]
|
||||
|
||||
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
|
||||
// unmockedModulePathPatterns: undefined,
|
||||
|
||||
// Indicates whether each individual test should be reported during the run
|
||||
// verbose: undefined,
|
||||
|
||||
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
|
||||
// watchPathIgnorePatterns: [],
|
||||
|
||||
// Whether to use watchman for file crawling
|
||||
// watchman: true,
|
||||
}
|
||||
|
||||
3290
package-lock.json
generated
3290
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@@ -7,7 +7,7 @@
|
||||
"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}'",
|
||||
"lint": "npx prettier --check 'src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}'",
|
||||
"test": "jest --coverage",
|
||||
"test": "jest --silent --coverage",
|
||||
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
|
||||
"postpublish": "git clean -fd",
|
||||
"semantic-release": "semantic-release",
|
||||
@@ -36,32 +36,31 @@
|
||||
},
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"@types/isomorphic-fetch": "0.0.35",
|
||||
"@types/jest": "^26.0.20",
|
||||
"cp": "^0.2.0",
|
||||
"dotenv": "^8.2.0",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^26.6.3",
|
||||
"jest-extended": "^0.11.5",
|
||||
"path": "^0.12.7",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^17.3.1",
|
||||
"semantic-release": "^17.3.9",
|
||||
"terser-webpack-plugin": "^4.2.3",
|
||||
"ts-jest": "^25.5.1",
|
||||
"ts-loader": "^8.0.14",
|
||||
"ts-loader": "^8.0.17",
|
||||
"tslint": "^6.1.3",
|
||||
"tslint-config-prettier": "^1.18.0",
|
||||
"typedoc": "^0.19.2",
|
||||
"typedoc-neo-theme": "^1.0.10",
|
||||
"typedoc-neo-theme": "^1.1.0",
|
||||
"typedoc-plugin-external-module-name": "^4.0.6",
|
||||
"typescript": "^3.9.7",
|
||||
"webpack": "^5.13.0",
|
||||
"webpack-cli": "^4.3.1"
|
||||
"typescript": "^3.9.9",
|
||||
"webpack": "^5.21.2",
|
||||
"webpack-cli": "^4.5.0"
|
||||
},
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^2.0.2",
|
||||
"es6-promise": "^4.2.8",
|
||||
"axios": "^0.21.1",
|
||||
"@sasjs/utils": "^2.5.0",
|
||||
"form-data": "^3.0.0",
|
||||
"https": "^1.0.0",
|
||||
"isomorphic-fetch": "^2.2.1"
|
||||
"https": "^1.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
12348
sasjs-tests/package-lock.json
generated
12348
sasjs-tests/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -4,21 +4,18 @@
|
||||
"homepage": ".",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "^1.18.2",
|
||||
"@sasjs/adapter": "^2.1.0",
|
||||
"@sasjs/test-framework": "^1.4.0",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"@types/jest": "^26.0.3",
|
||||
"@types/node": "^14.0.14",
|
||||
"@types/react": "^16.9.41",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"@types/jest": "^26.0.20",
|
||||
"@types/node": "^14.14.25",
|
||||
"@types/react": "^17.0.1",
|
||||
"@types/react-dom": "^17.0.0",
|
||||
"@types/react-router-dom": "^5.1.7",
|
||||
"react": "^17.0.1",
|
||||
"react-dom": "^17.0.1",
|
||||
"react-router-dom": "^5.2.0",
|
||||
"react-scripts": "3.4.1",
|
||||
"typescript": "^3.9.6"
|
||||
"react-scripts": "^4.0.2",
|
||||
"typescript": "^4.1.3"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
@@ -45,6 +42,6 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"node-sass": "^4.14.1"
|
||||
"node-sass": "^5.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import SASjs, { ServerType, SASjsConfig } from "@sasjs/adapter";
|
||||
import SASjs, { SASjsConfig } from "@sasjs/adapter";
|
||||
import { TestSuite } from "@sasjs/test-framework";
|
||||
import { ServerType } from "@sasjs/utils/types";
|
||||
|
||||
const defaultConfig: SASjsConfig = {
|
||||
serverUrl: window.location.origin,
|
||||
pathSAS9: '/SASStoredProcess/do',
|
||||
pathSASViya: '/SASJobExecution',
|
||||
appLoc: '/Public/seedapp',
|
||||
serverType: ServerType.SASViya,
|
||||
pathSAS9: "/SASStoredProcess/do",
|
||||
pathSASViya: "/SASJobExecution",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SasViya,
|
||||
debug: false,
|
||||
contextName: 'SAS Job Execution compute context',
|
||||
useComputeApi: false
|
||||
contextName: "SAS Job Execution compute context",
|
||||
useComputeApi: false,
|
||||
allowInsecureRequests: false
|
||||
};
|
||||
|
||||
const customConfig = {
|
||||
@@ -17,7 +19,7 @@ const customConfig = {
|
||||
pathSAS9: "sas9",
|
||||
pathSASViya: "viya",
|
||||
appLoc: "/Public/seedapp",
|
||||
serverType: ServerType.SAS9,
|
||||
serverType: ServerType.Sas9,
|
||||
debug: false
|
||||
};
|
||||
|
||||
@@ -39,11 +41,12 @@ export const basicTests = (
|
||||
},
|
||||
{
|
||||
title: "Multiple Log in attempts",
|
||||
description: "Should fail on first attempt and should log the user in on second attempt",
|
||||
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)
|
||||
await adapter.logOut();
|
||||
await adapter.logIn("invalid", "invalid");
|
||||
return adapter.logIn(userName, password);
|
||||
},
|
||||
assertion: (response: any) =>
|
||||
response && response.isLoggedIn && response.userName === userName
|
||||
|
||||
@@ -12,7 +12,7 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
return adapter.startComputeJob("/Public/app/common/sendArr", data);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedProperties = ["id", "applicationName", "attributes"]
|
||||
const expectedProperties = ["id", "applicationName", "attributes"];
|
||||
return validate(expectedProperties, res);
|
||||
}
|
||||
},
|
||||
@@ -21,11 +21,22 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
description: "Should start a compute job and return the job",
|
||||
test: () => {
|
||||
const data: any = { table1: [{ col1: "first col value" }] };
|
||||
return adapter.startComputeJob("/Public/app/common/sendArr", data, {}, "", true);
|
||||
return adapter.startComputeJob(
|
||||
"/Public/app/common/sendArr",
|
||||
data,
|
||||
{},
|
||||
"",
|
||||
true
|
||||
);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedProperties = ["id", "state", "creationTimeStamp", "jobConditionCode"]
|
||||
return validate(expectedProperties, res.result);
|
||||
const expectedProperties = [
|
||||
"id",
|
||||
"state",
|
||||
"creationTimeStamp",
|
||||
"jobConditionCode"
|
||||
];
|
||||
return validate(expectedProperties, res.job);
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -38,19 +49,19 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
`output;`,
|
||||
`end;`,
|
||||
`run;`
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
return adapter.executeScriptSASViya(
|
||||
'sasCode.sas',
|
||||
"sasCode.sas",
|
||||
fileLines,
|
||||
'SAS Studio compute context',
|
||||
"SAS Studio compute context",
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`
|
||||
|
||||
const expectedLogContent = `1 data;\\n2 do x=1 to 100;\\n3 output;\\n4 end;\\n5 run;\\n\\n`;
|
||||
|
||||
return validateLog(expectedLogContent, res.log);
|
||||
}
|
||||
},
|
||||
@@ -58,21 +69,21 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
title: "Execute Script Viya - failed job",
|
||||
description: "Should execute sas file and return log",
|
||||
test: () => {
|
||||
const fileLines = [
|
||||
`%abort;`
|
||||
]
|
||||
|
||||
return adapter.executeScriptSASViya(
|
||||
'sasCode.sas',
|
||||
fileLines,
|
||||
'SAS Studio compute context',
|
||||
undefined,
|
||||
true
|
||||
).catch((err: any) => err )
|
||||
const fileLines = [`%abort;`];
|
||||
|
||||
return adapter
|
||||
.executeScriptSASViya(
|
||||
"sasCode.sas",
|
||||
fileLines,
|
||||
"SAS Studio compute context",
|
||||
undefined,
|
||||
true
|
||||
)
|
||||
.catch((err: any) => err);
|
||||
},
|
||||
assertion: (res: any) => {
|
||||
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`
|
||||
|
||||
const expectedLogContent = `1 %abort;\\nERROR: The %ABORT statement is not valid in open code.\\n`;
|
||||
|
||||
return validateLog(expectedLogContent, res.log);
|
||||
}
|
||||
}
|
||||
@@ -80,16 +91,16 @@ export const computeTests = (adapter: SASjs): TestSuite => ({
|
||||
});
|
||||
|
||||
const validateLog = (text: string, log: string): boolean => {
|
||||
const isValid = JSON.stringify(log).includes(text)
|
||||
const isValid = JSON.stringify(log).includes(text);
|
||||
|
||||
return isValid
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
const validate = (expectedProperties: string[], data: any): boolean => {
|
||||
const actualProperties = Object.keys(data);
|
||||
|
||||
const isValid = expectedProperties.every(
|
||||
(property) => actualProperties.includes(property)
|
||||
const isValid = expectedProperties.every((property) =>
|
||||
actualProperties.includes(property)
|
||||
);
|
||||
return isValid
|
||||
}
|
||||
return isValid;
|
||||
};
|
||||
|
||||
@@ -185,7 +185,8 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
|
||||
};
|
||||
return adapter.request("common/sendObj", invalidData).catch((e) => e);
|
||||
},
|
||||
assertion: (error: any) => !!error && !!error.error && !!error.error.message
|
||||
assertion: (error: any) =>
|
||||
!!error && !!error.error && !!error.error.message
|
||||
},
|
||||
{
|
||||
title: "Single string value",
|
||||
|
||||
@@ -23,26 +23,23 @@ export const sasjsRequestTests = (adapter: SASjs): TestSuite => ({
|
||||
},
|
||||
{
|
||||
title: "Make error and capture log",
|
||||
description: "Should make an error and capture log, in the same time it is testing if debug override is working",
|
||||
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, {debug: true})
|
||||
.then((res) => {
|
||||
//no action here, this request must throw error
|
||||
})
|
||||
.catch((err) => {
|
||||
let sasRequests = adapter.getSasRequests();
|
||||
let makeErrRequest: any =
|
||||
sasRequests.find((req) =>
|
||||
req.serviceLink.includes("makeErr")
|
||||
) || null;
|
||||
return adapter
|
||||
.request("common/makeErr", data, { debug: true })
|
||||
.catch(() => {
|
||||
const sasRequests = adapter.getSasRequests();
|
||||
const makeErrRequest: any =
|
||||
sasRequests.find((req) => req.serviceLink.includes("makeErr")) ||
|
||||
null;
|
||||
|
||||
if (!makeErrRequest) resolve(false)
|
||||
if (!makeErrRequest) return false;
|
||||
|
||||
resolve(!!(makeErrRequest.logFile && makeErrRequest.logFile.length > 0));
|
||||
});
|
||||
});
|
||||
return !!(
|
||||
makeErrRequest.logFile && makeErrRequest.logFile.length > 0
|
||||
);
|
||||
});
|
||||
},
|
||||
assertion: (response) => {
|
||||
return response;
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react"
|
||||
"jsx": "react-jsx",
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
|
||||
@@ -1,12 +1,7 @@
|
||||
import {
|
||||
Context,
|
||||
CsrfToken,
|
||||
EditContextInput,
|
||||
ContextAllAttributes
|
||||
} from './types'
|
||||
import { makeRequest, isUrl } from './utils'
|
||||
import { SASViyaApiClient } from './SASViyaApiClient'
|
||||
import { Context, EditContextInput, ContextAllAttributes } from './types'
|
||||
import { isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
export class ContextManager {
|
||||
private defaultComputeContexts = [
|
||||
@@ -29,8 +24,6 @@ export class ContextManager {
|
||||
'SAS Visual Forecasting launcher context'
|
||||
]
|
||||
|
||||
private csrfToken: CsrfToken | null = null
|
||||
|
||||
get getDefaultComputeContexts() {
|
||||
return this.defaultComputeContexts
|
||||
}
|
||||
@@ -38,28 +31,19 @@ export class ContextManager {
|
||||
return this.defaultLauncherContexts
|
||||
}
|
||||
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||
) {
|
||||
constructor(private serverUrl: string, private requestClient: RequestClient) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
public async getComputeContexts(accessToken?: string) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute contexts. ')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute contexts. ')
|
||||
})
|
||||
|
||||
const contextsList = contexts && contexts.items ? contexts.items : []
|
||||
|
||||
@@ -73,20 +57,14 @@ export class ContextManager {
|
||||
}
|
||||
|
||||
public async getLauncherContexts(accessToken?: string) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/launcher/contexts?limit=10000`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting launcher contexts. ')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/launcher/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting launcher contexts. ')
|
||||
})
|
||||
|
||||
const contextsList = contexts && contexts.items ? contexts.items : []
|
||||
|
||||
@@ -184,18 +162,15 @@ export class ContextManager {
|
||||
requestBody.environment = { autoExecLines }
|
||||
}
|
||||
|
||||
const createContextRequest: RequestInit = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(requestBody)
|
||||
}
|
||||
|
||||
const { result: context } = await this.request<Context>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
createContextRequest
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating compute context. ')
|
||||
})
|
||||
const { result: context } = await this.requestClient
|
||||
.post<Context>(
|
||||
`${this.serverUrl}/compute/contexts`,
|
||||
requestBody,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating compute context. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -238,18 +213,15 @@ export class ContextManager {
|
||||
launchType
|
||||
}
|
||||
|
||||
const createContextRequest: RequestInit = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(requestBody)
|
||||
}
|
||||
|
||||
const { result: context } = await this.request<Context>(
|
||||
`${this.serverUrl}/launcher/contexts`,
|
||||
createContextRequest
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating launcher context. ')
|
||||
})
|
||||
const { result: context } = await this.requestClient
|
||||
.post<Context>(
|
||||
`${this.serverUrl}/launcher/contexts`,
|
||||
requestBody,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while creating launcher context. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -268,14 +240,6 @@ export class ContextManager {
|
||||
true
|
||||
)
|
||||
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
let originalContext
|
||||
|
||||
originalContext = await this.getComputeContextByName(
|
||||
@@ -291,39 +255,33 @@ export class ContextManager {
|
||||
)
|
||||
}
|
||||
|
||||
const { result: context, etag } = await this.request<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
|
||||
{
|
||||
headers
|
||||
}
|
||||
).catch((err) => {
|
||||
if (err && err.status === 404) {
|
||||
throw new Error(
|
||||
`The context '${contextName}' was not found on this server.`
|
||||
)
|
||||
}
|
||||
const { result: context, etag } = await this.requestClient
|
||||
.get<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${originalContext.id}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err && err.status === 404) {
|
||||
throw new Error(
|
||||
`The context '${contextName}' was not found on this server.`
|
||||
)
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
throw err
|
||||
})
|
||||
|
||||
// An If-Match header with the value of the last ETag for the context
|
||||
// is required to be able to update it
|
||||
// https://developer.sas.com/apis/rest/Compute/#update-a-context-definition
|
||||
headers['If-Match'] = etag
|
||||
|
||||
const updateContextRequest: RequestInit = {
|
||||
method: 'PUT',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
return await this.requestClient.put<Context>(
|
||||
`/compute/contexts/${context.id}`,
|
||||
{
|
||||
...context,
|
||||
...editedContext,
|
||||
attributes: { ...context.attributes, ...editedContext.attributes }
|
||||
})
|
||||
}
|
||||
|
||||
return await this.request<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${context.id}`,
|
||||
updateContextRequest
|
||||
},
|
||||
accessToken,
|
||||
{ 'If-Match': etag }
|
||||
)
|
||||
}
|
||||
|
||||
@@ -331,20 +289,17 @@ export class ContextManager {
|
||||
contextName: string,
|
||||
accessToken?: string
|
||||
): Promise<Context> {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute context by name. ')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?filter=eq(name, "${contextName}")`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while getting compute context by name. '
|
||||
)
|
||||
})
|
||||
|
||||
if (!contexts || !(contexts.items && contexts.items.length)) {
|
||||
throw new Error(
|
||||
@@ -359,20 +314,16 @@ export class ContextManager {
|
||||
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 prefixMessage(err, 'Error while getting compute context by id. ')
|
||||
})
|
||||
const {
|
||||
result: context
|
||||
} = await this.requestClient
|
||||
.get<ContextAllAttributes>(
|
||||
`${this.serverUrl}/compute/contexts/${contextId}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while getting compute context by id. ')
|
||||
})
|
||||
|
||||
return context
|
||||
}
|
||||
@@ -381,20 +332,14 @@ export class ContextManager {
|
||||
executeScript: Function,
|
||||
accessToken?: string
|
||||
) {
|
||||
const headers: any = {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw prefixMessage(err, 'Error while fetching compute contexts.')
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw prefixMessage(err, 'Error while fetching compute contexts.')
|
||||
})
|
||||
|
||||
const contextsList = contexts.items || []
|
||||
const executableContexts: any[] = []
|
||||
@@ -471,14 +416,9 @@ export class ContextManager {
|
||||
|
||||
const context = await this.getComputeContextByName(contextName, accessToken)
|
||||
|
||||
const deleteContextRequest: RequestInit = {
|
||||
method: 'DELETE',
|
||||
headers
|
||||
}
|
||||
|
||||
return await this.request<Context>(
|
||||
return await this.requestClient.delete<Context>(
|
||||
`${this.serverUrl}/compute/contexts/${context.id}`,
|
||||
deleteContextRequest
|
||||
accessToken
|
||||
)
|
||||
}
|
||||
|
||||
@@ -486,34 +426,6 @@ export class ContextManager {
|
||||
|
||||
// TODO: implement deleteLauncherContext method
|
||||
|
||||
private async request<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
) {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
}
|
||||
}
|
||||
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(token) => {
|
||||
this.csrfToken = token
|
||||
this.setCsrfToken(token)
|
||||
},
|
||||
contentType
|
||||
).catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
'Error while making request in Context Manager. '
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
private validateContextName(name: string) {
|
||||
if (!name) throw new Error('Context name is required.')
|
||||
}
|
||||
|
||||
@@ -1,115 +1,70 @@
|
||||
import { isLogInRequired, needsRetry, isUrl } from './utils'
|
||||
import { CsrfToken } from './types/CsrfToken'
|
||||
import { isUrl } from './utils'
|
||||
import { UploadFile } from './types/UploadFile'
|
||||
import { ErrorResponse } from './types'
|
||||
|
||||
const requestRetryLimit = 5
|
||||
import { ErrorResponse, LoginRequiredError } from './types'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
export class FileUploader {
|
||||
constructor(
|
||||
private appLoc: string,
|
||||
private serverUrl: string,
|
||||
serverUrl: string,
|
||||
private jobsPath: string,
|
||||
private setCsrfTokenWeb: any,
|
||||
private csrfToken: CsrfToken | null = null
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
private retryCount = 0
|
||||
|
||||
public uploadFile(sasJob: string, files: UploadFile[], params: any) {
|
||||
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.'))
|
||||
if (files?.length < 1)
|
||||
return Promise.reject(
|
||||
new ErrorResponse('At least one file must be provided.')
|
||||
)
|
||||
if (!sasJob || sasJob === '')
|
||||
return Promise.reject(new ErrorResponse('sasJob must be provided.'))
|
||||
|
||||
let paramsString = ''
|
||||
let paramsString = ''
|
||||
|
||||
for (let param in params) {
|
||||
if (params.hasOwnProperty(param)) {
|
||||
paramsString += `&${param}=${params[param]}`
|
||||
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.jobsPath}/?${
|
||||
'_program=' + program
|
||||
}${paramsString}`
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (let file of files) {
|
||||
formData.append('file', file.file, file.fileName)
|
||||
}
|
||||
|
||||
const csrfToken = this.requestClient.getCsrfToken('file')
|
||||
if (csrfToken) formData.append('_csrf', csrfToken.value)
|
||||
|
||||
const headers = {
|
||||
'cache-control': 'no-cache',
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
|
||||
return this.requestClient
|
||||
.post(uploadUrl, formData, undefined, 'application/json', headers)
|
||||
.then((res) =>
|
||||
typeof res.result === 'string' ? JSON.parse(res.result) : res.result
|
||||
)
|
||||
.catch((err: Error) => {
|
||||
if (err instanceof LoginRequiredError) {
|
||||
return Promise.reject(
|
||||
new ErrorResponse('You must be logged in to upload a file.', err)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
formData.append('file', file.file, file.fileName)
|
||||
}
|
||||
|
||||
if (this.csrfToken) formData.append('_csrf', this.csrfToken.value)
|
||||
|
||||
fetch(uploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
referrerPolicy: 'same-origin',
|
||||
headers
|
||||
return Promise.reject(
|
||||
new ErrorResponse('File upload request failed.', err)
|
||||
)
|
||||
})
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
const tokenHeader = response.headers.get('X-CSRF-HEADER')
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers.get(tokenHeader)
|
||||
this.csrfToken = {
|
||||
headerName: tokenHeader,
|
||||
value: token || ''
|
||||
}
|
||||
|
||||
this.setCsrfTokenWeb(this.csrfToken)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return response.text()
|
||||
})
|
||||
.then((responseText) => {
|
||||
if (isLogInRequired(responseText))
|
||||
reject(new ErrorResponse('You must be logged in to upload a file.'))
|
||||
|
||||
if (needsRetry(responseText)) {
|
||||
if (this.retryCount < requestRetryLimit) {
|
||||
this.retryCount++
|
||||
this.uploadFile(sasJob, files, params).then(
|
||||
(res: any) => resolve(res),
|
||||
(err: any) => reject(err)
|
||||
)
|
||||
} else {
|
||||
this.retryCount = 0
|
||||
reject(responseText)
|
||||
}
|
||||
} else {
|
||||
this.retryCount = 0
|
||||
|
||||
try {
|
||||
resolve(JSON.parse(responseText))
|
||||
} catch (e) {
|
||||
reject(
|
||||
new ErrorResponse(
|
||||
'Error while parsing json from upload response.',
|
||||
e
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch((err: any) => {
|
||||
reject(new ErrorResponse('Upload request failed.', err))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { isUrl } from './utils'
|
||||
|
||||
/**
|
||||
@@ -5,8 +6,11 @@ import { isUrl } from './utils'
|
||||
*
|
||||
*/
|
||||
export class SAS9ApiClient {
|
||||
private httpClient: AxiosInstance
|
||||
|
||||
constructor(private serverUrl: string) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
this.httpClient = axios.create({ baseURL: this.serverUrl })
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,18 +42,18 @@ export class SAS9ApiClient {
|
||||
repositoryName: string
|
||||
) {
|
||||
const requestPayload = linesOfCode.join('\n')
|
||||
const executeScriptRequest = {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
body: `command=${requestPayload}`
|
||||
}
|
||||
const executeScriptResponse = await fetch(
|
||||
`${this.serverUrl}/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
||||
executeScriptRequest
|
||||
).then((res) => res.text())
|
||||
|
||||
return executeScriptResponse
|
||||
const executeScriptResponse = await this.httpClient.put(
|
||||
`/sas/servers/${serverName}/cmd?repositoryName=${repositoryName}`,
|
||||
`command=${requestPayload}`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/json'
|
||||
},
|
||||
responseType: 'text'
|
||||
}
|
||||
)
|
||||
|
||||
return executeScriptResponse.data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import {
|
||||
isAuthorizeFormRequired,
|
||||
parseAndSubmitAuthorizeForm,
|
||||
convertToCSV,
|
||||
makeRequest,
|
||||
isRelativePath,
|
||||
isUri,
|
||||
isUrl
|
||||
} from './utils'
|
||||
import { convertToCSV, isRelativePath, isUri, isUrl } from './utils'
|
||||
import * as NodeFormData from 'form-data'
|
||||
import {
|
||||
Job,
|
||||
@@ -14,16 +6,21 @@ import {
|
||||
Context,
|
||||
ContextAllAttributes,
|
||||
Folder,
|
||||
CsrfToken,
|
||||
EditContextInput,
|
||||
JobDefinition,
|
||||
PollOptions
|
||||
PollOptions,
|
||||
ComputeJobExecutionError,
|
||||
JobExecutionError
|
||||
} from './types'
|
||||
import { formatDataForRequest } from './utils/formatDataForRequest'
|
||||
import { SessionManager } from './SessionManager'
|
||||
import { ContextManager } from './ContextManager'
|
||||
import { timestampToYYYYMMDDHHMMSS } from '@sasjs/utils/time'
|
||||
import { Logger, LogLevel } from '@sasjs/utils/logger'
|
||||
import { isAuthorizeFormRequired } from './auth/isAuthorizeFormRequired'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
import { NotFoundError } from './types/NotFoundError'
|
||||
import { SasAuthResponse } from '@sasjs/utils/types'
|
||||
|
||||
/**
|
||||
* A client for interfacing with the SAS Viya REST API.
|
||||
@@ -34,20 +31,21 @@ export class SASViyaApiClient {
|
||||
private serverUrl: string,
|
||||
private rootFolderName: string,
|
||||
private contextName: string,
|
||||
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
private csrfToken: CsrfToken | null = null
|
||||
private fileUploadCsrfToken: CsrfToken | null = null
|
||||
private _debug = false
|
||||
private sessionManager = new SessionManager(
|
||||
this.serverUrl,
|
||||
this.contextName,
|
||||
this.setCsrfToken
|
||||
this.requestClient
|
||||
)
|
||||
private contextManager = new ContextManager(
|
||||
this.serverUrl,
|
||||
this.requestClient
|
||||
)
|
||||
private contextManager = new ContextManager(this.serverUrl, this.setCsrfToken)
|
||||
private folderMap = new Map<string, Job[]>()
|
||||
|
||||
public get debug() {
|
||||
@@ -146,10 +144,10 @@ export class SASViyaApiClient {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: contexts } = await this.request<{ items: Context[] }>(
|
||||
`${this.serverUrl}/compute/contexts?limit=10000`,
|
||||
{ headers }
|
||||
)
|
||||
const { result: contexts } = await this.requestClient.get<{
|
||||
items: Context[]
|
||||
}>(`/compute/contexts?limit=10000`, accessToken)
|
||||
|
||||
const executionContext =
|
||||
contexts.items && contexts.items.length
|
||||
? contexts.items.find((c: any) => c.name === contextName)
|
||||
@@ -165,9 +163,10 @@ export class SASViyaApiClient {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}
|
||||
const { result: createdSession } = await this.request<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${executionContext.id}/sessions`,
|
||||
createSessionRequest
|
||||
const { result: createdSession } = await this.requestClient.post<Session>(
|
||||
`/compute/contexts/${executionContext.id}/sessions`,
|
||||
{},
|
||||
accessToken
|
||||
)
|
||||
|
||||
return createdSession
|
||||
@@ -370,24 +369,22 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
// Execute job in session
|
||||
const postJobRequest = {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
name: fileName,
|
||||
description: 'Powered by SASjs',
|
||||
code: linesOfCode,
|
||||
variables: jobVariables,
|
||||
arguments: jobArguments
|
||||
})
|
||||
const jobRequestBody = {
|
||||
name: fileName,
|
||||
description: 'Powered by SASjs',
|
||||
code: linesOfCode,
|
||||
variables: jobVariables,
|
||||
arguments: jobArguments
|
||||
}
|
||||
|
||||
const { result: postedJob, etag } = await this.request<Job>(
|
||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs`,
|
||||
postJobRequest
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const { result: postedJob, etag } = await this.requestClient
|
||||
.post<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs`,
|
||||
jobRequestBody,
|
||||
accessToken
|
||||
)
|
||||
.catch((err: any) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
if (!waitForResult) {
|
||||
return session
|
||||
@@ -409,12 +406,14 @@ export class SASViyaApiClient {
|
||||
pollOptions
|
||||
)
|
||||
|
||||
const { result: currentJob } = await this.request<Job>(
|
||||
`${this.serverUrl}/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||
{ headers }
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const { result: currentJob } = await this.requestClient
|
||||
.get<Job>(
|
||||
`/compute/sessions/${executionSessionId}/jobs/${postedJob.id}`,
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
let jobResult
|
||||
let log
|
||||
@@ -422,12 +421,8 @@ export class SASViyaApiClient {
|
||||
const logLink = currentJob.links.find((l) => l.rel === 'log')
|
||||
|
||||
if (debug && logLink) {
|
||||
log = await this.request<any>(
|
||||
`${this.serverUrl}${logLink.href}/content?limit=10000`,
|
||||
{
|
||||
headers
|
||||
}
|
||||
)
|
||||
log = await this.requestClient
|
||||
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
|
||||
.then((res: any) =>
|
||||
res.result.items.map((i: any) => i.line).join('\n')
|
||||
)
|
||||
@@ -437,7 +432,7 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
if (jobStatus === 'failed' || jobStatus === 'error') {
|
||||
return Promise.reject({ job: currentJob, log })
|
||||
return Promise.reject(new ComputeJobExecutionError(currentJob, log))
|
||||
}
|
||||
|
||||
let resultLink
|
||||
@@ -449,36 +444,30 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
if (resultLink) {
|
||||
jobResult = await this.request<any>(
|
||||
`${this.serverUrl}${resultLink}`,
|
||||
{ headers },
|
||||
'text'
|
||||
).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')
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
jobResult = await this.requestClient
|
||||
.get<any>(resultLink, accessToken, 'text/plain')
|
||||
.catch(async (e) => {
|
||||
if (e instanceof NotFoundError) {
|
||||
if (logLink) {
|
||||
log = await this.requestClient
|
||||
.get<any>(`${logLink.href}/content?limit=10000`, accessToken)
|
||||
.then((res: any) =>
|
||||
res.result.items.map((i: any) => i.line).join('\n')
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
log: log
|
||||
})
|
||||
return Promise.reject({
|
||||
status: 500,
|
||||
log
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: JSON.stringify(e)
|
||||
}
|
||||
})
|
||||
return {
|
||||
result: JSON.stringify(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
await this.sessionManager
|
||||
@@ -506,6 +495,17 @@ export class SASViyaApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a folder. Path to the folder is required.
|
||||
* @param folderPath - the absolute path to the folder.
|
||||
* @param accessToken - an access token for authorizing the request.
|
||||
*/
|
||||
public async getFolder(folderPath: string, accessToken?: string) {
|
||||
return await this.requestClient
|
||||
.get(`/folders/folders/@item?path=${folderPath}`, accessToken)
|
||||
.then((res) => res.result)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a folder. Path to or URI of the parent folder is required.
|
||||
* @param folderName - the name of the new folder.
|
||||
@@ -568,22 +568,15 @@ export class SASViyaApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
const createFolderRequest: RequestInit = {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
const {
|
||||
result: createFolderResponse
|
||||
} = await this.requestClient.post<Folder>(
|
||||
`/folders/folders?parentFolderUri=${parentFolderUri}`,
|
||||
{
|
||||
name: folderName,
|
||||
type: 'folder'
|
||||
})
|
||||
}
|
||||
|
||||
createFolderRequest.headers = { 'Content-Type': 'application/json' }
|
||||
if (accessToken) {
|
||||
createFolderRequest.headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
const { result: createFolderResponse } = await this.request<Folder>(
|
||||
`${this.serverUrl}/folders/folders?parentFolderUri=${parentFolderUri}`,
|
||||
createFolderRequest
|
||||
},
|
||||
accessToken
|
||||
)
|
||||
|
||||
// update folder map with newly created folder.
|
||||
@@ -617,13 +610,9 @@ export class SASViyaApiClient {
|
||||
parentFolderUri = await this.getFolderUri(parentFolderPath, accessToken)
|
||||
}
|
||||
|
||||
const createJobDefinitionRequest: RequestInit = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/vnd.sas.job.definition+json',
|
||||
Accept: 'application/vnd.sas.job.definition+json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
return await this.requestClient.post<Job>(
|
||||
`${this.serverUrl}/jobDefinitions/definitions?parentFolderUri=${parentFolderUri}`,
|
||||
{
|
||||
name: jobName,
|
||||
parameters: [
|
||||
{
|
||||
@@ -634,19 +623,8 @@ export class SASViyaApiClient {
|
||||
],
|
||||
type: 'Compute',
|
||||
code
|
||||
})
|
||||
}
|
||||
|
||||
if (accessToken) {
|
||||
createJobDefinitionRequest!.headers = {
|
||||
...createJobDefinitionRequest.headers,
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
|
||||
return await this.request<Job>(
|
||||
`${this.serverUrl}/jobDefinitions/definitions?parentFolderUri=${parentFolderUri}`,
|
||||
createJobDefinitionRequest
|
||||
},
|
||||
accessToken
|
||||
)
|
||||
}
|
||||
|
||||
@@ -657,18 +635,13 @@ export class SASViyaApiClient {
|
||||
public async getAuthCode(clientId: string) {
|
||||
const authUrl = `${this.serverUrl}/SASLogon/oauth/authorize?client_id=${clientId}&response_type=code`
|
||||
|
||||
const authCode = await fetch(authUrl, {
|
||||
referrerPolicy: 'same-origin',
|
||||
credentials: 'include'
|
||||
})
|
||||
.then((response) => response.text())
|
||||
const authCode = await this.requestClient
|
||||
.get<string>(authUrl, undefined, 'text/plain')
|
||||
.then((response) => response.result)
|
||||
.then(async (response) => {
|
||||
let code = ''
|
||||
if (isAuthorizeFormRequired(response)) {
|
||||
const formResponse: any = await parseAndSubmitAuthorizeForm(
|
||||
response,
|
||||
this.serverUrl
|
||||
)
|
||||
const formResponse: any = await this.requestClient.authorize(response)
|
||||
|
||||
const responseBody = formResponse
|
||||
.split('<body>')[1]
|
||||
@@ -701,14 +674,12 @@ export class SASViyaApiClient {
|
||||
* @param clientId - the client ID to authenticate with.
|
||||
* @param clientSecret - the client secret to authenticate with.
|
||||
* @param authCode - the auth code received from the server.
|
||||
* @param insecure - this boolean tells adapter to ignore SSL errors. [Not Recommended]
|
||||
*/
|
||||
public async getAccessToken(
|
||||
clientId: string,
|
||||
clientSecret: string,
|
||||
authCode: string,
|
||||
insecure: boolean = false
|
||||
) {
|
||||
authCode: string
|
||||
): Promise<SasAuthResponse> {
|
||||
const url = this.serverUrl + '/SASLogon/oauth/token'
|
||||
let token
|
||||
if (typeof Buffer === 'undefined') {
|
||||
@@ -731,24 +702,15 @@ export class SASViyaApiClient {
|
||||
formData.append('code', authCode)
|
||||
}
|
||||
|
||||
let moreOptions = {}
|
||||
if (insecure) {
|
||||
const https = require('https')
|
||||
moreOptions = {
|
||||
agent: new https.Agent({
|
||||
rejectUnauthorized: false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const authResponse = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
body: formData as any,
|
||||
referrerPolicy: 'same-origin',
|
||||
...moreOptions
|
||||
}).then((res) => res.json())
|
||||
const authResponse = await this.requestClient
|
||||
.post(
|
||||
url,
|
||||
formData,
|
||||
undefined,
|
||||
'multipart/form-data; boundary=' + (formData as any)._boundary,
|
||||
headers
|
||||
)
|
||||
.then((res) => res.result as SasAuthResponse)
|
||||
|
||||
return authResponse
|
||||
}
|
||||
@@ -786,13 +748,15 @@ export class SASViyaApiClient {
|
||||
formData.append('refresh_token', refreshToken)
|
||||
}
|
||||
|
||||
const authResponse = await fetch(url, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
headers,
|
||||
body: formData as any,
|
||||
referrerPolicy: 'same-origin'
|
||||
}).then((res) => res.json())
|
||||
const authResponse = await this.requestClient
|
||||
.post<SasAuthResponse>(
|
||||
url,
|
||||
formData,
|
||||
undefined,
|
||||
'multipart/form-data; boundary=' + (formData as any)._boundary,
|
||||
headers
|
||||
)
|
||||
.then((res) => res.result)
|
||||
|
||||
return authResponse
|
||||
}
|
||||
@@ -808,13 +772,10 @@ export class SASViyaApiClient {
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
const deleteResponse = await this.request(url, {
|
||||
method: 'DELETE',
|
||||
credentials: 'include',
|
||||
headers
|
||||
})
|
||||
|
||||
return deleteResponse
|
||||
const deleteResponse = await this.requestClient.delete(url, accessToken)
|
||||
|
||||
return deleteResponse.result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -884,9 +845,11 @@ export class SASViyaApiClient {
|
||||
throw new Error(`URI of job definition was not found.`)
|
||||
}
|
||||
|
||||
const { result: jobDefinition } = await this.request<JobDefinition>(
|
||||
const {
|
||||
result: jobDefinition
|
||||
} = await this.requestClient.get<JobDefinition>(
|
||||
`${this.serverUrl}${jobDefinitionLink.href}`,
|
||||
{ headers }
|
||||
accessToken
|
||||
)
|
||||
|
||||
code = jobDefinition.code
|
||||
@@ -961,20 +924,10 @@ export class SASViyaApiClient {
|
||||
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>(
|
||||
const { result: jobDefinition } = await this.requestClient.get<Job>(
|
||||
`${this.serverUrl}${jobDefinitionLink}`,
|
||||
requestInfo
|
||||
accessToken
|
||||
)
|
||||
|
||||
const jobArguments: { [key: string]: any } = {
|
||||
@@ -1001,47 +954,46 @@ export class SASViyaApiClient {
|
||||
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 postJobRequestBody = {
|
||||
name: `exec-${jobName}`,
|
||||
description: 'Powered by SASjs',
|
||||
jobDefinition,
|
||||
arguments: jobArguments
|
||||
}
|
||||
const { result: postedJob, etag } = await this.request<Job>(
|
||||
const { result: postedJob, etag } = await this.requestClient.post<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs?_action=wait`,
|
||||
postJobRequest
|
||||
postJobRequestBody,
|
||||
accessToken
|
||||
)
|
||||
const jobStatus = await this.pollJobState(postedJob, etag, accessToken)
|
||||
const { result: currentJob } = await this.request<Job>(
|
||||
const { result: currentJob } = await this.requestClient.get<Job>(
|
||||
`${this.serverUrl}/jobExecution/jobs/${postedJob.id}`,
|
||||
{ headers }
|
||||
accessToken
|
||||
)
|
||||
|
||||
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>(
|
||||
jobResult = await this.requestClient.get<any>(
|
||||
`${this.serverUrl}${resultLink}/content`,
|
||||
{ headers },
|
||||
'text'
|
||||
accessToken,
|
||||
'text/plain'
|
||||
)
|
||||
}
|
||||
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'))
|
||||
log = await this.requestClient
|
||||
.get<any>(`${this.serverUrl}${logLink.href}/content`, accessToken)
|
||||
.then((res: any) => res.result.items.map((i: any) => i.line).join('\n'))
|
||||
}
|
||||
if (jobStatus === 'failed') {
|
||||
throw new JobExecutionError(
|
||||
currentJob.error?.errorCode,
|
||||
currentJob.error?.message,
|
||||
log
|
||||
)
|
||||
}
|
||||
return { result: jobResult?.result, log }
|
||||
}
|
||||
@@ -1055,22 +1007,16 @@ export class SASViyaApiClient {
|
||||
}
|
||||
|
||||
const url = '/folders/folders/@item?path=' + path
|
||||
const requestInfo: any = {
|
||||
method: 'GET'
|
||||
}
|
||||
if (accessToken) {
|
||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` }
|
||||
}
|
||||
const { result: folder } = await this.request<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
requestInfo
|
||||
const { result: folder } = await this.requestClient.get<Folder>(
|
||||
`${url}`,
|
||||
accessToken
|
||||
)
|
||||
if (!folder) {
|
||||
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?limit=${folder.memberCount}`,
|
||||
requestInfo
|
||||
const { result: members } = await this.requestClient.get<{ items: any[] }>(
|
||||
`/folders/folders/${folder.id}/members?limit=${folder.memberCount}`,
|
||||
accessToken
|
||||
)
|
||||
|
||||
const itemsAtRoot = members.items
|
||||
@@ -1084,7 +1030,7 @@ export class SASViyaApiClient {
|
||||
accessToken?: string,
|
||||
pollOptions?: PollOptions
|
||||
) {
|
||||
let POLL_INTERVAL = 100
|
||||
let POLL_INTERVAL = 300
|
||||
let MAX_POLL_COUNT = 1000
|
||||
|
||||
if (pollOptions) {
|
||||
@@ -1106,12 +1052,10 @@ export class SASViyaApiClient {
|
||||
Promise.reject(`Job state link was not found.`)
|
||||
}
|
||||
|
||||
const { result: state } = await this.request<string>(
|
||||
const { result: state } = await this.requestClient.get<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||
{
|
||||
headers
|
||||
},
|
||||
'text'
|
||||
accessToken,
|
||||
'text/plain'
|
||||
)
|
||||
|
||||
const currentState = state.trim()
|
||||
@@ -1129,12 +1073,10 @@ export class SASViyaApiClient {
|
||||
postedJobState === 'pending'
|
||||
) {
|
||||
if (stateLink) {
|
||||
const { result: jobState } = await this.request<string>(
|
||||
const { result: jobState } = await this.requestClient.get<string>(
|
||||
`${this.serverUrl}${stateLink.href}?_action=wait&wait=30`,
|
||||
{
|
||||
headers
|
||||
},
|
||||
'text'
|
||||
accessToken,
|
||||
'text/plain'
|
||||
)
|
||||
|
||||
postedJobState = jobState.trim()
|
||||
@@ -1177,17 +1119,10 @@ export class SASViyaApiClient {
|
||||
)
|
||||
}
|
||||
|
||||
const createFileRequest = {
|
||||
method: 'POST',
|
||||
body: csv,
|
||||
headers
|
||||
}
|
||||
|
||||
const uploadResponse = await this.request<any>(
|
||||
const uploadResponse = await this.requestClient.uploadFile(
|
||||
`${this.serverUrl}/files/files#rawUpload`,
|
||||
createFileRequest,
|
||||
'json',
|
||||
'fileUpload'
|
||||
csv,
|
||||
accessToken
|
||||
)
|
||||
|
||||
uploadedFiles.push({ tableName, file: uploadResponse.result })
|
||||
@@ -1197,18 +1132,11 @@ export class SASViyaApiClient {
|
||||
|
||||
private async getFolderUri(folderPath: string, accessToken?: string) {
|
||||
const url = '/folders/folders/@item?path=' + folderPath
|
||||
const requestInfo: any = {
|
||||
method: 'GET'
|
||||
}
|
||||
if (accessToken) {
|
||||
requestInfo.headers = { Authorization: `Bearer ${accessToken}` }
|
||||
}
|
||||
const { result: folder } = await this.request<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
requestInfo
|
||||
).catch((err) => {
|
||||
return { result: null }
|
||||
})
|
||||
const { result: folder } = await this.requestClient
|
||||
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
|
||||
.catch(() => {
|
||||
return { result: null }
|
||||
})
|
||||
|
||||
if (!folder) return undefined
|
||||
return `/folders/folders/${folder.id}`
|
||||
@@ -1216,20 +1144,12 @@ export class SASViyaApiClient {
|
||||
|
||||
private async getRecycleBinUri(accessToken: string) {
|
||||
const url = '/folders/folders/@myRecycleBin'
|
||||
const requestInfo = {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + accessToken
|
||||
}
|
||||
}
|
||||
|
||||
const { result: folder } = await this.request<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
requestInfo
|
||||
).catch((err) => {
|
||||
return { result: null }
|
||||
})
|
||||
const { result: folder } = await this.requestClient
|
||||
.get<Folder>(`${this.serverUrl}${url}`, accessToken)
|
||||
.catch(() => {
|
||||
return { result: null }
|
||||
})
|
||||
|
||||
if (!folder) return undefined
|
||||
|
||||
@@ -1293,35 +1213,31 @@ export class SASViyaApiClient {
|
||||
const sourceFolderId = sourceFolderUri?.split('/').pop()
|
||||
const url = sourceFolderUri
|
||||
|
||||
const requestInfo = {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: 'Bearer ' + accessToken
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: sourceFolderId,
|
||||
name: targetFolderName,
|
||||
parentFolderUri: targetParentFolderUri
|
||||
})
|
||||
}
|
||||
const { result: folder } = await this.requestClient
|
||||
.patch<Folder>(
|
||||
`${this.serverUrl}${url}`,
|
||||
{
|
||||
id: sourceFolderId,
|
||||
name: targetFolderName,
|
||||
parentFolderUri: targetParentFolderUri
|
||||
},
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err.code && err.code === 'ENOTFOUND') {
|
||||
const notFoundError = {
|
||||
body: {
|
||||
message: `Folder '${sourceFolder
|
||||
.split('/')
|
||||
.pop()}' was not found.`
|
||||
}
|
||||
}
|
||||
|
||||
const { result: folder } = await this.request<Folder>(
|
||||
`${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 notFoundError
|
||||
}
|
||||
|
||||
throw err
|
||||
})
|
||||
throw err
|
||||
})
|
||||
|
||||
if (!folder) return undefined
|
||||
|
||||
@@ -1349,42 +1265,4 @@ export class SASViyaApiClient {
|
||||
|
||||
return movedFolder
|
||||
}
|
||||
|
||||
setCsrfTokenLocal = (csrfToken: CsrfToken) => {
|
||||
this.csrfToken = csrfToken
|
||||
this.setCsrfToken(csrfToken)
|
||||
}
|
||||
|
||||
setFileUploadCsrfToken = (csrfToken: CsrfToken) => {
|
||||
this.fileUploadCsrfToken = csrfToken
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json',
|
||||
type: 'fileUpload' | 'other' = 'other'
|
||||
) {
|
||||
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, callback, contentType)
|
||||
}
|
||||
}
|
||||
|
||||
1052
src/SASjs.ts
1052
src/SASjs.ts
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import { Session, Context, CsrfToken, SessionVariable } from './types'
|
||||
import { asyncForEach, makeRequest, isUrl } from './utils'
|
||||
import { asyncForEach, isUrl } from './utils'
|
||||
import { prefixMessage } from '@sasjs/utils/error'
|
||||
import { RequestClient } from './request/RequestClient'
|
||||
|
||||
const MAX_SESSION_COUNT = 1
|
||||
const RETRY_LIMIT: number = 3
|
||||
@@ -14,14 +15,13 @@ export class SessionManager {
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private contextName: string,
|
||||
private setCsrfToken: (csrfToken: CsrfToken) => void
|
||||
private requestClient: RequestClient
|
||||
) {
|
||||
if (serverUrl) isUrl(serverUrl)
|
||||
}
|
||||
|
||||
private sessions: Session[] = []
|
||||
private currentContext: Context | null = null
|
||||
private csrfToken: CsrfToken | null = null
|
||||
private _debug: boolean = false
|
||||
private printedSessionState = {
|
||||
printed: false,
|
||||
@@ -58,15 +58,8 @@ export class SessionManager {
|
||||
}
|
||||
|
||||
async clearSession(id: string, accessToken?: string) {
|
||||
const deleteSessionRequest = {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
|
||||
return await this.request<Session>(
|
||||
`${this.serverUrl}/compute/sessions/${id}`,
|
||||
deleteSessionRequest
|
||||
)
|
||||
return await this.requestClient
|
||||
.delete<Session>(`/compute/sessions/${id}`, accessToken)
|
||||
.then(() => {
|
||||
this.sessions = this.sessions.filter((s) => s.id !== id)
|
||||
})
|
||||
@@ -98,17 +91,20 @@ export class SessionManager {
|
||||
}
|
||||
|
||||
private async createAndWaitForSession(accessToken?: string) {
|
||||
const createSessionRequest = {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
|
||||
const { result: createdSession, etag } = await this.request<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${this.currentContext!.id}/sessions`,
|
||||
createSessionRequest
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const {
|
||||
result: createdSession,
|
||||
etag
|
||||
} = await this.requestClient
|
||||
.post<Session>(
|
||||
`${this.serverUrl}/compute/contexts/${
|
||||
this.currentContext!.id
|
||||
}/sessions`,
|
||||
{},
|
||||
accessToken
|
||||
)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
await this.waitForSession(createdSession, etag, accessToken)
|
||||
|
||||
@@ -119,13 +115,13 @@ export class SessionManager {
|
||||
|
||||
private async setCurrentContext(accessToken?: string) {
|
||||
if (!this.currentContext) {
|
||||
const { result: contexts } = await this.request<{
|
||||
items: Context[]
|
||||
}>(`${this.serverUrl}/compute/contexts?limit=10000`, {
|
||||
headers: this.getHeaders(accessToken)
|
||||
}).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
const { result: contexts } = await this.requestClient
|
||||
.get<{
|
||||
items: Context[]
|
||||
}>(`${this.serverUrl}/compute/contexts?limit=10000`, accessToken)
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
const contextsList =
|
||||
contexts && contexts.items && contexts.items.length
|
||||
@@ -166,10 +162,7 @@ export class SessionManager {
|
||||
accessToken?: string
|
||||
) {
|
||||
let sessionState = session.state
|
||||
const headers: any = {
|
||||
...this.getHeaders(accessToken),
|
||||
'If-None-Match': etag
|
||||
}
|
||||
|
||||
const stateLink = session.links.find((l: any) => l.rel === 'state')
|
||||
|
||||
return new Promise(async (resolve, _) => {
|
||||
@@ -185,12 +178,10 @@ export class SessionManager {
|
||||
this.printedSessionState.printed = true
|
||||
}
|
||||
|
||||
const { result: state } = await this.requestSessionStatus<string>(
|
||||
const state = await this.getSessionState(
|
||||
`${this.serverUrl}${stateLink.href}?wait=30`,
|
||||
{
|
||||
headers
|
||||
},
|
||||
'text'
|
||||
etag!,
|
||||
accessToken
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
@@ -223,73 +214,33 @@ export class SessionManager {
|
||||
})
|
||||
}
|
||||
|
||||
private async request<T>(
|
||||
private async getSessionState(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
etag: string,
|
||||
accessToken?: string
|
||||
) {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
}
|
||||
}
|
||||
return await this.requestClient
|
||||
.get(url, accessToken, 'text/plain', { 'If-None-Match': etag })
|
||||
.then((res) => res.result as string)
|
||||
.catch((err) => {
|
||||
if (err.status === INTERNAL_SAS_ERROR.status)
|
||||
return INTERNAL_SAS_ERROR.message
|
||||
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(token) => {
|
||||
this.csrfToken = token
|
||||
this.setCsrfToken(token)
|
||||
},
|
||||
contentType
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
private async requestSessionStatus<T>(
|
||||
url: string,
|
||||
options: RequestInit,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
) {
|
||||
if (this.csrfToken) {
|
||||
options.headers = {
|
||||
...options.headers,
|
||||
[this.csrfToken.headerName]: this.csrfToken.value
|
||||
}
|
||||
}
|
||||
|
||||
return await makeRequest<T>(
|
||||
url,
|
||||
options,
|
||||
(token) => {
|
||||
this.csrfToken = token
|
||||
this.setCsrfToken(token)
|
||||
},
|
||||
contentType
|
||||
).catch((err) => {
|
||||
if (err.status === INTERNAL_SAS_ERROR.status)
|
||||
return { result: INTERNAL_SAS_ERROR.message }
|
||||
|
||||
throw err
|
||||
})
|
||||
throw err
|
||||
})
|
||||
}
|
||||
|
||||
async getVariable(sessionId: string, variable: string, accessToken?: string) {
|
||||
const getSessionVariable = {
|
||||
method: 'GET',
|
||||
headers: this.getHeaders(accessToken)
|
||||
}
|
||||
|
||||
return await this.request<SessionVariable>(
|
||||
`${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
|
||||
getSessionVariable
|
||||
).catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
`Error while fetching session variable '${variable}'.`
|
||||
return await this.requestClient
|
||||
.get<SessionVariable>(
|
||||
`${this.serverUrl}/compute/sessions/${sessionId}/variables/${variable}`,
|
||||
accessToken
|
||||
)
|
||||
})
|
||||
.catch((err) => {
|
||||
throw prefixMessage(
|
||||
err,
|
||||
`Error while fetching session variable '${variable}'.`
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
7
src/__mocks__/axios.ts
Normal file
7
src/__mocks__/axios.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { AxiosStatic } from 'axios'
|
||||
|
||||
const mockAxios = jest.genMockFromModule('axios') as AxiosStatic
|
||||
|
||||
mockAxios.create = jest.fn(() => mockAxios)
|
||||
|
||||
export default mockAxios
|
||||
156
src/auth/AuthManager.ts
Normal file
156
src/auth/AuthManager.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { isAuthorizeFormRequired } from '.'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { serialize } from '../utils'
|
||||
|
||||
export class AuthManager {
|
||||
public userName = ''
|
||||
private loginUrl: string
|
||||
private logoutUrl: string
|
||||
constructor(
|
||||
private serverUrl: string,
|
||||
private serverType: ServerType,
|
||||
private requestClient: RequestClient,
|
||||
private loginCallback: () => Promise<void>
|
||||
) {
|
||||
this.loginUrl = `/SASLogon/login`
|
||||
this.logoutUrl =
|
||||
this.serverType === ServerType.Sas9
|
||||
? '/SASLogon/logout?'
|
||||
: '/SASLogon/logout.do?'
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs into the SAS server with the supplied credentials.
|
||||
* @param username - a string representing the username.
|
||||
* @param password - a string representing the password.
|
||||
*/
|
||||
public async logIn(username: string, password: string) {
|
||||
const loginParams: any = {
|
||||
_service: 'default',
|
||||
username,
|
||||
password
|
||||
}
|
||||
|
||||
this.userName = loginParams.username
|
||||
|
||||
const { isLoggedIn, loginForm } = await this.checkSession()
|
||||
if (isLoggedIn) {
|
||||
await this.loginCallback()
|
||||
|
||||
return {
|
||||
isLoggedIn,
|
||||
userName: this.userName
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in loginForm) {
|
||||
loginParams[key] = loginForm[key]
|
||||
}
|
||||
const loginParamsStr = serialize(loginParams)
|
||||
|
||||
const { result: loginResponse } = await this.requestClient.post<string>(
|
||||
this.loginUrl,
|
||||
loginParamsStr,
|
||||
undefined,
|
||||
'text/plain',
|
||||
{
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
)
|
||||
|
||||
let loggedIn = isLogInSuccess(loginResponse)
|
||||
|
||||
if (!loggedIn) {
|
||||
const currentSession = await this.checkSession()
|
||||
loggedIn = currentSession.isLoggedIn
|
||||
}
|
||||
|
||||
if (loggedIn) {
|
||||
this.loginCallback()
|
||||
}
|
||||
|
||||
return {
|
||||
isLoggedIn: !!loggedIn,
|
||||
userName: this.userName
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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`.
|
||||
*/
|
||||
public async checkSession() {
|
||||
const { result: loginResponse } = await this.requestClient.get<string>(
|
||||
this.loginUrl.replace('.do', ''),
|
||||
undefined,
|
||||
'text/plain'
|
||||
)
|
||||
const responseText = loginResponse
|
||||
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,
|
||||
loginForm
|
||||
})
|
||||
}
|
||||
|
||||
private 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
|
||||
}
|
||||
|
||||
private setLoginUrl = (matches: RegExpExecArray) => {
|
||||
let parsedURL = matches[1].replace(/\?.*/, '')
|
||||
if (parsedURL[0] === '/') {
|
||||
parsedURL = parsedURL.substr(1)
|
||||
|
||||
const tempLoginLink = this.serverUrl
|
||||
? `${this.serverUrl}/${parsedURL}`
|
||||
: `${parsedURL}`
|
||||
|
||||
const loginUrl = tempLoginLink
|
||||
|
||||
this.loginUrl =
|
||||
this.serverType === ServerType.SasViya
|
||||
? tempLoginLink
|
||||
: loginUrl.replace('.do', '')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs out of the configured SAS server.
|
||||
*/
|
||||
public logOut() {
|
||||
this.requestClient.clearCsrfTokens()
|
||||
return this.requestClient.get(this.logoutUrl, undefined).then(() => true)
|
||||
}
|
||||
}
|
||||
|
||||
const isLogInSuccess = (response: string): boolean =>
|
||||
/You have signed in/gm.test(response)
|
||||
3
src/auth/index.ts
Normal file
3
src/auth/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from './AuthManager'
|
||||
export * from './isAuthorizeFormRequired'
|
||||
export * from './isLoginRequired'
|
||||
217
src/auth/spec/AuthManager.spec.ts
Normal file
217
src/auth/spec/AuthManager.spec.ts
Normal file
@@ -0,0 +1,217 @@
|
||||
import { AuthManager } from '../AuthManager'
|
||||
import * as dotenv from 'dotenv'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import axios from 'axios'
|
||||
import {
|
||||
mockLoginAuthoriseRequiredResponse,
|
||||
mockLoginSuccessResponse
|
||||
} from './mockResponses'
|
||||
import { serialize } from '../../utils'
|
||||
import { RequestClient } from '../../request/RequestClient'
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe('AuthManager', () => {
|
||||
const authCallback = jest.fn().mockImplementation(() => Promise.resolve())
|
||||
const serverUrl = 'http://test-server.com'
|
||||
const serverType = ServerType.SasViya
|
||||
const userName = 'test-username'
|
||||
const password = 'test-password'
|
||||
const requestClient = new RequestClient(serverUrl)
|
||||
|
||||
beforeAll(() => {
|
||||
dotenv.config()
|
||||
jest.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('should instantiate and set the correct URLs for a Viya server', () => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
|
||||
expect(authManager).toBeTruthy()
|
||||
expect((authManager as any).serverUrl).toEqual(serverUrl)
|
||||
expect((authManager as any).serverType).toEqual(serverType)
|
||||
expect((authManager as any).loginUrl).toEqual(`/SASLogon/login`)
|
||||
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout.do?')
|
||||
})
|
||||
|
||||
it('should instantiate and set the correct URLs for a SAS9 server', () => {
|
||||
const authCallback = () => Promise.resolve()
|
||||
const serverType = ServerType.Sas9
|
||||
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
|
||||
expect(authManager).toBeTruthy()
|
||||
expect((authManager as any).serverUrl).toEqual(serverUrl)
|
||||
expect((authManager as any).serverType).toEqual(serverType)
|
||||
expect((authManager as any).loginUrl).toEqual(`/SASLogon/login`)
|
||||
expect((authManager as any).logoutUrl).toEqual('/SASLogon/logout?')
|
||||
})
|
||||
|
||||
it('should call the auth callback and return when already logged in', async (done) => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: true,
|
||||
userName: 'test',
|
||||
loginForm: 'test'
|
||||
})
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
done()
|
||||
})
|
||||
|
||||
it('should post a login request to the server if not logged in', async (done) => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: 'test',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: mockLoginSuccessResponse })
|
||||
)
|
||||
|
||||
const loginResponse = await authManager.logIn(userName, password)
|
||||
|
||||
expect(loginResponse.isLoggedIn).toBeTruthy()
|
||||
expect(loginResponse.userName).toEqual(userName)
|
||||
|
||||
const loginParams = serialize({
|
||||
_service: 'default',
|
||||
username: userName,
|
||||
password,
|
||||
name: 'test'
|
||||
})
|
||||
expect(mockedAxios.post).toHaveBeenCalledWith(
|
||||
`/SASLogon/login`,
|
||||
loginParams,
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: '*/*'
|
||||
}
|
||||
}
|
||||
)
|
||||
expect(authCallback).toHaveBeenCalledTimes(1)
|
||||
done()
|
||||
})
|
||||
|
||||
it('should parse and submit the authorisation form when necessary', async (done) => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
jest
|
||||
.spyOn(requestClient, 'authorize')
|
||||
.mockImplementation(() => Promise.resolve())
|
||||
jest.spyOn(authManager, 'checkSession').mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
isLoggedIn: false,
|
||||
userName: 'test',
|
||||
loginForm: { name: 'test' }
|
||||
})
|
||||
)
|
||||
mockedAxios.post.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
data: mockLoginAuthoriseRequiredResponse,
|
||||
config: { url: 'https://test.com/SASLogon/login' },
|
||||
request: { responseURL: 'https://test.com/OAuth/authorize' }
|
||||
})
|
||||
)
|
||||
|
||||
mockedAxios.get.mockImplementationOnce(() =>
|
||||
Promise.resolve({
|
||||
data: mockLoginAuthoriseRequiredResponse
|
||||
})
|
||||
)
|
||||
|
||||
await authManager.logIn(userName, password)
|
||||
|
||||
expect(requestClient.authorize).toHaveBeenCalledWith(
|
||||
mockLoginAuthoriseRequiredResponse
|
||||
)
|
||||
done()
|
||||
})
|
||||
|
||||
it('should check and return session information if logged in', async (done) => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: '<button onClick="logout">' })
|
||||
)
|
||||
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(1, `/SASLogon/login`, {
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
it('should check and return session information if logged in', async (done) => {
|
||||
const authManager = new AuthManager(
|
||||
serverUrl,
|
||||
serverType,
|
||||
requestClient,
|
||||
authCallback
|
||||
)
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: '<button onClick="logout">' })
|
||||
)
|
||||
|
||||
const response = await authManager.checkSession()
|
||||
expect(response.isLoggedIn).toBeTruthy()
|
||||
expect(mockedAxios.get).toHaveBeenNthCalledWith(1, `/SASLogon/login`, {
|
||||
withCredentials: true,
|
||||
responseType: 'text',
|
||||
transformResponse: undefined,
|
||||
headers: {
|
||||
Accept: '*/*',
|
||||
'Content-Type': 'text/plain'
|
||||
}
|
||||
})
|
||||
|
||||
done()
|
||||
})
|
||||
})
|
||||
2
src/auth/spec/mockResponses.ts
Normal file
2
src/auth/spec/mockResponses.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export const mockLoginAuthoriseRequiredResponse = `<form id="application_authorization" action="/SASLogon/oauth/authorize" method="POST"><input type="hidden" name="X-Uaa-Csrf" value="2nfuxIn6WaOURWL7tzTXCe"/>`
|
||||
export const mockLoginSuccessResponse = `You have signed in`
|
||||
24
src/file/generateFileUploadForm.ts
Normal file
24
src/file/generateFileUploadForm.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { convertToCSV } from '../utils/convertToCsv'
|
||||
|
||||
export const generateFileUploadForm = (
|
||||
formData: FormData,
|
||||
data: any
|
||||
): FormData => {
|
||||
for (const tableName in data) {
|
||||
const name = tableName
|
||||
const csv = convertToCSV(data[tableName])
|
||||
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
||||
throw new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
)
|
||||
}
|
||||
|
||||
const file = new Blob([csv], {
|
||||
type: 'application/csv'
|
||||
})
|
||||
|
||||
formData.append(name, file, `${name}.csv`)
|
||||
}
|
||||
|
||||
return formData
|
||||
}
|
||||
31
src/file/generateTableUploadForm.ts
Normal file
31
src/file/generateTableUploadForm.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { convertToCSV } from '../utils/convertToCsv'
|
||||
import { splitChunks } from '../utils/splitChunks'
|
||||
|
||||
export const generateTableUploadForm = (formData: FormData, data: any) => {
|
||||
const sasjsTables = []
|
||||
const requestParams: any = {}
|
||||
let tableCounter = 0
|
||||
for (const tableName in data) {
|
||||
tableCounter++
|
||||
sasjsTables.push(tableName)
|
||||
const csv = convertToCSV(data[tableName])
|
||||
if (csv === 'ERROR: LARGE STRING LENGTH') {
|
||||
throw new Error(
|
||||
'The max length of a string value in SASjs is 32765 characters.'
|
||||
)
|
||||
}
|
||||
// if csv has length more then 16k, send in chunks
|
||||
if (csv.length > 16000) {
|
||||
const csvChunks = splitChunks(csv)
|
||||
// append chunks to form data with same key
|
||||
csvChunks.map((chunk) => {
|
||||
formData.append(`sasjs${tableCounter}data`, chunk)
|
||||
})
|
||||
} else {
|
||||
requestParams[`sasjs${tableCounter}data`] = csv
|
||||
}
|
||||
}
|
||||
requestParams['sasjs_tables'] = sasjsTables.join(' ')
|
||||
|
||||
return { formData, requestParams }
|
||||
}
|
||||
54
src/job-execution/ComputeJobExecutor.ts
Normal file
54
src/job-execution/ComputeJobExecutor.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { ErrorResponse } from '..'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import { ComputeJobExecutionError, LoginRequiredError } from '../types'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
|
||||
export class ComputeJobExecutor extends BaseJobExecutor {
|
||||
constructor(serverUrl: string, private sasViyaApiClient: SASViyaApiClient) {
|
||||
super(serverUrl, ServerType.SasViya)
|
||||
}
|
||||
|
||||
async execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
const waitForResult = true
|
||||
const expectWebout = true
|
||||
|
||||
return this.sasViyaApiClient
|
||||
?.executeComputeJob(
|
||||
sasJob,
|
||||
config.contextName,
|
||||
config.debug,
|
||||
data,
|
||||
accessToken,
|
||||
waitForResult,
|
||||
expectWebout
|
||||
)
|
||||
.then((response) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
let responseJson
|
||||
|
||||
return response.result
|
||||
|
||||
return responseJson
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof ComputeJobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
}
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() =>
|
||||
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||
)
|
||||
}
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
}
|
||||
}
|
||||
40
src/job-execution/JesJobExecutor.ts
Normal file
40
src/job-execution/JesJobExecutor.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { ErrorResponse } from '..'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import { JobExecutionError, LoginRequiredError } from '../types'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
|
||||
export class JesJobExecutor extends BaseJobExecutor {
|
||||
constructor(serverUrl: string, private sasViyaApiClient: SASViyaApiClient) {
|
||||
super(serverUrl, ServerType.SasViya)
|
||||
}
|
||||
|
||||
async execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
return await this.sasViyaApiClient
|
||||
?.executeJob(sasJob, config.contextName, config.debug, data, accessToken)
|
||||
.then((response) => {
|
||||
this.appendRequest(response, sasJob, config.debug)
|
||||
|
||||
return response.result
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
}
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() =>
|
||||
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||
)
|
||||
}
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
}
|
||||
}
|
||||
96
src/job-execution/JobExecutor.ts
Normal file
96
src/job-execution/JobExecutor.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { SASjsRequest } from '../types'
|
||||
import { asyncForEach, parseGeneratedCode, parseSourceCode } from '../utils'
|
||||
|
||||
export type ExecuteFunction = () => Promise<any>
|
||||
|
||||
export interface JobExecutor {
|
||||
execute: (
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string
|
||||
) => Promise<any>
|
||||
resendWaitingRequests: () => Promise<void>
|
||||
getRequests: () => SASjsRequest[]
|
||||
clearRequests: () => void
|
||||
}
|
||||
|
||||
export abstract class BaseJobExecutor implements JobExecutor {
|
||||
constructor(protected serverUrl: string, protected serverType: ServerType) {}
|
||||
|
||||
private waitingRequests: ExecuteFunction[] = []
|
||||
private requests: SASjsRequest[] = []
|
||||
|
||||
abstract execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any,
|
||||
accessToken?: string | undefined
|
||||
): Promise<any>
|
||||
|
||||
resendWaitingRequests = async () => {
|
||||
await asyncForEach(
|
||||
this.waitingRequests,
|
||||
async (waitingRequest: ExecuteFunction) => {
|
||||
await waitingRequest()
|
||||
}
|
||||
)
|
||||
|
||||
this.waitingRequests = []
|
||||
return
|
||||
}
|
||||
|
||||
getRequests = () => this.requests
|
||||
|
||||
clearRequests = () => {
|
||||
this.requests = []
|
||||
}
|
||||
|
||||
protected appendWaitingRequest(request: ExecuteFunction) {
|
||||
this.waitingRequests.push(request)
|
||||
}
|
||||
|
||||
protected appendRequest(response: any, program: string, debug: boolean) {
|
||||
let sourceCode = ''
|
||||
let generatedCode = ''
|
||||
let sasWork = null
|
||||
|
||||
if (debug) {
|
||||
if (response?.result && response?.log) {
|
||||
sourceCode = parseSourceCode(response.log)
|
||||
generatedCode = parseGeneratedCode(response.log)
|
||||
|
||||
if (response.log) {
|
||||
sasWork = response.log
|
||||
} else {
|
||||
sasWork = response.result.WORK
|
||||
}
|
||||
} else if (response?.result) {
|
||||
sourceCode = parseSourceCode(response.result)
|
||||
generatedCode = parseGeneratedCode(response.result)
|
||||
sasWork = response.result.WORK
|
||||
}
|
||||
}
|
||||
|
||||
const stringifiedResult =
|
||||
typeof response?.result === 'string'
|
||||
? response?.result
|
||||
: JSON.stringify(response?.result, null, 2)
|
||||
|
||||
this.requests.push({
|
||||
logFile: response?.log || stringifiedResult || response,
|
||||
serviceLink: program,
|
||||
timestamp: new Date(),
|
||||
sourceCode,
|
||||
generatedCode,
|
||||
SASWORK: sasWork
|
||||
})
|
||||
|
||||
if (this.requests.length > 20) {
|
||||
this.requests.splice(0, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
189
src/job-execution/WebJobExecutor.ts
Normal file
189
src/job-execution/WebJobExecutor.ts
Normal file
@@ -0,0 +1,189 @@
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
import { ErrorResponse, JobExecutionError, LoginRequiredError } from '..'
|
||||
import { generateFileUploadForm } from '../file/generateFileUploadForm'
|
||||
import { generateTableUploadForm } from '../file/generateTableUploadForm'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import { SASViyaApiClient } from '../SASViyaApiClient'
|
||||
import { isRelativePath } from '../utils'
|
||||
import { BaseJobExecutor } from './JobExecutor'
|
||||
|
||||
export class WebJobExecutor extends BaseJobExecutor {
|
||||
constructor(
|
||||
serverUrl: string,
|
||||
serverType: ServerType,
|
||||
private jobsPath: string,
|
||||
private requestClient: RequestClient,
|
||||
private sasViyaApiClient: SASViyaApiClient
|
||||
) {
|
||||
super(serverUrl, serverType)
|
||||
}
|
||||
|
||||
async execute(
|
||||
sasJob: string,
|
||||
data: any,
|
||||
config: any,
|
||||
loginRequiredCallback?: any
|
||||
) {
|
||||
const loginCallback = loginRequiredCallback || (() => Promise.resolve())
|
||||
const program = isRelativePath(sasJob)
|
||||
? config.appLoc
|
||||
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
|
||||
: sasJob
|
||||
: sasJob
|
||||
const jobUri =
|
||||
config.serverType === ServerType.SasViya
|
||||
? await this.getJobUri(sasJob)
|
||||
: ''
|
||||
const apiUrl = `${config.serverUrl}${this.jobsPath}/?${
|
||||
jobUri.length > 0
|
||||
? '__program=' + program + '&_job=' + jobUri
|
||||
: '_program=' + program
|
||||
}`
|
||||
|
||||
let requestParams = {
|
||||
...this.getRequestParams(config)
|
||||
}
|
||||
|
||||
let formData = new FormData()
|
||||
|
||||
if (data) {
|
||||
const stringifiedData = JSON.stringify(data)
|
||||
if (
|
||||
config.serverType === ServerType.Sas9 ||
|
||||
stringifiedData.length > 500000 ||
|
||||
stringifiedData.includes(';')
|
||||
) {
|
||||
// file upload approach
|
||||
try {
|
||||
formData = generateFileUploadForm(formData, data)
|
||||
} catch (e) {
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
} else {
|
||||
// param based approach
|
||||
try {
|
||||
const {
|
||||
formData: newFormData,
|
||||
requestParams: params
|
||||
} = generateTableUploadForm(formData, data)
|
||||
formData = newFormData
|
||||
requestParams = { ...requestParams, ...params }
|
||||
} catch (e) {
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const key in requestParams) {
|
||||
if (requestParams.hasOwnProperty(key)) {
|
||||
formData.append(key, requestParams[key])
|
||||
}
|
||||
}
|
||||
|
||||
return this.requestClient!.post(apiUrl, formData, undefined)
|
||||
.then(async (res) => {
|
||||
if (this.serverType === ServerType.SasViya && config.debug) {
|
||||
const jsonResponse = await this.parseSasViyaDebugResponse(
|
||||
res.result as string
|
||||
)
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
return jsonResponse
|
||||
}
|
||||
this.appendRequest(res, sasJob, config.debug)
|
||||
return res.result
|
||||
})
|
||||
.catch(async (e: Error) => {
|
||||
if (e instanceof JobExecutionError) {
|
||||
this.appendRequest(e, sasJob, config.debug)
|
||||
}
|
||||
if (e instanceof LoginRequiredError) {
|
||||
await loginCallback()
|
||||
this.appendWaitingRequest(() =>
|
||||
this.execute(sasJob, data, config, loginRequiredCallback)
|
||||
)
|
||||
}
|
||||
return Promise.reject(new ErrorResponse(e?.message, e))
|
||||
})
|
||||
}
|
||||
|
||||
private parseSasViyaDebugResponse = async (response: string) => {
|
||||
const iframeStart = response.split(
|
||||
'<iframe style="width: 99%; height: 500px" src="'
|
||||
)[1]
|
||||
const jsonUrl = iframeStart ? iframeStart.split('"></iframe>')[0] : null
|
||||
if (!jsonUrl) {
|
||||
throw new Error('Unable to find webout file URL.')
|
||||
}
|
||||
|
||||
return this.requestClient
|
||||
.get(this.serverUrl + jsonUrl, undefined)
|
||||
.then((res) => res.result)
|
||||
}
|
||||
|
||||
private async getJobUri(sasJob: string) {
|
||||
if (!this.sasViyaApiClient) return ''
|
||||
let uri = ''
|
||||
|
||||
let folderPath
|
||||
let jobName: string
|
||||
if (isRelativePath(sasJob)) {
|
||||
const folderPathParts = sasJob.split('/')
|
||||
folderPath = folderPathParts.length > 1 ? folderPathParts[0] : ''
|
||||
jobName = folderPathParts.length > 1 ? folderPathParts[1] : ''
|
||||
} else {
|
||||
const folderPathParts = sasJob.split('/')
|
||||
jobName = folderPathParts.pop() || ''
|
||||
folderPath = folderPathParts.join('/')
|
||||
}
|
||||
|
||||
if (!jobName) {
|
||||
throw new Error('Job name is empty, null or undefined.')
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
private getRequestParams(config: any): any {
|
||||
const requestParams: any = {}
|
||||
|
||||
if (config.debug) {
|
||||
requestParams['_omittextlog'] = 'false'
|
||||
requestParams['_omitsessionresults'] = 'false'
|
||||
|
||||
requestParams['_debug'] = 131
|
||||
}
|
||||
|
||||
return requestParams
|
||||
}
|
||||
|
||||
private parseSAS9ErrorResponse(response: string) {
|
||||
const logLines = response.split('\n')
|
||||
const parsedLines: string[] = []
|
||||
let firstErrorLineIndex: number = -1
|
||||
|
||||
logLines.map((line: string, index: number) => {
|
||||
if (
|
||||
line.toLowerCase().includes('error') &&
|
||||
!line.toLowerCase().includes('this request completed with errors.') &&
|
||||
firstErrorLineIndex === -1
|
||||
) {
|
||||
firstErrorLineIndex = index
|
||||
}
|
||||
})
|
||||
|
||||
for (let i = firstErrorLineIndex - 10; i <= firstErrorLineIndex + 10; i++) {
|
||||
parsedLines.push(logLines[i])
|
||||
}
|
||||
|
||||
return parsedLines.join(', ')
|
||||
}
|
||||
}
|
||||
4
src/job-execution/index.ts
Normal file
4
src/job-execution/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from './ComputeJobExecutor'
|
||||
export * from './JesJobExecutor'
|
||||
export * from './JobExecutor'
|
||||
export * from './WebJobExecutor'
|
||||
464
src/request/RequestClient.ts
Normal file
464
src/request/RequestClient.ts
Normal file
@@ -0,0 +1,464 @@
|
||||
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
|
||||
import { CsrfToken, JobExecutionError } from '..'
|
||||
import { isAuthorizeFormRequired, isLogInRequired } from '../auth'
|
||||
import { LoginRequiredError } from '../types'
|
||||
import { AuthorizeError } from '../types/AuthorizeError'
|
||||
import { NotFoundError } from '../types/NotFoundError'
|
||||
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
|
||||
|
||||
export interface HttpClient {
|
||||
get<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined,
|
||||
contentType: string,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
post<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
contentType: string,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
put<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
overrideHeaders: { [key: string]: string | number }
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
delete<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined
|
||||
): Promise<{ result: T; etag: string }>
|
||||
|
||||
getCsrfToken(type: 'general' | 'file'): CsrfToken | undefined
|
||||
clearCsrfTokens(): void
|
||||
}
|
||||
|
||||
export class RequestClient implements HttpClient {
|
||||
private csrfToken: CsrfToken = { headerName: '', value: '' }
|
||||
private fileUploadCsrfToken: CsrfToken | undefined
|
||||
private httpClient: AxiosInstance
|
||||
|
||||
constructor(private baseUrl: string, allowInsecure = false) {
|
||||
const https = require('https')
|
||||
if (allowInsecure && https.Agent) {
|
||||
this.httpClient = axios.create({
|
||||
baseURL: baseUrl,
|
||||
httpsAgent: new https.Agent({
|
||||
rejectUnauthorized: !allowInsecure
|
||||
})
|
||||
})
|
||||
} else {
|
||||
this.httpClient = axios.create({
|
||||
baseURL: baseUrl
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public getCsrfToken(type: 'general' | 'file' = 'general') {
|
||||
return type === 'file' ? this.fileUploadCsrfToken : this.csrfToken
|
||||
}
|
||||
|
||||
public clearCsrfTokens() {
|
||||
this.csrfToken = { headerName: '', value: '' }
|
||||
this.fileUploadCsrfToken = { headerName: '', value: '' }
|
||||
}
|
||||
|
||||
public async get<T>(
|
||||
url: string,
|
||||
accessToken: string | undefined,
|
||||
contentType: string = 'application/json',
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, contentType),
|
||||
...overrideHeaders
|
||||
}
|
||||
|
||||
const requestConfig: AxiosRequestConfig = {
|
||||
headers,
|
||||
responseType: contentType === 'text/plain' ? 'text' : 'json',
|
||||
withCredentials: true
|
||||
}
|
||||
if (contentType === 'text/plain') {
|
||||
requestConfig.transformResponse = undefined
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.get<T>(url, requestConfig)
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.get<T>(url, accessToken, contentType, overrideHeaders)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public post<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
contentType = 'application/json',
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, contentType),
|
||||
...overrideHeaders
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.post<T>(url, data, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.post<T>(url, data, accessToken, contentType, overrideHeaders)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public async put<T>(
|
||||
url: string,
|
||||
data: any,
|
||||
accessToken: string | undefined,
|
||||
overrideHeaders: { [key: string]: string | number } = {}
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = {
|
||||
...this.getHeaders(accessToken, 'application/json'),
|
||||
...overrideHeaders
|
||||
}
|
||||
|
||||
return this.httpClient
|
||||
.put<T>(url, data, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.put<T>(url, data, accessToken, overrideHeaders)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public async delete<T>(
|
||||
url: string,
|
||||
accessToken?: string
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = this.getHeaders(accessToken, 'application/json')
|
||||
|
||||
return this.httpClient
|
||||
.delete<T>(url, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () => this.delete<T>(url, accessToken))
|
||||
})
|
||||
}
|
||||
|
||||
public async patch<T>(
|
||||
url: string,
|
||||
data: any = {},
|
||||
accessToken?: string
|
||||
): Promise<{ result: T; etag: string }> {
|
||||
const headers = this.getHeaders(accessToken, 'application/json')
|
||||
|
||||
return this.httpClient
|
||||
.patch<T>(url, data, { headers, withCredentials: true })
|
||||
.then((response) => {
|
||||
throwIfError(response)
|
||||
return this.parseResponse<T>(response)
|
||||
})
|
||||
.catch(async (e) => {
|
||||
return await this.handleError(e, () =>
|
||||
this.patch<T>(url, data, accessToken)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
public async uploadFile(
|
||||
url: string,
|
||||
content: string,
|
||||
accessToken?: string
|
||||
): Promise<any> {
|
||||
const headers = this.getHeaders(accessToken, 'application/json')
|
||||
|
||||
if (this.fileUploadCsrfToken?.value) {
|
||||
headers[
|
||||
this.fileUploadCsrfToken.headerName
|
||||
] = this.fileUploadCsrfToken.value
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await this.httpClient.post(url, content, { headers })
|
||||
return {
|
||||
result: response.data,
|
||||
etag: response.headers['etag'] as string
|
||||
}
|
||||
} catch (e) {
|
||||
const response = e.response as AxiosResponse
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetFileUploadCsrfToken(response)
|
||||
|
||||
if (this.fileUploadCsrfToken) {
|
||||
return this.uploadFile(url, content, accessToken)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
public authorize = async (response: string) => {
|
||||
let authUrl: string | null = null
|
||||
const params: any = {}
|
||||
|
||||
const responseBody = response.split('<body>')[1].split('</body>')[0]
|
||||
const bodyElement = document.createElement('div')
|
||||
bodyElement.innerHTML = responseBody
|
||||
|
||||
const form = bodyElement.querySelector('#application_authorization')
|
||||
authUrl = form ? this.baseUrl + form.getAttribute('action') : null
|
||||
|
||||
const inputs: any = form?.querySelectorAll('input')
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.name === 'user_oauth_approval') {
|
||||
input.value = 'true'
|
||||
}
|
||||
|
||||
params[input.name] = input.value
|
||||
}
|
||||
|
||||
const csrfTokenKey = Object.keys(params).find((k) =>
|
||||
k?.toLowerCase().includes('csrf')
|
||||
)
|
||||
if (csrfTokenKey) {
|
||||
this.csrfToken.value = params[csrfTokenKey]
|
||||
this.csrfToken.headerName = this.csrfToken.headerName || 'x-csrf-token'
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (const key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
formData.append(key, params[key])
|
||||
}
|
||||
}
|
||||
|
||||
if (!authUrl) {
|
||||
throw new Error('Auth Form URL is null or undefined.')
|
||||
}
|
||||
|
||||
return await this.httpClient
|
||||
.post(authUrl, formData, {
|
||||
responseType: 'text',
|
||||
headers: { Accept: '*/*', 'Content-Type': 'text/plain' }
|
||||
})
|
||||
.then((res) => res.data)
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
}
|
||||
|
||||
private getHeaders = (
|
||||
accessToken: string | undefined,
|
||||
contentType: string
|
||||
) => {
|
||||
const headers: any = {}
|
||||
|
||||
if (contentType !== 'application/x-www-form-urlencoded') {
|
||||
headers['Content-Type'] = contentType
|
||||
}
|
||||
|
||||
if (contentType === 'application/json') {
|
||||
headers.Accept = 'application/json'
|
||||
} else {
|
||||
headers.Accept = '*/*'
|
||||
}
|
||||
if (accessToken) {
|
||||
headers.Authorization = `Bearer ${accessToken}`
|
||||
}
|
||||
if (this.csrfToken.headerName && this.csrfToken.value) {
|
||||
headers[this.csrfToken.headerName] = this.csrfToken.value
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
private parseAndSetFileUploadCsrfToken = (response: AxiosResponse) => {
|
||||
const token = this.parseCsrfToken(response)
|
||||
|
||||
if (token) {
|
||||
this.fileUploadCsrfToken = token
|
||||
}
|
||||
}
|
||||
|
||||
private parseAndSetCsrfToken = (response: AxiosResponse) => {
|
||||
const token = this.parseCsrfToken(response)
|
||||
|
||||
if (token) {
|
||||
this.csrfToken = token
|
||||
}
|
||||
}
|
||||
|
||||
private parseCsrfToken = (response: AxiosResponse): CsrfToken | undefined => {
|
||||
const tokenHeader = (response.headers[
|
||||
'x-csrf-header'
|
||||
] as string)?.toLowerCase()
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers[tokenHeader]
|
||||
const csrfToken = {
|
||||
headerName: tokenHeader,
|
||||
value: token || ''
|
||||
}
|
||||
|
||||
return csrfToken
|
||||
}
|
||||
}
|
||||
|
||||
private handleError = async (e: any, callback: any) => {
|
||||
const response = e.response as AxiosResponse
|
||||
if (e instanceof AuthorizeError) {
|
||||
const res = await this.httpClient.get(e.confirmUrl, {
|
||||
responseType: 'text',
|
||||
headers: { 'Content-Type': 'text/plain', Accept: '*/*' }
|
||||
})
|
||||
|
||||
if (isAuthorizeFormRequired(res?.data as string)) {
|
||||
await this.authorize(res.data as string)
|
||||
}
|
||||
return await callback()
|
||||
}
|
||||
if (e instanceof LoginRequiredError) {
|
||||
this.clearCsrfTokens()
|
||||
}
|
||||
if (response?.status === 403 || response?.status === 449) {
|
||||
this.parseAndSetCsrfToken(response)
|
||||
if (this.csrfToken.headerName && this.csrfToken.value) {
|
||||
return await callback()
|
||||
}
|
||||
throw e
|
||||
} else if (response?.status === 404) {
|
||||
throw new NotFoundError(response.config.url!)
|
||||
}
|
||||
throw e
|
||||
}
|
||||
|
||||
private async parseResponse<T>(response: AxiosResponse<any>) {
|
||||
const etag = response?.headers ? response.headers['etag'] : ''
|
||||
let parsedResponse
|
||||
|
||||
try {
|
||||
if (typeof response.data === 'string') {
|
||||
parsedResponse = JSON.parse(response.data)
|
||||
} else {
|
||||
parsedResponse = response.data
|
||||
}
|
||||
} catch {
|
||||
try {
|
||||
parsedResponse = JSON.parse(parseWeboutResponse(response.data))
|
||||
} catch {
|
||||
parsedResponse = response.data
|
||||
}
|
||||
}
|
||||
return {
|
||||
result: parsedResponse as T,
|
||||
etag
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const throwIfError = (response: AxiosResponse) => {
|
||||
if (response.status === 401) {
|
||||
throw new LoginRequiredError()
|
||||
}
|
||||
|
||||
if (response.data?.entityID?.includes('login')) {
|
||||
throw new LoginRequiredError()
|
||||
}
|
||||
|
||||
if (
|
||||
typeof response.data === 'string' &&
|
||||
isAuthorizeFormRequired(response.data)
|
||||
) {
|
||||
throw new AuthorizeError(
|
||||
'Authorization required',
|
||||
response.request.responseURL
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
typeof response.data === 'string' &&
|
||||
isLogInRequired(response.data) &&
|
||||
!response.config?.url?.includes('/SASLogon/login')
|
||||
) {
|
||||
throw new LoginRequiredError()
|
||||
}
|
||||
if (response.data?.auth_request) {
|
||||
const authorizeRequestUrl = response.request.responseURL
|
||||
throw new AuthorizeError(response.data.message, authorizeRequestUrl)
|
||||
}
|
||||
|
||||
const error = parseError(response.data as string)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
const parseError = (data: string) => {
|
||||
try {
|
||||
const responseJson = JSON.parse(data?.replace(/[\n\r]/g, ' '))
|
||||
return responseJson.errorCode && responseJson.message
|
||||
? new JobExecutionError(
|
||||
responseJson.errorCode,
|
||||
responseJson.message,
|
||||
data?.replace(/[\n\r]/g, ' ')
|
||||
)
|
||||
: null
|
||||
} catch (_) {
|
||||
try {
|
||||
const hasError = data?.includes('{"errorCode')
|
||||
if (hasError) {
|
||||
const parts = data.split('{"errorCode')
|
||||
if (parts.length > 1) {
|
||||
const error = '{"errorCode' + parts[1].split('"}')[0] + '"}'
|
||||
const errorJson = JSON.parse(error.replace(/[\n\r]/g, ' '))
|
||||
return new JobExecutionError(
|
||||
errorJson.errorCode,
|
||||
errorJson.message,
|
||||
data?.replace(/[\n\r]/g, '\n')
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
try {
|
||||
const hasError = !!data?.match(/stored process not found: /i)
|
||||
if (hasError) {
|
||||
const parts = data.split(/stored process not found: /i)
|
||||
if (parts.length > 1) {
|
||||
const storedProcessPath = parts[1].split('<i>')[1].split('</i>')[0]
|
||||
const message = `Stored process not found: ${storedProcessPath}`
|
||||
return new JobExecutionError(404, message, '')
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
} catch (_) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,16 @@
|
||||
import { ContextManager } from '../ContextManager'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import * as dotenv from 'dotenv'
|
||||
import axios from 'axios'
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe('ContextManager', () => {
|
||||
let originalFetch: any
|
||||
let fetchCallNumber = 0
|
||||
|
||||
const fakeGlobalFetch = (fakeResponses: object[]) => {
|
||||
;(global as any).fetch = jest.fn().mockImplementation(() => {
|
||||
const fakeResponse = fakeResponses[fetchCallNumber]
|
||||
|
||||
if (
|
||||
fetchCallNumber !== fakeResponses.length &&
|
||||
fakeResponses.length > 1
|
||||
) {
|
||||
if (fetchCallNumber + 1 === fakeResponses.length) fetchCallNumber = 0
|
||||
else fetchCallNumber += 1
|
||||
} else {
|
||||
fetchCallNumber = 0
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
ok: true,
|
||||
headers: { get: () => '' },
|
||||
json: () => Promise.resolve(fakeResponse)
|
||||
})
|
||||
})
|
||||
}
|
||||
dotenv.config()
|
||||
|
||||
const contextManager = new ContextManager(
|
||||
process.env.SERVER_URL as string,
|
||||
() => {}
|
||||
new RequestClient(process.env.SERVER_URL as string)
|
||||
)
|
||||
|
||||
const defaultComputeContexts = contextManager.getDefaultComputeContexts
|
||||
@@ -43,14 +25,6 @@ describe('ContextManager', () => {
|
||||
Math.floor(Math.random() * defaultLauncherContexts.length)
|
||||
]
|
||||
|
||||
beforeAll(() => {
|
||||
originalFetch = (global as any).fetch
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
;(global as any).fetch = originalFetch
|
||||
})
|
||||
|
||||
describe('getComputeContexts', () => {
|
||||
it('should fetch compute contexts', async () => {
|
||||
const sampleComputeContext = {
|
||||
@@ -65,7 +39,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(contextManager.getComputeContexts()).resolves.toEqual([
|
||||
sampleComputeContext
|
||||
@@ -87,7 +63,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(contextManager.getLauncherContexts()).resolves.toEqual([
|
||||
sampleComputeContext
|
||||
@@ -137,7 +115,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
@@ -176,10 +156,13 @@ describe('ContextManager', () => {
|
||||
items: [sampleNewComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([
|
||||
sampleResponseExistingComputeContexts,
|
||||
sampleResponseCreatedComputeContext
|
||||
])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingComputeContexts })
|
||||
)
|
||||
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedComputeContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
@@ -226,10 +209,13 @@ describe('ContextManager', () => {
|
||||
items: [sampleNewComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([
|
||||
sampleResponseExistingComputeContexts,
|
||||
sampleResponseCreatedComputeContext
|
||||
])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingComputeContexts })
|
||||
)
|
||||
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedComputeContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
@@ -287,11 +273,16 @@ describe('ContextManager', () => {
|
||||
items: [sampleNewComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([
|
||||
sampleResponseExistingComputeContexts,
|
||||
sampleResponseCreatedLauncherContext,
|
||||
sampleResponseCreatedComputeContext
|
||||
])
|
||||
mockedAxios.get
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingComputeContexts })
|
||||
)
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedLauncherContext })
|
||||
)
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedComputeContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createComputeContext(
|
||||
@@ -346,7 +337,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleLauncherContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createLauncherContext(contextName, 'Test Description')
|
||||
@@ -380,10 +373,13 @@ describe('ContextManager', () => {
|
||||
items: [sampleNewLauncherContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([
|
||||
sampleResponseExistingLauncherContext,
|
||||
sampleResponseCreatedLauncherContext
|
||||
])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseExistingLauncherContext })
|
||||
)
|
||||
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseCreatedLauncherContext })
|
||||
)
|
||||
|
||||
await expect(
|
||||
contextManager.createLauncherContext(contextName, 'Test Description')
|
||||
@@ -448,7 +444,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponseGetComputeContextByName])
|
||||
mockedAxios.put.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseGetComputeContextByName })
|
||||
)
|
||||
|
||||
const expectedResponse = {
|
||||
etag: '',
|
||||
@@ -475,7 +473,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
const user = 'testUser'
|
||||
|
||||
@@ -508,7 +508,9 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([sampleResponse])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
const fakedExecuteScript = async () => {
|
||||
return Promise.resolve({ log: '' })
|
||||
@@ -567,10 +569,13 @@ describe('ContextManager', () => {
|
||||
items: [sampleComputeContext]
|
||||
}
|
||||
|
||||
fakeGlobalFetch([
|
||||
sampleResponseGetComputeContextByName,
|
||||
sampleResponseDeletedContext
|
||||
])
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseGetComputeContextByName })
|
||||
)
|
||||
|
||||
mockedAxios.delete.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponseDeletedContext })
|
||||
)
|
||||
|
||||
const expectedResponse = {
|
||||
etag: '',
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { FileUploader } from '../FileUploader'
|
||||
import { UploadFile } from '../types'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import axios from 'axios'
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
const sampleResponse = `{
|
||||
"SYSUSERID": "cas",
|
||||
@@ -24,39 +28,22 @@ const prepareFilesAndParams = () => {
|
||||
}
|
||||
|
||||
describe('FileUploader', () => {
|
||||
let originalFetch: any
|
||||
const fileUploader = new FileUploader(
|
||||
'/sample/apploc',
|
||||
'https://sample.server.com',
|
||||
'/jobs/path',
|
||||
null,
|
||||
null
|
||||
new RequestClient('https://sample.server.com')
|
||||
)
|
||||
|
||||
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()
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
fileUploader.uploadFile(sasJob, files, params).then((res: any) => {
|
||||
expect(JSON.stringify(res)).toEqual(
|
||||
JSON.stringify(JSON.parse(sampleResponse))
|
||||
)
|
||||
expect(res).toEqual(JSON.parse(sampleResponse))
|
||||
done()
|
||||
})
|
||||
})
|
||||
@@ -83,10 +70,8 @@ describe('FileUploader', () => {
|
||||
})
|
||||
|
||||
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">')
|
||||
})
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: '<form action="Logon">' })
|
||||
)
|
||||
|
||||
const sasJob = 'test'
|
||||
@@ -101,35 +86,29 @@ describe('FileUploader', () => {
|
||||
})
|
||||
|
||||
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"')
|
||||
})
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.resolve({ data: '{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.'
|
||||
)
|
||||
expect(err.error.message).toEqual('File upload request failed.')
|
||||
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"}')
|
||||
})
|
||||
mockedAxios.post.mockImplementation(() =>
|
||||
Promise.reject({ data: '{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.')
|
||||
expect(err.error.message).toEqual('File upload request failed.')
|
||||
|
||||
done()
|
||||
})
|
||||
|
||||
@@ -1,25 +1,19 @@
|
||||
import { SessionManager } from '../SessionManager'
|
||||
import * as dotenv from 'dotenv'
|
||||
import { RequestClient } from '../request/RequestClient'
|
||||
import axios from 'axios'
|
||||
jest.mock('axios')
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
|
||||
describe('SessionManager', () => {
|
||||
dotenv.config()
|
||||
|
||||
let originalFetch: any
|
||||
|
||||
const sessionManager = new SessionManager(
|
||||
process.env.SERVER_URL as string,
|
||||
process.env.DEFAULT_COMPUTE_CONTEXT as string,
|
||||
() => {}
|
||||
new RequestClient('https://sample.server.com')
|
||||
)
|
||||
|
||||
beforeAll(() => {
|
||||
originalFetch = (global as any).fetch
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
;(global as any).fetch = originalFetch
|
||||
})
|
||||
|
||||
describe('getVariable', () => {
|
||||
it('should fetch session variable', async () => {
|
||||
const sampleResponse = {
|
||||
@@ -31,12 +25,8 @@ describe('SessionManager', () => {
|
||||
version: 1
|
||||
}
|
||||
|
||||
;(global as any).fetch = jest.fn().mockImplementation(() =>
|
||||
Promise.resolve({
|
||||
ok: true,
|
||||
headers: { get: () => '' },
|
||||
json: () => Promise.resolve(sampleResponse)
|
||||
})
|
||||
mockedAxios.get.mockImplementation(() =>
|
||||
Promise.resolve({ data: sampleResponse })
|
||||
)
|
||||
|
||||
const expectedResponse = { etag: '', result: sampleResponse }
|
||||
|
||||
7
src/types/AuthorizeError.ts
Normal file
7
src/types/AuthorizeError.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class AuthorizeError extends Error {
|
||||
constructor(public message: string, public confirmUrl: string) {
|
||||
super(message)
|
||||
this.name = 'AuthorizeError'
|
||||
Object.setPrototypeOf(this, AuthorizeError.prototype)
|
||||
}
|
||||
}
|
||||
9
src/types/ComputeJobExecutionError.ts
Normal file
9
src/types/ComputeJobExecutionError.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { Job } from './Job'
|
||||
|
||||
export class ComputeJobExecutionError extends Error {
|
||||
constructor(public job: Job, public log: string) {
|
||||
super('Error: Job execution failed')
|
||||
this.name = 'ComputeJobExecutionError'
|
||||
Object.setPrototypeOf(this, ComputeJobExecutionError.prototype)
|
||||
}
|
||||
}
|
||||
11
src/types/JobExecutionError.ts
Normal file
11
src/types/JobExecutionError.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
export class JobExecutionError extends Error {
|
||||
constructor(
|
||||
public errorCode: number,
|
||||
public errorMessage: string,
|
||||
public result: string
|
||||
) {
|
||||
super(`Error Code ${errorCode}: ${errorMessage}`)
|
||||
this.name = 'JobExecutionError'
|
||||
Object.setPrototypeOf(this, JobExecutionError.prototype)
|
||||
}
|
||||
}
|
||||
7
src/types/LoginRequiredError.ts
Normal file
7
src/types/LoginRequiredError.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class LoginRequiredError extends Error {
|
||||
constructor() {
|
||||
super('Auth error: You must be logged in to access this resource')
|
||||
this.name = 'LoginRequiredError'
|
||||
Object.setPrototypeOf(this, LoginRequiredError.prototype)
|
||||
}
|
||||
}
|
||||
7
src/types/NotFoundError.ts
Normal file
7
src/types/NotFoundError.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export class NotFoundError extends Error {
|
||||
constructor(public url: string) {
|
||||
super(`Error: Resource at ${url} was not found`)
|
||||
this.name = 'NotFoundError'
|
||||
Object.setPrototypeOf(this, NotFoundError.prototype)
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ServerType } from './ServerType'
|
||||
import { ServerType } from '@sasjs/utils/types'
|
||||
|
||||
/**
|
||||
* Specifies the configuration for the SASjs instance - eg where and how to
|
||||
@@ -57,4 +57,10 @@ export class SASjsConfig {
|
||||
* triggered using the APIs instead of the Job Execution Web Service broker.
|
||||
*/
|
||||
useComputeApi = false
|
||||
/**
|
||||
* Defaults to `false`.
|
||||
* When set to `true`, the adapter will allow requests to SAS servers that use a self-signed SSL certificate.
|
||||
* Changing this setting is not recommended.
|
||||
*/
|
||||
allowInsecureRequests = false
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/**
|
||||
* Represents requests that are queued, pending a signon event.
|
||||
*
|
||||
*/
|
||||
export interface SASjsWaitingRequest {
|
||||
requestPromise: {
|
||||
promise: any
|
||||
resolve: any
|
||||
reject: any
|
||||
}
|
||||
SASjob: string
|
||||
data: any
|
||||
config?: any
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Server type that can be `Viya` or `SAS9`.
|
||||
*
|
||||
*/
|
||||
export enum ServerType {
|
||||
SASViya = 'SASVIYA',
|
||||
SAS9 = 'SAS9'
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
export * from './ComputeJobExecutionError'
|
||||
export * from './Context'
|
||||
export * from './CsrfToken'
|
||||
export * from './ErrorResponse'
|
||||
export * from './Folder'
|
||||
export * from './Job'
|
||||
export * from './JobExecutionError'
|
||||
export * from './JobDefinition'
|
||||
export * from './JobResult'
|
||||
export * from './Link'
|
||||
export * from './LoginRequiredError'
|
||||
export * from './SASjsConfig'
|
||||
export * from './SASjsRequest'
|
||||
export * from './SASjsWaitingRequest'
|
||||
export * from './ServerType'
|
||||
export * from './Session'
|
||||
export * from './UploadFile'
|
||||
export * from './PollOptions'
|
||||
|
||||
@@ -1,15 +1,10 @@
|
||||
export * from './asyncForEach'
|
||||
export * from './compareTimestamps'
|
||||
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'
|
||||
export * from './parseGeneratedCode'
|
||||
export * from './parseSourceCode'
|
||||
export * from './parseSasViyaLog'
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export const isLogInSuccess = (response: string): boolean =>
|
||||
/You have signed in/gm.test(response)
|
||||
@@ -1,154 +0,0 @@
|
||||
import { CsrfToken } from '../types'
|
||||
import { needsRetry } from './needsRetry'
|
||||
|
||||
let retryCount: number = 0
|
||||
const retryLimit: number = 5
|
||||
|
||||
export async function makeRequest<T>(
|
||||
url: string,
|
||||
request: RequestInit,
|
||||
callback: (value: CsrfToken) => any,
|
||||
contentType: 'text' | 'json' = 'json'
|
||||
): Promise<{ result: T; etag: string | null }> {
|
||||
let retryRequest: any = null
|
||||
|
||||
const responseTransform =
|
||||
contentType === 'json'
|
||||
? (res: Response) => res.json()
|
||||
: (res: Response) => res.text()
|
||||
let etag = null
|
||||
|
||||
const result = await fetch(url, request)
|
||||
.then(async (response) => {
|
||||
if (response.redirected && response.url.includes('SASLogon/login')) {
|
||||
return Promise.reject({ status: 401 })
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 403) {
|
||||
const tokenHeader = response.headers.get('X-CSRF-HEADER')
|
||||
|
||||
if (tokenHeader) {
|
||||
const token = response.headers.get(tokenHeader)
|
||||
callback({
|
||||
headerName: tokenHeader,
|
||||
value: token || ''
|
||||
})
|
||||
|
||||
retryRequest = {
|
||||
...request,
|
||||
headers: { ...request.headers, [tokenHeader]: token }
|
||||
}
|
||||
|
||||
return await fetch(url, retryRequest).then((res) => {
|
||||
etag = res.headers.get('ETag')
|
||||
return responseTransform(res)
|
||||
})
|
||||
} else {
|
||||
let body: any = await response.text().catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
try {
|
||||
body = JSON.parse(body)
|
||||
|
||||
body.message = `Forbidden. Check your permissions and user groups, and also the scopes granted when registering your CLIENT_ID. ${
|
||||
body.message || ''
|
||||
}`
|
||||
|
||||
body = JSON.stringify(body)
|
||||
} catch (_) {}
|
||||
|
||||
return Promise.reject({ status: response.status, body })
|
||||
}
|
||||
} else {
|
||||
let body: any = await response.text().catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
if (needsRetry(body)) {
|
||||
if (retryCount < retryLimit) {
|
||||
retryCount++
|
||||
let retryResponse = await makeRequest(
|
||||
url,
|
||||
retryRequest || request,
|
||||
callback,
|
||||
contentType
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
retryCount = 0
|
||||
|
||||
etag = retryResponse.etag
|
||||
return retryResponse.result
|
||||
} else {
|
||||
retryCount = 0
|
||||
|
||||
throw new Error('Request retry limit exceeded')
|
||||
}
|
||||
}
|
||||
|
||||
if (response.status === 401) {
|
||||
try {
|
||||
body = JSON.parse(body)
|
||||
|
||||
body.message = `Unauthorized request. Check your credentials(client, secret, access token). ${
|
||||
body.message || ''
|
||||
}`
|
||||
|
||||
body = JSON.stringify(body)
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
return Promise.reject({ status: response.status, body })
|
||||
}
|
||||
} else {
|
||||
if (response.status === 204) {
|
||||
return Promise.resolve()
|
||||
}
|
||||
const responseTransformed = await responseTransform(response).catch(
|
||||
(err) => {
|
||||
throw err
|
||||
}
|
||||
)
|
||||
let responseText = ''
|
||||
|
||||
if (typeof responseTransformed === 'string') {
|
||||
responseText = responseTransformed
|
||||
} else {
|
||||
responseText = JSON.stringify(responseTransformed)
|
||||
}
|
||||
|
||||
if (needsRetry(responseText)) {
|
||||
if (retryCount < retryLimit) {
|
||||
retryCount++
|
||||
const retryResponse = await makeRequest(
|
||||
url,
|
||||
retryRequest || request,
|
||||
callback,
|
||||
contentType
|
||||
).catch((err) => {
|
||||
throw err
|
||||
})
|
||||
retryCount = 0
|
||||
|
||||
etag = retryResponse.etag
|
||||
return retryResponse.result
|
||||
} else {
|
||||
retryCount = 0
|
||||
|
||||
throw new Error('Request retry limit exceeded')
|
||||
}
|
||||
}
|
||||
|
||||
etag = response.headers.get('ETag')
|
||||
|
||||
return responseTransformed
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
throw err
|
||||
})
|
||||
|
||||
return { result, etag }
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
export const parseAndSubmitAuthorizeForm = async (
|
||||
response: string,
|
||||
serverUrl: string
|
||||
) => {
|
||||
let authUrl: string | null = null
|
||||
const params: any = {}
|
||||
|
||||
const responseBody = response.split('<body>')[1].split('</body>')[0]
|
||||
const bodyElement = document.createElement('div')
|
||||
bodyElement.innerHTML = responseBody
|
||||
|
||||
const form = bodyElement.querySelector('#application_authorization')
|
||||
authUrl = form ? serverUrl + form.getAttribute('action') : null
|
||||
|
||||
const inputs: any = form?.querySelectorAll('input')
|
||||
|
||||
for (const input of inputs) {
|
||||
if (input.name === 'user_oauth_approval') {
|
||||
input.value = 'true'
|
||||
}
|
||||
|
||||
params[input.name] = input.value
|
||||
}
|
||||
|
||||
const formData = new FormData()
|
||||
|
||||
for (const key in params) {
|
||||
if (params.hasOwnProperty(key)) {
|
||||
formData.append(key, params[key])
|
||||
}
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
if (authUrl) {
|
||||
fetch(authUrl, {
|
||||
method: 'POST',
|
||||
credentials: 'include',
|
||||
body: formData,
|
||||
referrerPolicy: 'same-origin'
|
||||
})
|
||||
.then((res) => res.text())
|
||||
.then((res) => {
|
||||
resolve(res)
|
||||
})
|
||||
} else {
|
||||
reject('Auth form url is null')
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user