diff --git a/all.sas b/all.sas index 3faa7ca4..ac2e2f53 100644 --- a/all.sas +++ b/all.sas @@ -23114,6 +23114,236 @@ run; %mend ms_testservice; /** + @file + @brief Triggers a SASjs Server STP using the /SASjsApi/stp/trigger endpoint + @details Triggers the STP and returns the sessionId + + Example: + + %ms_triggerstp(/some/stored/program + ,debug=131 + ,outds=work.myresults + ) + + @param [in] pgm The full path to the Stored Program in SASjs Drive (_program + parameter) + @param [in] debug= (131) The value to supply to the _debug URL parameter + @param [in] mdebug= (0) Set to 1 to enable DEBUG messages + @param [in] inputparams=(_null_) A dataset containing name/value pairs in the + following format: + |name:$32|value:$10000| + |---|---| + |stpmacname|some value| + |mustbevalidname|can be anything, oops, %abort!!| + @param [in] inputfiles= (_null_) A dataset containing fileref/name/filename in + the following format: + |fileref:$8|name:$32|filename:$256| + |---|---|--| + |someref|some_name|some_filename.xls| + |fref2|another_file|zyx_v2.csv| + @param [in] expiresaftermins= (15) The number of minutes to retain the session + folder after the session ends. + + @param [out] outds= (work.ms_triggerstp) Set to the name of a dataset to + contain the sessionId. If this dataset already exists, and contains the + sessionId, it will be appended to. + Format: + |sessionId:$36| + |---| + |20241028074744-54132-1730101664824| + +

SAS Macros

+ @li mf_getuniquefileref.sas + @li mf_getuniquelibref.sas + @li mf_getuniquename.sas + @li mp_abort.sas + @li mp_dropmembers.sas + @li mf_nobs.sas + +**/ + +%macro ms_triggerstp(pgm + ,debug=131 + ,inputparams=_null_ + ,inputfiles=_null_ + ,expiresAfterMins=15 + ,outds=work.ms_triggerstp + ,mdebug=0 + ); + %local dbg mainref authref boundary libref triggered_sid; + %let mainref=%mf_getuniquefileref(); + %let authref=%mf_getuniquefileref(); + %let boundary=%mf_getuniquename(); + %if &inputparams=0 %then %let inputparams=_null_; + + %if &mdebug=1 %then %do; + %put &sysmacroname entry vars:; + %put _local_; + %end; + %else %let dbg=*; + + + %mp_abort(iftrue=("&pgm"="") + ,mac=&sysmacroname + ,msg=%str(Program not provided) + ) + %mp_abort(iftrue=("&outds"="") + ,mac=&sysmacroname + ,msg=%str(Output dataset not provided) + ) + + /* avoid sending bom marker to API */ + %local optval; + %let optval=%sysfunc(getoption(bomfile)); + options nobomfile; + + /* Add params to the content */ + data _null_; + file &mainref termstr=crlf lrecl=32767 mod; + length line $1000 name $32 value $32767; + if _n_=1 then call missing(of _all_); + set &inputparams; + put "--&boundary"; + line=cats('Content-Disposition: form-data; name="',name,'"'); + put line; + put ; + put value; + run; + + /* parse input file list */ + %local webcount; + %let webcount=0; + data _null_; + set &inputfiles end=last; + length fileref $8 name $32 filename $256; + call symputx(cats('webref',_n_),fileref,'l'); + call symputx(cats('webname',_n_),name,'l'); + call symputx(cats('webfilename',_n_),filename,'l'); + if last then do; + call symputx('webcount',_n_); + call missing(of _all_); + end; + run; + + /* write out the input files to the content */ + %local i; + %do i=1 %to &webcount; + data _null_; + file &mainref termstr=crlf lrecl=32767 mod; + infile &&webref&i lrecl=32767; + if _n_ = 1 then do; + length line $32767; + line=cats( + 'Content-Disposition: form-data; name="' + ,"&&webname&i" + ,'"; filename="' + ,"&&webfilename&i" + ,'"' + ); + put "--&boundary"; + put line; + put "Content-Type: text/plain"; + put ; + end; + input; + put _infile_; /* add the actual file to be sent */ + run; + %end; + + /* Add footer to the content */ + data _null_; + file &mainref termstr=crlf mod; + put / "--&boundary--"; + run; + + data _null_; + file &authref lrecl=1000; + infile "&_sasjs_tokenfile" lrecl=1000; + input; + if _n_=1 then put "Content-Type: multipart/form-data; boundary=&boundary"; + put _infile_; + run; + + %if &mdebug=1 %then %do; + data _null_; + if _n_ eq 1 then putlog "NOTE: ***** authref=&authref content *****"; + infile &authref; + input; + put _infile_; + data _null_; + if _n_ eq 1 then putlog "NOTE: ***** mainref=&mainref content *****"; + infile &mainref; + input; + put _infile_; + run; + %end; + + %local resp_path outref; + %let resp_path=%sysfunc(pathname(work))/%mf_getuniquename(); + %let outref=%mf_getuniquefileref(); + filename &outref "&resp_path" lrecl=32767; + + /* prepare request*/ + proc http method='POST' headerin=&authref in=&mainref out=&outref + url="&_sasjs_apiserverurl/SASjsApi/stp/trigger?%trim( + )_program=&pgm%str(&)_debug=131%str(&)expiresAfterMins=&expiresaftermins"; + %if &mdebug=1 %then %do; + debug level=2; + %end; + run; + + %if (&SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201) + or &mdebug=1 + %then %do; + data _null_; + if _n_ eq 1 then putlog "NOTE: ***** outref=&outref content *****"; + infile &outref; + input; + putlog _infile_; + run; + %end; + %mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200 + and &SYS_PROCHTTP_STATUS_CODE ne 201) + ,mac=&sysmacroname + ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) + ) + + /* reset options */ + options &optval; + + %let libref=%mf_getuniquelibref(); + libname &libref JSON fileref=&outref; + %let triggered_sid=%mf_getuniquename(prefix=triggered_sid_); + + data work.&triggered_sid (keep=sessionid); + set &libref..root; + + %if &mdebug=1 %then %do; + putlog (_all_)(=); + %end; + run; + + %if %mf_nobs(work.&triggered_sid)>0 %then %do; + proc append base=&outds data=work.&triggered_sid; + run; + %end; + + %if &mdebug=1 %then %do; + %put &sysmacroname exit vars:; + %put _local_; + %end; + %else %do; + /* clear refs */ + filename &authref; + filename &mainref; + filename &outref; + libname &libref clear; + /* and remove temp dataset */ + %mp_dropmembers(&triggered_sid,libref=work); + %end; + +%mend ms_triggerstp;/** @file @brief Send data to/from sasjs/server @details This macro should be added to the start of each web service, diff --git a/server/ms_triggerstp.sas b/server/ms_triggerstp.sas new file mode 100644 index 00000000..293a2ded --- /dev/null +++ b/server/ms_triggerstp.sas @@ -0,0 +1,231 @@ +/** + @file + @brief Triggers a SASjs Server STP using the /SASjsApi/stp/trigger endpoint + @details Triggers the STP and returns the sessionId + + Example: + + %ms_triggerstp(/some/stored/program + ,debug=131 + ,outds=work.myresults + ) + + @param [in] pgm The full path to the Stored Program in SASjs Drive (_program + parameter) + @param [in] debug= (131) The value to supply to the _debug URL parameter + @param [in] mdebug= (0) Set to 1 to enable DEBUG messages + @param [in] inputparams=(_null_) A dataset containing name/value pairs in the + following format: + |name:$32|value:$10000| + |---|---| + |stpmacname|some value| + |mustbevalidname|can be anything, oops, %abort!!| + @param [in] inputfiles= (_null_) A dataset containing fileref/name/filename in + the following format: + |fileref:$8|name:$32|filename:$256| + |---|---|--| + |someref|some_name|some_filename.xls| + |fref2|another_file|zyx_v2.csv| + @param [in] expiresaftermins= (15) The number of minutes to retain the session + folder after the session ends. + + @param [out] outds= (work.ms_triggerstp) Set to the name of a dataset to + contain the sessionId. If this dataset already exists, and contains the + sessionId, it will be appended to. + Format: + |sessionId:$36| + |---| + |20241028074744-54132-1730101664824| + +

SAS Macros

+ @li mf_getuniquefileref.sas + @li mf_getuniquelibref.sas + @li mf_getuniquename.sas + @li mp_abort.sas + @li mp_dropmembers.sas + @li mf_nobs.sas + +**/ + +%macro ms_triggerstp(pgm + ,debug=131 + ,inputparams=_null_ + ,inputfiles=_null_ + ,expiresAfterMins=15 + ,outds=work.ms_triggerstp + ,mdebug=0 + ); + %local dbg mainref authref boundary libref triggered_sid; + %let mainref=%mf_getuniquefileref(); + %let authref=%mf_getuniquefileref(); + %let boundary=%mf_getuniquename(); + %if &inputparams=0 %then %let inputparams=_null_; + + %if &mdebug=1 %then %do; + %put &sysmacroname entry vars:; + %put _local_; + %end; + %else %let dbg=*; + + + %mp_abort(iftrue=("&pgm"="") + ,mac=&sysmacroname + ,msg=%str(Program not provided) + ) + %mp_abort(iftrue=("&outds"="") + ,mac=&sysmacroname + ,msg=%str(Output dataset not provided) + ) + + /* avoid sending bom marker to API */ + %local optval; + %let optval=%sysfunc(getoption(bomfile)); + options nobomfile; + + /* Add params to the content */ + data _null_; + file &mainref termstr=crlf lrecl=32767 mod; + length line $1000 name $32 value $32767; + if _n_=1 then call missing(of _all_); + set &inputparams; + put "--&boundary"; + line=cats('Content-Disposition: form-data; name="',name,'"'); + put line; + put ; + put value; + run; + + /* parse input file list */ + %local webcount; + %let webcount=0; + data _null_; + set &inputfiles end=last; + length fileref $8 name $32 filename $256; + call symputx(cats('webref',_n_),fileref,'l'); + call symputx(cats('webname',_n_),name,'l'); + call symputx(cats('webfilename',_n_),filename,'l'); + if last then do; + call symputx('webcount',_n_); + call missing(of _all_); + end; + run; + + /* write out the input files to the content */ + %local i; + %do i=1 %to &webcount; + data _null_; + file &mainref termstr=crlf lrecl=32767 mod; + infile &&webref&i lrecl=32767; + if _n_ = 1 then do; + length line $32767; + line=cats( + 'Content-Disposition: form-data; name="' + ,"&&webname&i" + ,'"; filename="' + ,"&&webfilename&i" + ,'"' + ); + put "--&boundary"; + put line; + put "Content-Type: text/plain"; + put ; + end; + input; + put _infile_; /* add the actual file to be sent */ + run; + %end; + + /* Add footer to the content */ + data _null_; + file &mainref termstr=crlf mod; + put / "--&boundary--"; + run; + + data _null_; + file &authref lrecl=1000; + infile "&_sasjs_tokenfile" lrecl=1000; + input; + if _n_=1 then put "Content-Type: multipart/form-data; boundary=&boundary"; + put _infile_; + run; + + %if &mdebug=1 %then %do; + data _null_; + if _n_ eq 1 then putlog "NOTE: ***** authref=&authref content *****"; + infile &authref; + input; + put _infile_; + data _null_; + if _n_ eq 1 then putlog "NOTE: ***** mainref=&mainref content *****"; + infile &mainref; + input; + put _infile_; + run; + %end; + + %local resp_path outref; + %let resp_path=%sysfunc(pathname(work))/%mf_getuniquename(); + %let outref=%mf_getuniquefileref(); + filename &outref "&resp_path" lrecl=32767; + + /* prepare request*/ + proc http method='POST' headerin=&authref in=&mainref out=&outref + url="&_sasjs_apiserverurl/SASjsApi/stp/trigger?%trim( + )_program=&pgm%str(&)_debug=131%str(&)expiresAfterMins=&expiresaftermins"; + %if &mdebug=1 %then %do; + debug level=2; + %end; + run; + + %if (&SYS_PROCHTTP_STATUS_CODE ne 200 and &SYS_PROCHTTP_STATUS_CODE ne 201) + or &mdebug=1 + %then %do; + data _null_; + if _n_ eq 1 then putlog "NOTE: ***** outref=&outref content *****"; + infile &outref; + input; + putlog _infile_; + run; + %end; + %mp_abort( + iftrue=(&SYS_PROCHTTP_STATUS_CODE ne 200 + and &SYS_PROCHTTP_STATUS_CODE ne 201) + ,mac=&sysmacroname + ,msg=%str(&SYS_PROCHTTP_STATUS_CODE &SYS_PROCHTTP_STATUS_PHRASE) + ) + + /* reset options */ + options &optval; + + %let libref=%mf_getuniquelibref(); + libname &libref JSON fileref=&outref; + %let triggered_sid=%mf_getuniquename(prefix=triggered_sid_); + + data work.&triggered_sid (keep=sessionid); + set &libref..root; + + %if &mdebug=1 %then %do; + putlog (_all_)(=); + %end; + run; + + %if %mf_nobs(work.&triggered_sid)>0 %then %do; + proc append base=&outds data=work.&triggered_sid; + run; + %end; + + %if &mdebug=1 %then %do; + %put &sysmacroname exit vars:; + %put _local_; + %end; + %else %do; + /* clear refs */ + filename &authref; + filename &mainref; + filename &outref; + libname &libref clear; + /* and remove temp dataset */ + %mp_dropmembers(&triggered_sid,libref=work); + %end; + +%mend ms_triggerstp; \ No newline at end of file diff --git a/tests/serveronly/ms_triggerstp.test.sas b/tests/serveronly/ms_triggerstp.test.sas new file mode 100644 index 00000000..a87cd62d --- /dev/null +++ b/tests/serveronly/ms_triggerstp.test.sas @@ -0,0 +1,90 @@ +/** + @file + @brief Testing ms_triggerstp.sas macro + +

SAS Macros

+ @li mf_getuniquename.sas + @li mp_assert.sas + @li mp_assertscope.sas + @li ms_createfile.sas + @li ms_triggerstp.sas + @li mf_existds.sas + @li mp_assertdsobs.sas + @li mp_assertcols.sas + @li mf_getvartype.sas + @li ms_deletefile.sas + +**/ + +/* first, create multiple STPs to run */ +filename stpcode1 temp; +data _null_; + file stpcode1; + put '%put hello world;'; + put '%put _all_;'; + put 'data _null_; file _webout1; put "triggerstp test 1";run;'; +run; +filename stpcode2 temp; +data _null_; + file stpcode2; + put '%put Lorem Ipsum;'; + put '%put _all_;'; + put 'data _null_; file _webout2; put "triggerstp test 2";run;'; +run; +options mprint; +%let fname1=%mf_getuniquename(); +%let fname2=%mf_getuniquename(); + +%ms_createfile(/sasjs/tests/&fname1..sas + ,inref=stpcode1 + ,mdebug=1 +) +%ms_createfile(/sasjs/tests/&fname2..sas + ,inref=stpcode2 +) + +%mp_assertscope(SNAPSHOT) + %ms_triggerstp(/sasjs/tests/&fname1 + ,debug=131 + ,outds=work.mySessions + ) + %ms_triggerstp(/sasjs/tests/&fname2 + ,outds=work.mySessions + ) +%mp_assertscope(COMPARE + ,ignorelist=MCLIB0_JADP1LEN MCLIB0_JADPNUM MCLIB0_JADVLEN) + +%mp_assert(iftrue=%str(%mf_existds(work.mySessions)=1) + ,desc=Testing output exists + ,outds=work.test_results +) + +%mp_assertdsobs(work.mySessions, + test=EQUALS 2, + desc=Testing observations, + outds=work.test_results +) +%mp_assertcols(work.mySessions, + cols=sessionid, + test=ALL, + desc=Testing column exists, + outds=work.test_results +) + +data _null_; + retain contentCheck 1; + set work.mySessions end=last; + if missing(sessionID) then contentCheck = 0; + if last then do; + call symputx("contentCheck",contentCheck,"l"); + end; +run; +%let typeCheck = %mf_getvartype(work.mySessions,sessionid); + +%mp_assert(iftrue=%str(&typeCheck = C and &contentCheck = 1) + ,desc=Testing type and content of output + ,outds=work.test_results +) + +%ms_deletefile(/sasjs/tests/&fname1..sas) +%ms_deletefile(/sasjs/tests/&fname2..sas) \ No newline at end of file