Skip to content

Commit a736ae3

Browse files
authored
[API-69] Implement managed users endpoint (#69)
Follow on to #65 which implements the managed users endpoint. The query is very similar but it needs to select the user by id and join grants by their wallet address. And the resulting JSON for each record uses `{ user, grant }` instead of `{ manager, grant }`. Fun thing: My original queries were missing parens around the OR clause at the end and that duped me into thinking things were really broken... I also learned that if you put the `IS NULL` check first on a `sqlc.narg()` param, it won't generate the correct type (will give you an `interface {}` instead of a nullable boolean). But if it's last, things work correctly 🤷
1 parent 5e8b8be commit a736ae3

13 files changed

Lines changed: 282 additions & 99 deletions

api/dbv1/full_managers.go

Lines changed: 0 additions & 47 deletions
This file was deleted.

api/dbv1/get_grants.sql.go

Lines changed: 15 additions & 17 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/dbv1/managers.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package dbv1
2+
3+
import (
4+
"context"
5+
)
6+
7+
type FullManagerGrant struct {
8+
GetGrantsForUserIdRow
9+
GranteeUserID *struct{} `json:"grantee_user_id,omitempty"`
10+
}
11+
12+
type FullManagedUserGrant struct {
13+
GetGrantsForGranteeUserIdRow
14+
GranteeUserID *struct{} `json:"grantee_user_id,omitempty"`
15+
}
16+
17+
type FullManager struct {
18+
Manager FullUser `json:"manager"`
19+
Grant FullManagerGrant `json:"grant"`
20+
}
21+
22+
type FullManagedUser struct {
23+
User FullUser `json:"user"`
24+
Grant FullManagedUserGrant `json:"grant"`
25+
}
26+
27+
func (q *Queries) FullManagers(ctx context.Context, params GetGrantsForUserIdParams) ([]FullManager, error) {
28+
29+
grants, err := q.GetGrantsForUserId(ctx, params)
30+
if err != nil {
31+
return nil, err
32+
}
33+
34+
user_ids := make([]int32, len(grants))
35+
for i, grant := range grants {
36+
user_ids[i] = int32(grant.GranteeUserID)
37+
}
38+
39+
users, err := q.FullUsersKeyed(ctx, GetUsersParams{
40+
Ids: user_ids,
41+
MyID: params.UserID,
42+
})
43+
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
managers := make([]FullManager, len(grants))
49+
for i, grant := range grants {
50+
managers[i] = FullManager{
51+
Manager: users[int32(grant.GranteeUserID)],
52+
Grant: FullManagerGrant{GetGrantsForUserIdRow: grant},
53+
}
54+
}
55+
56+
return managers, nil
57+
}
58+
59+
func (q *Queries) FullManagedUsers(ctx context.Context, params GetGrantsForGranteeUserIdParams) ([]FullManagedUser, error) {
60+
grants, err := q.GetGrantsForGranteeUserId(ctx, GetGrantsForGranteeUserIdParams{
61+
IsRevoked: params.IsRevoked,
62+
IsApproved: params.IsApproved,
63+
UserID: params.UserID,
64+
})
65+
if err != nil {
66+
return nil, err
67+
}
68+
69+
user_ids := make([]int32, len(grants))
70+
for i, grant := range grants {
71+
user_ids[i] = int32(grant.UserID)
72+
}
73+
74+
users, err := q.FullUsersKeyed(ctx, GetUsersParams{
75+
Ids: user_ids,
76+
MyID: params.UserID,
77+
})
78+
79+
if err != nil {
80+
return nil, err
81+
}
82+
83+
managedUsers := make([]FullManagedUser, len(grants))
84+
for i, grant := range grants {
85+
managedUsers[i] = FullManagedUser{
86+
User: users[int32(grant.UserID)],
87+
Grant: FullManagedUserGrant{GetGrantsForGranteeUserIdRow: grant},
88+
}
89+
}
90+
91+
return managedUsers, nil
92+
}

api/dbv1/queries/get_grants.sql

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ JOIN users u ON u.wallet = g.grantee_address
1212
WHERE g.user_id = @user_id::int
1313
AND g.is_revoked = @is_revoked
1414
AND g.is_current = true
15-
AND sqlc.narg('is_approved')::boolean IS NULL OR g.is_approved = sqlc.narg('is_approved')
16-
ORDER BY g.created_at DESC;
15+
AND (sqlc.narg('is_approved')::boolean IS NULL OR g.is_approved = sqlc.narg('is_approved'));
1716

18-
-- name: GetGrantsForGranteeAddress :many
17+
-- name: GetGrantsForGranteeUserId :many
1918
SELECT
2019
g.user_id,
2120
g.grantee_address,
@@ -24,10 +23,9 @@ SELECT
2423
g.created_at,
2524
g.updated_at,
2625
u.user_id as grantee_user_id
27-
FROM grants g
28-
JOIN users u ON u.wallet = g.grantee_address
29-
WHERE g.grantee_address = @grantee_address
26+
FROM users u
27+
JOIN grants g ON g.grantee_address = u.wallet
28+
WHERE u.user_id = @user_id::int
3029
AND g.is_current = true
3130
AND g.is_revoked = @is_revoked
32-
AND sqlc.narg('is_approved')::boolean IS NULL OR g.is_approved = sqlc.narg('is_approved')
33-
ORDER BY g.created_at DESC;
31+
AND (sqlc.narg('is_approved')::boolean IS NULL OR g.is_approved = sqlc.narg('is_approved'));

api/request_helpers.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package api
2+
3+
import (
4+
"strconv"
5+
6+
"github.com/gofiber/fiber/v2"
7+
"github.com/jackc/pgx/v5/pgtype"
8+
)
9+
10+
func getOptionalBool(c *fiber.Ctx, key string) (pgtype.Bool, error) {
11+
var value *bool
12+
if valueStr := c.Query(key); valueStr != "" {
13+
parsed, err := strconv.ParseBool(valueStr)
14+
if err != nil {
15+
return pgtype.Bool{}, err
16+
}
17+
value = &parsed
18+
}
19+
return pgtype.Bool{Bool: value != nil && *value, Valid: value != nil}, nil
20+
}

api/resolve_middleware.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@ func (app *ApiServer) getMyId(c *fiber.Ctx) int32 {
3333
return int32(myId.(int))
3434
}
3535

36+
func (app *ApiServer) getUserId(c *fiber.Ctx) int32 {
37+
userId := c.Locals("userId")
38+
if userId == nil {
39+
return 0
40+
}
41+
return int32(userId.(int))
42+
}
43+
3644
func (app *ApiServer) requireUserIdMiddleware(c *fiber.Ctx) error {
3745
userId, err := trashid.DecodeHashId(c.Params("userId"))
3846
if err != nil || userId == 0 {

api/server.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,7 @@ func NewApiServer(config config.Config) *ApiServer {
226226
g.Get("/users/:userId/library/tracks", app.v1UsersLibraryTracks)
227227
g.Get("/users/:userId/library/:playlistType", app.v1UsersLibraryPlaylists)
228228
g.Get("/users/:userId/managers", app.v1UsersManagers)
229+
g.Get("/users/:userId/managed_users", app.v1UsersManagedUsers)
229230
g.Get("/users/:userId/mutuals", app.v1UsersMutuals)
230231
g.Get("/users/:userId/reposts", app.v1UsersReposts)
231232
g.Get("/users/:userId/related", app.v1UsersRelated)

api/server_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ func Test200UnAuthed(t *testing.T) {
120120
"/v1/full/users/7eP5n/library/albums?type=purchase&sort_method=saves",
121121

122122
"/v1/full/users/7eP5n/managers",
123+
"/v1/full/users/7eP5n/managed_users",
123124
"/v1/full/users/7eP5n/mutuals",
124125
"/v1/full/users/7eP5n/reposts",
125126
"/v1/full/users/7eP5n/related",

api/testdata/grants_fixtures.csv

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ user_id,grantee_address,is_approved,is_revoked
33
1,0xc451c1f8943b575158310552b41230c61844a1c1,false,true
44
1,0x1234567890abcdef,true,true
55
1,0x681c616ae836ceca1effe00bd07f2fdbf9a082bc,false,false
6+
2,0x681c616ae836ceca1effe00bd07f2fdbf9a082bc,true,false
7+
3,0x681c616ae836ceca1effe00bd07f2fdbf9a082bc,true,true
8+
4,0x681c616ae836ceca1effe00bd07f2fdbf9a082bc,false,true

api/v1_users_managed_users.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package api
2+
3+
import (
4+
"strconv"
5+
6+
"bridgerton.audius.co/api/dbv1"
7+
"github.com/gofiber/fiber/v2"
8+
)
9+
10+
func (app *ApiServer) v1UsersManagedUsers(c *fiber.Ctx) error {
11+
// Behavior of this field is a little odd. We only want to filter by it
12+
// if it is passed, but otherwise not use a default value for either.
13+
isApproved, err := getOptionalBool(c, "is_approved")
14+
if err != nil {
15+
return fiber.NewError(fiber.StatusBadRequest, "Invalid value for is_approved")
16+
}
17+
18+
isRevoked, err := strconv.ParseBool(c.Query("is_revoked", "false"))
19+
if err != nil {
20+
return fiber.NewError(fiber.StatusBadRequest, "Invalid value for is_revoked")
21+
}
22+
params := dbv1.GetGrantsForGranteeUserIdParams{
23+
UserID: app.getUserId(c),
24+
IsApproved: isApproved,
25+
IsRevoked: isRevoked,
26+
}
27+
28+
managedUsers, err := app.queries.FullManagedUsers(c.Context(), params)
29+
if err != nil {
30+
return err
31+
}
32+
33+
return c.JSON(fiber.Map{
34+
"data": managedUsers,
35+
})
36+
}

0 commit comments

Comments
 (0)