Artículo
· 16 jun, 2023 Lectura de 10 min

Creando un servicio REST en IRIS

Una de las necesidades más comunes por parte de nuestros clientes es la de la creación de servicios REST que permitan acceder a la información presente en IRIS / HealthConnect. La ventaja de estos servicios REST es que permite el desarrollo de interfaces de usuario personalizadas con las tecnologías más actuales aprovechando la fiabilidad y el rendimiento de IRIS en el back-end.

En el artículo de hoy vamos a crear paso a paso un servicio web que nos va a permitir tanto almacenar datos en nuestra base de datos como posteriormente consultarlos. Además, vamos a hacerlo configurando una producción que nos permita controlar en todo momento el flujo de mensajes que estamos recibiendo y así poder controlar el correcto funcionamiento del mismo.

Antes de comenzar únicamente indicaros que tenéis todo el código disponible en Open Exchange para que podáis replicarlo las veces que necesitéis, el proyecto está configurado en Docker, haciendo uso de IRIS Community para que no tengáis que hacer nada más que desplegarlo.

Muy bien, comencemos.

Preparación del entorno

Antes de comenzar con la configuración del servicio REST debemos preparar nuestro entorno de desarrollo, debemos conocer que información vamos a recibir y qué vamos a hacer con ella. Para este ejemplo hemos decidido que vamos a recibir los datos personales en el siguiente formato JSON:

{
    "PersonId": 1,
    "Name": "Irene",
    "LastName": "Dukas",
    "Sex": "Female",
    "Dob": "01/04/1975"
}

Como uno de los objetivos es el de almacenar la información que recibamos vamos a crear una clase de Objectscript que nos permita registrar dicha información en nuestro IRIS. Como veis son unos datos bastante simples, así que la clase no tendrá mayor complicación:

Class WSTEST.Object.Person Extends %Persistent
{

/// ID of the person
Property PersonId As %Integer;
/// Name of the person
Property Name As %String;
/// Lastname of the person
Property LastName As %String;
/// Sex of the person
Property Sex As %String;
/// DOB of the person
Property Dob As %String;
Index PersonIDX On PersonId [ PrimaryKey ];
}

 Perfecto, ya tenemos nuestra clase definida y podemos empezar a trabajar con la misma.

Creación de nuestro endpoint

Ahora que ya tenemos definida la clase de datos con la que vamos a trabajar es el momento de crear nuestra clase de Objectscript que funcionará como endpoint al que se invocará desde nuestro front-end. Veamos la clase de ejemplo que tenemos en nuestro proyecto paso a paso:

Class WSTEST.Endpoint Extends %CSP.REST
{

Parameter HandleCorsRequest = 0;
XData UrlMap [ XMLNamespace = "https://www.intersystems.com/urlmap" ]
{
<Routes>
	<Route Url="/testGet/:pid" Method="GET" Call="TestGet" />
	<Route Url="/testPost" Method="POST" Call="TestPost" />
</Routes>
}

ClassMethod OnHandleCorsRequest(url As %String) As %Status
{
	set url = %request.GetCgiEnv("HTTP_REFERER")
    set origin = $p(url,"/",1,3) // origin = "http(s)://origin.com:port"
    // here you can check specific origins
    // otherway, it will allow all origins (useful while developing only)
	do %response.SetHeader("Access-Control-Allow-Credentials","true")
	do %response.SetHeader("Access-Control-Allow-Methods","GET,POST,PUT,DELETE,OPTIONS")
	do %response.SetHeader("Access-Control-Allow-Origin",origin)
	do %response.SetHeader("Access-Control-Allow-Headers","Access-Control-Allow-Origin, Origin, X-Requested-With, Content-Type, Accept, Authorization, Cache-Control")
	quit $$$OK
}
// Class method to retrieve the data of a person filtered by PersonId
ClassMethod TestGet(pid As %Integer) As %Status
{
    Try {
        Do ##class(%REST.Impl).%SetContentType("application/json")
        If '##class(%REST.Impl).%CheckAccepts("application/json") Do ##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit
        // Creation of BS instance
        set status = ##class(Ens.Director).CreateBusinessService("WSTEST.BS.PersonSearchBS", .instance)

        // Invocation of BS with pid parameter
        set status = instance.OnProcessInput(pid, .response)
       	if $ISOBJECT(response) {
            // Sending person data to client in JSON format
        	Do ##class(%REST.Impl).%WriteResponse(response.%JSONExport())
		}
        
    } Catch (ex) {
        Do ##class(%REST.Impl).%SetStatusCode("400")
        Do ##class(%REST.Impl).%WriteResponse(ex.DisplayString())
        return {"errormessage": "Client error"}
    }
    Quit $$$OK
}
// Class method to receive person data to persist in our database
ClassMethod TestPost() As %Status
{
    Try {
        Do ##class(%REST.Impl).%SetContentType("application/json")
        If '##class(%REST.Impl).%CheckAccepts("application/json") Do ##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit
        // Reading the body of the http call with the person data
        set bodyJson = %request.Content.Read()
        
        // Creation of BS instance
        set status = ##class(Ens.Director).CreateBusinessService("WSTEST.BS.PersonSaveBS", .instance)
       	#dim response as %DynamicObject
        // Invocation of BS with person data
        set status = instance.OnProcessInput(bodyJson, .response)
        
        if $ISOBJECT(response) {
            // Returning to the client the person object in JSON format after save it
            Do ##class(%REST.Impl).%WriteResponse(response.%JSONExport())
	    }
        
    } Catch (ex) {
        Do ##class(%REST.Impl).%SetStatusCode("400")
        Do ##class(%REST.Impl).%WriteResponse(ex.DisplayString())
        return {"errormessage": "Client error"}
    }
    Quit $$$OK
}

}

No os preocupéis si os parece ininteligible, veamos las partes más relevantes de nuestra clase:

Declaración de la clase:

Class WSTEST.Endpoint Extends %CSP.REST

Como véis, nuestra clase WSTEST.Endpoint extiende de %CSP.REST, esto es necesario para poder utilizar dicha clase como endpoint.

Definición de las rutas:

XData UrlMap [ XMLNamespace = "https://www.intersystems.com/urlmap" ]
{
<Routes>
	<Route Url="/testGet/:pid" Method="GET" Call="TestGet" />
	<Route Url="/testPost" Method="POST" Call="TestPost" />
</Routes>
}

En este fragmento de código estamos declarando las rutas que podrán ser invocadas desde nuestro front-end.

Como véis tenemos dos rutas declaradas, la primera de ellas será una llamada GET en la que se nos enviará el parámetro pid que utilizaremos para las búsquedas de personas por su identificador y que será gestionado por el ClassMethod TestGet. La segunda llamada será de tipo POST en la que se nos remitirá la información de la persona que tenemos que grabar en nuestra base de datos y que será tratada por el ClassMethod TestPost.

Echemos un vistazo a ambos métodos:

Recuperando datos de una persona:

ClassMethod TestGet(pid As %Integer) As %Status
{
    Try {
        Do ##class(%REST.Impl).%SetContentType("application/json")
        If '##class(%REST.Impl).%CheckAccepts("application/json") Do ##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit
        // Creation of BS instance
        set status = ##class(Ens.Director).CreateBusinessService("WSTEST.BS.PersonSearchBS", .instance)

        // Invocation of BS with pid parameter
        set status = instance.OnProcessInput(pid, .response)
       	if $ISOBJECT(response) {
            // Sending person data to client in JSON format
        	Do ##class(%REST.Impl).%WriteResponse(response.%JSONExport())
		}
        
    } Catch (ex) {
        Do ##class(%REST.Impl).%SetStatusCode("400")
        Do ##class(%REST.Impl).%WriteResponse(ex.DisplayString())
        return {"errormessage": "Client error"}
    }
    Quit $$$OK
}

En este método podéis ver como hemos declarado en nuestro ClassMethod la recepción del atributo pid que utilizaremos en la posterior búsqueda. Si bien podríamos haber hecho la búsqueda directamente desde esta clase hemos decidido hacerlo dentro de una producción para poder controlar cada una de las operaciones, es por ello que estamos creando una instancia al Business Service WSTEST.BS.PersonSearchBS a la que posteriormente invocamos su método OnProcessInput con el pid recibido. La respuesta que recibiremos será del tipo WSTEST.Object.PersonSearchResponse que transformaremos a JSON antes de enviárselo al peticionario.

Almacenando datos de una persona

ClassMethod TestPost() As %Status
{
    Try {
        Do ##class(%REST.Impl).%SetContentType("application/json")
        If '##class(%REST.Impl).%CheckAccepts("application/json") Do ##class(%REST.Impl).%ReportRESTError(..#HTTP406NOTACCEPTABLE,$$$ERROR($$$RESTBadAccepts)) Quit
        // Reading the body of the http call with the person data
        set bodyJson = %request.Content.Read()
        
        // Creation of BS instance
        set status = ##class(Ens.Director).CreateBusinessService("WSTEST.BS.PersonSaveBS", .instance)
       	#dim response as %DynamicObject
        // Invocation of BS with person data
        set status = instance.OnProcessInput(bodyJson, .response)
        
        if $ISOBJECT(response) {
            // Returning to the client the person object in JSON format after save it
            Do ##class(%REST.Impl).%WriteResponse(response.%JSONExport())
	    }
        
    } Catch (ex) {
        Do ##class(%REST.Impl).%SetStatusCode("400")
        Do ##class(%REST.Impl).%WriteResponse(ex.DisplayString())
        return {"errormessage": "Client error"}
    }
    Quit $$$OK
}

Como en el anterior caso, podríamos haber grabado directamente nuestro objeto persona directamente desde esta clase, pero hemos decidido hacerlo desde un Business Operation que será invocado desde el Business Service WSTEST.BS.PersonSaveBS.

Como podéis observar en el código estamos recuperando la información remitida por el cliente en la llamada POST leyendo el Stream presente en %request.Content. El string obtenido será lo que pasaremos al Business Service.

 

Publicando nuestro Endpoint

Para evitar eternizar este artículo vamos a obviar la explicación relativa a la producción, podéis revisar el código directamente en el proyecto de OpenExchange. A grandes trazos la producción que tenemos configurada es tal que así:

Tenemos 2 Business Services declarados, uno para recibir las peticiones de búsqueda y otro para las peticiones de almacenamiento de los datos de la persona. Cada uno de ellos invocará a su Business Operation oportuno.

Perfecto, repasemos que tenemos cofigurado en nuestro proyecto:

  • 1 clase endpoint que recibirá las peticiones del cliente (WSTest.Endpoint)
  • 2 Business Services que se invocarán desde nuestra clase endpoint (WSTest.BS.PersonSaveBS WSTest.BS.PersonSearchBS).
  • 2 Business Operations encargados de realizar la búsqueda y el grabado de los datos (WSTest.BS.PersonSaveBS WSTest.BS.PersonSearchBS)
  • 4 clases para enviar y recibir los datos dentro de la producción que extienden de Ens.Request y Ens.Response (WSTest.Object.PersonSearchRequestWSTest.Object.PersonSaveRequestWSTest.Object.PersonSearchResponse WSTest.Object.PersonSaveResponse).

Sólo nos queda un último paso para poner en funcionamiento nuestro servicio web y es la publicación del mismo. Para ello accederemos a la opción del Portal de Gestión System Administration -->  Security -> Applications -> Web Applications

Veremos una lista de todas las Web Applications configuradas en nuestra instancia:

Creemos nuestra aplicación web:

Repasemos los puntos que necesitamos configurar:

  • Name: definiremos la ruta que vamos a usar para realizar las invocaciones a nuestro servicio, para nuestro ejemplo será /csp/rest/wstest
  • Namespace: el namespace sobre el que trabajará el servicio web, en este caso hemos creado WSTEST en el que hemos configurado nuestra producción y nuestras clases.
  • Enable Application: habilitación del servicio web para poder recibir llamadas.
  • Enable - REST: al seleccionar REST indicamos que este servicio web está configurado para recibir llamadas REST, al seleccionarlo deberemos definir el Dispatch Class que será nuestra clase endpoint WSTEST.Endpoint.
  • Allowed Authentication Methods: configuración de la autenticación del usuario que realice la llamada al servicio web. En este caso hemos definido que se haga mediante Password, por lo que en nuestro Postman configuraremos el modo Basic Authorization en el que indicaremos el usuario y su contraseña. Si veis tenemos la opción de definir el uso de JWT Authentication lo cual es bastante útil al no exponer los datos del usuario y su contraseña en las llamadas REST, si estáis interesado en profundizar en JWT podéis consultar este artículo.

Una vez que finalicemos la configuración de nuestra aplicación web podremos lanzar un par de pruebas abriendo Postman e importando el fichero WSTest.postman_collection.json presente en el proyecto.

Probando nuestro Endpoint

Con todo configurado en nuestro proyecto y la producción arrancada ya podemos lanzar un par de pruebas sobre nuestro servicio web. Tenemos configurado como usuario peticionario a superuser por lo que no tendremos problemas de cara a grabar y recuperar datos. En caso de usar un usuario diferente deberemos asegurarnos que o bien tiene los roles necesarios asignados o bien se los asignamos en la pestaña de Application Roles de la definición de nuestra aplicación web. 

Empecemos grabando alguna persona en nuestra base de datos:

Hemos recibido un 200, por lo que parece que todo ha ido bien, comprobemos el mensaje en la producción:

Todo ha ido bien, el mensaje con el JSON se ha recibido correctamente en el BS y se ha grabado con éxito en el BO.

Probemos ahora a recuperar los datos de nuestro querido Alexios Kommenos:

¡Bingo! Ahí está Alexios con todos sus datos. Vamos a comprobar el mensaje en la producción:

Perfecto, todo ha funcionado como es debido. Como habéis visto, es realmente sencillo crear un servicio web REST en IRIS, podéis usar este proyecto como base para vuestros futuros desarrollos y si tenéis alguna pregunta no dudéis en dejar un comentario.

Comentarios (4)2
Inicie sesión o regístrese para continuar

Buenos días @Luis Angel Pérez Ramos, muy interesante el artículo, nunca he usado producciones y tengo una duda, ¿Qué beneficios nos aporta usar la producción en lugar de llamar al metodo de la clase directamente?

Veo que se puede explorar la request y la response por las capturas que pones:

En caso de tener miles de peticiones se puede filtrar la petición por algún valor para buscar que se recibió en la petición X del día Y ?

Estos "logs" se almacenan por defecto durante días, meses... para siempre?

¿Nos aporta alguna ventaja adicional el uso de producciones frente a llamar al metodo de una clase directamente?

Muchas gracias!

Buenas @Daniel Aguilar! Pues la principal ventaja de usar producciones es que puedes hacer un seguimiento de las peticiones que has recibido así como el ciclo de vida de esa petición dentro de tu sistema, desde la recepción, transformaciones en BP y almacenamientos u otras operaciones en el BO.

Una vez que esos mensajes se persisten puedes realizar búsquedas por la fuente del mensaje, fechas, cabeceras del mensaje, etc...y estos quedarán almacenados hasta que se programe una tarea de purgado.