Skip to content

Commit

Permalink
frontend: implement linking cards to entities
Browse files Browse the repository at this point in the history
  • Loading branch information
NickSavage committed Dec 19, 2024
1 parent 81577d5 commit dfdafec
Show file tree
Hide file tree
Showing 9 changed files with 234 additions and 21 deletions.
105 changes: 88 additions & 17 deletions go-backend/handlers/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type UpdateEntityRequest struct {
Name string `json:"name"`
Description string `json:"description"`
Type string `json:"type"`
CardPK *int `json:"card_pk"`
}

func (s *Handler) ExtractSaveCardEntities(userID int, card models.Card) error {
Expand Down Expand Up @@ -65,10 +66,10 @@ func (s *Handler) UpsertEntities(userID int, cardPK int, entities []models.Entit
if err == sql.ErrNoRows {
// Entity doesn't exist, insert it
err = s.DB.QueryRow(`
INSERT INTO entities (user_id, name, description, type, embedding)
VALUES ($1, $2, $3, $4, $5)
INSERT INTO entities (user_id, name, description, type, embedding, card_pk)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id
`, userID, entity.Name, entity.Description, entity.Type, entity.Embedding).Scan(&entityID)
`, userID, entity.Name, entity.Description, entity.Type, entity.Embedding, entity.CardPK).Scan(&entityID)
if err != nil {
log.Printf("error inserting entity: %v", err)
continue
Expand All @@ -82,9 +83,10 @@ func (s *Handler) UpsertEntities(userID int, cardPK int, entities []models.Entit
UPDATE entities
SET description = $1,
type = $2,
card_pk = $3,
updated_at = NOW()
WHERE id = $3
`, entity.Description, entity.Type, entityID)
WHERE id = $4
`, entity.Description, entity.Type, entity.CardPK, entityID)
if err != nil {
log.Printf("error updating entity: %v", err)
continue
Expand Down Expand Up @@ -146,15 +148,24 @@ func (s *Handler) GetEntitiesRoute(w http.ResponseWriter, r *http.Request) {
e.type,
e.created_at,
e.updated_at,
COUNT(DISTINCT ecj.card_pk) as card_count
e.card_pk,
COUNT(DISTINCT ecj.card_pk) as card_count,
c.id as linked_card_id,
c.card_id as linked_card_card_id,
c.title as linked_card_title,
c.user_id as linked_card_user_id,
c.parent_id as linked_card_parent_id,
c.created_at as linked_card_created_at,
c.updated_at as linked_card_updated_at
FROM
entities e
LEFT JOIN entity_card_junction ecj ON e.id = ecj.entity_id
LEFT JOIN cards c ON ecj.card_pk = c.id AND c.is_deleted = FALSE
LEFT JOIN cards c ON e.card_pk = c.id AND c.is_deleted = FALSE
WHERE
e.user_id = $1
GROUP BY
e.id, e.user_id, e.name, e.description, e.type, e.created_at, e.updated_at
e.id, e.user_id, e.name, e.description, e.type, e.created_at, e.updated_at, e.card_pk,
c.id, c.card_id, c.title, c.user_id, c.parent_id, c.created_at, c.updated_at
ORDER BY
e.name ASC
`
Expand All @@ -170,6 +181,11 @@ func (s *Handler) GetEntitiesRoute(w http.ResponseWriter, r *http.Request) {
var entities []models.Entity
for rows.Next() {
var entity models.Entity
var cardID sql.NullInt64
var cardCardID, cardTitle sql.NullString
var cardUserID, cardParentID sql.NullInt64
var cardCreatedAt, cardUpdatedAt sql.NullTime

err := rows.Scan(
&entity.ID,
&entity.UserID,
Expand All @@ -178,13 +194,36 @@ func (s *Handler) GetEntitiesRoute(w http.ResponseWriter, r *http.Request) {
&entity.Type,
&entity.CreatedAt,
&entity.UpdatedAt,
&entity.CardPK,
&entity.CardCount,
&cardID,
&cardCardID,
&cardTitle,
&cardUserID,
&cardParentID,
&cardCreatedAt,
&cardUpdatedAt,
)
if err != nil {
log.Printf("error scanning entity: %v", err)
http.Error(w, "Failed to scan entities", http.StatusInternalServerError)
return
}

// If we have a linked card, populate the card field
if cardID.Valid {
entity.Card = &models.PartialCard{
ID: int(cardID.Int64),
CardID: cardCardID.String,
Title: cardTitle.String,
UserID: int(cardUserID.Int64),
ParentID: int(cardParentID.Int64),
CreatedAt: cardCreatedAt.Time,
UpdatedAt: cardUpdatedAt.Time,
Tags: []models.Tag{}, // Empty tags array since we don't need them here
}
}

entities = append(entities, entity)
}

Expand All @@ -194,20 +233,22 @@ func (s *Handler) GetEntitiesRoute(w http.ResponseWriter, r *http.Request) {

func (s *Handler) QueryEntitiesForCard(userID int, cardPK int) ([]models.Entity, error) {
query := `
SELECT
e.id, e.user_id, e.name, e.description, e.type, e.created_at, e.updated_at
SELECT DISTINCT
e.id, e.user_id, e.name, e.description, e.type, e.created_at, e.updated_at, e.card_pk
FROM
entities e
JOIN
LEFT JOIN
entity_card_junction ecj ON e.id = ecj.entity_id
WHERE
ecj.card_pk = $1 AND e.user_id = $2`
e.user_id = $2
AND (ecj.card_pk = $1 OR e.card_pk = $1)`

rows, err := s.DB.Query(query, cardPK, userID)
if err != nil {
log.Printf("err %v", err)
return []models.Entity{}, err
}
defer rows.Close()

var entities []models.Entity
for rows.Next() {
Expand All @@ -220,6 +261,7 @@ func (s *Handler) QueryEntitiesForCard(userID int, cardPK int) ([]models.Entity,
&entity.Type,
&entity.CreatedAt,
&entity.UpdatedAt,
&entity.CardPK,
); err != nil {
log.Printf("err %v", err)
return entities, err
Expand Down Expand Up @@ -411,6 +453,26 @@ func (s *Handler) DeleteEntityRoute(w http.ResponseWriter, r *http.Request) {
})
}

func (s *Handler) validateCardAccess(userID int, cardPK int) error {
var exists bool
err := s.DB.QueryRow(`
SELECT EXISTS(
SELECT 1 FROM cards
WHERE id = $1 AND user_id = $2 AND is_deleted = FALSE
)
`, cardPK, userID).Scan(&exists)

if err != nil {
return fmt.Errorf("error checking card access: %w", err)
}

if !exists {
return fmt.Errorf("card not found or access denied")
}

return nil
}

func (s *Handler) UpdateEntity(userID int, entityID int, params UpdateEntityRequest) error {
// Start transaction
tx, err := s.DB.Begin()
Expand All @@ -435,6 +497,13 @@ func (s *Handler) UpdateEntity(userID int, entityID int, params UpdateEntityRequ
return fmt.Errorf("entity not found or does not belong to user")
}

// Validate card access if CardPK is provided
if params.CardPK != nil {
if err := s.validateCardAccess(userID, *params.CardPK); err != nil {
return fmt.Errorf("invalid card reference: %w", err)
}
}

// Check if name is unique for this user
var nameExists bool
err = tx.QueryRow(`
Expand Down Expand Up @@ -470,9 +539,10 @@ func (s *Handler) UpdateEntity(userID int, entityID int, params UpdateEntityRequ
SET name = $1,
description = $2,
type = $3,
card_pk = $4,
updated_at = NOW()
WHERE id = $4 AND user_id = $5`
queryArgs = []interface{}{params.Name, params.Description, params.Type, entityID, userID}
WHERE id = $5 AND user_id = $6`
queryArgs = []interface{}{params.Name, params.Description, params.Type, params.CardPK, entityID, userID}
} else {
// In normal mode, update with new embedding
embedding, err := llms.GenerateEntityEmbedding(s.Server.LLMClient, entity)
Expand All @@ -484,10 +554,11 @@ func (s *Handler) UpdateEntity(userID int, entityID int, params UpdateEntityRequ
SET name = $1,
description = $2,
type = $3,
embedding = $4,
card_pk = $4,
embedding = $5,
updated_at = NOW()
WHERE id = $5 AND user_id = $6`
queryArgs = []interface{}{params.Name, params.Description, params.Type, embedding, entityID, userID}
WHERE id = $6 AND user_id = $7`
queryArgs = []interface{}{params.Name, params.Description, params.Type, params.CardPK, embedding, entityID, userID}
}

// Update the entity
Expand Down
73 changes: 73 additions & 0 deletions go-backend/handlers/entity_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package handlers

import (
"go-backend/models"
"go-backend/tests"
"strings"
"testing"
Expand Down Expand Up @@ -215,3 +216,75 @@ func TestUpdateEntityNonExistent(t *testing.T) {
t.Error("Expected error when updating non-existent entity")
}
}

func TestUpdateEntityWithCardPK(t *testing.T) {
s := setup()
defer tests.Teardown()

// Create a test card
var cardID int
err := s.DB.QueryRow(`
INSERT INTO cards (user_id, title, body)
VALUES ($1, 'Test Card', 'Test Content')
RETURNING id
`, 1).Scan(&cardID)
if err != nil {
t.Fatalf("Failed to create test card: %v", err)
}

// Update entity with card_pk
params := UpdateEntityRequest{
Name: "Updated Entity",
Description: "Updated Description",
Type: "person",
CardPK: &cardID,
}

err = s.UpdateEntity(1, 1, params)
if err != nil {
t.Errorf("UpdateEntity failed: %v", err)
}

// Verify the update
var entity models.Entity
err = s.DB.QueryRow(`
SELECT id, user_id, name, description, type, card_pk
FROM entities
WHERE id = $1
`, 1).Scan(
&entity.ID,
&entity.UserID,
&entity.Name,
&entity.Description,
&entity.Type,
&entity.CardPK,
)
if err != nil {
t.Errorf("Failed to verify entity update: %v", err)
}
if *entity.CardPK != cardID {
t.Errorf("Expected card_pk to be %d, got %d", cardID, *entity.CardPK)
}
}

func TestUpdateEntityWithInvalidCardPK(t *testing.T) {
s := setup()
defer tests.Teardown()

// Try to update with non-existent card
invalidCardID := 99999
params := UpdateEntityRequest{
Name: "Updated Entity",
Description: "Updated Description",
Type: "person",
CardPK: &invalidCardID,
}

err := s.UpdateEntity(1, 1, params)
if err == nil {
t.Error("Expected error when updating with invalid card_pk")
}
if !strings.Contains(err.Error(), "card not found") {
t.Errorf("Expected 'card not found' error, got: %v", err)
}
}
2 changes: 2 additions & 0 deletions go-backend/models/entity.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ type Entity struct {
UpdatedAt time.Time `json:"updated_at"`
Embedding pgvector.Vector `json:"embedding"`
CardCount int `json:"card_count"`
CardPK *int `json:"card_pk"`
Card *PartialCard `json:"card,omitempty"`
}

type EntityCardJunction struct {
Expand Down
3 changes: 3 additions & 0 deletions go-backend/schema/0035-add-card-pk-to-entities.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
ALTER TABLE entities
ADD COLUMN card_pk INTEGER,
ADD CONSTRAINT fk_card_pk FOREIGN KEY (card_pk) REFERENCES cards(id);
1 change: 1 addition & 0 deletions zettelkasten-front/src/api/entities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ export interface UpdateEntityRequest {
name: string;
description: string;
type: string;
card_pk: number | null;
}

export function updateEntity(entityId: number, data: UpdateEntityRequest): Promise<void> {
Expand Down
Loading

0 comments on commit dfdafec

Please sign in to comment.