mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
31 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
98a00ec7ac |
66
CHANGELOG.md
66
CHANGELOG.md
@@ -2,6 +2,72 @@
|
|||||||
|
|
||||||
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.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)
|
### [0.0.35](https://github.com/sasjs/server/compare/v0.0.33...v0.0.35) (2022-03-21)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
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/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:
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import cors from 'cors'
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
connectDB,
|
connectDB,
|
||||||
|
copySASjsCore,
|
||||||
getWebBuildFolderPath,
|
getWebBuildFolderPath,
|
||||||
|
loadAppStreamConfig,
|
||||||
sasJSCoreMacros,
|
sasJSCoreMacros,
|
||||||
setProcessVariables
|
setProcessVariables
|
||||||
} from './utils'
|
} from './utils'
|
||||||
@@ -41,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()
|
||||||
|
|||||||
@@ -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 }
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
|
||||||
', '
|
|
||||||
)}`
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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({})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
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 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}`
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|||||||
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 {
|
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'
|
|
||||||
|
|||||||
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
|
sasLoc: string
|
||||||
driveLoc: string
|
driveLoc: string
|
||||||
sessionController?: import('../controllers/internal').SessionController
|
sessionController?: import('../controllers/internal').SessionController
|
||||||
|
appStreamConfig: import('./').AppStreamConfig
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
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 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 = () =>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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
20
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.35",
|
"version": "0.0.43",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.35",
|
"version": "0.0.43",
|
||||||
"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",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "server",
|
"name": "server",
|
||||||
"version": "0.0.35",
|
"version": "0.0.43",
|
||||||
"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": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,11 +54,11 @@ const Studio = () => {
|
|||||||
|
|
||||||
let weboutString: string
|
let weboutString: string
|
||||||
try {
|
try {
|
||||||
weboutString = res.data.webout
|
weboutString = res.data._webout
|
||||||
.split('>>weboutBEGIN<<')[1]
|
.split('>>weboutBEGIN<<')[1]
|
||||||
.split('>>weboutEND<<')[0]
|
.split('>>weboutEND<<')[0]
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
weboutString = res?.data?.webout ?? ''
|
weboutString = res?.data?._webout ?? ''
|
||||||
}
|
}
|
||||||
|
|
||||||
let webout: string
|
let webout: string
|
||||||
@@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user