From e9519cb3c65715b397422d5bbb8348d2df870855 Mon Sep 17 00:00:00 2001 From: Yury Date: Tue, 29 Oct 2024 16:20:27 +0300 Subject: [PATCH 1/3] chore(code): used correct type --- api/src/controllers/code.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From b0723f14448d60ffce4f2175cf8a73fc4d4dd0ee Mon Sep 17 00:00:00 2001 From: Yury Date: Tue, 29 Oct 2024 16:27:53 +0300 Subject: [PATCH 2/3] feat(stp): added trigger endpoint --- api/public/swagger.yaml | 57 ++++++++++++++++++++ api/src/controllers/stp.ts | 100 +++++++++++++++++++++++++++++++++++- api/src/routes/api/stp.ts | 28 +++++++++- api/src/utils/validation.ts | 9 ++++ 4 files changed, 191 insertions(+), 3 deletions(-) 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/stp.ts b/api/src/controllers/stp.ts index dbc881d..3c35adc 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,31 @@ 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, + @Query() _program?: string + ): Promise { + const program = _program ?? body?._program + const vars = { ...request.query, ...request.body } + const filesNamesMap = request.files?.length + ? makeFilesNamesMap(request.files as MulterFile[]) + : null + const otherArgs = { filesNamesMap: filesNamesMap } + const { expiresAfterMins } = body + + return triggerProgram(request, program!, vars, otherArgs, expiresAfterMins) + } } const execute = async ( @@ -117,3 +169,47 @@ const execute = async ( } } } + +const triggerProgram = async ( + req: express.Request, + _program: string, + vars: ExecutionVars, + otherArgs?: any, + expiresAfterMins?: number +): Promise => { + try { + 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..9ce45e5 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,27 @@ 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, + req.query?._program as string + ) + + 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) From deee34f5fd008aa9efe80b9bf2299f9466c31712 Mon Sep 17 00:00:00 2001 From: Yury Date: Tue, 29 Oct 2024 16:55:40 +0300 Subject: [PATCH 3/3] chore(stp): removed query logic from trigger endpoint --- api/src/controllers/stp.ts | 23 ++++++++--------------- api/src/routes/api/stp.ts | 6 +----- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/api/src/controllers/stp.ts b/api/src/controllers/stp.ts index 3c35adc..573cf0a 100644 --- a/api/src/controllers/stp.ts +++ b/api/src/controllers/stp.ts @@ -118,18 +118,9 @@ export class STPController { @Post('/trigger') public async triggerProgram( @Request() request: express.Request, - @Body() body: TriggerProgramPayload, - @Query() _program?: string + @Body() body: TriggerProgramPayload ): Promise { - const program = _program ?? body?._program - const vars = { ...request.query, ...request.body } - const filesNamesMap = request.files?.length - ? makeFilesNamesMap(request.files as MulterFile[]) - : null - const otherArgs = { filesNamesMap: filesNamesMap } - const { expiresAfterMins } = body - - return triggerProgram(request, program!, vars, otherArgs, expiresAfterMins) + return triggerProgram(request, body) } } @@ -172,12 +163,14 @@ const execute = async ( const triggerProgram = async ( req: express.Request, - _program: string, - vars: ExecutionVars, - otherArgs?: any, - expiresAfterMins?: number + { _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 diff --git a/api/src/routes/api/stp.ts b/api/src/routes/api/stp.ts index 9ce45e5..632bcbd 100644 --- a/api/src/routes/api/stp.ts +++ b/api/src/routes/api/stp.ts @@ -78,11 +78,7 @@ stpRouter.post('/trigger', async (req, res) => { if (error) return res.status(400).send(error.details[0].message) try { - const response = await controller.triggerProgram( - req, - body, - req.query?._program as string - ) + const response = await controller.triggerProgram(req, body) res.status(200) res.send(response)