mirror of
https://github.com/sasjs/server.git
synced 2025-12-10 19:34:34 +00:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa2a1cbe13 | ||
|
|
6f2c53555c | ||
|
|
73d965daf5 | ||
|
|
4f1763db67 | ||
|
|
28222add04 | ||
|
|
068edfd6a5 | ||
|
|
7e8cbbf377 | ||
|
|
1fc1431442 | ||
|
|
3387efbb9a | ||
|
|
e2996b495f | ||
|
|
41c627f93a | ||
|
|
49f5dc7555 | ||
|
|
f6e77f99a4 | ||
|
|
b57dfa429b | ||
| 9586dbb2d0 | |||
|
|
a4f78ab48d | ||
|
|
2f47a2213b | ||
|
|
0f91395fbb | ||
|
|
167b14fed0 | ||
|
|
8940f4dc47 | ||
|
|
48c1ada1b6 | ||
|
|
0532488b55 | ||
|
|
d458b5bb81 | ||
|
|
958ab9cad2 | ||
|
|
78ceed13e1 | ||
|
|
a17814fc90 | ||
|
|
9aaffce820 | ||
|
|
e78f87f5c0 | ||
|
|
bd1b58086d | ||
|
|
9f521634d9 | ||
|
|
a696168443 | ||
|
|
31df72ad88 | ||
|
|
d2239f75c2 | ||
|
|
45428892cc | ||
| ac27a9b894 | |||
| dba53de646 | |||
|
|
eb42683fff | ||
|
|
d2de9dc13e | ||
|
|
6dd2f4f876 | ||
|
|
c0f38ba7c9 |
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
@@ -56,4 +56,4 @@ jobs:
|
||||
|
||||
- name: Release
|
||||
run: |
|
||||
GITHUB_TOKEN=${{ secrets.GH_TOKEN }} semantic-release
|
||||
GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} semantic-release
|
||||
|
||||
51
CHANGELOG.md
51
CHANGELOG.md
@@ -1,3 +1,54 @@
|
||||
## [0.35.4](https://github.com/sasjs/server/compare/v0.35.3...v0.35.4) (2024-01-15)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** fixed env issue in MacOS executable ([73d965d](https://github.com/sasjs/server/commit/73d965daf54b16c0921e4b18d11a1e6f8650884d))
|
||||
|
||||
## [0.35.3](https://github.com/sasjs/server/compare/v0.35.2...v0.35.3) (2023-11-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* enable embedded LFs in JS STP vars ([7e8cbbf](https://github.com/sasjs/server/commit/7e8cbbf377b27a7f5dd9af0bc6605c01f302f5d9))
|
||||
|
||||
## [0.35.2](https://github.com/sasjs/server/compare/v0.35.1...v0.35.2) (2023-08-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* add _debug as optional query param in swagger apis for GET stp/execute ([9586dbb](https://github.com/sasjs/server/commit/9586dbb2d0d6611061c9efdfb84030144f62c2ee))
|
||||
|
||||
## [0.35.1](https://github.com/sasjs/server/compare/v0.35.0...v0.35.1) (2023-07-25)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **log-separator:** log separator should always wrap log ([8940f4d](https://github.com/sasjs/server/commit/8940f4dc47abae2036b4fcdeb772c31a0ca07cca))
|
||||
|
||||
# [0.35.0](https://github.com/sasjs/server/compare/v0.34.2...v0.35.0) (2023-05-03)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** fixed log/webout/print tabs ([d2de9dc](https://github.com/sasjs/server/commit/d2de9dc13ef2e980286dd03cca5e22cea443ed0c))
|
||||
* **execute:** added atribute indicating stp api ([e78f87f](https://github.com/sasjs/server/commit/e78f87f5c00038ea11261dffb525ac8f1024e40b))
|
||||
* **execute:** fixed adding print output ([9aaffce](https://github.com/sasjs/server/commit/9aaffce82051d81bf39adb69942bb321e9795141))
|
||||
* **execution:** removed empty webout from response ([6dd2f4f](https://github.com/sasjs/server/commit/6dd2f4f87673336135bc7a6de0d2e143e192c025))
|
||||
* **webout:** fixed adding empty webout to response payload ([31df72a](https://github.com/sasjs/server/commit/31df72ad88fe2c771d0ef8445d6db9dd147c40c9))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **editor:** parse print output in response payload ([eb42683](https://github.com/sasjs/server/commit/eb42683fff701bd5b4d2b68760fe0c3ecad573dd))
|
||||
|
||||
## [0.34.2](https://github.com/sasjs/server/compare/v0.34.1...v0.34.2) (2023-05-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* use custom logic for handling sequence ids ([dba53de](https://github.com/sasjs/server/commit/dba53de64664c9d8a40fe69de6281c53d1c73641))
|
||||
|
||||
## [0.34.1](https://github.com/sasjs/server/compare/v0.34.0...v0.34.1) (2023-04-28)
|
||||
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ CORS=
|
||||
WHITELIST=
|
||||
|
||||
# HELMET Cross Origin Embedder Policy
|
||||
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
|
||||
# Sets the Cross-Origin-Embedder-Policy header to require-corp when `true`
|
||||
# options: [true|false] default: true
|
||||
# Docs: https://helmetjs.github.io/#reference (`crossOriginEmbedderPolicy`)
|
||||
HELMET_COEP=
|
||||
|
||||
98
api/package-lock.json
generated
98
api/package-lock.json
generated
@@ -21,7 +21,6 @@
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"ldapjs": "2.3.3",
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"rate-limiter-flexible": "2.4.1",
|
||||
@@ -43,7 +42,6 @@
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/ldapjs": "^2.2.4",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^15.12.2",
|
||||
@@ -53,7 +51,7 @@
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "0.27.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"http-headers-validation": "^0.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "8.11.4",
|
||||
@@ -3216,25 +3214,6 @@
|
||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mongoose": {
|
||||
"version": "5.11.97",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.97.tgz",
|
||||
"integrity": "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==",
|
||||
"deprecated": "Mongoose publishes its own types, so you do not need to install this package.",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"mongoose": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/mongoose-sequence": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongoose-sequence/-/mongoose-sequence-3.0.6.tgz",
|
||||
"integrity": "sha512-S6DD4rSlSnUI9BQvR/ACtekpylSIm0pEKayG9NqOlkUo3Q/AZLBmdi0IozSGPQ8JcB2ZSm81nLdZPhTqyOqrQg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/mongoose": "^5.10.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/morgan": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.3.tgz",
|
||||
@@ -3672,14 +3651,6 @@
|
||||
"node": ">=0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"node_modules/async-mutex": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz",
|
||||
@@ -4817,12 +4788,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dotenv": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
|
||||
"integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/duplexer2": {
|
||||
@@ -8125,7 +8096,8 @@
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
@@ -8583,18 +8555,6 @@
|
||||
"url": "https://opencollective.com/mongoose"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose-sequence": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose-sequence/-/mongoose-sequence-5.3.1.tgz",
|
||||
"integrity": "sha512-kQB1ctCdAQT8YdQzoHV0CpBRsO4RNVy03SOkzM6TQKBbGBs1ZgVS4UlKsuvBPaiPt9q5tKgQZvorGJ1awbHDqA==",
|
||||
"dependencies": {
|
||||
"async": "^2.5.0",
|
||||
"lodash": "^4.17.20"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"mongoose": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/mongoose/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
@@ -14014,24 +13974,6 @@
|
||||
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mongoose": {
|
||||
"version": "5.11.97",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.11.97.tgz",
|
||||
"integrity": "sha512-cqwOVYT3qXyLiGw7ueU2kX9noE8DPGRY6z8eUxudhXY8NZ7DMKYAxyZkLSevGfhCX3dO/AoX5/SO9lAzfjon0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"mongoose": "*"
|
||||
}
|
||||
},
|
||||
"@types/mongoose-sequence": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/mongoose-sequence/-/mongoose-sequence-3.0.6.tgz",
|
||||
"integrity": "sha512-S6DD4rSlSnUI9BQvR/ACtekpylSIm0pEKayG9NqOlkUo3Q/AZLBmdi0IozSGPQ8JcB2ZSm81nLdZPhTqyOqrQg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/mongoose": "^5.10.5"
|
||||
}
|
||||
},
|
||||
"@types/morgan": {
|
||||
"version": "1.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/morgan/-/morgan-1.9.3.tgz",
|
||||
@@ -14409,14 +14351,6 @@
|
||||
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
|
||||
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
|
||||
},
|
||||
"async": {
|
||||
"version": "2.6.4",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz",
|
||||
"integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==",
|
||||
"requires": {
|
||||
"lodash": "^4.17.14"
|
||||
}
|
||||
},
|
||||
"async-mutex": {
|
||||
"version": "0.3.2",
|
||||
"resolved": "https://registry.npmjs.org/async-mutex/-/async-mutex-0.3.2.tgz",
|
||||
@@ -15283,9 +15217,9 @@
|
||||
}
|
||||
},
|
||||
"dotenv": {
|
||||
"version": "10.0.0",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz",
|
||||
"integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==",
|
||||
"version": "16.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz",
|
||||
"integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==",
|
||||
"dev": true
|
||||
},
|
||||
"duplexer2": {
|
||||
@@ -17760,7 +17694,8 @@
|
||||
"lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"dev": true
|
||||
},
|
||||
"lodash.escaperegexp": {
|
||||
"version": "4.1.2",
|
||||
@@ -18126,15 +18061,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mongoose-sequence": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/mongoose-sequence/-/mongoose-sequence-5.3.1.tgz",
|
||||
"integrity": "sha512-kQB1ctCdAQT8YdQzoHV0CpBRsO4RNVy03SOkzM6TQKBbGBs1ZgVS4UlKsuvBPaiPt9q5tKgQZvorGJ1awbHDqA==",
|
||||
"requires": {
|
||||
"async": "^2.5.0",
|
||||
"lodash": "^4.17.20"
|
||||
}
|
||||
},
|
||||
"morgan": {
|
||||
"version": "1.10.0",
|
||||
"resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz",
|
||||
|
||||
@@ -61,7 +61,6 @@
|
||||
"jsonwebtoken": "^8.5.1",
|
||||
"ldapjs": "2.3.3",
|
||||
"mongoose": "^6.0.12",
|
||||
"mongoose-sequence": "^5.3.1",
|
||||
"morgan": "^1.10.0",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"rate-limiter-flexible": "2.4.1",
|
||||
@@ -80,7 +79,6 @@
|
||||
"@types/jest": "^26.0.24",
|
||||
"@types/jsonwebtoken": "^8.5.5",
|
||||
"@types/ldapjs": "^2.2.4",
|
||||
"@types/mongoose-sequence": "^3.0.6",
|
||||
"@types/morgan": "^1.9.3",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^15.12.2",
|
||||
@@ -90,7 +88,7 @@
|
||||
"adm-zip": "^0.5.9",
|
||||
"axios": "0.27.2",
|
||||
"csrf": "^3.1.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"dotenv": "^16.0.1",
|
||||
"http-headers-validation": "^0.0.1",
|
||||
"jest": "^27.0.6",
|
||||
"mongodb-memory-server": "8.11.4",
|
||||
|
||||
@@ -772,7 +772,7 @@ paths:
|
||||
examples:
|
||||
'Example 1':
|
||||
value: [{clientId: someClientID1234, clientSecret: someRandomCryptoString, accessTokenExpiration: 86400}, {clientId: someOtherClientID, clientSecret: someOtherRandomCryptoString, accessTokenExpiration: 86400}]
|
||||
summary: 'Admin only task. Returns the list of all the clients *'
|
||||
summary: 'Admin only task. Returns the list of all the clients'
|
||||
tags:
|
||||
- Client
|
||||
security:
|
||||
@@ -792,7 +792,7 @@ paths:
|
||||
- {type: string}
|
||||
- {type: string, format: byte}
|
||||
description: 'Execute Code on the Specified Runtime'
|
||||
summary: 'Run Code and Return Webout Content and Log'
|
||||
summary: "Run Code and Return Webout Content, Log and Print output\nThe order of returned parts of the payload is:\n1. Webout (if present)\n2. Logs UUID (used as separator)\n3. Log\n4. Logs UUID (used as separator)\n5. Print (if present and if the runtime is SAS)\nPlease see"
|
||||
tags:
|
||||
- Code
|
||||
security:
|
||||
@@ -1798,13 +1798,22 @@ paths:
|
||||
bearerAuth: []
|
||||
parameters:
|
||||
-
|
||||
description: 'Location of code in SASjs Drive'
|
||||
description: 'Location of the Stored Program in SASjs Drive'
|
||||
in: query
|
||||
name: _program
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
example: /Projects/myApp/some/program
|
||||
-
|
||||
description: 'Optional query param for setting debug mode (returns the session log in the response body)'
|
||||
in: query
|
||||
name: _debug
|
||||
required: false
|
||||
schema:
|
||||
format: double
|
||||
type: number
|
||||
example: 131
|
||||
post:
|
||||
operationId: ExecutePostRequest
|
||||
responses:
|
||||
|
||||
@@ -28,7 +28,14 @@ interface ExecuteCodePayload {
|
||||
export class CodeController {
|
||||
/**
|
||||
* Execute Code on the Specified Runtime
|
||||
* @summary Run Code and Return Webout Content and Log
|
||||
* @summary Run Code and Return Webout Content, Log and Print output
|
||||
* The order of returned parts of the payload is:
|
||||
* 1. Webout (if present)
|
||||
* 2. Logs UUID (used as separator)
|
||||
* 3. Log
|
||||
* 4. Logs UUID (used as separator)
|
||||
* 5. Print (if present and if the runtime is SAS)
|
||||
* Please see @sasjs/server/api/src/controllers/internal/Execution.ts for more information
|
||||
*/
|
||||
@Post('/execute')
|
||||
public async executeCode(
|
||||
@@ -55,7 +62,8 @@ const executeCode = async (
|
||||
preProgramVariables: getPreProgramVariables(req),
|
||||
vars: { ...req.query, _debug: 131 },
|
||||
otherArgs: { userAutoExec },
|
||||
runTime: runTime
|
||||
runTime: runTime,
|
||||
includePrintOutput: true
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
@@ -33,6 +33,7 @@ interface ExecuteFileParams {
|
||||
|
||||
interface ExecuteProgramParams extends Omit<ExecuteFileParams, 'programPath'> {
|
||||
program: string
|
||||
includePrintOutput?: boolean
|
||||
}
|
||||
|
||||
export class ExecutionController {
|
||||
@@ -67,7 +68,8 @@ export class ExecutionController {
|
||||
otherArgs,
|
||||
session: sessionByFileUpload,
|
||||
runTime,
|
||||
forceStringResult
|
||||
forceStringResult,
|
||||
includePrintOutput
|
||||
}: ExecuteProgramParams): Promise<ExecuteReturnRaw> {
|
||||
const sessionController = getSessionController(runTime)
|
||||
|
||||
@@ -78,7 +80,6 @@ export class ExecutionController {
|
||||
|
||||
const logPath = path.join(session.path, 'log.log')
|
||||
const headersPath = path.join(session.path, 'stpsrv_header.txt')
|
||||
|
||||
const weboutPath = path.join(session.path, 'webout.txt')
|
||||
const tokenFile = path.join(session.path, 'reqHeaders.txt')
|
||||
|
||||
@@ -122,12 +123,29 @@ export class ExecutionController {
|
||||
// it should be deleted by scheduleSessionDestroy
|
||||
session.inUse = false
|
||||
|
||||
const resultParts = []
|
||||
|
||||
// INFO: webout can be a Buffer, that is why it's length should be checked to determine if it is empty
|
||||
if (webout && webout.length !== 0) resultParts.push(webout)
|
||||
|
||||
// INFO: log separator wraps the log from the beginning and the end
|
||||
resultParts.push(process.logsUUID)
|
||||
resultParts.push(log)
|
||||
resultParts.push(process.logsUUID)
|
||||
|
||||
if (includePrintOutput && runTime === RunTimeType.SAS) {
|
||||
const printOutputPath = path.join(session.path, 'output.lst')
|
||||
const printOutput = (await fileExists(printOutputPath))
|
||||
? await readFile(printOutputPath)
|
||||
: ''
|
||||
|
||||
if (printOutput) resultParts.push(printOutput)
|
||||
}
|
||||
|
||||
return {
|
||||
httpHeaders,
|
||||
result:
|
||||
isDebugOn(vars) || session.crashed
|
||||
? `${webout}\n${process.logsUUID}\n${log}`
|
||||
: webout
|
||||
isDebugOn(vars) || session.crashed ? resultParts.join(`\n`) : webout
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -190,17 +190,20 @@ ${autoExecContent}`
|
||||
}
|
||||
|
||||
private scheduleSessionDestroy(session: Session) {
|
||||
setTimeout(async () => {
|
||||
if (session.inUse) {
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
setTimeout(
|
||||
async () => {
|
||||
if (session.inUse) {
|
||||
// adding 10 more minutes
|
||||
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000
|
||||
session.deathTimeStamp = newDeathTimeStamp.toString()
|
||||
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
await this.deleteSession(session)
|
||||
}
|
||||
}, parseInt(session.deathTimeStamp) - new Date().getTime() - 100)
|
||||
this.scheduleSessionDestroy(session)
|
||||
} else {
|
||||
await this.deleteSession(session)
|
||||
}
|
||||
},
|
||||
parseInt(session.deathTimeStamp) - new Date().getTime() - 100
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const createJSProgram = async (
|
||||
) => {
|
||||
const varStatments = Object.keys(vars).reduce(
|
||||
(computed: string, key: string) =>
|
||||
`${computed}const ${key} = '${vars[key]}';\n`,
|
||||
`${computed}const ${key} = \`${vars[key]}\`;\n`,
|
||||
''
|
||||
)
|
||||
|
||||
|
||||
@@ -3,12 +3,11 @@ import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
|
||||
import { ExecutionController, ExecutionVars } from './internal'
|
||||
import {
|
||||
getPreProgramVariables,
|
||||
HTTPHeaders,
|
||||
LogLine,
|
||||
makeFilesNamesMap,
|
||||
getRunTimeAndFilePath
|
||||
} from '../utils'
|
||||
import { MulterFile } from '../types/Upload'
|
||||
import { debug } from 'console'
|
||||
|
||||
interface ExecutePostRequestPayload {
|
||||
/**
|
||||
@@ -25,20 +24,31 @@ export class STPController {
|
||||
/**
|
||||
* Trigger a Stored Program using the _program URL parameter.
|
||||
*
|
||||
* Accepts URL parameters and file uploads. For more details, see docs:
|
||||
* Accepts additional URL parameters (converted to session variables)
|
||||
* and file uploads. For more details, see docs:
|
||||
*
|
||||
* https://server.sasjs.io/storedprograms
|
||||
*
|
||||
* @summary Execute a Stored Program, returns _webout and (optionally) log.
|
||||
* @param _program Location of code in SASjs Drive
|
||||
* @param _debug Optional query param for setting debug mode, which will return the session log.
|
||||
* @example _program "/Projects/myApp/some/program"
|
||||
* @example _debug 131
|
||||
*/
|
||||
@Get('/execute')
|
||||
public async executeGetRequest(
|
||||
@Request() request: express.Request,
|
||||
@Query() _program: string
|
||||
@Query() _program: string,
|
||||
@Query() _debug?: number
|
||||
): Promise<string | Buffer> {
|
||||
const vars = request.query as ExecutionVars
|
||||
let vars = request.query as ExecutionVars
|
||||
if (_debug) {
|
||||
vars = {
|
||||
...vars,
|
||||
_debug
|
||||
}
|
||||
}
|
||||
|
||||
return execute(request, _program, vars)
|
||||
}
|
||||
|
||||
|
||||
15
api/src/model/Counter.ts
Normal file
15
api/src/model/Counter.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import mongoose, { Schema } from 'mongoose'
|
||||
|
||||
const CounterSchema = new Schema({
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
seq: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
export default mongoose.model('Counter', CounterSchema)
|
||||
@@ -1,8 +1,7 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
import { Schema, model, Document, Model } from 'mongoose'
|
||||
import { GroupDetailsResponse } from '../controllers'
|
||||
import User, { IUser } from './User'
|
||||
import { AuthProviderType } from '../utils'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
import { AuthProviderType, getSequenceNextValue } from '../utils'
|
||||
|
||||
export const PUBLIC_GROUP_NAME = 'Public'
|
||||
|
||||
@@ -44,6 +43,10 @@ const groupSchema = new Schema<IGroupDocument>({
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
groupId: {
|
||||
type: Number,
|
||||
unique: true
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
default: 'Group description.'
|
||||
@@ -59,9 +62,13 @@ const groupSchema = new Schema<IGroupDocument>({
|
||||
users: [{ type: Schema.Types.ObjectId, ref: 'User' }]
|
||||
})
|
||||
|
||||
groupSchema.plugin(AutoIncrement, { inc_field: 'groupId' })
|
||||
|
||||
// Hooks
|
||||
groupSchema.pre('save', async function () {
|
||||
if (this.isNew) {
|
||||
this.groupId = await getSequenceNextValue('groupId')
|
||||
}
|
||||
})
|
||||
|
||||
groupSchema.post('save', function (group: IGroup, next: Function) {
|
||||
group.populate('users', 'id username displayName -_id').then(function () {
|
||||
next()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
import { Schema, model, Document, Model } from 'mongoose'
|
||||
import { PermissionDetailsResponse } from '../controllers'
|
||||
import { getSequenceNextValue } from '../utils'
|
||||
|
||||
interface GetPermissionBy {
|
||||
user?: Schema.Types.ObjectId
|
||||
@@ -23,6 +23,10 @@ interface IPermissionModel extends Model<IPermission> {
|
||||
}
|
||||
|
||||
const permissionSchema = new Schema<IPermissionDocument>({
|
||||
permissionId: {
|
||||
type: Number,
|
||||
unique: true
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -39,7 +43,12 @@ const permissionSchema = new Schema<IPermissionDocument>({
|
||||
group: { type: Schema.Types.ObjectId, ref: 'Group' }
|
||||
})
|
||||
|
||||
permissionSchema.plugin(AutoIncrement, { inc_field: 'permissionId' })
|
||||
// Hooks
|
||||
permissionSchema.pre('save', async function () {
|
||||
if (this.isNew) {
|
||||
this.permissionId = await getSequenceNextValue('permissionId')
|
||||
}
|
||||
})
|
||||
|
||||
// Static Methods
|
||||
permissionSchema.static('get', async function (getBy: GetPermissionBy): Promise<
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import mongoose, { Schema, model, Document, Model } from 'mongoose'
|
||||
const AutoIncrement = require('mongoose-sequence')(mongoose)
|
||||
import { Schema, model, Document, Model } from 'mongoose'
|
||||
import bcrypt from 'bcryptjs'
|
||||
import { AuthProviderType } from '../utils'
|
||||
import { AuthProviderType, getSequenceNextValue } from '../utils'
|
||||
|
||||
export interface UserPayload {
|
||||
/**
|
||||
@@ -66,6 +65,10 @@ const userSchema = new Schema<IUserDocument>({
|
||||
required: true,
|
||||
unique: true
|
||||
},
|
||||
id: {
|
||||
type: Number,
|
||||
unique: true
|
||||
},
|
||||
password: {
|
||||
type: String,
|
||||
required: true
|
||||
@@ -107,7 +110,15 @@ const userSchema = new Schema<IUserDocument>({
|
||||
}
|
||||
]
|
||||
})
|
||||
userSchema.plugin(AutoIncrement, { inc_field: 'id' })
|
||||
|
||||
// Hooks
|
||||
userSchema.pre('save', async function (next) {
|
||||
if (this.isNew) {
|
||||
this.id = await getSequenceNextValue('id')
|
||||
}
|
||||
|
||||
next()
|
||||
})
|
||||
|
||||
// Static Methods
|
||||
userSchema.static('hashPassword', (password: string): string => {
|
||||
|
||||
@@ -13,7 +13,11 @@ stpRouter.get('/execute', async (req, res) => {
|
||||
if (error) return res.status(400).send(error.details[0].message)
|
||||
|
||||
try {
|
||||
const response = await controller.executeGetRequest(req, query._program)
|
||||
const response = await controller.executeGetRequest(
|
||||
req,
|
||||
query._program,
|
||||
query._debug
|
||||
)
|
||||
|
||||
if (response instanceof Buffer) {
|
||||
res.writeHead(200, (req as any).sasHeaders)
|
||||
|
||||
15
api/src/utils/getSequenceNextValue.ts
Normal file
15
api/src/utils/getSequenceNextValue.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import Counter from '../model/Counter'
|
||||
|
||||
export const getSequenceNextValue = async (seqName: string) => {
|
||||
const seqDoc = await Counter.findOne({ id: seqName })
|
||||
if (!seqDoc) {
|
||||
await Counter.create({ id: seqName, seq: 1 })
|
||||
return 1
|
||||
}
|
||||
|
||||
seqDoc.seq += 1
|
||||
|
||||
await seqDoc.save()
|
||||
|
||||
return seqDoc.seq
|
||||
}
|
||||
@@ -14,6 +14,7 @@ export * from './getCertificates'
|
||||
export * from './getDesktopFields'
|
||||
export * from './getPreProgramVariables'
|
||||
export * from './getRunTimeAndFilePath'
|
||||
export * from './getSequenceNextValue'
|
||||
export * from './getServerUrl'
|
||||
export * from './getTokensFromDB'
|
||||
export * from './instantiateLogger'
|
||||
|
||||
@@ -1,9 +1,31 @@
|
||||
import path from 'path'
|
||||
import { createFolder, getAbsolutePath, getRealPath } from '@sasjs/utils'
|
||||
|
||||
import {
|
||||
createFolder,
|
||||
getAbsolutePath,
|
||||
getRealPath,
|
||||
fileExists
|
||||
} from '@sasjs/utils'
|
||||
import dotenv from 'dotenv'
|
||||
import { connectDB, getDesktopFields, ModeType, RunTimeType, SECRETS } from '.'
|
||||
|
||||
export const setProcessVariables = async () => {
|
||||
const { execPath } = process
|
||||
|
||||
// Check if execPath ends with 'api-macos' to determine executable for MacOS.
|
||||
// This is needed to fix picking .env file issue in MacOS executable.
|
||||
if (execPath) {
|
||||
const envPathSplitted = execPath.split(path.sep)
|
||||
|
||||
if (envPathSplitted.pop() === 'api-macos') {
|
||||
const envPath = path.join(envPathSplitted.join(path.sep), '.env')
|
||||
|
||||
// Override environment variables from envPath if file exists
|
||||
if (await fileExists(envPath)) {
|
||||
dotenv.config({ path: envPath, override: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const { MODE, RUN_TIMES } = process.env
|
||||
|
||||
if (MODE === ModeType.Server) {
|
||||
@@ -21,6 +43,7 @@ export const setProcessVariables = async () => {
|
||||
if (process.env.NODE_ENV === 'test') {
|
||||
process.sasjsRoot = path.join(process.cwd(), 'sasjs_root')
|
||||
process.driveLoc = path.join(process.cwd(), 'sasjs_root', 'drive')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -41,7 +64,9 @@ export const setProcessVariables = async () => {
|
||||
|
||||
const { SASJS_ROOT } = process.env
|
||||
const absPath = getAbsolutePath(SASJS_ROOT ?? 'sasjs_root', process.cwd())
|
||||
|
||||
await createFolder(absPath)
|
||||
|
||||
process.sasjsRoot = getRealPath(absPath)
|
||||
|
||||
const { DRIVE_LOCATION } = process.env
|
||||
@@ -49,6 +74,7 @@ export const setProcessVariables = async () => {
|
||||
DRIVE_LOCATION ?? path.join(process.sasjsRoot, 'drive'),
|
||||
process.cwd()
|
||||
)
|
||||
|
||||
await createFolder(absDrivePath)
|
||||
process.driveLoc = getRealPath(absDrivePath)
|
||||
|
||||
@@ -57,7 +83,9 @@ export const setProcessVariables = async () => {
|
||||
LOG_LOCATION ?? path.join(process.sasjsRoot, 'logs'),
|
||||
process.cwd()
|
||||
)
|
||||
|
||||
await createFolder(absLogsPath)
|
||||
|
||||
process.logsLoc = getRealPath(absLogsPath)
|
||||
|
||||
process.logsUUID = 'SASJS_LOGS_SEPARATOR_163ee17b6ff24f028928972d80a26784'
|
||||
|
||||
@@ -51,9 +51,8 @@ export const generateFileUploadSasCode = async (
|
||||
let fileCount = 0
|
||||
const uploadedFiles: UploadedFiles[] = []
|
||||
|
||||
const sasSessionFolderList: string[] = await listFilesInFolder(
|
||||
sasSessionFolder
|
||||
)
|
||||
const sasSessionFolderList: string[] =
|
||||
await listFilesInFolder(sasSessionFolder)
|
||||
sasSessionFolderList.forEach((fileName) => {
|
||||
let fileCountString = fileCount < 100 ? '0' + fileCount : fileCount
|
||||
fileCountString = fileCount < 10 ? '00' + fileCount : fileCount
|
||||
|
||||
@@ -180,7 +180,8 @@ export const runCodeValidation = (data: any): Joi.ValidationResult =>
|
||||
|
||||
export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
|
||||
Joi.object({
|
||||
_program: Joi.string().required()
|
||||
_program: Joi.string().required(),
|
||||
_debug: Joi.number()
|
||||
})
|
||||
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
|
||||
.validate(data)
|
||||
|
||||
@@ -3,12 +3,11 @@ import Snackbar from '@mui/material/Snackbar'
|
||||
import MuiAlert, { AlertProps } from '@mui/material/Alert'
|
||||
import Slide, { SlideProps } from '@mui/material/Slide'
|
||||
|
||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(function Alert(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
|
||||
})
|
||||
const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
|
||||
function Alert(props, ref) {
|
||||
return <MuiAlert elevation={6} ref={ref} variant="filled" {...props} />
|
||||
}
|
||||
)
|
||||
|
||||
const Transition = (props: SlideProps) => {
|
||||
return <Slide {...props} direction="up" />
|
||||
|
||||
@@ -62,6 +62,7 @@ const SASjsEditor = ({
|
||||
selectedRunTime,
|
||||
showDiff,
|
||||
webout,
|
||||
printOutput,
|
||||
Dialog,
|
||||
handleChangeRunTime,
|
||||
handleDiffEditorDidMount,
|
||||
@@ -153,30 +154,35 @@ const SASjsEditor = ({
|
||||
>
|
||||
<TabList onChange={handleTabChange} centered>
|
||||
<StyledTab label="Code" value="code" />
|
||||
<StyledTab
|
||||
label={logWithErrorsOrWarnings ? '' : 'log'}
|
||||
value="log"
|
||||
icon={
|
||||
logWithErrorsOrWarnings ? (
|
||||
<LogTabWithIcons log={log as LogObject} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
const logWrapper = document.querySelector(`#logWrapper`)
|
||||
{log && (
|
||||
<StyledTab
|
||||
label={logWithErrorsOrWarnings ? '' : 'log'}
|
||||
value="log"
|
||||
icon={
|
||||
logWithErrorsOrWarnings ? (
|
||||
<LogTabWithIcons log={log as LogObject} />
|
||||
) : (
|
||||
''
|
||||
)
|
||||
}
|
||||
onClick={() => {
|
||||
const logWrapper = document.querySelector(`#logWrapper`)
|
||||
|
||||
if (logWrapper) logWrapper.scrollTop = 0
|
||||
}}
|
||||
/>
|
||||
<StyledTab
|
||||
label={
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
<Typography>Webout</Typography>
|
||||
</Tooltip>
|
||||
}
|
||||
value="webout"
|
||||
/>
|
||||
if (logWrapper) logWrapper.scrollTop = 0
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{webout && (
|
||||
<StyledTab
|
||||
label={
|
||||
<Tooltip title="Displays content from the _webout fileref">
|
||||
<Typography>Webout</Typography>
|
||||
</Tooltip>
|
||||
}
|
||||
value="webout"
|
||||
/>
|
||||
)}
|
||||
{printOutput && <StyledTab label="print" value="printOutput" />}
|
||||
</TabList>
|
||||
</Box>
|
||||
|
||||
@@ -222,11 +228,20 @@ const SASjsEditor = ({
|
||||
<LogComponent log={log} selectedRunTime={selectedRunTime} />
|
||||
)}
|
||||
</StyledTabPanel>
|
||||
<StyledTabPanel value="webout">
|
||||
<div>
|
||||
<pre>{webout}</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
{webout && (
|
||||
<StyledTabPanel value="webout">
|
||||
<div>
|
||||
<pre>{webout}</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
)}
|
||||
{printOutput && (
|
||||
<StyledTabPanel value="printOutput">
|
||||
<div>
|
||||
<pre>{printOutput}</pre>
|
||||
</div>
|
||||
</StyledTabPanel>
|
||||
)}
|
||||
</TabContext>
|
||||
)}
|
||||
<Dialog />
|
||||
|
||||
@@ -7,8 +7,10 @@
|
||||
border: none;
|
||||
outline: none;
|
||||
transition: 0.4s;
|
||||
box-shadow: rgba(0, 0, 0, 0.2) 0px 2px 1px -1px,
|
||||
rgba(0, 0, 0, 0.14) 0px 1px 1px 0px, rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
|
||||
box-shadow:
|
||||
rgba(0, 0, 0, 0.2) 0px 2px 1px -1px,
|
||||
rgba(0, 0, 0, 0.14) 0px 1px 1px 0px,
|
||||
rgba(0, 0, 0, 0.12) 0px 1px 3px 0px;
|
||||
}
|
||||
|
||||
.ChunkDetails {
|
||||
|
||||
@@ -39,14 +39,14 @@ const useEditor = ({
|
||||
const { Snackbar, setOpenSnackbar, setSnackbarMessage, setSnackbarSeverity } =
|
||||
useSnackbar()
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const [prevFileContent, setPrevFileContent] = useStateWithCallback('')
|
||||
const [fileContent, setFileContent] = useState('')
|
||||
const [log, setLog] = useState<LogObject | string>()
|
||||
const [webout, setWebout] = useState('')
|
||||
const [webout, setWebout] = useState<string>()
|
||||
const [printOutput, setPrintOutput] = useState<string>()
|
||||
const [runTimes, setRunTimes] = useState<string[]>([])
|
||||
const [selectedRunTime, setSelectedRunTime] = useState<RunTimeType | string>(
|
||||
''
|
||||
const [selectedRunTime, setSelectedRunTime] = useState<RunTimeType>(
|
||||
RunTimeType.SAS
|
||||
)
|
||||
const [selectedFileExtension, setSelectedFileExtension] = useState('')
|
||||
const [openFilePathInputModal, setOpenFilePathInputModal] = useState(false)
|
||||
@@ -169,25 +169,30 @@ const useEditor = ({
|
||||
),
|
||||
runTime: selectedRunTime
|
||||
})
|
||||
.then((res: any) => {
|
||||
if (selectedRunTime === RunTimeType.SAS) {
|
||||
const { errors, warnings, logLines } = parseErrorsAndWarnings(
|
||||
res.data.split(SASJS_LOGS_SEPARATOR)[1]
|
||||
)
|
||||
.then((res: { data: string }) => {
|
||||
// INFO: the order of payload parts is set in @sasjs/server/api/src/controllers/internal/Execution.ts
|
||||
const resDataSplitted = res.data.split(SASJS_LOGS_SEPARATOR)
|
||||
const webout = resDataSplitted[0]
|
||||
const log = resDataSplitted[1]
|
||||
const printOutput = resDataSplitted[2]
|
||||
|
||||
const log: LogObject = {
|
||||
if (selectedRunTime === RunTimeType.SAS) {
|
||||
const { errors, warnings, logLines } = parseErrorsAndWarnings(log)
|
||||
|
||||
const logObject: LogObject = {
|
||||
body: logLines.join(`\n`),
|
||||
errors,
|
||||
warnings,
|
||||
linesCount: logLines.length
|
||||
}
|
||||
|
||||
setLog(log)
|
||||
setLog(logObject)
|
||||
} else {
|
||||
setLog(res.data.split(SASJS_LOGS_SEPARATOR)[1] ?? '')
|
||||
setLog(log)
|
||||
}
|
||||
|
||||
setWebout(res.data.split(SASJS_LOGS_SEPARATOR)[0] ?? '')
|
||||
setWebout(webout)
|
||||
setPrintOutput(printOutput)
|
||||
setTab('log')
|
||||
|
||||
// Scroll to bottom of log
|
||||
@@ -335,6 +340,7 @@ const useEditor = ({
|
||||
selectedRunTime,
|
||||
showDiff,
|
||||
webout,
|
||||
printOutput,
|
||||
Dialog,
|
||||
handleChangeRunTime,
|
||||
handleDiffEditorDidMount,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
|
||||
Reference in New Issue
Block a user