04. APIs de acesso e transformações de dados

Dentro do REST Framework existem responsabilidades que são do Broker e outras que são controlador/ações que serão criados pelos desenvolvedores do ERP e consumidos pelos desenvolvedores de outras aplicações. Abaixo detalhamos as responsabilidades de cada um deles.

Broker

  1. Prover uma forma compreensiva de realizar a ligação entre operações de recursos e rotas.
  2. Tratar a camada do protocolo HTTP, gerando respostas no formato adequado.
  3. Controlar um conjunto de sessões Stateless que serão utilizadas para executar os métodos dos objetos controladores.
  4. Prover um padrão de documentação da API, assim como a ferramenta para extração e visualização dessa documentação.
  5. Possibilitar uma gestão de exceções e erros compatível com o modelo REST.
  6. Possibilitar o rastreamento de erros após a execução pelo suporte por meio de um ticket de erro.
  7. Prover facilidades para realizar validações das regras de negócio e segurança do sistema.
  8. Prover facilidades para realizar transformações de objetos de negócio, fontes de dados, entidades do modelo de dados, chaves, DataSets e erros em JSON.

Controlador/ações

  1. Manter a integridade relacional entre os dados enviados pelo cliente e os já existentes no banco de dados.
  2. Realizar todas as validações necessárias, incluíndo segurança, para garantir a integridade dos dados da requisição, fazendo uso de APIs de alto nível disponibilizadas pelo Framework.
  3. Realizar validação de regras de negócio.

Observar que o controlador deve tratar a requisição e a geração de resposta, apesar que, é de responsabilidade dele também, realizar validações de integridade, de regras de negócio e de segurança, mas essas lógicas não devem ser implementadas no controlador em si. Ele deve delegar esse papel a outras classes, como os objetos de gestão.

Ações básicas

Todo objeto controlador, em nosso framework, deve implementar um conjunto de ações básicas, equivalentes às ações básicas CRUD usadas em entidades de um SGBD relacional:

  • Insere
  • Obtém
  • Atualiza
  • Apaga
O protocolo HTTP define um conjunto de semânticas para os seus métodos, que necessitam ser respeitadas numa aplicação RESTful. Cada método do HTTP é classificado de duas formas: seguro e/ou idempotente. Um método é dito como seguro se ele não modifica um recurso. Idempotente é o método que tem o mesmo resultado independentemente do número de vezes que ele é chamado. Ao respeitar essas semânticas, a aplicação pode tirar proveito da infraestrutura já existente na Web, como o cache de servidores proxy. No entanto, por decisões de arquitetura, é comum vermos aplicações que não seguem fielmente o protocolo REST, podendo ser dito que elas são baseadas no REST. As regras definidas para o comportamento dos métodos HTTP são as seguintes:

Método Escopo Semântica Idempotente Seguro
 GET Recurso Obtém um único recurso Sim Sim
 GET Coleção Obtém todos os recursos Sim Sim
 HEAD Recurso Obtém um único recurso(somente cabeçalho) Sim Sim
 HEAD Coleção Obtém todos os recursos(somente cabeçalho) Sim Sim
 POST Coleção Insere um novo recursos numa coleção Não Não
 PUT Recurso Atualiza um recurso Sim Não
 PATCH Recurso Atualiza um recurso Não Não
 DELETE Recurso Apaga um recurso Sim Não
 OPTIONS Qualquer Obtém as ações disponíveis para o recurso Sim Sim


A Idempotência é importante na construção de uma API robusta. Se a aplicação cliente, ao atualizar um recurso, chamar o método POST várias vezes, irá resultar numa atualização incorreta. Como deve reagir uma aplicação cliente se, ao enviar uma requisição POST, ocorrer um timeout? É seguro para a aplicação enviar a requisição novamente, ou ela precisa antes verificar o status do recurso? Usando métodos idempotentes, não é necessário responder essas questões, pois é padronizado que para esses métodos é seguro reenviar a requisição até receber uma resposta do servidor.  PUT e POST são ambos inseguros, sendo que PUT é idempotente, ao contrário de POST.

Outras ações que alteram um recurso

Em geral, nem toda as ações sobre um recurso podem ser representadas por meio das operações básicas CRUD acima descritas. Para representar essas ações específicas sobre um determinado recurso, deve-se criar uma rota específica, acrescentando um verbo no tempo infinitivo ao final da URL do recurso que sofrerá a ação. Por exemplo, em nosso recurso Pedido temos a ação de aprovar. Uma possível abordagem seria criar a rota abaixo:

method: 'POST', url: '/api/operacoes/v1/pedidos/:chcriacao/aprovar', action: 'aprovar(chcriacao)'

Apesar dessa abordagem ser válida, dê preferência a tentar criar uma representação de estado para uma ação. No exemplo acima, a aprovação de um pedido pode ser modelado como o estado "aprovacao" que pode ser obtido para saber se um pedido está aprovado ou não, pode ser criado (aprovar) e excluído (desaprovar). Nessa abordagem, as rotas seriam:

method: 'GET', url: '/api/operacoes/v1/pedidos/:chcriacao/aprovacao', action: 'obterAprovacao(chcriacao)'method: 'POST', url: '/api/operacoes/v1/pedidos/:chcriacao/aprovacao', action: 'aprovar(chcriacao)'
method: 'DELETE', url: '/api/operacoes/v1/pedidos/:chcriacao/aprovacao', action: 'desaprovar(chcriacao)'

Modelando relações semânticas

Relações entre recursos podem ser representadas por intermédio de links, ou usando sub-coleções. As seguintes regras devem ser usadas para se obter uma consistência na API.

Numa relação 1:N, onde o objeto alvo possui uma relação de dependência e não pode existir sem que exista o recurso principal, ele deve ser representado como uma sub-coleção. Por exemplo: uma operação de pedido ou provisão estabelece uma relação de um 1:N com os seus itens e esses somente podem existir se a operação existir. Para esse caso, os itens devem ser modelados como uma sub-coleção:

method: 'GET', url: '/api/operacoes/v1/pedidos/:chcriacao/itens/:chave', action: 'obterItem(chcriacao, chave)'

Numa relação 1:N, onde o dado é associado à ligação, ele deve ser representado como uma sub-coleção. Estamos nos referindo ao dado que não pertence nem ao recurso principal, nem ao recurso alvo, como as classes de vínculos. Por exemplo: os vínculos entre os produtos e suas imagens devem ser modelados como uma sub-coleção:

method: 'GET', url: '/api/cadastros/v1/recursos/:chave/imagens', action: 'listarImagens(chave)'

Em qualquer outra relação 1:N, é recomendado o uso de links relacionado os recursos. O exemplo mais comum dessa relação são os campos lookup, onde a propriedade de um recurso guarda apenas uma identificação para outro. Por padrão, o Framework criará links para os campos lookup, comportamento que será detalhado mais a frente deste documento. Em versões futuras do Framework, será possibilitado outros tipos de links entre os recursos, possibilitando a construção de APIs HATEOAS.

No caso de uma relação N:M, é recomendado o uso de uma sub-coleção, definida na relação de busca mais comum. Se a procura ocorre frequentemente em ambas as relações, duas sub-coleções podem ser definidas. Por exemplo: a relação entre grupos e usuários é N:M. Ambas as rotas abaixo são adequadas para permitir o acesso dessas informações:

method: 'GET', url: '/api/seguranca/v1/grupos/:grupo/usuarios, action: 'listarUsuarios(grupo)'
method: 'GET', url: '/api/seguranca/v1/usuarios/:usuario/grupos, action: 'listarGrupos(usuario)'

API de acesso as entidades do modelo da dados (Business Data Objects)

Juntamente com o REST Framework, foi disponibilizada uma nova API de acesso aos registros das classes do modelo de dados. Essas classes permitem que o desenvolvedor manipule um registro com todas as regras de negócio definidas nos arquivos x-class da classe, arquivos esses agora chamados de x-model. Também são validadas as permissões de modificação e visualização do usuário. O objetivo dessa nova API é prover ao desenvolvedor o mesmo nível de validação que ocorre nas grades do Web Framework, sendo assim elas poderão ser utilizadas também no WebFramework e nos objetos de gestão. As novas classes são:

  • bdo.orm.Entity: Permite o acesso e a manipulação de um registro de uma classe de dados.
  • bdo.orm.EntitySet: Permite o acesso e a manipulação de um conjunto de registros de uma classe de dados, como o dataset “pedido” da OperacaoPedido.

Para manter compatibilidade com as APIs que utilizam o DataSet para manipulação de dados foi necessário criar um método que pudesse receber um DataSet chamado de fromDataSet.

Exemplo de uso com DataSet utilizando bdo.orm.Entity.fromDataSet:

ControladorPedido.prototype.cabecalhoPedido = bdo.orm.Entity.fromDataSet(classeDePedido, ds, { 
  fields: function (fld) {                                                             
    return fld.ehCabecalho && outraCondicaoComplexa(fld);                              
  }                                                                                    
});                                                                                    
ControladorPedido.prototype.obterCabecalho = function (chcriacao) {                    
  this.operacao.abre(chcriacao);                                                       
  this.cabecalhoPedido.bindDataSet(this.operacao.pedidoCab);                           
  return this.cabecalhoPedido;                                                         
}; 

Ações que não precisam de objetos de gestão podem utilizar a API de consumo da forma natural sem necessidade de encaixar um DataSet, como está representado a seguir:

MyUserController.prototype.list = function (request) {
  return this.ok(bdo.orm.EntitySet.fromClass(ngin.keys.Classes.USERS));
};

A API atual de consumo de dados não permite o consumo de registros detalhes a partir da mestre em apenas uma requisição. No entanto, para realizar derivação de informações detalhe a partir de um dado mestre, podem ser realizadas chamadas REST para o sub-recurso a partir do recurso anteriormente consumido, aonde a rota devolve uma informação detalhe a partir do registro mestre, este é o caso das rotas abaixo:

Requisição para obter o registro mestre. No exemplo abaixo estão sendo obtidos os dados de um cliente cuja chave é a 11658484.

GET /api/classes/v1/entities/11658484 HTTP/1.1
Host: localhost:8080
Accept: application/json
Authorization: Basic XXXXXXXXXXXXXXXXXXXXXXXXXX
Cache-Control: no-cache
Requisição para obter o registro detalhe. No exemplo abaixo, estão sendo obtidos os endereços do cliente 11658484. A chave -1897047822 é a classe de Endereços.
GET /api/classes/v1/classes/-1897047822/entities?entidade=11658484 HTTP/1.1
Host: localhost:8080
Accept: application/json
Authorization: Basic XXXXXXXXXXXXXXXXXXXXXXXXXX
Cache-Control: no-cache
Consulte o JSDoc para encontrar mais informações sobre Entity e EntitySet.

Adicionando transformações

O REST Framework é capaz hoje de lidar com transformações de resultados, após a execução de uma ação, isto quer dizer que ele pode converter os tipos de dados existentes na plataforma bematech ERP para um JSON.

O objeto ngin.router.Result consegue empacotar conteúdos que podem ser transformados, por esse motivo ele deve ser amplamente utilizado dentro dos controladores de forma implícita ou explícita para padronizar os resultados das ações e tornar possível as transformações de forma organizada, as exceções dos tipos primitivos do Javascript e o DataSet do Engine.

As transformações são realizadas por uma pilha de funções seguindo a lógica FILO (first in first out) que são empilhadas por ngin.router.Result.addTransform para fazerem parte do fluxo da requisição, após a execução da ação do controlador, abaixo segue exemplo de uma transformação.

// Registrada uma transformação que indica que qualquer erro do tipo
// PermissionError deve ser retornado com o status FORBIDDEN
ngin.router.Result.addTransform(function (result, request) {
  if (result.content instanceof PermissionError) {
    return result.withStatus(ngin.http.Header.FORBIDDEN);
  } else {
    return result;
  }
});

O exemplo acima consegue transformar o que será guardado no content do objeto response no final do fluxo da requisição, ou seja essas funções irão realizar tratamentos após a execução da ação na ordem em que foram declaradas.

As transformações têm dois objetivos: 

  1. Simplificar as controladoras, ao evitar a repetição de códigos e tratamentos comuns as ações. 
  2. Versatilidade da resposta em função dos formatos suportados pelo cliente.

Quanto a simplificação, as ações dos controladores devem deixar a cargo das transformações executarem códigos que irão se repetir para qualquer ação de qualquer recurso, evite sempre o exemplo abaixo:

MyUserController.prototype.list = function (request) {
  return this.ok(bdo.orm.EntitySet.fromClass(ngin.keys.Classes.USERS).toJSON());
};

Prefira sempre implementar a ação da forma mais genérica possível

MyUserController.prototype.list = function (request) {
  return this.ok(bdo.orm.EntitySet.fromClass(ngin.keys.Classes.USERS));
};

A abordagem de conversão para um formato ou tipo específico limita os benefícios das transformações. Se a ação for implementada com os objetos de alto nível, um requisitante poderá informar o header Accept esperando que o recurso resultante seja convertido para o tipo de mídia esperado, ou quando necessário para uma hipermídia. Um exemplo prático disso é tentar obter um mesmo resultado requisitando-os com tipos de mídia diferentes, conforme exemplo a seguir:


Obtendo um cliente em formato JSON

GET /api/classes/v1/entities/11658484 HTTP/1.1
Host: localhost:8080
Accept: application/json
Authorization: Basic XXXXXXXXXXXXXXXXXXXXXXXXXX
Cache-Control: no-cache  

Obtendo um cliente em formato XML

GET /api/classes/v1/entities/11658484 HTTP/1.1
Host: localhost:8080
Accept: application/xml
Authorization: Basic XXXXXXXXXXXXXXXXXXXXXXXXXX
Cache-Control: no-cache

O exemplo acima ilustra as formas de se obter o recurso no tipo esperado, contudo atualmente o REST Framework apenas suporta o tipo application/json.

A declaração das transformações deve ocorrer na classe de
/Configurações/Inicialização do Roteador HTTP.

A princípio as transformações padrões do REST já estão contempladas, como mencionado anteriormente. Na sessão de validações e tratamento de erros serão abordados alguns tipos de erros que também sofrem transformações para JSON.

O desenvolvedor só deve adicionar uma nova transformação se o tipo a ser retornado não estiver dentro dos tipos que já sofrem transformações. Por esse motivo, criar novas transformações além das disponibilizadas pelo REST Framework não será algo frequente.

Importante: quando não for especificada uma codificação de forma explícita, os objetos serão convertidos em JSON utilizando a codificação UTF-8, conforme RFC 7159.


< Declaração de Controladores e das Rotas Página anterior | Proxima página Validações e tratamento de erros >