DEV Community

loading...

Um microblog usando Protheus - Rest Server, parte 2, classe para serviço básico

Josimar Junior
Updated on ・13 min read

Introdução

Na primeira parte 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.

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 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.
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:

; REST CONFIG
[HTTPV11]
ENABLE=1
SOCKETS=HTTPREST
; ADVPL=1
Enter fullscreen mode Exit fullscreen mode
  • 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.
[HTTPREST]
PORT=18085
URIS=URI
SECURITY=1
Enter fullscreen mode Exit fullscreen mode

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.

[URI]
URL=/rest
PREPAREIN=99,01
INSTANCES=1,2,1,1
CORSENABLE=1
ALLOWORIGIN=*
ENVIRONMENT=p12microblog
Enter fullscreen mode Exit fullscreen mode

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

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:

#include "protheus.ch"
#include "restful.ch"
Enter fullscreen mode Exit fullscreen mode

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.

wsrestful Perfis description "Trata a atualização dos perfis que usam o microblog"
.
.
.
end wsrestful
Enter fullscreen mode Exit fullscreen mode

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.

    wsdata pageSize         as integer optional
    wsdata page             as integer optional
    wsdata perfilId         as character optional
Enter fullscreen mode Exit fullscreen mode

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:

    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"
Enter fullscreen mode Exit fullscreen mode

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:

    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}"
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

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. 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

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
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

DELETE - exclui um item

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
Enter fullscreen mode Exit fullscreen mode

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

A imagem a seguir mostra os detalhes do serviço de Perfis.
Métodos disponíveis no serviço de Perfis

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.

Discussion (0)