475 lines
20 KiB
Python
475 lines
20 KiB
Python
# Importações de bibliotecas padrão
|
|
from uuid import UUID
|
|
from typing import Generic, TypeVar, Any, List, Optional, Dict, Union
|
|
|
|
# Importações de bibliotecas de terceiros
|
|
from sqlalchemy import select, delete, or_
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.exc import IntegrityError, SQLAlchemyError
|
|
from pydantic import BaseModel
|
|
from fastapi import HTTPException, status
|
|
from sqlalchemy.orm import joinedload
|
|
from sqlalchemy import and_
|
|
|
|
# Importações do seu próprio projeto
|
|
from app.database import models
|
|
from app.database.audit_log import audit_log
|
|
from app.database.TratamentoErros import ErrorHandler
|
|
|
|
Model = TypeVar("Model", bound=models.Base)
|
|
Schema = TypeVar("Schema", bound=BaseModel)
|
|
|
|
|
|
class SnippetException(Exception):
|
|
pass
|
|
|
|
|
|
class IntegrityConflictException(Exception):
|
|
pass
|
|
|
|
|
|
class NotFoundException(Exception):
|
|
pass
|
|
|
|
|
|
class RepositoryBase(Generic[Model]):
|
|
"""Repository for performing database queries."""
|
|
|
|
def __init__(
|
|
self, model: type[Model],
|
|
session: AsyncSession,
|
|
default_order_by: str = None,
|
|
ascending: bool = True) -> None:
|
|
self.model = model
|
|
self.session = session
|
|
self.default_order_by = default_order_by
|
|
self.ascending = ascending
|
|
|
|
async def create(self, data_one: Schema) -> Model:
|
|
|
|
try:
|
|
db_model = self.model(**data_one.model_dump())
|
|
self.session.add(db_model)
|
|
await self.session.commit()
|
|
await self.session.refresh(db_model)
|
|
return db_model.__dict__
|
|
|
|
except SQLAlchemyError as e:
|
|
handler = ErrorHandler()
|
|
handler.handle_error(e)
|
|
|
|
# Testado ok
|
|
async def create_many(self, data: List[Schema], return_models: bool = False) -> list[Model] | bool:
|
|
|
|
# Cria instâncias dos modelos a partir dos dados fornecidos
|
|
db_models = [self.model(**d.model_dump()) for d in data]
|
|
|
|
try:
|
|
self.session.add_all(db_models)
|
|
await self.session.commit()
|
|
except SQLAlchemyError as e:
|
|
handler = ErrorHandler()
|
|
handler.handle_error(e)
|
|
|
|
if not return_models:
|
|
return True
|
|
|
|
for m in db_models:
|
|
await self.session.refresh(m)
|
|
|
|
return db_models
|
|
|
|
async def get_one_by_id(self, uuid: str | UUID, coluna: str, with_for_update: bool = False, ) -> Model:
|
|
|
|
try:
|
|
q = select(self.model).where(getattr(self.model, coluna) == uuid)
|
|
|
|
except AttributeError:
|
|
raise HTTPException(status_code=400, detail=f"A Coluna {coluna} não existe em: {self.model.__tablename__}.")
|
|
|
|
# Verifica se o modelo tem a coluna 'ativo' e adiciona a condição
|
|
if hasattr(self.model, 'ativo'):
|
|
q = q.where(self.model.ativo.is_(True))
|
|
|
|
if with_for_update:
|
|
q = q.with_for_update()
|
|
|
|
results = await self.session.execute(q)
|
|
result = results.unique().scalar_one_or_none()
|
|
|
|
if result is None:
|
|
raise HTTPException(status_code=404,
|
|
detail=f"Registro com {coluna}={uuid} não encontrado na tabela "
|
|
f"{self.model.__tablename__}.")
|
|
return result
|
|
|
|
async def get_many_by_ids(self, coluna: str, uuids: List[str | UUID] = None, with_for_update: bool = False,
|
|
order_by: str = None, ascending: bool = True):
|
|
try:
|
|
q = select(self.model)
|
|
|
|
if uuids:
|
|
try:
|
|
q = q.where(getattr(self.model, coluna).in_(uuids))
|
|
except AttributeError:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"A coluna '{coluna}' não existe em: {self.model.__tablename__}."
|
|
)
|
|
# Verifica se o modelo tem a coluna 'ativo' e adiciona a condição
|
|
if hasattr(self.model, 'ativo'):
|
|
q = q.where(self.model.ativo.is_(True))
|
|
|
|
if with_for_update:
|
|
q = q.with_for_update()
|
|
|
|
# Verificar se a ordenação foi solicitada e aplicar à consulta
|
|
if order_by:
|
|
order_by_column = getattr(self.model, order_by, None)
|
|
if not order_by_column:
|
|
raise HTTPException(
|
|
status_code=400,
|
|
detail=f"A coluna de ordenação '{order_by}' não foi encontrada na tabela "
|
|
f"{self.model.__tablename__}."
|
|
)
|
|
q = q.order_by(order_by_column.asc() if ascending else order_by_column.desc())
|
|
|
|
rows = await self.session.execute(q)
|
|
|
|
return rows.unique().scalars().all()
|
|
|
|
except SQLAlchemyError as e:
|
|
handler = ErrorHandler()
|
|
handler.handle_error(e)
|
|
|
|
async def get_filter(
|
|
self,
|
|
coluna: str,
|
|
uuids: Optional[List[Union[str, UUID]]] = None,
|
|
filters: Optional[List[Dict[str, Any]]] = None,
|
|
relationships: Optional[List[str]] = None,
|
|
order_by: Optional[Union[str, List[str]]] = None, # Aceita str ou List[str]
|
|
ascending: Optional[List[bool]] = None
|
|
):
|
|
|
|
try:
|
|
query = select(self.model)
|
|
|
|
# Adicionar relacionamentos com joinedload para otimizar carregamento
|
|
if relationships:
|
|
for relation in relationships:
|
|
try:
|
|
relation_attr = getattr(self.model, relation)
|
|
query = query.options(joinedload(relation_attr))
|
|
except AttributeError:
|
|
raise ValueError(
|
|
f"Relacionamento '{relation}' não encontrado no modelo '{self.model.__name__}'.")
|
|
|
|
# Aplicar filtros dinâmicos com suporte a múltiplos níveis de relacionamento
|
|
if filters:
|
|
# Inicializamos um conjunto para controlar os joins já visitados
|
|
visited_joins = set()
|
|
|
|
# Acumular condições separadas por tipo lógico
|
|
and_conditions = []
|
|
or_conditions = []
|
|
|
|
for condition in filters: # Iteramos diretamente sobre a lista de condições
|
|
|
|
column_path = getattr(condition, "column") # Acessamos diretamente os atributos
|
|
operator = getattr(condition, "operator", "==") # Operador padrão é igualdade
|
|
value = getattr(condition, "value")
|
|
logical = getattr(condition, "logical", "AND") # Operador lógico padrão é AND
|
|
|
|
path_parts = column_path.split(".")
|
|
current_model = self.model
|
|
|
|
for i, part in enumerate(path_parts[:-1]): # Navegar pelos relacionamentos no caminho
|
|
if part not in visited_joins: # Verificar se o relacionamento já foi adicionado
|
|
# Obtemos o relacionamento usando o modelo atual
|
|
try:
|
|
related_table = getattr(current_model, part).property.mapper.class_
|
|
except AttributeError as e:
|
|
raise ValueError(
|
|
f"Relacionamento '{part}' não encontrado em '{current_model.__name__}'."
|
|
f"Erro: {str(e)}"
|
|
)
|
|
|
|
# Adicionamos o relacionamento à query com um join
|
|
query = query.join(related_table, isouter=True)
|
|
|
|
# Registramos o relacionamento no conjunto para evitar duplicação
|
|
visited_joins.add(part)
|
|
else:
|
|
# Atualizar o modelo mesmo que o join seja pulado
|
|
try:
|
|
related_table = getattr(current_model, part).property.mapper.class_
|
|
except AttributeError as e:
|
|
raise ValueError(
|
|
f"Relacionamento '{part}' não encontrado em '{current_model.__name__}'."
|
|
f"Erro: {str(e)}"
|
|
)
|
|
|
|
# Atualizamos o modelo atual para o próximo relacionamento
|
|
current_model = related_table
|
|
|
|
# Obtém a coluna final no caminho para aplicar o filtro
|
|
try:
|
|
final_column = getattr(current_model, path_parts[-1])
|
|
except AttributeError as e:
|
|
raise ValueError(
|
|
f"Coluna '{path_parts[-1]}' não encontrada em '{current_model.__name__}'."
|
|
f"Erro: {str(e)}"
|
|
)
|
|
|
|
# Mapear operadores para SQLAlchemy
|
|
operator_mapping = {
|
|
"==": final_column == value,
|
|
"!=": final_column != value,
|
|
">": final_column > value,
|
|
"<": final_column < value,
|
|
">=": final_column >= value,
|
|
"<=": final_column <= value,
|
|
"IN": final_column.in_(value) if isinstance(value, list) else final_column == value,
|
|
"NOT IN": final_column.notin_(value) if isinstance(value, list) else final_column != value,
|
|
"LIKE": final_column.like(f"%{value}%"),
|
|
"ILIKE": final_column.ilike(f"%{value}%"), # Apenas para PostgreSQL
|
|
"IS NULL": final_column.is_(None),
|
|
"IS NOT NULL": final_column.isnot(None),
|
|
"BETWEEN": final_column.between(value[0], value[1]) if isinstance(value, list) and len(
|
|
value) == 2 else None,
|
|
}
|
|
|
|
# Adiciona a condição à lista apropriada (AND ou OR)
|
|
if operator in operator_mapping:
|
|
condition_expression = operator_mapping[operator]
|
|
if logical.upper() == "AND":
|
|
and_conditions.append(condition_expression)
|
|
elif logical.upper() == "OR":
|
|
or_conditions.append(condition_expression)
|
|
else:
|
|
raise ValueError(f"Operador '{operator}' não suportado.")
|
|
|
|
# Aplicar condições acumuladas na query
|
|
if and_conditions:
|
|
query = query.filter(and_(*and_conditions)) # Aplica todos os AND combinados
|
|
if or_conditions:
|
|
query = query.filter(or_(*or_conditions)) # Aplica todos os OR combinados
|
|
|
|
# Filtrar por IDs
|
|
if uuids:
|
|
query = query.where(getattr(self.model, coluna).in_(uuids))
|
|
|
|
# Verifica se o modelo tem a coluna 'ativo' e adiciona a condição
|
|
if hasattr(self.model, 'ativo'):
|
|
query = query.where(self.model.ativo.is_(True))
|
|
|
|
# Ordenação
|
|
# Resolução de colunas no contexto do modelo e seus relacionamentos
|
|
if order_by:
|
|
if ascending is None:
|
|
ascending = [True] * len(order_by) # Define `True` para todas as colunas por padrão
|
|
|
|
if filters: # Caso existam filtros, usamos o formato atual
|
|
for i, order_col in enumerate(order_by):
|
|
path_parts = order_col.split(".")
|
|
column = self.model
|
|
|
|
# Percorrer os relacionamentos
|
|
for part in path_parts[:-1]:
|
|
column = getattr(column, part).property.mapper.class_
|
|
|
|
# Resgatar a coluna final
|
|
final_column = getattr(column, path_parts[-1], None)
|
|
if final_column is None:
|
|
raise ValueError(f"Coluna de ordenação '{order_col}' não encontrada.")
|
|
|
|
# Adicionar a ordenação na consulta
|
|
query = query.order_by(
|
|
final_column.asc() if ascending[i] else final_column.desc()
|
|
)
|
|
else: # Caso não existam filtros, usamos o formato simples
|
|
order_by_column = getattr(self.model, order_by, None)
|
|
if not order_by_column:
|
|
raise ValueError(
|
|
f"A coluna de ordenação '{order_by}' não foi encontrada na tabela "
|
|
f"{self.model.__tablename__}."
|
|
)
|
|
query = query.order_by(
|
|
order_by_column.asc() if ascending else order_by_column.desc()
|
|
)
|
|
|
|
# Executar a consulta
|
|
result = await self.session.execute(query)
|
|
return result.scalars().all()
|
|
|
|
except SQLAlchemyError as e:
|
|
# Lidar com erros do SQLAlchemy
|
|
http_exception = HTTPException(
|
|
status_code=400,
|
|
detail=f"Erro interno do servidor ao acessar o banco de dados: {str(e)}"
|
|
)
|
|
|
|
raise http_exception
|
|
|
|
@audit_log
|
|
async def update_by_id(self, update: Schema, coluna: str) -> dict:
|
|
uuid = str(update.uuid)
|
|
db_model = await self.get_one_by_id(uuid, coluna, with_for_update=True)
|
|
|
|
if not db_model:
|
|
raise HTTPException(status_code=404,
|
|
detail=f"{self.model.__tablename__}{coluna}={uuid} não encontrada.")
|
|
|
|
# Guardar o estado atual antes da modificação
|
|
original_model = db_model.__dict__.copy()
|
|
|
|
values = update.model_dump(exclude_unset=True)
|
|
for k, v in values.items():
|
|
setattr(db_model, k, v)
|
|
|
|
try:
|
|
|
|
return {"db_models": db_model, "original_models": original_model, "operation": "UPDATE"}
|
|
except IntegrityError:
|
|
raise IntegrityConflictException(
|
|
f"{self.model.__tablename__} {coluna}={uuid} conflito com dados existentes."
|
|
)
|
|
|
|
@audit_log
|
|
async def update_many_by_ids(self, updates: List[Schema], coluna: str, return_models: bool = False) -> dict:
|
|
uuids = [str(update.uuid) for update in updates]
|
|
db_models = await self.get_many_by_ids(coluna, uuids, with_for_update=True)
|
|
|
|
if not db_models:
|
|
raise HTTPException(status_code=404,
|
|
detail=f"{self.model.__tablename__} {coluna}={uuids} não encontrada.")
|
|
try:
|
|
# Capturar o estado original dos modelos antes da modificação
|
|
original_models = [db_model.__dict__.copy() for db_model in db_models]
|
|
|
|
# Aplicar as atualizações
|
|
for db_model in db_models:
|
|
update_data = next((item for item in updates if item.uuid == getattr(db_model, coluna)), None)
|
|
if update_data:
|
|
values = update_data.model_dump(exclude_unset=True)
|
|
for k, v in values.items():
|
|
setattr(db_model, k, v)
|
|
|
|
# Retornar os modelos atualizados e os originais para o audit_log
|
|
return {
|
|
"db_models": db_models, # Lista de modelos que foram modificados
|
|
"original_models": original_models, # Lista de estados originais
|
|
"operation": "UPDATE"
|
|
}
|
|
except SQLAlchemyError as e:
|
|
handler = ErrorHandler()
|
|
handler.handle_error(e)
|
|
|
|
@audit_log
|
|
async def remove_by_id(self, uuid: str | UUID, coluna: str) -> dict:
|
|
if not uuid:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Não foi informado nenhum UUID")
|
|
|
|
# Tentar buscar o objeto antes de removê-lo para auditoria
|
|
db_model = await self.get_one_by_id(uuid, coluna)
|
|
if not db_model:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"{self.model.__tablename__} com {coluna}={uuid} não encontrado.")
|
|
|
|
try:
|
|
query = delete(self.model).where(getattr(self.model, coluna) == uuid)
|
|
except AttributeError:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"A Coluna {coluna} não existe em: {self.model.__tablename__}.")
|
|
|
|
try:
|
|
rows = await self.session.execute(query)
|
|
await self.session.flush() # Confirma a exclusão, mas não comita ainda
|
|
|
|
if rows.rowcount is None or rows.rowcount == 0:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, )
|
|
|
|
# Retorna o modelo deletado e o rowcount
|
|
return {"db_models": db_model, "rowcount": rows.rowcount, "operation": "DELETE"}
|
|
except SQLAlchemyError as e:
|
|
handler = ErrorHandler()
|
|
handler.handle_error(e)
|
|
|
|
@audit_log
|
|
async def remove_many_by_ids(self, uuids: List[str | UUID], coluna: str) -> dict:
|
|
if not uuids:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"Não foi informando nenhum uuid")
|
|
|
|
# Obter os modelos antes de deletá-los para fins de auditoria
|
|
db_models = await self.get_many_by_ids(coluna, uuids)
|
|
|
|
if not db_models:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
|
detail=f"{self.model.__tablename__} {coluna}={uuids} não encontrada.")
|
|
|
|
try:
|
|
query = delete(self.model).where(getattr(self.model, coluna).in_(uuids))
|
|
rows = await self.session.execute(query)
|
|
except IntegrityError:
|
|
await self.session.rollback()
|
|
raise IntegrityConflictException(
|
|
f"Erro ao deletar registros em {self.model.__tablename__}."
|
|
)
|
|
|
|
if rows.rowcount is None or rows.rowcount == 0:
|
|
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND)
|
|
|
|
# Retornar os modelos deletados e a contagem de linhas para o audit_log
|
|
return {
|
|
"db_models": db_models, # Modelos que foram deletados
|
|
"operation": "DELETE", # Especifica que é uma operação de deleção
|
|
"rowcount": rows.rowcount # Número de registros deletados
|
|
}
|
|
|
|
async def remove_by_column(self, column_name: str, value: str | UUID) -> dict:
|
|
"""
|
|
Remove um registro com base no nome da coluna e no valor correspondente.
|
|
"""
|
|
try:
|
|
# Verificar se a coluna existe no modelo
|
|
if not hasattr(self.model, column_name):
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"A coluna {column_name} não existe em {self.model.__tablename__}."
|
|
)
|
|
|
|
# Executar a exclusão
|
|
query = delete(self.model).where(getattr(self.model, column_name) == value)
|
|
rows = await self.session.execute(query)
|
|
await self.session.flush()
|
|
|
|
if rows.rowcount is None or rows.rowcount == 0:
|
|
raise HTTPException(
|
|
status_code=404,
|
|
detail=f"Nenhum registro encontrado em {self.model.__tablename__} com {column_name}={value}."
|
|
)
|
|
|
|
return {"rowcount": rows.rowcount, "operation": "DELETE", "column": column_name, "value": value}
|
|
|
|
except SQLAlchemyError as e:
|
|
handler = ErrorHandler()
|
|
handler.handle_error(e)
|
|
|
|
async def ativar_registro(self, update: Schema, coluna: str) -> dict:
|
|
"""
|
|
Ativa um registro atualizando o campo 'ativo' para True.
|
|
"""
|
|
update.ativo = True
|
|
return await self.update_by_id(update, coluna)
|
|
|
|
async def desativar_registro(self, update: Schema, coluna: str) -> dict:
|
|
"""
|
|
Desativa um registro atualizando o campo 'ativo' para False.
|
|
"""
|
|
|
|
update.ativo = False
|
|
return await self.update_by_id(update, coluna)
|