O arquivo deverá exportar um objeto ou um array de definições de rotas. Uma rota é uma associação entre um método HTTP, uma URL e um método de uma controladora, responsável por processar a requisição e gerar a resposta dessa requisição.
Abordaremos aqui alguns conceitos sobre implementações que permeiam a arquitetura de um recurso.
Todo controlador deve herdar um componente abstrato chamado de ngin.router.Controller e então todos os métodos a serem vinculados com a definição de rotas devem ser declaradas no protótipo do controlador desenvolvido.
Abaixo segue um exemplo de uma definição de um controlador do recurso usuário do sistema Bematech ERP.
/*global virtualFS, queryUtilities*/ __includeOnce('ufs:/bdo/orm/entityset.js'); __includeOnce('ufs:/bdo/orm/entity.js'); __includeOnce('ufs:/goog/array/array.js'); __includeOnce('ufs:/goog/object/object.js'); __includeOnce('ufs:/ngin/router/controller.js'); __includeOnce('ufs:/ngin/keys/classes.js'); __includeOnce('ufs:/ngin/http/error.js'); __includeOnce('ufs:/ngin/http/status.js'); var MyUserController = function () { ngin.router.Controller.call(this); }; goog.inherits(MyUserController, ngin.router.Controller); MyUserController.prototype.getUser = function (request, userKey) { if (!dbCache.findKey(userKey)) { return this.notFound(new Error('A chave ' + userKey + ' não foi encontrada.')); } var entitySet = bdo.orm.EntitySet.fromClass(ngin.keys.Classes.USERS); var entity = entitySet.findByKey(userKey); if (entity) { return this.ok(entity); } else { return this.notFound(new Error('Não foi possível encontrar um usuário para chave ' + userKey)); } }; MyUserController.prototype.getUserImage = function (request, userKey) { if (!dbCache.findKey(userKey)) { return this.notFound(new Error('A chave ' + userKey + ' não foi encontrada.')); } var userImagesRelation = classes.getCachedDataSet(ngin.keys.Classes.USERS_IMAGE); userImagesRelation.indexFieldNames = 'iGroupUser'; if (userImagesRelation.find(userKey)) { return this.ok(virtualFS.getFileContent(userImagesRelation.ifile)). as(userImagesRelation.ifile.imimetype.icode); } else { return this.notFound(new Error('Não foi possível encontrar uma imagem ' + 'para o usuário de chave ' + userKey)); } }; MyUserController.prototype.getLogsFromUser = function (request, userKey) { if (!dbCache.findKey(userKey)) { return this.notFound(new Error('A chave ' + userKey + ' não foi encontrada.')); } var requestParam = goog.bind(function (name, value) { var params = request.params; var result = value; if (params[name]) { result = ngin.date.parseISO8601Date(params[name]); } if (!result) { throw new ngin.http.Error('Não foi possível entender o filtro ' + name + '.', 'Informe uma data no padrão ISO-8601 para o filtro ' + name + '.', 'Valor informado: ' + params[name], ngin.http.Status.BAD_REQUEST); } return result; }, this); if (userKey !== session.userKey && !goog.string.contains(session.userKey.igroups, ngin.keys.Groups.ADMINISTRATORS)) { return this.forbidden(new Error('Você não tem permissão para acessar os logs do usuário ' + userKey)); } var ds = database.query('select * from iLog where iUser = ' + userKey + ' and iDate >= ' + requestParam('begin', new Date()).toSqlString() + ' and iDate <= ' + requestParam('end', new Date()).toSqlString()); if (ds.isEmpty) { return this.notFound(new Error('Não existem ocorrências para os filtros ' + 'informados')); } return this.ok(ds); }; MyUserController.prototype.updateUser = function (request, userKey) { if (dbCache.findKey(userKey)) { var user = bdo.orm.Entity.fromKey(userKey); user.assign(request.body.asJson()); user.post(); return this.ok(user); } else { return this.notFound(new Error('A chave ' + userKey + ' não existe.')); } }; module.exports = MyUserController;
Pelo exemplo acima, é possível notar que as respostas servidas pelas ações do controlador fazem uso de chamadas de métodos que estão definidos na hierarquia, ou seja métodos que estão definidos em ngin.router.Controller.
Sempre que o desenvolvedor quiser devolver uma resposta esperada pelo cliente, que refere à informações do recurso consumido, será utilizado o método ngin.router.Controller.ok, quando não houver nenhuma informação a ser retornada, será utilizado o método ngin.router.Controller.notFound e quando uma requisição não atende os pré-requisitos, será utilizado ngin.router.Controller.badRequest. Um ponto interessante ainda sobre o método ngin.router.Controller.notFound é que há deve ser utilizado no conceito ao qual se propõe, para determinar se o recurso pesquisado existe, como nos exemplos, a chave de usuário pertence a uma tabela de cache, logo, é possível deduzir a existência da chave de usuário a ser pesquisada e retornar o código de estado 404 Not Found para o requisitante, sinalizando que a chave não foi encontrada.
Baseado nisto pode-se ter a noção de que as respostas dos métodos dos controladores representam os códigos de estados do protocolo de transferência de hipertexto. Entretanto, a realidade não é exatamente esta, pois apenas alguns desses códigos são realmente utilizados, contudo qualquer código HTTP poderá ser utilizado dentro da API e é aconselhado que utilize-se somente àqueles definidos pela especificação do REST.
Os códigos de estados mais utilizados são:
200 - OK
201 - Created
202 - Accepted - usado quando uma API irá processar a requisição de forma assíncrona
204 - No Content - Retornado quando um recurso é deletado
400 - Bad Request
401 - Unauthorized
403 - Forbidden - usado quando o cliente não tem permissão de acesso ao dado
404 - Not Found
405 - Method Not Allowed - Usado para indicar que o método HTTP não é suportado para este recurso
500 - Internal Server Error - Erro do servidor não tratado pelo objeto controlador
Os códigos HTTP são divididos em:
1xx - Informacionais
2xx - Bem sucedidos
3xx - Redirecionamento
4xx - Erros de cliente
5xx - Erros de servidor
Ainda sobre o exemplo de controlador, vale destacar o uso das APIs bdo.orm.Entity e bdo.orm.EntitySet. Essas duas APIs servem para representar as entidades de classes de dados de forma genérica e elas devem consumir uma chave de alguma classe de dados, utilizando bdo.orm.EntitySet.fromClass, ou um conjunto de dados em uma estrutura como DataSet, ou utilizando bdo.orm.Entity.fromDataSet, ou um JSON, ou um Array ou ainda a chave de uma entidade utilizando bdo.orm.Entity.fromKey, mais detalhes sobre essas APIs de manipulação de entidades das classes de dados serão abordados posteriormente. É importante notar que o exemplo do método do controlador realiza validação sobre o usuário, ou seja, somente o próprio usuário poderá visualizar as alterações realizadas por ele mesmo ou pelo adminstrador do sistema. Quando o desenvolvedor faz uso de APIs ricas como o caso do bdo.datasource.DataSource, bdo.orm.Entity ou bdo.orm.EntitySet, as validações de permissões são realizadas sem que o desenvolvedor do controlador de um recurso se preocupe em expor dados para grupos ou pessoas não autorizadas. Contudo, para os casos similares aos da consulta de logs do exemplo, os tratamentos de segurança dos dados são resposabilidade do desenvolvedor do controlador.
Percebe-se ainda que, com este exemplo, todos os métodos possuem parâmetros, isto é, o roteador é capaz de injetar os parâmetros para o método do controlador. Essa injeção é realizada na propriedade action da configuração da rota. Nela o desenvolvedor indica qual método da controladora deve ser executado e quais os parâmetros que devem ser passados.
Parâmetros de ação
Abaixo estão elencados dois tipos de parâmetros que uma ação pode receber:
Parâmetros globais - São request e response, que representam a requisição e a resposta de um consumo de rota de um recurso HTTP.
Parâmetros da URL da rota - São parâmetros informados na URL da rota e sofrem extração do caminho da rota para serem injetados no método do controlador , geralmente apresentam semântica para o recurso, no caso do exemplo de código de um controlador, é utilizada a chave do usuário.
Esses parâmetros podem ser tipados ou não, entretanto é fortemente aconselhado que o desenvolvedor utilize os tipos de dados na URL. Os tipos suportados são todos os tipos primitivos para o Javascript, com a adição do tipo Date que é consumido ou fornecido no padrão ISO-8601 (YYYY-MM-DD|YYYY-MM-DDThh:mm:ssZ).
Os tipos de parâmetros de caminho são informados no arquivo de interface das rotas e serão abordados adiante ainda nesta mesma página.
Corpo de requisição
Podem ser obtidos a partir da requisição o corpo como que pode estar nos formatos de texto ou JSON, contudo o header Content-Type é responsável por definir o tratamento adequado do dado durante a sua captura. As chamadas para a captura do corpo estão documentadas na API de ngin.http.RequestBody O desenvolvedor precisa levar em consideração que request.body é apenas uma instância de ngin.http.RequestBody.
Querystring
A querystring é um tipo de parametrização adicional e opcional, que não precisa estar declarada no arquivo de rotas e nem na assinatura da ação ela é informada na URL e é tratada pelo objeto de requisição do engine, sendo acessada em request.params. Abaixo uma URL válida com querystring age=18&name=Atom que é obtida a partir do caractere ?.
http://127.0.0.1:81/api/user/v1/user?age=18&name=Atom
Rotas
Abaixo segue um exemplo de uma definição de um arquivo de rotas relativo ao exemplo do controlador ilustrado.
module.exports = { apiName: 'My API', basePath: '/api/mines/v1/user/', requiresAuth: true, controller: 'ufs:/engine/controllers/example.js', routes: [ { method: 'GET', path: ':key<number>', action: 'getUser(request, key)' }, { method: 'GET', path: ':key<number>/image', action: 'getUserImage(request, key)' }, { method: 'GET', path: ':key<number>/permissions', action: 'getUserPermissions(request, key)' }, { method: 'GET', path: ':key<number>/logs', action: 'getLogsFromUser(request, key)' }, { method: 'PUT', path: ':key<number>', action: 'updateUser(request, key)' } ] };
Com base no exemplo, pode ser constatado que uma definição de uma API qualquer é declarada por meio de um objeto literal com as propriedades definidas por ngin.router.RouteSetDef.
Abaixo segue um resumo dessas propriedades:
apiName: Indica que este conjunto de rotas está associado a API informada. Essa informação é importante para as ferramentas de documentação, que agrupam as rotas pelo nome da API.
apiHelp: Resource string que terá uma descrição mais detalhada da finalidade da API.
basePath: Caminho comum a todas as rotas definidas em routes. Ele será prefixado a url de cada rota.
requiresAuth: Determina se as rotas solicitadas exigem que o requisitante informe alguma autenticação válida. Caso a autenticação seja solicitada, o objeto session estará autenticado com o usuário indicado no token durante a execução da controladora. Os tipos de tokens de autorização permitidos são o Basic e o Bearer eles devem ser informados pelo cabeçalho HTTP Authorization.
realm: indica a configuração e o isolamento dos ambientes JavaScript que devem ser utilizados para executar as ações das rotas. Por padrão, todas as rotas utilizam um pool comum de sessões Stateless do realm "http-router". Defina um realm diferente do padrão quando observar que os recursos e scripts utilizados por um controlador serão muito diferentes do padrão de uso dos demais controladores ou quando for necessário um controle diferenciado do tempo de vida do ambiente JavaScript ou do tipo de ambiente (stateful ou stateless). Mais detalhes sobre a configuração de realms pode ser observado na classe ngin.http.RealmConfig.
controller: Chave ou caminho do arquivo que contém a controladora que executará as ações indicadas nas rotas.
routes: Array com as definições das rotas. Uma rota é constituída do método HTTP que ela atende, a URL a ser tratada e o método a ser executado na controladora. Mais detalhes de uma definição de rota em ngin.router.RouteDef. O array routes também poderá conter uma outra definição de ngin.router.RouteDef. Isso permite definir uma API mais complexa que define vários grupos de rotas associados a recursos distintos.
A URL dos recursos deverá ter o seguinte formato: /api/<nome-api>/<versão>/<recurso>/<sub-rota>
Cujos elementos são:
nome-api: Definirá a URL base de todas as rotas da API. Evite nomes longos, pois é um texto enviado a cada requisição e dificulta a leitura de logs HTTP.
versão: versão da API (v1, v2, etc.). Deverá ser em minúsculo.
recurso: O nome do recurso. Ele deve estar formatado para o padrão REST, onde substantivos compostos são separados por hífens. Ex: titulos_a_vencer torna-se titulos-a-vencer e titulos_vencidos torna-se titulos-vencidos.
sub-rota: livre, normalmente uma ou identificação ou um sub-recurso. Exemplo: pedidos/:chave.
Uma rota poderá ser parametrizada, permitindo indicar que uma parte dela é variável e que o seu valor deve ser extraído em um parâmetro que poderá ser passado para o método do objeto controlador, conforme mencionado anteriormente. Para isso, deverá ser utilizada as seguintes sintaxes:
:param_name - extrai o parâmetro até a localização do próximo separador “/” dentro da URL. Exemplo: a rota com URL “/api/classes/:id/def” interceptará uma requisição para “/api/classes/123456/def” e extraíra o parâmetro id com o valor ‘123456’. Todos os parâmetros extraídos por essa sintaxe são do tipo “string”.
:param_name<type> - extrai o parâmetro de forma similar a sintaxe anterior, mas o converte para o tipo informado. Se o tipo não puder ser convertido, será retornado um erro. Inicialmente, será suportado apenas o tipo “number”.
*param_name - extraí o restante da URL e armazena o valor extraído em param_name. O nome do parâmetro pode ser suprimido, informando apenas *, quando deseja-se criar uma rota para o caminho informado e todos os subcaminhos dele. Exemplo: a rota com URL “/api/files/*path” interceptará uma requisição para “/api/files/parent/file.js” e extraíra o parâmetro path com o valor ‘parent/file.js’.
A definição de rotas deve deve ser retornada por meio da propriedade module.exports, seguindo a convenção de módulos do CommonJS.
O REST Framework disponibiliza alguns controladores padrões para fornecer algumas informações comuns a todas as demais APIs a serem disponibilizadas.
Um objeto controlador especial, o Controlador de Cadastro Genérico, implementa as operações CRUD básicas para as entidades cadastrais do sistema. Ele é análogo ao processo Admin > Explorer.ip da interface Web. Detalhes sobre o seu funcionamento estão definidas aqui.
Este é outro controlador especial. Ele permite a execução de DataSources ou DataSourceQueries do sistema, e retorna o DataSet resultante serializado como JSON. Detalhes sobre o seu funcionamento serão acrescentadas futuramente a este documento.
< Versionamento Página anterior | Próxima página APIs de acesso e transformações de dados >