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

Compare commits

...

14 Commits

Author SHA1 Message Date
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
Saad Jutt
19d4430b31 chore(release): 0.0.13 2021-12-16 16:13:05 +05:00
Saad Jutt
e5be0e6789 fix: output for Studio 2021-12-16 12:44:57 +05:00
Saad Jutt
27129a8921 feat(studio): run selected code + open in studio 2021-12-16 12:14:32 +05:00
Saad Jutt
da11c03d55 chore: sasjs/core version bumped 2021-12-15 19:57:09 +05:00
Saad Jutt
4fbdda0365 chore: .env.example updated 2021-12-15 18:58:04 +05:00
Saad Jutt
efacb1e916 chore(release): 0.0.12 2021-12-15 18:24:17 +05:00
Saad Jutt
d19ce253b4 fix: use env if provided for desktop mode 2021-12-15 18:24:04 +05:00
18 changed files with 1390 additions and 133 deletions

View File

@@ -2,6 +2,34 @@
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.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)
### Features
* **studio:** run selected code + open in studio ([27129a8](https://github.com/sasjs/server/commit/27129a8921084c72968383fdbc2ecbd2f417456c))
### Bug Fixes
* output for Studio ([e5be0e6](https://github.com/sasjs/server/commit/e5be0e678965b05c64bcc8f55c48a366e0ff55a3))
### [0.0.12](https://github.com/sasjs/server/compare/v0.0.11...v0.0.12) (2021-12-15)
### Bug Fixes
* use env if provided for desktop mode ([d19ce25](https://github.com/sasjs/server/commit/d19ce253b4e2d2a7dd912d43a553d4c1bd60ba58))
### [0.0.11](https://github.com/sasjs/server/compare/v0.0.10...v0.0.11) (2021-12-15)

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`:

View File

@@ -5,4 +5,7 @@ PORT_WEB=[port for sasjs web component(react)] default value is 3000
ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret>
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
DB_CONNECT=mongodb+srv://<DB_USERNAME>:<DB_PASSWORD>@<CLUSTER>/<DB_NAME>?retryWrites=true&w=majority
SAS_PATH=/opt/sas/sas9/SASHome/SASFoundation/9.4/sas
DRIVE_PATH=./tmp

1207
api/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -358,6 +358,16 @@ 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:
@@ -1027,6 +1037,30 @@ 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

View File

@@ -6,7 +6,7 @@ import { PreProgramVars, TreeNode } from '../../types'
import { generateFileUploadSasCode, getTmpFilesFolderPath } from '../../utils'
export class ExecutionController {
async execute(
async executeFile(
programPath: string,
preProgramVariables: PreProgramVars,
vars: { [key: string]: string | number | undefined },
@@ -16,8 +16,23 @@ export class ExecutionController {
if (!(await fileExists(programPath)))
throw 'ExecutionController: SAS file does not exist.'
let program = await readFile(programPath)
const program = await readFile(programPath)
return this.executeProgram(
program,
preProgramVariables,
vars,
otherArgs,
returnJson
)
}
async executeProgram(
program: string,
preProgramVariables: PreProgramVars,
vars: { [key: string]: string | number | undefined },
otherArgs?: any,
returnJson?: boolean
) {
const sessionController = getSessionController()
const session = await sessionController.getSession()

View File

@@ -5,6 +5,13 @@ 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
@@ -40,6 +47,19 @@ export class STPController {
): Promise<string> {
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.
@@ -72,7 +92,7 @@ const executeReturnRaw = async (
.replace(new RegExp('/', 'g'), path.sep) + '.sas'
try {
const result = await new ExecutionController().execute(
const result = await new ExecutionController().executeFile(
sasCodePath,
getPreProgramVariables(req),
query
@@ -89,6 +109,25 @@ 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
@@ -101,7 +140,7 @@ const executeReturnJson = async (
const filesNamesMap = req.files?.length ? makeFilesNamesMap(req.files) : null
try {
const { webout, log } = (await new ExecutionController().execute(
const { webout, log } = (await new ExecutionController().executeFile(
sasCodePath,
getPreProgramVariables(req),
{ ...req.query, ...req.body },

View File

@@ -1,5 +1,5 @@
import express from 'express'
import { executeProgramRawValidation } from '../../utils'
import { executeProgramRawValidation, runSASValidation } from '../../utils'
import { STPController } from '../../controllers/'
import { FileUploadController } from '../../controllers/internal'
@@ -24,6 +24,22 @@ 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

@@ -1,7 +1,7 @@
declare namespace NodeJS {
export interface Process {
sasLoc: string
driveLoc?: string
driveLoc: string
sessionController?: import('../controllers/internal').SessionController
}
}

View File

@@ -3,12 +3,14 @@ import mongoose from 'mongoose'
import { configuration } from '../../package.json'
import { getDesktopFields } from '.'
import { populateClients } from '../routes/api/auth'
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') {
const { MODE } = process.env
if (MODE?.trim() !== 'server') {
console.log('Running in Destop Mode, no DB to connect.')
@@ -16,16 +18,19 @@ export const connectDB = async () => {
process.sasLoc = sasLoc
process.driveLoc = driveLoc
} else {
const { SAS_PATH, DRIVE_PATH } = process.env
return
process.sasLoc = SAS_PATH ?? configuration.sasPath
process.driveLoc = getRealPath(
path.join(process.cwd(), DRIVE_PATH ?? 'tmp')
)
}
const { SAS_PATH } = process.env
const sasDir = SAS_PATH ?? configuration.sasPath
process.sasLoc = path.join(sasDir, 'sas')
console.log('sasLoc: ', process.sasLoc)
console.log('sasDrive: ', process.driveLoc)
if (MODE?.trim() !== 'server') return
mongoose.connect(process.env.DB_CONNECT as string, async (err) => {
if (err) throw err

View File

@@ -1,5 +1,4 @@
import path from 'path'
import { getRealPath } from '@sasjs/utils'
export const apiRoot = path.join(__dirname, '..', '..')
export const codebaseRoot = path.join(apiRoot, '..')
@@ -12,8 +11,7 @@ export const sysInitCompiledPath = path.join(
export const getWebBuildFolderPath = () =>
path.join(codebaseRoot, 'web', 'build')
export const getTmpFolderPath = () =>
process.driveLoc ?? getRealPath(path.join(process.cwd(), 'tmp'))
export const getTmpFolderPath = () => process.driveLoc
export const getTmpFilesFolderPath = () =>
path.join(getTmpFolderPath(), 'files')

View File

@@ -5,8 +5,10 @@ import { createFolder, fileExists, folderExists } from '@sasjs/utils'
const isWindows = () => process.platform === 'win32'
export const getDesktopFields = async () => {
const sasLoc = await getSASLocation()
const driveLoc = await getDriveLocation()
const { SAS_PATH, DRIVE_PATH } = process.env
const sasLoc = SAS_PATH ?? (await getSASLocation())
const driveLoc = DRIVE_PATH ?? (await getDriveLocation())
return { sasLoc, driveLoc }
}

View File

@@ -77,6 +77,11 @@ export const updateFileDriveValidation = (data: any): Joi.ValidationResult =>
fileContent: Joi.string().required()
}).validate(data)
export const runSASValidation = (data: any): Joi.ValidationResult =>
Joi.object({
code: Joi.string().required()
}).validate(data)
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
Joi.object({
_program: Joi.string().required()

4
package-lock.json generated
View File

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

View File

@@ -1,7 +1,8 @@
{
"name": "server",
"version": "0.0.11",
"version": "0.0.14",
"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

@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import axios from 'axios'
import Editor from '@monaco-editor/react'
@@ -89,10 +90,10 @@ const Main = (props: any) => {
style={{ position: 'absolute', left: '50%', top: '50%' }}
/>
)}
{!isLoading && props?.selectedFilePath !== '' && !editMode && (
{!isLoading && props?.selectedFilePath && !editMode && (
<code style={{ whiteSpace: 'break-spaces' }}>{fileContent}</code>
)}
{!isLoading && props?.selectedFilePath !== '' && editMode && (
{!isLoading && props?.selectedFilePath && editMode && (
<Editor
height="95%"
value={fileContent}
@@ -110,17 +111,26 @@ const Main = (props: any) => {
<Button
variant="contained"
onClick={handleEditSaveBtnClick}
disabled={isLoading || props?.selectedFilePath === ''}
disabled={isLoading || !props?.selectedFilePath}
>
{!editMode ? 'Edit' : 'Save'}
</Button>
<Button
variant="contained"
onClick={handleCancelExecuteBtnClick}
disabled={isLoading || props?.selectedFilePath === ''}
disabled={isLoading || !props?.selectedFilePath}
>
{editMode ? 'Cancel' : 'Execute'}
</Button>
{props?.selectedFilePath && (
<Button
variant="contained"
component={Link}
to={`/SASjsStudio?_program=${props.selectedFilePath}`}
>
Open in Studio
</Button>
)}
</Stack>
</Box>
)

View File

@@ -1,13 +1,96 @@
import React from 'react'
import React, { useEffect, useRef, useState } from 'react'
import axios from 'axios'
import CssBaseline from '@mui/material/CssBaseline'
import Box from '@mui/material/Box'
import { Button, Paper, Stack, Toolbar } from '@mui/material'
import Editor from '@monaco-editor/react'
import { useLocation } from 'react-router-dom'
const Studio = () => {
const location = useLocation()
const [fileContent, setFileContent] = useState('')
const [log, setLog] = useState('')
const editorRef = useRef(null)
const handleEditorDidMount = (editor: any) => (editorRef.current = editor)
const getSelection = () => {
const editor = editorRef.current as any
const selection = editor?.getModel().getValueInRange(editor?.getSelection())
return selection ?? ''
}
const handleRunSelectionBtnClick = () => runCode(getSelection())
const handleRunBtnClick = () => runCode(fileContent)
const runCode = (code: string) => {
axios
.post(`/SASjsApi/stp/run`, { code })
.then((res: any) => {
const data =
typeof res.data === 'string'
? res.data
: `<pre><code>${JSON.stringify(res.data, null, 4)}</code></pre>`
setLog(data)
document?.getElementById('sas_log')?.scrollIntoView()
})
.catch((err) => console.log(err))
}
useEffect(() => {
const params = new URLSearchParams(location.search)
const programPath = params.get('_program')
if (programPath?.length)
axios
.get(`/SASjsApi/drive/file?filePath=${programPath}`)
.then((res: any) => setFileContent(res.data.fileContent))
.catch((err) => console.log(err))
}, [location.search])
return (
<Box className="main">
<CssBaseline />
<h2>This is container for SASjs studio</h2>
<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>
)
}