Files
owen bff58fd842 Populate Repository.Owner in list/get store queries
ListReposVisible, GetRepositoryByName, and GetRepositoryByID left
Repository.Owner as nil because they only selected repository columns.
LEFT JOIN users in all three and populate Owner, so the API (and the
web UI's repositories table) can show the owner's username.
2026-06-28 18:42:31 -05:00

166 lines
5.8 KiB
Go

package store
import (
"context"
"database/sql"
"fmt"
"urapt/shared/models"
)
// CreateRepository inserts a new repository owned by userID.
func (s *Store) CreateRepository(ctx context.Context, name, ownerUserID string, visibility models.Visibility, description string) (*models.Repository, error) {
id := newID()
now := s.now()
_, err := s.exec(ctx, `INSERT INTO repositories (id, name, owner_user_id, visibility, description, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?)`, id, name, ownerUserID, string(visibility), description, now, now)
if err != nil {
return nil, fmt.Errorf("insert repository: %w", err)
}
return &models.Repository{
ID: id, Name: name, OwnerUserID: ownerUserID, Visibility: visibility,
Description: description, CreatedAt: now, UpdatedAt: now,
}, nil
}
// ListReposVisible returns repositories visible to userID: owned, public, or
// where the user is a member. Each repository's Owner is populated.
func (s *Store) ListReposVisible(ctx context.Context, userID string) ([]*models.Repository, error) {
rows, err := s.db.QueryContext(ctx, `
SELECT DISTINCT r.id, r.name, r.owner_user_id, r.visibility, r.description, r.created_at, r.updated_at,
u.id, u.username, u.is_admin, u.created_at, u.updated_at
FROM repositories r
LEFT JOIN users u ON u.id = r.owner_user_id
WHERE r.owner_user_id = ?
OR r.visibility = 'public'
OR EXISTS (SELECT 1 FROM repository_members m WHERE m.repository_id = r.id AND m.user_id = ?)
ORDER BY r.name`, userID, userID)
if err != nil {
return nil, fmt.Errorf("list repos: %w", err)
}
defer rows.Close()
var out []*models.Repository
for rows.Next() {
r := &models.Repository{}
var (
ownerID sql.NullString
ownerUser sql.NullString
ownerAdmin sql.NullBool
ownerCreated sql.NullString
ownerUpdated sql.NullString
)
if err := rows.Scan(
&r.ID, &r.Name, &r.OwnerUserID, &r.Visibility, &r.Description, &r.CreatedAt, &r.UpdatedAt,
&ownerID, &ownerUser, &ownerAdmin, &ownerCreated, &ownerUpdated,
); err != nil {
return nil, err
}
if ownerID.Valid {
r.Owner = &models.User{
ID: ownerID.String, Username: ownerUser.String, IsAdmin: ownerAdmin.Bool,
CreatedAt: ownerCreated.String, UpdatedAt: ownerUpdated.String,
}
}
out = append(out, r)
}
return out, rows.Err()
}
// UpdateRepository mutates a repository's name, visibility, and description.
// Empty strings leave the field unchanged.
func (s *Store) UpdateRepository(ctx context.Context, id, name string, visibility *models.Visibility, description *string) error {
now := s.now()
if name != "" {
if _, err := s.exec(ctx, `UPDATE repositories SET name = ?, updated_at = ? WHERE id = ?`, name, now, id); err != nil {
return fmt.Errorf("update repo name: %w", err)
}
}
if visibility != nil {
if _, err := s.exec(ctx, `UPDATE repositories SET visibility = ?, updated_at = ? WHERE id = ?`, string(*visibility), now, id); err != nil {
return fmt.Errorf("update repo visibility: %w", err)
}
}
if description != nil {
if _, err := s.exec(ctx, `UPDATE repositories SET description = ?, updated_at = ? WHERE id = ?`, *description, now, id); err != nil {
return fmt.Errorf("update repo description: %w", err)
}
}
return nil
}
// DeleteRepository deletes a repository and all its child rows (cascade).
// The caller must have already handled blob ref-count cleanup.
func (s *Store) DeleteRepository(ctx context.Context, id string) error {
if _, err := s.exec(ctx, `DELETE FROM repositories WHERE id = ?`, id); err != nil {
return fmt.Errorf("delete repository: %w", err)
}
return nil
}
// GetRepositoryByName returns a repository by its unique name, with Owner populated.
func (s *Store) GetRepositoryByName(ctx context.Context, name string) (*models.Repository, error) {
row := s.db.QueryRowContext(ctx,
`SELECT r.id, r.name, r.owner_user_id, r.visibility, r.description, r.created_at, r.updated_at,
u.id, u.username, u.is_admin, u.created_at, u.updated_at
FROM repositories r
LEFT JOIN users u ON u.id = r.owner_user_id
WHERE r.name = ?`, name)
return scanRepo(row)
}
// GetRepositoryByID returns a repository by id, with Owner populated.
func (s *Store) GetRepositoryByID(ctx context.Context, id string) (*models.Repository, error) {
row := s.db.QueryRowContext(ctx,
`SELECT r.id, r.name, r.owner_user_id, r.visibility, r.description, r.created_at, r.updated_at,
u.id, u.username, u.is_admin, u.created_at, u.updated_at
FROM repositories r
LEFT JOIN users u ON u.id = r.owner_user_id
WHERE r.id = ?`, id)
return scanRepo(row)
}
func scanRepo(row *sql.Row) (*models.Repository, error) {
r := &models.Repository{}
var (
ownerID sql.NullString
ownerUser sql.NullString
ownerAdmin sql.NullBool
ownerCreated sql.NullString
ownerUpdated sql.NullString
)
err := row.Scan(
&r.ID, &r.Name, &r.OwnerUserID, &r.Visibility, &r.Description, &r.CreatedAt, &r.UpdatedAt,
&ownerID, &ownerUser, &ownerAdmin, &ownerCreated, &ownerUpdated,
)
if isErrNoRows(err) {
return nil, ErrNotFound
}
if err != nil {
return nil, err
}
if ownerID.Valid {
r.Owner = &models.User{
ID: ownerID.String, Username: ownerUser.String, IsAdmin: ownerAdmin.Bool,
CreatedAt: ownerCreated.String, UpdatedAt: ownerUpdated.String,
}
}
return r, nil
}
// GetMemberAccess returns the access level granted to userID on repoID, or
// ("", false) if the user is not an explicit member.
func (s *Store) GetMemberAccess(ctx context.Context, repoID, userID string) (models.Access, bool, error) {
row := s.db.QueryRowContext(ctx,
`SELECT access FROM repository_members WHERE repository_id = ? AND user_id = ?`,
repoID, userID)
var access string
err := row.Scan(&access)
if isErrNoRows(err) {
return "", false, nil
}
if err != nil {
return "", false, fmt.Errorf("get member access: %w", err)
}
return models.Access(access), true, nil
}