Skip to content
Merged

V5 #6

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 87 additions & 1 deletion backend/app/controllers/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

from fastapi import APIRouter, Header, HTTPException

from app.models.project import ProjectListResponse
from app.models.project import (
Project,
ProjectCreateRequest,
ProjectListResponse,
ProjectUpdateRequest,
)
from app.services.project import ProjectService
from app.utils.database import db_client

Expand Down Expand Up @@ -53,3 +58,84 @@ async def get_projects(
raise HTTPException(
status_code=500, detail="An unexpected error occurred"
)

@router.post(
"",
response_model=Project,
status_code=201,
)
async def create_project(
project_data: ProjectCreateRequest,
authorization: str = Header(None),
) -> Project:
"""
Create a new project.
"""
log.info(f"Creating project for user_id: {project_data.user_id}")
try:
# Extract token from authorization header
token = authorization.replace("Bearer ", "") if authorization else ""

# Get database client
supabase_client = await db_client(token=token)

# Create project
project = await self.service.create_project(
supabase_client=supabase_client, project_data=project_data
)

log.info(f"Successfully created project with id: {project.id}")
return project

except RuntimeError as e:
log.error(f"Service error: {e}")
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
log.error(f"Unexpected error: {e}")
raise HTTPException(
status_code=500, detail="An unexpected error occurred"
)

@router.put(
"/{project_id}",
response_model=Project,
)
async def update_project(
project_id: str,
project_data: ProjectUpdateRequest,
user_id: str,
authorization: str = Header(None),
) -> Project:
"""
Update an existing project.
"""
log.info(f"Updating project {project_id} for user_id: {user_id}")
try:
# Extract token from authorization header
token = authorization.replace("Bearer ", "") if authorization else ""

# Get database client
supabase_client = await db_client(token=token)

# Update project
project = await self.service.update_project(
supabase_client=supabase_client,
project_id=project_id,
user_id=user_id,
project_data=project_data,
)

log.info(f"Successfully updated project with id: {project.id}")
return project

except RuntimeError as e:
log.error(f"Service error: {e}")
# Check if it's an authorization error
if "unauthorized" in str(e).lower() or "not found" in str(e).lower():
raise HTTPException(status_code=404, detail=str(e))
raise HTTPException(status_code=500, detail=str(e))
except Exception as e:
log.error(f"Unexpected error: {e}")
raise HTTPException(
status_code=500, detail="An unexpected error occurred"
)
21 changes: 21 additions & 0 deletions backend/app/models/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,24 @@ class Project(BaseModel):

class ProjectListResponse(BaseModel):
projects: List[Project] = Field(description="List of projects for the user.")


class ProjectCreateRequest(BaseModel):
user_id: str = Field(description="The user ID who owns the project.")
name: str = Field(description="The name of the project.")
description: Optional[str] = Field(
default=None, description="The description of the project."
)
snapshot: Optional[Dict[str, Any]] = Field(
default=None, description="The snapshot data for the project."
)


class ProjectUpdateRequest(BaseModel):
name: Optional[str] = Field(default=None, description="The name of the project.")
description: Optional[str] = Field(
default=None, description="The description of the project."
)
snapshot: Optional[Dict[str, Any]] = Field(
default=None, description="The snapshot data for the project."
)
104 changes: 102 additions & 2 deletions backend/app/services/project.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import logging
from typing import List
from typing import Dict, List
from uuid import uuid4

from supabase._async.client import AsyncClient as Client

from app.models.project import Project
from app.models.project import Project, ProjectCreateRequest, ProjectUpdateRequest

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -47,3 +48,102 @@ async def get_projects_by_user_id(
except Exception as e:
log.error(f"Error fetching projects for user_id {user_id}: {e}")
raise RuntimeError(f"Failed to fetch projects: {e}")

async def create_project(
self, supabase_client: Client, project_data: ProjectCreateRequest
) -> Project:
"""
Create a new project.

Args:
supabase_client: The Supabase client instance
project_data: The project data to create

Returns:
Created Project object
"""
log.info(f"Creating project for user_id: {project_data.user_id}")

try:
# Prepare project data with generated ID
new_project = {
"id": str(uuid4()),
"user_id": project_data.user_id,
"name": project_data.name,
"description": project_data.description,
"snapshot": project_data.snapshot,
}

# Insert into projects table
response = (
await supabase_client.table("projects").insert(new_project).execute()
)

if not response.data or len(response.data) == 0:
raise RuntimeError("Failed to create project: No data returned")

project = Project(**response.data[0])
log.info(f"Successfully created project with id: {project.id}")

return project

except Exception as e:
log.error(f"Error creating project: {e}")
raise RuntimeError(f"Failed to create project: {e}")

async def update_project(
self,
supabase_client: Client,
project_id: str,
user_id: str,
project_data: ProjectUpdateRequest,
) -> Project:
"""
Update an existing project.

Args:
supabase_client: The Supabase client instance
project_id: The ID of the project to update
user_id: The user ID who owns the project (for authorization)
project_data: The project data to update

Returns:
Updated Project object
"""
log.info(f"Updating project {project_id} for user_id: {user_id}")

try:
# Prepare update data (only include non-None fields)
update_data: Dict = {}
if project_data.name is not None:
update_data["name"] = project_data.name
if project_data.description is not None:
update_data["description"] = project_data.description
if project_data.snapshot is not None:
update_data["snapshot"] = project_data.snapshot

if not update_data:
raise RuntimeError("No fields to update")

# Update the project (with user_id check for authorization)
response = (
await supabase_client.table("projects")
.update(update_data)
.eq("id", project_id)
.eq("user_id", user_id)
.execute()
)

if not response.data or len(response.data) == 0:
raise RuntimeError(
"Failed to update project: Project not found or unauthorized"
)

project = Project(**response.data[0])
log.info(f"Successfully updated project with id: {project.id}")

return project

except Exception as e:
log.error(f"Error updating project {project_id}: {e}")
raise RuntimeError(f"Failed to update project: {e}")
54 changes: 54 additions & 0 deletions frontend/src/actions/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export interface Project {
user_id: string;
name: string;
description?: string;
snapshot?: Record<string, unknown>;
created_at: string;
updated_at: string;
}
Expand All @@ -15,6 +16,19 @@ export interface ProjectsResponse {
projects: Project[];
}

export interface CreateProjectRequest {
user_id: string;
name: string;
description?: string;
snapshot?: Record<string, unknown>;
}

export interface UpdateProjectRequest {
name?: string;
description?: string;
snapshot?: Record<string, unknown>;
}

export async function fetchProjects(userId: string): Promise<ProjectsResponse> {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
const response = await fetch(`${apiUrl}/api/projects/${userId}`, {
Expand All @@ -31,3 +45,43 @@ export async function fetchProjects(userId: string): Promise<ProjectsResponse> {

return response.json();
}

export async function createProject(projectData: CreateProjectRequest): Promise<Project> {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
const response = await fetch(`${apiUrl}/api/projects`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(projectData),
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to create project');
}

return response.json();
}

export async function updateProject(
projectId: string,
userId: string,
projectData: UpdateProjectRequest,
): Promise<Project> {
const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8080';
const response = await fetch(`${apiUrl}/api/projects/${projectId}?user_id=${userId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(projectData),
});

if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
throw new Error(errorData.detail || 'Failed to update project');
}

return response.json();
}
Loading
Loading