1
0
mirror of https://github.com/sasjs/server.git synced 2025-12-10 19:34:34 +00:00

Compare commits

...

38 Commits

Author SHA1 Message Date
semantic-release-bot
70655e74d3 chore(release): 0.19.0 [skip ci]
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)

### Features

* added mocking endpoints ([0a0ba2c](0a0ba2cca5))
2022-09-05 12:21:34 +00:00
Allan Bowe
cb82fea0d8 Merge pull request #264 from sasjs/mocker
Mocker
2022-09-05 13:16:10 +01:00
b9a596616d chore: cleanup 2022-09-05 12:20:56 +02:00
semantic-release-bot
72a5393be3 chore(release): 0.18.0 [skip ci]
# [0.18.0](https://github.com/sasjs/server/compare/v0.17.5...v0.18.0) (2022-09-02)

### Features

* add option for program launch in context menu ([ee2db27](ee2db276bb))
2022-09-02 19:26:49 +00:00
Allan Bowe
769a840e9f Merge pull request #273 from sasjs/issue-270
feat: add option for program launch in context menu
2022-09-02 20:23:02 +01:00
730c7c52ac chore: remove commented code 2022-09-03 00:09:48 +05:00
ee2db276bb feat: add option for program launch in context menu 2022-09-02 23:40:02 +05:00
semantic-release-bot
d0a24aacb6 chore(release): 0.17.5 [skip ci]
## [0.17.5](https://github.com/sasjs/server/compare/v0.17.4...v0.17.5) (2022-09-02)

### Bug Fixes

* SASINITIALFOLDER split over 2 params, closes [#271](https://github.com/sasjs/server/issues/271) ([393b5ea](393b5eaf99))
2022-09-02 18:08:49 +00:00
Allan Bowe
57dfdf89a4 Merge pull request #272 from sasjs/allanbowe/session-crashed-since-271
fix: SASINITIALFOLDER split over 2 params, closes #271
2022-09-02 19:03:37 +01:00
Allan Bowe
393b5eaf99 fix: SASINITIALFOLDER split over 2 params, closes #271 2022-09-02 17:59:10 +00:00
Saad Jutt
7477326b22 chore: lower cased env values 2022-09-01 23:38:04 +05:00
Saad Jutt
76bf84316e chore: MOCK_SERVERTYPE instead of string literals 2022-09-01 23:34:57 +05:00
semantic-release-bot
e355276e44 chore(release): 0.17.4 [skip ci]
## [0.17.4](https://github.com/sasjs/server/compare/v0.17.3...v0.17.4) (2022-09-01)

### Bug Fixes

* invalid JS logic ([9f06080](9f06080348))
2022-09-01 12:50:14 +00:00
Allan Bowe
a3a9e3bd9f Merge pull request #269 from sasjs/allanbowe/error-unrecognized-sas-267
fix: invalid JS logic
2022-09-01 13:41:52 +01:00
Allan Bowe
9f06080348 fix: invalid JS logic 2022-09-01 12:35:58 +00:00
semantic-release-bot
4bbf9cfdb3 chore(release): 0.17.3 [skip ci]
## [0.17.3](https://github.com/sasjs/server/compare/v0.17.2...v0.17.3) (2022-09-01)

### Bug Fixes

* making SASINITIALFOLDER option windows only.  Closes [#267](https://github.com/sasjs/server/issues/267) ([e63271a](e63271a67a))
2022-09-01 12:25:33 +00:00
Allan Bowe
e8e71fcde9 Merge pull request #268 from sasjs/allanbowe/error-unrecognized-sas-267
fix: making SASINITIALFOLDER option windows only.  Closes #267
2022-09-01 13:21:25 +01:00
Allan Bowe
e63271a67a fix: making SASINITIALFOLDER option windows only. Closes #267 2022-09-01 12:18:53 +00:00
7633608318 chore: mocker architecture fix, env validation 2022-08-31 13:31:28 +02:00
semantic-release-bot
e67d27d264 chore(release): 0.17.2 [skip ci]
## [0.17.2](https://github.com/sasjs/server/compare/v0.17.1...v0.17.2) (2022-08-31)

### Bug Fixes

* addition of SASINITIALFOLDER startup option.  Closes [#260](https://github.com/sasjs/server/issues/260) ([a5ee2f2](a5ee2f2923))
2022-08-31 09:35:51 +00:00
Allan Bowe
53033ccc96 Merge pull request #262 from sasjs/allanbowe/sas-default-folder-should-260
fix: addition of SASINITIALFOLDER startup option.  Closes #260
2022-08-31 10:32:14 +01:00
semantic-release-bot
6131ed1cbe chore(release): 0.17.1 [skip ci]
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)

### Bug Fixes

* typo mistake ([ee17d37](ee17d37aa1))
2022-08-30 18:15:33 +00:00
Allan Bowe
5d624e3399 Merge pull request #266 from sasjs/issue-265
fix: typo mistake
2022-08-30 19:10:47 +01:00
ee17d37aa1 fix: typo mistake 2022-08-30 22:42:11 +05:00
572fe22d50 chore: mocksas9 controller 2022-08-30 17:27:37 +02:00
091268bf58 chore: mocking only mandatory bits from sas9 responses 2022-08-29 12:40:29 +02:00
71a4a48443 chore: generic sas9 mock responses 2022-08-29 10:30:01 +02:00
3b188cd724 style: lint 2022-08-26 18:03:28 +02:00
eeba2328c0 chore: added login, logout endpoints 2022-08-26 17:59:07 +02:00
0a0ba2cca5 feat: added mocking endpoints 2022-08-25 15:58:08 +02:00
semantic-release-bot
476f834a80 chore(release): 0.17.0 [skip ci]
# [0.17.0](https://github.com/sasjs/server/compare/v0.16.1...v0.17.0) (2022-08-25)

### Bug Fixes

* allow underscores in file name ([bce83cb](bce83cb6fb))

### Features

* add the functionality of saving file by ctrl + s in editor ([3a3c90d](3a3c90d9e6))
2022-08-25 09:47:48 +00:00
Sabir Hassan
8b8739a873 Merge pull request #263 from sasjs/ctrl-save
feat: add the functionality of saving file by ctrl + s in editor
2022-08-25 14:43:50 +05:00
bce83cb6fb fix: allow underscores in file name 2022-08-25 14:27:42 +05:00
3a3c90d9e6 feat: add the functionality of saving file by ctrl + s in editor 2022-08-25 14:12:51 +05:00
semantic-release-bot
e63eaa5302 chore(release): 0.16.1 [skip ci]
## [0.16.1](https://github.com/sasjs/server/compare/v0.16.0...v0.16.1) (2022-08-24)

### Bug Fixes

* update response of /SASjsApi/stp/execute and /SASjsApi/code/execute ([98ea2ac](98ea2ac9b9))
2022-08-24 16:07:11 +00:00
Sabir Hassan
65de1bb175 Merge pull request #261 from sasjs/issue-258
fix: update response of /SASjsApi/stp/execute and /SASjsApi/code/execute
2022-08-24 21:02:57 +05:00
Allan Bowe
a5ee2f2923 fix: addition of SASINITIALFOLDER startup option. Closes #260 2022-08-19 15:20:36 +00:00
98ea2ac9b9 fix: update response of /SASjsApi/stp/execute and /SASjsApi/code/execute 2022-08-19 15:06:39 +05:00
26 changed files with 2436 additions and 2197 deletions

2
.gitignore vendored
View File

@@ -5,6 +5,8 @@ node_modules/
.env*
sas/
sasjs_root/
api/mocks/custom/*
!api/mocks/custom/.keep
tmp/
build/
sasjsbuild/

View File

@@ -1,3 +1,71 @@
# [0.19.0](https://github.com/sasjs/server/compare/v0.18.0...v0.19.0) (2022-09-05)
### Features
* added mocking endpoints ([0a0ba2c](https://github.com/sasjs/server/commit/0a0ba2cca5db867de46fb2486d856a84ec68d3b4))
# [0.18.0](https://github.com/sasjs/server/compare/v0.17.5...v0.18.0) (2022-09-02)
### Features
* add option for program launch in context menu ([ee2db27](https://github.com/sasjs/server/commit/ee2db276bb0bbd522f758e0b66f7e7b2f4afd9d5))
## [0.17.5](https://github.com/sasjs/server/compare/v0.17.4...v0.17.5) (2022-09-02)
### Bug Fixes
* SASINITIALFOLDER split over 2 params, closes [#271](https://github.com/sasjs/server/issues/271) ([393b5ea](https://github.com/sasjs/server/commit/393b5eaf990049c39eecf2b9e8dd21a001b6e298))
## [0.17.4](https://github.com/sasjs/server/compare/v0.17.3...v0.17.4) (2022-09-01)
### Bug Fixes
* invalid JS logic ([9f06080](https://github.com/sasjs/server/commit/9f06080348aed076f8188a26fb4890d38a5a3510))
## [0.17.3](https://github.com/sasjs/server/compare/v0.17.2...v0.17.3) (2022-09-01)
### Bug Fixes
* making SASINITIALFOLDER option windows only. Closes [#267](https://github.com/sasjs/server/issues/267) ([e63271a](https://github.com/sasjs/server/commit/e63271a67a0deb3059a5f2bec1854efee5a6e5a5))
## [0.17.2](https://github.com/sasjs/server/compare/v0.17.1...v0.17.2) (2022-08-31)
### Bug Fixes
* addition of SASINITIALFOLDER startup option. Closes [#260](https://github.com/sasjs/server/issues/260) ([a5ee2f2](https://github.com/sasjs/server/commit/a5ee2f292384f90e9d95d003d652311c0d91a7a7))
## [0.17.1](https://github.com/sasjs/server/compare/v0.17.0...v0.17.1) (2022-08-30)
### Bug Fixes
* typo mistake ([ee17d37](https://github.com/sasjs/server/commit/ee17d37aa188b0ca43cea0e89d6cd1a566b765cb))
# [0.17.0](https://github.com/sasjs/server/compare/v0.16.1...v0.17.0) (2022-08-25)
### Bug Fixes
* allow underscores in file name ([bce83cb](https://github.com/sasjs/server/commit/bce83cb6fbc98f8198564c9399821f5829acc767))
### Features
* add the functionality of saving file by ctrl + s in editor ([3a3c90d](https://github.com/sasjs/server/commit/3a3c90d9e690ac5267bf1acc834b5b5c5b4dadb6))
## [0.16.1](https://github.com/sasjs/server/compare/v0.16.0...v0.16.1) (2022-08-24)
### Bug Fixes
* update response of /SASjsApi/stp/execute and /SASjsApi/code/execute ([98ea2ac](https://github.com/sasjs/server/commit/98ea2ac9b98631605e39e5900e533727ea0e3d85))
# [0.16.0](https://github.com/sasjs/server/compare/v0.15.3...v0.16.0) (2022-08-17)

View File

@@ -96,6 +96,9 @@ PROTOCOL=
# default: 5000
PORT=
# options: [sas9|sasviya]
# If not present, mocking function is disabled
MOCK_SERVERTYPE=
#
## Additional SAS Options

0
api/mocks/custom/.keep Normal file
View File

View File

@@ -0,0 +1 @@
You have signed in.

View File

@@ -0,0 +1 @@
You have signed out.

View File

@@ -0,0 +1,30 @@
<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" dir="ltr" class="bg">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="initial-scale=1" />
</head>
<div class="content">
<form id="credentials" class="minimal" action="/SASLogon/login?service=http%3A%2F%2Flocalhost:5004%2FSASStoredProcess%2Fj_spring_cas_security_check" method="post">
<!--form container-->
<input type="hidden" name="lt" value="LT-8-WGkt9EXwICBihaVbxGc92opjufTK1D" aria-hidden="true" />
<input type="hidden" name="execution" value="e2s1" aria-hidden="true" />
<input type="hidden" name="_eventId" value="submit" aria-hidden="true" />
<span class="userid">
<input id="username" name="username" tabindex="3" aria-labelledby="username1 message1 message2 message3" name="username" placeholder="User ID" type="text" autofocus="true" value="" maxlength="500" autocomplete="off" />
</span>
<span class="password">
<input id="password" name="password" tabindex="4" name="password" placeholder="Password" type="password" value="" maxlength="500" autocomplete="off" />
</span>
<button type="submit" class="btn-submit" title="Sign In" tabindex="5" onClick="this.disabled=true;setSubmitUrl(this.form);this.form.submit();return false;">Sign In</button>
</form>
</div>
</html>

View File

@@ -0,0 +1 @@
"title": "Log Off SAS Demo User"

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,6 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecuteReturnJson, ExecutionController } from './internal'
import { ExecuteReturnJsonResponse } from '.'
import { ExecutionController } from './internal'
import {
getPreProgramVariables,
getUserAutoExec,
@@ -35,7 +34,7 @@ export class CodeController {
public async executeCode(
@Request() request: express.Request,
@Body() body: ExecuteCodePayload
): Promise<ExecuteReturnJsonResponse> {
): Promise<string | Buffer> {
return executeCode(request, body)
}
}
@@ -51,22 +50,15 @@ const executeCode = async (
: await getUserAutoExec()
try {
const { webout, log, httpHeaders } =
(await new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
returnJson: true,
runTime: runTime
})) as ExecuteReturnJson
const { result } = await new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime
})
return {
status: 'success',
_webout: webout as string,
log: parseLogToArray(log),
httpHeaders
}
return result
} catch (err: any) {
throw {
code: 400,

View File

@@ -20,12 +20,6 @@ export interface ExecuteReturnRaw {
result: string | Buffer
}
export interface ExecuteReturnJson {
httpHeaders: HTTPHeaders
webout: string | Buffer
log?: string
}
interface ExecuteFileParams {
programPath: string
preProgramVariables: PreProgramVars
@@ -68,10 +62,9 @@ export class ExecutionController {
preProgramVariables,
vars,
otherArgs,
returnJson,
session: sessionByFileUpload,
runTime
}: ExecuteProgramParams): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
const sessionController = getSessionController(runTime)
const session =
@@ -89,8 +82,6 @@ export class ExecutionController {
tokenFile,
preProgramVariables?.httpHeaders.join('\n') ?? ''
)
if (returnJson)
await createFile(headersPath, 'Content-type: application/json')
await processProgram(
program,
@@ -110,10 +101,7 @@ export class ExecutionController {
? await readFile(headersPath)
: ''
const httpHeaders: HTTPHeaders = extractHeaders(headersContent)
const fileResponse: boolean =
httpHeaders.hasOwnProperty('content-type') &&
!returnJson && // not a POST Request
!isDebugOn(vars) // Debug is not enabled
const fileResponse: boolean = httpHeaders.hasOwnProperty('content-type')
const webout = (await fileExists(weboutPath))
? fileResponse
@@ -124,19 +112,11 @@ export class ExecutionController {
// it should be deleted by scheduleSessionDestroy
session.inUse = false
if (returnJson) {
return {
httpHeaders,
webout,
log: isDebugOn(vars) || session.crashed ? log : undefined
}
}
return {
httpHeaders,
result:
isDebugOn(vars) || session.crashed
? `<html><body>${webout}<div style="text-align:left"><hr /><h2>SAS Log</h2><pre>${log}</pre></div></body></html>`
? `${webout}\n${process.logsUUID}\n${log}`
: webout
}
}

View File

@@ -101,12 +101,14 @@ ${autoExecContent}`
session.path,
'-AUTOEXEC',
autoExecPath,
isWindows() ? '-nologo' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nosplash' : '',
process.sasLoc!.endsWith('sas.exe') ? '-icon' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nodms' : '',
process.sasLoc!.endsWith('sas.exe') ? '-noterminal' : '',
process.sasLoc!.endsWith('sas.exe') ? '-nostatuswin' : '',
isWindows() ? '-nologo' : ''
process.sasLoc!.endsWith('sas.exe') ? '-SASINITIALFOLDER' : '',
process.sasLoc!.endsWith('sas.exe') ? session.path : ''
])
.then(() => {
session.completed = true

View File

@@ -0,0 +1,143 @@
import { readFile } from '@sasjs/utils'
import express from 'express'
import path from 'path'
import { Request, Post, Get } from 'tsoa'
export interface Sas9Response {
content: string
redirect?: string
error?: boolean
}
export interface MockFileRead {
content: string
error?: boolean
}
export class MockSas9Controller {
private loggedIn: boolean = false
@Get('/SASStoredProcess')
public async sasStoredProcess(): Promise<Sas9Response> {
if (!this.loggedIn) {
return {
content: '',
redirect: '/SASLogon/login'
}
}
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'generic',
'sas9',
'sas-stored-process'
])
}
@Post('/SASStoredProcess/do/')
public async sasStoredProcessDo(
@Request() req: express.Request
): Promise<Sas9Response> {
if (!this.loggedIn) {
return {
content: '',
redirect: '/SASLogon/login'
}
}
let program = req.query._program?.toString() || ''
program = program.replace('/', '')
const content = await getMockResponseFromFile([
process.cwd(),
'mocks',
...program.split('/')
])
if (content.error) {
return content
}
const parsedContent = parseJsonIfValid(content.content)
return {
content: parsedContent
}
}
@Get('/SASLogon/login')
public async loginGet(): Promise<Sas9Response> {
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'generic',
'sas9',
'login'
])
}
@Post('/SASLogon/login')
public async loginPost(): Promise<Sas9Response> {
this.loggedIn = true
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'generic',
'sas9',
'logged-in'
])
}
@Get('/SASLogon/logout')
public async logout(): Promise<Sas9Response> {
this.loggedIn = false
return await getMockResponseFromFile([
process.cwd(),
'mocks',
'generic',
'sas9',
'logged-out'
])
}
}
/**
* If JSON is valid it will be parsed otherwise will return text unaltered
* @param content string to be parsed
* @returns JSON or string
*/
const parseJsonIfValid = (content: string) => {
let fileContent = ''
try {
fileContent = JSON.parse(content)
} catch (err: any) {
fileContent = content
}
return fileContent
}
const getMockResponseFromFile = async (
filePath: string[]
): Promise<MockFileRead> => {
const filePathParsed = path.join(...filePath)
let error: boolean = false
let file = await readFile(filePathParsed).catch((err: any) => {
const errMsg = `Error reading mocked file on path: ${filePathParsed}\nError: ${err}`
console.error(errMsg)
error = true
return errMsg
})
return {
content: file,
error: error
}
}

View File

@@ -1,33 +1,16 @@
import express from 'express'
import {
Request,
Security,
Route,
Tags,
Post,
Body,
Get,
Query,
Example
} from 'tsoa'
import {
ExecuteReturnJson,
ExecuteReturnRaw,
ExecutionController,
ExecutionVars
} from './internal'
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
import { ExecutionController, ExecutionVars } from './internal'
import {
getPreProgramVariables,
HTTPHeaders,
isDebugOn,
LogLine,
makeFilesNamesMap,
parseLogToArray,
getRunTimeAndFilePath
} from '../utils'
import { MulterFile } from '../types/Upload'
interface ExecuteReturnJsonPayload {
interface ExecutePostRequestPayload {
/**
* Location of SAS program
* @example "/Public/somefolder/some.file"
@@ -35,17 +18,6 @@ interface ExecuteReturnJsonPayload {
_program?: string
}
interface IRecordOfAny {
[key: string]: any
}
export interface ExecuteReturnJsonResponse {
status: string
_webout: string | IRecordOfAny
log: LogLine[]
message?: string
httpHeaders: HTTPHeaders
}
@Security('bearerAuth')
@Route('SASjsApi/stp')
@Tags('STP')
@@ -62,11 +34,12 @@ export class STPController {
* @example _program "/Projects/myApp/some/program"
*/
@Get('/execute')
public async executeReturnRaw(
public async executeGetRequest(
@Request() request: express.Request,
@Query() _program: string
): Promise<string | Buffer> {
return executeReturnRaw(request, _program)
const vars = request.query as ExecutionVars
return execute(request, _program, vars)
}
/**
@@ -87,50 +60,42 @@ export class STPController {
* @param _program Location of SAS or JS code
* @example _program "/Projects/myApp/some/program"
*/
@Example<ExecuteReturnJsonResponse>({
status: 'success',
_webout: 'webout content',
log: [],
httpHeaders: {
'Content-type': 'application/zip',
'Cache-Control': 'public, max-age=1000'
}
})
@Post('/execute')
public async executeReturnJson(
public async executePostRequest(
@Request() request: express.Request,
@Body() body?: ExecuteReturnJsonPayload,
@Body() body?: ExecutePostRequestPayload,
@Query() _program?: string
): Promise<ExecuteReturnJsonResponse> {
): Promise<string | Buffer> {
const program = _program ?? body?._program
return executeReturnJson(request, program!)
const vars = { ...request.query, ...request.body }
const filesNamesMap = request.files?.length
? makeFilesNamesMap(request.files as MulterFile[])
: null
const otherArgs = { filesNamesMap: filesNamesMap }
return execute(request, program!, vars, otherArgs)
}
}
const executeReturnRaw = async (
const execute = async (
req: express.Request,
_program: string
_program: string,
vars: ExecutionVars,
otherArgs?: any
): Promise<string | Buffer> => {
const query = req.query as ExecutionVars
try {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { result, httpHeaders } =
(await new ExecutionController().executeFile({
const { result, httpHeaders } = await new ExecutionController().executeFile(
{
programPath: codePath,
runTime,
preProgramVariables: getPreProgramVariables(req),
vars: query,
runTime
})) as ExecuteReturnRaw
// Should over-ride response header for debug
// on GET request to see entire log rendering on browser.
if (isDebugOn(query)) {
httpHeaders['content-type'] = 'text/plain'
}
req.res?.set(httpHeaders)
vars,
otherArgs,
session: req.sasjsSession
}
)
if (result instanceof Buffer) {
;(req as any).sasHeaders = httpHeaders
@@ -146,48 +111,3 @@ const executeReturnRaw = async (
}
}
}
const executeReturnJson = async (
req: express.Request,
_program: string
): Promise<ExecuteReturnJsonResponse> => {
const filesNamesMap = req.files?.length
? makeFilesNamesMap(req.files as MulterFile[])
: null
try {
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
const { webout, log, httpHeaders } =
(await new ExecutionController().executeFile({
programPath: codePath,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, ...req.body },
otherArgs: { filesNamesMap: filesNamesMap },
returnJson: true,
session: req.sasjsSession,
runTime
})) as ExecuteReturnJson
let weboutRes: string | IRecordOfAny = webout
if (httpHeaders['content-type']?.toLowerCase() === 'application/json') {
try {
weboutRes = JSON.parse(webout as string)
} catch (_) {}
}
return {
status: 'success',
_webout: weboutRes,
log: parseLogToArray(log),
httpHeaders
}
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -13,7 +13,7 @@ stpRouter.get('/execute', async (req, res) => {
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.executeReturnRaw(req, query._program)
const response = await controller.executeGetRequest(req, query._program)
if (response instanceof Buffer) {
res.writeHead(200, (req as any).sasHeaders)
@@ -42,7 +42,7 @@ stpRouter.post(
// if (errQ && errB) return res.status(400).send(errB.details[0].message)
try {
const response = await controller.executeReturnJson(
const response = await controller.executePostRequest(
req,
req.body,
req.query?._program as string

View File

@@ -1,8 +1,25 @@
import express from 'express'
import sas9WebRouter from './sas9-web'
import sasViyaWebRouter from './sasviya-web'
import webRouter from './web'
import { MOCK_SERVERTYPEType } from '../../utils'
const router = express.Router()
router.use('/', webRouter)
const { MOCK_SERVERTYPE } = process.env
switch (MOCK_SERVERTYPE) {
case MOCK_SERVERTYPEType.SAS9: {
router.use('/', sas9WebRouter)
break
}
case MOCK_SERVERTYPEType.SASVIYA: {
router.use('/', sasViyaWebRouter)
break
}
default: {
router.use('/', webRouter)
}
}
export default router

View File

@@ -0,0 +1,88 @@
import express from 'express'
import { WebController } from '../../controllers'
import { MockSas9Controller } from '../../controllers/mock-sas9'
const sas9WebRouter = express.Router()
const webController = new WebController()
// Mock controller must be singleton because it keeps the states
// for example `isLoggedIn` and potentially more in future mocks
const controller = new MockSas9Controller()
sas9WebRouter.get('/', async (req, res) => {
let response
try {
response = await webController.home()
} catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>'
} finally {
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
)
return res.send(injectedContent)
}
})
sas9WebRouter.get('/SASStoredProcess', async (req, res) => {
const response = await controller.sasStoredProcess()
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.post('/SASStoredProcess/do/', async (req, res) => {
const response = await controller.sasStoredProcessDo(req)
if (response.redirect) {
res.redirect(response.redirect)
return
}
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.get('/SASLogon/login', async (req, res) => {
const response = await controller.loginGet()
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.post('/SASLogon/login', async (req, res) => {
const response = await controller.loginPost()
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
sas9WebRouter.get('/SASLogon/logout', async (req, res) => {
const response = await controller.logout()
try {
res.send(response.content)
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default sas9WebRouter

View File

@@ -0,0 +1,32 @@
import express from 'express'
import { WebController } from '../../controllers/web'
const sasViyaWebRouter = express.Router()
const controller = new WebController()
sasViyaWebRouter.get('/', async (req, res) => {
let response
try {
response = await controller.home()
} catch (_) {
response = '<html><head></head><body>Web Build is not present</body></html>'
} finally {
const codeToInject = `<script>document.cookie = 'XSRF-TOKEN=${req.csrfToken()}; Max-Age=86400; SameSite=Strict; Path=/;'</script>`
const injectedContent = response?.replace(
'</head>',
`${codeToInject}</head>`
)
return res.send(injectedContent)
}
})
sasViyaWebRouter.post('/SASJobExecution/', async (req, res) => {
try {
res.send({ test: 'test' })
} catch (err: any) {
res.status(403).send(err.toString())
}
})
export default sasViyaWebRouter

View File

@@ -5,6 +5,7 @@ declare namespace NodeJS {
pythonLoc?: string
driveLoc: string
logsLoc: string
logsUUID: string
sasSessionController?: import('../../controllers/internal').SASSessionController
jsSessionController?: import('../../controllers/internal').JSSessionController
pythonSessionController?: import('../../controllers/internal').PythonSessionController

View File

@@ -16,7 +16,7 @@ export const getDesktopFields = async () => {
nodeLoc = NODE_PATH ?? (await getNodeLocation())
}
if (process.runTimes.includes(RunTimeType.JS)) {
if (process.runTimes.includes(RunTimeType.PY)) {
pythonLoc = PYTHON_PATH ?? (await getPythonLocation())
}

View File

@@ -50,6 +50,8 @@ export const setProcessVariables = async () => {
await createFolder(absLogsPath)
process.logsLoc = getRealPath(absLogsPath)
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc)
console.log('sasLogs: ', process.logsLoc)

View File

@@ -1,3 +1,8 @@
export enum MOCK_SERVERTYPEType {
SAS9 = 'sas9',
SASVIYA = 'sasviya'
}
export enum ModeType {
Server = 'server',
Desktop = 'desktop'
@@ -40,6 +45,8 @@ export enum ReturnCode {
export const verifyEnvVariables = (): ReturnCode => {
const errors: string[] = []
errors.push(...verifyMOCK_SERVERTYPE())
errors.push(...verifyMODE())
errors.push(...verifyPROTOCOL())
@@ -66,6 +73,23 @@ export const verifyEnvVariables = (): ReturnCode => {
return ReturnCode.Success
}
const verifyMOCK_SERVERTYPE = (): string[] => {
const errors: string[] = []
const { MOCK_SERVERTYPE } = process.env
if (MOCK_SERVERTYPE) {
const modeTypes = Object.values(MOCK_SERVERTYPEType)
if (!modeTypes.includes(MOCK_SERVERTYPE as MOCK_SERVERTYPEType))
errors.push(
`- MOCK_SERVERTYPE '${MOCK_SERVERTYPE}'\n - valid options ${modeTypes}`
)
} else {
process.env.MOCK_SERVERTYPE = undefined
}
return errors
}
const verifyMODE = (): string[] => {
const errors: string[] = []
const { MODE } = process.env

View File

@@ -23,7 +23,7 @@ const FilePathInputModal = ({
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value
const specialChars = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>?~]/
const specialChars = /[`!@#$%^&*()+\-=[\]{};':"\\|,<>?~]/
const fileExtension = /\.(exe|sh|htaccess)$/i
if (specialChars.test(value)) {

View File

@@ -44,7 +44,7 @@ const NameInputModal = ({
const value = event.target.value
const folderNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?~]/
const fileNameRegex = /[`!@#$%^&*()_+\-=[\]{};':"\\|,<>/?~]/
const fileNameRegex = /[`!@#$%^&*()+\-=[\]{};':"\\|,<>/?~]/
const fileNameExtensionRegex = /.(exe|sh|htaccess)$/i
const specialChars = isFolder ? folderNameRegex : fileNameRegex

View File

@@ -78,6 +78,18 @@ const TreeViewNode = ({
mouseY: number
} | null>(null)
const launchProgram = () => {
const baseUrl = window.location.origin
window.open(`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}`)
}
const launchProgramWithDebug = () => {
const baseUrl = window.location.origin
window.open(
`${baseUrl}/SASjsApi/stp/execute?_program=${node.relativePath}&_debug=131`
)
}
const handleContextMenu = (event: React.MouseEvent) => {
event.preventDefault()
event.stopPropagation()
@@ -224,8 +236,8 @@ const TreeViewNode = ({
: undefined
}
>
{node.isFolder && (
<div>
{node.isFolder ? (
<>
<MenuItem onClick={handleNewFolderItemClick}>Add Folder</MenuItem>
<MenuItem
disabled={!node.relativePath}
@@ -233,14 +245,21 @@ const TreeViewNode = ({
>
Add File
</MenuItem>
</div>
</>
) : (
<>
<MenuItem onClick={launchProgram}>Launch</MenuItem>
<MenuItem onClick={launchProgramWithDebug}>
Launch and Debug
</MenuItem>
</>
)}
{!!node.relativePath && (
<>
<MenuItem onClick={handleRenameItemClick}>Rename</MenuItem>
<MenuItem onClick={handleDeleteItemClick}>Delete</MenuItem>
</>
)}
<MenuItem disabled={!node.relativePath} onClick={handleRenameItemClick}>
Rename
</MenuItem>
<MenuItem disabled={!node.relativePath} onClick={handleDeleteItemClick}>
Delete
</MenuItem>
</Menu>
</div>
)

View File

@@ -4,7 +4,8 @@ import React, {
useEffect,
useRef,
useState,
useContext
useContext,
useCallback
} from 'react'
import axios from 'axios'
@@ -70,6 +71,8 @@ type SASjsEditorProps = {
}
const baseUrl = window.location.origin
const SASJS_LOGS_SEPARATOR =
'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
const SASjsEditor = ({
selectedFilePath,
@@ -137,6 +140,88 @@ const SASjsEditor = ({
prevFileContent !== fileContent && !!selectedFilePath
)
const saveFile = useCallback(
(filePath?: string) => {
setIsLoading(true)
if (filePath) {
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
}
const formData = new FormData()
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
formData.append('file', stringBlob)
formData.append('filePath', filePath ?? selectedFilePath)
const axiosPromise = filePath
? axios.post('/SASjsApi/drive/file', formData)
: axios.patch('/SASjsApi/drive/file', formData)
axiosPromise
.then(() => {
if (filePath && fileContent === prevFileContent) {
// when fileContent and prevFileContent is same,
// callback function in setPrevFileContent method is not called
// because behind the scene useEffect hook is being used
// for calling callback function, and it's only fired when the
// new value is not equal to old value.
// So, we'll have to explicitly update the selected file path
setSelectedFilePath(filePath, true)
} else {
setPrevFileContent(fileContent, () => {
if (filePath) {
setSelectedFilePath(filePath, true)
}
})
}
setSnackbarMessage('File saved!')
setSnackbarSeverity(AlertSeverityType.Success)
setOpenSnackbar(true)
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => {
setIsLoading(false)
})
},
[
fileContent,
prevFileContent,
selectedFilePath,
setPrevFileContent,
setSelectedFilePath
]
)
useEffect(() => {
editorRef.current.addAction({
// An unique identifier of the contributed action.
id: 'save-file',
// A label of the action that will be presented to the user.
label: 'Save',
// An optional array of keybindings for the action.
keybindings: [monaco.KeyMod.CtrlCmd | monaco.KeyCode.KeyS],
// Method that will be executed when the action is triggered.
// @param editor The editor instance is passed in as a convenience
run: () => {
if (!selectedFilePath) return setOpenFilePathInputModal(true)
if (prevFileContent !== fileContent) return saveFile()
}
})
}, [fileContent, prevFileContent, selectedFilePath, saveFile])
useEffect(() => {
setRunTimes(Object.values(appContext.runTimes))
}, [appContext.runTimes])
@@ -203,13 +288,8 @@ const SASjsEditor = ({
axios
.post(`/SASjsApi/code/execute`, { code, runTime: selectedRunTime })
.then((res: any) => {
const parsedLog = res?.data?.log
.map((logLine: any) => logLine.line)
.join('\n')
setLog(parsedLog)
setWebout(`${res.data?._webout}`)
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
setTab('log')
// Scroll to bottom of log
@@ -252,59 +332,6 @@ const SASjsEditor = ({
saveFile(filePath)
}
const saveFile = (filePath?: string) => {
setIsLoading(true)
if (filePath) {
filePath = filePath.startsWith('/') ? filePath : `/${filePath}`
}
const formData = new FormData()
const stringBlob = new Blob([fileContent], { type: 'text/plain' })
formData.append('file', stringBlob, 'filename.sas')
formData.append('filePath', filePath ?? selectedFilePath)
const axiosPromise = filePath
? axios.post('/SASjsApi/drive/file', formData)
: axios.patch('/SASjsApi/drive/file', formData)
axiosPromise
.then(() => {
if (filePath && fileContent === prevFileContent) {
// when fileContent and prevFileContent is same,
// callback function in setPrevFileContent method is not called
// because behind the scene useEffect hook is being used
// for calling callback function, and it's only fired when the
// new value is not equal to old value.
// So, we'll have to explicitly update the selected file path
setSelectedFilePath(filePath, true)
} else {
setPrevFileContent(fileContent, () => {
if (filePath) {
setSelectedFilePath(filePath, true)
}
})
}
setSnackbarMessage('File saved!')
setSnackbarSeverity(AlertSeverityType.Success)
setOpenSnackbar(true)
})
.catch((err) => {
setModalTitle('Abort')
setModalPayload(
typeof err.response.data === 'object'
? JSON.stringify(err.response.data)
: err.response.data
)
setOpenModal(true)
})
.finally(() => {
setIsLoading(false)
})
}
return (
<Box sx={{ width: '100%', typography: 'body1', marginTop: '50px' }}>
<Backdrop