Listagem de usuários usando GeoSpatial Queries com Mongo Db no CrazyStack Node.js
Nesta aula, você irá aprender sobre como usar consultas GeoSpatial no MongoDB para listar usuários. O MongoDB tem suporte a vários tipos de consultas GeoSpatial, como consultas de índice espacial 2D, consultas de índice espacial 2D com raio, consultas de índice espacial 3D, consultas poligonais e consultas de linhas. Você aprenderá como usar estas consultas para encontrar usuários que estejam dentro de uma determinada área geográfica, usando a latitude e longitude do usuário. Isso permitirá que você implemente recursos de pesquisa por localização em sua aplicação, como por exemplo, encontrar todos os usuários em um raio de X km de um ponto específico.
export class QueryBuilder {
private readonly query: any = [];
match(data: any): QueryBuilder {
this.query.push({ $match: data });
return this;
}
group(data: any): QueryBuilder {
this.query.push({ $group: data });
return this;
}
count(data: any): QueryBuilder {
this.query.push({ $count: data });
return this;
}
geoNear(data: any): QueryBuilder {
this.query.push({ $geoNear: data });
return this;
}
project(data: any): QueryBuilder {
this.query.push({ $project: data });
return this;
}
skip(data: any): QueryBuilder {
this.query.push({ $skip: data });
return this;
}
limit(data: any): QueryBuilder {
this.query.push({ $limit: data });
return this;
}
lookup(data: any): QueryBuilder {
this.query.push({ $lookup: data });
return this;
}
sort(data: any): QueryBuilder {
this.query.push({ $sort: data });
return this;
}
unwind(data: any): QueryBuilder {
this.query.push({ $unwind: data });
return this;
}
build(): QueryBuilder {
return this.query;
}
}
Este é um código de um construtor de consultas para o MongoDB escrito em TypeScript. A classe QueryBuilder cria uma consulta de agregação para o MongoDB, armazenando as etapas da consulta em um array privado, query.
Os métodos na classe QueryBuilder adicionam diferentes etapas da consulta de agregação ao array query. Por exemplo, o método match adiciona a etapa $match, o método group adiciona a etapa $group, e assim por diante. Cada um destes métodos retorna this, permitindo que você encadeie vários métodos juntos para construir sua consulta de agregação.
Finalmente, o método build retorna o array query completo com todas as etapas da consulta de agregação adicionadas.
Este construtor de consultas é útil porque permite que você construa uma consulta de agregação de forma programática, em vez de escrever manualmente a consulta no formato JSON. Isso pode ser útil em aplicativos em que a consulta precisa ser construída dinamicamente baseada em entradas do usuário ou outros fatores.
import { Query } from "@/application/types";
import { UserPaginated } from "@/slices/user/entities";
export interface LoadUserByPageGeoNearRepository {
loadUserByPageGeoNear(query: Query): Promise<UserPaginated | null>;
}
Este código representa uma interface para um repositório que carrega usuários por página com base em uma consulta geoNear para o MongoDB. A interface LoadUserByPageGeoNearRepository define um único método, loadUserByPageGeoNear, que aceita uma consulta do tipo Query e retorna uma Promise que, quando resolvida, retorna um objeto do tipo UserPaginated ou null se não houver resultados.
A interface LoadUserByPageGeoNearRepository é utilizada para garantir que qualquer classe que a implemente tenha um método loadUserByPageGeoNear que siga a mesma assinatura de função. Isso é útil porque permite que você troque a implementação concreta do repositório sem que isso afete o resto do código que depende dele.
A interface também fornece uma maneira de documentar a funcionalidade esperada de qualquer classe que a implemente, tornando mais fácil para outros desenvolvedores entenderem como a classe deve ser usada.
import { LoadUserByPageGeoNearRepository } from "@/slices/user/repositories";
import { UserPaginated } from "@/slices/user/entities";
import { Query } from "@/application/types";
export type LoadUserByPageGeoNear = (query: Query) => Promise<UserPaginated | null>;
export type LoadUserByPageGeoNearSignature = (
loadUserByPageGeoNear: LoadUserByPageGeoNearRepository
) => LoadUserByPageGeoNear;
export const loadUserByPageGeoNear: LoadUserByPageGeoNearSignature =
(loadUserByPageGeoNearRepository: LoadUserByPageGeoNearRepository) =>
async (query: Query) => {
return loadUserByPageGeoNearRepository.loadUserByPageGeoNear(query);
};
Este código define uma função loadUserByPageGeoNear que serve como uma camada de abstração entre a camada de aplicação e a camada de persistência de dados. A função loadUserByPageGeoNear é definida como um "adapter", que recebe uma instância de LoadUserByPageGeoNearRepository e retorna uma função que, por sua vez, aceita uma consulta do tipo Query e retorna uma Promise que, quando resolvida, retorna um objeto do tipo UserPaginated ou null se não houver resultados.
A função loadUserByPageGeoNear é usada para lidar com a chamada para o repositório, evitando que a camada de aplicação tenha que conhecer detalhes sobre a implementação concreta do repositório. Isso permite que você altere a implementação do repositório sem que isso afete o resto do código que depende dele.
A vantagem de se usar uma camada de abstração como esta é que você pode reutilizar a lógica da função loadUserByPageGeoNear em vários pontos da aplicação, sem que haja a necessidade de escrever código duplicado. Além disso, essa camada de abstração é útil porque permite que você teste a camada de aplicação de forma isolada, sem a necessidade de acessar o banco de dados.
async loadUserByPageGeoNear(query: Query): Promise<UserPaginated | null> {
if (!query?.options?.userLoggedId) {
return null;
}
const { coord = null } =
(await this.repository.getOne(
{ _id: new ObjectId(query?.options?.userLoggedId) },
{ projection: { password: 0 } }
)) || {};
const queryMongo = mapQueryParamsToQueryMongo({
...((query?.fields ?? {}) as object),
active: true,
_id: { $ne: new ObjectId(query?.options?.userLoggedId) },
});
if (queryMongo?.$text) {
const resultPaginatedArray =
(await this.repository.getPaginate(
query?.options?.page ?? 0,
queryMongo,
query?.options?.sort ?? { createdAt: -1 },
10,
query?.options?.projection ?? {}
)) ?? [];
const totalPaginated = (await this.repository.getCount(queryMongo)) ?? 0;
return { users: resultPaginatedArray, total: totalPaginated };
}
if (!coord?.coordinates) return null;
const { coordinates } = coord;
const queryBuilded = new QueryBuilder()
.geoNear(mountGeoNearQuery({ query: queryMongo, coordinates }))
.sort({ distance: 1 })
.skip(((query?.options?.page ?? 0) - 1) * 10)
.limit(10)
.project({ password: 0 })
.build();
const totalQueryBuilded = new QueryBuilder()
.geoNear(mountGeoNearQuery({ query: queryMongo, coordinates }))
.count("name")
.build();
const resultGeoNearPaginatedArray =
(await this.repository.aggregate(queryBuilded)) ?? [];
const totalResult = (await this.repository.aggregate(totalQueryBuilded)) ?? null;
const total = totalResult?.[0]?.name ?? 0;
return { users: resultGeoNearPaginatedArray, total };
}
O método loadUserByPageGeoNear é um método que retorna uma lista paginada de usuários baseados em uma consulta dada. Ele primeiro verifica se o userLoggedId existe nas opções de consulta. Se não existir, ele retorna null. Em seguida, ele recupera o usuário com o userLoggedId dado e seu valor coord.
Ele constrói duas consultas MongoDB usando a classe QueryBuilder - uma para buscar uma lista paginada de usuários e outra para contar o número total de usuários. A primeira consulta usa o método geoNear para classificar os usuários com base na proximidade de um determinado conjunto de coordenadas (que foram recuperadas do usuário com o userLoggedId dado). A consulta também limita os resultados a 10 usuários por página e exclui o campo password do conjunto de resultados.
A segunda consulta usa o método count para contar o número de usuários que atendem aos critérios. Finalmente, o método agrega ambas as consultas e retorna o resultado como um objeto UserPaginated, que contém uma matriz de usuários e uma contagem total.
Resumindo: Este é um método que carrega usuários por página baseado em sua localização geográfica. Ele começa verificando se o ID do usuário logado foi fornecido na consulta. Em seguida, ele recupera as coordenadas do usuário logado e, em seguida, constrói duas consultas: uma para os usuários e outra para o total de usuários. Se a consulta incluir um termo de busca ($text), a consulta retornará uma lista de usuários paginação e o total de usuários correspondendo aos critérios de busca. Caso contrário, a consulta retornará usuários que estão próximos geograficamente ao usuário logado. A consulta também especifica a página, a classificação, a projeção e a quantidade de resultados a serem retornados.