opencode: delete method for stacks
This commit is contained in:
@ -133,29 +133,29 @@ func (client AgentClient) getRequest(body []byte) (*http.Request, error) {
|
||||
func (client AgentClient) Request(req *AgentRequestBody) (AgentResponse, error) {
|
||||
jsonAiRequest, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not format JSON", err)
|
||||
return AgentResponse{}, fmt.Errorf("Could not format JSON: %w", err)
|
||||
}
|
||||
|
||||
httpRequest, err := client.getRequest(jsonAiRequest)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not get request", err)
|
||||
return AgentResponse{}, fmt.Errorf("Could not get request: %w", err)
|
||||
}
|
||||
|
||||
resp, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not send request", err)
|
||||
return AgentResponse{}, fmt.Errorf("Could not send request: %w", err)
|
||||
}
|
||||
|
||||
response, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not read body", err)
|
||||
return AgentResponse{}, fmt.Errorf("Could not read body: %w", err)
|
||||
}
|
||||
|
||||
agentResponse := AgentResponse{}
|
||||
err = json.Unmarshal(response, &agentResponse)
|
||||
|
||||
if err != nil {
|
||||
return AgentResponse{}, fmt.Errorf("Could not unmarshal response, response: %s", string(response), err)
|
||||
return AgentResponse{}, fmt.Errorf("Could not unmarshal response, response: %s: %w", string(response), err)
|
||||
}
|
||||
|
||||
if len(agentResponse.Choices) != 1 {
|
||||
|
@ -354,6 +354,163 @@ func TestAllRoutes(t *testing.T) {
|
||||
t.Errorf("Expected status 400 for invalid ID, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack without authentication", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/"+fakeUUID.String(), "", nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusUnauthorized {
|
||||
t.Errorf("Expected status 401 for unauthenticated delete, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack with invalid ID", func(t *testing.T) {
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/invalid-id", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for invalid ID, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete non-existent stack", func(t *testing.T) {
|
||||
fakeUUID := uuid.New()
|
||||
resp := tc.makeRequest(t, "DELETE", "/stacks/"+fakeUUID.String(), stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 for non-existent stack, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Create and delete stack successfully", func(t *testing.T) {
|
||||
// First create a stack
|
||||
stackData := map[string]string{
|
||||
"title": "Stack to Delete",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to create stack for deletion test, got %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the list of stacks to find the created stack ID
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
|
||||
var stacks []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stacks response: %v", err)
|
||||
resp.Body.Close()
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
if len(stacks) == 0 {
|
||||
t.Errorf("No stacks found after creation")
|
||||
return
|
||||
}
|
||||
|
||||
// Find the stack we just created
|
||||
var stackToDelete map[string]interface{}
|
||||
for _, stack := range stacks {
|
||||
if name, ok := stack["Name"].(string); ok && name == "Stack to Delete" {
|
||||
stackToDelete = stack
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if stackToDelete == nil {
|
||||
t.Errorf("Could not find created stack")
|
||||
return
|
||||
}
|
||||
|
||||
stackID, ok := stackToDelete["ID"].(string)
|
||||
if !ok {
|
||||
t.Errorf("Stack ID not found or not a string")
|
||||
return
|
||||
}
|
||||
|
||||
// Now delete the stack
|
||||
resp = tc.makeRequest(t, "DELETE", "/stacks/"+stackID, stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status 200 for successful delete, got %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// Verify the stack is gone by trying to get it again
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
var stacksAfterDelete []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacksAfterDelete); err != nil {
|
||||
t.Errorf("Failed to decode stacks response after delete: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Check that the deleted stack is no longer in the list
|
||||
for _, stack := range stacksAfterDelete {
|
||||
if id, ok := stack["ID"].(string); ok && id == stackID {
|
||||
t.Errorf("Stack still exists after deletion")
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Delete stack belonging to different user", func(t *testing.T) {
|
||||
// Create a stack with stackUser
|
||||
stackData := map[string]string{
|
||||
"title": "Other User's Stack",
|
||||
"fields": "name,description,value",
|
||||
}
|
||||
|
||||
resp := tc.makeJSONRequest(t, "POST", "/stacks/", stackUser.Token, stackData)
|
||||
resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Failed to create stack for ownership test, got %d", resp.StatusCode)
|
||||
return
|
||||
}
|
||||
|
||||
// Get the stack ID
|
||||
resp = tc.makeRequest(t, "GET", "/stacks/", stackUser.Token, nil)
|
||||
|
||||
var stacks []map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&stacks); err != nil {
|
||||
t.Errorf("Failed to decode stacks response: %v", err)
|
||||
resp.Body.Close()
|
||||
return
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
var stackID string
|
||||
for _, stack := range stacks {
|
||||
if name, ok := stack["Name"].(string); ok && name == "Other User's Stack" {
|
||||
if id, ok := stack["ID"].(string); ok {
|
||||
stackID = id
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if stackID == "" {
|
||||
t.Errorf("Could not find created stack ID")
|
||||
return
|
||||
}
|
||||
|
||||
// Try to delete the stack with a different user (imageUser)
|
||||
resp = tc.makeRequest(t, "DELETE", "/stacks/"+stackID, imageUser.Token, nil)
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusBadRequest {
|
||||
t.Errorf("Expected status 400 when deleting another user's stack, got %d", resp.StatusCode)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Image Routes", func(t *testing.T) {
|
||||
|
@ -1,78 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/auth"
|
||||
"screenmark/screenmark/images"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/stacks"
|
||||
|
||||
ourmiddleware "screenmark/screenmark/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
type TestAiClient struct {
|
||||
ImageInfo client.ImageMessageContent
|
||||
}
|
||||
|
||||
func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (client.ImageMessageContent, error) {
|
||||
return client.ImageInfo, nil
|
||||
}
|
||||
|
||||
func setupRouter(db *sql.DB) chi.Router {
|
||||
imageModel := models.NewImageModel(db)
|
||||
stackModel := models.NewListModel(db)
|
||||
|
||||
stackHandler := stacks.CreateStackHandler(db)
|
||||
authHandler := auth.CreateAuthHandler(db)
|
||||
imageHandler := images.CreateImageHandler(db)
|
||||
|
||||
notifier := NewNotifier[Notification](10)
|
||||
|
||||
// Only start event listeners if not in test environment
|
||||
if os.Getenv("GO_TEST_ENVIRONMENT") != "true" {
|
||||
|
||||
// TODO: should extract these into a notification manager
|
||||
// And actually make them the same code.
|
||||
// The events are basically the same.
|
||||
|
||||
go ListenNewImageEvents(db)
|
||||
go ListenProcessingImageStatus(db, imageModel, ¬ifier)
|
||||
go ListenNewStackEvents(db)
|
||||
go ListenProcessingStackStatus(db, stackModel, ¬ifier)
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(ourmiddleware.CorsMiddleware)
|
||||
|
||||
r.Route("/stacks", stackHandler.CreateRoutes)
|
||||
r.Route("/auth", authHandler.CreateRoutes)
|
||||
r.Route("/images", imageHandler.CreateRoutes)
|
||||
|
||||
r.Route("/notifications", func(r chi.Router) {
|
||||
r.Use(ourmiddleware.GetUserIdFromUrl)
|
||||
|
||||
r.Get("/", CreateEventsHandler(¬ifier))
|
||||
})
|
||||
|
||||
logWriter := DatabaseWriter{
|
||||
dbPool: db,
|
||||
}
|
||||
|
||||
r.Route("/logs", createLogHandler(&logWriter))
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
func main() {
|
||||
err := godotenv.Load()
|
||||
if err != nil {
|
||||
|
@ -38,7 +38,7 @@ type UserProcessingImage struct {
|
||||
func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.Image) (model.UserImagesToProcess, error) {
|
||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Failed to begin transaction", err)
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Failed to begin transaction: %w", err)
|
||||
}
|
||||
|
||||
insertImageStmt := Image.
|
||||
@ -49,7 +49,7 @@ func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.I
|
||||
insertedImage := model.Image{}
|
||||
err = insertImageStmt.QueryContext(ctx, tx, &insertedImage)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert/query new image. SQL %s.", insertImageStmt.DebugSql(), err)
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert/query new image. SQL %s: %w", insertImageStmt.DebugSql(), err)
|
||||
}
|
||||
|
||||
stmt := UserImagesToProcess.
|
||||
@ -60,7 +60,7 @@ func (m ImageModel) Process(ctx context.Context, userId uuid.UUID, image model.I
|
||||
userImage := model.UserImagesToProcess{}
|
||||
err = stmt.QueryContext(ctx, tx, &userImage)
|
||||
if err != nil {
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert user_image", err)
|
||||
return model.UserImagesToProcess{}, fmt.Errorf("Could not insert user_image: %w", err)
|
||||
}
|
||||
|
||||
err = tx.Commit()
|
||||
|
@ -247,6 +247,64 @@ func (m ListModel) SaveProcessing(ctx context.Context, userID uuid.UUID, title s
|
||||
return err
|
||||
}
|
||||
|
||||
// ========================================
|
||||
// DELETE methods
|
||||
// ========================================
|
||||
|
||||
func (m ListModel) Delete(ctx context.Context, listID uuid.UUID, userID uuid.UUID) error {
|
||||
// First verify the list belongs to the user
|
||||
checkOwnershipStmt := Lists.
|
||||
SELECT(Lists.ID).
|
||||
WHERE(Lists.ID.EQ(UUID(listID)).AND(Lists.UserID.EQ(UUID(userID))))
|
||||
|
||||
var existingList model.Lists
|
||||
err := checkOwnershipStmt.QueryContext(ctx, m.dbPool, &existingList)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not verify list ownership: %w", err)
|
||||
}
|
||||
|
||||
// Start a transaction to ensure all deletions happen atomically
|
||||
tx, err := m.dbPool.BeginTx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not start transaction: %w", err)
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// Delete in reverse order of dependencies:
|
||||
// 1. Delete schema items first
|
||||
deleteSchemaItemsStmt := SchemaItems.DELETE().
|
||||
WHERE(SchemaItems.SchemaID.IN(
|
||||
Schemas.SELECT(Schemas.ID).
|
||||
WHERE(Schemas.ListID.EQ(UUID(listID))),
|
||||
))
|
||||
_, err = deleteSchemaItemsStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not delete schema items: %w", err)
|
||||
}
|
||||
|
||||
// 2. Delete schemas
|
||||
deleteSchemasStmt := Schemas.DELETE().WHERE(Schemas.ListID.EQ(UUID(listID)))
|
||||
_, err = deleteSchemasStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not delete schemas: %w", err)
|
||||
}
|
||||
|
||||
// 3. Delete the list itself
|
||||
deleteListStmt := Lists.DELETE().WHERE(Lists.ID.EQ(UUID(listID)))
|
||||
_, err = deleteListStmt.ExecContext(ctx, tx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not delete list: %w", err)
|
||||
}
|
||||
|
||||
// Commit the transaction
|
||||
err = tx.Commit()
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not commit transaction: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewListModel(db *sql.DB) ListModel {
|
||||
return ListModel{dbPool: db}
|
||||
}
|
||||
|
71
backend/router.go
Normal file
71
backend/router.go
Normal file
@ -0,0 +1,71 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"os"
|
||||
"screenmark/screenmark/agents/client"
|
||||
"screenmark/screenmark/auth"
|
||||
"screenmark/screenmark/images"
|
||||
"screenmark/screenmark/models"
|
||||
"screenmark/screenmark/stacks"
|
||||
|
||||
ourmiddleware "screenmark/screenmark/middleware"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
)
|
||||
|
||||
type TestAiClient struct {
|
||||
ImageInfo client.ImageMessageContent
|
||||
}
|
||||
|
||||
func (client TestAiClient) GetImageInfo(imageName string, imageData []byte) (client.ImageMessageContent, error) {
|
||||
return client.ImageInfo, nil
|
||||
}
|
||||
|
||||
func setupRouter(db *sql.DB) chi.Router {
|
||||
imageModel := models.NewImageModel(db)
|
||||
stackModel := models.NewListModel(db)
|
||||
|
||||
stackHandler := stacks.CreateStackHandler(db)
|
||||
authHandler := auth.CreateAuthHandler(db)
|
||||
imageHandler := images.CreateImageHandler(db)
|
||||
|
||||
notifier := NewNotifier[Notification](10)
|
||||
|
||||
// Only start event listeners if not in test environment
|
||||
if os.Getenv("GO_TEST_ENVIRONMENT") != "true" {
|
||||
|
||||
// TODO: should extract these into a notification manager
|
||||
// And actually make them the same code.
|
||||
// The events are basically the same.
|
||||
|
||||
go ListenNewImageEvents(db)
|
||||
go ListenProcessingImageStatus(db, imageModel, ¬ifier)
|
||||
go ListenNewStackEvents(db)
|
||||
go ListenProcessingStackStatus(db, stackModel, ¬ifier)
|
||||
}
|
||||
|
||||
r := chi.NewRouter()
|
||||
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(ourmiddleware.CorsMiddleware)
|
||||
|
||||
r.Route("/stacks", stackHandler.CreateRoutes)
|
||||
r.Route("/auth", authHandler.CreateRoutes)
|
||||
r.Route("/images", imageHandler.CreateRoutes)
|
||||
|
||||
r.Route("/notifications", func(r chi.Router) {
|
||||
r.Use(ourmiddleware.GetUserIdFromUrl)
|
||||
|
||||
r.Get("/", CreateEventsHandler(¬ifier))
|
||||
})
|
||||
|
||||
logWriter := DatabaseWriter{
|
||||
dbPool: db,
|
||||
}
|
||||
|
||||
r.Route("/logs", createLogHandler(&logWriter))
|
||||
|
||||
return r
|
||||
}
|
@ -2,10 +2,13 @@ package stacks
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
. "screenmark/screenmark/.gen/haystack/haystack/model"
|
||||
"screenmark/screenmark/middleware"
|
||||
"screenmark/screenmark/models"
|
||||
"strings"
|
||||
|
||||
"github.com/charmbracelet/log"
|
||||
"github.com/go-chi/chi/v5"
|
||||
@ -66,6 +69,29 @@ func (h *StackHandler) editStack(req EditStack, w http.ResponseWriter, r *http.R
|
||||
w.WriteHeader(http.StatusNotImplemented)
|
||||
}
|
||||
|
||||
func (h *StackHandler) deleteStack(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
userID, err := middleware.GetUserID(ctx, h.logger, w)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
listID, err := middleware.GetPathParamID(h.logger, "listID", w, r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = h.stackModel.Delete(ctx, listID, userID)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not delete stack", "err", err)
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
type CreateStackBody struct {
|
||||
Title string `json:"title"`
|
||||
|
||||
@ -80,9 +106,27 @@ func (h *StackHandler) createStack(body CreateStackBody, w http.ResponseWriter,
|
||||
return
|
||||
}
|
||||
|
||||
err = h.stackModel.SaveProcessing(ctx, userID, body.Title, body.Fields)
|
||||
// Convert fields string to basic schema items
|
||||
// For now, create a simple schema item for each field
|
||||
var schemaItems []SchemaItems
|
||||
if body.Fields != "" {
|
||||
fields := strings.Split(body.Fields, ",")
|
||||
for i, field := range fields {
|
||||
field = strings.TrimSpace(field)
|
||||
if field != "" {
|
||||
schemaItems = append(schemaItems, SchemaItems{
|
||||
Item: field,
|
||||
Value: "",
|
||||
Description: fmt.Sprintf("Field %d: %s", i+1, field),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Use empty description for now since the API doesn't provide one
|
||||
_, err = h.stackModel.Save(ctx, userID, body.Title, "", schemaItems)
|
||||
if err != nil {
|
||||
h.logger.Warn("could not save processing", "err", err)
|
||||
h.logger.Warn("could not save stack", "err", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
@ -102,6 +146,7 @@ func (h *StackHandler) CreateRoutes(r chi.Router) {
|
||||
|
||||
r.Post("/", middleware.WithValidatedPost(h.createStack))
|
||||
r.Patch("/{listID}", middleware.WithValidatedPost(h.editStack))
|
||||
r.Delete("/{listID}", h.deleteStack)
|
||||
})
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user