api-admim/app/database/RepositoryBase.py

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)