IR
irwinrodriguez.dev
Volver a documentación

Referencia API

La superficie real de ApiController, req y res: lo que existe, con su firma. Si un miembro no está aquí, no está.

ApiController

Clase base de la que heredan todos los controladores. Da las utilidades de JSON, los resultados con su código de estado, el registro y los dos ganchos del ciclo de vida. El JWT NO se genera aquí: lo emite el middleware cuando el login contesta con X-Auth-Status.

Métodos de respuesta

Propiedad / MétodoTipoDescripción
Ok(obj)200El resultado normal. Serializa el objeto y responde 200. Es la forma preferible, y la que respeta el responseformat del servidor.
OkRaw(json)200Igual, pero con un JSON que ya tienes hecho. Es el que va detrás de TableToJson().
Created(obj, location)201Para lo que acabas de crear. location es opcional y pone la cabecera Location.
NoContent()204Fue bien y no hay nada que devolver. Típico de un DELETE.
BadRequest(msg, code)400Lo que mandó el cliente no vale. code es tuyo y viaja en el Problem Details.
Unauthorized(msg) · Forbidden(msg)401 · 403Sin identificar, o identificado y sin permiso.
NotFound(msg)404No existe lo que pidió.
Fail(status, msg, code)el que digasCualquier otro error, con el código de estado que le pongas.
newObject(fields)ObjectCrea un objeto VFP con esas propiedades a NULL. fields es una lista separada por comas.
ToJson(obj) · ParseJson(txt)String · ObjectSerializar y deserializar, vía JSONFox. El inverso de ToJson se llama ParseJson: no hay ningún FromJson.
LogInfo · LogWarning · LogError · LogFromExceptionVoidEscriben en el log del servidor. El último vuelca la excepción entera, con su línea.

Qué sale cuando devuelves un error

Desde 0.9.15, los resultados de error se describen con Problem Details (RFC 9457), el mismo formato que usa ASP.NET, y viajan con Content-Type: application/problem+json; charset=utf-8.

{
  "type": "about:blank",
  "title": "Not Found",
  "status": 404,
  "detail": "No existe el artículo ZZZZ",
  "instance": "/articulos/ZZZZ",
  "code": "not_found"
}

detail es tu mensaje, el que le pasaste a this.NotFound(...); code es el tuyo y viaja como miembro de extensión, que es lo que la RFC permite para lo que no es suyo; title es la frase de razón del código y no se traduce, porque es legible por máquina; instance la pone el motor. Los 200 NO cambian: salen sin sobre. Y si tu servidor declara responseformat: foxserver, ese formato tampoco cambia.

Hooks del ciclo de vida

Propiedad / MétodoTipoDescripción
BeforeEndpoint(req, res)BooleanCorre antes de cada endpoint DE ESE CONTROLADOR. Devolver .F. corta la petición. Es donde se abren las tablas.
AfterEndpoint(req, res)VoidCorre después de cada endpoint de ese controlador. Es donde se cierran.

FoxServerRequest

Objeto que encapsula toda la información de la petición HTTP entrante. Disponible como parámetro req en todos los endpoints.

Propiedades

Propiedad / MétodoTipoDescripción
MethodStringGET, POST, PUT, PATCH, DELETE
uriObjectUn OBJETO, no una cadena: Scheme, Host, Port, AbsolutePath, PathAndQuery y AbsoluteUri. La ruta sin la query está en uri.AbsolutePath.
ParamsObjectParámetros de la ruta. req.Params.id desde /{id}, o req.GetParam("id", "") con valor por defecto.
queryObjectQuery string. req.query.page desde ?page=2, o req.GetQuery("page", 1).
HeadersObjectLas cabeceras. Se leen con req.GetHeader("Authorization", "") y se comprueban con req.HasHeader().
RawBodyStringEl cuerpo sin parsear. También req.GetRawBody(). Es lo que hay que leer cuando el cuerpo puede no venir, o cuando una fecha no debe pasar por JSONFox.
jsonObjectEl cuerpo JSON ya parseado. req.HasJsonBody() dice si lo hay, y req.HasJsonError() / req.JsonError si no se pudo parsear.
ContentTypeStringEl Content-Type de la petición.
AuthenticatedUserObjectCon el token ya validado: .id, .name y .role.
ClaimsObjectLos claims extra del token. req.GetClaim(nombre, defecto), req.HasClaim(nombre) y req.ClaimNames().
MultipartData · FormFieldsObjectSubidas y formularios: req.file(campo), req.files(campo), req.HasFiles(), req.GetFormField(campo, defecto).
req.path, req.body, req.remoteIP y req.scheme no existen Y leer una propiedad que no existe devuelve vacío sin dar error, que es la peor forma de equivocarse. La ruta está en req.uri.AbsolutePath, el cuerpo en req.RawBody y el método en req.Method. La IP del cliente NO se expone al controlador.

Métodos

Leer parámetros, cabeceras y el cuerpo JSON
* Un parametro de ruta, con valor por defecto
lcId = req.GetParam("id", "")

* Una cabecera: GetHeader, no req.headers[...]
IF req.HasHeader("X-Api-Key")
    lcKey = req.GetHeader("X-Api-Key", "")
ENDIF

* El JSON del cuerpo, con cuidado: leer una propiedad que no vino
* da error 1734, asi que se comprueba antes o se envuelve en TRY.
IF req.HasJsonBody() AND !ISNULL(req.json)
    TRY
        lcCiudad = NVL(req.json.direccion.ciudad, "")
    CATCH
        lcCiudad = ""
    ENDTRY
ENDIF

FoxServerResponse

Objeto para construir y enviar la respuesta HTTP. Disponible como parámetro res en todos los endpoints. Todos los métodos son encadenables (retornan res).

Propiedad / MétodoDescripción
status(code)Establece el código de estado HTTP (200, 201, 400, 401, 404, 500...)
json(jsonStr)Envia body JSON. Establece Content-Type: application/json automáticamente.
send(text)Envia texto plano o HTML como body.
header(key, value)Agrega o sobreescribe un header de respuesta. Encadenable.
location(url)Establece el header Location. Usar con status(301) o status(302) para redirección.
sendFile(path, disp)Envía un fichero. disp puede ser "inline" (mostrar en el navegador) o "attachment" (forzar la descarga).
events(tipo)Declara un tipo de evento SSE que este endpoint soporta. Sin declararlo, nadie se puede suscribir a él.
emit(tipo, json)Emite un evento SSE desde cualquier endpoint. El framing, el retry y los pings los pone el middleware.

Ejemplo

Responder encadenando los métodos de res
* Standard success response
res.status(200).json(THIS.ToJson(loResult))

* Created with location header
res.status(201)
   .header("Location", "/api/v1/users/" + lcNewId)
   .json(THIS.ToJson(loResult))

* Error response
res.status(400).json('{"error":"Name is required"}')

* File download
res.status(200)
   .header("Content-Disposition", 'attachment; filename="report.pdf"')
   .sendFile("C:\reports\report.pdf", "attachment")

Métodos detallados de ApiController

Gestión de objetos y JSON

Propiedad / MétodoDescripción
NewObject(fields) Crea un objeto VFP con propiedades inicializadas a NULL. fields es una lista separada por comas: "status,data,message".
NewObjectFromCursor(cursor, session) Crea un objeto VFP con la estructura de un cursor. Útil para plantillas basadas en estructuras de tabla.
ToJson(obj) Serializa un objeto VFP a cadena JSON via JSONFox.
ParseJson(jsonStr) Deserializa una cadena JSON a objeto VFP via JSONFox.
TableToJson(cursor, currentRow) Convierte un cursor a JSON. currentRow=.T. devuelve el registro actual como objeto; .F. devuelve todos los registros como array.
TableToJsonObject(cursor, currentRow, session) Igual que TableToJson pero retorna una colección de objetos VFP en lugar de cadena JSON. Útil para manipular datos antes de la serialización final.
MasterToJSON(master, detail, expr, attr) Genera JSON jerárquico maestro-detalle. expr es la condición de join (ej: "master.id = detail.master_id"), attr es el nombre del array de detalle en el JSON.
MasterToJSON: pedidos con sus líneas
* Ejemplo: maestro-detalle con MasterToJSON
SELECT id, fecha, cliente FROM pedidos INTO CURSOR curPedidos
SELECT id, pedido_id, producto, cantidad FROM lineas INTO CURSOR curLineas
lcJson = THIS.MasterToJSON("curPedidos", "curLineas", "curPedidos.id = curLineas.pedido_id", "lineas")
res.status(200).json(lcJson)

Utilidades de texto y formato

Propiedad / MétodoDescripción
Format(template, p0..p10) Sustituye marcadores {0}, {1}, ... en la cadena template. Similar a String.Format de C#.
EscapeCharsToJSON(text) Escapa caracteres especiales para uso seguro en JSON (acentos, barras, comillas).
EscapeCharsToHTML(text) Escapa caracteres especiales para uso en HTML (&, <, >, acentos).
Content(req, source) Serializa source (cursor, objeto o string) al formato que el cliente solicita via Accept header o ?format= (json, xml, csv).
TableToXml(cursor, currentRow) Convierte un cursor a XML.
TableToCsv(cursor, currentRow) Convierte un cursor a CSV.

Utilidades de fecha, codificación e identificadores

Propiedad / MétodoTipoDescripción
NewGuid() String Genera un GUID sin llaves ni guiones. Usar para todas las PKs.
DTOUNX(dt, useMsec) Number Convierte un DateTime VFP a timestamp Unix. useMsec=.T. devuelve milisegundos.
UNXTOD(timestamp) DateTime Convierte un timestamp Unix (segundos o milisegundos) a DateTime VFP.
Base64URLEncode(input) String Codifica en Base64URL (usado en JWT y URLs seguras).
Base64URLDecode(input) String Decodifica desde Base64URL.
Fechas Unix, GUID y Format
* Conversiones de fecha
lnTs = THIS.DTOUNX(DATETIME(), .F.)   && segundos
ldFecha = THIS.UNXTOD(1716825600)

* Identificadores
lcId = THIS.NewGuid()   && "a1b2c3d4e5f647g8h9i0j1k2l3m4n5o6"

* Formato de mensajes
lcMsg = THIS.Format("Usuario {0} accedio a {1} desde {2}", lcUser, lcUrl, lcIp)

Ciclo de vida y recursos

Propiedad / MétodoDescripción
Initialize() Se ejecuta automáticamente al crear una instancia. Configura cPath, carga JSONFox y ApiService.
LoadJsonFox() Carga JSONFox.app si aún no esta disponible. Llamado internamente por Initialize().
GetGlobalResource(name) Obtiene un recurso global compartido (conexiones, configuraciones) a traves del ciclo de vida de la aplicación.
SetWorkingDirectory(path) Establece cPath y llama a Initialize(). Útil para testing fuera del ciclo de FoxServer.
CleanUp() / Release() / Dispose() Libera recursos al finalizar el ciclo de vida del controlador. Invocados automáticamente por FoxServer.

FoxServerRequest: Métodos detallados

Propiedad / MétodoDescripción
GetHeader(key, default) Retorna el valor del header HTTP indicado. Si no existe retorna default.
GetParam(key, default) Retorna un parámetro de ruta URL (ej: {id}). Retorna default si no existe.
GetQuery(key, default) Retorna un parámetro de query string (?key=value). Retorna default si no existe.
GetQueryParams() Retorna todos los parámetros de query como FoxServerTupleDictionary.
HasJsonBody() .T. si la petición tiene body JSON valido y parseado. Verificar antes de acceder a req.json.*.
GetRawBody() Retorna el body de la petición sin parsear. Necesario para verificar firmas de webhooks.
GetFormField(key, default) Retorna el valor de un campo de formulario multipart o urlencoded.
HasFormField(key) .T. si el campo de formulario existe en la petición.
File(field) Retorna el objeto de un archivo subido via multipart. Expone FileName y SaveToFile(path).
Files(field) Retorna una colección de archivos subidos bajo el mismo nombre de campo.
Cabeceras, cuerpo, ficheros subidos y firma de webhook
* Leer headers y params
lcToken = req.GetHeader("Authorization", "")
lcId    = req.GetParam("id", "")
lcPage  = req.GetQuery("page", "1")

* Verificar body antes de acceder
IF req.HasJsonBody()
    lcEmail = req.json.email
ELSE
    res.status(400).json('{"error":"JSON body required"}')
    RETURN
ENDIF

* Subir archivo
loFile = req.File("avatar")
IF !ISNULL(loFile)
    loFile.SaveToFile(THIS.cPath + "uploads\" + loFile.FileName)
ENDIF

* Webhook: raw body para HMAC
lcRaw = req.GetRawBody()
lcSig = req.GetHeader("X-Signature", "")

Colecciones

FoxServerTupleDictionary

Diccionario clave-valor usado para Headers, Params y Query. Acceso case-insensitive.

Propiedad / MétodoDescripción
CountNúmero de entradas.
GetKey(n)Clave en la posición n (base 1).
GetValue(n)Valor en la posición n.
Contains(key).T. si la clave existe.
GetValueByKey(key)Valor por nombre de clave.
Recorrer todos los parámetros de la query
* Iterar todos los query params
loParams = req.GetQueryParams()
FOR lnI = 1 TO loParams.Count
    lcKey = loParams.GetKey(lnI)
    lcVal = loParams.GetValue(lnI)
    THIS.LogInfo(THIS.Format("Param {0}={1}", lcKey, lcVal))
ENDFOR

FoxServerTuple

Par clave-valor simple. Componente interno de FoxServerTupleDictionary.

Propiedad / MétodoDescripción
KeyNombre de la clave.
ValueValor asociado.

Siguiente: Preguntas frecuentes →