# 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)