Skip to content

Commit f72dd32

Browse files
Merge pull request #94 from fivelibx/develop
Migracao para Supabase e Integracao de Recursos
2 parents eb2d971 + d72eba4 commit f72dd32

21 files changed

Lines changed: 561 additions & 395 deletions

File tree

.gitignore

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,9 @@ notas/
33
.env
44
__pycache__/
55
.pytest_cache/
6-
.venv/
6+
.venv/
7+
8+
# Arquivos do Supabase
9+
supabase/
10+
supabase/.temp/
11+
EOF

backend/api/routes/health.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
11
from fastapi import APIRouter
2+
from database.config import supabase
23

3-
router = APIRouter(prefix="/health", tags=["health"])
4+
router = APIRouter(tags=["health"])
45

56
@router.get("/")
67
def health_check():
8+
try:
9+
supabase.table("tool").select("count", count="exact").limit(1).execute()
10+
db_status = "running"
11+
except Exception as e:
12+
db_status = f"unreachable: {str(e)}"
13+
714
return {
815
"status": "OK",
9-
"database": "untested", #TODO: Atualizar após subir banco está configurado.
10-
"service": "running"
11-
}
12-
16+
"database": db_status,
17+
"service": "running"
18+
}

backend/api/routes/tools.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from fastapi import APIRouter, Depends, HTTPException
2+
3+
from services.tool_service import ToolService
4+
from supabase import Client
5+
from database.base import get_supabase
6+
from repositories.tool_repository import ToolRepository
7+
8+
router = APIRouter()
9+
10+
@router.get("/")
11+
async def get_tools(db: Client = Depends(get_supabase)):
12+
repository = ToolRepository(db)
13+
return repository.get_all()
14+
15+
@router.patch("/{tool_id}/star")
16+
async def increment_star(tool_id: int, db: Client = Depends(get_supabase)):
17+
repository = ToolRepository(db)
18+
service = ToolService(repository)
19+
20+
new_stars = service.increment_tool_stars(tool_id)
21+
22+
if new_stars is None:
23+
raise HTTPException(status_code=404, detail="Ferramenta não encontrada")
24+
25+
return {"stars": new_stars}

backend/database/base.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
from sqlalchemy.ext.declarative import declarative_base
2-
from sqlalchemy.orm import DeclarativeBase
1+
from supabase import create_client, Client
2+
from .config import settings
33

4-
class Base(DeclarativeBase):
5-
pass
4+
supabase: Client = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)
5+
6+
def get_supabase() -> Client:
7+
return supabase

backend/database/config.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
from pydantic_settings import BaseSettings, SettingsConfigDict
2+
from supabase import create_client, Client
23

34
class Settings(BaseSettings):
4-
DATABASE_URL: str = ""
5-
model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore")
6-
7-
settings = Settings()
5+
SUPABASE_URL: str = ""
6+
SUPABASE_KEY: str = ""
7+
8+
model_config = SettingsConfigDict(
9+
env_file=".env",
10+
env_file_encoding="utf-8",
11+
extra="ignore"
12+
)
13+
14+
settings = Settings()
15+
16+
supabase: Client = create_client(settings.SUPABASE_URL, settings.SUPABASE_KEY)

backend/database/init_schema.sql

Lines changed: 68 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,73 @@
1-
CREATE TABLE "user"(
2-
"id" SERIAL NOT NULL,
3-
"nome" VARCHAR(255) NOT NULL,
4-
"email" VARCHAR(255) NOT NULL,
5-
"senha" VARCHAR(255) NOT NULL,
6-
"data_nascimento" DATE NOT NULL,
7-
"perfil" VARCHAR(255) NOT NULL,
8-
"criado_at" TIMESTAMP(0) WITH
9-
TIME zone NOT NULL
1+
-- Usuários
2+
CREATE TABLE "user" (
3+
"id" SERIAL NOT NULL,
4+
"nome" VARCHAR(255) NOT NULL,
5+
"email" VARCHAR(255) NOT NULL,
6+
"senha" VARCHAR(255) NOT NULL,
7+
"data_nascimento" DATE NOT NULL,
8+
"perfil" VARCHAR(255) NOT NULL,
9+
"criado_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
10+
PRIMARY KEY ("id"),
11+
CONSTRAINT "user_email_unique" UNIQUE ("email")
1012
);
11-
ALTER TABLE
12-
"user" ADD PRIMARY KEY("id");
13-
ALTER TABLE
14-
"user" ADD CONSTRAINT "user_email_unique" UNIQUE("email");
15-
CREATE TABLE "tool"(
16-
"id" SERIAL NOT NULL,
17-
"nome" VARCHAR(255) NOT NULL,
18-
"descricao" TEXT NOT NULL,
19-
"url_oficial" TEXT NOT NULL,
20-
"linguagem" VARCHAR(255) NOT NULL,
21-
"status_ativo" BOOLEAN NOT NULL DEFAULT TRUE
13+
14+
-- Ferramentas
15+
CREATE TABLE "tool" (
16+
"id" SERIAL NOT NULL,
17+
"nome" VARCHAR(255) NOT NULL,
18+
"descricao" TEXT NOT NULL,
19+
"url_oficial" TEXT NOT NULL,
20+
"linguagem" VARCHAR(255) NOT NULL,
21+
"status_ativo" BOOLEAN NOT NULL DEFAULT TRUE,
22+
PRIMARY KEY ("id"),
23+
CONSTRAINT "tool_nome_unique" UNIQUE ("nome")
2224
);
23-
ALTER TABLE
24-
"tool" ADD PRIMARY KEY("id");
25-
ALTER TABLE
26-
"tool" ADD CONSTRAINT "tool_nome_unique" UNIQUE("nome");
27-
COMMENT
28-
ON COLUMN
29-
"tool"."status_ativo" IS 'Por padrão, toda ferramenta nova cadastrada entra como ativa.';
30-
CREATE TABLE "favorite"(
31-
"usuario_id" INTEGER NOT NULL,
32-
"ferramenta_id" INTEGER NOT NULL,
33-
"adicionado_at" TIMESTAMP(0) WITH
34-
TIME zone NOT NULL
25+
26+
COMMENT ON COLUMN "tool"."status_ativo" IS 'Por padrão, toda ferramenta nova cadastrada entra como ativa.';
27+
28+
-- Favoritos (PK composta corrigida — original tinha dois ADD PRIMARY KEY separados, o que é inválido)
29+
CREATE TABLE "favorite" (
30+
"usuario_id" INTEGER NOT NULL,
31+
"ferramenta_id" INTEGER NOT NULL,
32+
"adicionado_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
33+
PRIMARY KEY ("usuario_id", "ferramenta_id")
3534
);
36-
CREATE INDEX "favorite_usuario_id_index" ON
37-
"favorite"("usuario_id");
38-
CREATE INDEX "favorite_ferramenta_id_index" ON
39-
"favorite"("ferramenta_id");
40-
CREATE INDEX "favorite_adicionado_at_index" ON
41-
"favorite"("adicionado_at");
42-
CREATE INDEX "favorite_usuario_id_ferramenta_id_adicionado_at_index" ON
43-
"favorite"(
44-
"usuario_id",
45-
"ferramenta_id",
46-
"adicionado_at"
47-
);
48-
ALTER TABLE
49-
"favorite" ADD PRIMARY KEY("usuario_id");
50-
ALTER TABLE
51-
"favorite" ADD PRIMARY KEY("ferramenta_id");
52-
CREATE TABLE "private_link"(
53-
"id" SERIAL NOT NULL,
54-
"usuario_id" INTEGER NOT NULL,
55-
"titulo" VARCHAR(255) NOT NULL,
56-
"url" TEXT NOT NULL
35+
36+
CREATE INDEX "favorite_usuario_id_idx" ON "favorite" ("usuario_id");
37+
CREATE INDEX "favorite_ferramenta_id_idx" ON "favorite" ("ferramenta_id");
38+
CREATE INDEX "favorite_adicionado_at_idx" ON "favorite" ("adicionado_at");
39+
40+
-- Links Privados (FK corrigida — original apontava "id" para "user"."id", o correto é "usuario_id")
41+
CREATE TABLE "private_link" (
42+
"id" SERIAL NOT NULL,
43+
"usuario_id" INTEGER NOT NULL,
44+
"titulo" VARCHAR(255) NOT NULL,
45+
"url" TEXT NOT NULL,
46+
PRIMARY KEY ("id")
5747
);
58-
CREATE INDEX "private_link_usuario_id_index" ON
59-
"private_link"("usuario_id");
60-
ALTER TABLE
61-
"private_link" ADD PRIMARY KEY("id");
62-
CREATE TABLE "support_ticket"(
63-
"id" SERIAL NOT NULL,
64-
"usuario_id" INTEGER NOT NULL,
65-
"email_contato" VARCHAR(255) NOT NULL,
66-
"mensagem" TEXT NOT NULL,
67-
"secao_site" VARCHAR(255) NOT NULL,
68-
"status" VARCHAR(255) NOT NULL DEFAULT 'Pendente',
69-
"criado_at" TIMESTAMP(0) WITH
70-
TIME zone NOT NULL DEFAULT CURRENT_TIMESTAMP
48+
49+
CREATE INDEX "private_link_usuario_id_idx" ON "private_link" ("usuario_id");
50+
51+
-- Chamados de Suporte
52+
CREATE TABLE "support_ticket" (
53+
"id" SERIAL NOT NULL,
54+
"usuario_id" INTEGER NOT NULL,
55+
"email_contato" VARCHAR(255) NOT NULL,
56+
"mensagem" TEXT NOT NULL,
57+
"secao_site" VARCHAR(255) NOT NULL,
58+
"status" VARCHAR(255) NOT NULL DEFAULT 'Pendente',
59+
"criado_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
60+
PRIMARY KEY ("id")
7161
);
72-
CREATE INDEX "support_ticket_usuario_id_index" ON
73-
"support_ticket"("usuario_id");
74-
ALTER TABLE
75-
"support_ticket" ADD PRIMARY KEY("id");
76-
COMMENT
77-
ON COLUMN
78-
"support_ticket"."status" IS 'Todo ticket novo aparecerá automaticamente como "Pendente".';
79-
COMMENT
80-
ON COLUMN
81-
"support_ticket"."criado_at" IS 'O banco registra a data/hora exata do momento em que o registro foi criado.';
82-
ALTER TABLE
83-
"support_ticket" ADD CONSTRAINT "support_ticket_usuario_id_foreign" FOREIGN KEY("usuario_id") REFERENCES "user"("id");
84-
ALTER TABLE
85-
"favorite" ADD CONSTRAINT "favorite_ferramenta_id_foreign" FOREIGN KEY("ferramenta_id") REFERENCES "tool"("id");
86-
ALTER TABLE
87-
"favorite" ADD CONSTRAINT "favorite_usuario_id_foreign" FOREIGN KEY("usuario_id") REFERENCES "user"("id");
88-
ALTER TABLE
89-
"private_link" ADD CONSTRAINT "private_link_id_foreign" FOREIGN KEY("id") REFERENCES "user"("id");
62+
63+
CREATE INDEX "support_ticket_status_idx" ON "support_ticket" ("status");
64+
CREATE INDEX "support_ticket_usuario_id_idx" ON "support_ticket" ("usuario_id");
65+
66+
COMMENT ON COLUMN "support_ticket"."status" IS 'Todo ticket novo aparecerá automaticamente como "Pendente".';
67+
COMMENT ON COLUMN "support_ticket"."criado_at" IS 'O banco registra a data/hora exata do momento em que o registro foi criado.';
68+
69+
-- Foreign Keys
70+
ALTER TABLE "favorite" ADD CONSTRAINT "favorite_usuario_id_fk" FOREIGN KEY ("usuario_id") REFERENCES "user" ("id");
71+
ALTER TABLE "favorite" ADD CONSTRAINT "favorite_ferramenta_id_fk" FOREIGN KEY ("ferramenta_id") REFERENCES "tool" ("id");
72+
ALTER TABLE "private_link" ADD CONSTRAINT "private_link_usuario_id_fk" FOREIGN KEY ("usuario_id") REFERENCES "user" ("id");
73+
ALTER TABLE "support_ticket" ADD CONSTRAINT "support_ticket_usuario_id_fk" FOREIGN KEY ("usuario_id") REFERENCES "user" ("id");

backend/database/models.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
from sqlalchemy import String, Text, Boolean, Integer, Date, DateTime, ForeignKey, Index, func
2+
from sqlalchemy.orm import Mapped, mapped_column, relationship
3+
from datetime import datetime
4+
from .base import Base
5+
6+
class Favorite(Base):
7+
"""
8+
Entidade: Favoritos (Relacionamento N:N)
9+
Finalidade: Persistência de itens favoritados (RF02).
10+
"""
11+
__tablename__ = "favorite"
12+
13+
usuario_id: Mapped[int] = mapped_column(
14+
Integer,
15+
ForeignKey("user.id", name="favorite_usuario_id_fk"),
16+
primary_key=True
17+
)
18+
ferramenta_id: Mapped[int] = mapped_column(
19+
Integer,
20+
ForeignKey("tool.id", name="favorite_ferramenta_id_fk"),
21+
primary_key=True
22+
)
23+
adicionado_at: Mapped[datetime] = mapped_column(
24+
DateTime(timezone=True),
25+
server_default=func.now()
26+
)
27+
28+
# Índices conforme schema.sql
29+
__table_args__ = (
30+
Index("favorite_usuario_id_idx", "usuario_id"),
31+
Index("favorite_ferramenta_id_idx", "ferramenta_id"),
32+
)
33+
34+
class Tool(Base):
35+
__tablename__ = "tool"
36+
37+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
38+
nome: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
39+
descricao: Mapped[str] = mapped_column(Text, nullable=False)
40+
url_oficial: Mapped[str] = mapped_column(Text, nullable=False)
41+
linguagem: Mapped[str] = mapped_column(String(255), nullable=False)
42+
status_ativo: Mapped[bool] = mapped_column(Boolean, server_default="true")
43+
44+
class User(Base):
45+
__tablename__ = "user"
46+
47+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
48+
nome: Mapped[str] = mapped_column(String(255), nullable=False)
49+
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False)
50+
senha: Mapped[str] = mapped_column(String(255), nullable=False)
51+
data_nascimento: Mapped[datetime] = mapped_column(Date, nullable=False)
52+
perfil: Mapped[str] = mapped_column(String(255), nullable=False)
53+
criado_at: Mapped[datetime] = mapped_column(
54+
DateTime(timezone=True),
55+
server_default=func.now()
56+
)
57+
58+
# Relacionamentos
59+
links_privados: Mapped[list["PrivateLink"]] = relationship(back_populates="usuario")
60+
tickets: Mapped[list["SupportTicket"]] = relationship(back_populates="usuario")
61+
62+
class PrivateLink(Base):
63+
__tablename__ = "private_link"
64+
65+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
66+
usuario_id: Mapped[int] = mapped_column(
67+
Integer,
68+
ForeignKey("user.id", name="private_link_usuario_id_fk"),
69+
nullable=False
70+
)
71+
titulo: Mapped[str] = mapped_column(String(255), nullable=False)
72+
url: Mapped[str] = mapped_column(Text, nullable=False)
73+
74+
usuario: Mapped["User"] = relationship(back_populates="links_privados")
75+
76+
__table_args__ = (
77+
Index("private_link_usuario_id_idx", "usuario_id"),
78+
)
79+
80+
class SupportTicket(Base):
81+
__tablename__ = "support_ticket"
82+
83+
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
84+
usuario_id: Mapped[int] = mapped_column(
85+
Integer,
86+
ForeignKey("user.id", name="support_ticket_usuario_id_fk"),
87+
nullable=False
88+
)
89+
email_contato: Mapped[str] = mapped_column(String(255), nullable=False)
90+
mensagem: Mapped[str] = mapped_column(Text, nullable=False)
91+
secao_site: Mapped[str] = mapped_column(String(255), nullable=False)
92+
status: Mapped[str] = mapped_column(String(255), server_default="Pendente")
93+
criado_at: Mapped[datetime] = mapped_column(
94+
DateTime(timezone=True),
95+
server_default=func.now()
96+
)
97+
98+
usuario: Mapped["User"] = relationship(back_populates="tickets")
99+
100+
__table_args__ = (
101+
Index("support_ticket_status_idx", "status"),
102+
)

backend/main.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22
from fastapi import FastAPI
33
from fastapi.middleware.cors import CORSMiddleware
44

5-
65
# Rotas da API
76
from api.routes.health import router as health_router
87
from api.routes.users import router as users_router
8+
from api.routes.tools import router as tools_router
99

1010
app = FastAPI(
1111
title="FiveLib API",
@@ -27,6 +27,11 @@
2727
allow_headers=["*"],
2828
)
2929

30+
@app.get("/")
31+
async def root():
32+
return {"message": "FiveLib API is running"}
33+
3034
# NOTE: Registro dos roteadores.
3135
app.include_router(health_router, prefix="/health")
32-
app.include_router(users_router, prefix="/users")
36+
app.include_router(users_router, prefix="/users")
37+
app.include_router(tools_router, prefix="/resources", tags=["Tools"])

0 commit comments

Comments
 (0)