diff --git a/Makefile b/Makefile index ffc7f66f..9071f9dc 100644 --- a/Makefile +++ b/Makefile @@ -20,4 +20,4 @@ setup:: go install -v github.com/sqlc-dev/sqlc/cmd/sqlc@latest apidiff:: - npx deno run -A --watch apidiff2.ts + npx deno run -A --watch apidiff.ts diff --git a/api/resolve_middleware.go b/api/resolve_middleware.go index eeb5ee22..95d81cfa 100644 --- a/api/resolve_middleware.go +++ b/api/resolve_middleware.go @@ -38,3 +38,21 @@ func (app *ApiServer) requireHandleMiddleware(c *fiber.Ctx) error { c.Locals("userId", userId) return c.Next() } + +func (app *ApiServer) requireTrackIdMiddleware(c *fiber.Ctx) error { + trackId, err := trashid.DecodeHashId(c.Params("trackId")) + if err != nil || trackId == 0 { + return sendError(c, 400, "invalid trackId") + } + c.Locals("trackId", trackId) + return c.Next() +} + +func (app *ApiServer) requirePlaylistIdMiddleware(c *fiber.Ctx) error { + playlistId, err := trashid.DecodeHashId(c.Params("playlistId")) + if err != nil || playlistId == 0 { + return sendError(c, 400, "invalid playlistId") + } + c.Locals("playlistId", playlistId) + return c.Next() +} diff --git a/api/response_helpers.go b/api/response_helpers.go index a27a3052..1a98ef42 100644 --- a/api/response_helpers.go +++ b/api/response_helpers.go @@ -5,7 +5,18 @@ import ( "github.com/gofiber/fiber/v2" ) -func v1UserResponse(c *fiber.Ctx, users []dbv1.FullUser) error { +func v1UserResponse(c *fiber.Ctx, user dbv1.FullUser) error { + if c.Locals("isFull").(bool) { + return c.JSON(fiber.Map{ + "data": user, + }) + } + return c.JSON(fiber.Map{ + "data": dbv1.ToMinUser(user), + }) +} + +func v1UsersResponse(c *fiber.Ctx, users []dbv1.FullUser) error { if c.Locals("isFull").(bool) { return c.JSON(fiber.Map{ "data": users, @@ -16,7 +27,18 @@ func v1UserResponse(c *fiber.Ctx, users []dbv1.FullUser) error { }) } -func v1PlaylistResponse(c *fiber.Ctx, playlists []dbv1.FullPlaylist) error { +func v1PlaylistResponse(c *fiber.Ctx, playlist dbv1.FullPlaylist) error { + if c.Locals("isFull").(bool) { + return c.JSON(fiber.Map{ + "data": playlist, + }) + } + return c.JSON(fiber.Map{ + "data": dbv1.ToMinPlaylist(playlist), + }) +} + +func v1PlaylistsResponse(c *fiber.Ctx, playlists []dbv1.FullPlaylist) error { if c.Locals("isFull").(bool) { return c.JSON(fiber.Map{ "data": playlists, @@ -27,7 +49,18 @@ func v1PlaylistResponse(c *fiber.Ctx, playlists []dbv1.FullPlaylist) error { }) } -func v1TrackResponse(c *fiber.Ctx, tracks []dbv1.FullTrack) error { +func v1TrackResponse(c *fiber.Ctx, track dbv1.FullTrack) error { + if c.Locals("isFull").(bool) { + return c.JSON(fiber.Map{ + "data": track, + }) + } + return c.JSON(fiber.Map{ + "data": dbv1.ToMinTrack(track), + }) +} + +func v1TracksResponse(c *fiber.Ctx, tracks []dbv1.FullTrack) error { if c.Locals("isFull").(bool) { return c.JSON(fiber.Map{ "data": tracks, diff --git a/api/server.go b/api/server.go index bf770936..f111ddcc 100644 --- a/api/server.go +++ b/api/server.go @@ -115,47 +115,45 @@ func NewApiServer(config Config) *ApiServer { app.Use(app.isFullMiddleware) app.Use(app.resolveMyIdMiddleware) - // v1/full - app.Get("/v1/full/users", app.v1Users) - - app.Use("/v1/full/users/handle/:handle", app.requireHandleMiddleware) - app.Get("/v1/full/users/handle/:handle/tracks", app.v1UserTracks) - app.Get("/v1/full/users/handle/:handle/reposts", app.v1UsersReposts) - - app.Use("/v1/full/users/:userId", app.requireUserIdMiddleware) - app.Get("/v1/full/users/:userId/followers", app.v1UsersFollowers) - app.Get("/v1/full/users/:userId/following", app.v1UsersFollowing) - app.Get("/v1/full/users/:userId/mutuals", app.v1UsersMutuals) - app.Get("/v1/full/users/:userId/reposts", app.v1UsersReposts) - app.Get("/v1/full/users/:userId/supporting", app.v1UsersSupporting) - app.Get("/v1/full/users/:userId/tracks", app.v1UserTracks) - - app.Get("/v1/full/tracks", app.v1Tracks) - app.Get("/v1/full/tracks/:trackId/reposts", app.v1TracksReposts) - app.Get("/v1/full/tracks/:trackId/favorites", app.v1TracksFavorites) - - app.Get("/v1/full/playlists", app.v1playlists) - app.Get("/v1/full/playlists/:playlistId/reposts", app.v1PlaylistsReposts) - app.Get("/v1/full/playlists/:playlistId/favorites", app.v1PlaylistsFavorites) - - app.Get("/v1/full/developer_apps/:address", app.v1DeveloperApps) - - // v1 - app.Get("/v1/users", app.v1Users) - app.Get("/v1/users/:userId/followers", app.v1UsersFollowers) - app.Get("/v1/users/:userId/following", app.v1UsersFollowing) - app.Get("/v1/users/:userId/mutuals", app.v1UsersMutuals) - app.Get("/v1/users/:userId/supporting", app.v1UsersSupporting) - - app.Get("/v1/tracks", app.v1Tracks) - app.Get("/v1/tracks/:trackId/reposts", app.v1TracksReposts) - app.Get("/v1/tracks/:trackId/favorites", app.v1TracksFavorites) - - app.Get("/v1/playlists", app.v1playlists) - app.Get("/v1/playlists/:playlistId/reposts", app.v1PlaylistsReposts) - app.Get("/v1/playlists/:playlistId/favorites", app.v1PlaylistsFavorites) - - app.Get("/v1/developer_apps/:address", app.v1DeveloperApps) + v1 := app.Group("/v1") + v1Full := app.Group("/v1/full") + + for _, g := range []fiber.Router{v1, v1Full} { + // Users + g.Get("/users", app.v1Users) + + g.Use("/users/handle/:handle", app.requireHandleMiddleware) + g.Get("/users/handle/:handle/tracks", app.v1UserTracks) + g.Get("/users/handle/:handle/reposts", app.v1UsersReposts) + + g.Use("/users/:userId", app.requireUserIdMiddleware) + g.Get("/users/:userId", app.v1User) + g.Get("/users/:userId/followers", app.v1UsersFollowers) + g.Get("/users/:userId/following", app.v1UsersFollowing) + g.Get("/users/:userId/mutuals", app.v1UsersMutuals) + g.Get("/users/:userId/reposts", app.v1UsersReposts) + g.Get("/users/:userId/supporting", app.v1UsersSupporting) + g.Get("/users/:userId/tracks", app.v1UserTracks) + + // Tracks + g.Get("/tracks", app.v1Tracks) + + g.Use("/tracks/:trackId", app.requireTrackIdMiddleware) + g.Get("/tracks/:trackId", app.v1Track) + g.Get("/tracks/:trackId/reposts", app.v1TracksReposts) + g.Get("/tracks/:trackId/favorites", app.v1TracksFavorites) + + // Playlists + g.Get("/playlists", app.v1playlists) + + g.Use("/playlists/:playlistId", app.requirePlaylistIdMiddleware) + g.Get("/playlists/:playlistId", app.v1Playlist) + g.Get("/playlists/:playlistId/reposts", app.v1PlaylistsReposts) + g.Get("/playlists/:playlistId/favorites", app.v1PlaylistsFavorites) + + // Developer Apps + g.Get("/developer_apps/:address", app.v1DeveloperApps) + } // proxy unhandled requests thru to existing discovery API { diff --git a/api/v1_playlist.go b/api/v1_playlist.go new file mode 100644 index 00000000..2e8904be --- /dev/null +++ b/api/v1_playlist.go @@ -0,0 +1,27 @@ +package api + +import ( + "bridgerton.audius.co/api/dbv1" + "github.com/gofiber/fiber/v2" +) + +func (app *ApiServer) v1Playlist(c *fiber.Ctx) error { + myId := c.Locals("myId").(int) + playlistId := c.Locals("playlistId").(int) + + playlists, err := app.queries.FullPlaylists(c.Context(), dbv1.GetPlaylistsParams{ + MyID: int32(myId), + Ids: []int32{int32(playlistId)}, + }) + if err != nil { + return err + } + + if len(playlists) == 0 { + return sendError(c, 404, "playlist not found") + } + + playlist := playlists[0] + + return v1PlaylistResponse(c, playlist) +} diff --git a/api/v1_playlist_test.go b/api/v1_playlist_test.go new file mode 100644 index 00000000..d3c1d447 --- /dev/null +++ b/api/v1_playlist_test.go @@ -0,0 +1,21 @@ +package api + +import ( + "strings" + "testing" + + "bridgerton.audius.co/api/dbv1" + "github.com/stretchr/testify/assert" +) + +func TestGetPlaylist(t *testing.T) { + var playlistResponse struct { + Data dbv1.FullPlaylist + } + + status, body := testGet(t, "/v1/full/playlists/7eP5n", &playlistResponse) + assert.Equal(t, 200, status) + + assert.True(t, strings.Contains(string(body), `"playlist_name":"First"`)) + assert.True(t, strings.Contains(string(body), `"id":"7eP5n"`)) +} diff --git a/api/v1_playlists.go b/api/v1_playlists.go index ff0b148c..510144b2 100644 --- a/api/v1_playlists.go +++ b/api/v1_playlists.go @@ -18,5 +18,5 @@ func (app *ApiServer) v1playlists(c *fiber.Ctx) error { return err } - return v1PlaylistResponse(c, playlists) + return v1PlaylistsResponse(c, playlists) } diff --git a/api/v1_track.go b/api/v1_track.go new file mode 100644 index 00000000..12357a3f --- /dev/null +++ b/api/v1_track.go @@ -0,0 +1,27 @@ +package api + +import ( + "bridgerton.audius.co/api/dbv1" + "github.com/gofiber/fiber/v2" +) + +func (app *ApiServer) v1Track(c *fiber.Ctx) error { + myId := c.Locals("myId").(int) + trackId := c.Locals("trackId").(int) + + tracks, err := app.queries.FullTracks(c.Context(), dbv1.GetTracksParams{ + MyID: int32(myId), + Ids: []int32{int32(trackId)}, + }) + if err != nil { + return err + } + + if len(tracks) == 0 { + return sendError(c, 404, "track not found") + } + + track := tracks[0] + + return v1TrackResponse(c, track) +} diff --git a/api/v1_track_test.go b/api/v1_track_test.go new file mode 100644 index 00000000..2189839d --- /dev/null +++ b/api/v1_track_test.go @@ -0,0 +1,21 @@ +package api + +import ( + "strings" + "testing" + + "bridgerton.audius.co/api/dbv1" + "github.com/stretchr/testify/assert" +) + +func TestGetTrack(t *testing.T) { + var trackResponse struct { + Data dbv1.FullTrack + } + + status, body := testGet(t, "/v1/full/tracks/eYJyn", &trackResponse) + assert.Equal(t, 200, status) + + assert.True(t, strings.Contains(string(body), `"title":"Culca Canyon"`)) + assert.True(t, strings.Contains(string(body), `"id":"eYJyn"`)) +} diff --git a/api/v1_tracks.go b/api/v1_tracks.go index 19738843..a37e2bbb 100644 --- a/api/v1_tracks.go +++ b/api/v1_tracks.go @@ -18,5 +18,5 @@ func (app *ApiServer) v1Tracks(c *fiber.Ctx) error { return err } - return v1TrackResponse(c, tracks) + return v1TracksResponse(c, tracks) } diff --git a/api/v1_user.go b/api/v1_user.go new file mode 100644 index 00000000..8ac50eb9 --- /dev/null +++ b/api/v1_user.go @@ -0,0 +1,28 @@ +package api + +import ( + "bridgerton.audius.co/api/dbv1" + "github.com/gofiber/fiber/v2" +) + +func (app *ApiServer) v1User(c *fiber.Ctx) error { + myId := c.Locals("myId") + userId := c.Locals("userId").(int) + + users, err := app.queries.FullUsers(c.Context(), dbv1.GetUsersParams{ + MyID: myId, + Ids: []int32{int32(userId)}, + }) + + if err != nil { + return err + } + + if len(users) == 0 { + return sendError(c, 404, "user not found") + } + + user := users[0] + + return v1UserResponse(c, user) +} diff --git a/api/v1_user_test.go b/api/v1_user_test.go new file mode 100644 index 00000000..ce967cdb --- /dev/null +++ b/api/v1_user_test.go @@ -0,0 +1,27 @@ +package api + +import ( + "strings" + "testing" + + "bridgerton.audius.co/api/dbv1" + "github.com/stretchr/testify/assert" +) + +func TestGetUser(t *testing.T) { + var userResponse struct { + Data dbv1.FullUser + } + + status, body := testGet(t, "/v1/full/users/7eP5n", &userResponse) + assert.Equal(t, 200, status) + + // body is response json + assert.True(t, strings.Contains(string(body), `"handle":"rayjacobson"`)) + assert.True(t, strings.Contains(string(body), `"user_id":1`)) + assert.True(t, strings.Contains(string(body), `"id":"7eP5n"`)) + + // but we also unmarshaled into userResponse + // for structured testing + assert.Equal(t, userResponse.Data.ID, "7eP5n") +} diff --git a/api/v1_users.go b/api/v1_users.go index de8ca82d..2cb595a9 100644 --- a/api/v1_users.go +++ b/api/v1_users.go @@ -23,7 +23,7 @@ func (app *ApiServer) v1Users(c *fiber.Ctx) error { return err } - return v1UserResponse(c, users) + return v1UsersResponse(c, users) } // a generic responder for all the simple user lists: @@ -61,5 +61,5 @@ func (app *ApiServer) queryFullUsers(c *fiber.Ctx, sql string, args pgx.NamedArg users[idx] = userMap[id] } - return v1UserResponse(c, users) + return v1UsersResponse(c, users) } diff --git a/api/v1_users_followers.go b/api/v1_users_followers.go index b63739e6..3aa5aa7d 100644 --- a/api/v1_users_followers.go +++ b/api/v1_users_followers.go @@ -1,6 +1,8 @@ package api import ( + "fmt" + "github.com/gofiber/fiber/v2" "github.com/jackc/pgx/v5" ) @@ -21,7 +23,9 @@ func (app *ApiServer) v1UsersFollowers(c *fiber.Ctx) error { ` userId := c.Locals("userId").(int) - return app.queryFullUsers(c, sql, pgx.NamedArgs{ + res := app.queryFullUsers(c, sql, pgx.NamedArgs{ "userId": userId, }) + fmt.Println(res) + return res } diff --git a/api/v1_users_test.go b/api/v1_users_test.go index 51880d71..252871ea 100644 --- a/api/v1_users_test.go +++ b/api/v1_users_test.go @@ -64,7 +64,7 @@ func TestUserQuery(t *testing.T) { } } -func TestGetUser(t *testing.T) { +func TestGetUsers(t *testing.T) { var userResponse struct { Data []dbv1.FullUser } diff --git a/apidiff2.ts b/apidiff.ts similarity index 99% rename from apidiff2.ts rename to apidiff.ts index d28c181d..ddab10be 100644 --- a/apidiff2.ts +++ b/apidiff.ts @@ -7,6 +7,7 @@ const testPaths = [ "/v1/full/users?id=0EoAm&user_id=aNzoj", "/v1/full/users?id=0EoAm&id=7eP5n&id=aWvp71&id=Wem1e&user_id=aNzoj", + "/v1/users/0EoAm", "/v1/full/users?id=invalid_id1&id=aNzoj", "/v1/full/tracks?id=ZaXp01y&user_id=aNzoj",