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

Compare commits

..

23 Commits

Author SHA1 Message Date
Saad Jutt
4e486fda69 chore(release): 0.0.15 2022-01-06 17:23:29 +05:00
Muhammad Saad
79cac53fdb Merge pull request #42 from sasjs/run-sas-code-route
Run sas code route
2022-01-06 16:23:07 +04:00
Saad Jutt
450d99f06e fix(web): sticky tabs on Studio + extra run code button removed 2022-01-05 17:41:17 +05:00
Saad Jutt
51ee8c0825 fix(web): autosave and autofocus 2021-12-30 12:56:23 +05:00
Saad Jutt
a1151606f2 fix(web): parsing of webout 2021-12-30 12:18:48 +05:00
Saad Jutt
38193c83dd chore: Merge branch 'main' into run-sas-code-route 2021-12-30 12:18:16 +05:00
Muhammad Saad
59ecc36f2b Merge pull request #43 from sasjs/allanbowe-patch-1
Update home.tsx
2021-12-30 12:07:14 +05:00
Allan Bowe
8bc459c9a7 Update home.tsx 2021-12-29 19:15:21 +00:00
Saad Jutt
f1f1e47f76 chore(web): display webout as well 2021-12-29 01:00:53 +05:00
Saad Jutt
679e9de245 chore: return webout and log seperately 2021-12-29 00:27:54 +05:00
Saad Jutt
f0ac996b3c chore(web): build fix 2021-12-28 22:18:18 +05:00
Saad Jutt
2d77222ae8 fix(studio): web component updated 2021-12-28 22:02:11 +05:00
Saad Jutt
e6e5a5fd64 chore(code): updated route to code/execute 2021-12-21 14:24:27 +05:00
Saad Jutt
e1eb04494a fix: updated route for sas code 2021-12-21 12:36:58 +05:00
Allan Bowe
b7fa8e5f80 Merge pull request #40 from sasjs/fix-specs
chore: added driveLoc for specs
2021-12-20 11:39:55 +02:00
Saad Jutt
ef4fae4496 chore: added driveLoc for specs 2021-12-20 10:47:47 +05:00
munja
3e5a4e0555 chore(release): 0.0.14 2021-12-19 11:35:08 +00:00
Allan Bowe
cf9a8091ea Merge pull request #39 from sasjs/master
chore: updating package.json
2021-12-19 13:33:41 +02:00
munja
0edc45dd0a chore: updating package.json 2021-12-19 11:32:48 +00:00
munja
ceca370e27 fix: switch to main branch 2021-12-19 11:30:27 +00:00
Allan Bowe
f235b9c2f9 Merge pull request #38 from sasjs/weboutfix
fix: actually a README change, the fix was in the previous commit (up…
2021-12-18 23:24:57 +02:00
Allan Bowe
d86c841f1f fix: actually a README change, the fix was in the previous commit (updating ms_webout) that should have been a PR, to trigger a release 2021-12-18 20:31:40 +00:00
Allan Bowe
076b866c02 fix: bumping sasjs/core with adjustment to ms_webout() 2021-12-18 20:26:07 +00:00
19 changed files with 326 additions and 181 deletions

View File

@@ -2,6 +2,26 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.0.15](https://github.com/sasjs/server/compare/v0.0.14...v0.0.15) (2022-01-06)
### Bug Fixes
* **studio:** web component updated ([2d77222](https://github.com/sasjs/server/commit/2d77222ae8a139acd9d96466d0e68291c4ebd70e))
* updated route for sas code ([e1eb044](https://github.com/sasjs/server/commit/e1eb04494a5650726c95990f74fc719eced4ccb5))
* **web:** autosave and autofocus ([51ee8c0](https://github.com/sasjs/server/commit/51ee8c0825f021d1d67b2d765d5b434cbf248a1f))
* **web:** parsing of webout ([a115160](https://github.com/sasjs/server/commit/a1151606f21e0007e2b1ca1245d592d96866f62a))
* **web:** sticky tabs on Studio + extra run code button removed ([450d99f](https://github.com/sasjs/server/commit/450d99f06e5929eb1679e6203284e4faa44e19b0))
### [0.0.14](https://github.com/sasjs/server/compare/v0.0.13...v0.0.14) (2021-12-19)
### Bug Fixes
* actually a README change, the fix was in the previous commit (updating ms_webout) that should have been a PR, to trigger a release ([d86c841](https://github.com/sasjs/server/commit/d86c841f1fb94455ac3500f215a42b4acb8b0017))
* bumping sasjs/core with adjustment to ms_webout() ([076b866](https://github.com/sasjs/server/commit/076b866c020fb017512c2764801022a57fe4cca8))
* switch to main branch ([ceca370](https://github.com/sasjs/server/commit/ceca370e2757baf2e8ebb90dab6dfd27f7b990fc))
### [0.0.13](https://github.com/sasjs/server/compare/v0.0.12...v0.0.13) (2021-12-16)

View File

@@ -8,6 +8,15 @@ SASjs Server provides a NodeJS wrapper for calling the SAS binary executable. It
One major benefit of using SASjs Server (alongside other components of the SASjs framework such as the [CLI](https://cli.sasjs.io), [Adapter](https://adapter.sasjs.io) and [Core](https://core.sasjs.io) library) is that the projects you create can be very easily ported to SAS 9 (Stored Process server) or Viya (Job Execution server).
## Installation
Just download the relevant package from the [releases](https://github.com/sasjs/server/releases) page and trigger, either by double clicking (windows) or executing from commandline.
You are presented with two prompts:
* Location of your `sas.exe` / `sas.sh` executable
* Path to a filesystem location for Stored Programs and temporary files
## Configuration
Configuration is made in the `configuration` section of `package.json`:

14
api/package-lock.json generated
View File

@@ -8,7 +8,7 @@
"name": "api",
"version": "0.0.1",
"dependencies": {
"@sasjs/core": "^2.59.0",
"@sasjs/core": "^3.0.2",
"@sasjs/utils": "2.34.1",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
@@ -1551,9 +1551,9 @@
}
},
"node_modules/@sasjs/core": {
"version": "2.59.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-2.59.0.tgz",
"integrity": "sha512-I85+V83Km6Naz4j0i7djPYBRQmkt4NCJcM13Nvhku5puwFn7Jcq0rHBnI2G3+uIQaXC6Tk+YwTODrrEvIQgRgA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-3.0.2.tgz",
"integrity": "sha512-KP1DP7t1TJa71xA7FmmN+ZTlEcwRNzz/0DC/oistvva64j7Tpu5BgZRUAj/u3yE1Z6+OmRYKKLjugxsQX0s2Tw==",
"dependencies": {
"ts-loader": "^9.2.6"
}
@@ -16665,9 +16665,9 @@
}
},
"@sasjs/core": {
"version": "2.59.0",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-2.59.0.tgz",
"integrity": "sha512-I85+V83Km6Naz4j0i7djPYBRQmkt4NCJcM13Nvhku5puwFn7Jcq0rHBnI2G3+uIQaXC6Tk+YwTODrrEvIQgRgA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@sasjs/core/-/core-3.0.2.tgz",
"integrity": "sha512-KP1DP7t1TJa71xA7FmmN+ZTlEcwRNzz/0DC/oistvva64j7Tpu5BgZRUAj/u3yE1Z6+OmRYKKLjugxsQX0s2Tw==",
"requires": {
"ts-loader": "^9.2.6"
}

View File

@@ -41,12 +41,12 @@
},
"release": {
"branches": [
"master"
"main"
]
},
"author": "Analytium Ltd",
"dependencies": {
"@sasjs/core": "^2.59.0",
"@sasjs/core": "^3.0.2",
"@sasjs/utils": "2.34.1",
"bcryptjs": "^2.4.3",
"cors": "^2.8.5",
@@ -88,4 +88,4 @@
"configuration": {
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
}
}
}

View File

@@ -92,6 +92,16 @@ components:
- clientSecret
type: object
additionalProperties: false
ExecuteSASCodePayload:
properties:
code:
type: string
description: 'Code of SAS program'
example: '* SAS Code HERE;'
required:
- code
type: object
additionalProperties: false
MemberType.folder:
enum:
- folder
@@ -358,16 +368,6 @@ components:
- description
type: object
additionalProperties: false
RunSASPayload:
properties:
code:
type: string
description: 'Code of SAS program'
example: '* SAS Code HERE;'
required:
- code
type: object
additionalProperties: false
ExecuteReturnJsonResponse:
properties:
status:
@@ -511,6 +511,30 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ClientPayload'
/SASjsApi/code/execute:
post:
operationId: ExecuteSASCode
responses:
'200':
description: Ok
content:
application/json:
schema:
type: string
description: 'Execute SAS code.'
summary: 'Run SAS Code and returns log'
tags:
- CODE
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ExecuteSASCodePayload'
/SASjsApi/drive/deploy:
post:
operationId: Deploy
@@ -982,6 +1006,26 @@ paths:
format: double
type: number
example: '6789'
/SASjsApi/session:
get:
operationId: Session
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
examples:
'Example 1':
value: {id: 123, username: johnusername, displayName: John}
summary: 'Get session info (username).'
tags:
- Session
security:
-
bearerAuth: []
parameters: []
/SASjsApi/stp/execute:
get:
operationId: ExecuteReturnRaw
@@ -1037,50 +1081,6 @@ paths:
application/json:
schema:
$ref: '#/components/schemas/ExecuteReturnJsonPayload'
/SASjsApi/stp/run:
post:
operationId: RunSAS
responses:
'200':
description: Ok
content:
application/json:
schema:
type: string
description: 'Trigger a SAS program.'
summary: 'Run SAS Program, return raw content'
tags:
- STP
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RunSASPayload'
/SASjsApi/session:
get:
operationId: Session
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
examples:
'Example 1':
value: {id: 123, username: johnusername, displayName: John}
summary: 'Get session info (username).'
tags:
- Session
security:
-
bearerAuth: []
parameters: []
servers:
-
url: /
@@ -1106,3 +1106,6 @@ tags:
-
name: STP
description: 'Operations about STP'
-
name: CODE
description: 'Operations on SAS code'

View File

@@ -0,0 +1,63 @@
import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecutionController } from './internal'
import { PreProgramVars } from '../types'
interface ExecuteSASCodePayload {
/**
* Code of SAS program
* @example "* SAS Code HERE;"
*/
code: string
}
@Security('bearerAuth')
@Route('SASjsApi/code')
@Tags('CODE')
export class CodeController {
/**
* Execute SAS code.
* @summary Run SAS Code and returns log
*/
@Post('/execute')
public async executeSASCode(
@Request() request: express.Request,
@Body() body: ExecuteSASCodePayload
): Promise<string> {
return executeSASCode(request, body)
}
}
const executeSASCode = async (req: any, { code }: ExecuteSASCodePayload) => {
try {
const result = await new ExecutionController().executeProgram(
code,
getPreProgramVariables(req),
{ ...req.query, _debug: 131 },
undefined,
true
)
return result as string
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}
const getPreProgramVariables = (req: any): PreProgramVars => {
const host = req.get('host')
const protocol = req.protocol + '://'
const { user, accessToken } = req
return {
username: user.username,
userId: user.userId,
displayName: user.displayName,
serverUrl: protocol + host,
accessToken
}
}

View File

@@ -1,7 +1,8 @@
export * from './auth'
export * from './client'
export * from './code'
export * from './drive'
export * from './group'
export * from './session'
export * from './stp'
export * from './user'
export * from './session'

View File

@@ -110,7 +110,7 @@ export class SessionController {
// TODO: don't wait forever
while ((await fileExists(codeFilePath)) && !session.crashed) {}
console.log('session crashed?', !!session.crashed, session.crashed)
console.log('session crashed?', !!session.crashed, session.crashed || '')
session.ready = true
return Promise.resolve(session)

View File

@@ -5,13 +5,6 @@ import { ExecutionController } from './internal'
import { PreProgramVars } from '../types'
import { getTmpFilesFolderPath, makeFilesNamesMap } from '../utils'
interface RunSASPayload {
/**
* Code of SAS program
* @example "* SAS Code HERE;"
*/
code: string
}
interface ExecuteReturnJsonPayload {
/**
* Location of SAS program
@@ -48,18 +41,6 @@ export class STPController {
return executeReturnRaw(request, _program)
}
/**
* Trigger a SAS program.
* @summary Run SAS Program, return raw content
*/
@Post('/run')
public async runSAS(
@Request() request: express.Request,
@Body() body: RunSASPayload
): Promise<string> {
return runSAS(request, body)
}
/**
* Trigger a SAS program using it's location in the _program parameter.
* Enable debugging using the _debug parameter.
@@ -109,25 +90,6 @@ const executeReturnRaw = async (
}
}
const runSAS = async (req: any, { code }: RunSASPayload) => {
try {
const result = await new ExecutionController().executeProgram(
code,
getPreProgramVariables(req),
req.query
)
return result as string
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}
const executeReturnJson = async (
req: any,
_program: string

View File

@@ -0,0 +1,25 @@
import express from 'express'
import { runSASValidation } from '../../utils'
import { CodeController } from '../../controllers/'
const runRouter = express.Router()
const controller = new CodeController()
runRouter.post('/execute', async (req, res) => {
const { error, value: body } = runSASValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.executeSASCode(req, body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default runRouter

View File

@@ -11,6 +11,7 @@ import {
import driveRouter from './drive'
import stpRouter from './stp'
import codeRouter from './code'
import userRouter from './user'
import groupRouter from './group'
import clientRouter from './client'
@@ -31,6 +32,7 @@ router.use(
router.use('/drive', authenticateAccessToken, driveRouter)
router.use('/group', desktopRestrict, groupRouter)
router.use('/stp', authenticateAccessToken, stpRouter)
router.use('/code', authenticateAccessToken, codeRouter)
router.use('/user', desktopRestrict, userRouter)
router.use(
'/',

View File

@@ -24,22 +24,6 @@ stpRouter.get('/execute', async (req, res) => {
}
})
stpRouter.post('/run', async (req, res) => {
const { error, value: body } = runSASValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.runSAS(req, body)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
stpRouter.post(
'/execute',
fileUploadController.preuploadMiddleware,

View File

@@ -8,7 +8,10 @@ import { getRealPath } from '@sasjs/utils'
export const connectDB = async () => {
// NOTE: when exporting app.js as agent for supertest
// we should exlcude connecting to the real database
if (process.env.NODE_ENV !== 'test') {
if (process.env.NODE_ENV === 'test') {
process.driveLoc = path.join(process.cwd(), 'tmp')
return
} else {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {

View File

@@ -38,6 +38,10 @@
{
"name": "STP",
"description": "Operations about STP"
},
{
"name": "CODE",
"description": "Operations on SAS code"
}
],
"yaml": true,

View File

@@ -43,7 +43,7 @@ services:
- ./web:/usr/server/web
mongodb:
image: mongo:latest
image: mongo:5.0.4
ports:
- 27017:27017
volumes:

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "server",
"version": "0.0.13",
"version": "0.0.15",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "server",
"version": "0.0.13",
"version": "0.0.15",
"devDependencies": {
"prettier": "^2.3.1",
"standard-version": "^9.3.2"

View File

@@ -1,7 +1,8 @@
{
"name": "server",
"version": "0.0.13",
"version": "0.0.15",
"description": "NodeJS wrapper for calling the SAS binary executable",
"repository": "https://github.com/sasjs/server",
"scripts": {
"server": "npm run server:prepare && npm run server:start",
"server:prepare": "cd web && npm ci && npm run build && cd ../api && npm ci && cd ..",

View File

@@ -23,7 +23,7 @@ const Home = () => {
and contributions are welcomed.
</p>
<p>
SASjs Server is maintained by the SASjs Apps team -{' '}
SASjs Server is maintained by the SAS Apps team -{' '}
<a
href="https://sasapps.io/contact-us"
target="_blank"

View File

@@ -2,17 +2,37 @@ import React, { useEffect, useRef, useState } from 'react'
import axios from 'axios'
import Box from '@mui/material/Box'
import { Button, Paper, Stack, Toolbar } from '@mui/material'
import Editor from '@monaco-editor/react'
import { Button, Paper, Stack, Tab } from '@mui/material'
import { makeStyles } from '@mui/styles'
import Editor, { OnMount } from '@monaco-editor/react'
import { useLocation } from 'react-router-dom'
import { TabContext, TabList, TabPanel } from '@mui/lab'
const useStyles = makeStyles(() => ({
root: {
fontSize: '1rem',
color: 'gray',
'&.Mui-selected': {
color: 'black'
}
}
}))
const Studio = () => {
const location = useLocation()
const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('')
const [webout, setWebout] = useState('')
const [tab, setTab] = React.useState('1')
const handleTabChange = (_e: any, newValue: string) => {
setTab(newValue)
}
const editorRef = useRef(null)
const handleEditorDidMount = (editor: any) => (editorRef.current = editor)
const editorRef = useRef(null as any)
const handleEditorDidMount: OnMount = (editor) => {
editor.focus()
editorRef.current = editor
}
const getSelection = () => {
const editor = editorRef.current as any
@@ -20,25 +40,47 @@ const Studio = () => {
return selection ?? ''
}
const handleRunSelectionBtnClick = () => runCode(getSelection())
const handleRunBtnClick = () => runCode(fileContent)
const handleRunBtnClick = () => runCode(getSelection() || fileContent)
const runCode = (code: string) => {
axios
.post(`/SASjsApi/stp/run`, { code })
.post(`/SASjsApi/code/execute`, { code })
.then((res: any) => {
const data =
typeof res.data === 'string'
? res.data
: `<pre><code>${JSON.stringify(res.data, null, 4)}</code></pre>`
setLog(`<div><h2>SAS Log</h2><pre>${res?.data?.log}</pre></div>`)
setLog(data)
document?.getElementById('sas_log')?.scrollIntoView()
let weboutString: string
try {
weboutString = res.data.webout
.split('>>weboutBEGIN<<')[1]
.split('>>weboutEND<<')[0]
} catch (_) {
weboutString = res?.data?.webout ?? ''
}
let webout: string
try {
webout = JSON.stringify(JSON.parse(weboutString), null, 4)
} catch (_) {
webout = weboutString
}
setWebout(`<pre><code>${webout}</code></pre>`)
setTab('2')
})
.catch((err) => console.log(err))
}
useEffect(() => {
const content = localStorage.getItem('fileContent') ?? ''
setFileContent(content)
}, [])
useEffect(() => {
if (fileContent.length) {
localStorage.setItem('fileContent', fileContent)
}
}, [fileContent])
useEffect(() => {
const params = new URLSearchParams(location.search)
const programPath = params.get('_program')
@@ -50,48 +92,74 @@ const Studio = () => {
.catch((err) => console.log(err))
}, [location.search])
const classes = useStyles()
return (
<Box component="main" sx={{ flexGrow: 1, p: 3 }}>
<Toolbar />
<Paper
sx={{
height: '75vh',
padding: '10px',
overflow: 'auto',
position: 'relative'
}}
elevation={3}
>
<Editor
height="95%"
value={fileContent}
onMount={handleEditorDidMount}
onChange={(val) => {
if (val) setFileContent(val)
}}
/>
</Paper>
<Stack
spacing={3}
direction="row"
sx={{ justifyContent: 'center', marginTop: '20px' }}
>
<Button variant="contained" onClick={handleRunBtnClick}>
Run SAS Code
</Button>
<Button variant="contained" onClick={handleRunSelectionBtnClick}>
Run Selected SAS Code
</Button>
</Stack>
{log && (
<>
<br />
<h2 id="sas_log">Output</h2>
<br />
<div dangerouslySetInnerHTML={{ __html: log }} />
</>
)}
</Box>
<>
<br />
<br />
<br />
<Box sx={{ width: '100%', typography: 'body1' }}>
<TabContext value={tab}>
<Box
sx={{
borderBottom: 1,
borderColor: 'divider'
}}
style={{ position: 'fixed', background: 'white', width: '100%' }}
>
<TabList onChange={handleTabChange} centered>
<Tab className={classes.root} label="Code" value="1" />
<Tab className={classes.root} label="Log" value="2" />
<Tab className={classes.root} label="Webout" value="3" />
</TabList>
</Box>
<TabPanel value="1">
{/* <Toolbar /> */}
<Paper
sx={{
height: '70vh',
marginTop: '50px',
padding: '10px',
overflow: 'auto',
position: 'relative'
}}
elevation={3}
>
<Editor
height="95%"
value={fileContent}
onMount={handleEditorDidMount}
onChange={(val) => {
if (val) setFileContent(val)
}}
/>
</Paper>
<Stack
spacing={3}
direction="row"
sx={{ justifyContent: 'center', marginTop: '20px' }}
>
<Button variant="contained" onClick={handleRunBtnClick}>
Run SAS Code
</Button>
</Stack>
</TabPanel>
<TabPanel value="2">
<div
id="sas_log"
style={{ marginTop: '50px' }}
dangerouslySetInnerHTML={{ __html: log }}
/>
</TabPanel>
<TabPanel value="3">
<div
style={{ marginTop: '50px' }}
dangerouslySetInnerHTML={{ __html: webout }}
/>
</TabPanel>
</TabContext>
</Box>
</>
)
}