diff --git a/api/public/swagger.yaml b/api/public/swagger.yaml index 922b2f4..2db3115 100644 --- a/api/public/swagger.yaml +++ b/api/public/swagger.yaml @@ -593,6 +593,31 @@ components: example: /Public/somefolder/some.file type: object additionalProperties: false + TriggerProgramResponse: + 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 program status.\nThis session ID should be used to poll program status." + example: '{ sessionId: ''20241028074744-54132-1730101664824'' }' + required: + - sessionId + type: object + additionalProperties: false + TriggerProgramPayload: + properties: + _program: + type: string + description: 'Location of SAS program' + example: /Public/somefolder/some.file + expiresAfterMins: + type: number + format: double + description: "Amount of minutes after the completion of the program when the session must be\ndestroyed." + example: 15 + required: + - _program + type: object + additionalProperties: false LoginPayload: properties: username: @@ -1901,6 +1926,38 @@ paths: application/json: schema: $ref: '#/components/schemas/ExecutePostRequestPayload' + /SASjsApi/stp/trigger: + post: + operationId: TriggerProgram + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/TriggerProgramResponse' + description: 'Trigger Program on the Specified Runtime' + summary: 'Triggers program and returns SessionId immediately - does not wait for program completion' + tags: + - STP + security: + - + bearerAuth: [] + parameters: + - + description: 'Location of code in SASjs Drive' + in: query + name: _program + required: false + schema: + type: string + example: /Projects/myApp/some/program + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/TriggerProgramPayload' /: get: operationId: Home diff --git a/api/src/controllers/code.ts b/api/src/controllers/code.ts index 4620ba5..039ce3c 100644 --- a/api/src/controllers/code.ts +++ b/api/src/controllers/code.ts @@ -120,7 +120,7 @@ const executeCode = async ( const triggerCode = async ( req: express.Request, { code, runTime, expiresAfterMins }: TriggerCodePayload -): Promise<{ sessionId: string }> => { +): Promise => { const { user } = req const userAutoExec = process.env.MODE === ModeType.Server diff --git a/api/src/controllers/stp.ts b/api/src/controllers/stp.ts index dbc881d..573cf0a 100644 --- a/api/src/controllers/stp.ts +++ b/api/src/controllers/stp.ts @@ -1,13 +1,16 @@ import express from 'express' import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa' -import { ExecutionController, ExecutionVars } from './internal' +import { + ExecutionController, + ExecutionVars, + getSessionController +} from './internal' import { getPreProgramVariables, makeFilesNamesMap, getRunTimeAndFilePath } from '../utils' import { MulterFile } from '../types/Upload' -import { debug } from 'console' interface ExecutePostRequestPayload { /** @@ -17,6 +20,30 @@ interface ExecutePostRequestPayload { _program?: string } +interface TriggerProgramPayload { + /** + * Location of SAS program + * @example "/Public/somefolder/some.file" + */ + _program: string + /** + * Amount of minutes after the completion of the program when the session must be + * destroyed. + * @example 15 + */ + expiresAfterMins?: number +} + +interface TriggerProgramResponse { + /** + * 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 program status. + * This session ID should be used to poll program status. + * @example "{ sessionId: '20241028074744-54132-1730101664824' }" + */ + sessionId: string +} + @Security('bearerAuth') @Route('SASjsApi/stp') @Tags('STP') @@ -79,6 +106,22 @@ export class STPController { return execute(request, program!, vars, otherArgs) } + + /** + * Trigger Program on the Specified Runtime + * @summary Triggers program and returns SessionId immediately - does not wait for program completion + * @param _program Location of code in SASjs Drive + * @example _program "/Projects/myApp/some/program" + * @param expiresAfterMins Amount of minutes after the completion of the program when the session must be destroyed + * @example expiresAfterMins 15 + */ + @Post('/trigger') + public async triggerProgram( + @Request() request: express.Request, + @Body() body: TriggerProgramPayload + ): Promise { + return triggerProgram(request, body) + } } const execute = async ( @@ -117,3 +160,49 @@ const execute = async ( } } } + +const triggerProgram = async ( + req: express.Request, + { _program, expiresAfterMins }: TriggerProgramPayload +): Promise => { + try { + const vars = { ...req.body } + const filesNamesMap = req.files?.length + ? makeFilesNamesMap(req.files as MulterFile[]) + : null + const otherArgs = { filesNamesMap: filesNamesMap } + const { codePath, runTime } = await getRunTimeAndFilePath(_program) + + // 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 } + } + + // call executeFile method of ExecutionController without awaiting + new ExecutionController().executeFile({ + programPath: codePath, + runTime, + preProgramVariables: getPreProgramVariables(req), + vars, + otherArgs, + session + }) + + // 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 + } + } +} diff --git a/api/src/routes/api/stp.ts b/api/src/routes/api/stp.ts index e65b25a..632bcbd 100644 --- a/api/src/routes/api/stp.ts +++ b/api/src/routes/api/stp.ts @@ -1,5 +1,8 @@ import express from 'express' -import { executeProgramRawValidation } from '../../utils' +import { + executeProgramRawValidation, + triggerProgramValidation +} from '../../utils' import { STPController } from '../../controllers/' import { FileUploadController } from '../../controllers/internal' @@ -69,4 +72,23 @@ stpRouter.post( } ) +stpRouter.post('/trigger', async (req, res) => { + const { error, value: body } = triggerProgramValidation(req.body) + + if (error) return res.status(400).send(error.details[0].message) + + try { + const response = await controller.triggerProgram(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 stpRouter diff --git a/api/src/utils/validation.ts b/api/src/utils/validation.ts index a1e3310..a4fe696 100644 --- a/api/src/utils/validation.ts +++ b/api/src/utils/validation.ts @@ -192,3 +192,12 @@ export const executeProgramRawValidation = (data: any): Joi.ValidationResult => }) .pattern(/^/, Joi.alternatives(Joi.string(), Joi.number())) .validate(data) + +export const triggerProgramValidation = (data: any): Joi.ValidationResult => + Joi.object({ + _program: Joi.string().required(), + _debug: Joi.number(), + expiresAfterMins: Joi.number().greater(0) + }) + .pattern(/^/, Joi.alternatives(Joi.string(), Joi.number())) + .validate(data)