185 lines
5.7 KiB
Go
185 lines
5.7 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
|
|
"git.threesix.ai/jordan/persona-community-1/services/persona-api/internal/domain"
|
|
"git.threesix.ai/jordan/persona-community-1/services/persona-api/internal/port"
|
|
)
|
|
|
|
// Compile-time interface check.
|
|
var _ port.MediaRepository = (*MediaObjectRepository)(nil)
|
|
|
|
// mediaObjectRow maps to the media_objects table.
|
|
type mediaObjectRow struct {
|
|
ID string `db:"id"`
|
|
UserID string `db:"user_id"`
|
|
Path string `db:"path"`
|
|
Filename string `db:"filename"`
|
|
ContentType string `db:"content_type"`
|
|
Size int64 `db:"size"`
|
|
GenerationJobID string `db:"generation_job_id"`
|
|
DeletedAt *time.Time `db:"deleted_at"`
|
|
CreatedAt time.Time `db:"created_at"`
|
|
UpdatedAt time.Time `db:"updated_at"`
|
|
}
|
|
|
|
func (r *mediaObjectRow) toDomain() *domain.MediaObject {
|
|
return &domain.MediaObject{
|
|
ID: domain.MediaObjectID(r.ID),
|
|
UserID: domain.UserID(r.UserID),
|
|
Path: r.Path,
|
|
Filename: r.Filename,
|
|
ContentType: r.ContentType,
|
|
Size: r.Size,
|
|
GenerationJobID: r.GenerationJobID,
|
|
DeletedAt: r.DeletedAt,
|
|
CreatedAt: r.CreatedAt,
|
|
UpdatedAt: r.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
// MediaObjectRepository implements port.MediaRepository with PostgreSQL/CockroachDB.
|
|
type MediaObjectRepository struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
// NewMediaObjectRepository creates a new Postgres-backed media repository.
|
|
func NewMediaObjectRepository(db *sqlx.DB) *MediaObjectRepository {
|
|
return &MediaObjectRepository{db: db}
|
|
}
|
|
|
|
func (r *MediaObjectRepository) Create(ctx context.Context, obj *domain.MediaObject) error {
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO media_objects (id, user_id, path, filename, content_type, size, generation_job_id, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
|
`, string(obj.ID), string(obj.UserID), obj.Path, obj.Filename, obj.ContentType,
|
|
obj.Size, obj.GenerationJobID, obj.CreatedAt, obj.UpdatedAt)
|
|
if err != nil {
|
|
return fmt.Errorf("insert media object: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *MediaObjectRepository) Get(ctx context.Context, id domain.MediaObjectID) (*domain.MediaObject, error) {
|
|
var row mediaObjectRow
|
|
err := r.db.GetContext(ctx, &row, `
|
|
SELECT id, user_id, path, filename, content_type, size, generation_job_id, deleted_at, created_at, updated_at
|
|
FROM media_objects WHERE id = $1 AND deleted_at IS NULL
|
|
`, string(id))
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, domain.ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("get media object: %w", err)
|
|
}
|
|
return row.toDomain(), nil
|
|
}
|
|
|
|
func (r *MediaObjectRepository) ListByUser(ctx context.Context, userID domain.UserID, opts port.ListMediaOptions) ([]domain.MediaObject, int, error) {
|
|
limit := opts.Limit
|
|
if limit <= 0 {
|
|
limit = 50
|
|
}
|
|
|
|
// Count total matching records
|
|
countQuery := `SELECT COUNT(*) FROM media_objects WHERE user_id = $1 AND deleted_at IS NULL`
|
|
args := []any{string(userID)}
|
|
argIdx := 2
|
|
|
|
if opts.ContentTypePrefix != "" {
|
|
countQuery += fmt.Sprintf(` AND content_type LIKE $%d`, argIdx)
|
|
args = append(args, opts.ContentTypePrefix+"%")
|
|
argIdx++
|
|
}
|
|
|
|
var total int
|
|
if err := r.db.GetContext(ctx, &total, countQuery, args...); err != nil {
|
|
return nil, 0, fmt.Errorf("count media objects: %w", err)
|
|
}
|
|
|
|
// Fetch paginated results
|
|
query := `
|
|
SELECT id, user_id, path, filename, content_type, size, generation_job_id, deleted_at, created_at, updated_at
|
|
FROM media_objects
|
|
WHERE user_id = $1 AND deleted_at IS NULL`
|
|
|
|
fetchArgs := []any{string(userID)}
|
|
fetchIdx := 2
|
|
|
|
if opts.ContentTypePrefix != "" {
|
|
query += fmt.Sprintf(` AND content_type LIKE $%d`, fetchIdx)
|
|
fetchArgs = append(fetchArgs, opts.ContentTypePrefix+"%")
|
|
fetchIdx++
|
|
}
|
|
|
|
query += ` ORDER BY created_at DESC`
|
|
query += fmt.Sprintf(` LIMIT $%d OFFSET $%d`, fetchIdx, fetchIdx+1)
|
|
fetchArgs = append(fetchArgs, limit, opts.Offset)
|
|
|
|
var rows []mediaObjectRow
|
|
if err := r.db.SelectContext(ctx, &rows, query, fetchArgs...); err != nil {
|
|
return nil, 0, fmt.Errorf("list media objects: %w", err)
|
|
}
|
|
|
|
objects := make([]domain.MediaObject, len(rows))
|
|
for i := range rows {
|
|
objects[i] = *rows[i].toDomain()
|
|
}
|
|
return objects, total, nil
|
|
}
|
|
|
|
func (r *MediaObjectRepository) SoftDelete(ctx context.Context, id domain.MediaObjectID) error {
|
|
result, err := r.db.ExecContext(ctx, `
|
|
UPDATE media_objects SET deleted_at = NOW(), updated_at = NOW()
|
|
WHERE id = $1 AND deleted_at IS NULL
|
|
`, string(id))
|
|
if err != nil {
|
|
return fmt.Errorf("soft delete media object: %w", err)
|
|
}
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("soft delete rows affected: %w", err)
|
|
}
|
|
if rows == 0 {
|
|
return domain.ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *MediaObjectRepository) HardDelete(ctx context.Context, id domain.MediaObjectID) error {
|
|
result, err := r.db.ExecContext(ctx, `DELETE FROM media_objects WHERE id = $1`, string(id))
|
|
if err != nil {
|
|
return fmt.Errorf("hard delete media object: %w", err)
|
|
}
|
|
rows, err := result.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("hard delete rows affected: %w", err)
|
|
}
|
|
if rows == 0 {
|
|
return domain.ErrNotFound
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *MediaObjectRepository) GetByPath(ctx context.Context, path string) (*domain.MediaObject, error) {
|
|
var row mediaObjectRow
|
|
err := r.db.GetContext(ctx, &row, `
|
|
SELECT id, user_id, path, filename, content_type, size, generation_job_id, deleted_at, created_at, updated_at
|
|
FROM media_objects WHERE path = $1 AND deleted_at IS NULL
|
|
`, path)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, domain.ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("get media object by path: %w", err)
|
|
}
|
|
return row.toDomain(), nil
|
|
}
|