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

Compare commits

..

1 Commits

Author SHA1 Message Date
a54df1e2cb fix: make ErrorBody interface public
So it could be used as a type in client
2022-03-18 17:12:49 +01:00
26 changed files with 251 additions and 305 deletions

View File

@@ -1,12 +0,0 @@
#!/bin/sh
# Using `--silent` helps for showing any errs in the first line of the response
# The first line is picked up by the VS Code GIT UI popup when rc is not 0
if npm run --silent lint:silent ; then
exit 0
else
npm run --silent lint:fix
echo "❌ Prettier check failed! We ran lint:fix for you. Please add & commit again."
exit 1
fi

14
package-lock.json generated
View File

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

View File

@@ -8,9 +8,8 @@
"build": "rimraf build && rimraf node && mkdir node && copyfiles -u 1 \"./src/**/*\" ./node && webpack && rimraf build/src && rimraf node",
"package:lib": "npm run build && copyfiles ./package.json ./checkNodeVersion.js build && cd build && npm version \"5.0.0\" && npm pack",
"publish:lib": "npm run build && cd build && npm publish",
"lint:fix": "npx prettier --loglevel silent --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --write \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"lint:fix": "npx prettier --write \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --write \"sasjs-tests/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}\" && npx prettier --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"lint:silent": "npx prettier --loglevel silent --check \"src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\" && npx prettier --loglevel silent --check \"sasjs-tests/src/**/*.{ts,tsx,js,jsx,html,css,sass,less,json,yml,md,graphql}\"",
"test": "jest --silent --coverage",
"prepublishOnly": "cp -r ./build/* . && rm -rf ./build",
"postpublish": "git clean -fd",
@@ -73,7 +72,7 @@
},
"main": "index.js",
"dependencies": {
"@sasjs/utils": "2.44.0",
"@sasjs/utils": "2.36.1",
"axios": "0.26.0",
"axios-cookiejar-support": "1.0.1",
"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
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
filename ft15f001 temp lrecl=1000;
filename ft15f001 temp;
parmcards4;
%webout(FETCH)
%webout(OPEN)
@@ -84,15 +84,6 @@ parmcards4;
%webout(CLOSE)
;;;;
%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;
let he who hath understanding, reckon the number of the beast
;;;;
@@ -113,7 +104,7 @@ data _null_;
```sas
filename mc url "https://raw.githubusercontent.com/sasjs/core/main/all.sas";
%inc mc;
filename ft15f001 temp lrecl=1000;
filename ft15f001 temp;
parmcards4;
%webout(FETCH)
%webout(OPEN)
@@ -127,6 +118,7 @@ parmcards4;
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=sendObj)
filename ft15f001 temp;
parmcards4;
%webout(FETCH)
%webout(OPEN)
@@ -140,15 +132,7 @@ parmcards4;
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=sendArr)
parmcards4;
data work.macvars;
set sashelp.vmacro;
run;
%webout(OPEN)
%webout(OBJ,macvars)
%webout(CLOSE)
;;;;
%mp_createwebservice(path=/Public/app/common,name=sendMacVars)
filename ft15f001 temp;
parmcards4;
If you can keep your head when all about you
Are losing theirs and blaming it on you,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,24 @@
import { AuthConfig, ServerType, ServicePackSASjs } from '@sasjs/utils/types'
import { ExecutionQuery } from './types'
import { AuthConfig, ServerType } from '@sasjs/utils/types'
import { FileTree, ExecutionQuery } from './types'
import { RequestClient } from './request/RequestClient'
import { getAccessTokenForSasjs } from './auth/getAccessTokenForSasjs'
import { refreshTokensForSasjs } from './auth/refreshTokensForSasjs'
import { getAuthCodeForSasjs } from './auth/getAuthCodeForSasjs'
import { parseWeboutResponse } from './utils'
import { getTokens } from './auth/getTokens'
export class SASjsApiClient {
constructor(private requestClient: RequestClient) {}
constructor(
private serverUrl: string,
private requestClient: RequestClient
) {}
public setConfig(serverUrl: string) {
if (serverUrl) this.serverUrl = serverUrl
}
public async deploy(
dataJson: ServicePackSASjs,
members: FileTree,
appLoc: string,
authConfig?: AuthConfig
) {
@@ -22,17 +30,13 @@ export class SASjsApiClient {
ServerType.Sasjs
))
}
dataJson.appLoc = dataJson.appLoc || appLoc
const { result } = await this.requestClient.post<{
status: string
message: string
streamServiceName?: string
example?: {}
}>(
'SASjsApi/drive/deploy',
dataJson,
{ fileTree: members, appLoc: appLoc },
access_token,
undefined,
{},
@@ -42,9 +46,7 @@ export class SASjsApiClient {
return Promise.resolve(result)
}
public async executeJob(query: ExecutionQuery, authConfig?: AuthConfig) {
const access_token = authConfig ? authConfig.access_token : undefined
public async executeJob(query: ExecutionQuery) {
const { result } = await this.requestClient.post<{
status: string
message: string
@@ -52,7 +54,7 @@ export class SASjsApiClient {
logPath?: string
error?: {}
_webout?: string
}>('SASjsApi/stp/execute', query, access_token)
}>('SASjsApi/stp/execute', query, undefined)
if (Object.keys(result).includes('_webout')) {
result._webout = parseWeboutResponse(result._webout!)
@@ -61,39 +63,6 @@ export class SASjsApiClient {
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.
* @param clientId - the client ID to authenticate with.
@@ -113,6 +82,20 @@ export class SASjsApiClient {
public async refreshTokens(refreshToken: string): Promise<SASjsAuthResponse> {
return refreshTokensForSasjs(this.requestClient, refreshToken)
}
/**
* Performs a login authenticate and returns an auth code for the given client.
* @param username - a string representing the username.
* @param password - a string representing the password.
* @param clientId - the client ID to authenticate with.
*/
public async getAuthCode(
username: string,
password: string,
clientId: string
) {
return getAuthCodeForSasjs(this.requestClient, username, password, clientId)
}
}
// todo move to sasjs/utils

View File

@@ -2,6 +2,8 @@ import { ServerType } from '@sasjs/utils/types'
import { RequestClient } from '../request/RequestClient'
import { LoginOptions, LoginResult } from '../types/Login'
import { serialize } from '../utils'
import { getAccessTokenForSasjs } from './getAccessTokenForSasjs'
import { getAuthCodeForSasjs } from './getAuthCodeForSasjs'
import { openWebPage } from './openWebPage'
import { verifySas9Login } from './verifySas9Login'
import { verifySasViyaLogin } from './verifySasViyaLogin'
@@ -81,6 +83,39 @@ export class AuthManager {
return { isLoggedIn: false, userName: '' }
}
/**
* Logs into the SAS server with the supplied credentials.
* @param userName - a string representing the username.
* @param password - a string representing the password.
* @param clientId - a string representing the client ID.
* @returns - a boolean `isLoggedin` and a string `username`
*/
public async logInSasjs(
username: string,
password: string,
clientId: string
): Promise<LoginResult> {
const isLoggedIn = await this.sendLoginRequestSasjs(
username,
password,
clientId
)
.then((res) => {
this.userName = username
this.requestClient.saveLocalStorageToken(
res.access_token,
res.refresh_token
)
return true
})
.catch(() => false)
return {
isLoggedIn,
userName: this.userName
}
}
/**
* Logs into the SAS server with the supplied credentials.
* @param username - a string representing the username.
@@ -117,7 +152,7 @@ export class AuthManager {
let loginResponse = await this.sendLoginRequest(loginForm, loginParams)
let isLoggedIn = isLogInSuccess(this.serverType, loginResponse)
let isLoggedIn = isLogInSuccess(loginResponse)
if (!isLoggedIn) {
if (isCredentialsVerifyError(loginResponse)) {
@@ -161,17 +196,6 @@ export class AuthManager {
loginForm: { [key: string]: any },
loginParams: { [key: string]: any }
) {
if (this.serverType === ServerType.Sasjs) {
const { username, password } = loginParams
const { result: loginResponse } = await this.requestClient.post<string>(
this.loginUrl,
{ username, password },
undefined
)
return loginResponse
}
for (const key in loginForm) {
loginParams[key] = loginForm[key]
}
@@ -191,6 +215,19 @@ export class AuthManager {
return loginResponse
}
private async sendLoginRequestSasjs(
username: string,
password: string,
clientId: string
) {
const authCode = await getAuthCodeForSasjs(
this.requestClient,
username,
password,
clientId
)
return getAccessTokenForSasjs(this.requestClient, clientId, authCode)
}
/**
* Checks whether a session is active, or login is required.
* @returns - a promise which resolves with an object containing three values
@@ -211,7 +248,8 @@ export class AuthManager {
//Residue can happen in case of session expiration
await this.logOut()
loginForm = await this.getNewLoginForm()
if (this.serverType !== ServerType.Sasjs)
loginForm = await this.getNewLoginForm()
}
return Promise.resolve({
@@ -222,12 +260,6 @@ export class AuthManager {
}
private async getNewLoginForm() {
if (this.serverType === ServerType.Sasjs) {
// server will be sending CSRF cookie,
// http client will use it automatically
return this.requestClient.get('/', undefined)
}
const { result: formResponse } = await this.requestClient.get<string>(
this.loginUrl.replace('.do', ''),
undefined,
@@ -352,8 +384,5 @@ const isCredentialsVerifyError = (response: string): boolean =>
response
)
const isLogInSuccess = (serverType: ServerType, response: any): boolean => {
if (serverType === ServerType.Sasjs) return response?.loggedin
return /You have signed in/gm.test(response)
}
const isLogInSuccess = (response: string): boolean =>
/You have signed in/gm.test(response)

View File

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

View File

@@ -0,0 +1,31 @@
import { prefixMessage } from '@sasjs/utils/error'
import { RequestClient } from '../request/RequestClient'
/**
* Performs a login authenticate and returns an auth code for the given client.
* @param requestClient - the pre-configured HTTP request client
* @param username - a string representing the username.
* @param password - a string representing the password.
* @param clientId - the client ID to authenticate with.
*/
export const getAuthCodeForSasjs = async (
requestClient: RequestClient,
username: string,
password: string,
clientId: string
) => {
const url = '/SASjsApi/auth/authorize'
const data = { username, password, clientId }
const { code: authCode } = await requestClient
.post<{ code: string }>(url, data, undefined)
.then((res) => res.result)
.catch((err) => {
throw prefixMessage(
err,
'Error while authenticating with provided username, password and clientId. '
)
})
return authCode
}

View File

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

View File

@@ -99,20 +99,7 @@ export class FileUploader extends BaseJobExecutor {
? parseWeboutResponse(res.result, uploadUrl)
: res.result
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 {
jsonResponse =
typeof res.result === 'string'

View File

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

View File

@@ -7,8 +7,7 @@ import {
LoginRequiredError,
NotFoundError,
InternalServerError,
JobExecutionError,
CertificateError
JobExecutionError
} from '../types/errors'
import { SASjsRequest } from '../types'
import { parseWeboutResponse } from '../utils/parseWeboutResponse'
@@ -132,26 +131,6 @@ export class RequestClient implements HttpClient {
} else {
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) {
sourceCode = parseSourceCode(response.result)
generatedCode = parseGeneratedCode(response.result)
@@ -518,10 +497,6 @@ export class RequestClient implements HttpClient {
else return
}
if (e.isAxiosError && e.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
throw new CertificateError(e.message)
}
if (e.message) throw e
else throw prefixMessage(e, 'Error while handling error. ')
}

View File

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

47
src/types/FileTree.ts Normal file
View File

@@ -0,0 +1,47 @@
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

@@ -1,12 +0,0 @@
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

@@ -21,7 +21,7 @@ export class ErrorResponse {
}
}
interface ErrorBody {
export interface ErrorBody {
message: string
details: string
raw: any

View File

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

View File

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

View File

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

View File

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