Both workarounds existed because Five was missing two features that
just landed upstream:
Five 7629f95 (variadic PValue) makes FUNCTION foo(...) / PValue()
actually return the caller's variadic args instead of the caller's
first LOCAL slot. AP_RPUTS / AP_ECHO can go back to their `( ... )`
signature now.
Five f3e0ffe (file-local STATIC FUNCTION) gives each .prg its own
namespace for `STATIC FUNCTION name`, so the seven duplicate
`STATIC FUNCTION fn_HGet` definitions across labdb's api/*.prg
files no longer collide. The sed-renamed unique names can revert
to the upstream definitions.
Files
app/bridge/bridge_request.prg ← cp from fivenode/native/
app/api/{device-status,record-detail,records-list,session-detail,
session-stats,sessions-list,session-export}.prg
← cp from fivenode/labdb/api/
fivenode-upstream is now byte-identical to fivenode_go's app/ copy
of those files. No more "// fivenode_go patch" comments, no more
file-prefix renames.
Verified end-to-end against the same live postgres@16 cluster:
/api/admin-stats.prg -> {"active_sessions":1,"devices":2,...}
/api/sessions-list.prg -> 2 rows w/ full session data
/api/admin-devices.prg -> 2 devices w/ api_key, created_at
/api/hello.prg -> hello (unchanged)
/ -> 200 text/html (static)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
354 lines
6.9 KiB
Plaintext
354 lines
6.9 KiB
Plaintext
/*
|
|
* bridge_request.prg - FiveNode
|
|
*
|
|
* Copyright (c) 2026 Charles KWON ( Charles KWON Ohjun, charleskwonohjun@gmail.com )
|
|
* All rights reserved.
|
|
*/
|
|
|
|
// bridge_request.prg
|
|
// AP_* function reimplementation - 100% identical function names/signatures
|
|
|
|
// ── Request functions ──
|
|
|
|
FUNCTION AP_METHOD()
|
|
RETURN ctx_get( "method", "GET" )
|
|
|
|
FUNCTION AP_FILENAME()
|
|
RETURN ctx_get( "filename", "" )
|
|
|
|
FUNCTION AP_ARGS()
|
|
RETURN ctx_get( "query_string", "" )
|
|
|
|
FUNCTION AP_USERIP()
|
|
RETURN ctx_get( "remote_ip", "127.0.0.1" )
|
|
|
|
FUNCTION AP_BODY()
|
|
RETURN ctx_get( "body", "" )
|
|
|
|
FUNCTION AP_GETBODY()
|
|
RETURN ctx_get( "body", "" )
|
|
|
|
FUNCTION AP_GETENV( cVarName )
|
|
|
|
LOCAL hEnv := ctx_get( "env", { => } )
|
|
|
|
IF hb_HHasKey( hEnv, Upper( cVarName ) )
|
|
RETURN hEnv[ Upper( cVarName ) ]
|
|
ENDIF
|
|
|
|
RETURN hb_GetEnv( cVarName, "" )
|
|
|
|
// ── Headers In ──
|
|
|
|
FUNCTION AP_HEADERSIN()
|
|
RETURN ctx_get( "headers_in", { => } )
|
|
|
|
FUNCTION AP_HEADERSINCOUNT()
|
|
RETURN Len( ctx_get( "headers_in", { => } ) )
|
|
|
|
FUNCTION AP_HEADERSINKEY( n )
|
|
|
|
LOCAL hHeaders := ctx_get( "headers_in", { => } )
|
|
|
|
IF n >= 1 .AND. n <= Len( hHeaders )
|
|
RETURN hb_HKeyAt( hHeaders, n )
|
|
ENDIF
|
|
|
|
RETURN ""
|
|
|
|
FUNCTION AP_HEADERSINVAL( n )
|
|
|
|
LOCAL hHeaders := ctx_get( "headers_in", { => } )
|
|
|
|
IF n >= 1 .AND. n <= Len( hHeaders )
|
|
RETURN hb_HValueAt( hHeaders, n )
|
|
ENDIF
|
|
|
|
RETURN ""
|
|
|
|
// ── Headers Out ──
|
|
|
|
FUNCTION AP_HEADERSOUT()
|
|
RETURN ctx_get( "headers_out", { => } )
|
|
|
|
FUNCTION AP_HEADERSOUTCOUNT()
|
|
RETURN Len( ctx_get( "headers_out", { => } ) )
|
|
|
|
FUNCTION AP_HEADERSOUTKEY( n )
|
|
|
|
LOCAL hHeaders := ctx_get( "headers_out", { => } )
|
|
|
|
IF n >= 1 .AND. n <= Len( hHeaders )
|
|
RETURN hb_HKeyAt( hHeaders, n )
|
|
ENDIF
|
|
|
|
RETURN ""
|
|
|
|
FUNCTION AP_HEADERSOUTVAL( n )
|
|
|
|
LOCAL hHeaders := ctx_get( "headers_out", { => } )
|
|
|
|
IF n >= 1 .AND. n <= Len( hHeaders )
|
|
RETURN hb_HValueAt( hHeaders, n )
|
|
ENDIF
|
|
|
|
RETURN ""
|
|
|
|
FUNCTION AP_HEADERSOUTSET( cKey, cValue )
|
|
|
|
LOCAL hHeaders := ctx_get( "headers_out", { => } )
|
|
|
|
hHeaders[ cKey ] := cValue
|
|
ctx_set( "headers_out", hHeaders )
|
|
|
|
RETURN NIL
|
|
|
|
FUNCTION AP_SETCONTENTTYPE( cType )
|
|
RETURN AP_HEADERSOUTSET( "Content-Type", cType )
|
|
|
|
// ── Output ──
|
|
|
|
FUNCTION AP_RPUTS( ... )
|
|
|
|
LOCAL n, u
|
|
|
|
FOR n := 1 TO PCount()
|
|
u := PValue( n )
|
|
IF ValType( u ) == "C"
|
|
bridge_output_append( u )
|
|
ELSE
|
|
bridge_output_append( fn_ValToChar( u ) )
|
|
ENDIF
|
|
NEXT
|
|
|
|
RETURN NIL
|
|
|
|
FUNCTION AP_ECHO( ... )
|
|
|
|
LOCAL n, u
|
|
|
|
FOR n := 1 TO PCount()
|
|
u := PValue( n )
|
|
IF ValType( u ) == "C"
|
|
bridge_output_append( u )
|
|
ELSEIF ValType( u ) != "U"
|
|
bridge_output_append( fn_ValToChar( u ) )
|
|
ENDIF
|
|
IF n < PCount()
|
|
bridge_output_append( " " )
|
|
ENDIF
|
|
NEXT
|
|
|
|
RETURN NIL
|
|
|
|
// ── Parameter Parsing ──
|
|
|
|
FUNCTION AP_GetPairs( lUrlDecode )
|
|
|
|
LOCAL cArgs, aParams, aPair, hResult, cParam
|
|
|
|
hb_default( @lUrlDecode, .T. )
|
|
hResult := { => }
|
|
cArgs := AP_ARGS()
|
|
|
|
IF ! Empty( cArgs )
|
|
aParams := hb_ATokens( cArgs, "&" )
|
|
FOR EACH cParam IN aParams
|
|
aPair := hb_ATokens( cParam, "=" )
|
|
IF Len( aPair ) == 2
|
|
hResult[ aPair[ 1 ] ] := iif( lUrlDecode, hb_UrlDecode( aPair[ 2 ] ), aPair[ 2 ] )
|
|
ELSEIF Len( aPair ) == 1
|
|
hResult[ aPair[ 1 ] ] := ""
|
|
ENDIF
|
|
NEXT
|
|
ENDIF
|
|
|
|
RETURN hResult
|
|
|
|
FUNCTION AP_PostPairs( lUrlDecode )
|
|
|
|
LOCAL cBody, aParams, aPair, hResult, cParam
|
|
|
|
hb_default( @lUrlDecode, .T. )
|
|
hResult := { => }
|
|
cBody := AP_BODY()
|
|
|
|
IF ! Empty( cBody )
|
|
aParams := hb_ATokens( cBody, "&" )
|
|
FOR EACH cParam IN aParams
|
|
aPair := hb_ATokens( cParam, "=" )
|
|
IF Len( aPair ) == 2
|
|
hResult[ aPair[ 1 ] ] := iif( lUrlDecode, hb_UrlDecode( aPair[ 2 ] ), aPair[ 2 ] )
|
|
ELSEIF Len( aPair ) == 1
|
|
hResult[ aPair[ 1 ] ] := ""
|
|
ENDIF
|
|
NEXT
|
|
ENDIF
|
|
|
|
RETURN hResult
|
|
|
|
// ── URL decode ──
|
|
|
|
FUNCTION hb_UrlDecode( cString )
|
|
|
|
LOCAL cResult := "", i, c
|
|
|
|
cString := StrTran( cString, "+", " " )
|
|
i := 1
|
|
|
|
DO WHILE i <= Len( cString )
|
|
c := SubStr( cString, i, 1 )
|
|
IF c == "%" .AND. i + 2 <= Len( cString )
|
|
cResult += Chr( hb_HexToNum( SubStr( cString, i + 1, 2 ) ) )
|
|
i += 3
|
|
ELSE
|
|
cResult += c
|
|
i++
|
|
ENDIF
|
|
ENDDO
|
|
|
|
RETURN cResult
|
|
|
|
// ── Path/URL ──
|
|
|
|
FUNCTION fn_PathUrl()
|
|
RETURN ctx_get( "uri", "/" )
|
|
|
|
FUNCTION fn_PathBase( cDirFile )
|
|
|
|
LOCAL cRoot := ctx_get( "document_root", "." )
|
|
|
|
IF cDirFile != NIL
|
|
RETURN cRoot + "/" + cDirFile
|
|
ENDIF
|
|
|
|
RETURN cRoot
|
|
|
|
FUNCTION fn_Include( cFile )
|
|
RETURN hb_MemoRead( fn_PathBase( cFile ) )
|
|
|
|
FUNCTION fn_GetUri()
|
|
|
|
LOCAL cScheme := iif( ctx_get( "https", .F. ), "https", "http" )
|
|
LOCAL cHost := ""
|
|
LOCAL hHeaders := AP_HEADERSIN()
|
|
|
|
IF hb_HHasKey( hHeaders, "Host" )
|
|
cHost := hHeaders[ "Host" ]
|
|
ENDIF
|
|
|
|
RETURN cScheme + "://" + cHost + ctx_get( "uri", "/" )
|
|
|
|
FUNCTION fn_Redirect( cUrl )
|
|
|
|
AP_HEADERSOUTSET( "Location", cUrl )
|
|
ctx_set( "status", 302 )
|
|
|
|
RETURN NIL
|
|
|
|
// ── Data Conversion ──
|
|
|
|
FUNCTION fn_ValToChar( u )
|
|
|
|
LOCAL cType := ValType( u )
|
|
|
|
SWITCH cType
|
|
CASE "C" ; RETURN u
|
|
CASE "N" ; RETURN hb_ntos( u )
|
|
CASE "D" ; RETURN DToC( u )
|
|
CASE "L" ; RETURN iif( u, ".T.", ".F." )
|
|
CASE "U" ; RETURN "NIL"
|
|
CASE "A" ; RETURN hb_jsonEncode( u )
|
|
CASE "H" ; RETURN hb_jsonEncode( u )
|
|
CASE "T" ; RETURN hb_TToC( u )
|
|
ENDSWITCH
|
|
|
|
RETURN "(" + cType + ")"
|
|
|
|
// ctx_get / ctx_set are defined as PUBLIC in bridge_context.prg
|
|
|
|
// -- mod_harbour compatibility wrappers --
|
|
|
|
FUNCTION MH_PathUrl()
|
|
RETURN fn_PathUrl()
|
|
|
|
FUNCTION MH_PathBase( cDirFile )
|
|
RETURN fn_PathBase( cDirFile )
|
|
|
|
FUNCTION MH_Include( cFile )
|
|
RETURN fn_Include( cFile )
|
|
|
|
FUNCTION mh_GetUri()
|
|
RETURN fn_GetUri()
|
|
|
|
FUNCTION mh_Redirect( cUrl )
|
|
RETURN fn_Redirect( cUrl )
|
|
|
|
FUNCTION MH_ValToChar( u )
|
|
RETURN fn_ValToChar( u )
|
|
|
|
// ── JSON Response helpers ──
|
|
|
|
FUNCTION AP_JSONRESPONSE( xData, nStatus )
|
|
|
|
IF nStatus != NIL
|
|
ctx_set( "status", nStatus )
|
|
ENDIF
|
|
|
|
AP_SETCONTENTTYPE( "application/json" )
|
|
AP_RPUTS( hb_jsonEncode( xData ) )
|
|
|
|
RETURN NIL
|
|
|
|
FUNCTION AP_SETSTATUS( nStatus )
|
|
|
|
ctx_set( "status", nStatus )
|
|
|
|
RETURN NIL
|
|
|
|
// ── Auth (read-only from .prg, set by Node.js only) ──
|
|
|
|
FUNCTION fn_Auth()
|
|
|
|
LOCAL cAuth := _AUTH_GET()
|
|
LOCAL hAuth
|
|
|
|
IF Empty( cAuth )
|
|
RETURN { "authenticated" => .F. }
|
|
ENDIF
|
|
|
|
hb_jsonDecode( cAuth, @hAuth )
|
|
|
|
IF ValType( hAuth ) != "H"
|
|
RETURN { "authenticated" => .F. }
|
|
ENDIF
|
|
|
|
RETURN hAuth
|
|
|
|
FUNCTION fn_IsAuth()
|
|
RETURN fn_HGetDef( fn_Auth(), "authenticated", .F. )
|
|
|
|
FUNCTION fn_AuthUser()
|
|
RETURN fn_HGetDef( fn_Auth(), "user", "" )
|
|
|
|
FUNCTION fn_AuthRole()
|
|
RETURN fn_HGetDef( fn_Auth(), "role", "" )
|
|
|
|
// ── Hash helper ──
|
|
|
|
FUNCTION fn_HGetDef( hHash, cKey, xDefault )
|
|
RETURN hb_HGetDef( hHash, cKey, xDefault )
|
|
|
|
// ── JSON Body helper ──
|
|
|
|
FUNCTION AP_JSONBODY()
|
|
|
|
LOCAL cBody := AP_BODY()
|
|
LOCAL xResult
|
|
|
|
IF ! Empty( cBody )
|
|
hb_jsonDecode( cBody, @xResult )
|
|
ENDIF
|
|
|
|
RETURN xResult
|