package item import ( "context" "database/sql" "fmt" "time" "github.com/jackc/pgx/v5/pgtype" ) type Row struct { ID int64 PubID string Title *string Description *string LinkURL *string ItemType string // 'image', 'video', 'quote', 'embed' EmbedProvider *string EmbedVideoID *string EmbedHTML *string CreatedAt time.Time DeletedAt *time.Time } type CreateParams struct { Title *string Description *string LinkURL *string ItemType string EmbedProvider *string EmbedVideoID *string EmbedHTML *string } // QCreate creates a new item. func QCreate(ctx context.Context, db *sql.DB, p CreateParams) (Row, error) { query := ` INSERT INTO item (title, description, link_url, item_type, embed_provider, embed_video_id, embed_html) VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id, pub_id, title, description, link_url, item_type, embed_provider, embed_video_id, embed_html, created_at, deleted_at ` var row Row var pubID pgtype.UUID err := db.QueryRowContext(ctx, query, p.Title, p.Description, p.LinkURL, p.ItemType, p.EmbedProvider, p.EmbedVideoID, p.EmbedHTML, ).Scan( &row.ID, &pubID, &row.Title, &row.Description, &row.LinkURL, &row.ItemType, &row.EmbedProvider, &row.EmbedVideoID, &row.EmbedHTML, &row.CreatedAt, &row.DeletedAt, ) if err == nil { row.PubID = formatUUID(pubID) } return row, err } // QFindByPubID finds an item by its public ID. func QFindByPubID(ctx context.Context, db *sql.DB, pubID string) (*Row, error) { query := ` SELECT id, pub_id, title, description, link_url, item_type, embed_provider, embed_video_id, embed_html, created_at, deleted_at FROM item WHERE pub_id = $1 ` var row Row var pubUUID pgtype.UUID err := db.QueryRowContext(ctx, query, pubID).Scan( &row.ID, &pubUUID, &row.Title, &row.Description, &row.LinkURL, &row.ItemType, &row.EmbedProvider, &row.EmbedVideoID, &row.EmbedHTML, &row.CreatedAt, &row.DeletedAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } row.PubID = formatUUID(pubUUID) return &row, nil } // QFindByID finds an item by its internal ID. func QFindByID(ctx context.Context, db *sql.DB, id int64) (*Row, error) { query := ` SELECT id, pub_id, title, description, link_url, item_type, embed_provider, embed_video_id, embed_html, created_at, deleted_at FROM item WHERE id = $1 ` var row Row var pubUUID pgtype.UUID err := db.QueryRowContext(ctx, query, id).Scan( &row.ID, &pubUUID, &row.Title, &row.Description, &row.LinkURL, &row.ItemType, &row.EmbedProvider, &row.EmbedVideoID, &row.EmbedHTML, &row.CreatedAt, &row.DeletedAt, ) if err == sql.ErrNoRows { return nil, nil } if err != nil { return nil, err } row.PubID = formatUUID(pubUUID) return &row, nil } // QList returns all non-deleted items, newest first. func QList(ctx context.Context, db *sql.DB) ([]Row, error) { query := ` SELECT id, pub_id, title, description, link_url, item_type, embed_provider, embed_video_id, embed_html, created_at, deleted_at FROM item WHERE deleted_at IS NULL ORDER BY created_at DESC ` rows, err := db.QueryContext(ctx, query) if err != nil { return nil, err } defer rows.Close() var items []Row for rows.Next() { var row Row var pubUUID pgtype.UUID if err := rows.Scan( &row.ID, &pubUUID, &row.Title, &row.Description, &row.LinkURL, &row.ItemType, &row.EmbedProvider, &row.EmbedVideoID, &row.EmbedHTML, &row.CreatedAt, &row.DeletedAt, ); err != nil { return nil, err } row.PubID = formatUUID(pubUUID) items = append(items, row) } return items, rows.Err() } // QListByTag returns all non-deleted items with a specific tag, newest first. func QListByTag(ctx context.Context, db *sql.DB, tagName string) ([]Row, error) { query := ` SELECT i.id, i.pub_id, i.title, i.description, i.link_url, i.item_type, i.embed_provider, i.embed_video_id, i.embed_html, i.created_at, i.deleted_at FROM item i JOIN item_tag it ON i.id = it.item_id JOIN tag t ON it.tag_id = t.id WHERE i.deleted_at IS NULL AND t.name = $1 ORDER BY i.created_at DESC ` rows, err := db.QueryContext(ctx, query, tagName) if err != nil { return nil, err } defer rows.Close() var items []Row for rows.Next() { var row Row var pubUUID pgtype.UUID if err := rows.Scan( &row.ID, &pubUUID, &row.Title, &row.Description, &row.LinkURL, &row.ItemType, &row.EmbedProvider, &row.EmbedVideoID, &row.EmbedHTML, &row.CreatedAt, &row.DeletedAt, ); err != nil { return nil, err } row.PubID = formatUUID(pubUUID) items = append(items, row) } return items, rows.Err() } type UpdateParams struct { Title *string Description *string LinkURL *string } // QUpdate updates an item's editable fields. func QUpdate(ctx context.Context, db *sql.DB, id int64, p UpdateParams) error { query := ` UPDATE item SET title = $2, description = $3, link_url = $4 WHERE id = $1 ` _, err := db.ExecContext(ctx, query, id, p.Title, p.Description, p.LinkURL) return err } // QUpdateType updates an item's type. func QUpdateType(ctx context.Context, db *sql.DB, id int64, itemType string) error { query := `UPDATE item SET item_type = $2 WHERE id = $1` _, err := db.ExecContext(ctx, query, id, itemType) return err } // QSoftDelete soft deletes an item. func QSoftDelete(ctx context.Context, db *sql.DB, id int64) error { query := `UPDATE item SET deleted_at = NOW() WHERE id = $1` _, err := db.ExecContext(ctx, query, id) return err } // QRestore restores a soft-deleted item. func QRestore(ctx context.Context, db *sql.DB, id int64) error { query := `UPDATE item SET deleted_at = NULL WHERE id = $1` _, err := db.ExecContext(ctx, query, id) return err } func formatUUID(u pgtype.UUID) string { if !u.Valid { return "" } b := u.Bytes return fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:16]) }