Skip to content

Commit a044176

Browse files
authored
Merge pull request #375 from sasjs/issue-373-stp
Issue 373 stp
2 parents 0838b81 + deee34f commit a044176

File tree

5 files changed

+181
-4
lines changed

5 files changed

+181
-4
lines changed

api/public/swagger.yaml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,31 @@ components:
593593
example: /Public/somefolder/some.file
594594
type: object
595595
additionalProperties: false
596+
TriggerProgramResponse:
597+
properties:
598+
sessionId:
599+
type: string
600+
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."
601+
example: '{ sessionId: ''20241028074744-54132-1730101664824'' }'
602+
required:
603+
- sessionId
604+
type: object
605+
additionalProperties: false
606+
TriggerProgramPayload:
607+
properties:
608+
_program:
609+
type: string
610+
description: 'Location of SAS program'
611+
example: /Public/somefolder/some.file
612+
expiresAfterMins:
613+
type: number
614+
format: double
615+
description: "Amount of minutes after the completion of the program when the session must be\ndestroyed."
616+
example: 15
617+
required:
618+
- _program
619+
type: object
620+
additionalProperties: false
596621
LoginPayload:
597622
properties:
598623
username:
@@ -1901,6 +1926,38 @@ paths:
19011926
application/json:
19021927
schema:
19031928
$ref: '#/components/schemas/ExecutePostRequestPayload'
1929+
/SASjsApi/stp/trigger:
1930+
post:
1931+
operationId: TriggerProgram
1932+
responses:
1933+
'200':
1934+
description: Ok
1935+
content:
1936+
application/json:
1937+
schema:
1938+
$ref: '#/components/schemas/TriggerProgramResponse'
1939+
description: 'Trigger Program on the Specified Runtime'
1940+
summary: 'Triggers program and returns SessionId immediately - does not wait for program completion'
1941+
tags:
1942+
- STP
1943+
security:
1944+
-
1945+
bearerAuth: []
1946+
parameters:
1947+
-
1948+
description: 'Location of code in SASjs Drive'
1949+
in: query
1950+
name: _program
1951+
required: false
1952+
schema:
1953+
type: string
1954+
example: /Projects/myApp/some/program
1955+
requestBody:
1956+
required: true
1957+
content:
1958+
application/json:
1959+
schema:
1960+
$ref: '#/components/schemas/TriggerProgramPayload'
19041961
/:
19051962
get:
19061963
operationId: Home

api/src/controllers/code.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ const executeCode = async (
120120
const triggerCode = async (
121121
req: express.Request,
122122
{ code, runTime, expiresAfterMins }: TriggerCodePayload
123-
): Promise<{ sessionId: string }> => {
123+
): Promise<TriggerCodeResponse> => {
124124
const { user } = req
125125
const userAutoExec =
126126
process.env.MODE === ModeType.Server

api/src/controllers/stp.ts

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import express from 'express'
22
import { Request, Security, Route, Tags, Post, Body, Get, Query } from 'tsoa'
3-
import { ExecutionController, ExecutionVars } from './internal'
3+
import {
4+
ExecutionController,
5+
ExecutionVars,
6+
getSessionController
7+
} from './internal'
48
import {
59
getPreProgramVariables,
610
makeFilesNamesMap,
711
getRunTimeAndFilePath
812
} from '../utils'
913
import { MulterFile } from '../types/Upload'
10-
import { debug } from 'console'
1114

1215
interface ExecutePostRequestPayload {
1316
/**
@@ -17,6 +20,30 @@ interface ExecutePostRequestPayload {
1720
_program?: string
1821
}
1922

23+
interface TriggerProgramPayload {
24+
/**
25+
* Location of SAS program
26+
* @example "/Public/somefolder/some.file"
27+
*/
28+
_program: string
29+
/**
30+
* Amount of minutes after the completion of the program when the session must be
31+
* destroyed.
32+
* @example 15
33+
*/
34+
expiresAfterMins?: number
35+
}
36+
37+
interface TriggerProgramResponse {
38+
/**
39+
* The SessionId is the name of the temporary folder used to store the outputs.
40+
* For SAS, this would be the SASWORK folder. Can be used to poll program status.
41+
* This session ID should be used to poll program status.
42+
* @example "{ sessionId: '20241028074744-54132-1730101664824' }"
43+
*/
44+
sessionId: string
45+
}
46+
2047
@Security('bearerAuth')
2148
@Route('SASjsApi/stp')
2249
@Tags('STP')
@@ -79,6 +106,22 @@ export class STPController {
79106

80107
return execute(request, program!, vars, otherArgs)
81108
}
109+
110+
/**
111+
* Trigger Program on the Specified Runtime
112+
* @summary Triggers program and returns SessionId immediately - does not wait for program completion
113+
* @param _program Location of code in SASjs Drive
114+
* @example _program "/Projects/myApp/some/program"
115+
* @param expiresAfterMins Amount of minutes after the completion of the program when the session must be destroyed
116+
* @example expiresAfterMins 15
117+
*/
118+
@Post('/trigger')
119+
public async triggerProgram(
120+
@Request() request: express.Request,
121+
@Body() body: TriggerProgramPayload
122+
): Promise<TriggerProgramResponse> {
123+
return triggerProgram(request, body)
124+
}
82125
}
83126

84127
const execute = async (
@@ -117,3 +160,49 @@ const execute = async (
117160
}
118161
}
119162
}
163+
164+
const triggerProgram = async (
165+
req: express.Request,
166+
{ _program, expiresAfterMins }: TriggerProgramPayload
167+
): Promise<TriggerProgramResponse> => {
168+
try {
169+
const vars = { ...req.body }
170+
const filesNamesMap = req.files?.length
171+
? makeFilesNamesMap(req.files as MulterFile[])
172+
: null
173+
const otherArgs = { filesNamesMap: filesNamesMap }
174+
const { codePath, runTime } = await getRunTimeAndFilePath(_program)
175+
176+
// get session controller based on runTime
177+
const sessionController = getSessionController(runTime)
178+
179+
// get session
180+
const session = await sessionController.getSession()
181+
182+
// add expiresAfterMins to session if provided
183+
if (expiresAfterMins) {
184+
// expiresAfterMins.used is set initially to false
185+
session.expiresAfterMins = { mins: expiresAfterMins, used: false }
186+
}
187+
188+
// call executeFile method of ExecutionController without awaiting
189+
new ExecutionController().executeFile({
190+
programPath: codePath,
191+
runTime,
192+
preProgramVariables: getPreProgramVariables(req),
193+
vars,
194+
otherArgs,
195+
session
196+
})
197+
198+
// return session id
199+
return { sessionId: session.id }
200+
} catch (err: any) {
201+
throw {
202+
code: 400,
203+
status: 'failure',
204+
message: 'Job execution failed.',
205+
error: typeof err === 'object' ? err.toString() : err
206+
}
207+
}
208+
}

api/src/routes/api/stp.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import express from 'express'
2-
import { executeProgramRawValidation } from '../../utils'
2+
import {
3+
executeProgramRawValidation,
4+
triggerProgramValidation
5+
} from '../../utils'
36
import { STPController } from '../../controllers/'
47
import { FileUploadController } from '../../controllers/internal'
58

@@ -69,4 +72,23 @@ stpRouter.post(
6972
}
7073
)
7174

75+
stpRouter.post('/trigger', async (req, res) => {
76+
const { error, value: body } = triggerProgramValidation(req.body)
77+
78+
if (error) return res.status(400).send(error.details[0].message)
79+
80+
try {
81+
const response = await controller.triggerProgram(req, body)
82+
83+
res.status(200)
84+
res.send(response)
85+
} catch (err: any) {
86+
const statusCode = err.code
87+
88+
delete err.code
89+
90+
res.status(statusCode).send(err)
91+
}
92+
})
93+
7294
export default stpRouter

api/src/utils/validation.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,3 +192,12 @@ export const executeProgramRawValidation = (data: any): Joi.ValidationResult =>
192192
})
193193
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
194194
.validate(data)
195+
196+
export const triggerProgramValidation = (data: any): Joi.ValidationResult =>
197+
Joi.object({
198+
_program: Joi.string().required(),
199+
_debug: Joi.number(),
200+
expiresAfterMins: Joi.number().greater(0)
201+
})
202+
.pattern(/^/, Joi.alternatives(Joi.string(), Joi.number()))
203+
.validate(data)

0 commit comments

Comments
 (0)