Um microblog usando Protheus - Rest Server, parte 2, classe para serviço básico
Tags: protheus, rest, api, advpl
Published: 2020-11-29 23:06:20

# Introdução
Na [primeira parte](https://dev.to/josimar_jr_/um-microblog-usando-protheus-rest-server-parte-1-5999) foi dito sobre o que seria construído e o que seria abordado e explicado nessa série de publicações.
Essa será a primeira publicação que conterá código e informações sobre a configuração do ambiente e o modus operandi para a construção do microblog.
Para o melhor aproveitamento nesta publicação é esperado a pessoa lendo tenha uma compreensão razoável de como o Protheus funciona com relação aos dicionários e tabelas/aliases, assim como entendimento intermediário sobre linguagem de programação Advpl.
Algumas definições e conceitos podem ter links de documentação e espero conseguir prover o máximo dessas referências para ajudar.

# Ambiente
O ambiente é o Protheus release 12.1.27 expedido em Outubro com os binários DbAccess, Appserver e Smarclient e a lib também expedidos junto com essa release. Portanto para replicar o ambiente de desenvolvimento pode-se iniciar uma base e seguir os passos com os recursos utilizados e mencionados no decorrer das publicações ou utilizar o arquivo de backup mencionado no repositório do Github com a construção do [microblog](https://github.com/josimar-jr/microblog-protheus).

# Estrutura de tabelas
Para a construção dos recursos do microblog foram criadas três tabelas:
- ZT0: conterá informações relacionadas com os perfis;
- ZT1: conterá informações sobre as publicações e comentários e;
- ZT2: conterá informações sobre os perfis que seguem uns aos outros.
Os dicionários de tabelas (SX2), índices (SIX) e campos (SX3) estão disponíveis na forma de arquivos .dtc no repositório do microblog para a consulta e réplica da configuração das tabelas, contudo não há segredo e um diagrama simplificado dos relacionamentos é exibido a seguir.
![Diagrama indicando os campos e relacionamentos das tabelas ZT0, ZT1 e ZT2](https://dev-to-uploads.s3.amazonaws.com/i/fgh964i1wumhumj5n9h4.jpg)
Com o diagrama é possível perceber que poucas regras serão aplicadas contudo, é seguro dizer que serão regras e validações o suficiente para entender onde melhor aplicá-las quando respondendo à requisições por api.
O diagrama simplificado mostra os relacionamentos da tabela de Perfis (ZT0) com as Publicações (ZT1) e com a tabela que guardará os Perfis seguidores e seguidos (ZT2).
O principal campo utilizado para relacionamento nessas tabelas é o ZT0_USRID, este será o id de um usuário do Protheus, sim um usuário do sistema. O principal motivo para utilizar um usuário do sistema é conseguir com a combinação de email (ou id) e senha realizar a autenticação no sistema (login no Protheus).
Além deste campo como chave, a tabela de Publicações tem o próprio campo chave ZT1_ID que terá um valor aleatório sendo gerado e conferido para não existir conflitos na base e a última tabela tem um chave composta por id de usuário seguidor e id de usuário seguido.
Essa é a estrutura de tabelas que conterá as principais operações e apoiará a construção do microblog.

# Configuração do servidoe de Rest Protheus
Os detalhes de configuração podem ser conseguidos em um dos links mencionados no tópico anterior, que é esta [documentação aqui](https://tdn.totvs.com/pages/viewpage.action?pageId=185747842).
As seções que vou destacar a importância são:
`[HTTPREST]`: aqui é determinado qual ips/dns o Appserver irá ouvir e aguardar pelas requisições.
Nesta série de publicações é utilizada a seguinte configuração:
```ini
; REST CONFIG
[HTTPV11]
ENABLE=1
SOCKETS=HTTPREST
; ADVPL=1
```
- `SOCKETS=HTTPREST` => determina qual a seção seguinte para as demais configurações que neste caso será a seção `[HTTPREST]`;
- `ADVPL=1` => determina qual modelo de accept de requisições será utilizado `1` é o Advpl e `0` é o misto de TLPP com binário. Neste caso a chave está comentada e portanto deixará o que produto decidir usar, que por enquanto é o modelo Advpl. Mais detalhes dessa configuração veja este [artigo](https://tdn.totvs.com/display/framework/Entendendo+as+novidades+do+REST).
```ini
[HTTPREST]
PORT=18085
URIS=URI
SECURITY=1
```
Na configuração para o microblog foi definida a porta como 18085 (com a chave `PORT=18085`) e habilitada a segurança no servidor Rest (com a chave SECURITY=1), com isso as requisições por padrão estão "seguras" e exigem que seja provido algum tipo de autenticação para que a resposta aconteça.
A chave `URIS=URI` define qual URLs vão ser estabelecidas como path raíz para a montagem das URLs/paths dos métodos.
```ini
[URI]
URL=/rest
PREPAREIN=99,01
INSTANCES=1,2,1,1
CORSENABLE=1
ALLOWORIGIN=*
ENVIRONMENT=p12microblog
```
Essa é a seção que possui a maior quantidade de configurações e onde boa parte dos problemas de configuração surgem, portanto item a item será explicado.
- `URL=/rest` => indica o começo da URL que o servidor irá aguarda e responder às requisições. Pode ser configurado com `/` e portanto logo após a raiz será o path para o método/classe. Aqui foi configurado como `/rest` pelo hábito. Este é o endereço que a página com a lista de serviços é exibida e pode ser consultada.
- `PREPAREIN=99,01` => determina o grupo de empresa e filial que as threads para resposta terão o ambiente preparado. Caso a empresa seja diferente de `99` garanta que tenha licenças o suficiente para a preparação destes ambientes. É possível também definir como `ALL` contudo isso traz uma necessidade das requisições começarem a incluir o header `tenantid: 99,01` para que seja possível determinar qual o grupo e filial responsável por responder a requisição, quando acontece de ter a configuração como `ALL` e não é informado o `tenantid` na requisição, qualquer thread poderá responder e com isso a primeira thread livre é que fará a resposta. Em situações de negócio, quando filial já é determinante para encontrar registros, ter a resposta acontecendo ao "acaso" considerando grupo e filial, definitivamente não é um risco que vale correr.
- `INSTANCES=1,2,1,1` => aqui são indicados o limite inferior e superior de threads para responder às requisições. Os últimos dois parâmetros indicam a quantidade para tentar deixar livre e quantidade para incrementar quando necessário. Um exemplo onde é possível explicar melhor é `10,50,2,5` que significa suba imediatamente `10` threads, pode subir até `50` threads, tente deixar `2` threads livres e quando não conseguir prepare `5` novas threads.
- `CORSENABLE=1` => essa é a configuração que permite aplicações clientes do rest server exibir conteúdo respondido pelo Rest Advpl nos navegadores. Essa chave é essencial para aplicações Angular, React ou Vuejs que façam requisições ao servidor Protheus.
- `ALLOWORIGIN=*` => configuração adicional à chave `CORSENABLE` aqui são indicados quais os hosts (ip ou dns do servidores) podem exibir o conteúdo respondido. O valor `*` determina que pode ser exibido por qualquer endereço, contudo uma configuração útil de exemplo é `ALLOWORIGIN=meuapp.company.com,appxyz.serverabc.com.br`.
- `ENVIRONMENT=p12microblog` => chave que indica qual o ambiente terá as threads preparadas. Este é outro elemento comum de problemas na configuração, pois eventuais problemas no ambiente indicado aqui (como acessos de usuários, limitação de licenças e uso simultâneo por Smartclient) afetarão a execução dos métodos e classes rest no Protheus.
A imagem a seguir mostra como verificar se a configuração inicial está correta.

![Página com os serviços rest disponíveis para serem utilizados](https://dev-to-uploads.s3.amazonaws.com/i/yctcusgnwzik1nnel8kx.jpg)

# Escrevendo os serviços
Este é o primeiro serviço sendo escrito e portanto terá a maior quantidade de detalhes oferecidos, os demais irão se basear na explicação contida nos próximos parágrafos.
Os arquivos de header exigidos para a compilação do arquivo `.prw` com a classe para o serviço rest são:
```ch
#include "protheus.ch"
#include "restful.ch"
```

## Definição da classe
A indicação do nome da classe `Perfis` e qual a descrição para exibição na página de serviços.
```c
wsrestful Perfis description "Trata a atualização dos perfis que usam o microblog"
.
.
.
end wsrestful
```
A definição das propriedades na classe `Perfil` que serão preenchidas e podem ser utilizadas para a montagem da resposta. Essas propriedades serão preenchidas quando vierem elementos com o mesmo nome como parâmetros de path e query ou no header da requisição, os listados a seguir serão exemplos nos parâmetros de path e query.
```c
    wsdata pageSize         as integer optional
    wsdata page             as integer optional
    wsdata perfilId         as character optional
```
Na construção acima as propriedades são opcionais e portanto caso não estejam presentes a execução não é interrompida ainda na camada de framework. É importante definir o tipo de dado para que não seja exigido a checagem ou conversão na camada de resposta, os tipos disponíveis são .
As definições dos métodos que irão responder para a URL `host:port/rest/microblog/v1/perfis` quando `GET` ou `POST` acontece da seguinte forma:
```c
    wsmethod GET V1ALL description "Recupera todos os perfis" wssyntax "/microblog/v1/perfis" path "/microblog/v1/perfis"
    wsmethod POST V1ROOT description "Cria um perfil para o microblog" wssyntax "/microblog/v1/perfis" path "/microblog/v1/perfis"
```
Os valores `GET V1ALL` e `POST V1ROOT` associados com `wsmethod` são as identificações dos métodos construídos para responder a requisição. A marcação `wssyntax` estabelece a URI que ficará visível na página de serviços e a marcação `path` estabelece o endereço de URI que o método responderá.
Os métodos de HTTP fazem parte da identificação do método e portanto não é possível fazer um http `POST` ser respondido pelo método `GET XYZ`.
As definições dos métodos que irão responder para a URL `host:port/rest/microblog/v1/perfis/{perfilId}` quando `GET`, `PUT` ou `DELETE` são:
```c
    wsmethod GET V1ID description "Recupera um perfil pelo id" wssyntax "/microblog/v1/perfis/{perfilId}" path "/microblog/v1/perfis/{perfilId}"
    wsmethod PUT V1ID description "Faz a atualização de um perfil" wssyntax "/microblog/v1/perfis/{perfilId}" path "/microblog/v1/perfis/{perfilId}"
    wsmethod DELETE V1 description "Faz a exclusão de um perfil" wssyntax "/microblog/v1/perfis/{perfilId}" path "/microblog/v1/perfis/{perfilId}"
```
O quê há de diferente com os primeiros métodos? A identificação dos verbos http e a inclusão da expressão `/{perfilId}` nos paths e isso define limites e comportamentos importantes. A primeira coisa é que esta expressão faz com que uma requisição como `GET /rest/microblog/v1/perfis/xxx001` seja respondida pelo método `GET V1ID` e não pelo método `GET V1ALL`. A segunda é que o valor da propriedade `perfilId` será `xxx001`.
Estas são as definições dos métodos e URIs que serão respondidas por esta classe.
As boas práticas envolvidas até aqui foram:
- definir versionamento no path/uri: é um formato que possui contestações, contudo é mais simples de perceber qual o serviço/método/classe/assinatura devem ser utilizados.
- incluir o nome da classe no path: neste exemplo a classe é `Perfis` e faz parte da combinação de agrupador de path escolhido `microblog/v1/perfis`.
## Definição dos métodos
A seguir a implementação dos métodos são mostradas e os comandos relacionados com o rest são explicados.

### POST - inclui um item
```c
wsmethod POST V1ROOT wsservice Perfis
    local lProcessed as logical
    local jBody      as object
    local jResponse  as object

    lProcessed := .T.
    self:SetContentType("application/json")

    jBody := JsonObject():New()
    jBody:FromJson(self:GetContent())

    jResponse := JsonObject():New()

    if (jBody["email"] == Nil .Or. jBody["user_id"] == Nil .Or. jBody["name"] == Nil)
        jResponse["error"] := "body_invalido"
        jResponse["description"] := "Forneça as propriedades 'email', 'user_id' e 'name' no body"

        self:SetResponse(jResponse:ToJson())
        SetRestFault(400, jResponse:ToJson(), , 400)
        lProcessed := .F.
    else
        DBSelectArea("ZT0")
        Reclock("ZT0", .T.)
            ZT0->ZT0_FILIAL := xFilial("ZT0")
            ZT0->ZT0_EMAIL  := jBody["email"]
            ZT0->ZT0_USRID  := jBody["user_id"]
            ZT0->ZT0_NOME   := jBody["name"]
        ZT0->(MsUnlock())

        jResponse["email"]   := ZT0->ZT0_EMAIL
        jResponse["user_id"] := ZT0->ZT0_USRID
        jResponse["name"]    := ZT0->ZT0_NOME
        // jResponse["inserted_at"] := ZT0->S_T_A_M_P_
        // jResponse["updated_at"] := ZT0->I_N_S_D_T_

        self:SetResponse(jResponse:ToJson())
    endif

return lProcessed
```
Neste método o objetivo é recuperar o conteúdo enviado no body pela requisição e criar um registro na tabela quando o conteúdo é válido. Os trechos relacionados com rest em Advpl são:
`wsmethod POST V1ROOT wsservice Perfis` => indicação do corpo do método definido anteriormente na classe.
`self:SetContentType("application/json")` => define que a resposta terá o conteúdo como `application/json`.
`jBody:FromJson(self:GetContent())` => preenche a variável `jBody` com o conteúdo recebido no body da requisição.
`if (jBody["email"] == Nil .Or. jBody["user_id"] == Nil .Or. jBody["name"] == Nil)` => forma com que os conteúdos das propriedades no body estão sendo validados, quando alguma destes valores não foi informado é considerada uma requisição inválida.
`self:SetResponse(jResponse:ToJson())` => definição da resposta quando percebido erro no body.
`SetRestFault(400, jResponse:ToJson(), , 400)` => definição do status HTTP de erro da requisição.
`jResponse["email"]   := ZT0->ZT0_EMAIL` => montagem do json de resposta à requisição quando há sucesso na operação. A atribuição é repetida para as outras propriedades do json de resposta.
`self:SetResponse(jResponse:ToJson())` => define a resposta que deve acontecer.
`return lProcessed` => indica se o processamento aconteceu com sucesso ou com erro/falha, isso indicará se o `status HTTP` definido pela função `SetRestFault` deve ser considerado. Neste exemplo quando um body é inválido (por exemplo não contém a propriedade `email`) é retornado status HTTP `400`.

### GET geral - retorna uma lista
```c
wsmethod GET V1ALL wsreceive page, pageSize wsservice Perfis
    local lProcessed as logical
    local jResponse  as object
    local jTempItem  as object
    lProcessed := .T.

    // Define o tipo de retorno do método
    self:SetContentType("application/json")

    // As propriedades da classe receberão os valores enviados por querystring
    // exemplo: http://localhost:18085/rest/microblog/v1/perfis?page=1&pageSize=5
    default self:page := 1
    default self:pageSize := 5

    DbSelectArea("ZT0")
    DbSetOrder(3) // ZT0_FILIAL+ZT0_NOME
    DbSeek(xFilial("ZT0"))

    // exemplo de retorno de uma lista de objetos JSON
    jResponse := JsonObject():New()
    jResponse['items'] := {}
    while ZT0->(!EOF())
        aAdd(jResponse['items'], JsonObject():New())
        jTempItem := aTail(jResponse['items'])

        jTempItem["email"]   := ZT0->ZT0_EMAIL
        jTempItem["user_id"] := ZT0->ZT0_USRID
        jTempItem["name"]    := ZT0->ZT0_NOME
        // jTempItem["inserted_at"] := ZT0->S_T_A_M_P_
        // jTempItem["updated_at"] := ZT0->I_N_S_D_T_
        ZT0->(DbSkip())
    end

    self:SetResponse(jResponse:ToJson())
return lProcessed
```
`wsreceive page, pageSize` => este trecho determina que as propriedades `page` e `pageSize` podem ser preenchidas com o conteúdo oferecido pelos parâmetros de path ou query. Neste caso são de query indicador por `?page=2&pageSize=15` na montagem da URL requisitada.
`default self:page := 1` => como parâmetros de query não são obrigatórios o default garante algum valor. Estes parâmetros somente serão usados em versões mais sofisticadas, pois seria complicado implementar paginação com esta versão simplificada do serviço rest em Advpl.
`jResponse['items'] := {}` => a resposta ao serviço é uma lista então é definida a propriedade `items` como `array` em Advpl.
`aAdd(jResponse['items'], JsonObject():New())` => um novo item é adicionado e recebe uma instância da classe [`JsonObject`](https://tdn.totvs.com/display/tec/Classe+JsonObject). Com o JsonObject é possível montar json de forma simplificada.
`jTempItem := aTail(jResponse['items'])` => recupera o último `item` adicionado ao `array`.
`jTempItem["email"]   := ZT0->ZT0_EMAIL` => atribui as propriedades do item corrente ao json de resposta.
`self:SetResponse(jResponse:ToJson())` => responde com a lista de itens recuperados da tabela ZT0.

### GET um - retorna um item
```c
wsmethod GET V1ID pathparam perfilId wsservice Perfis
    local lProcessed as logical
    local jResponse  as object

    lProcessed := .T.
    self:SetContentType("application/json")

    DbSelectArea("ZT0")
    DbSetOrder(2) // ZT0_FILIAL+ZT0_USRID

    jResponse := JsonObject():New()

    // Id não ser vazio e existir como item na tabela
    lProcessed := (!(Alltrim(self:perfilId) == "") .And. ZT0->(DbSeek(xFilial("ZT0")+self:perfilId)))
    if lProcessed

        jResponse["email"]   := ZT0->ZT0_EMAIL
        jResponse["user_id"] := ZT0->ZT0_USRID
        jResponse["name"]    := ZT0->ZT0_NOME
        // jResponse["inserted_at"] := ZT0->S_T_A_M_P_
        // jResponse["updated_at"] := ZT0->I_N_S_D_T_

        self:SetResponse(jResponse:ToJson())
    else
        jResponse["error"] := "id_invalido"
        jResponse["description"] := i18n("Perfil não encontrado utilizando o #[id] informado", {self:perfilId})

        self:SetResponse(jResponse:ToJson())
        SetRestFault(404, jResponse:ToJson(), , 404)
        lProcessed := .F.
    endif

return lProcessed
```
`pathparam perfilId` => este trecho na indicação de início do corpo do método determina o preenchimento da propriedade `perfilId` da classe `Perfil`. O parâmetro de path é obrigatório e portanto é seguro fazer o uso da propriedade sem receios.
`ZT0->(DbSeek(xFilial("ZT0")+self:perfilId))` => este é um exemplo do uso da propriedade diretamente em uma pesquisa/posicionamento de registro na tabela ZT0.
Essa foi a última particularidade relacionada com classes e métodos para serviços rest e montagem de respostas destes serviços.
Os demais métodos não terão suas particularidades em puro Advpl comentadas.
### PUT - altera um item e retorna este item
```c
wsmethod PUT V1ID pathparam perfilId wsservice Perfis
    local lProcessed as logical
    local jResponse  as object

    lProcessed := .T.
    self:SetContentType("application/json")

    DbSelectArea("ZT0")
    DbSetOrder(2) // ZT0_FILIAL+ZT0_USRID

    jResponse := JsonObject():New()

    // Id não ser vazio e existir como item na tabela
    lProcessed := (!(Alltrim(self:perfilId) == "") .And. ZT0->(DbSeek(xFilial("ZT0")+self:perfilId)))
    if lProcessed

        jBody := JsonObject():New()
        jBody:FromJson(self:GetContent())

        if (jBody["name"] == Nil)
            jResponse["error"] := "body_invalido"
            jResponse["description"] := "Forneça a propriedade 'name' no body"

            self:SetResponse(jResponse:ToJson())
            SetRestFault(400, jResponse:ToJson(), , 400)
            lProcessed := .F.
        else

            Reclock("ZT0", .F.)
                ZT0->ZT0_NOME   := jBody["name"]
            ZT0->(MsUnlock())

            jResponse["email"]   := ZT0->ZT0_EMAIL
            jResponse["user_id"] := ZT0->ZT0_USRID
            jResponse["name"]    := ZT0->ZT0_NOME
            // jResponse["inserted_at"] := ZT0->S_T_A_M_P_
            // jResponse["updated_at"] := ZT0->I_N_S_D_T_

            self:SetResponse(jResponse:ToJson())
        endif
    else
        jResponse["error"] := "id_invalido"
        jResponse["description"] := i18n("Perfil não encontrado utilizando o #[id] informado", {self:perfilId})

        self:SetResponse(jResponse:ToJson())
        SetRestFault(404, jResponse:ToJson(), , 404)
        lProcessed := .F.
    endif

return lProcessed
```
### DELETE - exclui um item
```c
wsmethod DELETE V1 pathparam perfilId wsservice Perfis
    local lProcessed as logical
    local lDelete    as logical
    local jResponse  as object

    lProcessed := .T.
    self:SetContentType("application/json")

    DbSelectArea("ZT0")
    DbSetOrder(2) // ZT0_FILIAL+ZT0_USRID

    jResponse := JsonObject():New()

    // Id não ser vazio e existir como item na tabela
    varinfo("id", self:perfilId)
    lProcessed := !(Alltrim(self:perfilId) == "")
    if lProcessed

        // Se não encontrar o registro, não faz nada e retorna verdadeiro
        lDelete := ZT0->(DbSeek(xFilial("ZT0")+self:perfilId))
        if lDelete
            Reclock("ZT0", .F.)
                DbDelete()
            ZT0->(MsUnlock())
        endif

        self:SetResponse("{}")
    else
        jResponse["error"] := "id_invalido"
        jResponse["description"] := i18n("Perfil não encontrado utilizando o #[id] informado", {self:perfilId})

        self:SetResponse(jResponse:ToJson())
        SetRestFault(404, jResponse:ToJson(), , 404)
        lProcessed := .F.
    endif

return lProcessed
```
A imagem a seguir mostra o serviço de Perfis na lista.
![Serviço de Perfis sendo exibido na lista de serviços Rest disponíveis](https://dev-to-uploads.s3.amazonaws.com/i/e3gd6ofddvddp6er95a5.jpg)

A imagem a seguir mostra os detalhes do serviço de Perfis.
![Métodos disponíveis no serviço de Perfis](https://dev-to-uploads.s3.amazonaws.com/i/xk31q9i7z3vuwyvlhwyd.jpg)

# Conclusão
Este é um modo de ter as operações básicas de CRUD acontecendo em determinada tabela do sistema Protheus usando serviços Rest.
Essa implementação falha em diversos conceitos e técnicas para evitar duplicidade de código, organização e design de componentes internos e principalmente cria alto acoplamento entre ler e traduzir a requisição para uma entidade Perfil e gravar isso na tabela. Esse alto acoplamento não será endereçado tão logo.
O próximo passo será mostrar como funciona a exposição de serviços rest quando utilizado modelos MVC e qual a diferença para o CRUD construído para a tabela ZT0.