Backend: - Add file_handlers.go: presigned upload/download for item attachments - Add item_files.go: item file and thumbnail DB operations - Add migration 011: item_files table and thumbnail_key column - Update items/projects/relationships DB with extended field support - Update routes: React SPA serving from web/dist, file upload endpoints - Update auth handlers and middleware for cookie + bearer token auth - Remove Go HTML templates (replaced by React SPA) - Update storage client for presigned URL generation Frontend: - Add TagInput component for tag/keyword entry - Add SVG assets for Silo branding and UI icons - Update API client and types for file uploads, auth, extended fields - Update AuthContext for session-based auth flow - Update LoginPage, ProjectsPage, SchemasPage, SettingsPage - Fix tsconfig.node.json Deployment: - Update config.prod.yaml: single-binary SPA layout at /opt/silo - Update silod.service: ReadOnlyPaths for /opt/silo - Add scripts/deploy.sh: build, package, ship, migrate, start - Update docker-compose.yaml and Dockerfile - Add frontend-spec.md design document
92 lines
2.5 KiB
Go
92 lines
2.5 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// ItemFile represents a file attachment on an item.
|
|
type ItemFile struct {
|
|
ID string
|
|
ItemID string
|
|
Filename string
|
|
ContentType string
|
|
Size int64
|
|
ObjectKey string
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// ItemFileRepository provides item_files database operations.
|
|
type ItemFileRepository struct {
|
|
db *DB
|
|
}
|
|
|
|
// NewItemFileRepository creates a new item file repository.
|
|
func NewItemFileRepository(db *DB) *ItemFileRepository {
|
|
return &ItemFileRepository{db: db}
|
|
}
|
|
|
|
// Create inserts a new item file record.
|
|
func (r *ItemFileRepository) Create(ctx context.Context, f *ItemFile) error {
|
|
err := r.db.pool.QueryRow(ctx,
|
|
`INSERT INTO item_files (item_id, filename, content_type, size, object_key)
|
|
VALUES ($1, $2, $3, $4, $5)
|
|
RETURNING id, created_at`,
|
|
f.ItemID, f.Filename, f.ContentType, f.Size, f.ObjectKey,
|
|
).Scan(&f.ID, &f.CreatedAt)
|
|
if err != nil {
|
|
return fmt.Errorf("creating item file: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ListByItem returns all file attachments for an item.
|
|
func (r *ItemFileRepository) ListByItem(ctx context.Context, itemID string) ([]*ItemFile, error) {
|
|
rows, err := r.db.pool.Query(ctx,
|
|
`SELECT id, item_id, filename, content_type, size, object_key, created_at
|
|
FROM item_files WHERE item_id = $1 ORDER BY created_at`,
|
|
itemID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("listing item files: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var files []*ItemFile
|
|
for rows.Next() {
|
|
f := &ItemFile{}
|
|
if err := rows.Scan(&f.ID, &f.ItemID, &f.Filename, &f.ContentType, &f.Size, &f.ObjectKey, &f.CreatedAt); err != nil {
|
|
return nil, fmt.Errorf("scanning item file: %w", err)
|
|
}
|
|
files = append(files, f)
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
// Get returns a single item file by ID.
|
|
func (r *ItemFileRepository) Get(ctx context.Context, id string) (*ItemFile, error) {
|
|
f := &ItemFile{}
|
|
err := r.db.pool.QueryRow(ctx,
|
|
`SELECT id, item_id, filename, content_type, size, object_key, created_at
|
|
FROM item_files WHERE id = $1`,
|
|
id,
|
|
).Scan(&f.ID, &f.ItemID, &f.Filename, &f.ContentType, &f.Size, &f.ObjectKey, &f.CreatedAt)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting item file: %w", err)
|
|
}
|
|
return f, nil
|
|
}
|
|
|
|
// Delete removes an item file record.
|
|
func (r *ItemFileRepository) Delete(ctx context.Context, id string) error {
|
|
tag, err := r.db.pool.Exec(ctx, `DELETE FROM item_files WHERE id = $1`, id)
|
|
if err != nil {
|
|
return fmt.Errorf("deleting item file: %w", err)
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
return fmt.Errorf("item file not found")
|
|
}
|
|
return nil
|
|
}
|