Problem
apps/backend/services/library.service.ts has three near-sibling functions that all do the same shape — "given some caller context (rotation id / legacy library id / library id), return the resolved Discogs release id, or NULL when the chain is incomplete":
| Function |
Caller |
Bridge |
getDiscogsReleaseIdByRotationId |
/library/rotation/:rotation_id/tracks (BS#944) |
rotation → library_identity (join via rotation.album_id) |
getDiscogsReleaseIdByLegacyId |
/proxy/library/:libraryId/tracks (BS#836) |
legacy_release_id → library_identity (join via library.id) |
(inline in proxy.controller.getAlbumMetadata) |
iOS metadata path |
library_id → library_identity directly |
Each one:
- Joins (or selects) the same
library_identity table
- Returns
null when the FK chain is broken at one of three documented points (no album_id / no library_identity row / no discogs_release_id)
- Has an identical docstring block listing the same null-fallback conditions
When a new data source for discogs_release_id lands (the upcoming rotation.discogs_release_id column from the tubafrenzy parity work — see the parent issue for that work), every site that resolves a Discogs release id needs the new fallback. Today that means editing all three sites, with all the risk of inconsistency that implies.
End state
A single resolveDiscogsReleaseId service function in apps/backend/services/library.service.ts:
type DiscogsReleaseIdSource =
| { rotationId: number }
| { legacyLibraryId: number }
| { libraryId: number };
export async function resolveDiscogsReleaseId(source: DiscogsReleaseIdSource): Promise<number | null>;
- Single canonical fallback chain inside the function (rotation column → library_identity → library.canonical_entity_id parse → null). Future sources land in one place.
- One docstring describing the gates, not three.
- Each existing callsite becomes
resolveDiscogsReleaseId({ rotationId }) / ({ legacyLibraryId }) / ({ libraryId }).
- No change to any HTTP endpoint surface — this is purely a service-layer refactor.
Acceptance
Why now
Surfaced during the endpoint-discipline review on 2026-05-21 while scoping the tubafrenzy parity work for rotation.discogs_release_id. That work adds another source-of-truth column; consolidating the three bridge functions first makes the parity PR clean (one line change, not three).
Related
- The parent tubafrenzy parity work (file separately)
- BS#944 —
/library/rotation/:rotation_id/tracks
- BS#836 —
/proxy/library/:libraryId/tracks
- BS#802 —
library_identity substrate (the read target this function gates)
Problem
apps/backend/services/library.service.tshas three near-sibling functions that all do the same shape — "given some caller context (rotation id / legacy library id / library id), return the resolved Discogs release id, or NULL when the chain is incomplete":getDiscogsReleaseIdByRotationId/library/rotation/:rotation_id/tracks(BS#944)getDiscogsReleaseIdByLegacyId/proxy/library/:libraryId/tracks(BS#836)proxy.controller.getAlbumMetadata)Each one:
library_identitytablenullwhen the FK chain is broken at one of three documented points (no album_id / no library_identity row / no discogs_release_id)When a new data source for
discogs_release_idlands (the upcomingrotation.discogs_release_idcolumn from the tubafrenzy parity work — see the parent issue for that work), every site that resolves a Discogs release id needs the new fallback. Today that means editing all three sites, with all the risk of inconsistency that implies.End state
A single
resolveDiscogsReleaseIdservice function inapps/backend/services/library.service.ts:resolveDiscogsReleaseId({ rotationId })/({ legacyLibraryId })/({ libraryId }).Acceptance
resolveDiscogsReleaseIdexists with the discriminated-source parameter shape above./library/rotation/:rotation_id/tracksand/proxy/library/:libraryId/tracksstill pass unchanged.Why now
Surfaced during the endpoint-discipline review on 2026-05-21 while scoping the tubafrenzy parity work for
rotation.discogs_release_id. That work adds another source-of-truth column; consolidating the three bridge functions first makes the parity PR clean (one line change, not three).Related
/library/rotation/:rotation_id/tracks/proxy/library/:libraryId/trackslibrary_identitysubstrate (the read target this function gates)