From d7aebafa5dbeab85c88b29772a8a7c9f297c4a8a Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Mon, 4 May 2026 08:21:18 -0400 Subject: [PATCH] feat(server/m7-352): AssertEditable + ErrSystemPlaylistReadonly on playlists service --- internal/playlists/service.go | 35 +++++++++++++++++++++++++++++++---- 1 file changed, 31 insertions(+), 4 deletions(-) diff --git a/internal/playlists/service.go b/internal/playlists/service.go index b08300f0..32c61676 100644 --- a/internal/playlists/service.go +++ b/internal/playlists/service.go @@ -30,10 +30,11 @@ import ( // Typed errors. The API layer maps each to its wire status code; tests // use errors.Is to assert the right path was taken. var ( - ErrNotFound = errors.New("playlists: playlist not found") - ErrForbidden = errors.New("playlists: forbidden") - ErrInvalidInput = errors.New("playlists: invalid input") - ErrTrackNotFound = errors.New("playlists: track not found") + ErrNotFound = errors.New("playlists: playlist not found") + ErrForbidden = errors.New("playlists: forbidden") + ErrInvalidInput = errors.New("playlists: invalid input") + ErrTrackNotFound = errors.New("playlists: track not found") + ErrSystemPlaylistReadonly = errors.New("playlists: system playlist is read-only") ) // Service wires the sqlc queries + on-disk cover-art directory together. @@ -292,6 +293,32 @@ func (s *Service) Delete(ctx context.Context, callerID, playlistID pgtype.UUID) return nil } +// AssertEditable returns nil if the playlist exists, the caller owns it, +// AND the playlist is user-created (kind='user'). Returns: +// - ErrNotFound — playlist doesn't exist +// - ErrForbidden — caller is not the owner +// - ErrSystemPlaylistReadonly — playlist exists but kind='system' +// +// Edit handlers call this first; on error, the API layer maps the typed +// error to the appropriate status code. +func (s *Service) AssertEditable(ctx context.Context, callerID, playlistID pgtype.UUID) error { + q := dbq.New(s.pool) + p, err := q.GetPlaylist(ctx, playlistID) + if err != nil { + if errors.Is(err, pgx.ErrNoRows) { + return ErrNotFound + } + return fmt.Errorf("playlists: load for editability check: %w", err) + } + if !pgtypeUUIDEqual(p.UserID, callerID) { + return ErrForbidden + } + if p.Kind == "system" { + return ErrSystemPlaylistReadonly + } + return nil +} + // AppendTracks adds the given track ids at the end of the playlist, // preserving order. Snapshot fields (title, artist, album, duration_sec) // are populated from the tracks/albums/artists join at insert time.