From 6968b73dadc222961c2c770ea755232c90be52f0 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Mon, 28 Apr 2025 17:01:41 -0700 Subject: [PATCH 1/2] [API-95] Add resolve endpoint, improve permalink matching --- api/dbv1/get_playlist_ids_by_permalink.sql.go | 48 ++++++++ api/dbv1/get_user_for_handle.sql.go | 28 +++++ .../queries/get_playlist_ids_by_permalink.sql | 10 ++ api/dbv1/queries/get_user_for_handle.sql | 7 ++ api/fixture_test.go | 24 ++++ api/resolve_middleware.go | 4 + api/server.go | 11 +- api/server_test.go | 3 +- api/testdata/playlist_fixtures.csv | 2 + api/testdata/playlist_routes_fixtures.csv | 3 + api/testdata/track_routes_fixtures.csv | 4 +- api/testdata/user_fixtures.csv | 2 + api/v1_playlists.go | 28 +++++ api/v1_playlists_test.go | 25 +++- api/v1_resolve.go | 109 ++++++++++++++++++ api/v1_resolve_test.go | 45 ++++++++ api/v1_tracks.go | 10 +- api/v1_tracks_test.go | 14 +++ api/v1_user.go | 2 +- api/v1_users_handle.go | 30 +++++ api/v1_users_handle_test.go | 21 ++++ api/v1_users_supporters.go | 2 +- api/v1_users_supporting.go | 2 +- 23 files changed, 412 insertions(+), 22 deletions(-) create mode 100644 api/dbv1/get_playlist_ids_by_permalink.sql.go create mode 100644 api/dbv1/get_user_for_handle.sql.go create mode 100644 api/dbv1/queries/get_playlist_ids_by_permalink.sql create mode 100644 api/dbv1/queries/get_user_for_handle.sql create mode 100644 api/testdata/playlist_routes_fixtures.csv create mode 100644 api/v1_resolve.go create mode 100644 api/v1_resolve_test.go create mode 100644 api/v1_users_handle.go create mode 100644 api/v1_users_handle_test.go diff --git a/api/dbv1/get_playlist_ids_by_permalink.sql.go b/api/dbv1/get_playlist_ids_by_permalink.sql.go new file mode 100644 index 00000000..1323922c --- /dev/null +++ b/api/dbv1/get_playlist_ids_by_permalink.sql.go @@ -0,0 +1,48 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: get_playlist_ids_by_permalink.sql + +package dbv1 + +import ( + "context" +) + +const getPlaylistIdsByPermalink = `-- name: GetPlaylistIdsByPermalink :many +SELECT playlist_id +FROM playlist_routes +JOIN users ON users.user_id = playlist_routes.owner_id +WHERE handle_lc = ANY($1::text[]) + AND slug = ANY($2::text[]) + AND ( + CONCAT(handle_lc, '/playlist/', slug) = ANY($3::text[]) -- in case of conflicts across users + OR CONCAT(handle_lc, '/album/', slug) = ANY($3::text[]) -- in case of conflicts across users + ) +` + +type GetPlaylistIdsByPermalinkParams struct { + Handles []string `json:"handles"` + Slugs []string `json:"slugs"` + Permalinks []string `json:"permalinks"` +} + +func (q *Queries) GetPlaylistIdsByPermalink(ctx context.Context, arg GetPlaylistIdsByPermalinkParams) ([]int32, error) { + rows, err := q.db.Query(ctx, getPlaylistIdsByPermalink, arg.Handles, arg.Slugs, arg.Permalinks) + if err != nil { + return nil, err + } + defer rows.Close() + var items []int32 + for rows.Next() { + var playlist_id int32 + if err := rows.Scan(&playlist_id); err != nil { + return nil, err + } + items = append(items, playlist_id) + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/api/dbv1/get_user_for_handle.sql.go b/api/dbv1/get_user_for_handle.sql.go new file mode 100644 index 00000000..4fb12b23 --- /dev/null +++ b/api/dbv1/get_user_for_handle.sql.go @@ -0,0 +1,28 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.28.0 +// source: get_user_for_handle.sql + +package dbv1 + +import ( + "context" + + "bridgerton.audius.co/trashid" +) + +const getUserForHandle = `-- name: GetUserForHandle :one +SELECT user_id FROM users +WHERE + handle_lc = lower($1) + AND is_current = true +ORDER BY created_at ASC +LIMIT 1 +` + +func (q *Queries) GetUserForHandle(ctx context.Context, handle string) (trashid.HashId, error) { + row := q.db.QueryRow(ctx, getUserForHandle, handle) + var user_id trashid.HashId + err := row.Scan(&user_id) + return user_id, err +} diff --git a/api/dbv1/queries/get_playlist_ids_by_permalink.sql b/api/dbv1/queries/get_playlist_ids_by_permalink.sql new file mode 100644 index 00000000..1425a824 --- /dev/null +++ b/api/dbv1/queries/get_playlist_ids_by_permalink.sql @@ -0,0 +1,10 @@ +-- name: GetPlaylistIdsByPermalink :many +SELECT playlist_id +FROM playlist_routes +JOIN users ON users.user_id = playlist_routes.owner_id +WHERE handle_lc = ANY(@handles::text[]) + AND slug = ANY(@slugs::text[]) + AND ( + CONCAT(handle_lc, '/playlist/', slug) = ANY(@permalinks::text[]) -- in case of conflicts across users + OR CONCAT(handle_lc, '/album/', slug) = ANY(@permalinks::text[]) -- in case of conflicts across users + ); diff --git a/api/dbv1/queries/get_user_for_handle.sql b/api/dbv1/queries/get_user_for_handle.sql new file mode 100644 index 00000000..4cfd2e5a --- /dev/null +++ b/api/dbv1/queries/get_user_for_handle.sql @@ -0,0 +1,7 @@ +-- name: GetUserForHandle :one +SELECT user_id FROM users +WHERE + handle_lc = lower(@handle) + AND is_current = true +ORDER BY created_at ASC +LIMIT 1; diff --git a/api/fixture_test.go b/api/fixture_test.go index 2d355973..c567a964 100644 --- a/api/fixture_test.go +++ b/api/fixture_test.go @@ -161,6 +161,30 @@ var ( "updated_at": time.Now(), "txhash": "tx123", } + + trackRouteBaseRow = map[string]any{ + "slug": nil, + "title_slug": nil, + "collision_id": nil, + "owner_id": nil, + "track_id": nil, + "is_current": true, + "blockhash": "block_abc123", + "blocknumber": 101, + "txhash": "tx123", + } + + playlistRouteBaseRow = map[string]any{ + "slug": nil, + "title_slug": nil, + "collision_id": nil, + "owner_id": nil, + "playlist_id": nil, + "is_current": true, + "blockhash": "block_abc123", + "blocknumber": 101, + "txhash": "tx123", + } ) func insertFixtures(table string, baseRow map[string]any, csvFile string) { diff --git a/api/resolve_middleware.go b/api/resolve_middleware.go index fa1b1e7e..916e0fb9 100644 --- a/api/resolve_middleware.go +++ b/api/resolve_middleware.go @@ -14,6 +14,10 @@ func (app *ApiServer) isFullMiddleware(c *fiber.Ctx) error { return c.Next() } +func (app *ApiServer) getIsFull(c *fiber.Ctx) bool { + return c.Locals("isFull").(bool) +} + // will set myId if valid, defaults to 0 func (app *ApiServer) resolveMyIdMiddleware(c *fiber.Ctx) error { myId, _ := trashid.DecodeHashId(c.Query("user_id")) diff --git a/api/server.go b/api/server.go index ec243d78..cf6148d1 100644 --- a/api/server.go +++ b/api/server.go @@ -244,6 +244,9 @@ func NewApiServer(config config.Config) *ApiServer { // Rewards g.Get("/rewards/claim", app.v1ClaimRewards) + + // Resolve + g.Get("/resolve", app.v1Resolve) } app.Static("/", "./static") @@ -308,11 +311,9 @@ func (app *ApiServer) resolveUserHandleToId(handle string) (int32, error) { if hit, ok := app.resolveHandleCache.Get(handle); ok { return hit, nil } - var userId int32 - sql := `select user_id from users where handle_lc = lower($1)` - err := app.pool.QueryRow(context.Background(), sql, handle).Scan(&userId) - app.resolveHandleCache.Set(handle, userId) - return userId, err + user_id, err := app.queries.GetUserForHandle(context.Background(), handle) + app.resolveHandleCache.Set(handle, int32(user_id)) + return int32(user_id), err } func (as *ApiServer) Serve() { diff --git a/api/server_test.go b/api/server_test.go index 7dcbaaa2..1a2db333 100644 --- a/api/server_test.go +++ b/api/server_test.go @@ -77,7 +77,8 @@ func TestMain(m *testing.M) { insertFixtures("associated_wallets", connectedWalletsBaseRow, "testdata/connected_wallets_fixtures.csv") insertFixtures("aggregate_user_tips", aggregateUserTipsBaseRow, "testdata/aggregate_user_tips_fixtures.csv") insertFixtures("usdc_purchases", usdcPurchaseBaseRow, "testdata/usdc_purchases_fixtures.csv") - insertFixtures("track_routes", map[string]any{}, "testdata/track_routes_fixtures.csv") + insertFixtures("track_routes", trackRouteBaseRow, "testdata/track_routes_fixtures.csv") + insertFixtures("playlist_routes", playlistRouteBaseRow, "testdata/playlist_routes_fixtures.csv") insertFixtures("grants", grantBaseRow, "testdata/grants_fixtures.csv") // index to es / os diff --git a/api/testdata/playlist_fixtures.csv b/api/testdata/playlist_fixtures.csv index 08cee516..d3fdfd0b 100644 --- a/api/testdata/playlist_fixtures.csv +++ b/api/testdata/playlist_fixtures.csv @@ -3,3 +3,5 @@ playlist_id,playlist_name,playlist_owner_id,is_album,playlist_contents,stream_co 2,Follow Gated Stream,3,t,"{}","{""follow_user_id"": 3}" 3,SecondAlbum,1,t,"{""track_ids"": [{""time"": 1722451644, ""track"": 200, ""metadata_time"": 1722451644},{""time"": 1722451644, ""track"": -1, ""metadata_time"": 1722451644},{""time"": 1722451644, ""track"": 300, ""metadata_time"": 1722451644}]}", 4,Purchase Gated Stream,3,t,"{}","{""usdc_purchase"": {""price"": 135, ""splits"": [{""user_id"": 3, ""percentage"": 100.0}]}}" +500,playlist by permalink,7,f,, +501,album by permalink,7,t,, \ No newline at end of file diff --git a/api/testdata/playlist_routes_fixtures.csv b/api/testdata/playlist_routes_fixtures.csv new file mode 100644 index 00000000..facf6bde --- /dev/null +++ b/api/testdata/playlist_routes_fixtures.csv @@ -0,0 +1,3 @@ +slug,title_slug,collision_id,owner_id,playlist_id +playlist-by-permalink,playlist-by-permalink,0,7,500 +album-by-permalink,album-by-permalink,0,8,501 \ No newline at end of file diff --git a/api/testdata/track_routes_fixtures.csv b/api/testdata/track_routes_fixtures.csv index 0c50a94c..4a01b2df 100644 --- a/api/testdata/track_routes_fixtures.csv +++ b/api/testdata/track_routes_fixtures.csv @@ -1,2 +1,2 @@ -"slug","title_slug","collision_id","owner_id","track_id","is_current","blockhash","blocknumber","txhash" -"track-by-permalink","track-by-permalink",0,6,500,TRUE,"0x24f1465e4bd8803b79b2cbcfad695363a640623053563ffd20fdeaf4656a7b89",23200013,"0x76dcea2bf98e56f683b95a071f5c87405a864eea87243920e60cbc7b96ad565b" +slug,title_slug,collision_id,owner_id,track_id +track-by-permalink,track-by-permalink,0,6,500 diff --git a/api/testdata/user_fixtures.csv b/api/testdata/user_fixtures.csv index 44130ce3..e9e0c32a 100644 --- a/api/testdata/user_fixtures.csv +++ b/api/testdata/user_fixtures.csv @@ -5,6 +5,8 @@ user_id,handle,handle_lc,is_deactivated,wallet,playlist_library 4,accesstester,accesstester,f,0x34567890abcdef12, 5,guyintrending,guyintrending,f,0x34567890abcdef13, 6,TracksByPermalink,tracksbypermalink,f,0xffffffffff, +7,PlaylistsByPermalink,playlistsbypermalink,f,0xffffffffff, +8,AlbumsByPermalink,albumsbypermalink,f,0xffffffffff, 91,badguy,badguy,t,0x4567890abcdef123, 100,authtest1,authtest1,f,0x681c616ae836ceca1effe00bd07f2fdbf9a082bc, 101,authtest2,authtest2,f,0xc451c1f8943b575158310552b41230c61844a1c1, diff --git a/api/v1_playlists.go b/api/v1_playlists.go index b5b2eda4..4797d281 100644 --- a/api/v1_playlists.go +++ b/api/v1_playlists.go @@ -1,6 +1,8 @@ package api import ( + "strings" + "bridgerton.audius.co/api/dbv1" "github.com/gofiber/fiber/v2" ) @@ -9,6 +11,32 @@ func (app *ApiServer) v1playlists(c *fiber.Ctx) error { myId := app.getMyId(c) ids := decodeIdList(c) + // Add permalink ID mappings + permalinks := queryMutli(c, "permalink") + if len(permalinks) > 0 { + handles := make([]string, len(permalinks)) + slugs := make([]string, len(permalinks)) + for i, permalink := range permalinks { + if match := playlistURLRegex.FindStringSubmatch(permalink); match != nil { + handles[i] = strings.ToLower(match[1]) + slugs[i] = match[3] + playlistType := match[2] + permalinks[i] = handles[i] + "/" + playlistType + "/" + slugs[i] + } else { + return fiber.NewError(fiber.StatusBadRequest, "Invalid permalink: "+permalinks[i]) + } + } + newIds, err := app.queries.GetPlaylistIdsByPermalink(c.Context(), dbv1.GetPlaylistIdsByPermalinkParams{ + Handles: handles, + Slugs: slugs, + Permalinks: permalinks, + }) + if err != nil { + return err + } + ids = append(ids, newIds...) + } + playlists, err := app.queries.FullPlaylists(c.Context(), dbv1.GetPlaylistsParams{ MyID: myId, Ids: ids, diff --git a/api/v1_playlists_test.go b/api/v1_playlists_test.go index 9568dc65..3b256922 100644 --- a/api/v1_playlists_test.go +++ b/api/v1_playlists_test.go @@ -4,7 +4,6 @@ import ( "testing" "bridgerton.audius.co/api/dbv1" - "bridgerton.audius.co/trashid" "github.com/stretchr/testify/assert" ) @@ -13,11 +12,25 @@ func TestPlaylistsEndpoint(t *testing.T) { Data []dbv1.FullPlaylist } - status, _ := testGet(t, "/v1/full/playlists?id=7eP5n", &resp) + status, body := testGet(t, "/v1/full/playlists?id=7eP5n", &resp) assert.Equal(t, 200, status) - pl := resp.Data[0] - assert.Equal(t, pl.ID, "7eP5n") - assert.Len(t, pl.Tracks, 2) - assert.Equal(t, trashid.HashId(2), pl.Tracks[0].User.ID) + jsonAssert(t, body, map[string]string{ + "data.0.id": "7eP5n", + "data.0.playlist_name": "First", + }) +} + +func TestPlaylistsEndpointWithPermalink(t *testing.T) { + var resp struct { + Data []dbv1.FullPlaylist + } + + status, body := testGet(t, "/v1/full/playlists?permalink=/PlaylistsByPermalink/playlist/playlist-by-permalink", &resp) + assert.Equal(t, 200, status) + + jsonAssert(t, body, map[string]string{ + "data.0.id": "eYake", + "data.0.playlist_name": "playlist by permalink", + }) } diff --git a/api/v1_resolve.go b/api/v1_resolve.go new file mode 100644 index 00000000..92cc257d --- /dev/null +++ b/api/v1_resolve.go @@ -0,0 +1,109 @@ +package api + +import ( + "net/url" + "regexp" + "strings" + + "bridgerton.audius.co/api/dbv1" + "bridgerton.audius.co/trashid" + "github.com/gofiber/fiber/v2" +) + +var ( + trackURLRegex = regexp.MustCompile(`^/(?P[^/]*)/(?P[^/]*)$`) + playlistURLRegex = regexp.MustCompile(`/(?P[^/]*)/(?Pplaylist|album)/(?P[^/]*)$`) + userURLRegex = regexp.MustCompile(`^/(?P[^/]*)$`) +) + +func (app *ApiServer) v1Resolve(c *fiber.Ctx) error { + isFull := app.getIsFull(c) + urlStr := c.Query("url") + if urlStr == "" { + return fiber.NewError(fiber.StatusBadRequest, "Missing url parameter") + } + + parsedURL, err := url.Parse(urlStr) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid URL") + } + + // Strip out any preceding protocol & domain + path := parsedURL.Path + + // Try to match track URL + if match := trackURLRegex.FindStringSubmatch(path); match != nil { + handle := strings.ToLower(match[1]) + slug := match[2] + permalink := handle + "/" + slug + + trackIds, err := app.queries.GetTrackIdsByPermalink(c.Context(), dbv1.GetTrackIdsByPermalinkParams{ + Handles: []string{handle}, + Slugs: []string{slug}, + Permalinks: []string{permalink}, + }) + if err != nil || len(trackIds) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Track not found") + } + + trackId, err := trashid.EncodeHashId(int(trackIds[0])) + if err != nil { + return err + } + + if isFull { + return c.Redirect("/v1/full/tracks/"+trackId, fiber.StatusFound) + } + return c.Redirect("/v1/tracks/"+trackId, fiber.StatusFound) + } + + // Try to match playlist URL + if match := playlistURLRegex.FindStringSubmatch(path); match != nil { + handle := strings.ToLower(match[1]) + playlistType := match[2] + slug := match[3] + permalink := handle + "/" + playlistType + "/" + slug + + playlistIds, err := app.queries.GetPlaylistIdsByPermalink(c.Context(), dbv1.GetPlaylistIdsByPermalinkParams{ + Handles: []string{handle}, + Slugs: []string{slug}, + Permalinks: []string{permalink}, + }) + if err != nil || len(playlistIds) == 0 { + return fiber.NewError(fiber.StatusNotFound, "Playlist not found") + } + + playlistId, err := trashid.EncodeHashId(int(playlistIds[0])) + if err != nil { + return err + } + + if isFull { + return c.Redirect("/v1/full/playlists/"+playlistId, fiber.StatusFound) + } + return c.Redirect("/v1/playlists/"+playlistId, fiber.StatusFound) + } + + // Try to match user URL + if match := userURLRegex.FindStringSubmatch(path); match != nil { + handle := strings.ToLower(match[1]) + + rawUserId, err := app.queries.GetUserForHandle(c.Context(), handle) + + if err != nil { + return fiber.NewError(fiber.StatusNotFound, "User not found") + } + + userId, err := trashid.EncodeHashId(int(rawUserId)) + if err != nil { + return err + } + + if isFull { + return c.Redirect("/v1/full/users/"+userId, fiber.StatusFound) + } + return c.Redirect("/v1/users/"+userId, fiber.StatusFound) + } + + return fiber.NewError(fiber.StatusNotFound, "URL not found") +} diff --git a/api/v1_resolve_test.go b/api/v1_resolve_test.go new file mode 100644 index 00000000..14c116c8 --- /dev/null +++ b/api/v1_resolve_test.go @@ -0,0 +1,45 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestResolveTrackURL(t *testing.T) { + // Test successful track resolution + status, _ := testGet(t, "/v1/resolve?url=https://audius.co/TracksByPermalink/track-by-permalink") + assert.Equal(t, 302, status) + + // Test failed track resolution + status, _ = testGet(t, "/v1/resolve?url=https://audius.co/nonexistent/track") + assert.Equal(t, 404, status) + status, _ = testGet(t, "/v1/resolve?url=invalid-url") + assert.Equal(t, 404, status) + status, _ = testGet(t, "/v1/resolve") + assert.Equal(t, 400, status) +} + +func TestResolvePlaylistURL(t *testing.T) { + // Test successful playlist resolution + status, _ := testGet(t, "/v1/resolve?url=https://audius.co/PlaylistsByPermalink/playlist/playlist-by-permalink") + assert.Equal(t, 302, status) + + // Test successful album resolution + status, _ = testGet(t, "/v1/resolve?url=https://audius.co/AlbumsByPermalink/album/album-by-permalink") + assert.Equal(t, 302, status) + + // Test failed playlist resolution + status, _ = testGet(t, "/v1/resolve?url=https://audius.co/nonexistent/playlist/playlist") + assert.Equal(t, 404, status) +} + +func TestResolveUserURL(t *testing.T) { + // Test successful user resolution + status, _ := testGet(t, "/v1/resolve?url=https://audius.co/rayjacobson") + assert.Equal(t, 302, status) + + // Test failed user resolution + status, _ = testGet(t, "/v1/resolve?url=https://audius.co/nonexistentuser") + assert.Equal(t, 404, status) +} diff --git a/api/v1_tracks.go b/api/v1_tracks.go index af2de91a..34e00d00 100644 --- a/api/v1_tracks.go +++ b/api/v1_tracks.go @@ -18,13 +18,13 @@ func (app *ApiServer) v1Tracks(c *fiber.Ctx) error { handles := make([]string, len(permalinks)) slugs := make([]string, len(permalinks)) for i, permalink := range permalinks { - permalinks[i] = strings.ToLower(strings.TrimPrefix(permalink, "/")) - splits := strings.Split(permalinks[i], "/") - if len(splits) != 2 { + if match := trackURLRegex.FindStringSubmatch(permalink); match != nil { + handles[i] = strings.ToLower(match[1]) + slugs[i] = match[2] + permalinks[i] = handles[i] + "/" + slugs[i] + } else { return fiber.NewError(fiber.StatusBadRequest, "Invalid permalink: "+permalink) } - handles[i] = splits[0] - slugs[i] = splits[1] } newIds, err := app.queries.GetTrackIdsByPermalink(c.Context(), dbv1.GetTrackIdsByPermalinkParams{ Handles: handles, diff --git a/api/v1_tracks_test.go b/api/v1_tracks_test.go index d83328c0..3992bd16 100644 --- a/api/v1_tracks_test.go +++ b/api/v1_tracks_test.go @@ -7,6 +7,20 @@ import ( "github.com/stretchr/testify/assert" ) +func TestTracksEndpoint(t *testing.T) { + var resp struct { + Data []dbv1.FullTrack + } + + status, body := testGet(t, "/v1/full/tracks?id=eYZmn", &resp) + assert.Equal(t, 200, status) + + jsonAssert(t, body, map[string]string{ + "data.0.id": "eYZmn", + "data.0.title": "T1", + }) +} + func TestGetTracksByPermalink(t *testing.T) { var tracksResponse struct { Data []dbv1.FullTrack diff --git a/api/v1_user.go b/api/v1_user.go index c4c0de11..ba59cf4a 100644 --- a/api/v1_user.go +++ b/api/v1_user.go @@ -25,7 +25,7 @@ func (app *ApiServer) v1User(c *fiber.Ctx) error { // full returns an array // non-full returns an object // wild - if c.Locals("isFull").(bool) { + if app.getIsFull(c) { return v1UsersResponse(c, users) } return v1UserResponse(c, users[0]) diff --git a/api/v1_users_handle.go b/api/v1_users_handle.go new file mode 100644 index 00000000..07302f26 --- /dev/null +++ b/api/v1_users_handle.go @@ -0,0 +1,30 @@ +package api + +import ( + "bridgerton.audius.co/api/dbv1" + "github.com/gofiber/fiber/v2" +) + +func (app *ApiServer) v1UsersHandle(c *fiber.Ctx) error { + handle := c.Params("handle") + if handle == "" { + return fiber.NewError(fiber.StatusBadRequest, "Missing handle parameter") + } + + userId, err := app.queries.GetUserForHandle(c.Context(), handle) + if err != nil { + return err + } + + users, err := app.queries.FullUsers(c.Context(), dbv1.GetUsersParams{ + Ids: []int32{int32(userId)}, + MyID: userId, + }) + if err != nil { + return err + } + + return c.JSON(fiber.Map{ + "data": users[0], + }) +} diff --git a/api/v1_users_handle_test.go b/api/v1_users_handle_test.go new file mode 100644 index 00000000..d874420f --- /dev/null +++ b/api/v1_users_handle_test.go @@ -0,0 +1,21 @@ +package api + +import ( + "testing" + + "bridgerton.audius.co/api/dbv1" + "github.com/stretchr/testify/assert" +) + +func TestGetUsersHandle(t *testing.T) { + var accountResponse struct { + Data dbv1.FullAccount + } + status, body := testGet(t, "/v1/users/handle/rayjacobson", &accountResponse) + assert.Equal(t, 200, status) + + jsonAssert(t, body, map[string]string{ + "data.id": "7eP5n", + "data.handle": "rayjacobson", + }) +} diff --git a/api/v1_users_supporters.go b/api/v1_users_supporters.go index 447cb0a2..fe9fa879 100644 --- a/api/v1_users_supporters.go +++ b/api/v1_users_supporters.go @@ -77,7 +77,7 @@ func (app *ApiServer) v1UsersSupporters(c *fiber.Ctx) error { supported[idx] = s } - if !c.Locals("isFull").(bool) { + if !app.getIsFull(c) { // Create a new array with MinUsers type minSupportedUser struct { supportedUser diff --git a/api/v1_users_supporting.go b/api/v1_users_supporting.go index faee5f77..ca496738 100644 --- a/api/v1_users_supporting.go +++ b/api/v1_users_supporting.go @@ -83,7 +83,7 @@ func (app *ApiServer) v1UsersSupporting(c *fiber.Ctx) error { supported[idx] = s } - if !c.Locals("isFull").(bool) { + if !app.getIsFull(c) { // Create a new array with MinUsers type minSupportedUser struct { supportedUser From e3019359adb1a2483c03b8c644661c105538ba36 Mon Sep 17 00:00:00 2001 From: Raymond Jacobson Date: Tue, 29 Apr 2025 15:31:10 -0700 Subject: [PATCH 2/2] Fix tests and address comments --- api/dbv1/get_playlist_ids_by_permalink.sql.go | 34 +++++++++++++------ api/dbv1/get_track_ids_by_permalink.sql.go | 28 ++++++++++----- api/dbv1/get_user_for_handle.sql.go | 1 - .../queries/get_playlist_ids_by_permalink.sql | 32 ++++++++++++----- .../queries/get_track_ids_by_permalink.sql | 25 ++++++++++---- api/dbv1/queries/get_user_for_handle.sql | 1 - api/server.go | 5 ++- api/testdata/playlist_fixtures.csv | 2 +- api/v1_playlists.go | 7 ++-- api/v1_playlists_test.go | 16 ++++++++- api/v1_resolve.go | 7 ++-- api/v1_tracks.go | 6 ++-- api/v1_users_handle.go | 30 ---------------- 13 files changed, 111 insertions(+), 83 deletions(-) delete mode 100644 api/v1_users_handle.go diff --git a/api/dbv1/get_playlist_ids_by_permalink.sql.go b/api/dbv1/get_playlist_ids_by_permalink.sql.go index 1323922c..8d251d07 100644 --- a/api/dbv1/get_playlist_ids_by_permalink.sql.go +++ b/api/dbv1/get_playlist_ids_by_permalink.sql.go @@ -10,25 +10,39 @@ import ( ) const getPlaylistIdsByPermalink = `-- name: GetPlaylistIdsByPermalink :many -SELECT playlist_id -FROM playlist_routes -JOIN users ON users.user_id = playlist_routes.owner_id -WHERE handle_lc = ANY($1::text[]) - AND slug = ANY($2::text[]) - AND ( - CONCAT(handle_lc, '/playlist/', slug) = ANY($3::text[]) -- in case of conflicts across users - OR CONCAT(handle_lc, '/album/', slug) = ANY($3::text[]) -- in case of conflicts across users +WITH lower_handles AS ( + SELECT LOWER(h) AS handle + FROM unnest($2::text[]) AS h +), +lower_permalinks AS ( + SELECT LOWER(p) AS permalink + FROM unnest($3::text[]) AS p +) +SELECT pr.playlist_id +FROM playlist_routes pr +JOIN users u ON u.user_id = pr.owner_id +JOIN lower_handles lh + ON u.handle_lc = lh.handle +WHERE pr.slug = ANY($1::text[]) + -- in case of conflicts across users + AND ( + CONCAT('/', u.handle_lc, '/playlist/', LOWER(pr.slug)) = ANY( + SELECT permalink FROM lower_permalinks + ) + OR CONCAT('/', u.handle_lc, '/album/', LOWER(pr.slug)) = ANY( + SELECT permalink FROM lower_permalinks ) + ) ` type GetPlaylistIdsByPermalinkParams struct { - Handles []string `json:"handles"` Slugs []string `json:"slugs"` + Handles []string `json:"handles"` Permalinks []string `json:"permalinks"` } func (q *Queries) GetPlaylistIdsByPermalink(ctx context.Context, arg GetPlaylistIdsByPermalinkParams) ([]int32, error) { - rows, err := q.db.Query(ctx, getPlaylistIdsByPermalink, arg.Handles, arg.Slugs, arg.Permalinks) + rows, err := q.db.Query(ctx, getPlaylistIdsByPermalink, arg.Slugs, arg.Handles, arg.Permalinks) if err != nil { return nil, err } diff --git a/api/dbv1/get_track_ids_by_permalink.sql.go b/api/dbv1/get_track_ids_by_permalink.sql.go index 4bc37253..63ffc33e 100644 --- a/api/dbv1/get_track_ids_by_permalink.sql.go +++ b/api/dbv1/get_track_ids_by_permalink.sql.go @@ -10,22 +10,34 @@ import ( ) const getTrackIdsByPermalink = `-- name: GetTrackIdsByPermalink :many -SELECT track_id -FROM track_routes -JOIN users ON users.user_id = track_routes.owner_id -WHERE handle_lc = ANY($1::text[]) - AND slug = ANY($2::text[]) - AND CONCAT(handle_lc, '/', slug) = ANY($3::text[]) -- in case of conflicts across users +WITH lower_handles AS ( + SELECT LOWER(h) AS handle + FROM unnest($2::text[]) AS h +), +lower_permalinks AS ( + SELECT LOWER(p) AS permalink + FROM unnest($3::text[]) AS p +) +SELECT tr.track_id +FROM track_routes tr +JOIN users u ON u.user_id = tr.owner_id +JOIN lower_handles lh + ON u.handle_lc = lh.handle +WHERE tr.slug = ANY($1::text[]) + -- in case of conflicts across usAers + AND CONCAT('/', u.handle_lc, '/', LOWER(tr.slug)) = ANY( + SELECT permalink FROM lower_permalinks + ) ` type GetTrackIdsByPermalinkParams struct { - Handles []string `json:"handles"` Slugs []string `json:"slugs"` + Handles []string `json:"handles"` Permalinks []string `json:"permalinks"` } func (q *Queries) GetTrackIdsByPermalink(ctx context.Context, arg GetTrackIdsByPermalinkParams) ([]int32, error) { - rows, err := q.db.Query(ctx, getTrackIdsByPermalink, arg.Handles, arg.Slugs, arg.Permalinks) + rows, err := q.db.Query(ctx, getTrackIdsByPermalink, arg.Slugs, arg.Handles, arg.Permalinks) if err != nil { return nil, err } diff --git a/api/dbv1/get_user_for_handle.sql.go b/api/dbv1/get_user_for_handle.sql.go index 4fb12b23..34ce3368 100644 --- a/api/dbv1/get_user_for_handle.sql.go +++ b/api/dbv1/get_user_for_handle.sql.go @@ -15,7 +15,6 @@ const getUserForHandle = `-- name: GetUserForHandle :one SELECT user_id FROM users WHERE handle_lc = lower($1) - AND is_current = true ORDER BY created_at ASC LIMIT 1 ` diff --git a/api/dbv1/queries/get_playlist_ids_by_permalink.sql b/api/dbv1/queries/get_playlist_ids_by_permalink.sql index 1425a824..97ec026e 100644 --- a/api/dbv1/queries/get_playlist_ids_by_permalink.sql +++ b/api/dbv1/queries/get_playlist_ids_by_permalink.sql @@ -1,10 +1,24 @@ -- name: GetPlaylistIdsByPermalink :many -SELECT playlist_id -FROM playlist_routes -JOIN users ON users.user_id = playlist_routes.owner_id -WHERE handle_lc = ANY(@handles::text[]) - AND slug = ANY(@slugs::text[]) - AND ( - CONCAT(handle_lc, '/playlist/', slug) = ANY(@permalinks::text[]) -- in case of conflicts across users - OR CONCAT(handle_lc, '/album/', slug) = ANY(@permalinks::text[]) -- in case of conflicts across users - ); +WITH lower_handles AS ( + SELECT LOWER(h) AS handle + FROM unnest(@handles::text[]) AS h +), +lower_permalinks AS ( + SELECT LOWER(p) AS permalink + FROM unnest(@permalinks::text[]) AS p +) +SELECT pr.playlist_id +FROM playlist_routes pr +JOIN users u ON u.user_id = pr.owner_id +JOIN lower_handles lh + ON u.handle_lc = lh.handle +WHERE pr.slug = ANY(@slugs::text[]) + -- in case of conflicts across users + AND ( + CONCAT('/', u.handle_lc, '/playlist/', LOWER(pr.slug)) = ANY( + SELECT permalink FROM lower_permalinks + ) + OR CONCAT('/', u.handle_lc, '/album/', LOWER(pr.slug)) = ANY( + SELECT permalink FROM lower_permalinks + ) + ); diff --git a/api/dbv1/queries/get_track_ids_by_permalink.sql b/api/dbv1/queries/get_track_ids_by_permalink.sql index 91afb0ed..05f9d8eb 100644 --- a/api/dbv1/queries/get_track_ids_by_permalink.sql +++ b/api/dbv1/queries/get_track_ids_by_permalink.sql @@ -1,8 +1,19 @@ -- name: GetTrackIdsByPermalink :many -SELECT track_id -FROM track_routes -JOIN users ON users.user_id = track_routes.owner_id -WHERE handle_lc = ANY(@handles::text[]) - AND slug = ANY(@slugs::text[]) - AND CONCAT(handle_lc, '/', slug) = ANY(@permalinks::text[]) -- in case of conflicts across users -; \ No newline at end of file +WITH lower_handles AS ( + SELECT LOWER(h) AS handle + FROM unnest(@handles::text[]) AS h +), +lower_permalinks AS ( + SELECT LOWER(p) AS permalink + FROM unnest(@permalinks::text[]) AS p +) +SELECT tr.track_id +FROM track_routes tr +JOIN users u ON u.user_id = tr.owner_id +JOIN lower_handles lh + ON u.handle_lc = lh.handle +WHERE tr.slug = ANY(@slugs::text[]) + -- in case of conflicts across usAers + AND CONCAT('/', u.handle_lc, '/', LOWER(tr.slug)) = ANY( + SELECT permalink FROM lower_permalinks + ); diff --git a/api/dbv1/queries/get_user_for_handle.sql b/api/dbv1/queries/get_user_for_handle.sql index 4cfd2e5a..64f4d39e 100644 --- a/api/dbv1/queries/get_user_for_handle.sql +++ b/api/dbv1/queries/get_user_for_handle.sql @@ -2,6 +2,5 @@ SELECT user_id FROM users WHERE handle_lc = lower(@handle) - AND is_current = true ORDER BY created_at ASC LIMIT 1; diff --git a/api/server.go b/api/server.go index cf6148d1..c7415e6d 100644 --- a/api/server.go +++ b/api/server.go @@ -312,8 +312,11 @@ func (app *ApiServer) resolveUserHandleToId(handle string) (int32, error) { return hit, nil } user_id, err := app.queries.GetUserForHandle(context.Background(), handle) + if err != nil { + return 0, err + } app.resolveHandleCache.Set(handle, int32(user_id)) - return int32(user_id), err + return int32(user_id), nil } func (as *ApiServer) Serve() { diff --git a/api/testdata/playlist_fixtures.csv b/api/testdata/playlist_fixtures.csv index d3fdfd0b..2aab6f9f 100644 --- a/api/testdata/playlist_fixtures.csv +++ b/api/testdata/playlist_fixtures.csv @@ -4,4 +4,4 @@ playlist_id,playlist_name,playlist_owner_id,is_album,playlist_contents,stream_co 3,SecondAlbum,1,t,"{""track_ids"": [{""time"": 1722451644, ""track"": 200, ""metadata_time"": 1722451644},{""time"": 1722451644, ""track"": -1, ""metadata_time"": 1722451644},{""time"": 1722451644, ""track"": 300, ""metadata_time"": 1722451644}]}", 4,Purchase Gated Stream,3,t,"{}","{""usdc_purchase"": {""price"": 135, ""splits"": [{""user_id"": 3, ""percentage"": 100.0}]}}" 500,playlist by permalink,7,f,, -501,album by permalink,7,t,, \ No newline at end of file +501,album by permalink,8,t,, \ No newline at end of file diff --git a/api/v1_playlists.go b/api/v1_playlists.go index 4797d281..836a270e 100644 --- a/api/v1_playlists.go +++ b/api/v1_playlists.go @@ -1,8 +1,6 @@ package api import ( - "strings" - "bridgerton.audius.co/api/dbv1" "github.com/gofiber/fiber/v2" ) @@ -18,10 +16,9 @@ func (app *ApiServer) v1playlists(c *fiber.Ctx) error { slugs := make([]string, len(permalinks)) for i, permalink := range permalinks { if match := playlistURLRegex.FindStringSubmatch(permalink); match != nil { - handles[i] = strings.ToLower(match[1]) + handles[i] = match[1] slugs[i] = match[3] - playlistType := match[2] - permalinks[i] = handles[i] + "/" + playlistType + "/" + slugs[i] + permalinks[i] = permalink } else { return fiber.NewError(fiber.StatusBadRequest, "Invalid permalink: "+permalinks[i]) } diff --git a/api/v1_playlists_test.go b/api/v1_playlists_test.go index 3b256922..bb8dc2e2 100644 --- a/api/v1_playlists_test.go +++ b/api/v1_playlists_test.go @@ -21,7 +21,7 @@ func TestPlaylistsEndpoint(t *testing.T) { }) } -func TestPlaylistsEndpointWithPermalink(t *testing.T) { +func TestPlaylistsEndpointWithPlaylistPermalink(t *testing.T) { var resp struct { Data []dbv1.FullPlaylist } @@ -34,3 +34,17 @@ func TestPlaylistsEndpointWithPermalink(t *testing.T) { "data.0.playlist_name": "playlist by permalink", }) } + +func TestPlaylistsEndpointWithAlbumPermalink(t *testing.T) { + var resp struct { + Data []dbv1.FullPlaylist + } + + status, body := testGet(t, "/v1/full/playlists?permalink=/AlbumsByPermalink/album/album-by-permalink", &resp) + assert.Equal(t, 200, status) + + jsonAssert(t, body, map[string]string{ + "data.0.id": "ePVXL", + "data.0.playlist_name": "album by permalink", + }) +} diff --git a/api/v1_resolve.go b/api/v1_resolve.go index 92cc257d..714a5ae5 100644 --- a/api/v1_resolve.go +++ b/api/v1_resolve.go @@ -35,12 +35,11 @@ func (app *ApiServer) v1Resolve(c *fiber.Ctx) error { if match := trackURLRegex.FindStringSubmatch(path); match != nil { handle := strings.ToLower(match[1]) slug := match[2] - permalink := handle + "/" + slug trackIds, err := app.queries.GetTrackIdsByPermalink(c.Context(), dbv1.GetTrackIdsByPermalinkParams{ Handles: []string{handle}, Slugs: []string{slug}, - Permalinks: []string{permalink}, + Permalinks: []string{path}, }) if err != nil || len(trackIds) == 0 { return fiber.NewError(fiber.StatusNotFound, "Track not found") @@ -60,14 +59,12 @@ func (app *ApiServer) v1Resolve(c *fiber.Ctx) error { // Try to match playlist URL if match := playlistURLRegex.FindStringSubmatch(path); match != nil { handle := strings.ToLower(match[1]) - playlistType := match[2] slug := match[3] - permalink := handle + "/" + playlistType + "/" + slug playlistIds, err := app.queries.GetPlaylistIdsByPermalink(c.Context(), dbv1.GetPlaylistIdsByPermalinkParams{ Handles: []string{handle}, Slugs: []string{slug}, - Permalinks: []string{permalink}, + Permalinks: []string{path}, }) if err != nil || len(playlistIds) == 0 { return fiber.NewError(fiber.StatusNotFound, "Playlist not found") diff --git a/api/v1_tracks.go b/api/v1_tracks.go index 34e00d00..092fe812 100644 --- a/api/v1_tracks.go +++ b/api/v1_tracks.go @@ -1,8 +1,6 @@ package api import ( - "strings" - "bridgerton.audius.co/api/dbv1" "bridgerton.audius.co/trashid" "github.com/gofiber/fiber/v2" @@ -19,9 +17,9 @@ func (app *ApiServer) v1Tracks(c *fiber.Ctx) error { slugs := make([]string, len(permalinks)) for i, permalink := range permalinks { if match := trackURLRegex.FindStringSubmatch(permalink); match != nil { - handles[i] = strings.ToLower(match[1]) + handles[i] = match[1] slugs[i] = match[2] - permalinks[i] = handles[i] + "/" + slugs[i] + permalinks[i] = permalink } else { return fiber.NewError(fiber.StatusBadRequest, "Invalid permalink: "+permalink) } diff --git a/api/v1_users_handle.go b/api/v1_users_handle.go deleted file mode 100644 index 07302f26..00000000 --- a/api/v1_users_handle.go +++ /dev/null @@ -1,30 +0,0 @@ -package api - -import ( - "bridgerton.audius.co/api/dbv1" - "github.com/gofiber/fiber/v2" -) - -func (app *ApiServer) v1UsersHandle(c *fiber.Ctx) error { - handle := c.Params("handle") - if handle == "" { - return fiber.NewError(fiber.StatusBadRequest, "Missing handle parameter") - } - - userId, err := app.queries.GetUserForHandle(c.Context(), handle) - if err != nil { - return err - } - - users, err := app.queries.FullUsers(c.Context(), dbv1.GetUsersParams{ - Ids: []int32{int32(userId)}, - MyID: userId, - }) - if err != nil { - return err - } - - return c.JSON(fiber.Map{ - "data": users[0], - }) -}