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

Compare commits

...

6 Commits

Author SHA1 Message Date
semantic-release-bot
0838b8112e chore(release): 0.36.0 [skip ci]
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)

### Features

* **code:** added code/trigger API endpoint ([ffcf193](ffcf193b87))
2024-10-29 10:32:01 +00:00
Yury Shkoda
441f8b7726 Merge pull request #374 from sasjs/issue-373
feat(code): added code/trigger API endpoint
2024-10-29 13:29:08 +03:00
Yury
049a7f4b80 chore(swagger): improved description 2024-10-29 12:02:26 +03:00
Yury
3053c68bdf chore(lint): fixed linting issues 2024-10-29 11:40:44 +03:00
Yury
76750e864d chore(lint): fixed lint issue 2024-10-29 11:30:05 +03:00
Yury
ffcf193b87 feat(code): added code/trigger API endpoint 2024-10-29 11:18:04 +03:00
10 changed files with 209 additions and 20 deletions

View File

@@ -1,3 +1,10 @@
# [0.36.0](https://github.com/sasjs/server/compare/v0.35.4...v0.36.0) (2024-10-29)
### Features
* **code:** added code/trigger API endpoint ([ffcf193](https://github.com/sasjs/server/commit/ffcf193b87d811b166d79af74013776a253b50b0))
## [0.35.4](https://github.com/sasjs/server/compare/v0.35.3...v0.35.4) (2024-01-15) ## [0.35.4](https://github.com/sasjs/server/compare/v0.35.3...v0.35.4) (2024-01-15)

View File

@@ -98,17 +98,47 @@ components:
properties: properties:
code: code:
type: string type: string
description: 'Code of program' description: 'The code to be executed'
example: '* Code HERE;' example: '* Your Code HERE;'
runTime: runTime:
$ref: '#/components/schemas/RunTimeType' $ref: '#/components/schemas/RunTimeType'
description: 'runtime for program' description: 'The runtime for the code - eg SAS, JS, PY or R'
example: js example: js
required: required:
- code - code
- runTime - runTime
type: object type: object
additionalProperties: false additionalProperties: false
TriggerCodeResponse:
properties:
sessionId:
type: string
description: "The SessionId is the name of the temporary folder used to store the outputs.\nFor SAS, this would be the SASWORK folder. Can be used to poll job status.\nThis session ID should be used to poll job status."
example: '{ sessionId: ''20241028074744-54132-1730101664824'' }'
required:
- sessionId
type: object
additionalProperties: false
TriggerCodePayload:
properties:
code:
type: string
description: 'The code to be executed'
example: '* Your Code HERE;'
runTime:
$ref: '#/components/schemas/RunTimeType'
description: 'The runtime for the code - eg SAS, JS, PY or R'
example: sas
expiresAfterMins:
type: number
format: double
description: "Amount of minutes after the completion of the job when the session must be\ndestroyed."
example: 15
required:
- code
- runTime
type: object
additionalProperties: false
MemberType.folder: MemberType.folder:
enum: enum:
- folder - folder
@@ -805,6 +835,30 @@ paths:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/ExecuteCodePayload' $ref: '#/components/schemas/ExecuteCodePayload'
/SASjsApi/code/trigger:
post:
operationId: TriggerCode
responses:
'200':
description: Ok
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerCodeResponse'
description: 'Trigger Code on the Specified Runtime'
summary: 'Triggers code and returns SessionId immediately - does not wait for job completion'
tags:
- Code
security:
-
bearerAuth: []
parameters: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/TriggerCodePayload'
/SASjsApi/drive/deploy: /SASjsApi/drive/deploy:
post: post:
operationId: Deploy operationId: Deploy
@@ -1789,7 +1843,7 @@ paths:
anyOf: anyOf:
- {type: string} - {type: string}
- {type: string, format: byte} - {type: string, format: byte}
description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts URL parameters and file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms" description: "Trigger a Stored Program using the _program URL parameter.\n\nAccepts additional URL parameters (converted to session variables)\nand file uploads. For more details, see docs:\n\nhttps://server.sasjs.io/storedprograms"
summary: 'Execute a Stored Program, returns _webout and (optionally) log.' summary: 'Execute a Stored Program, returns _webout and (optionally) log.'
tags: tags:
- STP - STP
@@ -1798,7 +1852,7 @@ paths:
bearerAuth: [] bearerAuth: []
parameters: parameters:
- -
description: 'Location of the Stored Program in SASjs Drive' description: 'Location of Stored Program in SASjs Drive.'
in: query in: query
name: _program name: _program
required: true required: true
@@ -1806,7 +1860,7 @@ paths:
type: string type: string
example: /Projects/myApp/some/program example: /Projects/myApp/some/program
- -
description: 'Optional query param for setting debug mode (returns the session log in the response body)' description: 'Optional query param for setting debug mode (returns the session log in the response body).'
in: query in: query
name: _debug name: _debug
required: false required: false

View File

@@ -1,27 +1,55 @@
import express from 'express' import express from 'express'
import { Request, Security, Route, Tags, Post, Body } from 'tsoa' import { Request, Security, Route, Tags, Post, Body } from 'tsoa'
import { ExecutionController } from './internal' import { ExecutionController, getSessionController } from './internal'
import { import {
getPreProgramVariables, getPreProgramVariables,
getUserAutoExec, getUserAutoExec,
ModeType, ModeType,
parseLogToArray,
RunTimeType RunTimeType
} from '../utils' } from '../utils'
interface ExecuteCodePayload { interface ExecuteCodePayload {
/** /**
* Code of program * The code to be executed
* @example "* Code HERE;" * @example "* Your Code HERE;"
*/ */
code: string code: string
/** /**
* runtime for program * The runtime for the code - eg SAS, JS, PY or R
* @example "js" * @example "js"
*/ */
runTime: RunTimeType runTime: RunTimeType
} }
interface TriggerCodePayload {
/**
* The code to be executed
* @example "* Your Code HERE;"
*/
code: string
/**
* The runtime for the code - eg SAS, JS, PY or R
* @example "sas"
*/
runTime: RunTimeType
/**
* Amount of minutes after the completion of the job when the session must be
* destroyed.
* @example 15
*/
expiresAfterMins?: number
}
interface TriggerCodeResponse {
/**
* The SessionId is the name of the temporary folder used to store the outputs.
* For SAS, this would be the SASWORK folder. Can be used to poll job status.
* This session ID should be used to poll job status.
* @example "{ sessionId: '20241028074744-54132-1730101664824' }"
*/
sessionId: string
}
@Security('bearerAuth') @Security('bearerAuth')
@Route('SASjsApi/code') @Route('SASjsApi/code')
@Tags('Code') @Tags('Code')
@@ -44,6 +72,18 @@ export class CodeController {
): Promise<string | Buffer> { ): Promise<string | Buffer> {
return executeCode(request, body) return executeCode(request, body)
} }
/**
* Trigger Code on the Specified Runtime
* @summary Triggers code and returns SessionId immediately - does not wait for job completion
*/
@Post('/trigger')
public async triggerCode(
@Request() request: express.Request,
@Body() body: TriggerCodePayload
): Promise<TriggerCodeResponse> {
return triggerCode(request, body)
}
} }
const executeCode = async ( const executeCode = async (
@@ -76,3 +116,49 @@ const executeCode = async (
} }
} }
} }
const triggerCode = async (
req: express.Request,
{ code, runTime, expiresAfterMins }: TriggerCodePayload
): Promise<{ sessionId: string }> => {
const { user } = req
const userAutoExec =
process.env.MODE === ModeType.Server
? user?.autoExec
: await getUserAutoExec()
// get session controller based on runTime
const sessionController = getSessionController(runTime)
// get session
const session = await sessionController.getSession()
// add expiresAfterMins to session if provided
if (expiresAfterMins) {
// expiresAfterMins.used is set initially to false
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
}
try {
// call executeProgram method of ExecutionController without awaiting
new ExecutionController().executeProgram({
program: code,
preProgramVariables: getPreProgramVariables(req),
vars: { ...req.query, _debug: 131 },
otherArgs: { userAutoExec },
runTime: runTime,
includePrintOutput: true,
session // session is provided
})
// return session id
return { sessionId: session.id }
} catch (err: any) {
throw {
code: 400,
status: 'failure',
message: 'Job execution failed.',
error: typeof err === 'object' ? err.toString() : err
}
}
}

View File

@@ -14,8 +14,7 @@ import {
createFile, createFile,
fileExists, fileExists,
generateTimestamp, generateTimestamp,
readFile, readFile
isWindows
} from '@sasjs/utils' } from '@sasjs/utils'
const execFilePromise = promisify(execFile) const execFilePromise = promisify(execFile)
@@ -194,13 +193,30 @@ ${autoExecContent}`
async () => { async () => {
if (session.inUse) { if (session.inUse) {
// adding 10 more minutes // adding 10 more minutes
const newDeathTimeStamp = parseInt(session.deathTimeStamp) + 10 * 1000 const newDeathTimeStamp =
parseInt(session.deathTimeStamp) + 10 * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString() session.deathTimeStamp = newDeathTimeStamp.toString()
this.scheduleSessionDestroy(session)
} else {
const { expiresAfterMins } = session
// delay session destroy if expiresAfterMins present
if (expiresAfterMins && !expiresAfterMins.used) {
// calculate session death time using expiresAfterMins
const newDeathTimeStamp =
parseInt(session.deathTimeStamp) +
expiresAfterMins.mins * 60 * 1000
session.deathTimeStamp = newDeathTimeStamp.toString()
// set expiresAfterMins to true to avoid using it again
session.expiresAfterMins!.used = true
this.scheduleSessionDestroy(session) this.scheduleSessionDestroy(session)
} else { } else {
await this.deleteSession(session) await this.deleteSession(session)
} }
}
}, },
parseInt(session.deathTimeStamp) - new Date().getTime() - 100 parseInt(session.deathTimeStamp) - new Date().getTime() - 100
) )

View File

@@ -30,8 +30,8 @@ export class STPController {
* https://server.sasjs.io/storedprograms * https://server.sasjs.io/storedprograms
* *
* @summary Execute a Stored Program, returns _webout and (optionally) log. * @summary Execute a Stored Program, returns _webout and (optionally) log.
* @param _program Location of code in SASjs Drive * @param _program Location of Stored Program in SASjs Drive.
* @param _debug Optional query param for setting debug mode, which will return the session log. * @param _debug Optional query param for setting debug mode (returns the session log in the response body).
* @example _program "/Projects/myApp/some/program" * @example _program "/Projects/myApp/some/program"
* @example _debug 131 * @example _debug 131
*/ */

View File

@@ -285,7 +285,7 @@ const getUser = async (
username: user.username, username: user.username,
isActive: user.isActive, isActive: user.isActive,
isAdmin: user.isAdmin, isAdmin: user.isAdmin,
autoExec: getAutoExec ? user.autoExec ?? '' : undefined, autoExec: getAutoExec ? (user.autoExec ?? '') : undefined,
groups: user.groups groups: user.groups
} }
} }

View File

@@ -1,5 +1,5 @@
import express from 'express' import express from 'express'
import { runCodeValidation } from '../../utils' import { runCodeValidation, triggerCodeValidation } from '../../utils'
import { CodeController } from '../../controllers/' import { CodeController } from '../../controllers/'
const runRouter = express.Router() const runRouter = express.Router()
@@ -28,4 +28,22 @@ runRouter.post('/execute', async (req, res) => {
} }
}) })
runRouter.post('/trigger', async (req, res) => {
const { error, value: body } = triggerCodeValidation(req.body)
if (error) return res.status(400).send(error.details[0].message)
try {
const response = await controller.triggerCode(req, body)
res.status(200)
res.send(response)
} catch (err: any) {
const statusCode = err.code
delete err.code
res.status(statusCode).send(err)
}
})
export default runRouter export default runRouter

View File

@@ -8,4 +8,5 @@ export interface Session {
consumed: boolean consumed: boolean
completed: boolean completed: boolean
crashed?: string crashed?: string
expiresAfterMins?: { mins: number; used: boolean }
} }

View File

@@ -178,6 +178,13 @@ export const runCodeValidation = (data: any): Joi.ValidationResult =>
runTime: Joi.string().valid(...process.runTimes) runTime: Joi.string().valid(...process.runTimes)
}).validate(data) }).validate(data)
export const triggerCodeValidation = (data: any): Joi.ValidationResult =>
Joi.object({
code: Joi.string().required(),
runTime: Joi.string().valid(...process.runTimes),
expiresAfterMins: Joi.number().greater(0)
}).validate(data)
export const executeProgramRawValidation = (data: any): Joi.ValidationResult => export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
Joi.object({ Joi.object({
_program: Joi.string().required(), _program: Joi.string().required(),