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

Compare commits

...

29 Commits

Author SHA1 Message Date
Saad Jutt
2204d54cd6 chore(release): 0.0.41 2022-03-23 21:32:06 +05:00
Saad Jutt
f4eb75ff34 fix(scroll): closes #100 2022-03-23 21:31:54 +05:00
Saad Jutt
a3cde343b7 chore(release): 0.0.40 2022-03-23 20:17:15 +05:00
Saad Jutt
7a70d40dbf fix: macros available for SAS 2022-03-23 20:15:18 +05:00
Saad Jutt
d27e070fc8 fix: moved macros from codebase to drive
This reverts commit d2956fc641.
2022-03-23 19:38:29 +05:00
Saad Jutt
27e260e6a4 fix(deploy): validating empty file or service in filetree 2022-03-23 19:38:20 +05:00
Saad Jutt
2796db8ead chore(release): 0.0.39 2022-03-23 18:07:12 +05:00
Muhammad Saad
84f7c2ab89 Merge pull request #103 from sasjs/executable-macros-fix
Executable macros fix
2022-03-23 18:07:01 +05:00
Saad Jutt
e68090181a fix: included sasjs core macros at compile time 2022-03-23 18:05:03 +05:00
Saad Jutt
d2956fc641 Revert "fix: moved macros from codebase to drive"
This reverts commit 9ac3191891.
2022-03-23 17:59:06 +05:00
Saad Jutt
a701bb25e7 Revert "fix: quick fix for executables"
This reverts commit 9e53470947.
2022-03-23 17:58:18 +05:00
Saad Jutt
5758bcd392 chore(release): 0.0.38 2022-03-23 17:16:01 +05:00
Saad Jutt
9e53470947 fix: quick fix for executables 2022-03-23 17:15:46 +05:00
Saad Jutt
81f6605249 chore(release): 0.0.37 2022-03-23 09:23:20 +05:00
Muhammad Saad
0b45402946 Merge pull request #102 from sasjs/issue-95
fix: moved macros from codebase to drive
2022-03-23 09:23:01 +05:00
Saad Jutt
9ac3191891 fix: moved macros from codebase to drive 2022-03-23 09:19:33 +05:00
Saad Jutt
cd00aa2af8 fix: appStream html view 2022-03-22 21:52:39 +05:00
Saad Jutt
0147bcb701 fix(webin): closes #99 2022-03-22 21:28:31 +05:00
Saad Jutt
bf53ad30f4 chore(release): 0.0.36 2022-03-22 04:07:27 +05:00
Muhammad Saad
a003b8836b Merge pull request #98 from sasjs/issue-91
feat: App Stream
2022-03-22 04:02:26 +05:00
Saad Jutt
df6003df94 fix(appstream): app logo + improvements 2022-03-22 03:55:51 +05:00
Saad Jutt
b1d0fdbb02 chore(release): 0.0.35 2022-03-21 18:24:35 +05:00
Muhammad Saad
2c34395110 Merge pull request #97 from sasjs/issue-96
feat(cors): whitelisting is configurable through .env variables
2022-03-21 18:21:39 +05:00
Saad Jutt
534e4e5bf3 chore: README.md updated 2022-03-21 18:17:26 +05:00
Saad Jutt
6146372eba chore: README.md updated 2022-03-21 18:05:40 +05:00
Saad Jutt
aaa469a142 chore: .env.example updated 2022-03-21 17:54:20 +05:00
Saad Jutt
4fd5bf948e fix(cors): removed trailing slashes of urls 2022-03-21 17:49:28 +05:00
Saad Jutt
99f91fbce2 feat(cors): whitelisting is configurable through .env variables 2022-03-21 17:36:42 +05:00
Saad Jutt
98a00ec7ac feat: App Stream, load on startup, new route added 2022-03-21 17:17:29 +05:00
32 changed files with 615 additions and 173 deletions

View File

@@ -2,6 +2,74 @@
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. 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.41](https://github.com/sasjs/server/compare/v0.0.40...v0.0.41) (2022-03-23)
### Bug Fixes
* **scroll:** closes [#100](https://github.com/sasjs/server/issues/100) ([f4eb75f](https://github.com/sasjs/server/commit/f4eb75ff347e78ac334e55ee26fbdd247bb8eaa2))
### [0.0.40](https://github.com/sasjs/server/compare/v0.0.39...v0.0.40) (2022-03-23)
### Bug Fixes
* **deploy:** validating empty file or service in filetree ([27e260e](https://github.com/sasjs/server/commit/27e260e6a453e9978830db63ab669bd48c029897))
* macros available for SAS ([7a70d40](https://github.com/sasjs/server/commit/7a70d40dbf0cd91cb3af156755f10006b860f917))
* moved macros from codebase to drive ([d27e070](https://github.com/sasjs/server/commit/d27e070fc83894854278df22a8223b8016a1f5f7))
### [0.0.39](https://github.com/sasjs/server/compare/v0.0.38...v0.0.39) (2022-03-23)
### Bug Fixes
* included sasjs core macros at compile time ([e680901](https://github.com/sasjs/server/commit/e68090181acd844f86f3e81153cb5a4e3f4a307f))
### [0.0.38](https://github.com/sasjs/server/compare/v0.0.37...v0.0.38) (2022-03-23)
### Bug Fixes
* quick fix for executables ([9e53470](https://github.com/sasjs/server/commit/9e53470947350f4b8d835a2cb6b70e3dabf247c4))
### [0.0.37](https://github.com/sasjs/server/compare/v0.0.36...v0.0.37) (2022-03-23)
### Bug Fixes
* appStream html view ([cd00aa2](https://github.com/sasjs/server/commit/cd00aa2af8c7e0df851050a02152dfeddaec7b0f))
* moved macros from codebase to drive ([9ac3191](https://github.com/sasjs/server/commit/9ac3191891bf53ff07135ccec6ddc83b34ea871a))
* **webin:** closes [#99](https://github.com/sasjs/server/issues/99) ([0147bcb](https://github.com/sasjs/server/commit/0147bcb701a209266144147a3746baf1eb1ccc63))
### [0.0.36](https://github.com/sasjs/server/compare/v0.0.35...v0.0.36) (2022-03-21)
### Features
* App Stream, load on startup, new route added ([98a00ec](https://github.com/sasjs/server/commit/98a00ec7ace5da765f049864799be44ba6538e8a))
### Bug Fixes
* **appstream:** app logo + improvements ([df6003d](https://github.com/sasjs/server/commit/df6003df942fd52b956f3d4069d6d7615441d372))
### [0.0.35](https://github.com/sasjs/server/compare/v0.0.33...v0.0.35) (2022-03-21)
### Features
* **cors:** whitelisting is configurable through .env variables ([99f91fb](https://github.com/sasjs/server/commit/99f91fbce2a029dd963ed30c9007a9b046ea6560))
* **web:** directory tree in sidebar of drive should be expanded by default at root level ([3d89b75](https://github.com/sasjs/server/commit/3d89b753f023beed4d51a64db4f74e1011437aab))
### Bug Fixes
* **cors:** removed trailing slashes of urls ([4fd5bf9](https://github.com/sasjs/server/commit/4fd5bf948e4ad8a274d3176d5509163e67980061))
* desktop mode web index.html js script included ([75291f9](https://github.com/sasjs/server/commit/75291f939770de963d48c2ff1c967da9493bd668))
* preferred to show param errors from query ([fd26298](https://github.com/sasjs/server/commit/fd2629862f10ec16e2266d68420499e715b5d58c))
* **stp:** write original file name in sas code for upload ([8822de9](https://github.com/sasjs/server/commit/8822de95df1d2d01dadfe6957391c254172f2819))
* **web-drive:** upon delete remove entry of deleted file from directory tree in sidebar ([fb77d99](https://github.com/sasjs/server/commit/fb77d99177851e7dc2a71e0b8f516daa3da29e36))
### [0.0.34](https://github.com/sasjs/server/compare/v0.0.33...v0.0.34) (2022-03-18) ### [0.0.34](https://github.com/sasjs/server/compare/v0.0.33...v0.0.34) (2022-03-18)

View File

@@ -13,7 +13,7 @@ SASjs Server is available in two modes - Desktop (without authentication) and Se
## Installation ## Installation
Installation can be made programmatically using command line, or by manually downloading and running the executable. Installation can be made programmatically using command line, or by manually downloading and running the executable.
### Programmatic ### Programmatic
@@ -48,16 +48,32 @@ When launching the app, it will make use of specific environment variables. Thes
Example contents of a `.env` file: Example contents of a `.env` file:
``` ```
MODE=desktop # options: [desktop|server] default: desktop MODE=desktop # options: [desktop|server] default: `desktop`
CORS=disable # options: [disable|enable] default: disable CORS=disable # options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
WHITELIST= # options: <http://localhost:3000 https://abc.com ...> space separated urls
PROTOCOL=http # options: [http|https] default: http PROTOCOL=http # options: [http|https] default: http
PORT=5000 # default: 5000 PORT=5000 # default: 5000
PORT_WEB=3000 # port for sasjs web component(react). default: 3000
# optional
# for MODE: `desktop`, prompts user
# for MODE: `server` gets value from api/package.json `configuration.sasPath`
SAS_PATH=/path/to/sas/executable.exe SAS_PATH=/path/to/sas/executable.exe
# optional
# for MODE: `desktop`, prompts user
# for MODE: `server` defaults to /tmp
DRIVE_PATH=/tmp DRIVE_PATH=/tmp
PROTOCOL=http # options: [http|https] default: http
# ENV variables required for PROTOCOL: `https`
PRIVATE_KEY=privkey.pem PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem FULL_CHAIN=fullchain.pem
# ENV variables required for MODE: `server`
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
``` ```
## Persisting the Session ## Persisting the Session
@@ -94,11 +110,10 @@ Instead of `app_name` you can pass:
- `all` to act on all processes - `all` to act on all processes
- `id` to act on a specific process id - `id` to act on a specific process id
## Server Version ## Server Version
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use. The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
* CLIENTID: `clientID1` - CLIENTID: `clientID1`
* USERNAME: `secretuser` - USERNAME: `secretuser`
* PASSWORD: `secretpassword` - PASSWORD: `secretpassword`

View File

@@ -1,10 +1,10 @@
MODE=[desktop|server] default considered as desktop MODE=[desktop|server] default considered as desktop
CORS=[disable|enable] default considered as disable CORS=[disable|enable] default considered as disable for server MODE & enable for desktop MODE
WHITELIST=<space separated urls, each starting with protocol `http` or `https`>
PROTOCOL=[http|https] default considered as http PROTOCOL=[http|https] default considered as http
PRIVATE_KEY=privkey.pem PRIVATE_KEY=privkey.pem
FULL_CHAIN=fullchain.pem FULL_CHAIN=fullchain.pem
PORT=[5000] default value is 5000 PORT=[5000] default value is 5000
PORT_WEB=[port for sasjs web component(react)] default value is 3000
ACCESS_TOKEN_SECRET=<secret> ACCESS_TOKEN_SECRET=<secret>
REFRESH_TOKEN_SECRET=<secret> REFRESH_TOKEN_SECRET=<secret>
AUTH_CODE_SECRET=<secret> AUTH_CODE_SECRET=<secret>

View File

@@ -29,6 +29,7 @@
"assets": [ "assets": [
"./build/public/**/*", "./build/public/**/*",
"./build/sasjsbuild/**/*", "./build/sasjsbuild/**/*",
"./build/sasjscore/**/*",
"./web/build/**/*" "./web/build/**/*"
], ],
"targets": [ "targets": [
@@ -88,5 +89,10 @@
}, },
"configuration": { "configuration": {
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas" "sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
},
"nodemonConfig": {
"ignore": [
"tmp/**/*"
]
} }
} }

21
api/public/sasjs-logo.svg Normal file
View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 32 32" style="enable-background:new 0 0 32 32;" xml:space="preserve">
<style type="text/css">
.st0{fill:#F6E40C;}
</style>
<rect id="XMLID_1_" width="32" height="32"/>
<g id="XMLID_654_">
<path id="XMLID_656_" class="st0" d="M27.9,17.4c-1.1,0-2.1,0-3,0c-1.2,0-2.3,0-3.5,0c-0.5,0-0.7,0.2-0.6,0.7c0,2.1,0,4.3,0,6.4
c0,0.5-0.2,0.8-0.6,1c-2.5,1.4-4.9,2.8-7.3,4.3c-0.4,0.2-0.6,0.2-1,0c-2.4-1.4-4.9-2.9-7.3-4.3c-0.2-0.1-0.5-0.5-0.5-0.7
c0-3.2,0-6.4,0-9.6c0-0.1,0-0.1,0.1-0.3c0.3,0,0.5,0,0.8,0c1.9,0,3.7,0,5.6,0c0.6,0,0.7-0.2,0.7-0.7c0-2.1,0-4.2,0-6.4
c0-0.5,0.1-0.8,0.6-1.1c2.5-1.4,4.9-2.9,7.3-4.3c0.2-0.1,0.6-0.1,0.9,0c2.5,1.4,5,2.9,7.5,4.4c0.2,0.1,0.4,0.4,0.4,0.6
C27.9,10.6,27.9,13.9,27.9,17.4z M20.8,14.8c1.4,0,2.7,0,4,0c0.5,0,0.7-0.2,0.7-0.7c0-1.7,0-3.3,0-5c0-0.5-0.2-0.7-0.6-1
c-1.6-0.9-3.2-1.9-4.8-2.8c-0.2-0.1-0.7-0.1-0.9,0c-1.6,0.9-3.2,1.9-4.8,2.8c-0.4,0.2-0.6,0.5-0.6,1c0,3.2,0,6.3,0,9.5
c0,1.9,0,1.9-1.9,1.9c-0.4,0-0.6-0.1-0.6-0.6c0-0.6,0-1.3,0-1.9c0-0.5-0.2-0.6-0.6-0.6c-1.1,0-2.2,0-3.3,0c-0.5,0-0.7,0.2-0.7,0.7
c0,1.6,0,3.3,0,4.9c0,0.5,0.2,0.8,0.6,1c1.6,0.9,3.2,1.9,4.8,2.8c0.2,0.1,0.7,0.1,0.9,0c1.6-0.9,3.2-1.9,4.8-2.8
c0.4-0.2,0.6-0.5,0.6-1c0-3.1,0-6.1,0-9.2c0-1.9,0-1.9,1.9-1.9c0.5,0,0.7,0.2,0.7,0.7C20.8,13.3,20.8,14,20.8,14.8z"/>
<path id="XMLID_655_" class="st0" d="M18,2.1l-6.8,3.9V2.7c0-0.3,0.3-0.6,0.6-0.6H18z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@@ -161,6 +161,8 @@ components:
$ref: '#/components/schemas/FolderMember' $ref: '#/components/schemas/FolderMember'
- -
$ref: '#/components/schemas/ServiceMember' $ref: '#/components/schemas/ServiceMember'
-
$ref: '#/components/schemas/FileMember'
type: array type: array
required: required:
- name - name
@@ -172,20 +174,30 @@ components:
enum: enum:
- service - service
type: string type: string
MemberType.file:
enum:
- file
type: string
ServiceMember: ServiceMember:
properties: properties:
name: name:
type: string type: string
type: type:
anyOf: $ref: '#/components/schemas/MemberType.service'
- code:
$ref: '#/components/schemas/MemberType.service' type: string
- required:
$ref: '#/components/schemas/MemberType.file' - name
- type
- code
type: object
additionalProperties: false
MemberType.file:
enum:
- file
type: string
FileMember:
properties:
name:
type: string
type:
$ref: '#/components/schemas/MemberType.file'
code: code:
type: string type: string
required: required:
@@ -203,6 +215,8 @@ components:
$ref: '#/components/schemas/FolderMember' $ref: '#/components/schemas/FolderMember'
- -
$ref: '#/components/schemas/ServiceMember' $ref: '#/components/schemas/ServiceMember'
-
$ref: '#/components/schemas/FileMember'
type: array type: array
required: required:
- members - members
@@ -214,6 +228,8 @@ components:
type: string type: string
message: message:
type: string type: string
streamServiceName:
type: string
example: example:
$ref: '#/components/schemas/FileTree' $ref: '#/components/schemas/FileTree'
required: required:
@@ -225,6 +241,8 @@ components:
properties: properties:
appLoc: appLoc:
type: string type: string
streamWebFolder:
type: string
fileTree: fileTree:
$ref: '#/components/schemas/FileTree' $ref: '#/components/schemas/FileTree'
required: required:

View File

@@ -1,7 +1,14 @@
import path from 'path' import path from 'path'
import { asyncForEach, copy, createFolder, deleteFolder } from '@sasjs/utils' import {
asyncForEach,
copy,
createFile,
createFolder,
deleteFolder,
listFilesInFolder
} from '@sasjs/utils'
import { apiRoot, sasJSCoreMacros } from '../src/utils' import { apiRoot, sasJSCoreMacros, sasJSCoreMacrosInfo } from '../src/utils'
const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core') const macroCorePath = path.join(apiRoot, 'node_modules', '@sasjs', 'core')
@@ -16,6 +23,10 @@ export const copySASjsCore = async () => {
await copy(coreSubFolderPath, sasJSCoreMacros) await copy(coreSubFolderPath, sasJSCoreMacros)
}) })
const fileNames = await listFilesInFolder(sasJSCoreMacros)
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
} }
copySASjsCore() copySASjsCore()

View File

@@ -7,7 +7,9 @@ import cors from 'cors'
import { import {
connectDB, connectDB,
copySASjsCore,
getWebBuildFolderPath, getWebBuildFolderPath,
loadAppStreamConfig,
sasJSCoreMacros, sasJSCoreMacros,
setProcessVariables setProcessVariables
} from './utils' } from './utils'
@@ -16,14 +18,17 @@ dotenv.config()
const app = express() const app = express()
const { MODE, CORS, PORT_WEB } = process.env const { MODE, CORS, WHITELIST } = process.env
const whiteList = [
`http://localhost:${PORT_WEB ?? 3000}`,
'https://sas.analytium.co.uk:8343'
]
if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') { if (MODE?.trim() !== 'server' || CORS?.trim() === 'enable') {
console.log('All CORS Requests are enabled') const whiteList: string[] = []
WHITELIST?.split(' ')?.forEach((url) => {
if (url.startsWith('http'))
// removing trailing slash of URLs listing for CORS
whiteList.push(url.replace(/\/$/, ''))
})
console.log('All CORS Requests are enabled for:', whiteList)
app.use(cors({ credentials: true, origin: whiteList })) app.use(cors({ credentials: true, origin: whiteList }))
} }
@@ -38,17 +43,19 @@ const onError: ErrorRequestHandler = (err, req, res, next) => {
} }
export default setProcessVariables().then(async () => { export default setProcessVariables().then(async () => {
await copySASjsCore()
// loading these modules after setting up variables due to // loading these modules after setting up variables due to
// multer's usage of process var process.driveLoc // multer's usage of process var process.driveLoc
const { setupRoutes } = await import('./routes/setupRoutes') const { setupRoutes } = await import('./routes/setupRoutes')
setupRoutes(app) setupRoutes(app)
await loadAppStreamConfig()
// should be served after setting up web route // should be served after setting up web route
// index.html needs to be injected with some js script. // index.html needs to be injected with some js script.
app.use(express.static(getWebBuildFolderPath())) app.use(express.static(getWebBuildFolderPath()))
console.log('sasJSCoreMacros', sasJSCoreMacros)
app.use(onError) app.use(onError)
await connectDB() await connectDB()

View File

@@ -29,12 +29,14 @@ import { getTmpFilesFolderPath } from '../utils'
interface DeployPayload { interface DeployPayload {
appLoc: string appLoc: string
streamWebFolder?: string
fileTree: FileTree fileTree: FileTree
} }
interface DeployResponse { interface DeployResponse {
status: string status: string
message: string message: string
streamServiceName?: string
example?: FileTree example?: FileTree
} }
@@ -190,14 +192,23 @@ const getFileTree = () => {
} }
const deploy = async (data: DeployPayload) => { const deploy = async (data: DeployPayload) => {
const driveFilesPath = getTmpFilesFolderPath()
const appLocParts = data.appLoc.replace(/^\//, '').split('/')
const appLocPath = path
.join(getTmpFilesFolderPath(), ...appLocParts)
.replace(new RegExp('/', 'g'), path.sep)
if (!appLocPath.includes(driveFilesPath)) {
throw new Error('appLoc cannot be outside drive.')
}
if (!isFileTree(data.fileTree)) { if (!isFileTree(data.fileTree)) {
throw { code: 400, ...invalidDeployFormatResponse } throw { code: 400, ...invalidDeployFormatResponse }
} }
await createFileTree( await createFileTree(data.fileTree.members, appLocParts).catch((err) => {
data.fileTree.members,
data.appLoc.replace(/^\//, '').split('/')
).catch((err) => {
throw { code: 500, ...execDeployErrorResponse, ...err } throw { code: 500, ...execDeployErrorResponse, ...err }
}) })

View File

@@ -13,9 +13,9 @@ import {
extractHeaders, extractHeaders,
generateFileUploadSasCode, generateFileUploadSasCode,
getTmpFilesFolderPath, getTmpFilesFolderPath,
getTmpMacrosPath,
HTTPHeaders, HTTPHeaders,
isDebugOn, isDebugOn
sasJSCoreMacros
} from '../../utils' } from '../../utils'
export interface ExecutionVars { export interface ExecutionVars {
@@ -106,7 +106,7 @@ export class ExecutionController {
` `
program = ` program = `
options insert=(SASAUTOS="${sasJSCoreMacros}"); options insert=(SASAUTOS="${getTmpMacrosPath()}");
/* runtime vars */ /* runtime vars */
${varStatments} ${varStatments}

View File

@@ -1,11 +1,17 @@
import path from 'path' import path from 'path'
import { MemberType, FolderMember, ServiceMember, FileTree } from '../../types' import {
MemberType,
FolderMember,
ServiceMember,
FileTree,
FileMember
} from '../../types'
import { getTmpFilesFolderPath } from '../../utils/file' import { getTmpFilesFolderPath } from '../../utils/file'
import { createFolder, createFile, asyncForEach } from '@sasjs/utils' import { createFolder, createFile, asyncForEach } from '@sasjs/utils'
// REFACTOR: export FileTreeCpntroller // REFACTOR: export FileTreeCpntroller
export const createFileTree = async ( export const createFileTree = async (
members: (FolderMember | ServiceMember)[], members: (FolderMember | ServiceMember | FileMember)[],
parentFolders: string[] = [] parentFolders: string[] = []
) => { ) => {
const destinationPath = path.join( const destinationPath = path.join(
@@ -13,29 +19,32 @@ export const createFileTree = async (
path.join(...parentFolders) path.join(...parentFolders)
) )
await asyncForEach(members, async (member: FolderMember | ServiceMember) => { await asyncForEach(
let name = member.name members,
async (member: FolderMember | ServiceMember | FileMember) => {
let name = member.name
if (member.type === MemberType.service) name += '.sas' if (member.type === MemberType.service) name += '.sas'
if (member.type === MemberType.folder) { if (member.type === MemberType.folder) {
await createFolder(path.join(destinationPath, name)).catch((err) => await createFolder(path.join(destinationPath, name)).catch((err) =>
Promise.reject({ error: err, failedToCreate: name }) Promise.reject({ error: err, failedToCreate: name })
) )
await createFileTree(member.members, [...parentFolders, name]).catch( await createFileTree(member.members, [...parentFolders, name]).catch(
(err) => Promise.reject({ error: err, failedToCreate: name }) (err) => Promise.reject({ error: err, failedToCreate: name })
) )
} else { } else {
const encoding = member.type === MemberType.file ? 'base64' : undefined const encoding = member.type === MemberType.file ? 'base64' : undefined
await createFile( await createFile(
path.join(destinationPath, name), path.join(destinationPath, name),
member.code, member.code,
encoding encoding
).catch((err) => Promise.reject({ error: err, failedToCreate: name })) ).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
}
} }
}) )
return Promise.resolve() return Promise.resolve()
} }

View File

@@ -1,9 +1,8 @@
import path from 'path' import path from 'path'
import { Request } from 'express' import { Request } from 'express'
import multer, { FileFilterCallback, Options } from 'multer' import multer, { FileFilterCallback, Options } from 'multer'
import { getTmpUploadsPath } from '../utils' import { blockFileRegex, getTmpUploadsPath } from '../utils'
const acceptableExtensions = ['.sas']
const fieldNameSize = 300 const fieldNameSize = 300
const fileSize = 10485760 // 10 MB const fileSize = 10485760 // 10 MB
@@ -31,15 +30,11 @@ const fileFilter: Options['fileFilter'] = (
file: Express.Multer.File, file: Express.Multer.File,
callback: FileFilterCallback callback: FileFilterCallback
) => { ) => {
const fileExtension = path.extname(file.originalname).toLocaleLowerCase() const fileExtension = path.extname(file.originalname)
const shouldBlockUpload = blockFileRegex.test(file.originalname)
if (!acceptableExtensions.includes(fileExtension)) { if (shouldBlockUpload) {
return callback( return callback(
new Error( new Error(`File extension '${fileExtension}' not acceptable.`)
`File extension '${fileExtension}' not acceptable. Valid extension(s): ${acceptableExtensions.join(
', '
)}`
)
) )
} }

View File

@@ -22,9 +22,15 @@ driveRouter.post('/deploy', async (req, res) => {
try { try {
const response = await controller.deploy(body) const response = await controller.deploy(body)
const appLoc = body.appLoc.replace(/^\//, '')?.split('/') if (body.streamWebFolder) {
const { streamServiceName } = await publishAppStream(
publishAppStream(appLoc) body.appLoc,
body.streamWebFolder,
body.streamServiceName,
body.streamLogo
)
response.streamServiceName = streamServiceName
}
res.send(response) res.send(response)
} catch (err: any) { } catch (err: any) {

View File

@@ -266,7 +266,7 @@ describe('files', () => {
it("should respond with Bad Request if filePath doesn't has correct extension", async () => { it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas') const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.oth' const pathToUpload = '/my/path/code.exe'
const res = await request(app) const res = await request(app)
.post(`/SASjsApi/drive/file?_filePath=${pathToUpload}`) .post(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
@@ -275,7 +275,7 @@ describe('files', () => {
.attach('file', fileToAttachPath) .attach('file', fileToAttachPath)
.expect(400) .expect(400)
expect(res.text).toEqual('Valid extensions for filePath: .sas') expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -293,7 +293,7 @@ describe('files', () => {
}) })
it("should respond with Bad Request if attached file doesn't has correct extension", async () => { it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth') const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
const pathToUpload = '/my/path/code.sas' const pathToUpload = '/my/path/code.sas'
const res = await request(app) const res = await request(app)
@@ -303,9 +303,7 @@ describe('files', () => {
.attach('file', fileToAttachPath) .attach('file', fileToAttachPath)
.expect(400) .expect(400)
expect(res.text).toEqual( expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
`File extension '.oth' not acceptable. Valid extension(s): .sas`
)
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -426,7 +424,7 @@ describe('files', () => {
it("should respond with Bad Request if filePath doesn't has correct extension", async () => { it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas') const fileToAttachPath = path.join(__dirname, 'files', 'sample.sas')
const pathToUpload = '/my/path/code.oth' const pathToUpload = '/my/path/code.exe'
const res = await request(app) const res = await request(app)
.patch(`/SASjsApi/drive/file?_filePath=${pathToUpload}`) .patch(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
@@ -435,7 +433,7 @@ describe('files', () => {
.attach('file', fileToAttachPath) .attach('file', fileToAttachPath)
.expect(400) .expect(400)
expect(res.text).toEqual('Valid extensions for filePath: .sas') expect(res.text).toEqual('Invalid file extension')
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })
@@ -453,7 +451,7 @@ describe('files', () => {
}) })
it("should respond with Bad Request if attached file doesn't has correct extension", async () => { it("should respond with Bad Request if attached file doesn't has correct extension", async () => {
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth') const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
const pathToUpload = '/my/path/code.sas' const pathToUpload = '/my/path/code.sas'
const res = await request(app) const res = await request(app)
@@ -463,9 +461,7 @@ describe('files', () => {
.attach('file', fileToAttachPath) .attach('file', fileToAttachPath)
.expect(400) .expect(400)
expect(res.text).toEqual( expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
`File extension '.oth' not acceptable. Valid extension(s): .sas`
)
expect(res.body).toEqual({}) expect(res.body).toEqual({})
}) })

View File

@@ -0,0 +1,54 @@
import { AppStreamConfig } from '../../types'
const style = `<style>
* {
font-family: 'Roboto', sans-serif;
}
.app-container {
display: flex;
flex-wrap: wrap;
align-items: baseline;
justify-content: center;
}
.app-container .app {
width: 150px;
margin: 10px;
overflow: hidden;
border-radius: 10px 10px 0 0;
text-align: center;
}
.app-container .app img{
width: 100%;
margin-bottom: 10px;
}
</style>`
const defaultAppLogo = '/sasjs-logo.svg'
const singleAppStreamHtml = (
streamServiceName: string,
appLoc: string,
logo?: string
) =>
` <a class="app" href="${streamServiceName}" title="${appLoc}">
<img src="${logo ? streamServiceName + '/' + logo : defaultAppLogo}" />
${streamServiceName}
</a>`
export const appStreamHtml = (appStreamConfig: AppStreamConfig) => `
<html>
<head>
<base href="/AppStream/">
${style}
</head>
<body>
<h1>App Stream</h1>
<div class="app-container">
${Object.entries(appStreamConfig)
.map(([streamServiceName, entry]) =>
singleAppStreamHtml(streamServiceName, entry.appLoc, entry.streamLogo)
)
.join('')}
</div>
</body>
</html>`

View File

@@ -2,25 +2,75 @@ import path from 'path'
import express from 'express' import express from 'express'
import { folderExists } from '@sasjs/utils' import { folderExists } from '@sasjs/utils'
import { getTmpFilesFolderPath } from '../../utils' import { addEntryToAppStreamConfig, getTmpFilesFolderPath } from '../../utils'
import { appStreamHtml } from './appStreamHtml'
const router = express.Router() const router = express.Router()
export const publishAppStream = async (appLoc: string[]) => { router.get('/', async (_, res) => {
const appLocUrl = encodeURI(appLoc.join('/')) const content = appStreamHtml(process.appStreamConfig)
const appLocPath = appLoc.join(path.sep)
const pathToDeployment = path.join( return res.send(content)
getTmpFilesFolderPath(), })
appLocPath,
'services', export const publishAppStream = async (
'webv' appLoc: string,
) streamWebFolder: string,
streamServiceName?: string,
streamLogo?: string,
addEntryToFile: boolean = true
) => {
const driveFilesPath = getTmpFilesFolderPath()
const appLocParts = appLoc.replace(/^\//, '')?.split('/')
const appLocPath = path.join(driveFilesPath, ...appLocParts)
if (!appLocPath.includes(driveFilesPath)) {
throw new Error('appLoc cannot be outside drive.')
}
const pathToDeployment = path.join(appLocPath, 'services', streamWebFolder)
if (!pathToDeployment.includes(appLocPath)) {
throw new Error('streamWebFolder cannot be outside appLoc.')
}
if (await folderExists(pathToDeployment)) { if (await folderExists(pathToDeployment)) {
router.use(`/${appLocUrl}`, express.static(pathToDeployment)) const appCount = process.appStreamConfig
console.log('Serving Stream App: ', appLocUrl) ? Object.keys(process.appStreamConfig).length
: 0
if (!streamServiceName) {
streamServiceName = `AppStreamName${appCount + 1}`
} else {
const alreadyDeployed = process.appStreamConfig[streamServiceName]
if (alreadyDeployed) {
if (alreadyDeployed.appLoc === appLoc) {
// redeploying to same streamServiceName
} else {
// trying to deploy to another existing streamServiceName
// assign new streamServiceName
streamServiceName = `${streamServiceName}-${appCount + 1}`
}
}
}
router.use(`/${streamServiceName}`, express.static(pathToDeployment))
addEntryToAppStreamConfig(
streamServiceName,
appLoc,
streamWebFolder,
streamLogo,
addEntryToFile
)
const sasJsPort = process.env.PORT ?? 5000
console.log(
'Serving Stream App: ',
`http://localhost:${sasJsPort}/AppStream/${streamServiceName}`
)
return { streamServiceName }
} }
return {}
} }
export default router export default router

View File

@@ -0,0 +1,7 @@
export interface AppStreamConfig {
[key: string]: {
appLoc: string
streamWebFolder: string
streamLogo?: string
}
}

View File

@@ -1,23 +1,28 @@
export interface FileTree { export enum MemberType {
members: (FolderMember | ServiceMember)[] service = 'service',
file = 'file',
folder = 'folder'
} }
export enum MemberType { export interface ServiceMember {
folder = 'folder', name: string
service = 'service', type: MemberType.service
file = 'file' code: string
}
export interface FileMember {
name: string
type: MemberType.file
code: string
} }
export interface FolderMember { export interface FolderMember {
name: string name: string
type: MemberType.folder type: MemberType.folder
members: (FolderMember | ServiceMember)[] members: (FolderMember | ServiceMember | FileMember)[]
} }
export interface FileTree {
export interface ServiceMember { members: (FolderMember | ServiceMember | FileMember)[]
name: string
type: MemberType.service | MemberType.file
code: string
} }
export const isFileTree = (arg: any): arg is FileTree => export const isFileTree = (arg: any): arg is FileTree =>
@@ -25,11 +30,25 @@ export const isFileTree = (arg: any): arg is FileTree =>
arg.members && arg.members &&
Array.isArray(arg.members) && Array.isArray(arg.members) &&
arg.members.filter( arg.members.filter(
(member: FolderMember | ServiceMember) => (member: ServiceMember | FileMember | FolderMember) =>
!isFolderMember(member) && !isServiceMember(member) !isServiceMember(member, '-') &&
!isFileMember(member, '-') &&
!isFolderMember(member, '-')
).length === 0 ).length === 0
const isFolderMember = (arg: any): arg is FolderMember => const isServiceMember = (arg: any, pre: string): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
typeof arg.code === 'string'
const isFileMember = (arg: any, pre: string): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.file &&
typeof arg.code === 'string'
const isFolderMember = (arg: any, pre: string): arg is FolderMember =>
arg && arg &&
typeof arg.name === 'string' && typeof arg.name === 'string' &&
arg.type === MemberType.folder && arg.type === MemberType.folder &&
@@ -37,21 +56,7 @@ const isFolderMember = (arg: any): arg is FolderMember =>
Array.isArray(arg.members) && Array.isArray(arg.members) &&
arg.members.filter( arg.members.filter(
(member: FolderMember | ServiceMember) => (member: FolderMember | ServiceMember) =>
!isFolderMember(member) && !isServiceMember(member, pre + '-') &&
!isServiceMember(member) && !isFileMember(member, pre + '-') &&
!isFileMember(member) !isFolderMember(member, pre + '-')
).length === 0 ).length === 0
const isServiceMember = (arg: any): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.service &&
arg.code &&
typeof arg.code === 'string'
const isFileMember = (arg: any): arg is ServiceMember =>
arg &&
typeof arg.name === 'string' &&
arg.type === MemberType.file &&
arg.code &&
typeof arg.code === 'string'

View File

@@ -3,5 +3,6 @@ declare namespace NodeJS {
sasLoc: string sasLoc: string
driveLoc: string driveLoc: string
sessionController?: import('../controllers/internal').SessionController sessionController?: import('../controllers/internal').SessionController
appStreamConfig: import('./').AppStreamConfig
} }
} }

View File

@@ -1,4 +1,5 @@
// TODO: uppercase types // TODO: uppercase types
export * from './AppStreamConfig'
export * from './Execution' export * from './Execution'
export * from './FileTree' export * from './FileTree'
export * from './InfoJWT' export * from './InfoJWT'

View File

@@ -0,0 +1,87 @@
import { createFile, fileExists, readFile } from '@sasjs/utils'
import { publishAppStream } from '../routes/appStream'
import { AppStreamConfig } from '../types'
import { getTmpAppStreamConfigPath } from './file'
export const loadAppStreamConfig = async () => {
const appStreamConfigPath = getTmpAppStreamConfigPath()
const content = (await fileExists(appStreamConfigPath))
? await readFile(appStreamConfigPath)
: '{}'
let appStreamConfig: AppStreamConfig
try {
appStreamConfig = JSON.parse(content)
if (!isValidAppStreamConfig(appStreamConfig)) throw 'invalid type'
} catch (_) {
appStreamConfig = {}
}
process.appStreamConfig = {}
for (const [streamServiceName, entry] of Object.entries(appStreamConfig)) {
const { appLoc, streamWebFolder, streamLogo } = entry
publishAppStream(
appLoc,
streamWebFolder,
streamServiceName,
streamLogo,
false
)
}
console.log('App Stream Config loaded!')
}
export const addEntryToAppStreamConfig = (
streamServiceName: string,
appLoc: string,
streamWebFolder: string,
streamLogo?: string,
addEntryToFile: boolean = true
) => {
if (streamServiceName && appLoc && streamWebFolder) {
process.appStreamConfig[streamServiceName] = {
appLoc,
streamWebFolder,
streamLogo
}
if (addEntryToFile) saveAppStreamConfig()
}
}
export const removeEntryFromAppStreamConfig = (streamServiceName: string) => {
if (streamServiceName) {
delete process.appStreamConfig[streamServiceName]
saveAppStreamConfig()
}
}
const saveAppStreamConfig = async () => {
const appStreamConfigPath = getTmpAppStreamConfigPath()
try {
await createFile(
appStreamConfigPath,
JSON.stringify(process.appStreamConfig, null, 2)
)
} catch (_) {}
}
const isValidAppStreamConfig = (config: any) => {
if (config) {
return !Object.entries(config).some(([streamServiceName, entry]) => {
const { appLoc, streamWebFolder, streamLogo } = entry as any
return (
typeof streamServiceName !== 'string' ||
typeof appLoc !== 'string' ||
typeof streamWebFolder !== 'string'
)
})
}
return false
}

View File

@@ -0,0 +1,32 @@
import path from 'path'
import {
asyncForEach,
createFile,
createFolder,
deleteFolder,
readFile
} from '@sasjs/utils'
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
export const copySASjsCore = async () => {
console.log('Copying Macros from container to drive(tmp).')
const macrosDrivePath = getTmpMacrosPath()
await deleteFolder(macrosDrivePath)
await createFolder(macrosDrivePath)
const macros = await readFile(sasJSCoreMacrosInfo)
await asyncForEach(macros.split('\n'), async (macroName) => {
const macroFileSourcePath = path.join(sasJSCoreMacros, macroName)
const macroContent = await readFile(macroFileSourcePath)
const macroFileDestPath = path.join(macrosDrivePath, macroName)
await createFile(macroFileDestPath, macroContent)
})
console.log('Macros Drive Path:', macrosDrivePath)
}

View File

@@ -9,12 +9,18 @@ export const sysInitCompiledPath = path.join(
) )
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore') export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
export const sasJSCoreMacrosInfo = path.join(apiRoot, 'sasjscore', '.macrolist')
export const getWebBuildFolderPath = () => export const getWebBuildFolderPath = () =>
path.join(codebaseRoot, 'web', 'build') path.join(codebaseRoot, 'web', 'build')
export const getTmpFolderPath = () => process.driveLoc export const getTmpFolderPath = () => process.driveLoc
export const getTmpAppStreamConfigPath = () =>
path.join(getTmpFolderPath(), 'appStreamConfig.json')
export const getTmpMacrosPath = () => path.join(getTmpFolderPath(), 'sasjscore')
export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads') export const getTmpUploadsPath = () => path.join(getTmpFolderPath(), 'uploads')
export const getTmpFilesFolderPath = () => export const getTmpFilesFolderPath = () =>

View File

@@ -1,12 +1,14 @@
export * from './appStreamConfig'
export * from './connectDB' export * from './connectDB'
export * from './copySASjsCore'
export * from './extractHeaders' export * from './extractHeaders'
export * from './file' export * from './file'
export * from './generateAccessToken' export * from './generateAccessToken'
export * from './generateAuthCode' export * from './generateAuthCode'
export * from './generateRefreshToken' export * from './generateRefreshToken'
export * from './isDebugOn'
export * from './getCertificates' export * from './getCertificates'
export * from './getDesktopFields' export * from './getDesktopFields'
export * from './isDebugOn'
export * from './parseLogToArray' export * from './parseLogToArray'
export * from './removeTokensInDB' export * from './removeTokensInDB'
export * from './saveTokensInDB' export * from './saveTokensInDB'

View File

@@ -1,9 +1,21 @@
import path from 'path'
import fs from 'fs'
import { getTmpSessionsFolderPath } from '.'
import { MulterFile } from '../types/Upload' import { MulterFile } from '../types/Upload'
import { listFilesInFolder } from '@sasjs/utils' import { listFilesInFolder } from '@sasjs/utils'
interface FilenameMapSingle {
fieldName: string
originalName: string
}
interface FilenamesMap {
[key: string]: FilenameMapSingle
}
interface UploadedFiles extends FilenameMapSingle {
fileref: string
filepath: string
count: number
}
/** /**
* It will create an object that maps hashed file names to the original names * It will create an object that maps hashed file names to the original names
* @param files array of files to be mapped * @param files array of files to be mapped
@@ -12,10 +24,13 @@ import { listFilesInFolder } from '@sasjs/utils'
export const makeFilesNamesMap = (files: MulterFile[]) => { export const makeFilesNamesMap = (files: MulterFile[]) => {
if (!files) return null if (!files) return null
const filesNamesMap: { [key: string]: string } = {} const filesNamesMap: FilenamesMap = {}
for (let file of files) { for (let file of files) {
filesNamesMap[file.filename] = file.originalname filesNamesMap[file.filename] = {
fieldName: file.fieldname,
originalName: file.originalname
}
} }
return filesNamesMap return filesNamesMap
@@ -28,17 +43,12 @@ export const makeFilesNamesMap = (files: MulterFile[]) => {
* @returns generated sas code * @returns generated sas code
*/ */
export const generateFileUploadSasCode = async ( export const generateFileUploadSasCode = async (
filesNamesMap: any, filesNamesMap: FilenamesMap,
sasSessionFolder: string sasSessionFolder: string
): Promise<string> => { ): Promise<string> => {
let uploadSasCode = '' let uploadSasCode = ''
let fileCount = 0 let fileCount = 0
let uploadedFilesMap: { const uploadedFiles: UploadedFiles[] = []
fileref: string
filepath: string
filename: string
count: number
}[] = []
const sasSessionFolderList: string[] = await listFilesInFolder( const sasSessionFolderList: string[] = await listFilesInFolder(
sasSessionFolder sasSessionFolder
@@ -50,31 +60,32 @@ export const generateFileUploadSasCode = async (
if (fileName.includes('req_file')) { if (fileName.includes('req_file')) {
fileCount++ fileCount++
uploadedFilesMap.push({ uploadedFiles.push({
fileref: `_sjs${fileCountString}`, fileref: `_sjs${fileCountString}`,
filepath: `${sasSessionFolder}/${fileName}`, filepath: `${sasSessionFolder}/${fileName}`,
filename: filesNamesMap[fileName], originalName: filesNamesMap[fileName].originalName,
fieldName: filesNamesMap[fileName].fieldName,
count: fileCount count: fileCount
}) })
} }
}) })
for (let uploadedMap of uploadedFilesMap) { for (const uploadedFile of uploadedFiles) {
uploadSasCode += `\nfilename ${uploadedMap.fileref} "${uploadedMap.filepath}";` uploadSasCode += `\nfilename ${uploadedFile.fileref} "${uploadedFile.filepath}";`
} }
uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};` uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};`
for (let uploadedMap of uploadedFilesMap) { for (const uploadedFile of uploadedFiles) {
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedMap.count}=${uploadedMap.filename};` uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedFile.count}=${uploadedFile.originalName};`
} }
for (let uploadedMap of uploadedFilesMap) { for (const uploadedFile of uploadedFiles) {
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedMap.count}=${uploadedMap.fileref};` uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedFile.count}=${uploadedFile.fileref};`
} }
for (let uploadedMap of uploadedFilesMap) { for (const uploadedFile of uploadedFiles) {
uploadSasCode += `\n%let _WEBIN_NAME${uploadedMap.count}=${uploadedMap.filepath};` uploadSasCode += `\n%let _WEBIN_NAME${uploadedFile.count}=${uploadedFile.fieldName};`
} }
if (fileCount > 0) { if (fileCount > 0) {

View File

@@ -3,6 +3,8 @@ import Joi from 'joi'
const usernameSchema = Joi.string().alphanum().min(6).max(20) const usernameSchema = Joi.string().alphanum().min(6).max(20)
const passwordSchema = Joi.string().min(6).max(1024) const passwordSchema = Joi.string().min(6).max(1024)
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
export const authorizeValidation = (data: any): Joi.ValidationResult => export const authorizeValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
username: usernameSchema.required(), username: usernameSchema.required(),
@@ -69,21 +71,31 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
export const deployValidation = (data: any): Joi.ValidationResult => export const deployValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
appLoc: Joi.string().pattern(/^\//).required().min(2), appLoc: Joi.string().pattern(/^\//).required().min(2),
streamServiceName: Joi.string(),
streamWebFolder: Joi.string(),
streamLogo: Joi.string(),
fileTree: Joi.any().required() fileTree: Joi.any().required()
}).validate(data) }).validate(data)
const filePathSchema = Joi.string()
.custom((value, helpers) => {
if (blockFileRegex.test(value)) return helpers.error('string.pattern.base')
return value
})
.required()
.messages({
'string.pattern.base': `Invalid file extension`
})
export const fileBodyValidation = (data: any): Joi.ValidationResult => export const fileBodyValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
filePath: Joi.string().pattern(/.sas$/).required().messages({ filePath: filePathSchema
'string.pattern.base': `Valid extensions for filePath: .sas`
})
}).validate(data) }).validate(data)
export const fileParamValidation = (data: any): Joi.ValidationResult => export const fileParamValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
_filePath: Joi.string().pattern(/.sas$/).required().messages({ _filePath: filePathSchema
'string.pattern.base': `Valid extensions for filePath: .sas`
})
}).validate(data) }).validate(data)
export const runSASValidation = (data: any): Joi.ValidationResult => export const runSASValidation = (data: any): Joi.ValidationResult =>

20
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "server", "name": "server",
"version": "0.0.34", "version": "0.0.41",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "server", "name": "server",
"version": "0.0.34", "version": "0.0.41",
"devDependencies": { "devDependencies": {
"prettier": "^2.3.1", "prettier": "^2.3.1",
"standard-version": "^9.3.2" "standard-version": "^9.3.2"
@@ -404,14 +404,14 @@
} }
}, },
"node_modules/conventional-changelog-writer": { "node_modules/conventional-changelog-writer": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz", "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz",
"integrity": "sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g==", "integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"conventional-commits-filter": "^2.0.7", "conventional-commits-filter": "^2.0.7",
"dateformat": "^3.0.0", "dateformat": "^3.0.0",
"handlebars": "^4.7.6", "handlebars": "^4.7.7",
"json-stringify-safe": "^5.0.1", "json-stringify-safe": "^5.0.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"meow": "^8.0.0", "meow": "^8.0.0",
@@ -2433,14 +2433,14 @@
"dev": true "dev": true
}, },
"conventional-changelog-writer": { "conventional-changelog-writer": {
"version": "5.0.0", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz", "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz",
"integrity": "sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g==", "integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"conventional-commits-filter": "^2.0.7", "conventional-commits-filter": "^2.0.7",
"dateformat": "^3.0.0", "dateformat": "^3.0.0",
"handlebars": "^4.7.6", "handlebars": "^4.7.7",
"json-stringify-safe": "^5.0.1", "json-stringify-safe": "^5.0.1",
"lodash": "^4.17.15", "lodash": "^4.17.15",
"meow": "^8.0.0", "meow": "^8.0.0",

View File

@@ -1,6 +1,6 @@
{ {
"name": "server", "name": "server",
"version": "0.0.34", "version": "0.0.41",
"description": "NodeJS wrapper for calling the SAS binary executable", "description": "NodeJS wrapper for calling the SAS binary executable",
"repository": "https://github.com/sasjs/server", "repository": "https://github.com/sasjs/server",
"scripts": { "scripts": {

View File

@@ -1,16 +1,14 @@
### testing upload file example ### testing upload file example
POST http://localhost:5000/SASjsApi/stp/execute/?_program=/Public/app/viya/services/editors/loadfile&table=DCCONFIG.MPE_X_TEST POST http://localhost:5000/SASjsApi/stp/execute/?_program=/Public/app/viya/services/editors/loadfile&table=DCCONFIG.MPE_X_TEST
Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynkYOqevUMKZrXeAy Content-Type: multipart/form-data; boundary=----WebKitFormBoundarynkYOqevUMKZrXeAy
------WebKitFormBoundarynkYOqevUMKZrXeAy ------WebKitFormBoundarynkYOqevUMKZrXeAy
Content-Disposition: form-data; name="file"; filename="DCCONFIG.MPE_X_TEST.xlsx" Content-Disposition: form-data; name="fileSome11"; filename="DCCONFIG.MPE_X_TEST.xlsx"
Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet
------WebKitFormBoundarynkYOqevUMKZrXeAy ------WebKitFormBoundarynkYOqevUMKZrXeAy
Content-Disposition: form-data; name="file"; filename="DCCONFIG.MPE_X_TEST.xlsx.csv" Content-Disposition: form-data; name="fileSome22"; filename="DCCONFIG.MPE_X_TEST.xlsx.csv"
Content-Type: application/csv Content-Type: application/csv
_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM _____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM

View File

@@ -66,11 +66,21 @@ const Header = (props: any) => {
variant="contained" variant="contained"
color="primary" color="primary"
size="large" size="large"
startIcon={<OpenInNewIcon />} endIcon={<OpenInNewIcon />}
style={{ marginLeft: '50px' }}
> >
API Docs API Docs
</Button> </Button>
<Button
href={`${baseUrl}/AppStream`}
target="_blank"
rel="noreferrer"
variant="contained"
color="primary"
size="large"
endIcon={<OpenInNewIcon />}
>
App Stream
</Button>
</Toolbar> </Toolbar>
</AppBar> </AppBar>
) )

View File

@@ -70,6 +70,9 @@ const Studio = () => {
setWebout(`<pre><code>${webout}</code></pre>`) setWebout(`<pre><code>${webout}</code></pre>`)
setTab('2') setTab('2')
// Scroll to bottom of log
window.scrollTo(0, document.body.scrollHeight)
}) })
.catch((err) => console.log(err)) .catch((err) => console.log(err))
} }