mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f48aeb1b0b | ||
|
|
ee80f3f968 | ||
|
|
7f4201ba85 | ||
|
|
f830bbc058 | ||
|
|
f8e1522a5a | ||
|
|
0a5aeceab5 | ||
|
|
6dc39c0d91 | ||
|
|
117a53ceea | ||
|
|
dd56a95314 | ||
|
|
c5117abe71 | ||
|
|
84c632a861 | ||
|
|
3ddd09eba0 | ||
|
|
0c0301433c | ||
|
|
954b2e3e2e | ||
|
|
5655311b96 | ||
|
|
9ace33d783 | ||
|
|
adc5aca0f0 | ||
|
|
71c6be6b84 | ||
|
|
9c751877d1 | ||
|
|
2204d54cd6 | ||
|
|
f4eb75ff34 | ||
|
|
a3cde343b7 | ||
|
|
7a70d40dbf | ||
|
|
d27e070fc8 | ||
|
|
27e260e6a4 | ||
|
|
2796db8ead | ||
|
|
84f7c2ab89 | ||
|
|
e68090181a | ||
|
|
d2956fc641 | ||
|
|
a701bb25e7 | ||
|
|
5758bcd392 | ||
|
|
9e53470947 | ||
|
|
81f6605249 | ||
|
|
0b45402946 | ||
|
|
9ac3191891 | ||
|
|
cd00aa2af8 | ||
|
|
0147bcb701 | ||
|
|
bf53ad30f4 | ||
|
|
a003b8836b | ||
|
|
df6003df94 | ||
|
|
b1d0fdbb02 | ||
|
|
2c34395110 | ||
|
|
534e4e5bf3 | ||
|
|
6146372eba | ||
|
|
aaa469a142 | ||
|
|
4fd5bf948e | ||
|
|
99f91fbce2 | ||
|
|
98a00ec7ac |
104
CHANGELOG.md
104
CHANGELOG.md
@@ -2,6 +2,110 @@
|
||||
|
||||
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.45](https://github.com/sasjs/server/compare/v0.0.43...v0.0.45) (2022-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
|
||||
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
|
||||
* proving a PRINT destination during SAS invocation. ([7f4201b](https://github.com/sasjs/server/commit/7f4201ba855743144fa6d3efac2b11e816d4696e)), closes [#111](https://github.com/sasjs/server/issues/111)
|
||||
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
|
||||
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
|
||||
|
||||
### [0.0.44](https://github.com/sasjs/server/compare/v0.0.43...v0.0.44) (2022-03-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* DELETE req cannot have body ([0a5aece](https://github.com/sasjs/server/commit/0a5aeceab560b022197d0c30c3da7f091b261b1e))
|
||||
* increased req body size ([6dc39c0](https://github.com/sasjs/server/commit/6dc39c0d91ac13d6d9b8c0a2240446bfc45bdd7f))
|
||||
* **session:** increased session + bug fixed ([117a53c](https://github.com/sasjs/server/commit/117a53ceeadf487a6326384ae11c10e98646631f))
|
||||
* **stp:** use same session from file upload ([dd56a95](https://github.com/sasjs/server/commit/dd56a95314f0b61480489118734e45877e1745ef))
|
||||
|
||||
### [0.0.43](https://github.com/sasjs/server/compare/v0.0.42...v0.0.43) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deploy:** user can deploy to same appName with different/same appLoc ([9ace33d](https://github.com/sasjs/server/commit/9ace33d7830a9def42d741c23b46090afe0c5510))
|
||||
* fallback logo on AppStream ([5655311](https://github.com/sasjs/server/commit/5655311b9663225823c192b39a03f39d17dda730))
|
||||
|
||||
### [0.0.42](https://github.com/sasjs/server/compare/v0.0.41...v0.0.42) (2022-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* execute api, webout as raw ([9c75187](https://github.com/sasjs/server/commit/9c751877d1ed0d0677aff816169a1df7c34c6bf5))
|
||||
|
||||
### [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)
|
||||
|
||||
|
||||
|
||||
45
README.md
45
README.md
@@ -48,16 +48,42 @@ When launching the app, it will make use of specific environment variables. Thes
|
||||
Example contents of a `.env` file:
|
||||
|
||||
```
|
||||
MODE=desktop # options: [desktop|server] default: desktop
|
||||
CORS=disable # options: [disable|enable] default: disable
|
||||
PROTOCOL=http # options: [http|https] default: http
|
||||
PORT=5000 # default: 5000
|
||||
PORT_WEB=3000 # port for sasjs web component(react). default: 3000
|
||||
# options: [desktop|server] default: `desktop`
|
||||
MODE=
|
||||
|
||||
# options: [disable|enable] default: `disable` for `server` & `enable` for `desktop`
|
||||
CORS=
|
||||
|
||||
# options: <http://localhost:3000 https://abc.com ...> space separated urls
|
||||
WHITELIST=
|
||||
|
||||
# options: [http|https] default: http
|
||||
PROTOCOL=
|
||||
|
||||
# default: 5000
|
||||
PORT=
|
||||
|
||||
|
||||
# optional
|
||||
# for MODE: `desktop`, prompts user
|
||||
# for MODE: `server` gets value from api/package.json `configuration.sasPath`
|
||||
SAS_PATH=/path/to/sas/executable.exe
|
||||
|
||||
|
||||
# optional
|
||||
# for MODE: `desktop`, prompts user
|
||||
# for MODE: `server` defaults to /tmp
|
||||
DRIVE_PATH=/tmp
|
||||
PROTOCOL=http # options: [http|https] default: http
|
||||
|
||||
# ENV variables required for PROTOCOL: `https`
|
||||
PRIVATE_KEY=privkey.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
|
||||
@@ -94,11 +120,10 @@ Instead of `app_name` you can pass:
|
||||
- `all` to act on all processes
|
||||
- `id` to act on a specific process id
|
||||
|
||||
|
||||
## Server Version
|
||||
|
||||
The following credentials can be used for the initial connection to SASjs/server. It is recommended to change these on first use.
|
||||
|
||||
* CLIENTID: `clientID1`
|
||||
* USERNAME: `secretuser`
|
||||
* PASSWORD: `secretpassword`
|
||||
- CLIENTID: `clientID1`
|
||||
- USERNAME: `secretuser`
|
||||
- PASSWORD: `secretpassword`
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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
|
||||
PRIVATE_KEY=privkey.pem
|
||||
FULL_CHAIN=fullchain.pem
|
||||
PORT=[5000] default value is 5000
|
||||
PORT_WEB=[port for sasjs web component(react)] default value is 3000
|
||||
ACCESS_TOKEN_SECRET=<secret>
|
||||
REFRESH_TOKEN_SECRET=<secret>
|
||||
AUTH_CODE_SECRET=<secret>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
"assets": [
|
||||
"./build/public/**/*",
|
||||
"./build/sasjsbuild/**/*",
|
||||
"./build/sasjscore/**/*",
|
||||
"./web/build/**/*"
|
||||
],
|
||||
"targets": [
|
||||
@@ -88,5 +89,10 @@
|
||||
},
|
||||
"configuration": {
|
||||
"sasPath": "/opt/sas/sas9/SASHome/SASFoundation/9.4/sas"
|
||||
},
|
||||
"nodemonConfig": {
|
||||
"ignore": [
|
||||
"tmp/**/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
21
api/public/sasjs-logo.svg
Normal file
21
api/public/sasjs-logo.svg
Normal 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 |
@@ -161,6 +161,8 @@ components:
|
||||
$ref: '#/components/schemas/FolderMember'
|
||||
-
|
||||
$ref: '#/components/schemas/ServiceMember'
|
||||
-
|
||||
$ref: '#/components/schemas/FileMember'
|
||||
type: array
|
||||
required:
|
||||
- name
|
||||
@@ -172,19 +174,29 @@ components:
|
||||
enum:
|
||||
- service
|
||||
type: string
|
||||
MemberType.file:
|
||||
enum:
|
||||
- file
|
||||
type: string
|
||||
ServiceMember:
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
type:
|
||||
anyOf:
|
||||
-
|
||||
$ref: '#/components/schemas/MemberType.service'
|
||||
-
|
||||
code:
|
||||
type: string
|
||||
required:
|
||||
- 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:
|
||||
type: string
|
||||
@@ -203,6 +215,8 @@ components:
|
||||
$ref: '#/components/schemas/FolderMember'
|
||||
-
|
||||
$ref: '#/components/schemas/ServiceMember'
|
||||
-
|
||||
$ref: '#/components/schemas/FileMember'
|
||||
type: array
|
||||
required:
|
||||
- members
|
||||
@@ -214,6 +228,8 @@ components:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
streamServiceName:
|
||||
type: string
|
||||
example:
|
||||
$ref: '#/components/schemas/FileTree'
|
||||
required:
|
||||
@@ -225,6 +241,8 @@ components:
|
||||
properties:
|
||||
appLoc:
|
||||
type: string
|
||||
streamWebFolder:
|
||||
type: string
|
||||
fileTree:
|
||||
$ref: '#/components/schemas/FileTree'
|
||||
required:
|
||||
@@ -631,7 +649,6 @@ paths:
|
||||
required:
|
||||
- status
|
||||
type: object
|
||||
description: "It's optional to either provide `_filePath` in url as query parameter\nOr provide `filePath` in body as form field.\nBut it's required to provide else API will respond with Bad Request."
|
||||
summary: 'Delete file from SASjs Drive'
|
||||
tags:
|
||||
- Drive
|
||||
@@ -642,19 +659,10 @@ paths:
|
||||
-
|
||||
in: query
|
||||
name: _filePath
|
||||
required: false
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: /Public/somefolder/some.file
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
filePath:
|
||||
type: string
|
||||
post:
|
||||
operationId: SaveFile
|
||||
responses:
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
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')
|
||||
|
||||
@@ -16,6 +23,10 @@ export const copySASjsCore = async () => {
|
||||
|
||||
await copy(coreSubFolderPath, sasJSCoreMacros)
|
||||
})
|
||||
|
||||
const fileNames = await listFilesInFolder(sasJSCoreMacros)
|
||||
|
||||
await createFile(sasJSCoreMacrosInfo, fileNames.join('\n'))
|
||||
}
|
||||
|
||||
copySASjsCore()
|
||||
|
||||
@@ -7,8 +7,9 @@ import cors from 'cors'
|
||||
|
||||
import {
|
||||
connectDB,
|
||||
copySASjsCore,
|
||||
getWebBuildFolderPath,
|
||||
sasJSCoreMacros,
|
||||
loadAppStreamConfig,
|
||||
setProcessVariables
|
||||
} from './utils'
|
||||
|
||||
@@ -16,20 +17,23 @@ dotenv.config()
|
||||
|
||||
const app = express()
|
||||
|
||||
const { MODE, CORS, PORT_WEB } = process.env
|
||||
const whiteList = [
|
||||
`http://localhost:${PORT_WEB ?? 3000}`,
|
||||
'https://sas.analytium.co.uk:8343'
|
||||
]
|
||||
const { MODE, CORS, WHITELIST } = process.env
|
||||
|
||||
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(cookieParser())
|
||||
app.use(morgan('tiny'))
|
||||
app.use(express.json({ limit: '50mb' }))
|
||||
app.use(express.json({ limit: '100mb' }))
|
||||
app.use(express.static(path.join(__dirname, '../public')))
|
||||
|
||||
const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
@@ -38,17 +42,19 @@ const onError: ErrorRequestHandler = (err, req, res, next) => {
|
||||
}
|
||||
|
||||
export default setProcessVariables().then(async () => {
|
||||
await copySASjsCore()
|
||||
|
||||
// loading these modules after setting up variables due to
|
||||
// multer's usage of process var process.driveLoc
|
||||
const { setupRoutes } = await import('./routes/setupRoutes')
|
||||
setupRoutes(app)
|
||||
|
||||
await loadAppStreamConfig()
|
||||
|
||||
// should be served after setting up web route
|
||||
// index.html needs to be injected with some js script.
|
||||
app.use(express.static(getWebBuildFolderPath()))
|
||||
|
||||
console.log('sasJSCoreMacros', sasJSCoreMacros)
|
||||
|
||||
app.use(onError)
|
||||
|
||||
await connectDB()
|
||||
|
||||
@@ -29,12 +29,14 @@ import { getTmpFilesFolderPath } from '../utils'
|
||||
|
||||
interface DeployPayload {
|
||||
appLoc: string
|
||||
streamWebFolder?: string
|
||||
fileTree: FileTree
|
||||
}
|
||||
|
||||
interface DeployResponse {
|
||||
status: string
|
||||
message: string
|
||||
streamServiceName?: string
|
||||
example?: FileTree
|
||||
}
|
||||
|
||||
@@ -106,20 +108,14 @@ export class DriveController {
|
||||
}
|
||||
|
||||
/**
|
||||
* It's optional to either provide `_filePath` in url as query parameter
|
||||
* Or provide `filePath` in body as form field.
|
||||
* But it's required to provide else API will respond with Bad Request.
|
||||
*
|
||||
* @summary Delete file from SASjs Drive
|
||||
* @query _filePath Location of SAS program
|
||||
* @example _filePath "/Public/somefolder/some.file"
|
||||
*/
|
||||
@Delete('/file')
|
||||
public async deleteFile(
|
||||
@Query() _filePath?: string,
|
||||
@FormField() filePath?: string
|
||||
) {
|
||||
return deleteFile((_filePath ?? filePath)!)
|
||||
public async deleteFile(@Query() _filePath: string) {
|
||||
return deleteFile(_filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,14 +186,23 @@ const getFileTree = () => {
|
||||
}
|
||||
|
||||
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)) {
|
||||
throw { code: 400, ...invalidDeployFormatResponse }
|
||||
}
|
||||
|
||||
await createFileTree(
|
||||
data.fileTree.members,
|
||||
data.appLoc.replace(/^\//, '').split('/')
|
||||
).catch((err) => {
|
||||
await createFileTree(data.fileTree.members, appLocParts).catch((err) => {
|
||||
throw { code: 500, ...execDeployErrorResponse, ...err }
|
||||
})
|
||||
|
||||
@@ -294,9 +299,3 @@ const updateFile = async (
|
||||
|
||||
return { status: 'success' }
|
||||
}
|
||||
|
||||
const validateFilePath = async (filePath: string) => {
|
||||
if (!(await fileExists(filePath))) {
|
||||
throw 'DriveController: File does not exists.'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,14 @@ import {
|
||||
moveFile,
|
||||
readFileBinary
|
||||
} from '@sasjs/utils'
|
||||
import { PreProgramVars, TreeNode } from '../../types'
|
||||
import { PreProgramVars, Session, TreeNode } from '../../types'
|
||||
import {
|
||||
extractHeaders,
|
||||
generateFileUploadSasCode,
|
||||
getTmpFilesFolderPath,
|
||||
getTmpMacrosPath,
|
||||
HTTPHeaders,
|
||||
isDebugOn,
|
||||
sasJSCoreMacros
|
||||
isDebugOn
|
||||
} from '../../utils'
|
||||
|
||||
export interface ExecutionVars {
|
||||
@@ -39,7 +39,8 @@ export class ExecutionController {
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
returnJson?: boolean,
|
||||
session?: Session
|
||||
) {
|
||||
if (!(await fileExists(programPath)))
|
||||
throw 'ExecutionController: SAS file does not exist.'
|
||||
@@ -51,7 +52,8 @@ export class ExecutionController {
|
||||
preProgramVariables,
|
||||
vars,
|
||||
otherArgs,
|
||||
returnJson
|
||||
returnJson,
|
||||
session
|
||||
)
|
||||
}
|
||||
|
||||
@@ -60,11 +62,13 @@ export class ExecutionController {
|
||||
preProgramVariables: PreProgramVars,
|
||||
vars: ExecutionVars,
|
||||
otherArgs?: any,
|
||||
returnJson?: boolean
|
||||
returnJson?: boolean,
|
||||
sessionByFileUpload?: Session
|
||||
): Promise<ExecuteReturnRaw | ExecuteReturnJson> {
|
||||
const sessionController = getSessionController()
|
||||
|
||||
const session = await sessionController.getSession()
|
||||
const session =
|
||||
sessionByFileUpload ?? (await sessionController.getSession())
|
||||
session.inUse = true
|
||||
session.consumed = true
|
||||
|
||||
@@ -106,7 +110,7 @@ export class ExecutionController {
|
||||
`
|
||||
|
||||
program = `
|
||||
options insert=(SASAUTOS="${sasJSCoreMacros}");
|
||||
options insert=(SASAUTOS="${getTmpMacrosPath()}");
|
||||
|
||||
/* runtime vars */
|
||||
${varStatments}
|
||||
|
||||
@@ -23,7 +23,9 @@ export class FileUploadController {
|
||||
|
||||
const sessionController = getSessionController()
|
||||
session = await sessionController.getSession()
|
||||
session.inUse = true
|
||||
// marking consumed true, so that it's not available
|
||||
// as readySession for any other request
|
||||
session.consumed = true
|
||||
|
||||
req.sasSession = session
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ export class SessionController {
|
||||
? readySessions[0]
|
||||
: await this.createSession()
|
||||
|
||||
if (readySessions.length < 2) this.createSession()
|
||||
if (readySessions.length < 3) this.createSession()
|
||||
|
||||
return session
|
||||
}
|
||||
@@ -87,6 +87,8 @@ ${autoExecContent}`
|
||||
codePath,
|
||||
'-LOG',
|
||||
path.join(session.path, 'log.log'),
|
||||
'-PRINT',
|
||||
path.join(session.path, 'output.lst'),
|
||||
'-WORK',
|
||||
session.path,
|
||||
'-AUTOEXEC',
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
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 { createFolder, createFile, asyncForEach } from '@sasjs/utils'
|
||||
|
||||
// REFACTOR: export FileTreeCpntroller
|
||||
export const createFileTree = async (
|
||||
members: (FolderMember | ServiceMember)[],
|
||||
members: (FolderMember | ServiceMember | FileMember)[],
|
||||
parentFolders: string[] = []
|
||||
) => {
|
||||
const destinationPath = path.join(
|
||||
@@ -13,7 +19,9 @@ export const createFileTree = async (
|
||||
path.join(...parentFolders)
|
||||
)
|
||||
|
||||
await asyncForEach(members, async (member: FolderMember | ServiceMember) => {
|
||||
await asyncForEach(
|
||||
members,
|
||||
async (member: FolderMember | ServiceMember | FileMember) => {
|
||||
let name = member.name
|
||||
|
||||
if (member.type === MemberType.service) name += '.sas'
|
||||
@@ -35,7 +43,8 @@ export const createFileTree = async (
|
||||
encoding
|
||||
).catch((err) => Promise.reject({ error: err, failedToCreate: name }))
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
return Promise.resolve()
|
||||
}
|
||||
|
||||
@@ -143,9 +143,8 @@ const executeReturnRaw = async (
|
||||
query
|
||||
)) as ExecuteReturnRaw
|
||||
|
||||
// Should over-ride response header for
|
||||
// debug on GET request to see entire log
|
||||
// rendering on browser.
|
||||
// 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'
|
||||
}
|
||||
@@ -185,7 +184,8 @@ const executeReturnJson = async (
|
||||
getPreProgramVariables(req),
|
||||
{ ...req.query, ...req.body },
|
||||
{ filesNamesMap: filesNamesMap },
|
||||
true
|
||||
true,
|
||||
req.sasSession
|
||||
)) as ExecuteReturnJson
|
||||
|
||||
let weboutRes: string | IRecordOfAny = webout
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import path from 'path'
|
||||
import { Request } from 'express'
|
||||
import multer, { FileFilterCallback, Options } from 'multer'
|
||||
import { getTmpUploadsPath } from '../utils'
|
||||
import { blockFileRegex, getTmpUploadsPath } from '../utils'
|
||||
|
||||
const acceptableExtensions = ['.sas']
|
||||
const fieldNameSize = 300
|
||||
const fileSize = 10485760 // 10 MB
|
||||
|
||||
@@ -31,15 +30,11 @@ const fileFilter: Options['fileFilter'] = (
|
||||
file: Express.Multer.File,
|
||||
callback: FileFilterCallback
|
||||
) => {
|
||||
const fileExtension = path.extname(file.originalname).toLocaleLowerCase()
|
||||
|
||||
if (!acceptableExtensions.includes(fileExtension)) {
|
||||
const fileExtension = path.extname(file.originalname)
|
||||
const shouldBlockUpload = blockFileRegex.test(file.originalname)
|
||||
if (shouldBlockUpload) {
|
||||
return callback(
|
||||
new Error(
|
||||
`File extension '${fileExtension}' not acceptable. Valid extension(s): ${acceptableExtensions.join(
|
||||
', '
|
||||
)}`
|
||||
)
|
||||
new Error(`File extension '${fileExtension}' not acceptable.`)
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -22,9 +22,15 @@ driveRouter.post('/deploy', async (req, res) => {
|
||||
try {
|
||||
const response = await controller.deploy(body)
|
||||
|
||||
const appLoc = body.appLoc.replace(/^\//, '')?.split('/')
|
||||
|
||||
publishAppStream(appLoc)
|
||||
if (body.streamWebFolder) {
|
||||
const { streamServiceName } = await publishAppStream(
|
||||
body.appLoc,
|
||||
body.streamWebFolder,
|
||||
body.streamServiceName,
|
||||
body.streamLogo
|
||||
)
|
||||
response.streamServiceName = streamServiceName
|
||||
}
|
||||
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
@@ -51,12 +57,11 @@ driveRouter.get('/file', async (req, res) => {
|
||||
|
||||
driveRouter.delete('/file', async (req, res) => {
|
||||
const { error: errQ, value: query } = fileParamValidation(req.query)
|
||||
const { error: errB, value: body } = fileBodyValidation(req.body)
|
||||
|
||||
if (errQ && errB) return res.status(400).send(errQ.details[0].message)
|
||||
if (errQ) return res.status(400).send(errQ.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.deleteFile(query._filePath, body.filePath)
|
||||
const response = await controller.deleteFile(query._filePath)
|
||||
res.send(response)
|
||||
} catch (err: any) {
|
||||
res.status(403).send(err.toString())
|
||||
|
||||
@@ -266,7 +266,7 @@ describe('files', () => {
|
||||
|
||||
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||
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)
|
||||
.post(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
|
||||
@@ -275,7 +275,7 @@ describe('files', () => {
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Valid extensions for filePath: .sas')
|
||||
expect(res.text).toEqual('Invalid file extension')
|
||||
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 () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth')
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
@@ -303,9 +303,7 @@ describe('files', () => {
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`File extension '.oth' not acceptable. Valid extension(s): .sas`
|
||||
)
|
||||
expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
@@ -426,7 +424,7 @@ describe('files', () => {
|
||||
|
||||
it("should respond with Bad Request if filePath doesn't has correct extension", async () => {
|
||||
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)
|
||||
.patch(`/SASjsApi/drive/file?_filePath=${pathToUpload}`)
|
||||
@@ -435,7 +433,7 @@ describe('files', () => {
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual('Valid extensions for filePath: .sas')
|
||||
expect(res.text).toEqual('Invalid file extension')
|
||||
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 () => {
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.oth')
|
||||
const fileToAttachPath = path.join(__dirname, 'files', 'sample.exe')
|
||||
const pathToUpload = '/my/path/code.sas'
|
||||
|
||||
const res = await request(app)
|
||||
@@ -463,9 +461,7 @@ describe('files', () => {
|
||||
.attach('file', fileToAttachPath)
|
||||
.expect(400)
|
||||
|
||||
expect(res.text).toEqual(
|
||||
`File extension '.oth' not acceptable. Valid extension(s): .sas`
|
||||
)
|
||||
expect(res.text).toEqual(`File extension '.exe' not acceptable.`)
|
||||
expect(res.body).toEqual({})
|
||||
})
|
||||
|
||||
|
||||
57
api/src/routes/appStream/appStreamHtml.ts
Normal file
57
api/src/routes/appStream/appStreamHtml.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
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;
|
||||
text-align: center;
|
||||
}
|
||||
.app-container .app img{
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
border-radius: 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}"
|
||||
onerror="this.src = '${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>`
|
||||
@@ -2,25 +2,64 @@ import path from 'path'
|
||||
import express from 'express'
|
||||
import { folderExists } from '@sasjs/utils'
|
||||
|
||||
import { getTmpFilesFolderPath } from '../../utils'
|
||||
import { addEntryToAppStreamConfig, getTmpFilesFolderPath } from '../../utils'
|
||||
import { appStreamHtml } from './appStreamHtml'
|
||||
|
||||
const router = express.Router()
|
||||
|
||||
export const publishAppStream = async (appLoc: string[]) => {
|
||||
const appLocUrl = encodeURI(appLoc.join('/'))
|
||||
const appLocPath = appLoc.join(path.sep)
|
||||
router.get('/', async (_, res) => {
|
||||
const content = appStreamHtml(process.appStreamConfig)
|
||||
|
||||
const pathToDeployment = path.join(
|
||||
getTmpFilesFolderPath(),
|
||||
appLocPath,
|
||||
'services',
|
||||
'webv'
|
||||
)
|
||||
return res.send(content)
|
||||
})
|
||||
|
||||
export const publishAppStream = async (
|
||||
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)) {
|
||||
router.use(`/${appLocUrl}`, express.static(pathToDeployment))
|
||||
console.log('Serving Stream App: ', appLocUrl)
|
||||
const appCount = process.appStreamConfig
|
||||
? Object.keys(process.appStreamConfig).length
|
||||
: 0
|
||||
|
||||
if (!streamServiceName) {
|
||||
streamServiceName = `AppStreamName${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
|
||||
|
||||
7
api/src/types/AppStreamConfig.ts
Normal file
7
api/src/types/AppStreamConfig.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
export interface AppStreamConfig {
|
||||
[key: string]: {
|
||||
appLoc: string
|
||||
streamWebFolder: string
|
||||
streamLogo?: string
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,28 @@
|
||||
export interface FileTree {
|
||||
members: (FolderMember | ServiceMember)[]
|
||||
export enum MemberType {
|
||||
service = 'service',
|
||||
file = 'file',
|
||||
folder = 'folder'
|
||||
}
|
||||
|
||||
export enum MemberType {
|
||||
folder = 'folder',
|
||||
service = 'service',
|
||||
file = 'file'
|
||||
export interface ServiceMember {
|
||||
name: string
|
||||
type: MemberType.service
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface FileMember {
|
||||
name: string
|
||||
type: MemberType.file
|
||||
code: string
|
||||
}
|
||||
|
||||
export interface FolderMember {
|
||||
name: string
|
||||
type: MemberType.folder
|
||||
members: (FolderMember | ServiceMember)[]
|
||||
members: (FolderMember | ServiceMember | FileMember)[]
|
||||
}
|
||||
|
||||
export interface ServiceMember {
|
||||
name: string
|
||||
type: MemberType.service | MemberType.file
|
||||
code: string
|
||||
export interface FileTree {
|
||||
members: (FolderMember | ServiceMember | FileMember)[]
|
||||
}
|
||||
|
||||
export const isFileTree = (arg: any): arg is FileTree =>
|
||||
@@ -25,11 +30,25 @@ export const isFileTree = (arg: any): arg is FileTree =>
|
||||
arg.members &&
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isFolderMember(member) && !isServiceMember(member)
|
||||
(member: ServiceMember | FileMember | FolderMember) =>
|
||||
!isServiceMember(member, '-') &&
|
||||
!isFileMember(member, '-') &&
|
||||
!isFolderMember(member, '-')
|
||||
).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 &&
|
||||
typeof arg.name === 'string' &&
|
||||
arg.type === MemberType.folder &&
|
||||
@@ -37,21 +56,7 @@ const isFolderMember = (arg: any): arg is FolderMember =>
|
||||
Array.isArray(arg.members) &&
|
||||
arg.members.filter(
|
||||
(member: FolderMember | ServiceMember) =>
|
||||
!isFolderMember(member) &&
|
||||
!isServiceMember(member) &&
|
||||
!isFileMember(member)
|
||||
!isServiceMember(member, pre + '-') &&
|
||||
!isFileMember(member, pre + '-') &&
|
||||
!isFolderMember(member, pre + '-')
|
||||
).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'
|
||||
|
||||
1
api/src/types/Process.d.ts
vendored
1
api/src/types/Process.d.ts
vendored
@@ -3,5 +3,6 @@ declare namespace NodeJS {
|
||||
sasLoc: string
|
||||
driveLoc: string
|
||||
sessionController?: import('../controllers/internal').SessionController
|
||||
appStreamConfig: import('./').AppStreamConfig
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// TODO: uppercase types
|
||||
export * from './AppStreamConfig'
|
||||
export * from './Execution'
|
||||
export * from './FileTree'
|
||||
export * from './InfoJWT'
|
||||
|
||||
89
api/src/utils/appStreamConfig.ts
Normal file
89
api/src/utils/appStreamConfig.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { createFile, fileExists, readFile } from '@sasjs/utils'
|
||||
import { publishAppStream } from '../routes/appStream'
|
||||
import { AppStreamConfig } from '../types'
|
||||
|
||||
import { getTmpAppStreamConfigPath } from './file'
|
||||
|
||||
export const loadAppStreamConfig = async () => {
|
||||
if (process.env.NODE_ENV === 'test') return
|
||||
|
||||
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
|
||||
}
|
||||
34
api/src/utils/copySASjsCore.ts
Normal file
34
api/src/utils/copySASjsCore.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import path from 'path'
|
||||
import {
|
||||
asyncForEach,
|
||||
createFile,
|
||||
createFolder,
|
||||
deleteFolder,
|
||||
readFile
|
||||
} from '@sasjs/utils'
|
||||
|
||||
import { getTmpMacrosPath, sasJSCoreMacros, sasJSCoreMacrosInfo } from '.'
|
||||
|
||||
export const copySASjsCore = async () => {
|
||||
if (process.env.NODE_ENV === 'test') return
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -9,12 +9,18 @@ export const sysInitCompiledPath = path.join(
|
||||
)
|
||||
|
||||
export const sasJSCoreMacros = path.join(apiRoot, 'sasjscore')
|
||||
export const sasJSCoreMacrosInfo = path.join(apiRoot, 'sasjscore', '.macrolist')
|
||||
|
||||
export const getWebBuildFolderPath = () =>
|
||||
path.join(codebaseRoot, 'web', 'build')
|
||||
|
||||
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 getTmpFilesFolderPath = () =>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
export * from './appStreamConfig'
|
||||
export * from './connectDB'
|
||||
export * from './copySASjsCore'
|
||||
export * from './extractHeaders'
|
||||
export * from './file'
|
||||
export * from './generateAccessToken'
|
||||
export * from './generateAuthCode'
|
||||
export * from './generateRefreshToken'
|
||||
export * from './isDebugOn'
|
||||
export * from './getCertificates'
|
||||
export * from './getDesktopFields'
|
||||
export * from './isDebugOn'
|
||||
export * from './parseLogToArray'
|
||||
export * from './removeTokensInDB'
|
||||
export * from './saveTokensInDB'
|
||||
|
||||
@@ -1,9 +1,21 @@
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import { getTmpSessionsFolderPath } from '.'
|
||||
import { MulterFile } from '../types/Upload'
|
||||
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
|
||||
* @param files array of files to be mapped
|
||||
@@ -12,10 +24,13 @@ import { listFilesInFolder } from '@sasjs/utils'
|
||||
export const makeFilesNamesMap = (files: MulterFile[]) => {
|
||||
if (!files) return null
|
||||
|
||||
const filesNamesMap: { [key: string]: string } = {}
|
||||
const filesNamesMap: FilenamesMap = {}
|
||||
|
||||
for (let file of files) {
|
||||
filesNamesMap[file.filename] = file.originalname
|
||||
filesNamesMap[file.filename] = {
|
||||
fieldName: file.fieldname,
|
||||
originalName: file.originalname
|
||||
}
|
||||
}
|
||||
|
||||
return filesNamesMap
|
||||
@@ -28,17 +43,12 @@ export const makeFilesNamesMap = (files: MulterFile[]) => {
|
||||
* @returns generated sas code
|
||||
*/
|
||||
export const generateFileUploadSasCode = async (
|
||||
filesNamesMap: any,
|
||||
filesNamesMap: FilenamesMap,
|
||||
sasSessionFolder: string
|
||||
): Promise<string> => {
|
||||
let uploadSasCode = ''
|
||||
let fileCount = 0
|
||||
let uploadedFilesMap: {
|
||||
fileref: string
|
||||
filepath: string
|
||||
filename: string
|
||||
count: number
|
||||
}[] = []
|
||||
const uploadedFiles: UploadedFiles[] = []
|
||||
|
||||
const sasSessionFolderList: string[] = await listFilesInFolder(
|
||||
sasSessionFolder
|
||||
@@ -50,31 +60,32 @@ export const generateFileUploadSasCode = async (
|
||||
if (fileName.includes('req_file')) {
|
||||
fileCount++
|
||||
|
||||
uploadedFilesMap.push({
|
||||
uploadedFiles.push({
|
||||
fileref: `_sjs${fileCountString}`,
|
||||
filepath: `${sasSessionFolder}/${fileName}`,
|
||||
filename: filesNamesMap[fileName],
|
||||
originalName: filesNamesMap[fileName].originalName,
|
||||
fieldName: filesNamesMap[fileName].fieldName,
|
||||
count: fileCount
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\nfilename ${uploadedMap.fileref} "${uploadedMap.filepath}";`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\nfilename ${uploadedFile.fileref} "${uploadedFile.filepath}";`
|
||||
}
|
||||
|
||||
uploadSasCode += `\n%let _WEBIN_FILE_COUNT=${fileCount};`
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedMap.count}=${uploadedMap.filename};`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILENAME${uploadedFile.count}=${uploadedFile.originalName};`
|
||||
}
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedMap.count}=${uploadedMap.fileref};`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\n%let _WEBIN_FILEREF${uploadedFile.count}=${uploadedFile.fileref};`
|
||||
}
|
||||
|
||||
for (let uploadedMap of uploadedFilesMap) {
|
||||
uploadSasCode += `\n%let _WEBIN_NAME${uploadedMap.count}=${uploadedMap.filepath};`
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
uploadSasCode += `\n%let _WEBIN_NAME${uploadedFile.count}=${uploadedFile.fieldName};`
|
||||
}
|
||||
|
||||
if (fileCount > 0) {
|
||||
|
||||
@@ -3,6 +3,8 @@ import Joi from 'joi'
|
||||
const usernameSchema = Joi.string().alphanum().min(6).max(20)
|
||||
const passwordSchema = Joi.string().min(6).max(1024)
|
||||
|
||||
export const blockFileRegex = /\.(exe|sh|htaccess)$/i
|
||||
|
||||
export const authorizeValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
username: usernameSchema.required(),
|
||||
@@ -69,21 +71,31 @@ export const registerClientValidation = (data: any): Joi.ValidationResult =>
|
||||
export const deployValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
appLoc: Joi.string().pattern(/^\//).required().min(2),
|
||||
streamServiceName: Joi.string(),
|
||||
streamWebFolder: Joi.string(),
|
||||
streamLogo: Joi.string(),
|
||||
fileTree: Joi.any().required()
|
||||
}).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 =>
|
||||
Joi.object({
|
||||
filePath: Joi.string().pattern(/.sas$/).required().messages({
|
||||
'string.pattern.base': `Valid extensions for filePath: .sas`
|
||||
})
|
||||
filePath: filePathSchema
|
||||
}).validate(data)
|
||||
|
||||
export const fileParamValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_filePath: Joi.string().pattern(/.sas$/).required().messages({
|
||||
'string.pattern.base': `Valid extensions for filePath: .sas`
|
||||
})
|
||||
_filePath: filePathSchema
|
||||
}).validate(data)
|
||||
|
||||
export const runSASValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
20
package-lock.json
generated
20
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.34",
|
||||
"version": "0.0.45",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "server",
|
||||
"version": "0.0.34",
|
||||
"version": "0.0.45",
|
||||
"devDependencies": {
|
||||
"prettier": "^2.3.1",
|
||||
"standard-version": "^9.3.2"
|
||||
@@ -404,14 +404,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/conventional-changelog-writer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz",
|
||||
"integrity": "sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz",
|
||||
"integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"conventional-commits-filter": "^2.0.7",
|
||||
"dateformat": "^3.0.0",
|
||||
"handlebars": "^4.7.6",
|
||||
"handlebars": "^4.7.7",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"meow": "^8.0.0",
|
||||
@@ -2433,14 +2433,14 @@
|
||||
"dev": true
|
||||
},
|
||||
"conventional-changelog-writer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.0.tgz",
|
||||
"integrity": "sha512-HnDh9QHLNWfL6E1uHz6krZEQOgm8hN7z/m7tT16xwd802fwgMN0Wqd7AQYVkhpsjDUx/99oo+nGgvKF657XP5g==",
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-5.0.1.tgz",
|
||||
"integrity": "sha512-5WsuKUfxW7suLblAbFnxAcrvf6r+0b7GvNaWUwUIk0bXMnENP/PEieGKVUQrjPqwPT4o3EPAASBXiY6iHooLOQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"conventional-commits-filter": "^2.0.7",
|
||||
"dateformat": "^3.0.0",
|
||||
"handlebars": "^4.7.6",
|
||||
"handlebars": "^4.7.7",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"lodash": "^4.17.15",
|
||||
"meow": "^8.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.34",
|
||||
"version": "0.0.45",
|
||||
"description": "NodeJS wrapper for calling the SAS binary executable",
|
||||
"repository": "https://github.com/sasjs/server",
|
||||
"scripts": {
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
|
||||
|
||||
### testing upload file example
|
||||
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
|
||||
|
||||
------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
|
||||
|
||||
|
||||
------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
|
||||
|
||||
_____DELETE__THIS__RECORD_____,PRIMARY_KEY_FIELD,SOME_CHAR,SOME_DROPDOWN,SOME_NUM,SOME_DATE,SOME_DATETIME,SOME_TIME,SOME_SHORTNUM,SOME_BESTNUM
|
||||
|
||||
@@ -66,11 +66,21 @@ const Header = (props: any) => {
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
startIcon={<OpenInNewIcon />}
|
||||
style={{ marginLeft: '50px' }}
|
||||
endIcon={<OpenInNewIcon />}
|
||||
>
|
||||
API Docs
|
||||
</Button>
|
||||
<Button
|
||||
href={`${baseUrl}/AppStream`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="large"
|
||||
endIcon={<OpenInNewIcon />}
|
||||
>
|
||||
App Stream
|
||||
</Button>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
)
|
||||
|
||||
@@ -54,11 +54,11 @@ const Studio = () => {
|
||||
|
||||
let weboutString: string
|
||||
try {
|
||||
weboutString = res.data.webout
|
||||
weboutString = res.data._webout
|
||||
.split('>>weboutBEGIN<<')[1]
|
||||
.split('>>weboutEND<<')[0]
|
||||
} catch (_) {
|
||||
weboutString = res?.data?.webout ?? ''
|
||||
weboutString = res?.data?._webout ?? ''
|
||||
}
|
||||
|
||||
let webout: string
|
||||
@@ -70,6 +70,9 @@ const Studio = () => {
|
||||
|
||||
setWebout(`<pre><code>${webout}</code></pre>`)
|
||||
setTab('2')
|
||||
|
||||
// Scroll to bottom of log
|
||||
window.scrollTo(0, document.body.scrollHeight)
|
||||
})
|
||||
.catch((err) => console.log(err))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user