Referencia API de FoxServerCore

Introducción a FoxServerCore

FoxServerCore es el componente central del framework FoxServer que permite crear APIs RESTful utilizando Visual FoxPro. Proporciona todas las clases y utilidades necesarias para manejar solicitudes HTTP, procesar datos y generar respuestas en formato JSON.

FoxServerCore actúa como un puente entre Visual FoxPro y el servidor web, permitiendo crear servicios web modernos sin abandonar el ecosistema VFP.

El archivo principal FoxServerCore.prg contiene las siguientes clases principales:

  • ApiController: Clase base para todos los controladores API
  • FoxServerRequest: Encapsula la solicitud HTTP entrante
  • FoxServerResponse: Encapsula la respuesta HTTP saliente
  • FoxServerLogger: Proporciona capacidades de logging
  • Clases auxiliares: Para manejo de datos y utilidades diversas

Clase ApiController

La clase ApiController es la base para todos los controladores de API en FoxServer. Hereda de la clase session de VFP y proporciona una amplia gama de métodos y propiedades para facilitar el desarrollo de APIs RESTful, incluyendo manejo de solicitudes, procesamiento de datos, conversión JSON, registro de eventos y utilidades diversas.

Propiedades principales

Propiedad Tipo Descripción
cPath Character Ruta base del servicio. Ej: c:\myService\
bEnableLog Logical Indica si el registro de logs está habilitado (.T.) o no (.F.)
Logger FoxServerLogger Instancia del objeto logger para registrar eventos y errores
cLogCorrelationId Character Identificador de correlación para logs. Se utiliza para rastrear solicitudes específicas a través de múltiples operaciones.
ApiService ApiServiceUtility Objeto de utilidad para servicios API comunes

Inicialización y Configuración

Initialize

Inicializa el controlador API. Configura la ruta base, carga servicios auxiliares como JSONFox y establece el entorno de trabajo. Este método es invocado automáticamente por FoxServer al iniciar el servicio.

PROCEDURE Initialize
Implementación:
* Se ejecuta automáticamente al crear una instancia del controlador
procedure Initialize
    set deleted on
    
    try
        this.ApiService = createobject("ApiServiceUtility")
    catch
    endtry
    
    try
        local lcMacro
        if empty(this.cPath)
            this.cPath = addbs(fullpath(curdir()))
        endif

        lcMacro = "set path to [" + this.cPath + "] additive"
        &lcMacro
        lcMacro = "set default to " + this.cPath
        &lcMacro
        this.LoadJsonFox()
    catch to loEx
        local lcError, lcFile
        lcError = textmerge("<>: Error in Initialize: <> line: <>")
        lcFile = this.cPath + "ApiControllerError.txt"
        strtofile(lcError + chr(13) + chr(10), lcFile, 1)
    endtry
endproc

GetGlobalResource

Obtiene un recurso global compartido a través del ciclo de vida de la aplicación. Permite acceso a recursos centralizados como conexiones de base de datos, configuraciones globales, etc.

FUNCTION GetGlobalResource(tcResourceName as string) AS Variant
Parámetros:
  • tcResourceName (Character): Nombre del recurso a obtener
Retorna:

Variant: El recurso solicitado o NULL si no existe

Ejemplo:
* Obtener una conexión de base de datos global
    loConnection = this.GetGlobalResource("DatabaseConnection")
    IF !ISNULL(loConnection)
        * Usar la conexión...
    ENDIF

    * Obtener configuración global
    loConfig = this.GetGlobalResource("AppConfig")
    IF !ISNULL(loConfig)
        lcServerUrl = loConfig.ServerUrl
    ENDIF

SetWorkingDirectory

Establece el directorio de trabajo para el controlador API y llama a Initialize para configurar el entorno.

PROCEDURE SetWorkingDirectory(tcWorkingDirectory as string)
Parámetros:
  • tcWorkingDirectory (Character): Ruta del directorio de trabajo
Ejemplo:
* Configurar directorio de trabajo para el controlador
loController.SetWorkingDirectory("C:\MiAplicacion\api\")

LoadJsonFox

Carga la biblioteca JSONFox para el procesamiento de datos JSON si aún no está disponible.

PROCEDURE LoadJsonFox
Implementación:
procedure LoadJsonFox
    try
        if type('_screen.json') != 'O' or vartype(_screen.json) == 'X'
            local lcJsonApp
            lcJsonApp = this.cPath + "JSONFox.app"
            if file(lcJsonApp)
                do (lcJsonApp)
                _screen.json.lShowErrors = .f.
            else
                this.logerror("JSONfox.app not found: " + lcJsonApp)
            endif
        endif
    catch to loEx
        this.LogFromException(loEx)
    endtry
endproc

Ciclo de vida de los endpoints

BeforeEndpoint

Método hook que se ejecuta antes de cada endpoint. Puede ser sobrescrito en clases derivadas para implementar lógica común como autenticación, validación o registro.

FUNCTION BeforeEndpoint(req, res) AS BOOLEAN
Parámetros:
  • req (FoxServerRequest): Objeto de solicitud HTTP
  • res (FoxServerResponse): Objeto de respuesta HTTP
Retorna:

Logical: .T. para continuar con el endpoint, .F. para detener el procesamiento

Ejemplo de implementación en clase derivada:
* En una clase que hereda de ApiController
FUNCTION BeforeEndpoint(req, res) AS BOOLEAN
    * Verificar autenticación
    lcTabla = THIS.cPath + "usuarios.dbf"
    IF !FILE(lcTabla)
        res.status(500).json('{"error":"La tabla de usuarios no existe"}')
        RETURN .F.  && Detener procesamiento
    ENDIF
    
    * Registrar inicio de solicitud
    this.LogInfo("Procesando solicitud: " + req.method + " " + req.uri.AbsolutePath)
    
    RETURN .T.  && Continuar con el endpoint
ENDFUNC

AfterEndpoint

Método hook que se ejecuta después de cada endpoint. Puede ser sobrescrito en clases derivadas para implementar lógica común como limpieza de recursos, registro o métricas.

PROCEDURE AfterEndpoint(req, res)
Parámetros:
  • req (FoxServerRequest): Objeto de solicitud HTTP
  • res (FoxServerResponse): Objeto de respuesta HTTP
Ejemplo de implementación en clase derivada:
* En una clase que hereda de ApiController
PROCEDURE AfterEndpoint(req, res)
    * Registrar finalización de solicitud
    this.LogInfo("Solicitud completada: " + req.method + " " + req.uri.AbsolutePath + " - Status: " + TRANSFORM(res.statusCode))
    
    * Liberar recursos si es necesario
    IF USED("tmpData")
        USE IN tmpData
    ENDIF
ENDPROC

Manejo de Logging

SetLogging / SetLogFile / SetCorrelationId

Métodos para configurar el sistema de registro de eventos y errores.

NOTA: Estos métodos son invocados por FoxServer por lo tanto no hay que intentar modificarlos.
PROCEDURE SetLogging(tbEnabled as Boolean)
PROCEDURE SetLogFile(tcLogFile as string)
PROCEDURE SetCorrelationId(tcCorrelationId as string)
Parámetros:
  • tbEnabled (Logical): Habilita (.T.) o deshabilita (.F.) el registro
  • tcLogFile (Character): Ruta completa del archivo de log
  • tcCorrelationId (Character): Identificador único para rastrear solicitudes
Ejemplo:
* Configurar logging para un controlador
loController.SetLogging(.T.)
loController.SetLogFile("C:\logs\api_" + DTOS(DATE()) + ".log")
loController.SetCorrelationId(SYS(2015))

* En un endpoint
FUNCTION GetUsuario(req, res)
    this.LogInfo("Obteniendo usuario con ID: " + req.GetParam("id"))
    * Resto del código...
ENDFUNC

LogInfo / LogError / LogFromException

Métodos para registrar diferentes tipos de mensajes en el log.

PROCEDURE LogInfo(tcMessage)
PROCEDURE LogError(tcMessage)
PROCEDURE LogFromException(toException)
Parámetros:
  • tcMessage (Character): Mensaje a registrar
  • toException (Exception): Objeto de excepción para registrar detalles
Ejemplo:
* Registrar información
this.LogInfo("Procesando solicitud para el usuario: " + lcUserId)

* Registrar error
this.LogError("No se pudo conectar a la base de datos")

* Registrar excepción
TRY
    * Código que podría fallar
CATCH TO loEx
    this.LogFromException(loEx)
    res.status(500).json('{"error":"Error interno del servidor"}')
ENDTRY

Manejo de JSON

ToJson

Convierte un objeto VFP a formato JSON utilizando JSONFox.

FUNCTION ToJson(toObj as object) AS memo
Parámetros:
  • toObj (Object): Objeto a convertir a JSON
Retorna:

Character (Memo): Cadena JSON

Ejemplo:
* Crear un objeto y convertirlo a JSON
loUsuario = this.NewObject("id,nombre,email,activo")
loUsuario.id = 1
loUsuario.nombre = "Juan Pérez"
loUsuario.email = "juan@example.com"
loUsuario.activo = .T.

* Convertir a JSON
lcJson = this.ToJson(loUsuario)
* Resultado: {"id":1,"nombre":"Juan Pérez","email":"juan@example.com","activo":true}

ParseJson

Convierte una cadena JSON a un objeto VFP utilizando JSONFox.

FUNCTION ParseJson(tcJsonStr as memo) AS object
Parámetros:
  • tcJsonStr (Character/Memo): Cadena JSON a convertir
Retorna:

Object: Objeto VFP con las propiedades definidas en el JSON

Ejemplo:
* Procesar JSON recibido en una solicitud
lcJson = req.GetRawBody()
loData = this.ParseJson(lcJson)

IF !ISNULL(loData)
    ? loData.nombre  && "Juan Pérez"
    ? loData.email   && "juan@example.com"
    ? loData.activo  && .T.
ENDIF

NewObject

Crea un nuevo objeto con propiedades específicas.

FUNCTION NewObject(tcSchema as memo) AS object
Parámetros:
  • tcSchema (Character/Memo): Lista de propiedades separadas por comas
Retorna:

Object: Objeto con las propiedades especificadas inicializadas a NULL

Ejemplo:
* Crear un objeto para respuesta de API
loRespuesta = this.NewObject("status,data,message,timestamp")
loRespuesta.status = "success"
loRespuesta.data = loResultados
loRespuesta.message = "Operación completada correctamente"
loRespuesta.timestamp = DATETIME()

* Enviar como JSON
res.json(this.ToJson(loRespuesta))

NewObjectFromCursor

Crea un nuevo objeto VFP con la estructura de un cursor, útil para crear plantillas de objetos basadas en estructuras de tabla.

FUNCTION NewObjectFromCursor(tcCursor as string, tnSession as integer) AS Object
Parámetros:
  • tcCursor (Character): Nombre del cursor base
  • tnSession (Integer): ID de sesión de datos donde se encuentra el cursor
Retorna:

Object: Objeto con las propiedades correspondientes a los campos del cursor

Ejemplo:
* Crear un objeto plantilla basado en la estructura de una tabla
USE usuarios IN 0
loTemplate = this.NewObjectFromCursor("usuarios", THIS.DataSessionId)

* El objeto tendrá las propiedades: id, nombre, email, etc.
* Útil para validaciones o crear objetos de respuesta consistentes

Conversión de datos a JSON

TableToJson

Convierte un cursor VFP a formato JSON. Puede convertir todo el cursor o solo el registro actual.

PROCEDURE TableToJson(tcCursor as string, tbCurrentRow as logical) AS memo
Parámetros:
  • tcCursor (Character): Nombre del cursor a convertir
  • tbCurrentRow (Logical): Si es .T., convierte solo el registro actual; si es .F., convierte todos los registros
Retorna:

Character (Memo): Cadena JSON con los datos del cursor

Ejemplo:
* Consultar datos
SELECT id, nombre, email, fecha_registro FROM usuarios WHERE activo = .T. INTO CURSOR curUsuarios

* Convertir todos los registros a JSON (como array)
lcJsonArray = this.TableToJson("curUsuarios", .F.)
* Resultado: [{"id":1,"nombre":"Juan","email":"juan@example.com","fecha_registro":"2023-05-15"},...]

* Convertir solo el registro actual (como objeto)
SELECT curUsuarios
LOCATE FOR id = 5
lcJsonObject = this.TableToJson("curUsuarios", .T.)
* Resultado: {"id":5,"nombre":"Ana","email":"ana@example.com","fecha_registro":"2023-06-20"}

TableToJsonObject

Convierte un cursor VFP a una colección de objetos VFP en lugar de una cadena JSON. Útil cuando necesitas manipular los datos antes de la serialización final.

FUNCTION TableToJsonObject(tcCursor as string, tbCurrentRow as logical, tnSession as integer) AS Collection
Parámetros:
  • tcCursor (Character): Nombre del cursor a convertir
  • tbCurrentRow (Logical): Si es .T., convierte solo el registro actual; si es .F., convierte todos los registros
  • tnSession (Integer): ID de sesión de datos donde se encuentra el cursor
Retorna:

Collection: Colección VFP con objetos que representan cada registro

Ejemplo:
* Obtener datos como colección de objetos
SELECT id, nombre, email FROM usuarios INTO CURSOR curUsuarios
loCollection = this.TableToJsonObject("curUsuarios", .F., THIS.DataSessionId)

* Manipular los objetos antes de serializar
FOR EACH loUsuario IN loCollection
    * Agregar propiedades calculadas
    ADDPROPERTY(loUsuario, "display_name", loUsuario.nombre + " (" + loUsuario.email + ")")
ENDFOR

* Serializar la colección modificada
lcJson = this.ToJson(loCollection)

MasterToJSON

Convierte una relación maestro-detalle entre dos cursores a formato JSON. Útil para generar estructuras JSON jerárquicas a partir de datos relacionados.

FUNCTION MasterToJSON(tcMaster as string, tcDetail as string, tcExpr as string, tcDetailAttribute as string) AS memo
Parámetros:
  • tcMaster (Character): Nombre del cursor maestro
  • tcDetail (Character): Nombre del cursor detalle
  • tcExpr (Character): Expresión para relacionar los cursores (ej: "master.id = detail.master_id")
  • tcDetailAttribute (Character): Nombre del atributo en el JSON donde se incluirán los detalles
Retorna:

Character (Memo): Cadena JSON con la estructura maestro-detalle

Ejemplo:
* Preparar cursores
SELECT id, nombre, fecha FROM facturas INTO CURSOR curFacturas
SELECT id, factura_id, producto, cantidad, precio FROM items INTO CURSOR curItems

* Convertir a JSON con estructura jerárquica
lcJson = this.MasterToJSON("curFacturas", "curItems", "curFacturas.id = curItems.factura_id", "items")

* Resultado:
* [
*   {
*     "id": 1,
*     "nombre": "Factura A-001",
*     "fecha": "2023-05-20",
*     "items": [
*       {"id": 101, "factura_id": 1, "producto": "Teclado", "cantidad": 2, "precio": 25.99},
*       {"id": 102, "factura_id": 1, "producto": "Mouse", "cantidad": 1, "precio": 15.50}
*     ]
*   },
*   ...
* ]

Utilidades de Texto y Formato

Format

Formatea una cadena de texto reemplazando marcadores de posición {n} con valores. Similar a String.Format en C# o format() en Python.

FUNCTION Format(tcSource as string, p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, p10) AS string
Parámetros:
  • tcSource (Character): Cadena de formato con marcadores {0}, {1}, etc.
  • p0...p10 (Variant): Valores a insertar en los marcadores
Retorna:

Character: Cadena formateada con los valores reemplazados

Ejemplo:
* Formatear un mensaje con valores
lcMensaje = this.Format("Usuario {0} realizó {1} en {2}", lcNombre, lcAccion, TTOC(DATETIME()))
* Resultado: "Usuario Juan realizó login en 2023-05-20 15:30:45"

* Formatear una URL
lcUrl = this.Format("/api/usuarios/{0}/pedidos/{1}", lcUserId, lcPedidoId)
* Resultado: "/api/usuarios/123/pedidos/456"

Content

Procesa contenido automáticamente según el formato solicitado por el cliente (JSON, XML, CSV). Detecta el formato preferido a través de headers Accept o parámetros de consulta.

FUNCTION Content(req as FoxServerRequest, tvSource as variant) AS memo
Parámetros:
  • req (FoxServerRequest): Objeto de solicitud para detectar formato preferido
  • tvSource (Variant): Datos a procesar (cursor, objeto, cadena)
Retorna:

Memo: Contenido formateado según el tipo solicitado

Ejemplo:
* El cliente puede solicitar diferentes formatos:
* GET /api/usuarios?format=json
* GET /api/usuarios?format=xml  
* GET /api/usuarios?format=csv
* O usar headers: Accept: application/xml

FUNCTION GetUsuarios(req, res) HELP "GET: usuarios"
    SELECT * FROM usuarios INTO CURSOR curUsuarios
    
    * Content() detecta automáticamente el formato y convierte
    lcContent = this.Content(req, "curUsuarios")
    res.status(200).send(lcContent)
ENDFUNC

TableToXml / TableToCsv

Métodos especializados para convertir cursores a formatos XML y CSV respectivamente.

FUNCTION TableToXml(tcCursor as string, tbCurrentRow as logical) AS memo
FUNCTION TableToCsv(tcCursor as string, tbCurrentRow as logical) AS memo
Parámetros:
  • tcCursor (Character): Nombre del cursor a convertir
  • tbCurrentRow (Logical): Si es .T., convierte solo el registro actual
Ejemplo:
* Generar XML de datos
SELECT * FROM productos INTO CURSOR curProductos
lcXml = this.TableToXml("curProductos", .F.)

* Generar CSV para exportación
lcCsv = this.TableToCsv("curProductos", .F.)
res.header("Content-Disposition", "attachment; filename=productos.csv").send(lcCsv)

EscapeCharsToJSON / EscapeCharsToHTML

Métodos para escapar caracteres especiales para su uso en JSON o HTML. Útil para manejar correctamente caracteres internacionales y especiales.

FUNCTION EscapeCharsToJSON(tcStream as string) AS string
FUNCTION EscapeCharsToHTML(tcStream as string) AS string
Parámetros:
  • tcStream (Character): Texto a escapar
Retorna:

Character: Texto con caracteres especiales escapados

Ejemplo:
* Escapar caracteres para JSON
lcTexto = "Café con leche & más"
lcEscapado = this.EscapeCharsToJSON(lcTexto)
* Resultado: "Caf\u00e9 con leche \u0026 m\u00e1s"

* Escapar caracteres para HTML
lcTexto = "Sección de información"
lcEscapado = this.EscapeCharsToHTML(lcTexto)
* Resultado: "Sección de información"

Utilidades de Fecha y Tiempo

DTOUNX / UNXTOD

Métodos para convertir entre fechas VFP y timestamps UNIX. Útil para interactuar con APIs que utilizan timestamps UNIX.

FUNCTION DTOUNX(tdFecha as datetime, tbUseMiliseconds as logical) AS number
FUNCTION UNXTOD(tnUnixTimestamp as number) AS datetime
Parámetros:
  • tdFecha (DateTime): Fecha y hora de VFP a convertir
  • tbUseMiliseconds (Logical): Si es .T., devuelve milisegundos; si es .F., devuelve segundos
  • tnUnixTimestamp (Number): Timestamp UNIX en segundos o milisegundos
Retorna:

Number/DateTime: Timestamp UNIX o fecha VFP según el método

Ejemplo:
* Convertir fecha VFP a timestamp UNIX
ldFecha = DATETIME()
lnTimestamp = this.DTOUNX(ldFecha, .F.)  && En segundos
lnTimestampMs = this.DTOUNX(ldFecha, .T.)  && En milisegundos

* Convertir timestamp UNIX a fecha VFP
ldFecha1 = this.UNXTOD(1621500000)  && Desde segundos
ldFecha2 = this.UNXTOD(1621500000000)  && Desde milisegundos

Utilidades de Codificación

Base64URLEncode / Base64URLDecode

Métodos para codificar y decodificar cadenas en formato Base64URL. Útil para trabajar con tokens JWT y URLs seguras.

FUNCTION Base64URLEncode(tcInput as string) AS string
FUNCTION Base64URLDecode(tcInput as string) AS string
Parámetros:
  • tcInput (Character): Texto a codificar o decodificar
Retorna:

Character: Texto codificado o decodificado

Ejemplo:
* Codificar payload JWT
lcPayload = '{"sub":"123","name":"John Doe","role":"admin"}'
lcEncodedPayload = this.Base64URLEncode(lcPayload)
* Resultado: "eyJzdWIiOiIxMjMiLCJuYW1lIjoiSm9obiBEb2UiLCJyb2xlIjoiYWRtaW4ifQ"

* Decodificar token
lcDecodedPayload = this.Base64URLDecode(lcEncodedPayload)
* Resultado: '{"sub":"123","name":"John Doe","role":"admin"}'

NewGuid

Genera un nuevo identificador único global (GUID). Útil para crear identificadores únicos para recursos, correlación de logs, etc.

FUNCTION NewGuid() AS string
Retorna:

Character: GUID en formato de cadena sin llaves

Ejemplo:
* Generar un GUID para un nuevo recurso
lcId = this.NewGuid()
* Resultado: "a1b2c3d4-e5f6-47g8-h9i0-j1k2l3m4n5o6"

* Usar como correlación para logs
this.SetCorrelationId(this.NewGuid())

Gestión de Recursos

CleanUp / Release / Dispose

Métodos para liberar recursos y limpiar el controlador cuando ya no se necesita. Ayudan a prevenir fugas de memoria y asegurar una correcta liberación de recursos.

Nota: Estos métodos son invocados automáticamente por FoxServer al finalizar el ciclo de vida del controlador. Se recomienda implementar la lógica de limpieza de recursos en estos métodos.
PROCEDURE CleanUp
PROCEDURE Release
PROCEDURE Dispose
Implementación:
procedure CleanUp
    try
        if type('this.Logger') == 'O' and vartype(this.Logger) == 'O'
            this.Logger = null
        endif
    catch
    endtry
endproc

procedure release
    this.CleanUp()
endproc

procedure Dispose
    this.CleanUp()
endproc

Ejemplos de Implementación

Ejemplo 1: Creación de un controlador básico

DEFINE CLASS UsuariosController AS ApiController
    * Implementación de métodos hooks
    FUNCTION BeforeEndpoint(req, res) AS BOOLEAN
        * Verificar si la tabla de usuarios existe
        LOCAL lcTabla
        lcTabla = THIS.cPath + "usuarios.dbf"
        IF !FILE(lcTabla)
            res.status(500).json('{"error":"La tabla de usuarios no existe"}')
            RETURN .F.  && Detener procesamiento
        ENDIF        
        
        * Registrar inicio de solicitud
        this.LogInfo(this.Format("Solicitud iniciada: {0} {1}", req.Method, req.Uri.AbsolutePath))
        RETURN .T.
    ENDFUNC
    
    PROCEDURE AfterEndpoint(req, res)
        * Registrar finalización
        this.LogInfo(this.Format("Solicitud completada: {0} {1} - Status: {2}", ;
            req.GetMethod(), req.Uri.AbsolutePath, TRANSFORM(res.GetStatusCode())))
                
        CLOSE DATABASES ALL
    ENDPROC

    * Endpoint para obtener lista de usuarios
    PROCEDURE GetUsuarios(req, res) HELP "GET: usuarios public"
        * Al usar "public" se indica que este endpoint es accesible sin autenticación
        LOCAL lcFiltro, lcOrden
        
        * Obtener parámetros de consulta
        lcFiltro = req.GetQuery("filtro", "")
        lcOrden = req.GetQuery("orden", "nombre")
        
        * Registrar información
        this.LogInfo(this.Format("Obteniendo usuarios con filtro: {0}, orden: {1}", lcFiltro, lcOrden))
        
        * Ejecutar consulta
        IF !EMPTY(lcFiltro)
            SELECT id, nombre, email, activo FROM usuarios ;
                WHERE UPPER(nombre) LIKE UPPER('%' + lcFiltro + '%') ;
                ORDER BY &lcOrden ;
                INTO CURSOR curUsuarios
        ELSE
            SELECT id, nombre, email, activo FROM usuarios ;
                ORDER BY &lcOrden ;
                INTO CURSOR curUsuarios
        ENDIF
                
        * Convertir a JSON y responder
        lcJson = this.TableToJson("curUsuarios", .F.)        
        res.status(200).json(lcJson)
    ENDPROC
    
    * Endpoint para obtener un usuario específico
    PROCEDURE GetUsuario(req, res) HELP "GET: usuarios/{id} public"
        * Al usar "public" se indica que este endpoint es accesible sin autenticación
        LOCAL lcId, lcJson
        
        * Obtener ID del usuario
        lcId = req.GetParam("id", "0")
        
        * Validar ID
        IF !ISDIGIT(lcId) OR VAL(lcId) <= 0
            res.status(400).json('{"error":"ID de usuario inválido"}')
            RETURN
        ENDIF
        
        * Buscar usuario
        SELECT id, nombre, email, telefono, direccion, activo ;
            FROM usuarios WHERE id = VAL(lcId) ;
            INTO CURSOR curUsuario
        
        * Verificar si existe
        IF RECCOUNT("curUsuario") = 0
            res.status(404).json('{"error":"Usuario no encontrado"}')
            RETURN
        ENDIF
        
        * Convertir a JSON y responder
        lcJson = this.TableToJson("curUsuario", .T.)
        res.status(200).json(lcJson)
    ENDPROC
    
    * Endpoint para crear un nuevo usuario
    PROCEDURE CreateUsuario(req, res) HELP "POST: usuarios public"
        * Al usar "public" se indica que este endpoint es accesible sin autenticación
        LOCAL loData, lcNombre, lcEmail, lcJson
        
        * Obtener datos del cuerpo de la solicitud
        loData = this.ParseJson(req.GetRawBody())
        
        * Validar datos requeridos
        IF ISNULL(loData) OR EMPTY(loData.nombre) OR EMPTY(loData.email)
            res.status(400).json('{"error":"Nombre y email son obligatorios"}')
            RETURN
        ENDIF
        
        * Verificar si el email ya existe
        SELECT COUNT(*) as total FROM usuarios WHERE email = loData.email INTO CURSOR curCheck
        IF curCheck.total > 0
            res.status(409).json('{"error":"El email ya está registrado"}')
            RETURN
        ENDIF
        
        * Insertar nuevo usuario
        lcNombre = loData.email
        lcEmail = loData.email
        
        INSERT INTO usuarios (nombre, email, activo, fecha_registro) ;
            VALUES (lcNombre, lcEmail, .T., DATETIME())
        
        * Obtener ID generado
        lnId = _tally
        
        * Crear objeto de respuesta
        loRespuesta = this.NewObject("id,mensaje")
        loRespuesta.id = lnId
        loRespuesta.mensaje = "Usuario creado correctamente"
        
        * Responder
        res.status(201).json(this.ToJson(loRespuesta))
    ENDPROC
ENDDEFINE

Consejos de uso

Buenas prácticas

  • Organización del código: Divida su API en controladores lógicos por funcionalidad o recurso.
  • Validación de entradas: Siempre valide los parámetros de entrada para evitar errores y vulnerabilidades:
    * Validar ID numérico
    lcId = req.GetParam("id", "0")
    IF !ISDIGIT(lcId) OR VAL(lcId) <= 0
        res.status(400).json('{"error":"ID inválido"}')
        RETURN
    ENDIF
  • Manejo de errores: Use bloques TRY-CATCH para capturar y manejar excepciones:
    TRY
        * Código que podría fallar
        loResultado = this.ProcesarDatos(req.json)
        res.status(200).json(this.ToJson(loResultado))
        EXIT && en los bloques TRY/CATCH no se puede usar RETURN
    CATCH TO loEx
        this.LogFromException(loEx)
        loRespuesta = this.NewObject("status,message")
        loRespuesta.status = "error"
        loRespuesta.message = "Error procesando solicitud: " + loEx.Message
        * Serializamos la respuesta por si loEx.Message contiene caracteres especiales
        res.status(500).json(this.ToJson(loRespuesta))
    ENDTRY
  • Uso de hooks: Aproveche los métodos BeforeEndpoint y AfterEndpoint para implementar lógica común:
    * En BeforeEndpoint: validación de autenticación, registro, etc.
    * En AfterEndpoint: limpieza de recursos, métricas, etc.
  • Respuestas consistentes: Mantenga un formato de respuesta consistente en toda su API:
    * Estructura de respuesta estándar
    loRespuesta = this.NewObject("status,data,message")
    loRespuesta.status = "success"  * o "error"
    loRespuesta.data = loResultados
    loRespuesta.message = "Operación completada"
  • Logging efectivo: Utilice el sistema de logging para facilitar la depuración:
    * Incluir información relevante en los logs
    this.LogInfo(this.Format("Usuario {0} solicitó recurso {1}", lcUserId, lcRecursoId))
  • Liberación de recursos: Asegúrese de cerrar cursores y liberar objetos cuando termine:
    * En AfterEndpoint o al final del procesamiento
    IF USED("curDatos")
        USE IN curDatos
    ENDIF

Objetos Request y Response

FoxServerRequest

El objeto FoxServerRequest encapsula la solicitud HTTP entrante y proporciona acceso a sus datos. Este objeto es pasado a cada manejador de ruta y contiene toda la información necesaria sobre la petición del cliente.

Propiedad Tipo Descripción
Method Character Método HTTP utilizado en la solicitud (GET, POST, PUT, DELETE, etc.)
Uri Object Objeto con información detallada de la URI, incluyendo Scheme, Host, Port, AbsolutePath, PathAndQuery y AbsoluteUri
Headers FoxServerTupleDictionary Colección de encabezados HTTP recibidos en la solicitud
Params Object Parámetros extraídos de la ruta URL (p.ej. /usuarios/:id donde :id es un parámetro)
Query Object Parámetros de consulta extraídos de la URL (p.ej. ?nombre=Juan&edad=30)
Json Object Cuerpo de la solicitud parseado como JSON (disponible cuando Content-Type es application/json)
RawBody Character (Memo) Cuerpo de la solicitud sin procesar, útil cuando se necesita acceso al contenido original
MultipartData FoxServerMultipartData Colección de archivos subidos mediante formularios multipart/form-data
FormFields FoxServerMultipartData Campos de formulario enviados en solicitudes multipart/form-data o application/x-www-form-urlencoded
AuthenticatedUser Object Información del usuario autenticado, con propiedades id, name y role
ContentType Character Tipo de contenido de la solicitud (p.ej. application/json, multipart/form-data)
cPath Character Ruta base del servicio (p.ej. c:\myService\)
Logger FoxServerLogger Objeto para registrar eventos y errores durante el procesamiento de la solicitud

Métodos para acceder a datos de la solicitud

GetHeader

Obtiene el valor de un encabezado HTTP específico de la solicitud. Si el encabezado no existe, retorna un valor por defecto.

FUNCTION GetHeader(tcKey as string, tvDefault as variant) AS variant
Parámetros:
  • tcKey (Character): Nombre del encabezado a obtener
  • tvDefault (Variant, opcional): Valor por defecto si el encabezado no existe (por defecto: '')
Ejemplo:
* Obtener token de autorización
lcToken = req.GetHeader("Authorization", "")
IF !EMPTY(lcToken)
    * Procesar token...
ENDIF

* Verificar tipo de contenido
lcContentType = req.GetHeader("Content-Type", "")
IF "application/json" $ lcContentType
    * Procesar como JSON...
ENDIF

GetParam / GetQuery

Obtienen valores de los parámetros de ruta o de consulta respectivamente.

FUNCTION GetParam(tcProp as string, tvDefault as variant) AS variant
FUNCTION GetQuery(tcProp as string, tvDefault as variant) AS variant
Parámetros:
  • tcProp (Character): Nombre del parámetro a obtener
  • tvDefault (Variant, opcional): Valor por defecto si el parámetro no existe (por defecto: '')
Ejemplo:
* Para una ruta como /usuarios/:id
lcUserId = req.GetParam("id", "0")

* Para una URL como /productos?categoria=electronica&orden=precio
lcCategoria = req.GetQuery("categoria", "todas")
lcOrden = req.GetQuery("orden", "nombre")

GetQueryParams

Obtiene todos los parámetros de consulta como un objeto FoxServerTupleDictionary. Útil para iterar sobre todos los parámetros de consulta.

FUNCTION GetQueryParams() AS FoxServerTupleDictionary
Retorna:

FoxServerTupleDictionary: Diccionario con todos los parámetros de consulta

Ejemplo:
* Iterar sobre todos los parámetros de consulta
loParams = req.GetQueryParams()
FOR lnI = 1 TO loParams.Count
    lcKey = loParams.GetKey(lnI)
    lcValue = loParams.GetValue(lnI)
    THIS.LogInfo("Parámetro: {0} = {1}", lcKey, lcValue)
ENDFOR

HasJsonBody

Verifica si la solicitud contiene un cuerpo JSON válido y procesado.

FUNCTION HasJsonBody() AS Boolean
Retorna:

Logical: .T. si hay un cuerpo JSON válido, .F. en caso contrario

Ejemplo:
* Validar que hay datos JSON antes de procesarlos
IF req.HasJsonBody()
    lcNombre = req.json.nombre
    lcEmail = req.json.email
ELSE
    res.status(400).json('{"error":"Se requiere un cuerpo JSON válido"}')
    RETURN
ENDIF

GetFormField / HasFormField

Obtiene el valor de un campo de formulario o verifica su existencia.

FUNCTION GetFormField(tcKey as string, tvDefault as variant) AS variant
FUNCTION HasFormField(tcKey as string) AS Boolean
Parámetros:
  • tcKey (Character): Nombre del campo de formulario
  • tvDefault (Variant, opcional): Valor por defecto si el campo no existe (por defecto: '')
Ejemplo:
* Procesar un formulario de registro
IF req.HasFormField("email") AND req.HasFormField("password")
    lcEmail = req.GetFormField("email")
    lcPassword = req.GetFormField("password")
    lcNombre = req.GetFormField("nombre", "Usuario")
    
    * Registrar usuario...
ELSE
    * Campos requeridos faltantes...
ENDIF

File / Files (GetFile / GetFiles)

Obtiene uno o varios archivos cargados desde un formulario multipart. Estos métodos son alias simplificados de GetMultipartFile y GetMultipartFiles.

* Métodos simplificados (recomendados)
FUNCTION File(tcKey as string) AS Object
FUNCTION Files(tcKey as string) AS Object

* Métodos tradicionales
FUNCTION GetFile(tcKey as string) AS Object
FUNCTION GetFiles(tcKey as string) AS Object

* Métodos originales
FUNCTION GetMultipartFile(tcKey as string, tvDefault as variant) AS variant
FUNCTION GetMultipartFiles(tcKey as string, tvDefault as variant) AS variant
Parámetros:
  • tcKey (Character): Nombre del campo de archivo en el formulario
  • tvDefault (Variant, opcional): Valor por defecto si no hay archivos (por defecto: NULL)
Retorna:

Object: Un objeto de archivo individual (File/GetFile) o una colección de objetos de archivo (Files/GetFiles)

Ejemplos:
* Obtener un solo archivo
loFile = req.File("avatar")
IF !ISNULL(loFile)
    lcDestino = req.cPath + "uploads\" + loFile.FileName
    loFile.SaveToFile(lcDestino)
    This.LogInfo("Archivo guardado en:{0} ", lcDestino)
ENDIF

* Obtener múltiples archivos
loFiles = req.Files("documentos")
IF !ISNULL(loFiles)
    FOR EACH loKey IN loFiles.Keys
        loFile = loFiles.GetValue(loKey)
        lcDestino = req.cPath + "uploads\" + loFile.FileName
        loFile.SaveToFile(lcDestino)
        this.LogInfo("Archivo guardado: {0}", loFile.FileName)
    ENDFOR
ENDIF

GetRawBody

Obtiene el cuerpo de la solicitud sin procesar como una cadena de texto.

FUNCTION GetRawBody() AS string
Ejemplo:
* Acceder al cuerpo raw para procesamiento personalizado
lcBody = req.GetRawBody()
IF !EMPTY(lcBody)
    * Procesar el cuerpo según sea necesario
    IF req.ContentType == "application/xml"
        * Procesar XML...
    ENDIF
ENDIF

SetAuthenticatedUser

Establece la información del usuario autenticado para la solicitud actual. Útil en middleware de autenticación.

Nota: Este método es invocado automáticamente por FoxServer cuando se autentica un usuario. No se debe modificar su implementación.

PROCEDURE SetAuthenticatedUser(tcId as string, tcName as string, tcRole as string) AS Void
Parámetros:
  • tcId (Character): Identificador único del usuario
  • tcName (Character): Nombre del usuario
  • tcRole (Character): Rol o nivel de acceso del usuario

SetLogging / SetLogFile / SetCorrelationId

Métodos para configurar el sistema de registro de la solicitud.

Nota: Estos métodos son invocados automáticamente por FoxServer al iniciar una solicitud. No se debe modificar su implementación.
PROCEDURE SetLogging(tbEnabled as Boolean)
PROCEDURE SetLogFile(tcLogFile as string)
PROCEDURE SetCorrelationId(tcCorrelationId as string)

FoxServerResponse

El objeto FoxServerResponse encapsula la respuesta HTTP saliente y proporciona métodos para configurarla. Este objeto permite establecer el código de estado, encabezados y el cuerpo de la respuesta, así como enviar diferentes tipos de contenido al cliente.

Propiedad Tipo Descripción
StatusCode Integer Código de estado HTTP de la respuesta (por defecto: 200)
Headers FoxServerTupleDictionary Colección de encabezados HTTP a enviar en la respuesta
Body Character (Memo) Contenido del cuerpo de la respuesta
ContentType Character Tipo de contenido de la respuesta (p.ej. application/json, text/html)
ContentDisposition Character Disposición del contenido para descargas de archivos
ResponseLocation Character URL para redirecciones (encabezado Location)
FilePath Character Ruta del archivo a enviar (cuando IsFile es .T.)
IsFile Logical Indica si la respuesta es un archivo (.T.) o contenido en memoria (.F.)
Logger FoxServerLogger Objeto para registrar eventos y errores durante la generación de la respuesta

Status

Establece el código de estado HTTP de la respuesta.

FUNCTION Status(tnStatusCode as integer) AS FoxServerResponse
Parámetros:
  • tnStatusCode (Integer): Código de estado HTTP (p.ej. 200, 201, 400, 404, 500)
Retorna:

Object: La instancia de FoxServerResponse (para encadenamiento de métodos)

Ejemplos:
* Respuesta exitosa
res.status(200).json('{"mensaje":"Operación completada"}')

* Recurso creado
res.status(201).json('{"id":123,"mensaje":"Recurso creado"}')

* Error de validación
res.status(400).json('{"error":"Datos inválidos"}')

* Recurso no encontrado
res.status(404).json('{"error":"El recurso solicitado no existe"}')

* Error interno
res.status(500).json('{"error":"Error interno del servidor"}')

Header

Establece un encabezado HTTP en la respuesta.

FUNCTION Header(tcKey as string, tvValue as variant) AS FoxServerResponse
Parámetros:
  • tcKey (Character): Nombre del encabezado
  • tvValue (Variant): Valor del encabezado
Retorna:

Object: La instancia de FoxServerResponse (para encadenamiento)

Ejemplos:
* Establecer encabezados personalizados
res.header("Content-Type", "application/json");
   .header("X-API-Version", "1.0");
   .header("Cache-Control", "no-cache");
   .json('{"data":"ejemplo"}')

Json

Establece el cuerpo de la respuesta como JSON y configura el Content-Type adecuado.

FUNCTION Json(tcContent as memo) AS FoxServerResponse
Parámetros:
  • tcContent (Character/Memo): Cadena JSON a enviar como respuesta
Retorna:

Object: La instancia de FoxServerResponse (para encadenamiento)

Ejemplos:
* Respuesta JSON simple
res.status(200).json('{"status":"success","message":"Operación completada"}')

* Respuesta JSON con datos complejos
LOCAL loRes, loData
loData = THIS.NewObject("id,nombre,email")
loData.id = 123
loData.nombre = "Juan Pérez"
loData.email = "juanperez@gmail.com"

loRes = THIS.NewObject("status,data,message,timestamp")
loRes.status = "success"
loRes.data = loData
loRes.message = "Datos obtenidos correctamente"
loRes.timestamp = DATETIME()

res.status(200).json(THIS.toJson(loRes))

Send

Envía una respuesta con el contenido especificado, detectando automáticamente el tipo de contenido.

FUNCTION Send(tcContent as memo, tcDisposition as string) AS FoxServerResponse
Parámetros:
  • tcContent (Character/Memo): Contenido a enviar o ruta de archivo
  • tcDisposition (Character, opcional): Tipo de disposición para archivos ("inline" o "attachment")
Retorna:

Object: La instancia de FoxServerResponse (para encadenamiento)

Ejemplos:
* Enviar texto plano
res.status(200).send("Operación completada correctamente")

* Enviar HTML
lcHtml = '<!DOCTYPE html><html><body><h1>Hola Mundo</h1></body></html>'
res.status(200).send(lcHtml)

* Enviar XML
lcXml = 'ok'
res.status(200).send(lcXml)

* Enviar archivo (detecta automáticamente si es una ruta de archivo)
res.status(200).send("C:\archivos\documento.pdf", "attachment")

SendFile

Envía un archivo como respuesta, configurando los encabezados adecuados para la descarga.

FUNCTION SendFile(tcFilePath as string, tcDisposition as string) AS FoxServerResponse
Parámetros:
  • tcFilePath (Character): Ruta completa del archivo a enviar
  • tcDisposition (Character): Tipo de disposición ("inline" para mostrar en navegador, "attachment" para descargar)
Retorna:

Object: La instancia de FoxServerResponse (para encadenamiento)

Ejemplos:
* Enviar un PDF para visualizar en el navegador
res.sendFile("C:\informes\reporte.pdf", "inline")

* Enviar un archivo Excel para descargar
res.sendFile("C:\datos\exportacion.xlsx", "attachment")

* Enviar una imagen
res.sendFile("C:\imagenes\foto.jpg", "inline")

* Enviar un archivo con estado personalizado
res.status(201).sendFile("C:\archivos\nuevo_documento.docx", "attachment")

Location

Establece el encabezado de redirección Location, útil para redirecciones HTTP.

FUNCTION Location(tvLocation as variant) AS FoxServerResponse
Parámetros:
  • tvLocation (Variant): URL de destino para la redirección
Retorna:

Object: La instancia de FoxServerResponse (para encadenamiento)

Ejemplos:
* Redirección temporal (302)
res.status(302).location("/login").send("")

* Redirección permanente (301)
res.status(301).location("https://nuevositio.com").send("")

* Redirección después de crear un recurso
res.status(303).location("/api/recursos/" + lcId).send("")

* Redirección con mensaje
res.status(302).location("/dashboard").send('{"message":"Redirigiendo al panel de control..."}')

HasHeader / GetHeader

Verifica la existencia de un encabezado o obtiene su valor para procesamiento interno.

FUNCTION HasHeader(tcHeader as string) AS Boolean
FUNCTION GetHeader() AS string
Ejemplo:
* Verificar si ya se ha establecido un encabezado
IF !res.hasHeader("Content-Type")
    res.header("Content-Type", "application/json")
ENDIF

* Iterar sobre todos los encabezados
res.nHeaderIndex = 1
DO WHILE res.HasNextHeader()
    lcHeader = res.GetHeader()
    ? lcHeader
ENDDO

Consejos de uso

Buenas prácticas

  • Encadenamiento de métodos: Aproveche el encadenamiento de métodos en FoxServerResponse para escribir código más conciso:
    res.status(200).header("X-Custom", "Value").json('{"status":"success"}')
  • Validación de entradas: Siempre valide los parámetros de entrada para evitar errores:
    lcId = req.getParam("id", "0")
    IF !ISDIGIT(lcId) OR VAL(lcId) <= 0
        res.status(400).json('{"error":"ID inválido"}')
        RETURN
    ENDIF
  • Manejo de errores: Use bloques TRY-CATCH para capturar y manejar excepciones:
    TRY
        * Código que podría fallar
        loResultado = this.ProcesarDatos(req.json)
        res.status(200).json(_screen.json.stringify(loResultado))
        EXIT && para salir del bloque TRY usamos EXIT en lugar de RETURN
    CATCH TO loEx
        req.LogFromException(loEx)
        res.status(500).json('{"error":"Error interno: ' + loEx.Message + '"}')
    ENDTRY
  • Logging: Utilice el sistema de logging para facilitar la depuración:
    req.LogInfo("Procesando solicitud para el usuario: " + req.AuthenticatedUser.id)
    * Código de procesamiento
    req.LogInfo("Solicitud procesada correctamente")

Clase Auth

La clase Auth es un componente especializado que hereda de ApiController y proporciona funcionalidades para la autenticación de usuarios en FoxServer. Esta clase se encarga de validar las credenciales del usuario y comunicar el resultado al middleware de autenticación mediante encabezados HTTP específicos.

Importante: La clase Auth se crea automáticamente cuando se activa la opción Autenticar con JWT al crear un nuevo proyecto FoxServer con WinFx. El encabezado X-Auth-Status es crítico para el correcto funcionamiento de la autenticación.

Estructura básica

define class Auth as ApiController olepublic
    function ValidateUser(req, res) help "POST: auth/login public"
        * implementa aquí el proceso de autenticación con tus datos.
        * implement here the authentication process with your data.
    endfunc
enddefine

Métodos principales

Método Parámetros Descripción
ValidateUser() req, res Método principal para la autenticación de usuarios. Recibe las credenciales, valida al usuario y comunica el resultado al middleware mediante el encabezado X-Auth-Status.

Ruta: POST: auth/login
Acceso: público

Flujo de autenticación

El proceso de autenticación en FoxServer sigue estos pasos:

  1. El cliente envía una solicitud POST a /auth/login con las credenciales del usuario.
  2. El método ValidateUser en FoxPro valida estas credenciales contra la base de datos.
  3. Dependiendo del resultado, se establece el encabezado X-Auth-Status a true o false.
  4. El middleware de autenticación en el servidor X# detecta este encabezado y, si es true, genera el token JWT.
  5. El token JWT se incluye en la respuesta final enviada al cliente.

Nota: El token JWT no se genera en el código FoxPro, sino en el middleware de autenticación del servidor X#.

Encabezado X-Auth-Status

Este encabezado es crucial para el funcionamiento de la autenticación:

  • X-Auth-Status: true - Indica que la autenticación fue exitosa y el middleware debe generar un token JWT.
  • X-Auth-Status: false - Indica que la autenticación falló y no debe generarse un token.

Si no se incluye el encabezado X-Auth-Status en la respuesta, el middleware devolverá un error 406 (Not Acceptable).

Formato de respuesta requerido

Para que el middleware pueda generar correctamente el token JWT, la respuesta JSON debe incluir los siguientes campos obligatorios:

{
    "status": "ok",
    "data": {
        "id": "123",      // Obligatorio: ID del usuario
        "name": "usuario", // Obligatorio: Nombre del usuario
        "role": "admin"   // Obligatorio: Rol del usuario
    },
    "message": "autenticación exitosa"
}

Implementación recomendada

A continuación se muestra una implementación completa del método ValidateUser:

procedure ValidateUser(req, res) help "POST: /auth/login public"
    local loRes, loData
    loRes = this.newObject('status,data,message')
    loData = this.newObject('id,name,role')
    try
        select users
        if seek(req.json.userName + req.json.password, 'users', 'login')
            loData.id = users.id
            loData.name = req.json.userName
            loData.Role = 'server'
            
            loRes.status = 'ok'
            loRes.data = loData
            loRes.message = 'autenticación exitosa'
            res.AddHeader('X-Auth-Status', 'true')
            res.status(200).json(this.toJson(loRes))
        else
            loRes.status = 'error'
            loRes.data = null
            loRes.message = 'el usuario y/o la contraseña no son válidos'
            res.AddHeader('X-Auth-Status', 'false')
            res.status(404).json(this.toJson(loRes))
        endif
    catch to loEx
        local lcMsg
        lcMsg = "Error en ValidateUser: " + loEx.message
        this.Logger.logerror(lcMsg)
        loRes.status = 'error'
        loRes.data = null
        loRes.message = lcMsg
        res.addHeader('X-Auth-Status', 'false')
        res.status(500).json(this.toJson(loRes))            
    endtry
endproc

Respuesta del middleware

Si la autenticación es exitosa, el middleware de X# generará un token JWT y modificará la respuesta para incluirlo:

{
    "status": "ok",
    "data": {
        "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
        "expiresIn": 3600
    },
    "message": "token generated successfully"
}

Protección de rutas

Cuando un proyecto FoxServer utiliza autenticación JWT, todos los endpoints requieren autenticación por defecto. Para indicar que una ruta debe ser accesible públicamente (sin token JWT), utilice la palabra clave public en el comentario de ayuda del método:

function ValidateUser(req, res) help "POST: auth/login public"
    * Esta función es accesible sin token JWT (pública)
    * ...
endfunc

function GetUserData(req, res) help "GET: users/:id"
    * Esta función requiere autenticación con token JWT (protegida por defecto)
    * ...
endfunc

Nota: No es necesario añadir ninguna palabra clave especial para rutas protegidas, ya que es el comportamiento predeterminado cuando se activa la autenticación JWT.

Sistema de Logging

FoxServerCore incluye un potente sistema de logging a través de la clase FoxServerLogger, que permite registrar eventos, información, advertencias y errores durante la ejecución de la aplicación. Este sistema facilita la depuración y el seguimiento de la actividad del servidor.

Clase FoxServerLogger

La clase FoxServerLogger es el componente central del sistema de logging, proporcionando métodos para registrar diferentes tipos de mensajes y configurar el comportamiento del logger.

Propiedades principales

Propiedad Tipo Descripción
cLogFile Character Ruta completa del archivo de log donde se escribirán los mensajes
cPath Character Directorio donde se almacena el archivo de log
cCorrelationId Character Identificador único para correlacionar mensajes relacionados con una misma operación o solicitud
bCanWriteLog Logical Indica si el logging está habilitado (.T.) o deshabilitado (.F.)
apiService Object Referencia al servicio de API para utilizar funciones de formato

Configuración del Logger

SetLogFile

Establece el archivo donde se escribirán los mensajes de log. Si el directorio especificado no existe, lo crea automáticamente.

Nota: Este método es invocado automáticamente por FoxServer al iniciar una solicitud. No se debe modificar su implementación.
PROCEDURE SetLogFile(tcLogFile as string)
Parámetros:
  • tcLogFile (Character): Ruta completa del archivo de log

SetLogging

Habilita o deshabilita la escritura de logs. Útil para controlar dinámicamente el logging durante la ejecución.

Nota: Este método es invocado automáticamente por FoxServer al iniciar una solicitud. No se debe modificar su implementación.
PROCEDURE SetLogging(tbEnabled as Boolean)
Parámetros:
  • tbEnabled (Logical): .T. para habilitar, .F. para deshabilitar

SetCorrelationId

Establece un identificador de correlación que se incluirá en todos los mensajes de log. Facilita el seguimiento de operaciones específicas a través de múltiples entradas de log.

Nota: Este método es invocado automáticamente por FoxServer al iniciar una solicitud. No se debe modificar su implementación.
PROCEDURE SetCorrelationId(tcCorrelationId as string)
Parámetros:
  • tcCorrelationId (Character): Identificador único para correlacionar mensajes

SetApiService

Establece una referencia al servicio de API para utilizar sus funciones de formato en los mensajes de log.

Nota: Este método es invocado automáticamente por FoxServer al iniciar una solicitud. No se debe modificar su implementación.
PROCEDURE SetApiService(toApiService as ApiServiceUtility)
Parámetros:
  • toApiService (ApiServiceUtility): Instancia del servicio de API

Métodos de Logging

LogInfo

Registra un mensaje de nivel informativo. Útil para eventos normales del sistema que no requieren atención especial. Soporta formato de cadenas con múltiples parámetros.

PROCEDURE LogInfo(tcMessage, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11)
Parámetros:
  • tcMessage (Character): Mensaje informativo a registrar, puede contener marcadores de posición para formato
  • p2...p11 (Varios): Parámetros opcionales para formatear el mensaje
Ejemplo:
* Registrar mensaje simple
loLogger.LogInfo("Iniciando procesamiento de datos")

* Registrar mensaje con formato
loLogger.LogInfo("Cliente ID: {0} procesado con {1} registros", lcClienteId, lnRegistros)

LogWarning

Registra un mensaje de advertencia. Indica situaciones que no son errores pero que podrían requerir atención. Soporta formato de cadenas con múltiples parámetros.

PROCEDURE LogWarning(tcMessage, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11)
Parámetros:
  • tcMessage (Character): Mensaje de advertencia a registrar, puede contener marcadores de posición para formato
  • p2...p11 (Varios): Parámetros opcionales para formatear el mensaje
Ejemplo:
* Advertir sobre datos incompletos
loLogger.LogWarning("Usuario ID {0} no tiene email configurado", lcUserId)

* Advertir sobre recursos bajos
loLogger.LogWarning("Memoria disponible baja: {0} bytes", lnMemoryAvailable)

LogError

Registra un mensaje de error. Indica problemas que requieren atención y podrían afectar el funcionamiento del sistema. Soporta formato de cadenas con múltiples parámetros.

PROCEDURE LogError(tcMessage, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11)
Parámetros:
  • tcMessage (Character): Mensaje de error a registrar, puede contener marcadores de posición para formato
  • p2...p11 (Varios): Parámetros opcionales para formatear el mensaje
Ejemplo:
* Registrar error de conexión
loLogger.LogError("No se pudo conectar a la base de datos: {0}", lcServer)

* Registrar error con múltiples parámetros
loLogger.LogError("Error al procesar el archivo: {0}. Código: {1}, Tipo: {2}", lcFileName, lnErrorCode, lcErrorType)

LogFromException

Registra información detallada de una excepción VFP. Extrae automáticamente el número de error, línea, mensaje y procedimiento.

PROCEDURE LogFromException(toEx as exception)
Parámetros:
  • toEx (Exception): Objeto de excepción capturado en un bloque TRY-CATCH
Ejemplo:
* Capturar y registrar una excepción
TRY
    * Código que puede generar error
    USE (lcDatabase) SHARED
    * Más operaciones...
CATCH TO loException
    loLogger.LogFromException(loException)
    * Manejar el error...
ENDTRY

Log

Método interno que implementa la funcionalidad de logging. Puede ser llamado directamente para especificar el tipo de log mediante un código numérico. Soporta formato de cadenas con múltiples parámetros.

PROCEDURE Log(tnType as integer, tcMessage, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11)
Parámetros:
  • tnType (Integer): Tipo de log (16=ERROR, 48=WARNING, 64=INFO)
  • tcMessage (Character): Mensaje a registrar, puede contener marcadores de posición para formato
  • p2...p11 (Varios): Parámetros opcionales para formatear el mensaje
Nota:

Normalmente no es necesario llamar a este método directamente. Use los métodos específicos LogInfo, LogWarning y LogError en su lugar.

Formato de mensajes con múltiples parámetros

La clase FoxServerLogger ahora soporta el formato de mensajes con múltiples parámetros, similar a las funciones printf en otros lenguajes. Esto se logra a través del servicio ApiServiceUtility que proporciona la función format.

Ejemplo de formato:

* Mensaje con formato y múltiples parámetros
loLogger.LogInfo("Usuario {0} realizó {1} operaciones en {2}", lcUserName, lnOperations, lcModule)

Formato de los mensajes de log

Los mensajes de log se escriben en el archivo con el siguiente formato:

FECHA_HORA | TIPO_LOG | [CORRELATION_ID] | MENSAJE

Ejemplo:

31/05/2025 11:45:23 | INFO    | [a1b2c3d4-e5f6-g7h8] | Iniciando procesamiento de solicitud GET /api/usuarios
31/05/2025 11:45:24 | WARNING | [a1b2c3d4-e5f6-g7h8] | Parámetro 'filtro' no especificado, usando valores predeterminados
31/05/2025 11:45:25 | ERROR   | [a1b2c3d4-e5f6-g7h8] | Error 1526 at line 45: File 'datos.dbf' does not exist in program PROCESAR

Integración con ApiController

La clase ApiController integra automáticamente el sistema de logging, proporcionando métodos para acceder al logger desde los controladores:

* En un controlador que hereda de ApiController
FUNCTION GetUsuarios(req, res) HELP "GET: /api/usuarios"
    this.LogInfo("Obteniendo lista de usuarios")
    
    TRY
        * Código para obtener usuarios...
    CATCH TO loEx
        this.LogFromException(loEx)
        res.status(500).json('{"error":"Error interno del servidor"}')
    ENDTRY
    
    this.LogInfo("Se encontraron {0} usuarios", lnUsuarios)
    res.status(200).json(lcJson)
ENDFUNC

Buenas prácticas de logging

Recomendaciones para un logging efectivo

  • Niveles apropiados: Use el nivel adecuado para cada mensaje (INFO, WARNING, ERROR).
  • Información contextual: Incluya información relevante como IDs, valores clave y estados.
  • Formato estructurado: Aproveche la capacidad de formato para crear mensajes claros y consistentes.
  • Correlation IDs: Use IDs de correlación para seguir operaciones a través de múltiples logs.
  • Rotación de logs: Implemente una estrategia para rotar archivos de log (por día, tamaño, etc.).
  • Datos sensibles: Evite registrar información sensible como contraseñas o datos personales.
  • Rendimiento: Considere deshabilitar logs detallados en producción para mejorar el rendimiento.

Manejo de Datos Multipart

FoxServerCore proporciona un sistema robusto para manejar datos multipart/form-data, permitiendo procesar formularios con archivos adjuntos y campos de texto. Este sistema se compone de dos clases principales: FoxServerMultipartData y FoxServerMultipartFile.

FoxServerMultipartData

La clase FoxServerMultipartData extiende FoxServerTupleDictionary y proporciona métodos para gestionar colecciones de archivos enviados en formularios multipart.

AddFile

Agrega un archivo a la colección de datos multipart. Si ya existen archivos con el mismo nombre de campo, los organiza como una colección.

Nota: Este método es invocado automáticamente por FoxServer. No se debe modificar su implementación.
PROCEDURE AddFile(tcFieldName as string, tcFileName as string, tcContentType as string, tvContent as Variant) as Void
Parámetros:
  • tcFieldName (Character): Nombre del campo del formulario
  • tcFileName (Character): Nombre original del archivo
  • tcContentType (Character): Tipo MIME del archivo
  • tvContent (Variant): Contenido binario del archivo
Ejemplo:
* Agregar un archivo a la colección
loMultipartData = CREATEOBJECT("FoxServerMultipartData")
loMultipartData.AddFile("avatar", "profile.jpg", "image/jpeg", lcFileContent)

* Agregar múltiples archivos con el mismo nombre de campo
loMultipartData.AddFile("documentos", "contrato.pdf", "application/pdf", lcPdfContent)
loMultipartData.AddFile("documentos", "anexo.docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document", lcDocxContent)

GetFiles

Obtiene todos los archivos asociados a un nombre de campo específico como un objeto FoxServerTupleDictionary.

FUNCTION GetFiles(tcKey as string) as Variant
Parámetros:
  • tcKey (Character): Nombre del campo del formulario
Retorna:

Variant: Objeto FoxServerTupleDictionary con los archivos o .NULL. si no existen

Ejemplo:
* Obtener todos los archivos de un campo y procesarlos
loFiles = loMultipartData.GetFiles("documentos")

IF !ISNULL(loFiles)
    * Iterar a través de todos los archivos
    FOR EACH loKey IN loFiles.Keys
        loFile = loFiles.GetValue(loKey)
        THIS.LogInfo("Procesando archivo: {0} Tamaño: {1}", loFile.FileName, loFile.GetSize())
        * Hacer algo con cada archivo...
    ENDFOR
ENDIF

GetFile

Obtiene el primer archivo asociado a un nombre de campo específico.

FUNCTION GetFile(tcKey as string) as Variant
Parámetros:
  • tcKey (Character): Nombre del campo del formulario
Retorna:

Variant: Objeto FoxServerMultipartFile o .NULL. si no existe

Ejemplo:
* Obtener un único archivo y procesarlo
loFile = loMultipartData.GetFile("avatar")

IF !ISNULL(loFile)
    * Verificar si es una imagen
    IF loFile.IsImage()
        * Guardar la imagen en un directorio
        lcPath = "C:\uploads\avatars\" + SYS(2015) + "." + loFile.GetBestExtension()
        IF loFile.SaveToFile(lcPath)
            THIS.LogInfo("Imagen guardada en: {0}", lcPath)
        ELSE
            THIS.LogError("Error al guardar la imagen")
        ENDIF
    ENDIF
ENDIF

FoxServerMultipartFile

La clase FoxServerMultipartFile representa un archivo individual enviado en un formulario multipart y proporciona métodos para manipular y validar el archivo.

Propiedades

Propiedad Tipo Descripción
FieldName Character Nombre del campo del formulario asociado al archivo
FileName Character Nombre original del archivo
ContentType Character Tipo MIME del archivo (ej: "image/jpeg", "application/pdf")
Content Variant Contenido binario del archivo

Métodos de configuración

SetFieldName / SetFileName / SetContentType / SetContent

Métodos para establecer las propiedades del archivo.

Nota: Estos métodos son invocados automáticamente por FoxServer. No se debe modificar su implementación.
PROCEDURE SetFieldName(tcFieldName as string) as Void
PROCEDURE SetFileName(tcFileName as string) as Void
PROCEDURE SetContentType(tcContentType as string) as Void
PROCEDURE SetContent(tvContent as Variant) as Void

Métodos de extensión de archivo

GetFileExtension

Determina la extensión de archivo basada en el tipo MIME.

FUNCTION GetFileExtension() as string
Retorna:

Character: Extensión de archivo sin el punto inicial (ej: "jpg", "pdf")

Ejemplo:
* Obtener extensión basada en el tipo MIME
lcExt = loFile.GetFileExtension()
? lcExt && "pdf" para un archivo con ContentType = "application/pdf"

GetOriginalExtension

Obtiene la extensión original del nombre de archivo.

FUNCTION GetOriginalExtension() as string
Retorna:

Character: Extensión original del archivo sin el punto inicial

Ejemplo:
* Obtener extensión del nombre original
lcExt = loFile.GetOriginalExtension()
? lcExt && "pdf" para un archivo con FileName = "documento.pdf"

GetBestExtension

Obtiene la mejor extensión disponible, usando la extensión original si existe, o la derivada del tipo MIME en caso contrario.

FUNCTION GetBestExtension() as string
Retorna:

Character: La mejor extensión disponible sin el punto inicial

Ejemplo:
* Obtener la mejor extensión para guardar el archivo
lcExt = loFile.GetBestExtension()
lcPath = "C:\uploads\" + SYS(2015) + "." + lcExt

Métodos de manejo de archivos

SaveToFile

Guarda el contenido del archivo en el sistema de archivos. Crea el directorio si no existe y agrega la extensión adecuada si es necesario.

FUNCTION SaveToFile(tcTargetPath as string) as Boolean
Parámetros:
  • tcTargetPath (Character): Ruta donde guardar el archivo
Retorna:

Logical: .T. si se guardó correctamente, .F. en caso de error

Ejemplo:
* Guardar archivo con nombre único
lcPath = "C:\uploads\" + SYS(2015)
IF loFile.SaveToFile(lcPath)
    THIS.Logger("Archivo guardado como: {0}", lcPath + "." + loFile.GetBestExtension())
ELSE
    THIS.Logger("Error al guardar el archivo")
ENDIF

* Guardar con ruta y nombre específicos
IF loFile.SaveToFile("C:\uploads\documentos\informe_final.pdf")
    ? "Archivo guardado correctamente"
ENDIF

GetSize

Obtiene el tamaño del archivo en bytes.

FUNCTION GetSize() as integer
Retorna:

Integer: Tamaño del archivo en bytes

Ejemplo:
* Verificar tamaño antes de procesar
lnSize = loFile.GetSize()
THIS.Logger("Tamaño del archivo: {0} bytes", lnSize)

* Verificar si excede un límite
IF lnSize > 5000000 && 5MB
    THIS.LogError("El archivo es demasiado grande")
ENDIF

IsEmpty

Verifica si el archivo está vacío (sin contenido).

FUNCTION IsEmpty() as Boolean
Retorna:

Logical: .T. si el archivo está vacío, .F. en caso contrario

Ejemplo:
* Verificar si el archivo tiene contenido
IF loFile.IsEmpty()
    THIS.LogError("El archivo está vacío")
ELSE
    THIS.LogInfo("El archivo contiene {0} bytes.", loFile.GetSize())
ENDIF

Métodos de validación de tipo

IsContentType

Verifica si el archivo es de un tipo MIME específico.

FUNCTION IsContentType(tcPattern as string) as Boolean
Parámetros:
  • tcPattern (Character): Tipo MIME a verificar
Retorna:

Logical: .T. si coincide con el tipo especificado, .F. en caso contrario

Ejemplo:
* Verificar tipo específico
IF loFile.IsContentType("application/pdf")
    THIS.LogInfo("Es un archivo PDF")
ENDIF

* Verificar tipo de imagen específico
IF loFile.IsContentType("image/jpeg")
    THIS.LogInfo("Es una imagen JPEG")
ENDIF

IsImage

Verifica si el archivo es una imagen (cualquier tipo de imagen).

FUNCTION IsImage() as Boolean
Retorna:

Logical: .T. si es una imagen, .F. en caso contrario

Ejemplo:
* Verificar si es una imagen
IF loFile.IsImage()
    THIS.LogInfo("Es una imagen de tipo: {0}", loFile.ContentType)
    
    * Procesar solo imágenes
    lcImagePath = "C:\uploads\images\" + SYS(2015) + "." + loFile.GetBestExtension()
    loFile.SaveToFile(lcImagePath)
ELSE
    ? "No es una imagen"
ENDIF

IsPDF

Verifica si el archivo es un PDF.

FUNCTION IsPDF() as Boolean
Retorna:

Logical: .T. si es un PDF, .F. en caso contrario

Ejemplo:
* Verificar si es un PDF
IF loFile.IsPDF()
    THIS.LogInfo("Es un archivo PDF")
    
    * Procesar solo PDFs
    lcPdfPath = "C:\uploads\documents\" + SYS(2015) + ".pdf"
    loFile.SaveToFile(lcPdfPath)
ELSE
    THIS.LogInfo("No es un archivo PDF")
ENDIF

IsAllowedType

Verifica si el archivo tiene una extensión permitida según una lista de extensiones.

FUNCTION IsAllowedType(tcAllowedTypes as string) as Boolean
Parámetros:
  • tcAllowedTypes (Character): Lista de extensiones permitidas separadas por comas (ej: "pdf,jpg,png" o ".pdf,.jpg,.png")
Retorna:

Logical: .T. si la extensión está permitida, .F. en caso contrario

Ejemplo:
* Verificar si es un tipo permitido
IF loFile.IsAllowedType("pdf,docx,xlsx")
    THIS.LogInfo("Tipo de archivo permitido")
ELSE
    THIS.LogInfo("Tipo de archivo no permitido. Solo se aceptan PDF, DOCX y XLSX")
ENDIF

* Verificar tipos de imagen permitidos
IF loFile.IsImage() AND loFile.IsAllowedType("jpg,png,gif")
    THIS.LogInfo("Formato de imagen permitido")
ELSE
    THIS.LogInfo("Solo se permiten imágenes JPG, PNG y GIF")
ENDIF

Integración con ApiController

El sistema de manejo multipart se integra perfectamente con ApiController para procesar formularios con archivos:

Ejemplo: Procesamiento de formulario con archivos

DEFINE CLASS UploadController AS ApiController
    
    * Endpoint para subir archivos
    FUNCTION Upload(req, res) HELP "POST: /api/upload"
        LOCAL loFiles, loFile, lcUploadPath, lcSavedPath, loResponse
        
        * Verificar si hay archivos
        IF !req.HasFiles()
            res.status(400).json('{"error":"No se encontraron archivos"}')
            RETURN
        ENDIF                     
        
        * Configurar directorio de carga
        lcUploadPath = ADDBS(this.GetConfig("upload_path", "C:\uploads"))
        
        * Verificar y crear directorio si no existe
        IF !DIRECTORY(lcUploadPath)
            MD (lcUploadPath)
        ENDIF
        
        * Procesar archivo de avatar (uno solo)
        loFile = req.GetFile("avatar")
        IF !ISNULL(loFile)
            * Validar que sea una imagen
            IF !loFile.IsImage()
                res.status(400).json('{"error":"El avatar debe ser una imagen"}')
                RETURN
            ENDIF
            
            * Validar tamaño máximo (1MB)
            IF loFile.GetSize() > 1048576
                res.status(400).json('{"error":"El avatar no debe superar 1MB"}')
                RETURN
            ENDIF
            
            * Guardar con nombre único
            lcSavedPath = ADDBS(lcUploadPath) + "avatar_" + req.GetParam("user_id") + "." + loFile.GetBestExtension()
            IF !loFile.SaveToFile(lcSavedPath)
                res.status(500).json('{"error":"Error al guardar el avatar"}')
                RETURN
            ENDIF
            
            this.LogInfo("Avatar guardado: {0}", lcSavedPath)
        ENDIF
        
        * Procesar múltiples documentos
        loDocumentos = req.GetFiles("documentos")
        IF !ISNULL(loDocumentos)
            LOCAL laRutas[1], lnCount, loDocumento
            
            lnCount = 0
            DIMENSION laRutas[1]
            
            * Crear directorio específico para documentos
            lcDocPath = ADDBS(lcUploadPath) + "docs_" + SYS(2015)
            MD (lcDocPath)
            
            * Procesar cada documento
            FOR EACH loKey IN loDocumentos.Keys
                loDocumento = loDocumentos.GetValue(loKey)
                
                * Validar tipo permitido
                IF !loDocumento.IsAllowedType("pdf,docx,xlsx")
                    this.LogWarning("Tipo de documento no permitido: {0}", loDocumento.FileName)
                    LOOP
                ENDIF
                
                * Guardar documento
                lcDocFile = ADDBS(lcDocPath) + loDocumento.FileName
                IF loDocumento.SaveToFile(lcDocFile)
                    lnCount = lnCount + 1
                    DIMENSION laRutas[lnCount]
                    laRutas[lnCount] = lcDocFile                    
                    this.LogInfo("Documento guardado: {0}", lcDocFile)
                ENDIF
            ENDFOR
            
            * Crear respuesta
            loResponse = this.NewObject("success,message,avatar,documentos")
            loResponse.success = .T.
            loResponse.message = "Archivos procesados correctamente"
            loResponse.avatar = lcSavedPath
            loResponse.documentos = laRutas

            res.status(200).json(this.ToJson(loResponse))
        ENDIF

        res.status(200).json('{"success":true,"message":"Avatar procesado correctamente"}')
    ENDFUNC
    
ENDDEFINE

Buenas prácticas

Recomendaciones para el manejo de archivos

  • Validación de tipos: Siempre valide los tipos de archivo permitidos para evitar vulnerabilidades.
  • Límites de tamaño: Establezca límites de tamaño para los archivos subidos.
  • Nombres seguros: Genere nombres únicos para los archivos guardados para evitar colisiones.
  • Estructura de directorios: Organice los archivos en directorios por tipo o fecha.
  • Permisos: Asegúrese de que los directorios tengan los permisos adecuados.
  • Verificación de contenido: Para mayor seguridad, verifique que el contenido coincida con el tipo declarado.

Colecciones y Diccionarios

FoxServerCore proporciona un sistema flexible para manejar colecciones de pares clave-valor a través de las clases FoxServerTuple y FoxServerTupleDictionary. Estas clases forman la base de muchas estructuras de datos utilizadas en el framework.

FoxServerTuple

La clase FoxServerTuple representa un par clave-valor simple, donde la clave es una cadena y el valor puede ser de cualquier tipo. Esta estructura básica se utiliza como componente fundamental para construir colecciones más complejas.

Propiedades

Propiedad Tipo Descripción
key Character Clave que identifica al elemento
value Variant Valor asociado a la clave (puede ser de cualquier tipo)

Constructor (init)

Inicializa una nueva instancia de FoxServerTuple con una clave y un valor opcionales.

PROCEDURE init(tcKey as string, tvValue as Variant)
Parámetros:
  • tcKey (Character): Clave del par (opcional)
  • tvValue (Variant): Valor asociado a la clave (opcional)

SetKey / GetKey

Establece o recupera la clave del par.

PROCEDURE SetKey(tcKey as string) as Void
FUNCTION GetKey() as string
Parámetros (SetKey):
  • tcKey (Character): Clave a establecer
Retorna (GetKey):

Character: La clave actual del par

SetValue / GetValue

Establece o recupera el valor asociado a la clave.

PROCEDURE SetValue(tvValue as Variant) as Void
FUNCTION GetValue() as Variant
Parámetros (SetValue):
  • tvValue (Variant): Valor a establecer
Retorna (GetValue):

Variant: El valor actual asociado a la clave

ToString

Devuelve una representación en cadena del par clave-valor, formateando el valor según su tipo.

FUNCTION ToString() as string
Retorna:

Character: Representación en cadena del par clave-valor

FoxServerTupleDictionary

La clase FoxServerTupleDictionary extiende la clase nativa Collection de VFP para proporcionar funcionalidad de diccionario (mapa clave-valor). Permite almacenar y recuperar valores utilizando claves de cadena, manteniendo un acceso eficiente.

Propiedades

Propiedad Tipo Descripción
akeys Array Array interno que almacena las claves para acceso rápido

Exists

Verifica si una clave específica existe en el diccionario.

FUNCTION Exists(tcKey as string) as Boolean
Parámetros:
  • tcKey (Character): Clave a verificar
Retorna:

Logical: .T. si la clave existe, .F. en caso contrario

AddPair

Agrega un nuevo par clave-valor al diccionario. Si la clave ya existe, actualiza su valor.

PROCEDURE AddPair(tcKey as string, tvValue as Variant)
Parámetros:
  • tcKey (Character): Clave del par
  • tvValue (Variant): Valor a asociar con la clave

AddEntry

Agrega un objeto FoxServerTuple al diccionario. Si la clave ya existe, actualiza su valor.

FUNCTION AddEntry(toEntry as FoxServerTuple) as Void
Parámetros:
  • toEntry (FoxServerTuple): Objeto tuple a agregar

GetIndex

Obtiene el índice numérico de una clave en el diccionario.

FUNCTION GetIndex(tcKey as string) as integer
Parámetros:
  • tcKey (Character): Clave a buscar
Retorna:

Integer: Índice de la clave en el diccionario (0 si no existe)

Get

Obtiene un objeto FoxServerTuple por su índice o clave.

FUNCTION Get(tvIndexOrKey as Variant) as Variant
Parámetros:
  • tvIndexOrKey (Variant): Índice numérico o clave de cadena
Retorna:

Variant: Objeto FoxServerTuple o .NULL. si no existe

GetValue

Obtiene el valor asociado a una clave o índice, con la opción de especificar un valor predeterminado si la clave no existe.

FUNCTION GetValue(tvIndexOrKey as Variant, tvDefault as Variant) as Variant
Parámetros:
  • tvIndexOrKey (Variant): Índice numérico o clave de cadena
  • tvDefault (Variant): Valor predeterminado a devolver si la clave no existe (opcional)
Retorna:

Variant: Valor asociado a la clave o el valor predeterminado

CleanUp / Release / Dispose

Métodos para limpiar y liberar recursos del diccionario.

PROCEDURE CleanUp() as Void
PROCEDURE Release() as Void
PROCEDURE Dispose() as Void

Utilidades de Servicio API

La clase ApiServiceUtility proporciona un conjunto de herramientas y funciones auxiliares comúnmente utilizadas en el desarrollo de APIs y servicios web. Estas utilidades facilitan tareas como la determinación de tipos MIME, validación de formatos, generación de identificadores únicos, manipulación de JSON y más.

ApiServiceUtility

Esta clase contiene métodos que pueden ser utilizados en cualquier parte de la aplicación para realizar operaciones comunes.

Propiedades principales

Propiedad Tipo Descripción
Logger Object Referencia al objeto FoxServerLogger para registrar eventos
cPath Character Ruta base utilizada para operaciones de archivo

Métodos de configuración

SetPath

Establece la ruta base utilizada por la clase para operaciones de archivo.

PROCEDURE SetPath(tcPath as String)
Parámetros:
  • tcPath (Character): Ruta base para operaciones de archivo

SetLogger

Establece el objeto logger utilizado para registrar eventos y errores.

PROCEDURE SetLogger(toLogger as FoxServerLogger)
Parámetros:
  • toLogger (FoxServerLogger): Instancia del logger para registrar eventos

Utilidades básicas

GetMimeType

Determina el tipo MIME correspondiente a una extensión de archivo. Este método es útil para establecer cabeceras Content-Type adecuadas al servir archivos estáticos o descargas.

FUNCTION GetMimeType(tcExtension as string) as string
Parámetros:
  • tcExtension (Character): Extensión del archivo sin el punto inicial (ej: "pdf", "jpg")
Retorna:

Character: Tipo MIME correspondiente a la extensión

Ejemplo:
* Obtener tipos MIME para diferentes extensiones
? loUtils.GetMimeType("pdf")    && "application/pdf"
? loUtils.GetMimeType("jpg")    && "image/jpeg"
? loUtils.GetMimeType("docx")   && "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
? loUtils.GetMimeType("json")   && "application/json"

IsValidEmail

Valida si una cadena tiene formato de dirección de correo electrónico válido utilizando expresiones regulares.

FUNCTION IsValidEmail(tcEmail as string) as Boolean
Parámetros:
  • tcEmail (Character): Dirección de correo electrónico a validar
Retorna:

Logical: .T. si el formato es válido, .F. en caso contrario

Ejemplo:
* Validar direcciones de correo electrónico
? loUtils.IsValidEmail("usuario@dominio.com")      && .T.
? loUtils.IsValidEmail("nombre.apellido@empresa.co.uk")  && .T.
? loUtils.IsValidEmail("usuario@dominio")          && .F.

GenerateUUID

Genera un identificador único universal (UUID/GUID) utilizando preferentemente el componente COM scriptlet.typelib o, como respaldo, una implementación manual.

FUNCTION GenerateUUID() as string
Retorna:

Character: Cadena UUID en formato estándar (ej: "550e8400-e29b-41d4-a716-446655440000")

Ejemplo:
* Generar identificadores únicos
lcUUID = loUtils.GenerateUUID()
? lcUUID && Por ejemplo: "550e8400-e29b-41d4-a716-446655440000"

GenerateUUIDManual

Implementación manual para generar un UUID cuando el componente COM no está disponible. Este método es utilizado internamente por GenerateUUID como respaldo.

FUNCTION GenerateUUIDManual() as string
Retorna:

Character: Cadena UUID en formato estándar

Utilidades de formato y texto

format

Formatea una cadena de texto reemplazando marcadores de posición con valores proporcionados. Similar a la función printf en otros lenguajes.

FUNCTION format(tcSource, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11) as string
Parámetros:
  • tcSource (Character): Cadena de formato con marcadores de posición {0}, {1}, etc.
  • p2...p11 (Varios): Valores para reemplazar los marcadores de posición
Retorna:

Character: Cadena formateada con los valores reemplazados

Ejemplo:
* Formatear una cadena con valores
lcNombre = "Juan"
lnEdad = 30
lcCiudad = "Madrid"

lcMensaje = loUtils.format("Hola, mi nombre es {0}, tengo {1} años y vivo en {2}.", lcNombre, lnEdad, lcCiudad)
? lcMensaje && "Hola, mi nombre es Juan, tengo 30 años y vivo en Madrid."

EscapeCharsToJSON

Convierte caracteres especiales y acentuados a su representación Unicode para uso en cadenas JSON.

FUNCTION EscapeCharsToJSON(tcStream) as string
Parámetros:
  • tcStream (Character): Cadena con caracteres que necesitan ser escapados
Retorna:

Character: Cadena con caracteres escapados para JSON

Ejemplo:
* Escapar caracteres especiales para JSON
lcTexto = "José Martínez & Cía."
lcEscapado = loUtils.EscapeCharsToJSON(lcTexto)
? lcEscapado && "Jos\u00e9 Mart\u00ednez \u0026 C\u00eda."

EscapeCharsToHTML

Convierte caracteres acentuados a su representación en entidades HTML.

FUNCTION EscapeCharsToHTML(tcStream) as string
Parámetros:
  • tcStream (Character): Cadena con caracteres que necesitan ser convertidos
Retorna:

Character: Cadena con caracteres convertidos a entidades HTML

Ejemplo:
* Convertir caracteres a entidades HTML
lcTexto = "José María"
lcHtml = loUtils.EscapeCharsToHTML(lcTexto)
? lcHtml && "José María"

Utilidades de fecha y hora

DTOUNX

Convierte una fecha/hora de VFP a un timestamp UNIX (segundos o milisegundos desde el 1 de enero de 1970).

FUNCTION DTOUNX(tdFecha as datetime, tbUseMiliseconds as logical) as number
Parámetros:
  • tdFecha (DateTime): Fecha y hora a convertir
  • tbUseMiliseconds (Logical): Si es .T., retorna milisegundos; si es .F., retorna segundos
Retorna:

Number: Timestamp UNIX en segundos o milisegundos

Ejemplo:
* Convertir fecha a timestamp UNIX
ldFecha = {^2025-06-01 12:30:00}

* En segundos
lnUnixSecs = loUtils.DTOUNX(ldFecha, .F.)
? lnUnixSecs && 1748905800

* En milisegundos
lnUnixMs = loUtils.DTOUNX(ldFecha, .T.)
? lnUnixMs && 1748905800000

UNXTOD

Convierte un timestamp UNIX a una fecha/hora de VFP.

FUNCTION UNXTOD(tnUnixTimestamp as number) as datetime
Parámetros:
  • tnUnixTimestamp (Number): Timestamp UNIX en segundos (10 dígitos) o milisegundos (13 dígitos)
Retorna:

DateTime: Fecha y hora correspondiente al timestamp

Ejemplo:
* Convertir timestamp UNIX a fecha
* Timestamp en segundos
ldFecha1 = loUtils.UNXTOD(1748905800)
? ldFecha1 && 01/06/2025 12:30:00

* Timestamp en milisegundos
ldFecha2 = loUtils.UNXTOD(1748905800000)
? ldFecha2 && 01/06/2025 12:30:00

Utilidades de codificación

Base64URLEncode

Codifica una cadena en formato Base64URL (variante de Base64 segura para URLs).

FUNCTION Base64URLEncode(tcInput as string) as string
Parámetros:
  • tcInput (Character): Cadena a codificar
Retorna:

Character: Cadena codificada en Base64URL

Ejemplo:
* Codificar en Base64URL
lcTexto = "Esto es un texto de prueba"
lcCodificado = loUtils.Base64URLEncode(lcTexto)
? lcCodificado && "RXN0byBlcyB1biB0ZXh0byBkZSBwcnVlYmE"

Base64URLDecode

Decodifica una cadena en formato Base64URL a su valor original.

FUNCTION Base64URLDecode(tcInput as string) as string
Parámetros:
  • tcInput (Character): Cadena codificada en Base64URL
Retorna:

Character: Cadena decodificada

Ejemplo:
* Decodificar desde Base64URL
lcCodificado = "RXN0byBlcyB1biB0ZXh0byBkZSBwcnVlYmE"
lcDecodificado = loUtils.Base64URLDecode(lcCodificado)
? lcDecodificado && "Esto es un texto de prueba"

Utilidades JSON

LoadJsonFox

Carga la biblioteca JSONFox para procesamiento de JSON si aún no está cargada.

PROCEDURE LoadJsonFox()
Notas:

Este método verifica si JSONFox ya está cargado en _screen.json y, si no, intenta cargarlo desde la ruta configurada en cPath.

ParseJson

Analiza una cadena JSON y la convierte en un objeto VFP.

FUNCTION ParseJson(tcJsonStr as memo) as object
Parámetros:
  • tcJsonStr (Memo): Cadena JSON a analizar
Retorna:

Object: Objeto VFP con la estructura del JSON, o .NULL. en caso de error

Ejemplo:
* Analizar una cadena JSON
lcJson = '{"nombre":"Juan","edad":30,"ciudad":"Madrid"}'
loObj = loUtils.ParseJson(lcJson)

? loObj.nombre && "Juan"
? loObj.edad && 30
? loObj.ciudad && "Madrid"

TableToJson

Convierte un cursor o tabla VFP a formato JSON.

FUNCTION TableToJson(tcCursor as string, tbCurrentRow as logical, tnSessionId as integer) as memo
Parámetros:
  • tcCursor (Character): Nombre del cursor a convertir
  • tbCurrentRow (Logical): Si es .T., solo convierte el registro actual; si es .F., convierte todos los registros
  • tnSessionId (Integer): ID de sesión de datos donde se encuentra el cursor
Retorna:

Memo: Cadena JSON con los datos del cursor

Ejemplo:
* Crear un cursor de ejemplo
CREATE CURSOR clientes (id i, nombre c(30), email c(50))
INSERT INTO clientes VALUES (1, 'Juan Pérez', 'juan@ejemplo.com')
INSERT INTO clientes VALUES (2, 'María López', 'maria@ejemplo.com')

* Convertir todo el cursor a JSON
lcJson = loUtils.TableToJson('clientes', .F., THIS.DataSessionId)
? lcJson 
&& [{"id":1,"nombre":"Juan Pérez","email":"juan@ejemplo.com"},
&&  {"id":2,"nombre":"María López","email":"maria@ejemplo.com"}]

* Convertir solo el registro actual
GO 2
lcJsonRow = loUtils.TableToJson('clientes', .T., THIS.DataSessionId)
? lcJsonRow 
&& {"id":2,"nombre":"María López","email":"maria@ejemplo.com"}

MasterToJSON

Convierte una relación maestro-detalle entre dos cursores a formato JSON.

FUNCTION MasterToJSON(tcMaster as string, tcDetail as string, tcExpr as string, tcDetailAttribute as string, tnSessionID as integer) as memo
Parámetros:
  • tcMaster (Character): Nombre del cursor maestro
  • tcDetail (Character): Nombre del cursor detalle
  • tcExpr (Character): Expresión de relación entre maestro y detalle
  • tcDetailAttribute (Character): Nombre del atributo que contendrá los detalles en el JSON
  • tnSessionID (Integer): ID de sesión de datos
Retorna:

Memo: Cadena JSON con la estructura maestro-detalle

Ejemplo:
* Crear cursores maestro-detalle
CREATE CURSOR facturas (id i, fecha d, cliente c(30))
CREATE CURSOR items (factura_id i, producto c(30), cantidad i, precio n(10,2))

* Insertar datos de ejemplo
INSERT INTO facturas VALUES (1, DATE(), 'Cliente A')
INSERT INTO facturas VALUES (2, DATE(), 'Cliente B')

INSERT INTO items VALUES (1, 'Producto 1', 2, 100.00)
INSERT INTO items VALUES (1, 'Producto 2', 1, 50.00)
INSERT INTO items VALUES (2, 'Producto 3', 3, 75.00)

* Convertir a JSON con relación maestro-detalle
lcJson = loUtils.MasterToJSON('facturas', 'items', 'facturas.id = items.factura_id', 'items', _VFP.DataSessionId)

* El resultado será un JSON con cada factura y sus items relacionados

Métodos de logging

ApiServiceUtility incluye métodos para registrar eventos a través del objeto Logger configurado. Estos métodos son wrappers que verifican la existencia del logger antes de intentar registrar mensajes.

LogInfo

Registra un mensaje informativo si hay un logger configurado.

PROCEDURE LogInfo(tcMessage, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11)
Parámetros:
  • tcMessage (Character): Mensaje a registrar, puede incluir marcadores de formato
  • p2...p11 (Varios): Parámetros opcionales para formatear el mensaje

LogError

Registra un mensaje de error si hay un logger configurado.

PROCEDURE LogError(tcMessage, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11)
Parámetros:
  • tcMessage (Character): Mensaje a registrar, puede incluir marcadores de formato
  • p2...p11 (Varios): Parámetros opcionales para formatear el mensaje

LogWarning

Registra un mensaje de advertencia si hay un logger configurado.

PROCEDURE LogWarning(tcMessage, p2, p3, p4, p5, p6, p7, p8, p9, p10, p11)
Parámetros:
  • tcMessage (Character): Mensaje a registrar, puede incluir marcadores de formato
  • p2...p11 (Varios): Parámetros opcionales para formatear el mensaje

LogFromException

Registra información de una excepción si hay un logger configurado.

PROCEDURE LogFromException(toException as exception)
Parámetros:
  • toException (Exception): Objeto de excepción a registrar

Tabla de tipos MIME soportados

Extensión Tipo MIME Categoría
pdf application/pdf Documento
xlsx, xls application/vnd.openxmlformats-officedocument.spreadsheetml.sheet Hoja de cálculo
docx, doc application/vnd.openxmlformats-officedocument.wordprocessingml.document Documento
txt text/plain Texto
csv text/csv Datos
zip application/zip Archivo comprimido
jpg, jpeg image/jpeg Imagen
png image/png Imagen
gif image/gif Imagen
svg image/svg+xml Imagen
html, htm text/html Documento web
css text/css Estilo web
js application/javascript Script
json application/json Datos
xml application/xml Datos
mp3 audio/mpeg Audio
mp4 video/mp4 Video
wav audio/wav Audio
avi video/x-msvideo Video
mov video/quicktime Video
rtf application/rtf Documento
ppt, pptx application/vnd.openxmlformats-officedocument.presentationml.presentation Presentación
otros application/octet-stream Genérico

Gestión del Ciclo de Vida de la Aplicación

La clase ApplicationLifecycle proporciona un mecanismo para gestionar recursos globales y eventos del ciclo de vida de la aplicación FoxServer. Esta clase hereda de ApiController y permite centralizar la inicialización y limpieza de recursos compartidos.

ApplicationLifecycle

Esta clase actúa como un contenedor para recursos globales y maneja los eventos de inicio y parada de la aplicación. Utiliza una colección interna (oResources) para almacenar y gestionar todos los recursos globales de forma centralizada.

Init

Constructor de la clase que inicializa la colección de recursos globales.

PROCEDURE Init()
Implementación:
PROCEDURE Init()
    THIS.oResources = CREATEOBJECT("Collection")
ENDPROC

OnApplicationStarting

Método hook que se ejecuta al iniciar la aplicación. Debe ser implementado en clases derivadas para inicializar recursos globales.

PROCEDURE OnApplicationStarting()
Ejemplo de implementación:
DEFINE CLASS MyAppLifecycle AS ApplicationLifecycle OLEPUBLIC
    PROCEDURE OnApplicationStarting()
        * Inicializar conexión de base de datos global
        LOCAL loConnection
        loConnection = CREATEOBJECT("ADODB.Connection")
        loConnection.Open("Provider=SQLOLEDB;Server=localhost;Database=MyApp;Trusted_Connection=yes")
        
        * Almacenar como recurso global
        THIS.SetResource("DatabaseConnection", loConnection)
        
        * Cargar configuración global
        LOCAL loConfig
        loConfig = THIS.LoadConfiguration()
        THIS.SetResource("AppConfig", loConfig)
        
        * Inicializar cache global
        LOCAL loCache
        loCache = CREATEOBJECT("ApplicationCache")
        THIS.SetResource("Cache", loCache)
        
        THIS.LogInfo("Aplicación iniciada correctamente")
    ENDPROC
    
ENDDEFINE

OnApplicationStopping

Método hook que se ejecuta al detener la aplicación. Debe ser implementado en clases derivadas para limpiar recursos globales de forma manual o si lo prefiere puede llamar al método CleanUp para liberar todos los recursos automáticamente.

PROCEDURE OnApplicationStopping()
Ejemplo de implementación:
PROCEDURE OnApplicationStopping()
    * Cerrar conexión de base de datos
    LOCAL loConnection
    loConnection = THIS.GetResource("DatabaseConnection")
    IF !ISNULL(loConnection)
        loConnection.Close()
        THIS.LogInfo("Conexión de base de datos cerrada")
    ENDIF
    
    * Limpiar cache
    LOCAL loCache
    loCache = THIS.GetResource("Cache")
    IF !ISNULL(loCache) AND PEMSTATUS(loCache, "Clear", 5)
        loCache.Clear()
        THIS.LogInfo("Cache limpiado")
    ENDIF
    
    THIS.LogInfo("Aplicación detenida correctamente")
ENDPROC

SetResource

Método protegido para almacenar un recurso global. Reemplaza automáticamente recursos existentes con el mismo nombre, garantizando que siempre se mantenga la versión más reciente.

PROTECTED PROCEDURE SetResource(tcResourceName as string, toReference as variant)
Parámetros:
  • tcResourceName (Character): Nombre único del recurso
  • toReference (Variant): Referencia al objeto o valor a almacenar
Ejemplo:
* En OnApplicationStarting()
LOCAL loConnection
loConnection = CREATEOBJECT("ADODB.Connection")
loConnection.Open("connection_string")
THIS.SetResource("DatabaseConnection", loConnection)

* Actualizar recurso existente
LOCAL loNewConfig
loNewConfig = THIS.LoadUpdatedConfiguration()
THIS.SetResource("AppConfig", loNewConfig)  && Reemplaza el anterior

* Almacenar diferentes tipos de recursos
THIS.SetResource("ServerStartTime", DATETIME())
THIS.SetResource("MaxConnections", 100)
THIS.SetResource("DebugMode", .T.)

Comportamiento del SetResource:

  • Si el recurso no existe, lo crea nuevo
  • Si el recurso ya existe, lo reemplaza completamente
  • Acepta cualquier tipo de dato: objetos, valores, referencias
  • Es thread-safe para operaciones de escritura

GetResource

Obtiene un recurso global por su nombre. Proporciona acceso seguro a recursos compartidos con validación automática de la colección.

FUNCTION GetResource(tcResourceName as string) AS Variant
Parámetros:
  • tcResourceName (Character): Nombre del recurso a obtener
Retorna:

Variant: El recurso solicitado o NULL si no existe o la colección no es válida

Ejemplo de uso:
* Obtener recursos de diferentes tipos
LOCAL loConnection, loConfig, lnMaxConn, llDebug
loConnection = THIS.GetResource("DatabaseConnection")
loConfig = THIS.GetResource("AppConfig")
lnMaxConn = THIS.GetResource("MaxConnections")
llDebug = THIS.GetResource("DebugMode")

* Siempre validar antes de usar
IF !ISNULL(loConnection)
    * Usar la conexión...
ELSE
    THIS.LogError("Conexión de base de datos no disponible")
ENDIF

* Obtener con valores por defecto
LOCAL lcServerUrl
lcServerUrl = IIF(ISNULL(THIS.GetResource("ServerUrl")), "localhost", THIS.GetResource("ServerUrl"))

IsValidResourceCollection

Método helper protegido que valida si la colección de recursos está correctamente inicializada.

PROTECTED FUNCTION IsValidResourceCollection() AS Boolean
Retorna:

Logical: .T. si la colección es válida, .F. en caso contrario

Uso interno:
FUNCTION GetResource(tcResourceName)
    IF !THIS.IsValidResourceCollection()
        RETURN NULL
    ENDIF
    
    LOCAL i
    i = THIS.oResources.GetKey(tcResourceName)
    IF i > 0
        RETURN THIS.oResources.Item(i)
    ENDIF
    RETURN NULL
ENDFUNC

GetResourceNames

Obtiene una lista de todos los nombres de recursos almacenados. Útil para debugging y monitoreo del estado de la aplicación.

FUNCTION GetResourceNames() AS String
Retorna:

String: Lista de nombres de recursos separados por comas, o cadena vacía si no hay recursos

Ejemplo:
* Listar todos los recursos disponibles
LOCAL lcResourceList
lcResourceList = THIS.GetResourceNames()
THIS.LogInfo("Recursos disponibles: {0}", lcResourceList)

* Resultado ejemplo: "DatabaseConnection, AppConfig, Cache, ServerStartTime"

* Usar para validaciones de estado
IF EMPTY(lcResourceList)
    THIS.LogWarning("No hay recursos globales inicializados")
ENDIF

CleanUp

Limpia todos los recursos almacenados y llama al CleanUp del ApiController padre. No se ejecuta automáticamente al detener la aplicación, debe ser llamado explícitamente a menos que se implemente un manejo personalizado en el método OnApplicationStopping.

PROCEDURE CleanUp()
Implementación completa:
PROCEDURE CleanUp()
    DODEFAULT()  && Llama al CleanUp del ApiController padre
    
    TRY
        IF THIS.IsValidResourceCollection()
            * Limpiar recursos individuales si tienen método CleanUp
            LOCAL i, loResource
            FOR i = 1 TO THIS.oResources.Count
                loResource = THIS.oResources.Item(i)
                IF VARTYPE(loResource) == 'O' AND !ISNULL(loResource)
                    IF PEMSTATUS(loResource, "CleanUp", 5)
                        loResource.CleanUp()
                    ENDIF
                ENDIF
            ENDFOR
            
            * Limpiar toda la colección
            THIS.oResources.Remove(-1)
            THIS.oResources = NULL
        ENDIF
    CATCH TO loEx
        * No propagar errores en cleanup
    ENDTRY
ENDPROC

Nota importante:

Este método no se ejecuta automáticamente cuando se libera la instancia. Debe ser llamado explícitamente a menos que se implemente un manejo personalizado en el método OnApplicationStopping.

Integración con ApiController

Los controladores API pueden acceder a los recursos globales a través del método GetGlobalResource que está disponible en todos los controladores que hereden de ApiController:

Ejemplo: Uso de recursos globales en controladores

DEFINE CLASS UsuariosController AS ApiController

    FUNCTION GetUsuarios(req, res) HELP "GET: usuarios"
        * Obtener conexión global de base de datos
        LOCAL loConnection
        loConnection = THIS.GetGlobalResource("DatabaseConnection")
        
        IF ISNULL(loConnection)
            res.Status(500).Json('{"error":"Base de datos no disponible"}')
            RETURN
        ENDIF
        
        * Obtener configuración global
        LOCAL loConfig
        loConfig = THIS.GetGlobalResource("AppConfig")
        LOCAL lnPageSize
        lnPageSize = IIF(ISNULL(loConfig), 10, loConfig.DefaultPageSize)
        
        * Usar la conexión para consultar datos
        LOCAL lcSql, loRecordset
        lcSql = "SELECT TOP " + ALLTRIM(STR(lnPageSize)) + " * FROM usuarios"
        loRecordset = loConnection.Execute(lcSql)
        
        * Procesar resultados...
        LOCAL lcJsonData
        lcJsonData = THIS.RecordsetToJson(loRecordset)
        res.Status(200).Json(lcJsonData)
    ENDFUNC
    
    FUNCTION CreateUsuario(req, res) HELP "POST: usuarios"
        * Validar que hay datos JSON
        IF !req.HasJsonBody()
            res.Status(400).Json('{"error":"Se requiere un cuerpo JSON válido"}')
            RETURN
        ENDIF
        
        * Obtener recursos globales
        LOCAL loConnection, loCache
        loConnection = THIS.GetGlobalResource("DatabaseConnection")
        loCache = THIS.GetGlobalResource("Cache")
        
        IF ISNULL(loConnection)
            res.Status(500).Json('{"error":"Servicio no disponible"}')
            RETURN
        ENDIF
        
        * Crear usuario...
        LOCAL lcSql
        lcSql = "INSERT INTO usuarios (nombre, email) VALUES (?, ?)"
        loConnection.Execute(lcSql, req.json.nombre, req.json.email)
        
        * Invalidar cache si existe
        IF !ISNULL(loCache) AND PEMSTATUS(loCache, "InvalidatePattern", 5)
            loCache.InvalidatePattern("usuarios*")
        ENDIF
        
        res.Status(201).Json('{"message":"Usuario creado correctamente"}')
    ENDFUNC
    
ENDDEFINE

Ejemplo: Implementación completa de ApplicationLifecycle

DEFINE CLASS MyAppLifecycle AS ApplicationLifecycle OLEPUBLIC
    PROCEDURE OnApplicationStarting()
        THIS.LogInfo("Iniciando aplicación...")
        
        TRY
            * 1. Inicializar conexión de base de datos
            THIS.InitializeDatabase()
            
            * 2. Cargar configuración
            THIS.LoadApplicationConfig()
            
            * 3. Inicializar cache
            THIS.InitializeCache()
            
            * 4. Configurar servicios externos
            THIS.InitializeExternalServices()
            
            THIS.LogInfo("Aplicación iniciada correctamente. Recursos: {0}", THIS.GetResourceNames())
            
        CATCH TO loEx
            THIS.LogError("Error al iniciar aplicación: {0}", loEx.Message)
            THROW loEx
        ENDTRY
    ENDPROC
    
    PROCEDURE OnApplicationStopping()
        THIS.LogInfo("Deteniendo aplicación...")
        
        * Cerrar conexiones de base de datos
        LOCAL loConnection
        loConnection = THIS.GetResource("DatabaseConnection")
        IF !ISNULL(loConnection)
            loConnection.Close()
            THIS.LogInfo("Conexión de base de datos cerrada")
        ENDIF
        
        * Limpiar cache
        LOCAL loCache
        loCache = THIS.GetResource("Cache")
        IF !ISNULL(loCache) AND PEMSTATUS(loCache, "SaveToDisk", 5)
            loCache.SaveToDisk()
            THIS.LogInfo("Cache guardado en disco")
        ENDIF
        
        THIS.LogInfo("Aplicación detenida correctamente")
    ENDPROC
    
    PROTECTED PROCEDURE InitializeDatabase()
        LOCAL loConnection
        loConnection = CREATEOBJECT("ADODB.Connection")
        loConnection.Open("Provider=SQLOLEDB;Server=localhost;Database=MyApp;Trusted_Connection=yes")
        THIS.SetResource("DatabaseConnection", loConnection)
        THIS.LogInfo("Conexión de base de datos inicializada")
    ENDPROC
    
    PROTECTED PROCEDURE LoadApplicationConfig()
        LOCAL loConfig
        loConfig = CREATEOBJECT("Empty")
        ADDPROPERTY(loConfig, "DefaultPageSize", 20)
        ADDPROPERTY(loConfig, "MaxUploadSize", 10485760)  && 10MB
        ADDPROPERTY(loConfig, "EnableCache", .T.)
        THIS.SetResource("AppConfig", loConfig)
        THIS.LogInfo("Configuración de aplicación cargada")
    ENDPROC
    
    PROTECTED PROCEDURE InitializeCache()
        LOCAL loCache
        loCache = CREATEOBJECT("ApplicationCache")  && Clase personalizada
        THIS.SetResource("Cache", loCache)
        THIS.LogInfo("Cache de aplicación inicializado")
    ENDPROC
    
    PROTECTED PROCEDURE InitializeExternalServices()
        * Configurar servicios externos, APIs, etc.
        LOCAL loEmailService
        loEmailService = CREATEOBJECT("EmailService")
        loEmailService.Configure("smtp.gmail.com", 587)
        THIS.SetResource("EmailService", loEmailService)
        THIS.LogInfo("Servicios externos configurados")
    ENDPROC
    
ENDDEFINE

Buenas prácticas para ApplicationLifecycle

  • Recursos thread-safe: Asegúrese de que los recursos globales sean seguros para acceso concurrente.
  • Gestión de errores: Implemente manejo robusto de errores en la inicialización y limpieza.
  • Logging detallado: Registre eventos importantes del ciclo de vida para facilitar la depuración y monitoreo.
  • Configuración externa: Use archivos de configuración externos para parámetros de conexión y configuraciones.
  • Cleanup seguro: Asegúrese de que la limpieza de recursos no genere excepciones que puedan afectar el cierre de la aplicación.
  • Inicialización ordenada: Inicialice recursos en orden de dependencia (primero la base de datos, luego servicios que la usen).
  • Validación de recursos: Siempre valide que los recursos existan antes de usarlos en los controladores.
  • Monitoreo de estado: Use GetResourceNames() para monitorear qué recursos están disponibles.