Skip to content

fix(etl): normalize empty playlist_contents on update (apps#14306 parity)#265

Open
raymondjacobson wants to merge 1 commit into
etl/parity-5e-developer-app-redirectfrom
etl/parity-empty-playlist-contents
Open

fix(etl): normalize empty playlist_contents on update (apps#14306 parity)#265
raymondjacobson wants to merge 1 commit into
etl/parity-5e-developer-app-redirectfrom
etl/parity-empty-playlist-contents

Conversation

@raymondjacobson
Copy link
Copy Markdown
Contributor

Summary

Picks up the backend behavior change from apps#14306 ("allow removing the last track from a playlist").

apps' bug

In Python, if not playlist_metadata.get("playlist_contents") is true for an empty list. So when the SDK sent {"playlist_contents": []} (user removing the last track from a playlist), apps' populate_playlist_record_metadata silently skipped the JSONB update. The UI showed the track removed (optimistic update), but a reload restored it because the persisted state was unchanged.

apps' fix swaps if not ... for if ... is None, so the empty list goes through process_playlist_contents like any other value.

go-openaudio status

go-openaudio's mergePlaylistFromMetadata already uses a _, ok := key-exists check, not a truthiness check — so the underlying bug does not reproduce here. Verified via TestPlaylistUpdate_EmptyArrayMarksAllTracksRemoved: the playlist_tracks junction correctly marks all rows is_removed=true.

Adjacent gap found and fixed

apps' process_playlist_contents always normalizes its output to the dict form {"track_ids": [...]} regardless of input shape. go-openaudio was persisting whatever shape the SDK sent (bare [] or legacy dict), which downstream readers wouldn't expect.

This PR adds normalizePlaylistContentsJSON:

{}                                              → {"track_ids":[]}
{"playlist_contents": null}                     → {"track_ids":[]}
{"playlist_contents": []}                       → {"track_ids":[]}
{"playlist_contents": {"track_ids": []}}        → {"track_ids":[]}
{"playlist_contents": [{...}]}                  → {"track_ids":[{...}]}
{"playlist_contents": {"track_ids": [{...}]}}   → {"track_ids":[{...}]}

Wired into both create (metadataPlaylistContentsJSON) and update (mergePlaylistFromMetadata).

Stack context

Stacked on #252 (5E — oauth_redirect_uris). Follow-up after the main parity stack.

Test plan

  • TestNormalizePlaylistContentsJSON — 6 sub-cases covering all input shapes.
  • TestPlaylistUpdate_EmptyArrayMarksAllTracksRemoved — both bare [] (new SDK) and legacy {track_ids:[]} paths correctly clear the junction.
  • TestPlaylistUpdate_EmptyArrayWritesJSONBColumn — asserts the persisted JSONB is a JSON object with a track_ids key (apps' canonical form).
  • All previously-passing playlist tests still pass (no regressions).

🤖 Generated with Claude Code

@raymondjacobson raymondjacobson force-pushed the etl/parity-5e-developer-app-redirect branch from f8ec063 to bbb8053 Compare May 13, 2026 23:39
@raymondjacobson raymondjacobson force-pushed the etl/parity-empty-playlist-contents branch from 77b7256 to 5ba7248 Compare May 13, 2026 23:40
@raymondjacobson raymondjacobson force-pushed the etl/parity-5e-developer-app-redirect branch from bbb8053 to fc0aca1 Compare May 14, 2026 00:00
@raymondjacobson raymondjacobson force-pushed the etl/parity-empty-playlist-contents branch from 5ba7248 to 3b51123 Compare May 14, 2026 00:00
@raymondjacobson raymondjacobson force-pushed the etl/parity-5e-developer-app-redirect branch from fc0aca1 to 21f3bcb Compare May 14, 2026 00:44
@raymondjacobson raymondjacobson force-pushed the etl/parity-empty-playlist-contents branch from 3b51123 to cf1ebad Compare May 14, 2026 00:44
@raymondjacobson raymondjacobson force-pushed the etl/parity-5e-developer-app-redirect branch from 21f3bcb to f9643da Compare May 14, 2026 00:57
@raymondjacobson raymondjacobson force-pushed the etl/parity-empty-playlist-contents branch from cf1ebad to 22d2137 Compare May 14, 2026 00:57
@raymondjacobson raymondjacobson force-pushed the etl/parity-5e-developer-app-redirect branch from f9643da to 0a12b34 Compare May 14, 2026 01:52
@raymondjacobson raymondjacobson force-pushed the etl/parity-empty-playlist-contents branch from 22d2137 to 996a8b7 Compare May 14, 2026 01:52
@raymondjacobson raymondjacobson force-pushed the etl/parity-5e-developer-app-redirect branch from 0a12b34 to 50ba103 Compare May 14, 2026 02:05
@raymondjacobson raymondjacobson force-pushed the etl/parity-empty-playlist-contents branch from 996a8b7 to fab81fa Compare May 14, 2026 02:06
…ity)

apps#14306 fixed a bug where Python's `if not playlist_metadata.get("playlist_contents")`
truthiness check treated the SDK-sent empty list `[]` as field-omitted and
silently skipped the JSONB update — so removing the last track from a
playlist appeared to succeed in the UI but reappeared on reload.

go-openaudio's mergePlaylistFromMetadata uses `_, ok :=` (key-exists)
rather than truthiness, so the underlying skip-on-empty bug does not
reproduce. Verified via TestPlaylistUpdate_EmptyArrayMarksAllTracksRemoved
(the junction table correctly clears, and the JSONB column gets updated).

However, an adjacent parity gap was discovered: go-openaudio was persisting
whatever shape the SDK sent (bare `[]` or legacy `{"track_ids":[]}`) into
playlists.playlist_contents, while apps' process_playlist_contents always
normalizes to the dict form `{"track_ids":[...]}`. Downstream API readers
expect one shape.

This commit:
- Introduces normalizePlaylistContentsJSON that accepts bare array, legacy
  dict, explicit null, and missing key, and always emits the canonical
  `{"track_ids":[...]}` form.
- Switches both create-path (metadataPlaylistContentsJSON) and update-path
  (mergePlaylistFromMetadata) to use it.
- Empty list now lands as `{"track_ids":[]}` in playlists.playlist_contents,
  matching apps.

Tests:
- TestNormalizePlaylistContentsJSON covers all input shapes.
- TestPlaylistUpdate_EmptyArrayMarksAllTracksRemoved exercises both bare
  `[]` (new SDK) and legacy `{track_ids:[]}` empty paths.
- TestPlaylistUpdate_EmptyArrayWritesJSONBColumn asserts the JSONB column
  ends up as a JSON object with a track_ids key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@raymondjacobson raymondjacobson force-pushed the etl/parity-5e-developer-app-redirect branch from 50ba103 to f696056 Compare May 15, 2026 16:52
@raymondjacobson raymondjacobson force-pushed the etl/parity-empty-playlist-contents branch from fab81fa to ed53a6b Compare May 15, 2026 16:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant