*! suso v1.6.4 18jun2026 (fix: flattened scalar names capped to Stata 32-char macro limit (was overflowing on e.g. ExistingQuestionnairesCount); workspace status count restored) *! Author: Attique Ur Rehman, Economist, The World Bank (DEC, Enterprise Surveys) *! attique@worldbank.org · https://sites.google.com/view/attique-ur-rehman *! The World Bank — Development Economics (DEC) · Enterprise Surveys *! Requires: a Java 11+ runtime (check with: suso doctor) and suso.jar on the adopath. *------------------------------------------------------------------------------- * suso — a thin, safe Stata front-end over the Survey Solutions REST API. * * The heavy lifting (HTTP, JSON, loading results into the dataset) is done by * suso.jar via -javacall-. This .ado parses syntax, builds requests, enforces * safety checks around destructive operations, writes an audit log, paginates, * and returns results in r(). * * See: help suso *------------------------------------------------------------------------------- * ----- Mata helpers (URL-encoding + JSON string escaping), UTF-8 byte-correct ---- capture mata: mata drop suso_urlencode() capture mata: mata drop suso_jsonesc() version 14.2 mata: mata set matastrict off string scalar suso_urlencode(string scalar s) { real scalar i, n, c string scalar out, ch, hex hex = "0123456789ABCDEF" out = "" n = strlen(s) // byte length for (i=1; i<=n; i++) { ch = substr(s, i, 1) // one byte if (regexm(ch, "[A-Za-z0-9._~-]")) out = out + ch else { c = ascii(ch) out = out + "%" + substr(hex, floor(c/16)+1, 1) + substr(hex, mod(c, 16)+1, 1) } } return(out) } string scalar suso_jsonesc(string scalar s) { s = subinstr(s, "\", "\\") s = subinstr(s, char(34), "\" + char(34)) s = subinstr(s, char(13), "\r") s = subinstr(s, char(10), "\n") s = subinstr(s, char(9), "\t") return(s) } end *=============================================================================== * Router *=============================================================================== program suso, rclass version 14.2 gettoken noun 0 : 0, parse(" ,") local noun = strlower(`"`noun'"') if "`noun'"=="" { di as txt _n "{bf:suso} — talk to Survey Solutions from Stata." di as txt " 1. {bf:suso config , server() workspace() user() password()}" di as txt " 2. {bf:suso ping} {txt}(check it works)" di as txt " 3. {bf:suso examples} {txt}(copy/paste recipes)" di as txt _n "Type {stata suso examples:suso examples} for ready-to-run commands, " /// "{stata suso endpoints:suso endpoints} for the full list, or {help suso} for help." _n exit } if inlist("`noun'","help","?") { capture help suso if _rc di as txt "suso — install suso.sthlp, then: {bf:help suso} (or {bf:suso examples})" exit } if inlist("`noun'","examples","example","recipes","cheatsheet","cheat") { _suso_examples exit } if inlist("`noun'","endpoints","endpoint","commands","menu","list") { _suso_endpoints exit } * single-word commands if "`noun'"=="login" { _suso_prompt exit } if "`noun'"=="backup" { _suso_backup `macval(0)' return add exit } if inlist("`noun'","config","doctor","ping","raw","version","about") { if "`noun'"=="version" | "`noun'"=="about" { _suso_about exit } _suso_`noun' `macval(0)' return add exit } * normalise plural nouns if "`noun'"=="assignments" local noun assignment if "`noun'"=="interviews" local noun interview if "`noun'"=="questionnaires" local noun questionnaire if "`noun'"=="exports" local noun export if "`noun'"=="users" local noun user if "`noun'"=="supervisors" local noun supervisor if "`noun'"=="interviewers" local noun interviewer if "`noun'"=="workspaces" local noun workspace if "`noun'"=="setting" local noun settings if "`noun'"=="statistic" | "`noun'"=="stats" local noun statistics if "`noun'"=="map" local noun maps if !inlist("`noun'","assignment","interview","questionnaire","export","user","maps") /// & !inlist("`noun'","supervisor","interviewer","workspace","settings","statistics") { di as err "suso: unknown subcommand '`noun''. See {help suso}." exit 198 } _suso_`noun' `macval(0)' return add end *=============================================================================== * Configuration *=============================================================================== program _suso_config, rclass version 14.2 syntax [, SERVER(string) Workspace(string) User(string) Password(string) /// TOKEN(string) AUTH(string) JAR(string) PROXYHost(string) /// PROXYPort(integer 0) PROXYUser(string) PROXYPass(string) /// INSECURE NOINSECURE CONNTimeout(integer 0) READTimeout(integer 0) /// MAXrows(integer 0) AUDITfile(string) GUID(string) QVER(integer 0) /// SHOW CLEAR ] if "`clear'"!="" { capture macro drop SUSO_BASE SUSO_WS SUSO_USER SUSO_PWD SUSO_TOKEN /// SUSO_AUTHTYPE SUSO_PROXYHOST SUSO_PROXYPORT SUSO_PROXYUSER SUSO_PROXYPWD /// SUSO_INSECURE SUSO_CONNTO SUSO_READTO SUSO_MAXROWS SUSO_AUDIT /// SUSO_GUID SUSO_QVER di as txt "suso: configuration cleared for this session." exit } if "`server'"!="" { local server = trim("`server'") if substr("`server'", -1, 1)=="/" local server = substr("`server'", 1, length("`server'")-1) global SUSO_BASE "`server'" } if "`workspace'"!="" global SUSO_WS "`workspace'" if "`user'"!="" global SUSO_USER "`user'" if "`password'"!="" global SUSO_PWD "`password'" if "`token'"!="" global SUSO_TOKEN "`token'" if "`auth'"!="" global SUSO_AUTHTYPE = strlower("`auth'") if "`jar'"!="" global SUSO_JAR "`jar'" if "`proxyhost'"!="" global SUSO_PROXYHOST "`proxyhost'" if `proxyport'>0 global SUSO_PROXYPORT "`proxyport'" if "`proxyuser'"!="" global SUSO_PROXYUSER "`proxyuser'" if "`proxypass'"!="" global SUSO_PROXYPWD "`proxypass'" if "`insecure'"!="" global SUSO_INSECURE "1" if "`noinsecure'"!="" global SUSO_INSECURE "0" if `conntimeout'>0 global SUSO_CONNTO = `conntimeout'*1000 if `readtimeout'>0 global SUSO_READTO = `readtimeout'*1000 if `maxrows'>0 global SUSO_MAXROWS "`maxrows'" if "`auditfile'"!="" global SUSO_AUDIT "`auditfile'" if "`guid'"!="" global SUSO_GUID "`guid'" if `qver'>0 global SUSO_QVER "`qver'" _suso_init if "`insecure'"!="" { di as err "suso: WARNING — TLS certificate/hostname verification is DISABLED for this session." di as err " Use this only as a last resort behind the corporate proxy. Prefer importing" di as err " the WBG root CA into your Stata JVM trust store (see the README)." } if "`show'"!="" | trim("`server'`workspace'`user'`password'`token'`auth'`jar'`proxyhost'")=="" { _suso_showconfig } end program _suso_showconfig di as txt _n "{hline 62}" di as txt "suso configuration (this Stata session)" di as txt "{hline 62}" di as txt " server : " as res cond("$SUSO_BASE"=="","(not set)","$SUSO_BASE") di as txt " workspace : " as res cond("$SUSO_WS"=="","(not set)","$SUSO_WS") if "$SUSO_GUID"!="" { di as txt " questionnaire: " as res "$SUSO_GUID" /// cond("$SUSO_QVER"!=""," (v$SUSO_QVER)"," (any version)") } di as txt " auth : " as res cond("$SUSO_AUTHTYPE"=="","basic","$SUSO_AUTHTYPE") di as txt " user : " as res cond("$SUSO_USER"=="","(not set)","$SUSO_USER") di as txt " password : " as res cond("$SUSO_PWD"=="","(not set)","********") if "$SUSO_TOKEN"!="" di as txt " bearer token: " as res "********" di as txt " jar : " as res cond("$SUSO_JAR"=="","(auto-locate on adopath)","$SUSO_JAR") if "$SUSO_PROXYHOST"!="" di as txt " proxy : " as res "$SUSO_PROXYHOST:$SUSO_PROXYPORT" di as txt " TLS verify : " as res cond("$SUSO_INSECURE"=="1","DISABLED (insecure)","on") di as txt " timeouts ms : " as res "connect=$SUSO_CONNTO read=$SUSO_READTO" di as txt " max rows : " as res "$SUSO_MAXROWS" local af "$SUSO_AUDIT" if "`af'"=="" local af "`c(sysdir_personal)'suso_audit.log" di as txt " audit log : " as res `"`af'"' di as txt "{hline 62}" end program _suso_about di as txt _n "{hline 66}" di as txt " suso v1.6.4 — Survey Solutions REST API client for Stata" di as txt "{hline 66}" di as txt " Author : Attique Ur Rehman, Economist, The World Bank" di as txt " Development Economics (DEC) · Enterprise Surveys" di as txt " Email : attique@worldbank.org" di as txt " Web : https://sites.google.com/view/attique-ur-rehman" di as txt "{hline 66}" di as txt " Java backend : suso.jar (requires a Java 11+ runtime)" di as txt " Help : {help suso} Diagnostics: {stata suso doctor:suso doctor}" di as txt "{hline 66}" end *=============================================================================== * Diagnostics *=============================================================================== program _suso_doctor version 14.2 di as txt _n "{hline 62}" di as txt "suso doctor — environment check" di as txt "{hline 62}" di as txt "Stata" di as txt " version : " as res "`c(flavor)' `c(stata_version)'" di as txt " sysdir PLUS : " as res "`c(sysdir_plus)'" di as txt " sysdir PERSON : " as res "`c(sysdir_personal)'" di as txt "Java backend" capture _suso_jar if _rc { di as err " suso.jar : NOT FOUND — put it on the adopath or set -suso config , jar(...)-" } else { di as txt " suso.jar : " as res "$SUSO_JAR" capture noisily javacall org.worldbank.suso.Stata jvm , classpath("$SUSO_JAR") if _rc { di as err " javacall : FAILED (rc=`=_rc') — is Java available to Stata? See {help java}." } else if "$SUSO_JAVAOK"=="1" { di as txt " Java 11+ : " as res "yes ($SUSO_JAVAVER)" } else { di as err " Java 11+ : NO ($SUSO_JAVAVER) — PATCH operations require Java 11 or newer." } } _suso_showconfig capture macro drop SUSO_JAVAVER SUSO_JAVAOK end program _suso_ping, rclass version 14.2 syntax [, VERBOSE] _suso_call , method(GET) path(/api/v2/export) query(limit=1) `verbose' di as txt "suso: connection OK (HTTP " as res "`r(http)'" as txt ") to $SUSO_BASE/$SUSO_WS" return add end *=============================================================================== * Core helpers *=============================================================================== program _suso_init if "$SUSO_AUTHTYPE"=="" global SUSO_AUTHTYPE "basic" if "$SUSO_CONNTO"=="" global SUSO_CONNTO "30000" if "$SUSO_READTO"=="" global SUSO_READTO "300000" if "$SUSO_MAXROWS"=="" global SUSO_MAXROWS "100000" if "$SUSO_PWD"=="" & "$SUSO_TOKEN"=="" { local e : environment SUSO_PASSWORD if "`e'"!="" global SUSO_PWD "`e'" } * Ask for the API user/password if they were never supplied (basic auth only). if "$SUSO_AUTHTYPE"=="basic" & "$SUSO_TOKEN"=="" & ("$SUSO_USER"=="" | "$SUSO_PWD"=="") { _suso_prompt , user("$SUSO_USER") } end program _suso_prompt, rclass syntax [ , USER(string) ] _suso_jar mata: st_global("SUSO_PROMPT_USER", st_local("user")) capture noisily javacall org.worldbank.suso.Stata prompt , classpath("$SUSO_JAR") local jrc = _rc capture macro drop SUSO_PROMPT_USER if `jrc' { di as err "suso: credential prompt could not run (rc=`jrc')." di as err " Set them directly: suso config , user() password()" exit `jrc' } if "$SUSO_RC"!="0" { local m "$SUSO_MSG" if "`m'"=="" local m "credential prompt cancelled" capture macro drop SUSO_RC SUSO_MSG di as err "suso: `m'" exit 198 } capture macro drop SUSO_RC SUSO_MSG di as txt "suso: signed in as " as res "$SUSO_USER" as txt "." end program _suso_unzip, rclass syntax , FILE(string) [ DIR(string) PWD(string) ] _suso_jar * default destination: a folder named after the archive, beside it if `"`dir'"' == "" { local k = strrpos(`"`file'"', ".") if `k' > 0 local dir = substr(`"`file'"', 1, `k'-1) else local dir `"`file'"' } mata: st_global("SUSO_ZIP_FILE", st_local("file")) mata: st_global("SUSO_ZIP_DIR", st_local("dir")) mata: st_global("SUSO_ZIP_PWD", st_local("pwd")) capture noisily javacall org.worldbank.suso.Stata unzip , classpath("$SUSO_JAR") local jrc = _rc capture macro drop SUSO_ZIP_FILE SUSO_ZIP_DIR SUSO_ZIP_PWD if `jrc' { di as err "suso: unzip bridge failed (rc=`jrc')." exit `jrc' } local rc = real("$SUSO_RC") if `rc'!=0 & !missing(`rc') { local m "$SUSO_MSG" if "`m'"=="" local m "unzip failed" capture macro drop SUSO_RC SUSO_MSG SUSO_UNZIP_N SUSO_UNZIP_DIR di as err "suso: `m'" exit `rc' } if "$SUSO_MSG"!="" di as txt "suso: $SUSO_MSG" di as txt "suso: extracted " as res "$SUSO_UNZIP_N" as txt " file(s) to " as res `"$SUSO_UNZIP_DIR"' return local unzipdir `"$SUSO_UNZIP_DIR"' return scalar nfiles = real("$SUSO_UNZIP_N") capture macro drop SUSO_RC SUSO_MSG SUSO_UNZIP_N SUSO_UNZIP_DIR end program _suso_gql, rclass syntax [ , TODATA NODEpath(string) VERBOSE ] _suso_init _suso_jar if "$SUSO_BASE"=="" { di as err "suso: no server configured. suso config , server() workspace()" exit 198 } * Body / operations / file / name are passed by the caller as SUSO_GQL_* globals * (set via mata to avoid macro-expansion of JSON braces and quotes). mata: st_global("SUSO_GQL_NODEPATH", st_local("nodepath")) global SUSO_GQL_TODATA = cond("`todata'"!="","1","0") global SUSO_VERBOSE = cond("`verbose'"!="","1","0") if "`todata'"!="" clear capture noisily javacall org.worldbank.suso.Stata gql , classpath("$SUSO_JAR") local jrc = _rc local rc "$SUSO_RC" local http "$SUSO_HTTP" local msg `"$SUSO_MSG"' local nobs "$SUSO_NOBS" local nvars "$SUSO_NVARS" local total "$SUSO_TOTALCOUNT" local fkeys "$SUSO_FKEYS" foreach k of local fkeys { if length("SUSO_F_`k'") <= 32 local F_`k' `"${SUSO_F_`k'}"' } capture macro drop SUSO_GQL_BODY SUSO_GQL_OPERATIONS SUSO_GQL_MAP SUSO_UP_FILE /// SUSO_UP_NAME SUSO_GQL_NODEPATH SUSO_GQL_TODATA SUSO_VERBOSE if `jrc' { di as err "suso: the Java call failed (Stata rc=`jrc'). See: suso doctor" exit `jrc' } if "`rc'"=="" { di as err "suso: no response from the Java backend." exit 459 } if "`rc'"!="0" { _suso_transport_err `"`macval(msg)'"' exit 459 } if "`todata'"!="" { if "`nobs'"!="" return scalar nobs = real("`nobs'") if "`nvars'"!="" return scalar nvars = real("`nvars'") if "`total'"!="" return scalar totalcount = real("`total'") } foreach k of local fkeys { return local `k' `"`F_`k''"' } return local http "`http'" capture macro drop SUSO_RC SUSO_HTTP SUSO_MSG SUSO_BODY SUSO_NOBS SUSO_NVARS SUSO_TOTALCOUNT SUSO_FKEYS local gl : all globals foreach g of local gl { if substr("`g'",1,7)=="SUSO_F_" capture macro drop `g' } end program _suso_maps, rclass version 14.2 gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="list" { syntax [ , WORKSPACE(string) PAGESize(integer 100) VERBOSE ] if "`workspace'"=="" local workspace "$SUSO_WS" if `"`workspace'"'=="" { di as err "suso maps: no workspace set. Run: suso config , workspace()" di as err " or add workspace() to this command." exit 198 } _suso_maps_fetch , workspace(`"`workspace'"') pagesize(`pagesize') `verbose' local got = r(nobs) local total = r(totalcount) local extra "" if "`total'"!="" & "`total'"!="." local extra " (of `total' on server)" di as txt "suso: fetched " as res "`got'" as txt " map(s)`extra'." return scalar nobs = `got' if "`total'"!="" & "`total'"!="." return scalar totalcount = `total' exit } if "`verb'"=="upload" { syntax , FILE(string) [ NAME(string) WORKSPACE(string) VERBOSE ] if "`workspace'"=="" local workspace "$SUSO_WS" if `"`workspace'"'=="" { di as err "suso maps: no workspace set. Run: suso config , workspace()" di as err " or add workspace() to this command." exit 198 } _suso_jsonesc `"`workspace'"' local jws `"`r(js)'"' local fn `"`name'"' if `"`fn'"' == "" { local f2 = subinstr(`"`file'"', "\", "/", .) local k = strrpos(`"`f2'"', "/") if `k' > 0 local fn = substr(`"`f2'"', `k'+1, .) else local fn `"`f2'"' } * Survey Solutions uploadMap takes a .zip archive (shapefile family / GeoTIFF / TPK). local ops `"{"query":"mutation(__DOLLAR__file:Upload!,__DOLLAR__workspace:String){uploadMap(file:__DOLLAR__file,workspace:__DOLLAR__workspace){fileName size shapeType wkid importDateUtc}}","variables":{"file":null,"workspace":"`jws'"}}"' mata: st_global("SUSO_GQL_BODY", "") mata: st_global("SUSO_GQL_OPERATIONS", st_local("ops")) mata: st_global("SUSO_UP_FILE", st_local("file")) mata: st_global("SUSO_UP_NAME", st_local("fn")) _suso_gql , `verbose' local h = r(http) di as txt "suso: uploaded " as res `"`fn'"' as txt " to workspace " as res "`workspace'" as txt " (HTTP `h')." return scalar http = `h' exit } if "`verb'"=="delete" { syntax , NAME(string) [ WORKSPACE(string) CONFIRM VERBOSE ] if "`workspace'"=="" local workspace "$SUSO_WS" if `"`workspace'"'=="" { di as err "suso maps: no workspace set. Run: suso config , workspace()" di as err " or add workspace() to this command." exit 198 } _suso_block , action("DELETE map `name' from workspace `workspace' (irreversible)") `confirm' _suso_maps_del1 , workspace(`"`workspace'"') name(`"`name'"') `verbose' local h = r(http) _suso_audit , action("map delete") target("`name'") http("`h'") di as txt "suso: deleted map " as res "`name'" as txt " (HTTP `h')." return scalar http = `h' exit } if "`verb'"=="deleteall" { syntax [ , WORKSPACE(string) Iknowthis(string) SLEEP(integer 200) PAGESize(integer 100) DRYrun VERBOSE ] if "`workspace'"=="" local workspace "$SUSO_WS" if `"`workspace'"'=="" { di as err "suso maps: no workspace set. Run: suso config , workspace()" di as err " or add workspace() to this command." exit 198 } preserve _suso_maps_fetch , workspace(`"`workspace'"') pagesize(`pagesize') `verbose' local N = r(nobs) if `N'==0 { di as txt "suso maps: workspace " as res "`workspace'" as txt " has no maps — nothing to delete." restore exit } * Two-phase safety (mirrors the wipe notebook): a dry run unless the user * confirms by typing the workspace name in iknowthis(). local doit = 0 if "`dryrun'"=="" & `"`iknowthis'"'==`"`workspace'"' local doit = 1 if `doit'==0 { di as txt _n "{hline 64}" di as txt " suso maps deleteall — DRY RUN (nothing deleted)" di as txt "{hline 64}" di as txt " Workspace : " as res "`workspace'" di as txt " Maps : " as res "`N'" as txt " would be permanently deleted." local show = min(`N',8) di as txt " Sample :" forvalues i = 1/`show' { di as txt " " as res `"`=fileName[`i']'"' } if `N' > `show' di as txt " ... and " as res "`=`N'-`show''" as txt " more." di as err _n " This is IRREVERSIBLE. To delete ALL `N' map(s), type the workspace name:" di as err " suso maps deleteall , iknowthis(`workspace')" restore exit } di as txt "suso maps: deleting " as res "`N'" as txt " map(s) from workspace " as res "`workspace'" as txt " ..." local ok = 0 local fail = 0 forvalues i = 1/`N' { local fn = fileName[`i'] capture _suso_maps_del1 , workspace(`"`workspace'"') name(`"`fn'"') if _rc local ++fail else local ++ok if mod(`i',100)==0 di as txt " ... `i'/`N' (" as res "`ok'" as txt " ok, " as res "`fail'" as txt " failed)" if `sleep' > 0 sleep `sleep' } _suso_audit , action("maps deleteall") target("`workspace' (`ok'/`N' deleted)") http("") local fx "" if `fail' > 0 local fx " — `fail' failed (re-run suso maps list to see any stragglers)" di as txt _n "suso maps: deleted " as res "`ok'" as txt " of `N' map(s) from " as res "`workspace'" as txt "`fx'." restore return scalar deleted = `ok' return scalar failed = `fail' return scalar total = `N' exit } if inlist("`verb'","assign","unassign") { syntax , NAME(string) USER(string) [ WORKSPACE(string) VERBOSE ] if "`workspace'"=="" local workspace "$SUSO_WS" if `"`workspace'"'=="" { di as err "suso maps: no workspace set. Run: suso config , workspace()" di as err " or add workspace() to this command." exit 198 } if "`verb'"=="assign" { local mut "addUserToMap" local prep "to" } else { local mut "deleteUserFromMap" local prep "from" } _suso_jsonesc `"`name'"' local jn `"`r(js)'"' _suso_jsonesc `"`user'"' local ju `"`r(js)'"' _suso_jsonesc `"`workspace'"' local jws `"`r(js)'"' local body `"{"query":"mutation(__DOLLAR__fileName:String!,__DOLLAR__userName:String!,__DOLLAR__workspace:String){`mut'(fileName:__DOLLAR__fileName,userName:__DOLLAR__userName,workspace:__DOLLAR__workspace){fileName}}","variables":{"fileName":"`jn'","userName":"`ju'","workspace":"`jws'"}}"' mata: st_global("SUSO_GQL_BODY", st_local("body")) mata: st_global("SUSO_GQL_OPERATIONS", "") mata: st_global("SUSO_UP_FILE", "") _suso_gql , `verbose' local h = r(http) di as txt "suso: map " as res "`name'" as txt " `verb'ed `prep' user " as res "`user'" as txt " (HTTP `h')." return scalar http = `h' exit } di as err "suso maps: unknown action '`verb''. See {help suso}." exit 198 end program _suso_maps_fetch, rclass * Load ALL maps in a workspace into memory (paginating with skip), since the * server caps a page at ~100. Returns r(nobs) and r(totalcount). syntax , WORKSPACE(string) [ PAGESize(integer 100) VERBOSE ] _suso_jsonesc `"`workspace'"' local jws `"`r(js)'"' tempfile acc local skip = 0 local total = . local haveacc = 0 local page = 0 while 1 { local page = `page' + 1 local body `"{"query":"query(__DOLLAR__workspace:String,__DOLLAR__take:Int,__DOLLAR__skip:Int){maps(workspace:__DOLLAR__workspace,take:__DOLLAR__take,skip:__DOLLAR__skip){totalCount nodes{fileName size shapeType shapesCount wkid importDateUtc uploadedBy}}}","variables":{"workspace":"`jws'","take":`pagesize',"skip":`skip'}}"' mata: st_global("SUSO_GQL_BODY", st_local("body")) mata: st_global("SUSO_GQL_OPERATIONS", "") mata: st_global("SUSO_UP_FILE", "") _suso_gql , todata nodepath(maps.nodes) `verbose' local n = r(nobs) if "`r(totalcount)'"!="" & "`r(totalcount)'"!="." local total = r(totalcount) if `n'==0 continue, break if `haveacc' append using `acc' quietly save `acc', replace local haveacc = 1 local skip = `skip' + `n' if `total'!=. & `skip' >= `total' continue, break if `page' >= 2000 continue, break } if `haveacc' use `acc', clear else clear return scalar nobs = _N if `total'!=. return scalar totalcount = `total' end program _suso_maps_del1, rclass * Delete one map (deleteMap GraphQL mutation). No interactive guard — callers * (suso maps delete / deleteall) handle confirmation. Returns r(http). syntax , WORKSPACE(string) NAME(string) [ VERBOSE ] _suso_jsonesc `"`name'"' local jn `"`r(js)'"' _suso_jsonesc `"`workspace'"' local jws `"`r(js)'"' local body `"{"query":"mutation(__DOLLAR__workspace:String,__DOLLAR__fileName:String!){deleteMap(workspace:__DOLLAR__workspace,fileName:__DOLLAR__fileName){fileName}}","variables":{"workspace":"`jws'","fileName":"`jn'"}}"' mata: st_global("SUSO_GQL_BODY", st_local("body")) mata: st_global("SUSO_GQL_OPERATIONS", "") mata: st_global("SUSO_UP_FILE", "") _suso_gql , `verbose' return scalar http = r(http) end program _suso_export_get, rclass * Start one export, poll to completion (showing progress as it changes), then * download it (optionally unzip). Errors (exit 459) on failure/timeout so callers * can wrap in capture. A Completed job with no data file returns r(status)=="NoFile" * (not an error). Mirrors the backup notebook's start/wait/download chain. syntax , TYPE(string) SAVING(string) [ GUID(string) QVER(integer 0) /// ISTATUS(string) META NOMETA POLLSecs(integer 10) JOBTimeout(integer 3600) /// replace UNZIP UNZIPW(string) UNZIPto(string) VERBOSE ] if "`istatus'"=="" local istatus "All" local metaopt = cond("`nometa'"!="","nometa","meta") suso export start , type(`type') guid(`guid') qver(`qver') istatus(`istatus') `metaopt' `verbose' local jid `"`r(jobid)'"' if `"`jid'"'=="" { di as err "suso: export start returned no JobId." exit 459 } local elapsed = 0 local status "" local hasfile "true" local lastline "" while 1 { quietly suso export status , id(`jid') `verbose' local status `"`r(exportstatus)'"' local hasfile `"`r(hasexportfile)'"' local pct `"`r(progress)'"' if "`status'"=="Completed" local pct "100" * print a progress line only when it changes (auto-suppressed under capture) if "`status' `pct'"!="`lastline'" { di as txt " export " as res "`jid'" as txt " {col 50}" as res "`status'" as txt " " as res "`pct'%" local lastline "`status' `pct'" } if "`status'"=="Completed" continue, break if inlist("`status'","Fail","Failed","Canceled","Cancelled") { di as err "suso: export job `jid' `status'." exit 459 } if `elapsed' >= `jobtimeout' { di as err "suso: export job `jid' timed out after `jobtimeout's (status=`status')." exit 459 } sleep `=`pollsecs'*1000' local elapsed = `elapsed' + `pollsecs' } * Completed but no data for this type -> nothing to download (not a failure). if inlist(lower(`"`hasfile'"'),"false","0","no") { return local saved "" return scalar jobid = `jid' return local status "NoFile" exit } capture suso export download , id(`jid') saving(`"`saving'"') `replace' `unzip' unzipw(`"`unzipw'"') unzipto(`"`unzipto'"') `verbose' if _rc { * the /file endpoint can 403/404 for a beat right after Completed: retry once sleep 2000 suso export download , id(`jid') saving(`"`saving'"') `replace' `unzip' unzipw(`"`unzipw'"') unzipto(`"`unzipto'"') `verbose' } return add return scalar jobid = `jid' return local status "`status'" end program _suso_backup, rclass * Full-workspace backup (mirrors data_backup_SuSo notebook), built entirely * on existing suso verbs: * questionnaires/ questionnaires_list.dta + _v<ver>_document.json * exports/ <title>_v<ver>_<TYPE>.zip (one per questionnaire x type) * workspace/ assignments.dta, supervisors.dta version 14.2 syntax , DIR(string) [ TYPEs(string) ISTATUS(string) NOMETA /// POLLSecs(integer 10) JOBTimeout(integer 3600) /// NOExports NOQuestionnaires NOWorkspace VERBOSE ] if "$SUSO_BASE"=="" | "$SUSO_WS"=="" { di as err "suso backup: configure first. suso config , server(<url>) workspace(<name>)" exit 198 } if `"`types'"'=="" local types "STATA" if "`istatus'"=="" local istatus "All" local metaopt = cond("`nometa'"!="","nometa","meta") local dir = subinstr(`"`dir'"', "\", "/", .) if substr(`"`dir'"',-1,1)=="/" local dir = substr(`"`dir'"',1,length(`"`dir'"')-1) capture mkdir `"`dir'"' capture mkdir `"`dir'/exports"' capture mkdir `"`dir'/questionnaires"' capture mkdir `"`dir'/workspace"' di as txt "{hline 66}" di as txt "suso backup: " as res "$SUSO_BASE/$SUSO_WS" as txt " -> " as res `"`dir'"' di as txt "{hline 66}" preserve local nok = 0 local nfail = 0 local nskip = 0 * ---- questionnaires: list metadata ---- local haveq = 0 capture suso questionnaire list , all if _rc { di as err " questionnaires: list FAILED (rc=`=_rc') — skipping documents & exports." local ++nfail } else { local haveq = 1 quietly save `"`dir'/questionnaires/questionnaires_list.dta"', replace di as txt " questionnaires: " as res "`=_N'" as txt " version(s)" } * ---- per-version: document + exports (none of these clobber the dataset) ---- if `haveq' { local nq = _N forvalues i = 1/`nq' { local guid = QuestionnaireId[`i'] local ver = Version[`i'] local title = Title[`i'] local tag = ustrregexra(`"`title'"', "[^A-Za-z0-9._-]+", "_") local tag = ustrregexra(`"`tag'"', "^_+|_+$", "") if length(`"`tag'"') > 60 local tag = substr(`"`tag'"',1,60) local stub "`tag'_v`ver'" if "`noquestionnaires'"=="" { capture suso questionnaire document , guid(`guid') qver(`ver') saving(`"`dir'/questionnaires/`stub'_document.json"') replace if _rc local ++nfail } if "`noexports'"=="" { foreach et of local types { local dest `"`dir'/exports/`stub'_`et'.zip"' di as txt " export: " as res "`stub' [`et']" as txt " ..." capture _suso_export_get , type(`et') guid(`guid') qver(`ver') /// istatus(`istatus') `metaopt' pollsecs(`pollsecs') /// jobtimeout(`jobtimeout') saving(`"`dest'"') replace `verbose' if _rc { local ++nfail di as err " FAILED (rc=`=_rc')" } else if `"`r(status)'"'=="NoFile" { local ++nskip di as txt " no data — skipped" } else { local ++nok di as txt " saved " as res `"`r(saved)'"' } } } } } * ---- workspace objects (these reload the dataset, so do them last) ---- if "`noworkspace'"=="" { capture suso assignment list , all if _rc { di as err " assignments: FAILED (rc=`=_rc')" local ++nfail } else { quietly save `"`dir'/workspace/assignments.dta"', replace di as txt " assignments: " as res "`=_N'" as txt " saved" } capture suso supervisor list , all if _rc { di as err " supervisors: FAILED (rc=`=_rc')" local ++nfail } else { quietly save `"`dir'/workspace/supervisors.dta"', replace di as txt " supervisors: " as res "`=_N'" as txt " saved" } } restore di as txt _n "{hline 66}" di as txt "suso backup: done. " as res "`nok'" as txt " export(s) saved, " /// as res "`nskip'" as txt " empty/skipped, " as res "`nfail'" as txt " failed." di as txt "Output: " as res `"`dir'"' return scalar ok = `nok' return scalar skipped = `nskip' return scalar failed = `nfail' end program _suso_transport_err * Display a backend transport error and, if it is a TLS/certificate trust * failure, explain how to fix it on ANY server (not just the WBG network). local msg `"`macval(0)'"' * drop the legacy WBG-specific hint the backend may append local msg = subinstr(`"`macval(msg)'"', `" (TLS/proxy issue on the WBG network? See 'suso doctor' and the SSL notes in the README.)"', "", .) di as err `"suso: `macval(msg)'"' local low = lower(`"`macval(msg)'"') if strpos(`"`low'"',"sslhandshake") | strpos(`"`low'"',"pkix") | strpos(`"`low'"',"certification path") | strpos(`"`low'"',"certpath") | strpos(`"`low'"',"unable to find valid cert") { di as txt "" di as txt " Stata's Java runtime does not trust this server's TLS certificate. This is" di as txt " common for non-World-Bank servers, self-signed certificates, or an outdated" di as txt " Java trust store. Fix it in one of these ways:" di as txt " 1) {bf:Trust the certificate (recommended).} Run {bf:suso doctor} to find the" di as txt " Java home, then import the server's root CA into that JVM with keytool" di as txt " (see the SSL / proxy notes in {bf:help suso} or the README)." di as txt " 2) {bf:Skip TLS verification for this session (quick, less secure).}" di as txt " {bf:. suso config , insecure}" di as txt " then re-run your command. Use only against a server you trust." } end program _suso_jar if "$SUSO_JAR"=="" { * 1) anywhere on the adopath capture findfile suso.jar if !_rc global SUSO_JAR "`r(fn)'" } if "$SUSO_JAR"=="" { * 2) right next to suso.ado capture findfile suso.ado if !_rc { local ad = subinstr(`"`r(fn)'"', "\", "/", .) local k = strrpos(`"`ad'"', "/") if `k'>0 { local dir = substr(`"`ad'"', 1, `k') foreach c in `"`dir'suso.jar"' `"`dir'jar/suso.jar"' { capture confirm file `"`c'"' if !_rc { global SUSO_JAR `"`c'"' continue, break } } } } } if "$SUSO_JAR"=="" { * 3) standard Stata folders foreach w in PERSONAL PLUS SITE OLDPLACE { capture local root : sysdir `w' if !_rc & `"`root'"'!="" { local root = subinstr(`"`root'"', "\", "/", .) foreach c in `"`root'suso.jar"' `"`root's/suso.jar"' `"`root'jar/suso.jar"' { capture confirm file `"`c'"' if !_rc { global SUSO_JAR `"`c'"' continue, break } } } if "$SUSO_JAR"!="" continue, break } } if "$SUSO_JAR"=="" { di as err "suso: could not locate suso.jar." di as err " Put it next to suso.ado (e.g. in `c(sysdir_plus)'s/) or run:" di as err " suso config , jar(c:/full/path/to/suso.jar)" exit 601 } * Normalize Windows backslashes to forward slashes for javacall/Java. mata: st_global("SUSO_JAR", subinstr(st_global("SUSO_JAR"), char(92), char(47))) capture confirm file "$SUSO_JAR" if _rc { di as err "suso: jar not found at: $SUSO_JAR" di as err " Fix with: suso config , jar(c:/full/path/to/suso.jar)" exit 601 } end * The workhorse: set bridge globals, call Java, surface results / errors in r(). * The request BODY (if any) is set by the caller in global SUSO_BODY_REQ. program _suso_call, rclass version 14.2 syntax , METHOD(string) PATH(string) [ QUERY(string) CType(string) /// ACCept(string) TODATA ARRAYkey(string) SAVEfile(string) /// DESTRUCTIVE ALLOW ROOT VERBOSE ] _suso_init _suso_jar if "$SUSO_BASE"=="" { di as err "suso: no server configured. suso config , server(<url>) workspace(<name>)" exit 198 } if "$SUSO_WS"=="" & "`root'"=="" { di as err "suso: no workspace configured. suso config , workspace(<name>)" exit 198 } global SUSO_PATH `"`path'"' global SUSO_METHOD "`method'" global SUSO_QUERY `"`query'"' global SUSO_CTYPE "`ctype'" global SUSO_ACCEPT "`accept'" * Resolve a relative save path against Stata's working dir (not the JVM's, which * is the bundled-JDK bin folder). Absolute = starts with drive (C:), / or \. if `"`savefile'"' != "" { local _abs 0 if substr(`"`savefile'"',2,1)==":" local _abs 1 if substr(`"`savefile'"',1,1)=="/" local _abs 1 if substr(`"`savefile'"',1,1)=="\" local _abs 1 if !`_abs' local savefile `"`c(pwd)'/`savefile'"' } global SUSO_SAVEFILE `"`savefile'"' global SUSO_ARRAYKEY "`arraykey'" global SUSO_TODATA = cond("`todata'"!="","1","0") global SUSO_VERBOSE = cond(("`verbose'"!="" | "$SUSO_DEBUG"=="1"),"1","0") global SUSO_DESTRUCTIVE = cond("`destructive'"!="","1","0") global SUSO_ALLOW_DESTRUCTIVE = cond("`allow'"!="","1","0") if "`root'"!="" global SUSO_PATHBASE "" else global SUSO_PATHBASE "/$SUSO_WS" * SUSO_BODY_REQ is set by the caller (may be empty). Check its length without * expanding it inline (the body holds double quotes / $ and would break a "..." compare). local _brq : copy global SUSO_BODY_REQ if `:length local _brq'==0 global SUSO_BODY_REQ "" if "`todata'"!="" clear capture noisily javacall org.worldbank.suso.Stata run , classpath("$SUSO_JAR") local jrc = _rc local rc "$SUSO_RC" local http "$SUSO_HTTP" local msg `"$SUSO_MSG"' local nobs "$SUSO_NOBS" local nvars "$SUSO_NVARS" local total "$SUSO_TOTALCOUNT" local saved `"$SUSO_SAVED"' local bytes "$SUSO_BYTES" local datecols "$SUSO_DATECOLS" local fkeys "$SUSO_FKEYS" foreach k of local fkeys { if length("SUSO_F_`k'") <= 32 local F_`k' `"${SUSO_F_`k'}"' } if `jrc' { _suso_clearbridge di as err "suso: the Java call failed (Stata rc=`jrc')." di as err " Check suso.jar and that Stata runs Java 11+ : suso doctor" exit `jrc' } if "`rc'"=="" { _suso_clearbridge di as err "suso: no response from the Java backend (it may not have executed)." exit 459 } if "`rc'"!="0" { _suso_clearbridge _suso_transport_err `"`macval(msg)'"' exit 459 } * ---- success ---- if "`todata'"!="" { if "`datecols'"!="" capture _suso_todate `datecols' if "`nobs'"!="" return scalar nobs = real("`nobs'") if "`nvars'"!="" return scalar nvars = real("`nvars'") if "`total'"!="" return scalar totalcount = real("`total'") } if "`savefile'"!="" { return local saved `"`saved'"' if "`bytes'"!="" return scalar bytes = real("`bytes'") } foreach k of local fkeys { return local `k' `"`F_`k''"' } return local http "`http'" if `"`macval(msg)'"'!="" return local message `"`macval(msg)'"' _suso_clearbridge end program _suso_clearbridge capture macro drop SUSO_PATH SUSO_METHOD SUSO_QUERY SUSO_BODY_REQ SUSO_CTYPE /// SUSO_ACCEPT SUSO_SAVEFILE SUSO_ARRAYKEY SUSO_TODATA SUSO_VERBOSE /// SUSO_DESTRUCTIVE SUSO_ALLOW_DESTRUCTIVE SUSO_PATHBASE SUSO_RC SUSO_HTTP /// SUSO_MSG SUSO_BODY SUSO_NOBS SUSO_NVARS SUSO_TOTALCOUNT SUSO_LIMIT /// SUSO_OFFSET SUSO_SAVED SUSO_BYTES SUSO_DATECOLS SUSO_FKEYS local gl : all globals foreach g of local gl { if substr("`g'", 1, 7)=="SUSO_F_" capture macro drop `g' } end * Convert ISO-8601 string columns (flagged by the backend) to Stata %tc doubles. program _suso_todate version 14.2 foreach v of local 0 { capture confirm string variable `v' if _rc continue local lbl : variable label `v' tempvar t quietly gen double `t' = clock(subinstr(substr(`v',1,19),"T"," ",1), "YMDhms") quietly drop `v' quietly rename `t' `v' format `v' %tcCCYY-NN-DD_HH:MM:SS if `"`lbl'"'!="" label variable `v' `"`lbl'"' } end * Generic paginator. MODE is "rows" (offset=#rows skipped) or "page" (offset/page=page no.). program _suso_getall, rclass version 14.2 syntax , PATH(string) MODE(string) SIZEparam(string) PAGEparam(string) /// [ BASEQ(string) MAXsize(integer 200) ARRAYkey(string) ROOT VERBOSE /// ALL LIMIT(integer 0) OFFSET(integer -1) PAGE(integer -1) PAGESize(integer 0) ] local rootopt = cond("`root'"!="","root","") local vopt = cond("`verbose'"!="","verbose","") local size = `pagesize' if `size'<=0 local size = `maxsize' if `size'>`maxsize' local size = `maxsize' if `size'<=0 local size 100 local single 0 if (`offset'>=0 | `page'>=0) local single 1 if "`all'"=="" & `single'==0 local single 1 if "`mode'"=="rows" local pos = cond(`offset'>=0, `offset', 0) else local pos = cond(`page'>=0, `page', 1) local maxrows = real("$SUSO_MAXROWS") if `maxrows'<=0 local maxrows 100000 tempfile acc local got 0 local total . local first 1 while (1) { local q "`baseq'" if "`q'"!="" local q "`q'&" local q "`q'`pageparam'=`pos'&`sizeparam'=`size'" _suso_call , method(GET) path(`path') query(`q') todata arraykey(`arraykey') `rootopt' `vopt' local n = r(nobs) if "`n'"=="" local n 0 if !missing(r(totalcount)) local total = r(totalcount) if `first' { quietly save `"`acc'"', replace local first 0 } else { tempfile pg quietly save `"`pg'"', replace quietly use `"`acc'"', clear capture quietly append using `"`pg'"' if _rc { di as txt "suso: stopping pagination (column types differ across pages); returning rows so far." quietly save `"`acc'"', replace continue, break } quietly save `"`acc'"', replace } local got = `got' + `n' if `single' continue, break if `n'==0 continue, break if `limit'>0 & `got'>=`limit' continue, break if `got'>=`maxrows' { di as txt "suso: reached safety cap of `maxrows' rows ({bf:SUSO_MAXROWS}). For very large pulls use {bf:suso export}." continue, break } if !missing(`total') & `got'>=`total' continue, break * The server may return fewer rows than requested (it caps the page size). * Adopt its real page size so the next page's offset stays aligned (no gaps). if `n'>0 & `n'<`size' local size = `n' if "`mode'"=="rows" local pos = `pos' + `n' else local pos = `pos' + 1 } quietly use `"`acc'"', clear if `limit'>0 & _N>`limit' quietly keep in 1/`limit' return scalar nobs = _N if !missing(`total') return scalar totalcount = `total' end * ---- safety gates -------------------------------------------------------------- program _suso_block version 14.2 syntax , ACTion(string) [ CONFIRM ] if "`confirm'"=="" { di as err "{hline 64}" di as err "DESTRUCTIVE OPERATION — not executed." di as err " `action'" di as err " " di as err " Re-run with the {bf:, confirm} option to actually perform it." di as err "{hline 64}" exit 1 } end program _suso_block_ws version 14.2 syntax , NAME(string) [ Iknowthis(string) ] if `"`iknowthis'"' != `"`name'"' { di as err "{hline 64}" di as err "DELETE WORKSPACE — refusing (this permanently removes ALL data in it)." di as err " To proceed you must type the exact workspace name back:" di as err " suso workspace delete , name(`name') iknowthis(`name')" di as err "{hline 64}" exit 1 } end program _suso_audit version 14.2 syntax , ACTion(string) [ TARGET(string) HTTP(string) ] local f "$SUSO_AUDIT" if "`f'"=="" local f "`c(sysdir_personal)'suso_audit.log" capture file open _sa using `"`f'"', write append text if _rc exit file write _sa `"`c(current_date)' `c(current_time)' | user=$SUSO_USER | $SUSO_BASE/$SUSO_WS | `action' | target=`target' | http=`http'"' _n file close _sa end * ---- tiny utilities ------------------------------------------------------------ program _suso_enc, rclass gettoken val : 0 mata: st_local("___enc", suso_urlencode(st_local("val"))) return local enc `"`___enc'"' end program _suso_jsonesc, rclass gettoken val : 0 mata: st_local("___js", suso_jsonesc(st_local("val"))) return local js `"`___js'"' end program _suso_isuuid, rclass gettoken val : 0 local val = trim("`val'") if regexm("`val'","^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$") /// return scalar isuuid = 1 else return scalar isuuid = 0 end * Build "{guid}${version}" without ever putting a literal $ into a macro. program _suso_qid, rclass version 14.2 syntax , GUID(string) [ QVER(integer 0) ] if `qver'>0 return local qid "`guid'__DOLLAR__`qver'" else return local qid "`guid'" end * Fill guid/qver in the CALLER from the session defaults ($SUSO_GUID/$SUSO_QVER) * whenever the user omitted them, so the questionnaire only needs to be set once. program _suso_gq args g q if `"`g'"'=="" & "$SUSO_GUID"!="" c_local guid "$SUSO_GUID" if (`"`q'"'=="" | `"`q'"'=="0") & "$SUSO_QVER"!="" c_local qver "$SUSO_QVER" end * Require a questionnaire (after _suso_gq); friendly message if still missing. program _suso_needq args g if `"`g'"'=="" { di as err "suso: this needs a questionnaire. Either add guid(<GUID>) qver(<ver>) ," di as err " or set it once for the session: suso config , guid(<GUID>) qver(<ver>)" di as err " (find the GUID/version with: suso questionnaire list )" exit 198 } end *=============================================================================== * raw — escape hatch to call any endpoint *=============================================================================== program _suso_raw, rclass version 14.2 syntax anything(name=path id="path"), [ METHOD(string) Query(string) /// CType(string) ACCept(string) TODATA ARRAYkey(string) SAVEfile(string) /// BODY(string) ROOT ALLOWdestructive VERBOSE ] if "`method'"=="" local method GET if `"`body'"'!="" global SUSO_BODY_REQ `"`body'"' local allowopt = cond("`allowdestructive'"!="","allow","") local destopt = cond("`allowdestructive'"!="","destructive","") local rootopt = cond("`root'"!="","root","") local vopt = cond("`verbose'"!="","verbose","") local todopt = cond("`todata'"!="","todata","") _suso_call , method(`method') path(`path') query(`query') ct(`ctype') acc(`accept') /// `todopt' arraykey(`arraykey') savefile(`savefile') `rootopt' `destopt' `allowopt' `vopt' return add end *=============================================================================== * Assignments *=============================================================================== program _suso_assignment, rclass version 14.2 gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="list" { syntax [, SEARCHby(string) GUID(string) QVER(integer 0) RESPonsible(string) /// SUPervisor(string) ORDer(string) ARCHIVEd ALL LIMIT(integer 0) /// OFFSET(integer -1) PAGESize(integer 0) VERBOSE ] _suso_gq "`guid'" "`qver'" local q "" if "`searchby'"!="" { _suso_enc `"`searchby'"' local q "`q'&SearchBy=`r(enc)'" } if "`guid'"!="" { _suso_qid , guid(`guid') qver(`qver') _suso_enc `"`r(qid)'"' local q "`q'&QuestionnaireId=`r(enc)'" } if "`responsible'"!="" { _suso_enc `"`responsible'"' local q "`q'&Responsible=`r(enc)'" } if "`supervisor'"!="" { _suso_enc `"`supervisor'"' local q "`q'&SupervisorId=`r(enc)'" } if "`order'"!="" { _suso_enc `"`order'"' local q "`q'&Order=`r(enc)'" } if "`archived'"!="" local q "`q'&ShowArchive=true" if substr("`q'",1,1)=="&" local q = substr("`q'",2,.) local vopt = cond("`verbose'"!="","verbose","") _suso_getall , path(/api/v1/assignments) mode(rows) sizeparam(Limit) pageparam(Offset) /// maxsize(200) arraykey(Assignments) baseq(`q') `all' limit(`limit') offset(`offset') /// pagesize(`pagesize') `vopt' di as txt "suso: fetched " as res "`=r(nobs)'" as txt " assignment(s)" /// cond(!missing(r(totalcount))," of `=r(totalcount)' on server","") return add exit } if "`verb'"=="get" { syntax , ID(string) [ VERBOSE ] _suso_call , method(GET) path(/api/v1/assignments/`id') `verbose' di as txt "Assignment " as res "`id'" as txt ": responsible=" as res `"`r(responsiblename)'"' /// as txt " quantity=" as res `"`r(quantity)'"' as txt " done=" as res `"`r(interviewscount)'"' /// as txt " archived=" as res `"`r(archived)'"' return add exit } if "`verb'"=="history" { syntax , ID(string) [ START(integer 0) LENGTH(integer 1000) VERBOSE ] _suso_call , method(GET) path(/api/v1/assignments/`id'/history) /// query(start=`start'&length=`length') todata arraykey(History) `verbose' di as txt "suso: " as res "`=r(nobs)'" as txt " history record(s) for assignment `id'." return add exit } if "`verb'"=="quantitysettings" { syntax , ID(string) [ VERBOSE ] _suso_call , method(GET) path(/api/v1/assignments/`id'/assignmentQuantitySettings) `verbose' di as txt "Assignment `id': CanChangeQuantity=" as res `"`r(canchangequantity)'"' return add exit } if "`verb'"=="create" { syntax , RESPonsible(string) [ GUID(string) QVER(integer 0) /// QUANTity(string) EMAIL(string) PASSword(string) WEBmode /// AUDIO COMMents(string) TARGETarea(string) IDENTifying(string) VERBOSE ] _suso_gq "`guid'" "`qver'" _suso_needq "`guid'" _suso_qid , guid(`guid') qver(`qver') local qid "`r(qid)'" _suso_jsonesc `"`responsible'"' local resp "`r(js)'" local body `"{"Responsible":"`resp'","QuestionnaireId":"`qid'""' if "`quantity'"!="" local body `"`body',"Quantity":`quantity'"' if "`email'"!="" { _suso_jsonesc `"`email'"' local body `"`body',"Email":"`r(js)'""' } if "`password'"!="" { _suso_jsonesc `"`password'"' local body `"`body',"Password":"`r(js)'""' } if "`webmode'"!="" local body `"`body',"WebMode":true"' if "`audio'"!="" local body `"`body',"IsAudioRecordingEnabled":true"' if "`comments'"!="" { _suso_jsonesc `"`comments'"' local body `"`body',"Comments":"`r(js)'""' } if "`targetarea'"!="" { _suso_jsonesc `"`targetarea'"' local body `"`body',"TargetArea":"`r(js)'""' } if `"`identifying'"'!="" local body `"`body',"IdentifyingData":`identifying'"' else local body `"`body',"IdentifyingData":[]"' local body `"`body'}"' global SUSO_BODY_REQ `"`body'"' _suso_call , method(POST) path(/api/v1/assignments) `verbose' di as txt "suso: assignment created (HTTP " as res "`r(http)'" as txt ")." return add exit } if "`verb'"=="assign" { syntax , ID(string) RESPonsible(string) [ VERBOSE ] _suso_jsonesc `"`responsible'"' local r "`r(js)'" global SUSO_BODY_REQ `"{"Responsible":"`r'"}"' _suso_call , method(PATCH) path(/api/v1/assignments/`id'/assign) `verbose' di as txt "suso: assignment `id' reassigned (HTTP " as res "`r(http)'" as txt ")." return add exit } if "`verb'"=="quantity" { syntax , ID(string) N(string) [ VERBOSE ] if !regexm("`n'","^-?[0-9]+$") { di as err "suso: -n()- must be an integer (use -1 for unlimited)." exit 198 } global SUSO_BODY_REQ "`n'" _suso_call , method(PATCH) path(/api/v1/assignments/`id'/changeQuantity) `verbose' di as txt "suso: assignment `id' quantity set to " as res "`n'" as txt " (HTTP `r(http)')." return add exit } if "`verb'"=="close" { syntax , ID(string) [ VERBOSE ] _suso_call , method(PATCH) path(/api/v1/assignments/`id'/close) `verbose' di as txt "suso: assignment `id' closed (HTTP `r(http)')." return add exit } if "`verb'"=="archive" { syntax , ID(string) [ CONFIRM VERBOSE ] _suso_block , action("Archive assignment `id' in workspace $SUSO_WS") `confirm' _suso_call , method(PATCH) path(/api/v1/assignments/`id'/archive) destructive allow `verbose' _suso_audit , action("assignment archive") target("`id'") http("`r(http)'") di as txt "suso: assignment `id' archived (HTTP `r(http)')." return add exit } if "`verb'"=="unarchive" { syntax , ID(string) [ VERBOSE ] _suso_call , method(PATCH) path(/api/v1/assignments/`id'/unarchive) `verbose' di as txt "suso: assignment `id' unarchived (HTTP `r(http)')." return add exit } if "`verb'"=="audio" { syntax , ID(string) [ ON OFF VERBOSE ] if "`on'"=="" & "`off'"=="" { di as err "suso: specify -on- or -off-." exit 198 } local en = cond("`on'"!="","true","false") global SUSO_BODY_REQ `"{"Enabled":`en'}"' _suso_call , method(PATCH) path(/api/v1/assignments/`id'/recordAudio) `verbose' di as txt "suso: assignment `id' audio recording = " as res "`en'" as txt " (HTTP `r(http)')." return add exit } if "`verb'"=="targetarea" { syntax , ID(string) AREA(string) [ VERBOSE ] _suso_jsonesc `"`area'"' local a "`r(js)'" global SUSO_BODY_REQ `""`a'""' _suso_call , method(POST) path(/api/v1/assignments/`id'/changeTargetArea) `verbose' di as txt "suso: assignment `id' target area updated (HTTP `r(http)')." return add exit } di as err "suso assignment: unknown action '`verb''. See {help suso}." exit 198 end *=============================================================================== * Interviews *=============================================================================== program _suso_interview, rclass version 14.2 gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="list" { syntax [, GUID(string) QVER(integer 0) STATUS(string) ID(string) /// ALL LIMIT(integer 0) PAGE(integer -1) PAGESize(integer 0) VERBOSE ] _suso_gq "`guid'" "`qver'" local q "" if "`guid'"!="" local q "`q'&questionnaireId=`guid'" if `qver'>0 local q "`q'&questionnaireVersion=`qver'" if "`status'"!="" local q "`q'&status=`status'" if "`id'"!="" local q "`q'&interviewId=`id'" if substr("`q'",1,1)=="&" local q = substr("`q'",2,.) local vopt = cond("`verbose'"!="","verbose","") _suso_getall , path(/api/v1/interviews) mode(page) sizeparam(pageSize) pageparam(page) /// maxsize(100) arraykey(Interviews) baseq(`q') `all' limit(`limit') page(`page') /// pagesize(`pagesize') `vopt' di as txt "suso: fetched " as res "`=r(nobs)'" as txt " interview(s)" /// cond(!missing(r(totalcount))," of `=r(totalcount)' on server","") return add exit } if "`verb'"=="get" { syntax , ID(string) [ VERBOSE ] _suso_call , method(GET) path(/api/v1/interviews/`id') todata arraykey(Answers) `verbose' di as txt "suso: " as res "`=r(nobs)'" as txt " answer rows for interview `id'." return add exit } if "`verb'"=="stats" { syntax , ID(string) [ VERBOSE ] _suso_call , method(GET) path(/api/v1/interviews/`id'/stats) `verbose' di as txt "Interview `id': answered=" as res `"`r(answered)'"' as txt " invalid=" /// as res `"`r(invalid)'"' as txt " withcomments=" as res `"`r(withcomments)'"' /// as txt " status=" as res `"`r(status)'"' return add exit } if "`verb'"=="history" { syntax , ID(string) [ VERBOSE ] _suso_call , method(GET) path(/api/v1/interviews/`id'/history) todata arraykey(Records) `verbose' di as txt "suso: " as res "`=r(nobs)'" as txt " history record(s) for interview `id'." return add exit } if "`verb'"=="pdf" { syntax , ID(string) SAVING(string) [ replace VERBOSE ] if "`replace'"=="" { capture confirm new file `"`saving'"' if _rc { di as err "suso: file already exists. Use -replace-." exit 602 } } _suso_call , method(GET) path(/api/v1/interviews/`id'/pdf) savefile(`saving') accept(application/pdf) `verbose' di as txt "suso: saved interview PDF to " as res `"`r(saved)'"' as txt " (`r(bytes)' bytes)." return add exit } if inlist("`verb'","approve","hqapprove","hqunapprove") { syntax , ID(string) [ COMMENT(string) VERBOSE ] local q "" if "`comment'"!="" { _suso_enc `"`comment'"' local q "comment=`r(enc)'" } _suso_call , method(PATCH) path(/api/v1/interviews/`id'/`verb') query(`q') `verbose' di as txt "suso: interview `id' `verb' OK (HTTP `r(http)')." return add exit } if inlist("`verb'","reject","hqreject") { syntax , ID(string) [ COMMENT(string) RESPonsible(string) VERBOSE ] local q "" if "`comment'"!="" { _suso_enc `"`comment'"' local q "comment=`r(enc)'" } if "`responsible'"!="" { _suso_enc `"`responsible'"' local q "`q'&responsibleId=`r(enc)'" } if substr("`q'",1,1)=="&" local q = substr("`q'",2,.) _suso_call , method(PATCH) path(/api/v1/interviews/`id'/`verb') query(`q') `verbose' di as txt "suso: interview `id' `verb' OK (HTTP `r(http)')." return add exit } if "`verb'"=="assign" | "`verb'"=="assignsupervisor" { syntax , ID(string) [ RESPonsible(string) RESPONSIBLEID(string) RESPONSIBLEName(string) VERBOSE ] local rid "`responsibleid'" local rnm "`responsiblename'" if "`responsible'"!="" { _suso_isuuid `"`responsible'"' if r(isuuid) local rid "`responsible'" else local rnm "`responsible'" } if "`rid'"=="" & "`rnm'"=="" { di as err "suso: specify responsible(), responsibleid() or responsiblename()." exit 198 } if "`rid'"!="" global SUSO_BODY_REQ `"{"ResponsibleId":"`rid'"}"' else { _suso_jsonesc `"`rnm'"' ; global SUSO_BODY_REQ `"{"ResponsibleName":"`r(js)'"}"' } _suso_call , method(PATCH) path(/api/v1/interviews/`id'/`verb') `verbose' di as txt "suso: interview `id' `verb' OK (HTTP `r(http)')." return add exit } if "`verb'"=="comment" { syntax , ID(string) QUESTION(string) COMMENT(string) [ VERBOSE ] _suso_enc `"`comment'"' local q "comment=`r(enc)'" _suso_call , method(POST) path(/api/v1/interviews/`id'/comment/`question') query(`q') `verbose' di as txt "suso: comment added to interview `id' (HTTP `r(http)')." return add exit } if "`verb'"=="commentbyvar" { syntax , ID(string) VARiable(string) COMMENT(string) [ ROSTERvector(numlist) VERBOSE ] _suso_enc `"`comment'"' local q "comment=`r(enc)'" foreach rv of numlist `rostervector' { local q "`q'&rosterVector=`rv'" } _suso_call , method(POST) path(/api/v1/interviews/`id'/comment-by-variable/`variable') query(`q') `verbose' di as txt "suso: comment added to interview `id', variable `variable' (HTTP `r(http)')." return add exit } if "`verb'"=="delete" { syntax , ID(string) [ CONFIRM VERBOSE ] _suso_block , action("DELETE interview `id' in workspace $SUSO_WS (irreversible)") `confirm' _suso_call , method(DELETE) path(/api/v1/interviews/`id') destructive allow `verbose' _suso_audit , action("interview delete") target("`id'") http("`r(http)'") di as txt "suso: interview `id' deleted (HTTP `r(http)')." return add exit } di as err "suso interview: unknown action '`verb''. See {help suso}." exit 198 end *=============================================================================== * Questionnaires *=============================================================================== program _suso_questionnaire, rclass version 14.2 gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="list" { syntax [, ALL LIMIT(integer 0) OFFSET(integer -1) PAGESize(integer 0) VERBOSE ] local vopt = cond("`verbose'"!="","verbose","") _suso_getall , path(/api/v1/questionnaires) mode(page) sizeparam(limit) pageparam(offset) /// maxsize(40) arraykey(Questionnaires) `all' limit(`limit') offset(`offset') /// pagesize(`pagesize') `vopt' di as txt "suso: fetched " as res "`=r(nobs)'" as txt " questionnaire(s)" /// cond(!missing(r(totalcount))," of `=r(totalcount)' on server","") return add exit } if "`verb'"=="get" { syntax [, GUID(string) QVER(integer 0) VERBOSE ] _suso_gq "`guid'" "`qver'" _suso_needq "`guid'" if `qver'>0 { _suso_call , method(GET) path(/api/v1/questionnaires/`guid'/`qver') `verbose' di as txt "Questionnaire " as res `"`r(title)'"' as txt " (v`qver'), variable=" /// as res `"`r(variable)'"' } else { _suso_call , method(GET) path(/api/v1/questionnaires/`guid') todata arraykey(Questionnaires) `verbose' di as txt "suso: " as res "`=r(nobs)'" as txt " version(s) of questionnaire `guid'." } return add exit } if "`verb'"=="document" { syntax , SAVING(string) [ GUID(string) QVER(integer 0) replace VERBOSE ] _suso_gq "`guid'" "`qver'" _suso_needq "`guid'" if `qver'<=0 { di as err "suso: questionnaire document needs a version: qver(<n>) (or set it via suso config)." exit 198 } if "`replace'"=="" { capture confirm new file `"`saving'"' if _rc { di as err "suso: file already exists. Use -replace-." exit 602 } } _suso_call , method(GET) path(/api/v1/questionnaires/`guid'/`qver'/document) savefile(`saving') `verbose' di as txt "suso: saved questionnaire document to " as res `"`r(saved)'"' as txt " (`r(bytes)' bytes)." return add exit } if "`verb'"=="interviews" { syntax [, GUID(string) QVER(integer 0) ALL LIMIT(integer 0) OFFSET(integer -1) PAGESize(integer 0) VERBOSE ] _suso_gq "`guid'" "`qver'" _suso_needq "`guid'" if `qver'<=0 { di as err "suso: questionnaire interviews needs a version: qver(<n>) (or set it via suso config)." exit 198 } local vopt = cond("`verbose'"!="","verbose","") _suso_getall , path(/api/v1/questionnaires/`guid'/`qver'/interviews) mode(page) /// sizeparam(limit) pageparam(offset) maxsize(200) arraykey(Interviews) `all' /// limit(`limit') offset(`offset') pagesize(`pagesize') `vopt' di as txt "suso: fetched " as res "`=r(nobs)'" as txt " interview(s) for questionnaire `guid' v`qver'." return add exit } if "`verb'"=="audio" { syntax [, GUID(string) QVER(integer 0) GET ON OFF VERBOSE ] _suso_gq "`guid'" "`qver'" _suso_needq "`guid'" if `qver'<=0 { di as err "suso: questionnaire audio needs a version: qver(<n>) (or set it via suso config)." exit 198 } if "`get'"!="" | ("`on'"=="" & "`off'"=="") { _suso_call , method(GET) path(/api/v1/questionnaires/`guid'/`qver'/recordAudio) `verbose' di as txt "Questionnaire `guid' v`qver': audio recording Enabled=" as res `"`r(enabled)'"' } else { local en = cond("`on'"!="","true","false") global SUSO_BODY_REQ `"{"Enabled":`en'}"' _suso_call , method(POST) path(/api/v1/questionnaires/`guid'/`qver'/recordAudio) `verbose' di as txt "suso: questionnaire `guid' v`qver' audio recording set to " as res "`en'" as txt " (HTTP `r(http)')." } return add exit } if "`verb'"=="criticality" { syntax [, GUID(string) QVER(integer 0) GET LEVEL(string) VERBOSE ] _suso_gq "`guid'" "`qver'" _suso_needq "`guid'" if `qver'<=0 { di as err "suso: questionnaire criticality needs a version: qver(<n>) (or set it via suso config)." exit 198 } if "`get'"!="" | "`level'"=="" { _suso_call , method(GET) path(/api/v1/questionnaires/`guid'/`qver'/criticalityLevel) `verbose' di as txt "Questionnaire `guid' v`qver': criticality Enabled=" as res `"`r(enabled)'"' } else { if !inlist(strproper("`level'"),"Unknown","Ignore","Warn","Block") { di as err "suso: level() must be one of Unknown, Ignore, Warn, Block." exit 198 } global SUSO_BODY_REQ `"{"CriticalityLevel":"`=strproper("`level'")'"}"' _suso_call , method(POST) path(/api/v1/questionnaires/`guid'/`qver'/criticalityLevel) `verbose' di as txt "suso: questionnaire `guid' v`qver' criticality set to " as res "`level'" as txt " (HTTP `r(http)')." } return add exit } di as err "suso questionnaire: unknown action '`verb''. See {help suso}." exit 198 end *=============================================================================== * Export *=============================================================================== program _suso_export, rclass version 14.2 gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="list" { syntax [, TYPE(string) ISTATUS(string) GUID(string) QVER(integer 0) /// ESTATUS(string) HASfile ALL LIMIT(integer 0) OFFSET(integer -1) /// PAGESize(integer 0) VERBOSE ] _suso_gq "`guid'" "`qver'" local q "" if "`type'"!="" local q "`q'&exportType=`type'" if "`istatus'"!="" local q "`q'&interviewStatus=`istatus'" if "`guid'"!="" { _suso_qid , guid(`guid') qver(`qver') _suso_enc `"`r(qid)'"' local q "`q'&questionnaireIdentity=`r(enc)'" } if "`estatus'"!="" local q "`q'&exportStatus=`estatus'" if "`hasfile'"!="" local q "`q'&hasFile=true" if substr("`q'",1,1)=="&" local q = substr("`q'",2,.) local vopt = cond("`verbose'"!="","verbose","") _suso_getall , path(/api/v2/export) mode(rows) sizeparam(limit) pageparam(offset) /// maxsize(200) arraykey() baseq(`q') `all' limit(`limit') offset(`offset') /// pagesize(`pagesize') `vopt' di as txt "suso: fetched " as res "`=r(nobs)'" as txt " export job(s)." return add exit } if "`verb'"=="start" { syntax , TYPE(string) [ ISTATUS(string) GUID(string) QVER(integer 0) /// FROM(string) TO(string) META NOMETA PARADATAReduced VERBOSE ] _suso_gq "`guid'" "`qver'" _suso_needq "`guid'" if `qver'<=0 { di as err "suso: export needs a questionnaire VERSION. Add qver(<n>) ," di as err " or set it once: suso config , guid(<GUID>) qver(<n>)" exit 198 } if "`istatus'"=="" local istatus All _suso_qid , guid(`guid') qver(`qver') local qid "`r(qid)'" local body `"{"ExportType":"`type'","QuestionnaireId":"`qid'","InterviewStatus":"`istatus'""' if "`from'"!="" { _suso_jsonesc `"`from'"' local body `"`body',"From":"`r(js)'""' } if "`to'"!="" { _suso_jsonesc `"`to'"' local body `"`body',"To":"`r(js)'""' } if "`meta'"!="" local body `"`body',"IncludeMeta":true"' if "`nometa'"!="" local body `"`body',"IncludeMeta":false"' if "`paradatareduced'"!="" local body `"`body',"ParadataReduced":true"' local body `"`body'}"' global SUSO_BODY_REQ `"`body'"' _suso_call , method(POST) path(/api/v2/export) `verbose' di as txt "suso: export started — JobId=" as res `"`r(jobid)'"' as txt " status=" /// as res `"`r(exportstatus)'"' as txt " (HTTP `r(http)')." return add exit } if "`verb'"=="status" { syntax , ID(string) [ VERBOSE ] _suso_call , method(GET) path(/api/v2/export/`id') `verbose' di as txt "Export `id': status=" as res `"`r(exportstatus)'"' as txt " progress=" /// as res `"`r(progress)'"' as txt "% hasFile=" as res `"`r(hasexportfile)'"' return add exit } if "`verb'"=="download" { syntax , ID(string) SAVING(string) [ replace UNZIP UNZIPW(string) UNZIPto(string) VERBOSE ] if "`replace'"=="" { capture confirm new file `"`saving'"' if _rc { di as err "suso: file already exists. Use -replace-." exit 602 } } _suso_call , method(GET) path(/api/v2/export/`id'/file) savefile(`saving') accept(application/zip) `verbose' di as txt "suso: downloaded export to " as res `"`r(saved)'"' as txt " (`r(bytes)' bytes)." local zsaved `"`r(saved)'"' local zhttp = r(http) return add if "`unzip'"!="" | `"`unzipw'"'!="" { _suso_unzip , file(`"`zsaved'"') dir(`"`unzipto'"') pwd(`"`unzipw'"') return local unzipdir `"`r(unzipdir)'"' return scalar unzipped = r(nfiles) return scalar http = `zhttp' } exit } if "`verb'"=="get" { * One-shot: start -> poll (live progress) -> auto-download when 100%. syntax , TYPE(string) SAVING(string) [ GUID(string) QVER(integer 0) /// ISTATUS(string) META NOMETA POLLSecs(integer 10) JOBTimeout(integer 3600) /// replace UNZIP UNZIPW(string) UNZIPto(string) VERBOSE ] if "`replace'"=="" { capture confirm new file `"`saving'"' if _rc { di as err "suso: file already exists. Use -replace-." exit 602 } } _suso_export_get , type(`type') saving(`"`saving'"') guid(`guid') qver(`qver') /// istatus(`istatus') `meta' `nometa' pollsecs(`pollsecs') /// jobtimeout(`jobtimeout') `replace' `unzip' unzipw(`"`unzipw'"') /// unzipto(`"`unzipto'"') `verbose' if "`r(status)'"=="NoFile" { di as txt "suso: export completed but has no data file for this filter — nothing downloaded." } return add exit } if "`verb'"=="cancel" { syntax , ID(string) [ CONFIRM VERBOSE ] _suso_block , action("Cancel/delete export job `id' in workspace $SUSO_WS") `confirm' _suso_call , method(DELETE) path(/api/v2/export/`id') destructive allow `verbose' _suso_audit , action("export cancel") target("`id'") http("`r(http)'") di as txt "suso: export job `id' cancelled (HTTP `r(http)')." return add exit } di as err "suso export: unknown action '`verb''. See {help suso}." exit 198 end *=============================================================================== * Users / Supervisors / Interviewers *=============================================================================== program _suso_user, rclass version 14.2 gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="get" { syntax , ID(string) [ VERBOSE ] _suso_call , method(GET) path(/api/v1/users/`id') `verbose' di as txt "User " as res `"`r(username)'"' as txt ": role=" as res `"`r(role)'"' /// as txt " locked=" as res `"`r(islocked)'"' as txt " archived=" as res `"`r(isarchived)'"' return add exit } if "`verb'"=="create" { syntax , ROLE(string) Username(string) Password(string) [ FULLname(string) /// PHONE(string) EMAIL(string) SUPERVISOR(string) VERBOSE ] if !inlist(strproper("`role'"),"Supervisor","Interviewer","Headquarter","Observer","Apiuser") { di as err "suso: role() must be Supervisor, Interviewer, Headquarter, Observer, or ApiUser." exit 198 } local role = cond(strlower("`role'")=="apiuser","ApiUser",strproper("`role'")) _suso_jsonesc `"`username'"' local un "`r(js)'" _suso_jsonesc `"`password'"' local pw "`r(js)'" local body `"{"Role":"`role'","UserName":"`un'","Password":"`pw'""' if "`fullname'"!="" { _suso_jsonesc `"`fullname'"' local body `"`body',"FullName":"`r(js)'""' } if "`phone'"!="" { _suso_jsonesc `"`phone'"' local body `"`body',"PhoneNumber":"`r(js)'""' } if "`email'"!="" { _suso_jsonesc `"`email'"' local body `"`body',"Email":"`r(js)'""' } if "`supervisor'"!="" { _suso_jsonesc `"`supervisor'"' local body `"`body',"Supervisor":"`r(js)'""' } local body `"`body'}"' global SUSO_BODY_REQ `"`body'"' _suso_call , method(POST) path(/api/v1/users) `verbose' di as txt "suso: user '`username'' created (HTTP `r(http)')." return add exit } if "`verb'"=="archive" { syntax , ID(string) [ CONFIRM VERBOSE ] _suso_block , action("Archive user `id' AND ALL of their interviewers in workspace $SUSO_WS") `confirm' _suso_call , method(PATCH) path(/api/v1/users/`id'/archive) destructive allow `verbose' _suso_audit , action("user archive") target("`id'") http("`r(http)'") di as txt "suso: user `id' archived (HTTP `r(http)')." return add exit } if "`verb'"=="unarchive" { syntax , ID(string) [ VERBOSE ] _suso_call , method(PATCH) path(/api/v1/users/`id'/unarchive) `verbose' di as txt "suso: user `id' unarchived (HTTP `r(http)')." return add exit } di as err "suso user: unknown action '`verb''. See {help suso}." exit 198 end program _suso_supervisor, rclass version 14.2 gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="list" { syntax [, ALL LIMIT(integer 0) OFFSET(integer -1) PAGESize(integer 0) VERBOSE ] local vopt = cond("`verbose'"!="","verbose","") _suso_getall , path(/api/v1/supervisors) mode(page) sizeparam(limit) pageparam(offset) /// maxsize(200) arraykey(Users) `all' limit(`limit') offset(`offset') /// pagesize(`pagesize') `vopt' di as txt "suso: fetched " as res "`=r(nobs)'" as txt " supervisor(s)." return add exit } if "`verb'"=="get" { syntax , ID(string) [ VERBOSE ] _suso_call , method(GET) path(/api/v1/supervisors/`id') `verbose' di as txt "Supervisor " as res `"`r(username)'"' as txt ": archived=" as res `"`r(isarchived)'"' return add exit } if "`verb'"=="interviewers" { syntax , ID(string) [ ALL LIMIT(integer 0) OFFSET(integer -1) PAGESize(integer 0) VERBOSE ] local vopt = cond("`verbose'"!="","verbose","") _suso_getall , path(/api/v1/supervisors/`id'/interviewers) mode(page) sizeparam(limit) /// pageparam(offset) maxsize(200) arraykey(Users) `all' limit(`limit') offset(`offset') /// pagesize(`pagesize') `vopt' di as txt "suso: fetched " as res "`=r(nobs)'" as txt " interviewer(s) under supervisor `id'." return add exit } di as err "suso supervisor: unknown action '`verb''. See {help suso}." exit 198 end program _suso_interviewer, rclass version 14.2 gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="get" { syntax , ID(string) [ VERBOSE ] _suso_call , method(GET) path(/api/v1/interviewers/`id') `verbose' di as txt "Interviewer " as res `"`r(username)'"' as txt ": supervisor=" as res `"`r(supervisorname)'"' /// as txt " locked=" as res `"`r(islocked)'"' as txt " archived=" as res `"`r(isarchived)'"' return add exit } if "`verb'"=="actionslog" { syntax , ID(string) [ START(string) END(string) VERBOSE ] local q "" if "`start'"!="" { _suso_enc `"`start'"' local q "`q'&start=`r(enc)'" } if "`end'"!="" { _suso_enc `"`end'"' local q "`q'&end=`r(enc)'" } if substr("`q'",1,1)=="&" local q = substr("`q'",2,.) _suso_call , method(GET) path(/api/v1/interviewers/`id'/actions-log) query(`q') todata arraykey() `verbose' di as txt "suso: " as res "`=r(nobs)'" as txt " action-log record(s) for interviewer `id'." return add exit } di as err "suso interviewer: unknown action '`verb''. See {help suso}." exit 198 end *=============================================================================== * Workspaces (server-level; default to server root, override with -usews-) *=============================================================================== program _suso_workspace, rclass version 14.2 gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="list" { syntax [, INCLUDEDISabled USEWS VERBOSE ] local rootopt = cond("`usews'"=="","root","") local q "Start=0&Length=1000" if "`includedisabled'"!="" local q "`q'&IncludeDisabled=true" _suso_call , method(GET) path(/api/v1/workspaces) query(`q') todata arraykey() `rootopt' `verbose' di as txt "suso: fetched " as res "`=r(nobs)'" as txt " workspace(s)." return add exit } if "`verb'"=="get" { syntax , NAME(string) [ USEWS VERBOSE ] local rootopt = cond("`usews'"=="","root","") _suso_call , method(GET) path(/api/v1/workspaces/`name') `rootopt' `verbose' di as txt "Workspace " as res `"`r(name)'"' as txt " — " as res `"`r(displayname)'"' return add exit } if "`verb'"=="status" { syntax , NAME(string) [ USEWS VERBOSE ] local rootopt = cond("`usews'"=="","root","") _suso_call , method(GET) path(/api/v1/workspaces/status/`name') `rootopt' `verbose' di as txt _n "Workspace status: " as res `"`name'"' di as txt " can be deleted : " as res `"`r(canbedeleted)'"' local _enq `"`r(existingquestionnairescou)'"' if `"`_enq'"'=="" local _enq `"`r(existingquestionnairescount)'"' di as txt " questionnaires : " as res `"`_enq'"' di as txt " supervisors : " as res `"`r(supervisorscount)'"' di as txt " interviewers : " as res `"`r(interviewerscount)'"' di as txt " maps : " as res `"`r(mapscount)'"' return add exit } if "`verb'"=="create" { syntax , NAME(string) DISPLAYname(string) [ USEWS VERBOSE ] if !regexm("`name'","^[0-9a-z,]+$") | length("`name'")>12 { di as err "suso: workspace name must match ^[0-9,a-z]+$ and be <= 12 chars." exit 198 } local rootopt = cond("`usews'"=="","root","") _suso_jsonesc `"`displayname'"' local dn "`r(js)'" global SUSO_BODY_REQ `"{"Name":"`name'","DisplayName":"`dn'"}"' _suso_call , method(POST) path(/api/v1/workspaces) `rootopt' `verbose' di as txt "suso: workspace '`name'' created (HTTP `r(http)')." return add exit } if "`verb'"=="update" { syntax , NAME(string) DISPLAYname(string) [ USEWS VERBOSE ] local rootopt = cond("`usews'"=="","root","") _suso_jsonesc `"`displayname'"' local dn "`r(js)'" global SUSO_BODY_REQ `"{"DisplayName":"`dn'"}"' _suso_call , method(PATCH) path(/api/v1/workspaces/`name') `rootopt' `verbose' di as txt "suso: workspace '`name'' updated (HTTP `r(http)')." return add exit } if "`verb'"=="enable" { syntax , NAME(string) [ USEWS VERBOSE ] local rootopt = cond("`usews'"=="","root","") _suso_call , method(POST) path(/api/v1/workspaces/`name'/enable) `rootopt' `verbose' di as txt "suso: workspace '`name'' enabled (HTTP `r(http)')." return add exit } if "`verb'"=="disable" { syntax , NAME(string) [ CONFIRM USEWS VERBOSE ] _suso_block , action("Disable workspace '`name'' (users can no longer use it)") `confirm' local rootopt = cond("`usews'"=="","root","") _suso_call , method(POST) path(/api/v1/workspaces/`name'/disable) destructive allow `rootopt' `verbose' _suso_audit , action("workspace disable") target("`name'") http("`r(http)'") di as txt "suso: workspace '`name'' disabled (HTTP `r(http)')." return add exit } if "`verb'"=="delete" { syntax , NAME(string) [ Iknowthis(string) FORCE USEWS VERBOSE ] local rootopt = cond("`usews'"=="","root","") * 1) typed-name confirmation _suso_block_ws , name(`name') iknowthis(`iknowthis') * 2) status pre-check _suso_call , method(GET) path(/api/v1/workspaces/status/`name') `rootopt' local can = strlower(`"`r(canbedeleted)'"') di as txt _n "About to DELETE workspace '" as res "`name'" as txt "':" di as txt " questionnaires=" as res `"`r(existingquestionnairescount)'"' as txt /// " supervisors=" as res `"`r(supervisorscount)'"' as txt /// " interviewers=" as res `"`r(interviewerscount)'"' as txt /// " maps=" as res `"`r(mapscount)'"' as txt " canBeDeleted=" as res "`can'" if "`can'"!="true" & "`can'"!="1" & "`force'"=="" { di as err "suso: the server reports this workspace CANNOT be safely deleted (CanBeDeleted=`can')." di as err " It still contains data/users. Disable it instead, or override with -force- if you are certain." exit 1 } * 3) execute _suso_call , method(DELETE) path(/api/v1/workspaces/`name') destructive allow `rootopt' `verbose' _suso_audit , action("workspace DELETE") target("`name'") http("`r(http)'") di as txt "suso: workspace '`name'' deleted (HTTP " as res "`r(http)'" as txt "). Success=" as res `"`r(success)'"' return add exit } if "`verb'"=="assign" { syntax , USERIDS(string) WORKSpaces(string) [ MODE(string) SUPERVISOR(string) USEWS VERBOSE ] local rootopt = cond("`usews'"=="","root","") if "`mode'"=="" local mode Assign if !inlist(strproper("`mode'"),"Assign","Add","Remove") { di as err "suso: mode() must be Assign, Add or Remove." exit 198 } * UserIds array local uids "" foreach u of local userids { local uids `"`uids',"`u'""' } local uids = substr(`"`uids'"',2,.) * Workspaces array local wss "" foreach w of local workspaces { if "`supervisor'"!="" local wss `"`wss',{"Workspace":"`w'","SupervisorId":"`supervisor'"}"' else local wss `"`wss',{"Workspace":"`w'"}"' } local wss = substr(`"`wss'"',2,.) global SUSO_BODY_REQ `"{"UserIds":[`uids'],"Workspaces":[`wss'],"Mode":"`=strproper("`mode'")'"}"' _suso_call , method(POST) path(/api/v1/workspaces/assign) `rootopt' `verbose' di as txt "suso: workspace assignment updated (HTTP `r(http)')." return add exit } di as err "suso workspace: unknown action '`verb''. See {help suso}." exit 198 end *=============================================================================== * Settings *=============================================================================== program _suso_settings, rclass version 14.2 gettoken what 0 : 0 local what = strlower("`what'") if "`what'"!="globalnotice" { di as err "suso settings: only 'globalnotice' is supported. See {help suso}." exit 198 } gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="get" { syntax [, VERBOSE] _suso_call , method(GET) path(/api/v1/settings/globalnotice) `verbose' di as txt "Global notice: " as res `"`r(message)'"' return add exit } if "`verb'"=="set" { syntax , MESSAGE(string) [ VERBOSE ] _suso_jsonesc `"`message'"' local m "`r(js)'" global SUSO_BODY_REQ `"{"Message":"`m'"}"' _suso_call , method(PUT) path(/api/v1/settings/globalnotice) `verbose' di as txt "suso: global notice set (HTTP `r(http)')." return add exit } if "`verb'"=="clear" { syntax [, VERBOSE] _suso_call , method(DELETE) path(/api/v1/settings/globalnotice) `verbose' di as txt "suso: global notice cleared (HTTP `r(http)')." return add exit } di as err "suso settings globalnotice: action must be get, set or clear." exit 198 end *=============================================================================== * Statistics *=============================================================================== program _suso_statistics, rclass version 14.2 gettoken verb 0 : 0, parse(" ,") local verb = strlower("`verb'") if "`verb'"=="questionnaires" { syntax [, VERBOSE] _suso_call , method(GET) path(/api/v1/statistics/questionnaires) todata arraykey() `verbose' di as txt "suso: " as res "`=r(nobs)'" as txt " questionnaire(s) with data." return add exit } if "`verb'"=="questions" { syntax [, GUID(string) QVER(integer 0) VERBOSE ] _suso_gq "`guid'" "`qver'" _suso_needq "`guid'" local q "questionnaireId=`guid'" if `qver'>0 local q "`q'&version=`qver'" _suso_call , method(GET) path(/api/v1/statistics/questions) query(`q') todata arraykey() `verbose' di as txt "suso: " as res "`=r(nobs)'" as txt " question(s) with data." return add exit } if "`verb'"=="report" { syntax , QUESTION(string) [ GUID(string) QVER(integer 0) EXPORTtype(string) /// SAVING(string) replace Query(string) VERBOSE ] _suso_gq "`guid'" "`qver'" _suso_needq "`guid'" local q "QuestionnaireId=`guid'&Question=`question'" if `qver'>0 local q "`q'&Version=`qver'" if "`exporttype'"!="" local q "`q'&exportType=`exporttype'" if `"`query'"'!="" local q `"`q'&`query'"' if "`saving'"!="" { if "`replace'"=="" { capture confirm new file `"`saving'"' if _rc { di as err "suso: file already exists. Use -replace-." exit 602 } } _suso_call , method(GET) path(/api/v1/statistics) query(`q') savefile(`saving') `verbose' di as txt "suso: saved statistics report to " as res `"`r(saved)'"' as txt " (`r(bytes)' bytes)." } else { _suso_call , method(GET) path(/api/v1/statistics) query(`q') todata arraykey() `verbose' di as txt "suso: loaded statistics report (" as res "`=r(nobs)'" as txt " rows)." } return add exit } di as err "suso statistics: action must be report, questions or questionnaires." exit 198 end *=============================================================================== * examples — copy/paste recipes printed in the Results window *=============================================================================== program _suso_examples di as txt _n "{hline 72}" di as res " suso — copy / paste recipes" di as txt " (replace the bits in <...>; clickable links run the safe ones)" di as txt "{hline 72}" di as res _n " 1) CONNECT (once per Stata session)" di as txt " suso config , server(<url>) workspace(<ws>) user(<apiuser>) password(<pw>)" di as txt " suso config , guid(<questionnaire-GUID>) qver(<version>) {txt}// set your survey ONCE" di as txt " suso ping" di as txt " {stata suso doctor:suso doctor} {txt}// check Stata + Java + settings" di as txt " Tip: set the SUSO_PASSWORD environment variable and omit password()." di as res _n " 2) SEE DATA (replaces the data in memory; preserve first if needed)" di as txt " suso questionnaire list {txt}// find the GUID + Version" di as txt " suso assignment list , all" di as txt " suso interview list , status(Completed) all" di as txt " suso interview list , status(RejectedBySupervisor) all" di as txt " suso interview list , all {txt}// uses your saved questionnaire" di as txt " suso interview stats , id(<interview-uuid>)" di as txt " suso interview get , id(<interview-uuid>) {txt}// loads the answers" di as txt " suso interview history , id(<interview-uuid>)" di as res _n " 3) REVIEW (approve / reject / comment)" di as txt `" suso interview approve , id(<uuid>) comment("looks good")"' di as txt `" suso interview reject , id(<uuid>) comment("please revisit the GPS point")"' di as txt " suso interview hqapprove , id(<uuid>)" di as txt `" suso interview hqreject , id(<uuid>) comment("see notes")"' di as txt `" suso interview commentbyvar , id(<uuid>) variable(d2_sales) comment("confirm units")"' di as res _n " 4) EXPORT + DOWNLOAD (best way to pull large data)" di as txt " suso export start , type(STATA) istatus(ApprovedBySupervisor)" di as txt " {txt}// guid/qver come from your saved questionnaire; add guid()/qver() to override" local bq = char(96) local eq = char(39) di as txt " suso export status , id(`bq'=r(jobid)`eq') {txt}// repeat until status=Completed" di as txt `" suso export download , id(`bq'=r(jobid)`eq') saving("ises.zip") replace"' di as res _n " 5) TEAM" di as txt " suso supervisor list , all" di as txt " suso supervisor interviewers , id(<supervisor-uuid>)" di as txt " suso interviewer actionslog , id(<interviewer-uuid>) start(2026-06-01) end(2026-06-17)" di as txt " suso assignment assign , id(<assignment-id>) responsible(<interviewer-login>)" di as res _n " 6) DANGER (need confirmation; written to the audit log)" di as txt " suso interview delete , id(<uuid>) confirm" di as txt " suso export cancel , id(<jobid>) confirm" di as txt " suso workspace status , name(<ws>)" di as txt " suso workspace delete , name(<ws>) iknowthis(<ws>)" di as txt _n " More: {stata suso endpoints:suso endpoints} (full command list) | {help suso}" di as txt "{hline 72}" _n end *=============================================================================== * endpoints — one-screen list of every command *=============================================================================== program _suso_endpoints di as txt _n "{hline 72}" di as res " suso — all commands (questionnaires use guid()+qver() ; ids use id())" di as txt "{hline 72}" di as res _n " setup " as txt "config | ping | doctor | examples | endpoints | about | raw" di as res _n " assignment" as txt " list get history quantitysettings create assign" di as txt " quantity close archive unarchive audio targetarea" di as res _n " interview " as txt " list get stats history pdf approve reject" di as txt " hqapprove hqreject hqunapprove assign assignsupervisor" di as txt " comment commentbyvar delete" di as res _n " questionnaire" as txt " list get document interviews audio criticality" di as res _n " export " as txt " list start status download get cancel" di as res _n " maps " as txt " list upload delete deleteall assign unassign" di as res _n " user " as txt " get create archive unarchive" di as res " supervisor" as txt " list get interviewers" di as res " interviewer" as txt " get actionslog" di as res _n " workspace " as txt " list get status create update enable disable delete assign" di as res _n " settings " as txt " globalnotice get|set|clear" di as res " statistics" as txt " questionnaires questions report" di as res _n " backup " as txt " full-workspace archive (questionnaires + exports + assignments/users)" di as txt _n " Recipes you can copy: {stata suso examples:suso examples} Help: {help suso}" di as txt "{hline 72}" _n end