Skip to content

Commit 816441e

Browse files
feat: add organization_ids in the user(s) response (#1184)
1 parent a7fb018 commit 816441e

File tree

8 files changed

+164
-33
lines changed

8 files changed

+164
-33
lines changed

coderd/database/databasefake/databasefake.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,29 @@ func (q *fakeQuerier) GetOrganizationMemberByUserID(_ context.Context, arg datab
709709
return database.OrganizationMember{}, sql.ErrNoRows
710710
}
711711

712+
func (q *fakeQuerier) GetOrganizationIDsByMemberIDs(_ context.Context, ids []uuid.UUID) ([]database.GetOrganizationIDsByMemberIDsRow, error) {
713+
q.mutex.RLock()
714+
defer q.mutex.RUnlock()
715+
716+
getOrganizationIDsByMemberIDRows := make([]database.GetOrganizationIDsByMemberIDsRow, 0, len(ids))
717+
for _, userID := range ids {
718+
userOrganizationIDs := make([]uuid.UUID, 0)
719+
for _, membership := range q.organizationMembers {
720+
if membership.UserID == userID {
721+
userOrganizationIDs = append(userOrganizationIDs, membership.OrganizationID)
722+
}
723+
}
724+
getOrganizationIDsByMemberIDRows = append(getOrganizationIDsByMemberIDRows, database.GetOrganizationIDsByMemberIDsRow{
725+
UserID: userID,
726+
OrganizationIDs: userOrganizationIDs,
727+
})
728+
}
729+
if len(getOrganizationIDsByMemberIDRows) == 0 {
730+
return nil, sql.ErrNoRows
731+
}
732+
return getOrganizationIDsByMemberIDRows, nil
733+
}
734+
712735
func (q *fakeQuerier) GetProvisionerDaemons(_ context.Context) ([]database.ProvisionerDaemon, error) {
713736
q.mutex.RLock()
714737
defer q.mutex.RUnlock()

coderd/database/querier.go

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries.sql.go

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

coderd/database/queries/organizationmembers.sql

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,13 @@ INSERT INTO
2020
)
2121
VALUES
2222
($1, $2, $3, $4, $5) RETURNING *;
23+
24+
-- name: GetOrganizationIDsByMemberIDs :many
25+
SELECT
26+
user_id, array_agg(organization_id) :: uuid [ ] AS "organization_IDs"
27+
FROM
28+
organization_members
29+
WHERE
30+
user_id = ANY(@ids :: uuid [ ])
31+
GROUP BY
32+
user_id;

coderd/users.go

Lines changed: 70 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -137,16 +137,34 @@ func (api *api) users(rw http.ResponseWriter, r *http.Request) {
137137
LimitOpt: int32(pageLimit),
138138
Search: searchName,
139139
})
140+
if err != nil {
141+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
142+
Message: err.Error(),
143+
})
144+
return
145+
}
140146

147+
userIDs := make([]uuid.UUID, 0, len(users))
148+
for _, user := range users {
149+
userIDs = append(userIDs, user.ID)
150+
}
151+
organizationIDsByMemberIDsRows, err := api.Database.GetOrganizationIDsByMemberIDs(r.Context(), userIDs)
152+
if xerrors.Is(err, sql.ErrNoRows) {
153+
err = nil
154+
}
141155
if err != nil {
142156
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
143157
Message: err.Error(),
144158
})
145159
return
146160
}
161+
organizationIDsByUserID := map[uuid.UUID][]uuid.UUID{}
162+
for _, organizationIDsByMemberIDsRow := range organizationIDsByMemberIDsRows {
163+
organizationIDsByUserID[organizationIDsByMemberIDsRow.UserID] = organizationIDsByMemberIDsRow.OrganizationIDs
164+
}
147165

148166
render.Status(r, http.StatusOK)
149-
render.JSON(rw, r, convertUsers(users))
167+
render.JSON(rw, r, convertUsers(users, organizationIDsByUserID))
150168
}
151169

152170
// Creates a new user.
@@ -213,15 +231,23 @@ func (api *api) postUser(rw http.ResponseWriter, r *http.Request) {
213231
return
214232
}
215233

216-
httpapi.Write(rw, http.StatusCreated, convertUser(user))
234+
httpapi.Write(rw, http.StatusCreated, convertUser(user, []uuid.UUID{createUser.OrganizationID}))
217235
}
218236

219237
// Returns the parameterized user requested. All validation
220238
// is completed in the middleware for this route.
221-
func (*api) userByName(rw http.ResponseWriter, r *http.Request) {
239+
func (api *api) userByName(rw http.ResponseWriter, r *http.Request) {
222240
user := httpmw.UserParam(r)
241+
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
242+
243+
if err != nil {
244+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
245+
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
246+
})
247+
return
248+
}
223249

224-
httpapi.Write(rw, http.StatusOK, convertUser(user))
250+
httpapi.Write(rw, http.StatusOK, convertUser(user, organizationIDs))
225251
}
226252

227253
func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
@@ -278,7 +304,15 @@ func (api *api) putUserProfile(rw http.ResponseWriter, r *http.Request) {
278304
return
279305
}
280306

281-
httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile))
307+
organizationIDs, err := userOrganizationIDs(r.Context(), api, user)
308+
if err != nil {
309+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
310+
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
311+
})
312+
return
313+
}
314+
315+
httpapi.Write(rw, http.StatusOK, convertUser(updatedUserProfile, organizationIDs))
282316
}
283317

284318
func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) {
@@ -297,7 +331,15 @@ func (api *api) putUserSuspend(rw http.ResponseWriter, r *http.Request) {
297331
return
298332
}
299333

300-
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser))
334+
organizations, err := userOrganizationIDs(r.Context(), api, user)
335+
if err != nil {
336+
httpapi.Write(rw, http.StatusInternalServerError, httpapi.Response{
337+
Message: fmt.Sprintf("get organization IDs: %s", err.Error()),
338+
})
339+
return
340+
}
341+
342+
httpapi.Write(rw, http.StatusOK, convertUser(suspendedUser, organizations))
301343
}
302344

303345
// Returns organizations the parameterized user has access to.
@@ -626,20 +668,34 @@ func (api *api) createUser(ctx context.Context, req codersdk.CreateUserRequest)
626668
})
627669
}
628670

629-
func convertUser(user database.User) codersdk.User {
671+
func convertUser(user database.User, organizationIDs []uuid.UUID) codersdk.User {
630672
return codersdk.User{
631-
ID: user.ID,
632-
Email: user.Email,
633-
CreatedAt: user.CreatedAt,
634-
Username: user.Username,
635-
Status: codersdk.UserStatus(user.Status),
673+
ID: user.ID,
674+
Email: user.Email,
675+
CreatedAt: user.CreatedAt,
676+
Username: user.Username,
677+
Status: codersdk.UserStatus(user.Status),
678+
OrganizationIDs: organizationIDs,
636679
}
637680
}
638681

639-
func convertUsers(users []database.User) []codersdk.User {
682+
func convertUsers(users []database.User, organizationIDsByUserID map[uuid.UUID][]uuid.UUID) []codersdk.User {
640683
converted := make([]codersdk.User, 0, len(users))
641684
for _, u := range users {
642-
converted = append(converted, convertUser(u))
685+
userOrganizationIDs := organizationIDsByUserID[u.ID]
686+
converted = append(converted, convertUser(u, userOrganizationIDs))
643687
}
644688
return converted
645689
}
690+
691+
func userOrganizationIDs(ctx context.Context, api *api, user database.User) ([]uuid.UUID, error) {
692+
organizationIDsByMemberIDsRows, err := api.Database.GetOrganizationIDsByMemberIDs(ctx, []uuid.UUID{user.ID})
693+
if errors.Is(err, sql.ErrNoRows) || len(organizationIDsByMemberIDsRows) == 0 {
694+
return []uuid.UUID{}, nil
695+
}
696+
if err != nil {
697+
return []uuid.UUID{}, err
698+
}
699+
member := organizationIDsByMemberIDsRows[0]
700+
return member.OrganizationIDs, nil
701+
}

coderd/users_test.go

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -321,9 +321,11 @@ func TestPutUserSuspend(t *testing.T) {
321321
func TestUserByName(t *testing.T) {
322322
t.Parallel()
323323
client := coderdtest.New(t, nil)
324-
_ = coderdtest.CreateFirstUser(t, client)
325-
_, err := client.User(context.Background(), codersdk.Me)
324+
firstUser := coderdtest.CreateFirstUser(t, client)
325+
user, err := client.User(context.Background(), codersdk.Me)
326+
326327
require.NoError(t, err)
328+
require.Equal(t, firstUser.OrganizationID, user.OrganizationIDs[0])
327329
}
328330

329331
func TestGetUsers(t *testing.T) {
@@ -340,6 +342,7 @@ func TestGetUsers(t *testing.T) {
340342
users, err := client.Users(context.Background(), codersdk.UsersRequest{})
341343
require.NoError(t, err)
342344
require.Len(t, users, 2)
345+
require.Len(t, users[0].OrganizationIDs, 1)
343346
}
344347

345348
func TestOrganizationsByUser(t *testing.T) {
@@ -451,14 +454,12 @@ func TestPaginatedUsers(t *testing.T) {
451454
coderdtest.CreateFirstUser(t, client)
452455
me, err := client.User(context.Background(), codersdk.Me)
453456
require.NoError(t, err)
457+
orgID := me.OrganizationIDs[0]
454458

455459
allUsers := make([]codersdk.User, 0)
456460
allUsers = append(allUsers, me)
457461
specialUsers := make([]codersdk.User, 0)
458462

459-
org, err := client.CreateOrganization(ctx, me.ID, codersdk.CreateOrganizationRequest{
460-
Name: "default",
461-
})
462463
require.NoError(t, err)
463464

464465
// When 100 users exist
@@ -481,7 +482,7 @@ func TestPaginatedUsers(t *testing.T) {
481482
Email: email,
482483
Username: username,
483484
Password: "password",
484-
OrganizationID: org.ID,
485+
OrganizationID: orgID,
485486
})
486487
require.NoError(t, err)
487488
allUsers = append(allUsers, newUser)

codersdk/users.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,12 @@ const (
3737

3838
// User represents a user in Coder.
3939
type User struct {
40-
ID uuid.UUID `json:"id" validate:"required"`
41-
Email string `json:"email" validate:"required"`
42-
CreatedAt time.Time `json:"created_at" validate:"required"`
43-
Username string `json:"username" validate:"required"`
44-
Status UserStatus `json:"status"`
40+
ID uuid.UUID `json:"id" validate:"required"`
41+
Email string `json:"email" validate:"required"`
42+
CreatedAt time.Time `json:"created_at" validate:"required"`
43+
Username string `json:"username" validate:"required"`
44+
Status UserStatus `json:"status"`
45+
OrganizationIDs []uuid.UUID `json:"organization_ids"`
4546
}
4647

4748
type CreateFirstUserRequest struct {

site/src/api/typesGenerated.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -90,49 +90,49 @@ export interface User {
9090
readonly status: UserStatus
9191
}
9292

93-
// From codersdk/users.go:47:6.
93+
// From codersdk/users.go:48:6.
9494
export interface CreateFirstUserRequest {
9595
readonly email: string
9696
readonly username: string
9797
readonly password: string
9898
readonly organization: string
9999
}
100100

101-
// From codersdk/users.go:60:6.
101+
// From codersdk/users.go:61:6.
102102
export interface CreateUserRequest {
103103
readonly email: string
104104
readonly username: string
105105
readonly password: string
106106
}
107107

108-
// From codersdk/users.go:67:6.
108+
// From codersdk/users.go:68:6.
109109
export interface UpdateUserProfileRequest {
110110
readonly email: string
111111
readonly username: string
112112
}
113113

114-
// From codersdk/users.go:73:6.
114+
// From codersdk/users.go:74:6.
115115
export interface LoginWithPasswordRequest {
116116
readonly email: string
117117
readonly password: string
118118
}
119119

120-
// From codersdk/users.go:79:6.
120+
// From codersdk/users.go:80:6.
121121
export interface LoginWithPasswordResponse {
122122
readonly session_token: string
123123
}
124124

125-
// From codersdk/users.go:84:6.
125+
// From codersdk/users.go:85:6.
126126
export interface GenerateAPIKeyResponse {
127127
readonly key: string
128128
}
129129

130-
// From codersdk/users.go:88:6.
130+
// From codersdk/users.go:89:6.
131131
export interface CreateOrganizationRequest {
132132
readonly name: string
133133
}
134134

135-
// From codersdk/users.go:93:6.
135+
// From codersdk/users.go:94:6.
136136
export interface AuthMethods {
137137
readonly password: boolean
138138
readonly github: boolean

0 commit comments

Comments
 (0)