Upload de imagens e geolocalização no AdonisJS
Esse post é a terceira parte da série de posts “Clone AirBnB com AdonisJS, React Native e ReactJS” onde iremos construir do zero uma aplicação tanto web quando mobile com dados servidos através de uma API RESTfeita com NodeJS utilizando o framework AdonisJS.
- Parte 1: Iniciando com AdonisJS: Autenticação JWT e API REST;
- Parte 2: Criando CRUD e relações em API REST no AdonisJS;
- Parte 3: Upload de imagens e geolocalização no AdonisJS;
- Parte 4: Iniciando com React Native: Navegação e Autenticação com JWT;
- Parte 5: Instalando o Mapbox e listando imóveis no React Native;
- Parte 6: Instalando a Câmera e realizando o cadastro de Imóveis;
- Parte 7: Listando em um Modal os dados detalhados dos Imóveis;
- Parte 8: Iniciando com ReactJS: Navegação e Autenticação com JWT;
- Parte 9: Instalando o Mapbox e listando os imóveis no ReactJS;
- Parte 10: Utilizando o ModalRoute e fazendo upload de imagens;
- Parte 11: Exibindo informações do imóvel com ModalRoute;
Chegaaaaamos na terceira e última parte de AdonisJS dessa série antes de partirmos para o ReactJS e React Native. Nesse post vamos terminar a API REST da nossa aplicação criando a parte de cadastro/edição de imóveis com upload de imagens e filtro por geolocalização (latitude/longitude).
Se você não seguiu as duas primeiras partes do post, fica tranquilo, o código construído até aqui está no Github, só clicar aqui.
Geolocalização
Pra começar, vamos pensar na usabilidade do usuário quando acessa nossa aplicação. O ideal é que assim que acesse o usuário veja em um mapa os imóveis mais próximos dele, vamos manter um valor padrão de 10km agora.
Para isso, precisamos adicionar uma nova regra de negócio ao nosso Model de Propery, mas por que no Model? Nesse caso, como vamos criar um filtro por distância que pode ser utilizado em mais locais do nosso código, podemos trabalhar com o conceito de Query Scopes do Adonis que permite criarmos nossos próprios métodos no construtor de queries.
Para isso, vamos acessar nosso arquivo app/models/Propery.js
e no início da classe vamos criar nosso Query Scope com a seguinte sintaxe:
static scopeNearBy (query, latitude, longitude, distance) {
return query;
}
Precisamos perceber duas coisas aqui:
- Todo Query Scope é estático, ou seja, você não tem acesso ao
this
; - O nome do método deve iniciar com
scope
e seguir o padrão Camel Case; - O primeiro parâmetro sempre deve ser a
query
, nela temos acesso à construção de query atual e podemos adicionar where’s, limit, order, etc…
Antes de prosseguirmos, vamos importar a classe Database
do Adonis antes da classe já que vamos utilizar um recurso dela logo logo.
const Database = use('Database')
Agora com o Scope criado, podemos adicionar algumas regras a ele:
static scopeNearBy (query, latitude, longitude, distance) {
const haversine = `(6371 * acos(cos(radians(${latitude}))
* cos(radians(latitude))
* cos(radians(longitude)
- radians(${longitude}))
+ sin(radians(${latitude}))
* sin(radians(latitude))))`
return query
.select('*', Database.raw(`${haversine} as distance`))
.whereRaw(`${haversine} < ${distance}`)
}
O código que acabamos de adicionar utiliza um cálculo naval de distância através de latitude e longitude conhecido como Haversine. Esse valor é multiplicado por 6371 que o transforma em quilômetros.
Logo após, adicionamos esse calculo ao SELECT
do banco de dados além de retornar todos os outros campos com *
. Ainda no fim, realizamos um whereRaw
, que é utilizado quando não estamos utilizando as colunas comuns das tabelas e sim valores gerados por nós, para comparar a distância obtida através do Haversine com nossa variável distance
que enviaremos depois para esse método.
Por mais que pareça complexo, esse método é muito comum e utilizado em todo tipo de aplicação com cálculo de distância por geolocalização.
Configurando controller
Até agora em nosso ProperyController
nós estamos retornando TODOS os imóveis para os usuários, mas vamos alterar esse comportamento utilizado essa Scope Query que acabamos de criar para filtrar apenas os imóveis próximos alterando o método index.
async index ({ request }) {
const { latitude, longitude } = request.all()
const properties = Property.query()
.nearBy(latitude, longitude, 10)
.fetch()
return properties
}
O que estamos fazendo aqui é buscar os dados de latitude e longitude do corpo da nossa requisição (que serão enviados através do front-end depois) e utilizando nosso método nearBy
para buscar apenas imóveis com no máximo 10km de distância.
Para testarmos esse comportamento vamos criar um imóvel no banco de dados, para isso, crie um registro no banco de dados com os seguinte dados:
"title": "Rocketseat"
"address": "Rua teste, 260"
"longitude": -49.644018
"latitude" : -27.210768
"price": 100
Agora vamos acessar através da API a rota de listagem de imóveis passando uma latitude e longitude com uma pequena distância:
Teremos um resultado como o seguinte:
Veja que além dos campos comuns do imóvel temos agora também o campo distance
e esse registro só retornou porque a distância é menor que 10km.
Criação/edição de imóveis
Agora que terminamos a parte de geolocalização precisamos criar os métodos de cadastro e edição de imóveis no controller. Mais do que isso, precisamos permitir que o usuário envie imagens do imóvel para cadastrar na tabela images
que criamos no post anterior.
Vamos começar com a parte simples de criação e edição, para isso, vamos editar o método store
do nosso PropertyController
para o seguinte:
async store ({ auth, request, response }) {
const { id } = auth.user
const data = request.only([
'title',
'address',
'latitude',
'longitude',
'price'
])
const property = await Property.create({ ...data, user_id: id })
return property
}
Não tem muito mistério, buscamos o ID do usuário logado através do objeto auth
embutido automaticamente em todos métodos dos controllers, e também os campos da requisição para criação do imóvel. Logo após, criamos o imóvel com o método create
utilizando todos campos da requisição mais o ID do usuário.
Agora, voltamos ao Insomnia para criar a requisição:
Agora, executando-a temos o seguinte resultado:
Agora que já conseguimos criar registros no banco de dados, vamos alterar o método update
do controller para fazer a parte de edição:
async update ({ params, request, response }) {
const property = await Property.findOrFail(params.id)
const data = request.only([
'title',
'address',
'latitude',
'longitude',
'price'
])
property.merge(data)
await property.save()
return property
}
A principal diferença desse método para o create
é que precisamos buscar o ID que vem na URL para buscar um imóvel que já existe e atualiza-lo. Para isso utilizamos o método findOrFail
que retornará erro caso o imóvel não existe.
Logo após, buscamos novamente os dados da requisição e realizamos um merge no registro salvando os novos dados.
Podemos novamente criar um método no Insomnia para testar essa requisição:
Executando essa requisição, temos o seguinte resultado:
Upload de imagens no AdonisJS
No caso dos uploads para API REST o fluxo é um pouco diferente do upload comum através de formulário. Nesse caso, quando fizermos o front-end precisaremos enviar os dados das imagens através do FormData, mas como eu não quero ter que enviar todos os dados do imóvel dessa forma, vou separar a parte de upload em outro controller.
Para isso, vamos criar um novo controller chamado ImageController com um único método store
:
'use strict'
const Image = use('App/Models/Image')
const Property = use('App/Models/Property')
/**
* Resourceful controller for interacting with images
*/
class ImageController {
/**
* Create/save a new image.
* POST images
*/
async store ({ request }) {
}
}
module.exports = ImageController
Agora, vamos criar a rota para acessar esse método no controller para adicionar novas imagens ao imóvel. Para isso no arquivo app/routes.js
adicione:
Route.post('properties/:id/images', 'ImageController.store')
.middleware('auth')
Veja que estou utilizando uma rota encadeada, ou seja, como a imagem sempre precisa do ID do imóvel associado a ela, podemos utilizar a rota que já temos de imóveis seguida da palavra images
.
Agora no controller de imagens vamos começar importando no início do arquivo a classe Helpers
do Adonis que vai nos dar acesso ao caminho da pasta de uploads chamada tmp
.
const Helpers = use('Helpers')
E agora em nosso método store
vamos começar buscando o imóvel pelo ID recebido na URL assim como fizemos na edição e buscando também as imagens da requisição:
async store ({ params, request }) {
const property = await Property.findOrFail(params.id)
const images = request.file('image', {
types: ['image'],
size: '2mb'
})
}
O que aprendemos de novo até aqui foi o request.file
que nos trás um ou mais arquivos com o nome do primeiro parâmetro e ainda podemos limitar para arquivos apenas do tipo de imagem com tamanho até 2mb.
Agora, com essas imagens, precisamos salvá-las em uma pasta de uploads para termos acesso posterior:
await images.moveAll(Helpers.tmpPath('uploads'), file => ({
name: `${Date.now()}-${file.clientName}`
}))
if (!images.movedAll()) {
return images.errors()
}
Veja que aqui estou movendo TODAS imagens para uma pasta tmp/uploads
no Adonis e para cada arquivo estou alterando o nome do mesmo com o timestamp atual evitando arquivos duplicados.
Caso aconteça qualquer erro no upload, o processo para por aí e nosso front-end fica sabendo dos problemas.
Agora que já temos os arquivos vamos criar os registros de imagens no banco de dados associados com o imóvel:
await Promise.all(
images
.movedList()
.map(image => property.images().create({ path: image.fileName }))
)
Com esse código estamos percorrendo todas imagens salvas e cadastrando dentro do imóvel, isso só é possível pois dentro do nosso model de imóvel temos um método images()
que é o relacionamento de imóvel com imagens.
Veja que utilizo a técnica de await Promise.all
pois temos uma iteração (map) assíncrona, ou seja, cada create
que estamos dando retorna uma Promise, ou seja, pode demorar, com o await
evitamos que nossa requisição retorne sucesso antes mesmo de terminar o processo.
Nosso método deve ficar assim no fim:
async store ({ params, request }) {
const property = await Property.findOrFail(params.id)
const images = request.file('image', {
types: ['image'],
size: '2mb'
})
await images.moveAll(Helpers.tmpPath('uploads'), file => ({
name: `${Date.now()}-${file.clientName}`
}))
if (!images.movedAll()) {
return images.errors()
}
await Promise.all(
images
.movedList()
.map(image => property.images().create({ path: image.fileName }))
)
}
Agora, para testarmos o upload vamos criar um novo método no Insomnia e diferente dos outros até agora vamos utilizar o formato do corpo Multipart Form
enviando duas imagens:
Enviando a requisição vamos ter apenas uma resposta de sucesso sem corpo já que não retornamos nada. Nós vamos mostrar as imagens do imóvel no método show
e index
do imóvel, por isso, no PropertyController
vamos alterar a query do método index
para:
const properties = Property.query()
.with('images')
.nearBy(latitude, longitude, 10)
.fetch()
Veja que faço o uso do .with
que realiza um processo de Eager Loading nas imagens adicionando-as ao retorno de cada imóvel. Testando a requisição de listar imóveis no Insomnia agora temos:
Pronto, no método show
já havíamos criado anteriormente esse comportamento com a linha await property.load('images')
, nesse caso a sintaxe é diferente pois realizamos um find
ao invés de uma query
.
O último ponto que vamos fazer é criar um campo “virtual” no nosso model de imagem para retornar o caminho completo da imagem para nosso front-end, para isso, no arquivo app/Models/Image.js
altere seu conteúdo para:
'use strict'
const Env = use('Env')
const Model = use('Model')
class Image extends Model {
static get computed () {
return ['url']
}
getUrl ({ path }) {
return `${Env.get('APP_URL')}/images/${path}`
}
}
module.exports = Image
Veja que criamos um campo “computed” chamado url e adicionamos seu valor com um método com prefixo get
seguido do campo em camel case. Utilizamos também o Env
para recuperar a URL da nossa API.
Se tentarmos acessar esse link ainda não temos uma rota para exibir a imagem, por isso vamos criar uma nova rota que apenas mostra a imagem. Para isso no app/routes.js
adicione:
Route.get('images/:path', 'ImageController.show')
Agora no ImageController
adicione um método show
com o código:
Pronto! Agora se você acessar o campo URL que é retornando na imagem poderá vê-la pelo navegador, e isso faz com que nosso front-end tenha acesso ao mesmo comportamento 🙂
Ufa… acabou
Agora sim, nossa API ficou garbosa com todas essas adições e já temos muitas funcionalidades lidando com relacionamentos, uploads, geolocalização, rotas, etc…
Para essa série terminamos a parte de Adonis lembrando que ainda vamos continuar essa aplicação no front-end consumindo essa API e criando todo seu funcionamento com ReactJS e React Native para web/mobile.
Código desse post: https://github.com/Rocketseat/blog-adonis-reactjs-react-native-airbnb/tree/upload-geo
Ah, não esquece que se tiver gostando dessa série deixa seu comentário aí em baixo que faz toda diferença!! 🙂