1
0
mirror of https://github.com/sasjs/adapter.git synced 2025-12-15 18:54:36 +00:00

Compare commits

..

53 Commits

Author SHA1 Message Date
Yury Shkoda
59195e6379 Merge branch 'master' into snyk-upgrade-f3e7f573ad5222a75980dcffc4381458 2022-05-10 12:27:32 +03:00
Allan Bowe
72ed5e3fab chore: update test suite README with lrecl option 2022-04-28 11:11:46 +01:00
Muhammad Saad
6bfd7024ce Merge pull request #705 from sasjs/issue-703
sasjs server type - request and job execution auth fix
2022-04-27 19:18:44 -07:00
fdc3e1cce8 style: lint 2022-04-26 17:41:35 +02:00
fc47222830 fix: web request method - login callback handling 2022-04-26 17:37:13 +02:00
Yury Shkoda
0a5de21386 Merge pull request #704 from sasjs/chore
chore: removed console.log
2022-04-26 17:34:58 +03:00
Yury Shkoda
1cbe57d512 chore: removed console.log 2022-04-26 17:25:21 +03:00
936e4f8c0a fix: sasjs server type - request and job execution auth fix 2022-04-26 16:18:36 +02:00
Allan Bowe
4ebf949912 Merge pull request #699 from sasjs/special-missing
Special missing function from @sasjs/utils
2022-04-26 13:54:11 +03:00
c00c8007e5 chore: utils update 2022-04-26 12:27:55 +02:00
snyk-bot
3a820c56a9 fix: upgrade @sasjs/utils from 2.42.0 to 2.42.1
Snyk has created this PR to upgrade @sasjs/utils from 2.42.0 to 2.42.1.

See this package in npm:
https://www.npmjs.com/package/@sasjs/utils

See this project in Snyk:
https://app.snyk.io/org/allanbowe/project/2cf085e5-c256-4a84-bf6a-227076754853?utm_source=github&utm_medium=referral&page=upgrade-pr
2022-04-23 23:21:18 +00:00
54516665bf chore: string escaping 2022-04-22 11:54:59 +02:00
ecec2e77c0 chore: error improved 2022-04-21 15:45:59 +02:00
Allan Bowe
102898ac33 Merge pull request #700 from sasjs/parse-log-in-executeScript
fix: parse log in executeScript method on sasjs server
2022-04-18 21:31:31 +03:00
7370a2be4c fix: can not read property map of undefined 2022-04-18 23:28:12 +05:00
135d019026 chore: update tsdoc for executeScript method 2022-04-18 22:56:51 +05:00
e2651344d7 fix: parse log in executeScript method on sasjs server 2022-04-18 22:50:27 +05:00
9bf3885868 chore: test fix 2022-04-18 14:54:50 +02:00
caa5aa47dc fix: isSpecialMissing from utils 2022-04-18 14:49:57 +02:00
Allan Bowe
7a42bc1b88 Merge pull request #698 from sasjs/executeScriptSASjs
feat: add method for executing scripts on sasjs server
2022-04-13 21:49:46 +03:00
Allan Bowe
6c02ee4cd6 Update SASjs.ts 2022-04-13 19:49:16 +01:00
73ee214b61 feat: add method for executing scripts on sasjs server 2022-04-13 18:22:26 +05:00
Muhammad Saad
77487bfa35 Merge pull request #696 from sasjs/certificate-error
fix(error): throw Certificate error wherever possible
2022-04-08 14:32:02 -07:00
Saad Jutt
9cf0165cf7 chore(error): removed extra prefix of Error: 2022-04-08 14:13:41 +05:00
Saad Jutt
e4d4b3142f chore: updated error message 2022-04-08 00:01:15 +05:00
Saad Jutt
a87be39b44 fix(error): throw Certificate error wherever possible 2022-04-07 23:57:44 +05:00
Allan Bowe
8ea621ac98 Merge pull request #687 from sasjs/issue-686
fix: file upload on SASJS SERVER
2022-04-04 12:37:29 +03:00
ea61119919 chore(git): Merge branch 'master' into issue-686 2022-03-31 15:09:21 +02:00
Allan Bowe
01235616a0 Merge pull request #690 from sasjs/issue-689
fix: update code for extracting JSON URL on viya4 when debug is enabled
2022-03-31 15:54:20 +03:00
Muhammad Saad
10051cb7d1 Merge pull request #692 from sasjs/deploy-to-sasjs-server
fix(deploy): to SASJS with complete JSON object
2022-03-31 17:18:17 +05:00
Saad Jutt
7b0ad2d60d chore: sasjs/utils version bump 2022-03-31 16:39:46 +05:00
Saad Jutt
fc0a450e94 fix(deploy): to SASJS with complete JSON object 2022-03-31 04:20:49 +05:00
6cab245cde fix: update code for extracting json url when debug is enabled 2022-03-30 01:22:03 +05:00
ed90cd8036 chore: uploadFile test added 2022-03-28 17:32:30 +02:00
Allan Bowe
784bab4522 fix: adding README for new sendMacVars service 2022-03-28 12:30:01 +00:00
Allan Bowe
ee97e8211e Merge pull request #685 from sasjs/issue-684
fix: blob is not defined
2022-03-28 14:26:58 +03:00
b0eb8b07a8 style: lint 2022-03-25 18:11:20 +01:00
1d1ef7179e fix: file upload on SASJS SERVER
Also fixed appendRequest when debug on while on SASJS SERVER
2022-03-25 18:05:47 +01:00
d0eb1a7bfb fix: should not create blob in node 2022-03-24 02:43:14 +05:00
Allan Bowe
256e4ef314 Merge pull request #681 from sasjs/sasjs-deploy-with-streaming-app
feat(deploy): stream app deployment on SASJS server
2022-03-21 23:54:40 +02:00
Saad Jutt
6a6dfc5e9d chore: corrected authorised in SASjs.ts only 2022-03-22 02:49:46 +05:00
Saad Jutt
5140848039 chore: sasjs deploy return template type 2022-03-22 02:38:20 +05:00
Saad Jutt
31baf01d3e feat(deploy): stream app deployment on SASJS server 2022-03-22 01:18:02 +05:00
Allan Bowe
804e78cf0c Merge pull request #678 from sasjs/issue-677
fix: return requestPromise from sas9JobExecutor
2022-03-11 12:54:17 +02:00
f6a621fe46 chore: update comments 2022-03-11 14:22:45 +05:00
c47d0c9789 fix: return requestPromise from sas9JobExecutor #677 2022-03-11 14:16:44 +05:00
Allan Bowe
1ddc71b017 Create CNAME 2022-03-10 12:08:46 +00:00
Allan Bowe
3e507885ab Merge pull request #676 from sasjs/issue-675
fix: no need to stringify res/err in sas9JobExecutor before appending…
2022-03-10 14:06:50 +02:00
e92d0d73b5 chore: add comments 2022-03-10 16:17:09 +05:00
00a99e752c fix: no need to stringify res/err in sas9JobExecutor before appending request #675 2022-03-10 15:44:38 +05:00
Allan Bowe
b13f3d2fcb Merge pull request #667 from sasjs/sas-viya-auth-code-responses
fix: raising error with details we get from server
2022-03-09 11:34:40 +02:00
Saad Jutt
495e4b9069 test: updated RequestClient specs 2022-03-02 20:42:14 +05:00
Saad Jutt
2e843e3f36 fix: raising error with details we get from server 2022-03-02 20:25:56 +05:00
26 changed files with 316 additions and 176 deletions

1
docs/CNAME Normal file
View File

@@ -0,0 +1 @@
adapter.sasjs.io

14
package-lock.json generated
View File

@@ -8,7 +8,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@sasjs/utils": "2.36.1", "@sasjs/utils": "2.44.0",
"axios": "0.26.0", "axios": "0.26.0",
"axios-cookiejar-support": "1.0.1", "axios-cookiejar-support": "1.0.1",
"form-data": "4.0.0", "form-data": "4.0.0",
@@ -1142,9 +1142,9 @@
} }
}, },
"node_modules/@sasjs/utils": { "node_modules/@sasjs/utils": {
"version": "2.36.1", "version": "2.44.0",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.44.0.tgz",
"integrity": "sha512-JkGUpLOODsvkeU+S25jb9k2WnvzyD2w6cEk7YyQ/byuqKL8xawH91PPWegrVcJlDY8WmqKE4CPcA3d1mM3B3LA==", "integrity": "sha512-hpC4erHYA8Mcb38mzxFEP0cXehfa0iKeqSW2d9MmxZ9g2qpy0BU6xyZJohN9kOiafXo5H359ndNKsg4DOq5YgA==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
@@ -13870,9 +13870,9 @@
} }
}, },
"@sasjs/utils": { "@sasjs/utils": {
"version": "2.36.1", "version": "2.44.0",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.44.0.tgz",
"integrity": "sha512-JkGUpLOODsvkeU+S25jb9k2WnvzyD2w6cEk7YyQ/byuqKL8xawH91PPWegrVcJlDY8WmqKE4CPcA3d1mM3B3LA==", "integrity": "sha512-hpC4erHYA8Mcb38mzxFEP0cXehfa0iKeqSW2d9MmxZ9g2qpy0BU6xyZJohN9kOiafXo5H359ndNKsg4DOq5YgA==",
"requires": { "requires": {
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/prompts": "2.0.13", "@types/prompts": "2.0.13",

View File

@@ -72,7 +72,7 @@
}, },
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@sasjs/utils": "2.36.1", "@sasjs/utils": "2.44.0",
"axios": "0.26.0", "axios": "0.26.0",
"axios-cookiejar-support": "1.0.1", "axios-cookiejar-support": "1.0.1",
"form-data": "4.0.0", "form-data": "4.0.0",

View File

@@ -65,7 +65,7 @@ The below services need to be created on your SAS server, at the location specif
```sas ```sas
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc; %inc mc;
filename ft15f001 temp; filename ft15f001 temp lrecl=1000;
parmcards4; parmcards4;
%webout(FETCH) %webout(FETCH)
%webout(OPEN) %webout(OPEN)
@@ -84,6 +84,15 @@ parmcards4;
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
%mm_createwebservice(path=/Public/app/common,name=sendArr) %mm_createwebservice(path=/Public/app/common,name=sendArr)
parmcards4;
data work.macvars;
set sashelp.vmacro;
run;
%webout(OPEN)
%webout(OBJ,macvars)
%webout(CLOSE)
;;;;
%mm_createwebservice(path=/Public/app/common,name=sendMacVars)
parmcards4; parmcards4;
let he who hath understanding, reckon the number of the beast let he who hath understanding, reckon the number of the beast
;;;; ;;;;
@@ -104,7 +113,7 @@ data _null_;
```sas ```sas
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas"; filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc; %inc mc;
filename ft15f001 temp; filename ft15f001 temp lrecl=1000;
parmcards4; parmcards4;
%webout(FETCH) %webout(FETCH)
%webout(OPEN) %webout(OPEN)
@@ -118,7 +127,6 @@ parmcards4;
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
%mp_createwebservice(path=/Public/app/common,name=sendObj) %mp_createwebservice(path=/Public/app/common,name=sendObj)
filename ft15f001 temp;
parmcards4; parmcards4;
%webout(FETCH) %webout(FETCH)
%webout(OPEN) %webout(OPEN)
@@ -132,7 +140,15 @@ parmcards4;
%webout(CLOSE) %webout(CLOSE)
;;;; ;;;;
%mp_createwebservice(path=/Public/app/common,name=sendArr) %mp_createwebservice(path=/Public/app/common,name=sendArr)
filename ft15f001 temp; parmcards4;
data work.macvars;
set sashelp.vmacro;
run;
%webout(OPEN)
%webout(OBJ,macvars)
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=sendMacVars)
parmcards4; parmcards4;
If you can keep your head when all about you If you can keep your head when all about you
Are losing theirs and blaming it on you, Are losing theirs and blaming it on you,

View File

@@ -2388,11 +2388,11 @@
"node_modules/@sasjs/adapter": { "node_modules/@sasjs/adapter": {
"version": "5.0.0", "version": "5.0.0",
"resolved": "file:../build/sasjs-adapter-5.0.0.tgz", "resolved": "file:../build/sasjs-adapter-5.0.0.tgz",
"integrity": "sha512-lbDWueAEnfNlu4OGrc9hBEzT0aoLfAy7eLd2nLHArrF6zukcSGBNhUgOqxIhlz4WeBdf1gt3nk1G7p5X1mrWYQ==", "integrity": "sha512-5qtEs9yFuZ4v2UrFGNHeCIr/yZTp7D9He+e+N333qW9mdLJJ8fzRifuur/rFE6bNPqC2bdCjicYkO/yrHR4LQw==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@sasjs/utils": "2.36.1", "@sasjs/utils": "2.40.1",
"axios": "0.26.0", "axios": "0.26.0",
"axios-cookiejar-support": "1.0.1", "axios-cookiejar-support": "1.0.1",
"form-data": "4.0.0", "form-data": "4.0.0",
@@ -2422,9 +2422,9 @@
} }
}, },
"node_modules/@sasjs/utils": { "node_modules/@sasjs/utils": {
"version": "2.36.1", "version": "2.40.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.40.1.tgz",
"integrity": "sha512-JkGUpLOODsvkeU+S25jb9k2WnvzyD2w6cEk7YyQ/byuqKL8xawH91PPWegrVcJlDY8WmqKE4CPcA3d1mM3B3LA==", "integrity": "sha512-wWYElDH71bSZTdZ5V38743vAnw2EPDhzH7+1s7zxINHpaQWK/qrDldI0vgVFLeGpxVU0D7WPZ/ltG6MoE2obeg==",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
@@ -20998,9 +20998,9 @@
}, },
"@sasjs/adapter": { "@sasjs/adapter": {
"version": "file:../build/sasjs-adapter-5.0.0.tgz", "version": "file:../build/sasjs-adapter-5.0.0.tgz",
"integrity": "sha512-lbDWueAEnfNlu4OGrc9hBEzT0aoLfAy7eLd2nLHArrF6zukcSGBNhUgOqxIhlz4WeBdf1gt3nk1G7p5X1mrWYQ==", "integrity": "sha512-5qtEs9yFuZ4v2UrFGNHeCIr/yZTp7D9He+e+N333qW9mdLJJ8fzRifuur/rFE6bNPqC2bdCjicYkO/yrHR4LQw==",
"requires": { "requires": {
"@sasjs/utils": "2.36.1", "@sasjs/utils": "2.40.1",
"axios": "0.26.0", "axios": "0.26.0",
"axios-cookiejar-support": "1.0.1", "axios-cookiejar-support": "1.0.1",
"form-data": "4.0.0", "form-data": "4.0.0",
@@ -21022,9 +21022,9 @@
} }
}, },
"@sasjs/utils": { "@sasjs/utils": {
"version": "2.36.1", "version": "2.40.1",
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.36.1.tgz", "resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-2.40.1.tgz",
"integrity": "sha512-JkGUpLOODsvkeU+S25jb9k2WnvzyD2w6cEk7YyQ/byuqKL8xawH91PPWegrVcJlDY8WmqKE4CPcA3d1mM3B3LA==", "integrity": "sha512-wWYElDH71bSZTdZ5V38743vAnw2EPDhzH7+1s7zxINHpaQWK/qrDldI0vgVFLeGpxVU0D7WPZ/ltG6MoE2obeg==",
"requires": { "requires": {
"@types/fs-extra": "9.0.13", "@types/fs-extra": "9.0.13",
"@types/prompts": "2.0.13", "@types/prompts": "2.0.13",

View File

@@ -6,6 +6,7 @@ import { specialCaseTests } from './testSuites/SpecialCases'
import { sasjsRequestTests } from './testSuites/SasjsRequests' import { sasjsRequestTests } from './testSuites/SasjsRequests'
import '@sasjs/test-framework/dist/index.css' import '@sasjs/test-framework/dist/index.css'
import { computeTests } from './testSuites/Compute' import { computeTests } from './testSuites/Compute'
import { fileUploadTests } from './testSuites/FileUpload'
const App = (): ReactElement<{}> => { const App = (): ReactElement<{}> => {
const { adapter, config } = useContext(AppContext) const { adapter, config } = useContext(AppContext)
@@ -18,7 +19,8 @@ const App = (): ReactElement<{}> => {
sendArrTests(adapter), sendArrTests(adapter),
sendObjTests(adapter), sendObjTests(adapter),
specialCaseTests(adapter), specialCaseTests(adapter),
sasjsRequestTests(adapter) sasjsRequestTests(adapter),
fileUploadTests(adapter)
] ]
if (adapter.getSasjsConfig().serverType === 'SASVIYA') { if (adapter.getSasjsConfig().serverType === 'SASVIYA') {

View File

@@ -0,0 +1,35 @@
import SASjs from '@sasjs/adapter'
import { TestSuite } from '@sasjs/test-framework'
export const fileUploadTests = (adapter: SASjs): TestSuite => ({
name: 'File Upload Tests',
tests: [
{
title: 'Upload File',
description: 'Should upload the file to VIYA',
test: async () => {
let blob: any = new Blob(['test'], { type: 'text/html' })
blob['lastModifiedDate'] = ''
blob['name'] = 'macvars_testfile'
let file = blob
const filesToUpload = [
{
file: file,
fileName: file.name
}
]
return adapter.uploadFile('common/sendMacVars', filesToUpload, null)
},
assertion: (response: any) =>
(response.macvars as any[]).findIndex(
(el: any) => el.NAME === '_WEBIN_FILE_COUNT' && el.VALUE === '1'
) > -1 &&
(response.macvars as any[]).findIndex(
(el: any) =>
el.NAME === '_WEBIN_FILENAME' && el.VALUE === 'macvars_testfile'
) > -1
}
]
})

View File

@@ -218,6 +218,7 @@ export const sendObjTests = (adapter: SASjs): TestSuite => ({
const invalidData: any = { const invalidData: any = {
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }] xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx: [{ col1: 42 }]
} }
return adapter.request('common/sendObj', invalidData).catch((e) => e) return adapter.request('common/sendObj', invalidData).catch((e) => e)
}, },
assertion: (error: any) => assertion: (error: any) =>

View File

@@ -11,7 +11,11 @@ import {
JobDefinition, JobDefinition,
PollOptions PollOptions
} from './types' } from './types'
import { JobExecutionError, RootFolderNotFoundError } from './types/errors' import {
CertificateError,
JobExecutionError,
RootFolderNotFoundError
} from './types/errors'
import { SessionManager } from './SessionManager' import { SessionManager } from './SessionManager'
import { ContextManager } from './ContextManager' import { ContextManager } from './ContextManager'
import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types' import { SasAuthResponse, MacroVar, AuthConfig } from '@sasjs/utils/types'
@@ -878,7 +882,8 @@ export class SASViyaApiClient {
const { result: folder } = await this.requestClient const { result: folder } = await this.requestClient
.get<Folder>(`${this.serverUrl}${url}`, accessToken) .get<Folder>(`${this.serverUrl}${url}`, accessToken)
.catch(() => { .catch((err) => {
if (err instanceof CertificateError) throw err
return { result: null } return { result: null }
}) })
@@ -899,7 +904,8 @@ export class SASViyaApiClient {
const { result: folder } = await this.requestClient const { result: folder } = await this.requestClient
.get<Folder>(`${this.serverUrl}${url}`, accessToken) .get<Folder>(`${this.serverUrl}${url}`, accessToken)
.catch(() => { .catch((err) => {
if (err instanceof CertificateError) throw err
return { result: null } return { result: null }
}) })

View File

@@ -5,8 +5,7 @@ import {
EditContextInput, EditContextInput,
PollOptions, PollOptions,
LoginMechanism, LoginMechanism,
ExecutionQuery, ExecutionQuery
FileTree
} from './types' } from './types'
import { SASViyaApiClient } from './SASViyaApiClient' import { SASViyaApiClient } from './SASViyaApiClient'
import { SAS9ApiClient } from './SAS9ApiClient' import { SAS9ApiClient } from './SAS9ApiClient'
@@ -17,7 +16,8 @@ import {
MacroVar, MacroVar,
AuthConfig, AuthConfig,
ExtraResponseAttributes, ExtraResponseAttributes,
SasAuthResponse SasAuthResponse,
ServicePackSASjs
} from '@sasjs/utils/types' } from '@sasjs/utils/types'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
import { SasjsRequestClient } from './request/SasjsRequestClient' import { SasjsRequestClient } from './request/SasjsRequestClient'
@@ -77,7 +77,7 @@ export default class SASjs {
} }
/** /**
* Executes code against a SAS 9 server. Requires a runner to be present in * Executes SAS code on a SAS 9 server. Requires a runner to be present in
* the users home directory in metadata. * the users home directory in metadata.
* @param linesOfCode - lines of sas code from the file to run. * @param linesOfCode - lines of sas code from the file to run.
* @param username - a string representing the username. * @param username - a string representing the username.
@@ -97,6 +97,17 @@ export default class SASjs {
) )
} }
/**
* Executes SAS code on a SASJS server
* @param code - a string of code from the file to run.
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute scripts.
*/
public async executeScriptSASjs(code: string, authConfig?: AuthConfig) {
this.isMethodSupported('executeScriptSASJS', [ServerType.Sasjs])
return await this.sasJSApiClient?.executeScript(code, authConfig)
}
/** /**
* Executes sas code in a SAS Viya compute session. * Executes sas code in a SAS Viya compute session.
* @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution. * @param fileName - name of the file to run. It will be converted to path to the file being submitted for execution.
@@ -134,7 +145,7 @@ export default class SASjs {
/** /**
* Gets compute contexts. * Gets compute contexts.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorised user.
*/ */
public async getComputeContexts(accessToken: string) { public async getComputeContexts(accessToken: string) {
this.isMethodSupported('getComputeContexts', [ServerType.SasViya]) this.isMethodSupported('getComputeContexts', [ServerType.SasViya])
@@ -144,7 +155,7 @@ export default class SASjs {
/** /**
* Gets launcher contexts. * Gets launcher contexts.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorised user.
*/ */
public async getLauncherContexts(accessToken: string) { public async getLauncherContexts(accessToken: string) {
this.isMethodSupported('getLauncherContexts', [ServerType.SasViya]) this.isMethodSupported('getLauncherContexts', [ServerType.SasViya])
@@ -163,7 +174,7 @@ export default class SASjs {
/** /**
* Gets executable compute contexts. * Gets executable compute contexts.
* @param authConfig - an access token, refresh token, client and secret for an authorized user. * @param authConfig - an access token, refresh token, client and secret for an authorised user.
*/ */
public async getExecutableContexts(authConfig: AuthConfig) { public async getExecutableContexts(authConfig: AuthConfig) {
this.isMethodSupported('getExecutableContexts', [ServerType.SasViya]) this.isMethodSupported('getExecutableContexts', [ServerType.SasViya])
@@ -177,8 +188,8 @@ export default class SASjs {
* @param launchContextName - the name of the launcher context used by the compute service. * @param launchContextName - the name of the launcher context used by the compute service.
* @param sharedAccountId - the ID of the account to run the servers for this context as. * @param sharedAccountId - the ID of the account to run the servers for this context as.
* @param autoExecLines - the lines of code to execute during session initialization. * @param autoExecLines - the lines of code to execute during session initialization.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorised user.
* @param authorizedUsers - an optional list of authorized user IDs. * @param authorisedUsers - an optional list of authorised user IDs.
*/ */
public async createComputeContext( public async createComputeContext(
contextName: string, contextName: string,
@@ -186,7 +197,7 @@ export default class SASjs {
sharedAccountId: string, sharedAccountId: string,
autoExecLines: string[], autoExecLines: string[],
accessToken: string, accessToken: string,
authorizedUsers?: string[] authorisedUsers?: string[]
) { ) {
this.isMethodSupported('createComputeContext', [ServerType.SasViya]) this.isMethodSupported('createComputeContext', [ServerType.SasViya])
@@ -196,7 +207,7 @@ export default class SASjs {
sharedAccountId, sharedAccountId,
autoExecLines, autoExecLines,
accessToken, accessToken,
authorizedUsers authorisedUsers
) )
} }
@@ -205,7 +216,7 @@ export default class SASjs {
* @param contextName - the name of the context to be created. * @param contextName - the name of the context to be created.
* @param description - the description of the context to be created. * @param description - the description of the context to be created.
* @param launchType - launch type of the context to be created. * @param launchType - launch type of the context to be created.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorised user.
*/ */
public async createLauncherContext( public async createLauncherContext(
contextName: string, contextName: string,
@@ -227,7 +238,7 @@ export default class SASjs {
* Updates a compute context on the given server. * Updates a compute context on the given server.
* @param contextName - the original name of the context to be deleted. * @param contextName - the original name of the context to be deleted.
* @param editedContext - an object with the properties to be updated. * @param editedContext - an object with the properties to be updated.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorised user.
*/ */
public async editComputeContext( public async editComputeContext(
contextName: string, contextName: string,
@@ -246,7 +257,7 @@ export default class SASjs {
/** /**
* Deletes a compute context on the given server. * Deletes a compute context on the given server.
* @param contextName - the name of the context to be deleted. * @param contextName - the name of the context to be deleted.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorised user.
*/ */
public async deleteComputeContext(contextName: string, accessToken?: string) { public async deleteComputeContext(contextName: string, accessToken?: string) {
this.isMethodSupported('deleteComputeContext', [ServerType.SasViya]) this.isMethodSupported('deleteComputeContext', [ServerType.SasViya])
@@ -261,7 +272,7 @@ export default class SASjs {
* Returns a JSON representation of a compute context. * Returns a JSON representation of a compute context.
* @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" } * @example: { "createdBy": "admin", "links": [...], "id": "ID", "version": 2, "name": "context1" }
* @param contextName - the name of the context to return. * @param contextName - the name of the context to return.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorised user.
*/ */
public async getComputeContextByName( public async getComputeContextByName(
contextName: string, contextName: string,
@@ -278,7 +289,7 @@ export default class SASjs {
/** /**
* Returns a JSON representation of a compute context. * Returns a JSON representation of a compute context.
* @param contextId - an id of the context to return. * @param contextId - an id of the context to return.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorised user.
*/ */
public async getComputeContextById(contextId: string, accessToken?: string) { public async getComputeContextById(contextId: string, accessToken?: string) {
this.isMethodSupported('getComputeContextById', [ServerType.SasViya]) this.isMethodSupported('getComputeContextById', [ServerType.SasViya])
@@ -888,24 +899,25 @@ export default class SASjs {
/** /**
* Creates the folders and services at the given location `appLoc` on the given server `serverUrl`. * Creates the folders and services at the given location `appLoc` on the given server `serverUrl`.
* @param members - the JSON specifying the folders and services to be created. * @param dataJson - the JSON specifying the folders and files to be created, can also includes
* @param appLoc - the base folder in which to create the new folders and * appLoc, streamServiceName, streamWebFolder, streamLogo
* services. If not provided, is taken from SASjsConfig. * @param appLoc - (optional) the base folder in which to create the new folders and
* @param authConfig - a valid client, secret, refresh and access tokens that are authorised to execute compute jobs. * services. If not provided, is taken from SASjsConfig. Precedence will be of appLoc present in dataJson.
* @param authConfig - (optional) a valid client, secret, refresh and access tokens that are authorised to execute compute jobs.
*/ */
public async deployToSASjs( public async deployToSASjs(
members: FileTree, dataJson: ServicePackSASjs,
appLoc?: string, appLoc?: string,
authConfig?: AuthConfig authConfig?: AuthConfig
) { ) {
if (!appLoc) { if (!appLoc) {
appLoc = this.sasjsConfig.appLoc appLoc = this.sasjsConfig.appLoc
} }
return await this.sasJSApiClient?.deploy(members, appLoc, authConfig) return await this.sasJSApiClient?.deploy(dataJson, appLoc, authConfig)
} }
public async executeJobSASjs(query: ExecutionQuery) { public async executeJobSASjs(query: ExecutionQuery, authConfig?: AuthConfig) {
return await this.sasJSApiClient?.executeJob(query) return await this.sasJSApiClient?.executeJob(query, authConfig)
} }
/** /**
@@ -972,7 +984,7 @@ export default class SASjs {
/** /**
* Fetches content of the log file * Fetches content of the log file
* @param logUrl - url of the log file. * @param logUrl - url of the log file.
* @param accessToken - an access token for an authorized user. * @param accessToken - an access token for an authorised user.
*/ */
public async fetchLogFileContent(logUrl: string, accessToken?: string) { public async fetchLogFileContent(logUrl: string, accessToken?: string) {
return await this.requestClient!.get(logUrl, accessToken).then((res) => { return await this.requestClient!.get(logUrl, accessToken).then((res) => {
@@ -1094,13 +1106,8 @@ export default class SASjs {
} }
if (this.sasjsConfig.serverType === ServerType.Sasjs) { if (this.sasjsConfig.serverType === ServerType.Sasjs) {
if (this.sasJSApiClient) { if (!this.sasJSApiClient) {
this.sasJSApiClient.setConfig(this.sasjsConfig.serverUrl) this.sasJSApiClient = new SASjsApiClient(this.requestClient)
} else {
this.sasJSApiClient = new SASjsApiClient(
this.sasjsConfig.serverUrl,
this.requestClient
)
} }
} }

View File

@@ -1,5 +1,5 @@
import { AuthConfig, ServerType } from '@sasjs/utils/types' import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types'
import { FileTree, ExecutionQuery } from './types' import { ExecutionQuery } from './types'
import { RequestClient } from './request/RequestClient' import { RequestClient } from './request/RequestClient'
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs' import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs' import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
@@ -8,17 +8,10 @@ import { parseWeboutResponse } from './utils'
import { getTokens } from './auth/getTokens' import { getTokens } from './auth/getTokens'
export class SASjsApiClient { export class SASjsApiClient {
constructor( constructor(private requestClient: RequestClient) {}
private serverUrl: string,
private requestClient: RequestClient
) {}
public setConfig(serverUrl: string) {
if (serverUrl) this.serverUrl = serverUrl
}
public async deploy( public async deploy(
members: FileTree, dataJson: ServicePackSASjs,
appLoc: string, appLoc: string,
authConfig?: AuthConfig authConfig?: AuthConfig
) { ) {
@@ -30,13 +23,17 @@ export class SASjsApiClient {
ServerType.Sasjs ServerType.Sasjs
)) ))
} }
dataJson.appLoc = dataJson.appLoc || appLoc
const { result } = await this.requestClient.post<{ const { result } = await this.requestClient.post<{
status: string status: string
message: string message: string
streamServiceName?: string
example?: {} example?: {}
}>( }>(
'SASjsApi/drive/deploy', 'SASjsApi/drive/deploy',
{ fileTree: members, appLoc: appLoc }, dataJson,
access_token, access_token,
undefined, undefined,
{}, {},
@@ -46,7 +43,9 @@ export class SASjsApiClient {
return Promise.resolve(result) return Promise.resolve(result)
} }
public async executeJob(query: ExecutionQuery) { public async executeJob(query: ExecutionQuery, authConfig?: AuthConfig) {
const access_token = authConfig ? authConfig.access_token : undefined
const { result } = await this.requestClient.post<{ const { result } = await this.requestClient.post<{
status: string status: string
message: string message: string
@@ -54,7 +53,7 @@ export class SASjsApiClient {
logPath?: string logPath?: string
error?: {} error?: {}
_webout?: string _webout?: string
}>('SASjsApi/stp/execute', query, undefined) }>('SASjsApi/stp/execute', query, access_token)
if (Object.keys(result).includes('_webout')) { if (Object.keys(result).includes('_webout')) {
result._webout = parseWeboutResponse(result._webout!) result._webout = parseWeboutResponse(result._webout!)
@@ -63,6 +62,39 @@ export class SASjsApiClient {
return Promise.resolve(result) return Promise.resolve(result)
} }
/**
* Executes code on a SASJS server.
* @param code - a string of code to execute.
* @param authConfig - an object for authentication.
*/
public async executeScript(code: string, authConfig?: AuthConfig) {
let access_token = (authConfig || {}).access_token
if (authConfig) {
;({ access_token } = await getTokens(
this.requestClient,
authConfig,
ServerType.Sasjs
))
}
let parsedSasjsServerLog = ''
await this.requestClient
.post('SASjsApi/code/execute', { code }, access_token)
.then((res: any) => {
if (res.result?.log) {
parsedSasjsServerLog = res.result.log
.map((logLine: any) => logLine.line)
.join('\n')
}
})
.catch((err) => {
parsedSasjsServerLog = err
})
return parsedSasjsServerLog
}
/** /**
* Exchanges the auth code for an access token for the given client. * Exchanges the auth code for an access token for the given client.
* @param clientId - the client ID to authenticate with. * @param clientId - the client ID to authenticate with.

View File

@@ -1,6 +1,7 @@
import { SasAuthResponse } from '@sasjs/utils/types' import { SasAuthResponse } from '@sasjs/utils/types'
import { prefixMessage } from '@sasjs/utils/error' import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from '../request/RequestClient' import { RequestClient } from '../request/RequestClient'
import { CertificateError } from '../types/errors'
/** /**
* Exchanges the auth code for an access token for the given client. * Exchanges the auth code for an access token for the given client.
@@ -36,6 +37,7 @@ export async function getAccessTokenForViya(
.post(url, data, undefined, 'application/x-www-form-urlencoded', headers) .post(url, data, undefined, 'application/x-www-form-urlencoded', headers)
.then((res) => res.result as SasAuthResponse) .then((res) => res.result as SasAuthResponse)
.catch((err) => { .catch((err) => {
if (err instanceof CertificateError) throw err
throw prefixMessage(err, 'Error while getting access token. ') throw prefixMessage(err, 'Error while getting access token. ')
}) })

View File

@@ -26,11 +26,18 @@ export const generateFileUploadForm = (
) )
} }
const file = new Blob([csv], { if (typeof FormData === 'undefined' && formData instanceof NodeFormData) {
type: 'application/csv' formData.append(name, csv, {
}) filename: `${name}.csv`,
contentType: 'application/csv'
})
} else {
const file = new Blob([csv], {
type: 'application/csv'
})
formData.append(name, file, `${name}.csv`) formData.append(name, file, `${name}.csv`)
}
} }
return formData return formData

View File

@@ -99,7 +99,20 @@ export class FileUploader extends BaseJobExecutor {
? parseWeboutResponse(res.result, uploadUrl) ? parseWeboutResponse(res.result, uploadUrl)
: res.result : res.result
break break
case ServerType.Sasjs:
if (typeof res.result._webout === 'object') {
jsonResponse = res.result._webout
} else {
const webout = parseWeboutResponse(
res.result._webout,
uploadUrl
)
jsonResponse = getValidJson(webout)
}
break
} }
} else if (this.serverType === ServerType.Sasjs) {
jsonResponse = getValidJson(res.result._webout)
} else { } else {
jsonResponse = jsonResponse =
typeof res.result === 'string' typeof res.result === 'string'

View File

@@ -74,38 +74,32 @@ export class Sas9JobExecutor extends BaseJobExecutor {
? 'multipart/form-data; boundary=' + (formData as any)._boundary ? 'multipart/form-data; boundary=' + (formData as any)._boundary
: 'text/plain' : 'text/plain'
return await this.sas9RequestClient!.post( const requestPromise = new Promise((resolve, reject) =>
apiUrl, this.sas9RequestClient!.post(apiUrl, formData, undefined, contentType, {
formData,
undefined,
contentType,
{
Accept: '*/*', Accept: '*/*',
Connection: 'Keep-Alive' Connection: 'Keep-Alive'
} })
.then((res: any) => {
// appending response to requests array that will be used for requests history reference
this.requestClient!.appendRequest(res, sasJob, config.debug)
resolve(res)
})
.catch((err: any) => {
// by default error string is equal to actual error object
let errString = err
// if error object contains non empty result attribute, set result to errString
if (err.result && err.result !== '') errString = err.result
// if there's no result but error message, set error message to errString
else if (err.message) errString = err.message
// appending error to requests array that will be used for requests history reference
this.requestClient!.appendRequest(errString, sasJob, config.debug)
reject(new ErrorResponse(err?.message, err))
})
) )
.then((res: any) => {
let resString = res
if (typeof res === 'object') { return requestPromise
resString = JSON.stringify(res)
}
this.requestClient!.appendRequest(resString, sasJob, config.debug)
return res
})
.catch((err: any) => {
let errString = err
if (typeof err === 'object') {
errString = JSON.stringify(errString)
}
this.requestClient!.appendRequest(errString, sasJob, config.debug)
return err
})
} }
private getRequestParams(config: any): any { private getRequestParams(config: any): any {

View File

@@ -21,6 +21,7 @@ import {
} from '../utils' } from '../utils'
import { BaseJobExecutor } from './JobExecutor' import { BaseJobExecutor } from './JobExecutor'
import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { parseWeboutResponse } from '../utils/parseWeboutResponse'
import { Server } from 'https'
export interface WaitingRequstPromise { export interface WaitingRequstPromise {
promise: Promise<any> | null promise: Promise<any> | null
@@ -46,7 +47,7 @@ export class WebJobExecutor extends BaseJobExecutor {
authConfig?: AuthConfig, authConfig?: AuthConfig,
extraResponseAttributes: ExtraResponseAttributes[] = [] extraResponseAttributes: ExtraResponseAttributes[] = []
) { ) {
const loginCallback = loginRequiredCallback || (() => Promise.resolve()) const loginCallback = loginRequiredCallback
const program = isRelativePath(sasJob) const program = isRelativePath(sasJob)
? config.appLoc ? config.appLoc
? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '') ? config.appLoc.replace(/\/?$/, '/') + sasJob.replace(/^\//, '')
@@ -79,7 +80,7 @@ export class WebJobExecutor extends BaseJobExecutor {
) )
}) })
await loginCallback() if (loginCallback) await loginCallback()
} else { } else {
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }
@@ -220,6 +221,15 @@ export class WebJobExecutor extends BaseJobExecutor {
} }
if (e instanceof LoginRequiredError) { if (e instanceof LoginRequiredError) {
if (!loginRequiredCallback) {
reject(
new ErrorResponse(
'Request is not authenticated. Make sure .env file exists with valid credentials.',
e
)
)
}
this.appendWaitingRequest(() => { this.appendWaitingRequest(() => {
return this.execute( return this.execute(
sasJob, sasJob,
@@ -238,7 +248,7 @@ export class WebJobExecutor extends BaseJobExecutor {
) )
}) })
await loginCallback() if (loginCallback) await loginCallback()
} else { } else {
reject(new ErrorResponse(e?.message, e)) reject(new ErrorResponse(e?.message, e))
} }

View File

@@ -7,7 +7,8 @@ import {
LoginRequiredError, LoginRequiredError,
NotFoundError, NotFoundError,
InternalServerError, InternalServerError,
JobExecutionError JobExecutionError,
CertificateError
} from '../types/errors' } from '../types/errors'
import { SASjsRequest } from '../types' import { SASjsRequest } from '../types'
import { parseWeboutResponse } from '../utils/parseWeboutResponse' import { parseWeboutResponse } from '../utils/parseWeboutResponse'
@@ -131,6 +132,26 @@ export class RequestClient implements HttpClient {
} else { } else {
sasWork = response.log sasWork = response.log
} }
} else if (response?.result?.log) {
//In this scenario we know we got the response from SASJS server
//Log is array of `{ line: '' }` so we need to convert it back to text
//To be able to parse it with current functions.
let log: string = ''
if (typeof log !== 'string') {
log = response.result.log
.map((logLine: any) => logLine.line)
.join('\n')
}
sourceCode = parseSourceCode(log)
generatedCode = parseGeneratedCode(log)
if (response?.result?._webout) {
sasWork = response.result._webout.WORK
} else {
sasWork = log
}
} else if (response?.result) { } else if (response?.result) {
sourceCode = parseSourceCode(response.result) sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result) generatedCode = parseGeneratedCode(response.result)
@@ -497,6 +518,10 @@ export class RequestClient implements HttpClient {
else return else return
} }
if (e.isAxiosError && e.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
throw new CertificateError(e.message)
}
if (e.message) throw e if (e.message) throw e
else throw prefixMessage(e, 'Error while handling error. ') else throw prefixMessage(e, 'Error while handling error. ')
} }
@@ -557,8 +582,18 @@ export class RequestClient implements HttpClient {
} }
export const throwIfError = (response: AxiosResponse) => { export const throwIfError = (response: AxiosResponse) => {
if (response.status === 401) { switch (response.status) {
throw new LoginRequiredError() case 400:
if (typeof response.data === 'object') {
throw new LoginRequiredError(response.data)
}
break
case 401:
if (typeof response.data === 'object') {
throw new LoginRequiredError(response.data)
} else {
throw new LoginRequiredError()
}
} }
if (response.data?.entityID?.includes('login')) { if (response.data?.entityID?.includes('login')) {

View File

@@ -30,6 +30,11 @@ const ERROR_MESSAGES = {
CCA: 'unable to verify the first certificate' CCA: 'unable to verify the first certificate'
} }
const incorrectAuthCodeErr = {
error: 'unauthorized',
error_description: 'Bad credentials'
}
describe('RequestClient', () => { describe('RequestClient', () => {
let server: http.Server let server: http.Server
@@ -65,7 +70,7 @@ describe('RequestClient', () => {
adapter.getAccessToken('clientId', 'clientSecret', 'incorrect') adapter.getAccessToken('clientId', 'clientSecret', 'incorrect')
).rejects.toEqual( ).rejects.toEqual(
prefixMessage( prefixMessage(
new LoginRequiredError(), new LoginRequiredError(incorrectAuthCodeErr),
'Error while getting access token. ' 'Error while getting access token. '
) )
) )
@@ -246,7 +251,7 @@ describe('RequestClient - Self Signed Server', () => {
adapter.getAccessToken('clientId', 'clientSecret', 'incorrect') adapter.getAccessToken('clientId', 'clientSecret', 'incorrect')
).rejects.toEqual( ).rejects.toEqual(
prefixMessage( prefixMessage(
new LoginRequiredError(), new LoginRequiredError(incorrectAuthCodeErr),
'Error while getting access token. ' 'Error while getting access token. '
) )
) )

View File

@@ -62,7 +62,7 @@ describe('formatDataForRequest', () => {
expect(() => formatDataForRequest(tableWithMissingValues)).toThrow( expect(() => formatDataForRequest(tableWithMissingValues)).toThrow(
new Error( new Error(
'Special missing value can only be a single character from A to Z or _' `A Special missing value can only be a single character from 'A' to 'Z', '_', '.[a-z]', '._'`
) )
) )
}) })

View File

@@ -1,47 +0,0 @@
export interface FileTree {
members: [FolderMember, ServiceMember]
}
export enum MemberType {
folder = 'folder',
service = 'service'
}
export interface FolderMember {
name: string
type: MemberType.folder
members: [FolderMember, ServiceMember]
}
export interface ServiceMember {
name: string
type: MemberType.service
code: string
}
export const isFileTree = (arg: any): arg is FileTree =>
arg &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member)
).length === 0
const isFolderMember = (arg: any): arg is FolderMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.folder &&
arg.members &&
Array.isArray(arg.members) &&
arg.members.filter(
(member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member)
).length === 0
const isServiceMember = (arg: any): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
arg.code &&
typeof arg.code === 'string'

View File

@@ -0,0 +1,12 @@
const instructionsToFix =
'https://github.com/sasjs/cli/issues/1181#issuecomment-1090638584'
export class CertificateError extends Error {
constructor(message: string) {
super(
`${message}\nPlease visit the link below for further information on this issue:\n- ${instructionsToFix}\n`
)
this.name = 'CertificateError'
Object.setPrototypeOf(this, CertificateError.prototype)
}
}

View File

@@ -1,6 +1,10 @@
export class LoginRequiredError extends Error { export class LoginRequiredError extends Error {
constructor() { constructor(details?: any) {
super('Auth error: You must be logged in to access this resource') const message = details
? JSON.stringify(details, null, 2)
: 'You must be logged in to access this resource'
super(`Auth error: ${message}`)
this.name = 'LoginRequiredError' this.name = 'LoginRequiredError'
Object.setPrototypeOf(this, LoginRequiredError.prototype) Object.setPrototypeOf(this, LoginRequiredError.prototype)
} }

View File

@@ -1,13 +1,14 @@
export * from './AuthorizeError' export * from './AuthorizeError'
export * from './CertificateError'
export * from './ComputeJobExecutionError' export * from './ComputeJobExecutionError'
export * from './ErrorResponse'
export * from './InternalServerError' export * from './InternalServerError'
export * from './InvalidJsonError'
export * from './JobExecutionError' export * from './JobExecutionError'
export * from './JobStatePollError' export * from './JobStatePollError'
export * from './LoginRequiredError'
export * from './NotFoundError'
export * from './ErrorResponse'
export * from './NoSessionStateError'
export * from './RootFolderNotFoundError'
export * from './JsonParseArrayError' export * from './JsonParseArrayError'
export * from './LoginRequiredError'
export * from './NoSessionStateError'
export * from './NotFoundError'
export * from './RootFolderNotFoundError'
export * from './WeboutResponseError' export * from './WeboutResponseError'
export * from './InvalidJsonError'

View File

@@ -12,5 +12,4 @@ export * from './Session'
export * from './UploadFile' export * from './UploadFile'
export * from './PollOptions' export * from './PollOptions'
export * from './WriteStream' export * from './WriteStream'
export * from './FileTree'
export * from './ExecuteScript' export * from './ExecuteScript'

View File

@@ -1,3 +1,5 @@
import { isSpecialMissing } from '@sasjs/utils'
/** /**
* Converts the given JSON object array to a CSV string. * Converts the given JSON object array to a CSV string.
* @param data - the array of JSON objects to convert. * @param data - the array of JSON objects to convert.
@@ -18,7 +20,6 @@ export const convertToCSV = (
let headers: string[] = [] let headers: string[] = []
let csvTest let csvTest
let invalidString = false let invalidString = false
const specialMissingValueRegExp = /^[a-z_]{1}$/i
if (formats) { if (formats) {
headers = Object.keys(formats).map((key) => `${key}:${formats![key]}`) headers = Object.keys(formats).map((key) => `${key}:${formats![key]}`)
@@ -36,7 +37,7 @@ export const convertToCSV = (
hasNullOrNumber = true hasNullOrNumber = true
} else if ( } else if (
typeof row[field] === 'string' && typeof row[field] === 'string' &&
specialMissingValueRegExp.test(row[field]) isSpecialMissing(row[field])
) { ) {
hasSpecialMissingString = true hasSpecialMissingString = true
} }
@@ -130,10 +131,9 @@ export const convertToCSV = (
value = currentCell === null ? '' : currentCell value = currentCell === null ? '' : currentCell
if (formats && formats[fieldName] === 'best.') { if (formats && formats[fieldName] === 'best.') {
if (value && !specialMissingValueRegExp.test(value)) { if (value && !isSpecialMissing(value)) {
console.log(`🤖[value]🤖`, value)
throw new Error( throw new Error(
'Special missing value can only be a single character from A to Z or _' `A Special missing value can only be a single character from 'A' to 'Z', '_', '.[a-z]', '._'`
) )
} }

View File

@@ -16,10 +16,15 @@ export const parseSasViyaDebugResponse = async (
requestClient: RequestClient, requestClient: RequestClient,
serverUrl: string serverUrl: string
) => { ) => {
// On viya 3.5, iframe is like <iframe style="width: 99%; height: 500px" src="..."></iframe>
// On viya 4, iframe is like <iframe style="width: 99%; height: 500px; background-color:Canvas;" src=...></iframe>
const iframeStart = response.split( const iframeStart = response.split(
'<iframe style="width: 99%; height: 500px" src="' /<iframe style="width: 99%; height: 500px" src="|<iframe style="width: 99%; height: 500px; background-color:Canvas;" src=/
)[1] )[1]
const jsonUrl = iframeStart ? iframeStart.split('"></iframe>')[0] : null const jsonUrl = iframeStart
? iframeStart.split(/"><\/iframe>|><\/iframe>/)[0]
: null
if (!jsonUrl) { if (!jsonUrl) {
throw new Error('Unable to find webout file URL.') throw new Error('Unable to find webout file URL.')
} }