Files
fivenode_go/app/bridge/bridge_request.prg
Charles KWON OhJun ed956a1504 revert: drop AP_RPUTS single-arg patch and fn_HGet renames
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>
2026-05-27 17:36:14 +09:00

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