From 06fddc8cb1ebfbd85c90c4e858390088fed3d674 Mon Sep 17 00:00:00 2001 From: Giannis Katsanos Date: Thu, 8 Feb 2024 17:31:02 +0200 Subject: [PATCH] feat: Organization Memberships API (#231) Added support for the Organization Memberships API. Available operations are Create, Update, Delete and List. --- clerk.go | 2 +- clerk_test.go | 25 ++-- organization/client.go | 9 +- organization/client_test.go | 2 + organization_membership.go | 32 ++++ organizationmembership/api.go | 36 +++++ organizationmembership/client.go | 145 ++++++++++++++++++ organizationmembership/client_test.go | 207 ++++++++++++++++++++++++++ 8 files changed, 436 insertions(+), 22 deletions(-) create mode 100644 organization_membership.go create mode 100644 organizationmembership/api.go create mode 100644 organizationmembership/client.go create mode 100644 organizationmembership/client_test.go diff --git a/clerk.go b/clerk.go index db7b327f..1d48428f 100644 --- a/clerk.go +++ b/clerk.go @@ -297,7 +297,7 @@ func setRequestQuery(req *http.Request, params Params) { paramsQuery := params.ToQuery() for k, values := range paramsQuery { for _, v := range values { - q.Set(k, v) + q.Add(k, v) } } req.URL.RawQuery = q.Encode() diff --git a/clerk_test.go b/clerk_test.go index e0c84ce3..98669e33 100644 --- a/clerk_test.go +++ b/clerk_test.go @@ -130,21 +130,15 @@ type testResourceList struct { type testResourceListParams struct { APIParams ListParams - Name string - Overriden string + Name string + Appended string } // We need to implement the Params interface. func (params testResourceListParams) ToQuery() url.Values { - q := url.Values{} + q := params.ListParams.ToQuery() q.Set("name", params.Name) - q.Set("overriden", params.Overriden) - listQ := params.ListParams.ToQuery() - for k, values := range listQ { - for _, v := range values { - q.Add(k, v) - } - } + q.Set("appended", params.Appended) return q } @@ -234,7 +228,6 @@ func TestBackendCall_SuccessfulResponse_PostRequest(t *testing.T) { func TestBackendCall_SuccessfulResponse_GetRequest(t *testing.T) { ctx := context.Background() name := "the-name" - overriden := "true" limit := 1 rawJSON := `{"data": [{"id":"res_123","object":"resource"}], "total_count": 1}` @@ -249,8 +242,8 @@ func TestBackendCall_SuccessfulResponse_GetRequest(t *testing.T) { assert.False(t, ok) // Existing query parameters are preserved assert.Equal(t, "still-here", q.Get("existing")) - // Existing query parameters can be overriden - assert.Equal(t, overriden, q.Get("overriden")) + // Existing query parameters will be appended, not overriden + assert.Equal(t, []string{"false", "true"}, q["appended"]) _, err := w.Write([]byte(rawJSON)) require.NoError(t, err) @@ -266,13 +259,13 @@ func TestBackendCall_SuccessfulResponse_GetRequest(t *testing.T) { // Simulate usage for an API operation on a testResourceList. // We need to initialize a request and use the Backend to send it. resource := &testResourceList{} - req := NewAPIRequest(http.MethodGet, "/resources?existing=still-here&overriden=false") + req := NewAPIRequest(http.MethodGet, "/resources?existing=still-here&appended=false") req.SetParams(&testResourceListParams{ ListParams: ListParams{ Limit: Int64(int64(limit)), }, - Name: name, - Overriden: overriden, + Name: name, + Appended: "true", }) err := GetBackend().Call(ctx, req, resource) require.NoError(t, err) diff --git a/organization/client.go b/organization/client.go index 51ae2b10..b6198169 100644 --- a/organization/client.go +++ b/organization/client.go @@ -132,11 +132,10 @@ func (c *Client) DeleteLogo(ctx context.Context, id string) (*clerk.Organization type ListParams struct { clerk.APIParams clerk.ListParams - IncludeMembersCount *bool `json:"include_members_count,omitempty"` - OrderBy *string `json:"order_by,omitempty"` - Query *string `json:"query,omitempty"` - // TODO do we need a pointer here? Probably not. - UserIDs []string `json:"user_id,omitempty"` + IncludeMembersCount *bool `json:"include_members_count,omitempty"` + OrderBy *string `json:"order_by,omitempty"` + Query *string `json:"query,omitempty"` + UserIDs []string `json:"user_id,omitempty"` } // ToQuery returns query string values from the params. diff --git a/organization/client_test.go b/organization/client_test.go index f6503006..db9a7df0 100644 --- a/organization/client_test.go +++ b/organization/client_test.go @@ -165,6 +165,7 @@ func TestOrganizationClientList(t *testing.T) { "offset": []string{"2"}, "order_by": []string{"-created_at"}, "query": []string{"Acme"}, + "user_id": []string{"user_123", "user_456"}, }, }, } @@ -172,6 +173,7 @@ func TestOrganizationClientList(t *testing.T) { params := &ListParams{ OrderBy: clerk.String("-created_at"), Query: clerk.String("Acme"), + UserIDs: []string{"user_123", "user_456"}, } params.Limit = clerk.Int64(1) params.Offset = clerk.Int64(2) diff --git a/organization_membership.go b/organization_membership.go new file mode 100644 index 00000000..174e79d4 --- /dev/null +++ b/organization_membership.go @@ -0,0 +1,32 @@ +package clerk + +import "encoding/json" + +type OrganizationMembership struct { + APIResource + Object string `json:"object"` + ID string `json:"id"` + Organization *Organization `json:"organization"` + Permissions []string `json:"permissions"` + PublicMetadata json.RawMessage `json:"public_metadata"` + PrivateMetadata json.RawMessage `json:"private_metadata"` + Role string `json:"role"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` + PublicUserData *OrganizationMembershipPublicUserData `json:"public_user_data,omitempty"` +} + +type OrganizationMembershipList struct { + APIResource + OrganizationMemberships []*OrganizationMembership `json:"data"` + TotalCount int64 `json:"total_count"` +} + +type OrganizationMembershipPublicUserData struct { + UserID string `json:"user_id"` + FirstName *string `json:"first_name"` + LastName *string `json:"last_name"` + ImageURL *string `json:"image_url"` + HasImage bool `json:"has_image"` + Identifier string `json:"identifier"` +} diff --git a/organizationmembership/api.go b/organizationmembership/api.go new file mode 100644 index 00000000..0d8594fc --- /dev/null +++ b/organizationmembership/api.go @@ -0,0 +1,36 @@ +// Code generated by "gen"; DO NOT EDIT. +// This file is meant to be re-generated in place and/or deleted at any time. +// Last generated at 2024-02-08 13:01:58.273814248 +0000 UTC +package organizationmembership + +import ( + "context" + + "github.com/clerk/clerk-sdk-go/v2" +) + +// Create adds a new member to the organization. +func Create(ctx context.Context, params *CreateParams) (*clerk.OrganizationMembership, error) { + return getClient().Create(ctx, params) +} + +// Update updates an organization membership. +func Update(ctx context.Context, params *UpdateParams) (*clerk.OrganizationMembership, error) { + return getClient().Update(ctx, params) +} + +// Delete removes a member from an organization. +func Delete(ctx context.Context, params *DeleteParams) (*clerk.OrganizationMembership, error) { + return getClient().Delete(ctx, params) +} + +// List returns a list of organization memberships. +func List(ctx context.Context, params *ListParams) (*clerk.OrganizationMembershipList, error) { + return getClient().List(ctx, params) +} + +func getClient() *Client { + return &Client{ + Backend: clerk.GetBackend(), + } +} diff --git a/organizationmembership/client.go b/organizationmembership/client.go new file mode 100644 index 00000000..1bcfe86a --- /dev/null +++ b/organizationmembership/client.go @@ -0,0 +1,145 @@ +// Package organizationmembership provides the Organization Memberships API. +package organizationmembership + +import ( + "context" + "net/http" + "net/url" + + "github.com/clerk/clerk-sdk-go/v2" +) + +//go:generate go run ../cmd/gen/main.go + +const path = "/organizations" + +// Client is used to invoke the Organization Memberships API. +type Client struct { + Backend clerk.Backend +} + +type ClientConfig struct { + clerk.BackendConfig +} + +func NewClient(config *ClientConfig) *Client { + return &Client{ + Backend: clerk.NewBackend(&config.BackendConfig), + } +} + +type CreateParams struct { + clerk.APIParams + UserID *string `json:"user_id,omitempty"` + Role *string `json:"role,omitempty"` + OrganizationID string `json:"-"` +} + +// Create adds a new member to the organization. +func (c *Client) Create(ctx context.Context, params *CreateParams) (*clerk.OrganizationMembership, error) { + path, err := clerk.JoinPath(path, params.OrganizationID, "/memberships") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPost, path) + req.SetParams(params) + membership := &clerk.OrganizationMembership{} + err = c.Backend.Call(ctx, req, membership) + return membership, err +} + +type UpdateParams struct { + clerk.APIParams + Role *string `json:"role,omitempty"` + OrganizationID string `json:"-"` + UserID string `json:"-"` +} + +// Update updates an organization membership. +func (c *Client) Update(ctx context.Context, params *UpdateParams) (*clerk.OrganizationMembership, error) { + path, err := clerk.JoinPath(path, params.OrganizationID, "/memberships", params.UserID) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPatch, path) + req.SetParams(params) + membership := &clerk.OrganizationMembership{} + err = c.Backend.Call(ctx, req, membership) + return membership, err +} + +type DeleteParams struct { + clerk.APIParams + OrganizationID string `json:"-"` + UserID string `json:"-"` +} + +// Delete removes a member from an organization. +func (c *Client) Delete(ctx context.Context, params *DeleteParams) (*clerk.OrganizationMembership, error) { + path, err := clerk.JoinPath(path, params.OrganizationID, "/memberships", params.UserID) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodDelete, path) + membership := &clerk.OrganizationMembership{} + err = c.Backend.Call(ctx, req, membership) + return membership, err +} + +type ListParams struct { + clerk.APIParams + clerk.ListParams + OrderBy *string `json:"order_by,omitempty"` + Query *string `json:"query,omitempty"` + Roles []string `json:"role,omitempty"` + UserIDs []string `json:"user_id,omitempty"` + EmailAddresses []string `json:"email_address,omitempty"` + PhoneNumbers []string `json:"phone_number,omitempty"` + Usernames []string `json:"username,omitempty"` + Web3Wallets []string `json:"web3_wallet,omitempty"` + OrganizationID string `json:"-"` +} + +// ToQuery returns the parameters as url.Values so they can be used +// in a URL query string. +func (params *ListParams) ToQuery() url.Values { + q := params.ListParams.ToQuery() + if params.OrderBy != nil { + q.Set("order_by", *params.OrderBy) + } + if params.Query != nil { + q.Set("query", *params.Query) + } + if params.Roles != nil { + q["role"] = params.Roles + } + if params.UserIDs != nil { + q["user_id"] = params.UserIDs + } + if params.EmailAddresses != nil { + q["email_address"] = params.EmailAddresses + } + if params.PhoneNumbers != nil { + q["phone_number"] = params.PhoneNumbers + } + if params.Usernames != nil { + q["username"] = params.Usernames + } + if params.Web3Wallets != nil { + q["web3_wallet"] = params.Web3Wallets + } + return q +} + +// List returns a list of organization memberships. +func (c *Client) List(ctx context.Context, params *ListParams) (*clerk.OrganizationMembershipList, error) { + path, err := clerk.JoinPath(path, params.OrganizationID, "/memberships") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodGet, path) + req.SetParams(params) + list := &clerk.OrganizationMembershipList{} + err = c.Backend.Call(ctx, req, list) + return list, err +} diff --git a/organizationmembership/client_test.go b/organizationmembership/client_test.go new file mode 100644 index 00000000..a933a082 --- /dev/null +++ b/organizationmembership/client_test.go @@ -0,0 +1,207 @@ +package organizationmembership + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + + "github.com/clerk/clerk-sdk-go/v2" + "github.com/clerk/clerk-sdk-go/v2/clerktest" + "github.com/stretchr/testify/require" +) + +func TestOrganizationMembershipClientCreate(t *testing.T) { + t.Parallel() + id := "orgmem_123" + organizationID := "org_123" + userID := "user_123" + role := "admin" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"user_id":"%s","role":"%s"}`, userID, role)), + Out: json.RawMessage(fmt.Sprintf(`{ +"id":"%s", +"role":"%s", +"organization":{"id":"%s"}, +"public_user_data":{"user_id":"%s"} +}`, + id, role, organizationID, userID)), + Method: http.MethodPost, + Path: "/v1/organizations/" + organizationID + "/memberships", + }, + } + client := NewClient(config) + membership, err := client.Create(context.Background(), &CreateParams{ + UserID: clerk.String(userID), + Role: clerk.String(role), + OrganizationID: organizationID, + }) + require.NoError(t, err) + require.Equal(t, id, membership.ID) + require.Equal(t, role, membership.Role) + require.Equal(t, organizationID, membership.Organization.ID) + require.Equal(t, userID, membership.PublicUserData.UserID) +} + +func TestOrganizationMembershipClientCreate_Error(t *testing.T) { + t.Parallel() + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Status: http.StatusBadRequest, + Out: json.RawMessage(`{ + "errors":[{ + "code":"create-error-code" + }], + "clerk_trace_id":"create-trace-id" +}`), + }, + } + client := NewClient(config) + _, err := client.Create(context.Background(), &CreateParams{}) + require.Error(t, err) + apiErr, ok := err.(*clerk.APIErrorResponse) + require.True(t, ok) + require.Equal(t, "create-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + require.Equal(t, "create-error-code", apiErr.Errors[0].Code) +} + +func TestOrganizationMembershipClientUpdate(t *testing.T) { + t.Parallel() + id := "orgmem_123" + organizationID := "org_123" + userID := "user_123" + role := "admin" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"role":"%s"}`, role)), + Out: json.RawMessage(fmt.Sprintf(`{ +"id":"%s", +"role":"%s", +"organization":{"id":"%s"}, +"public_user_data":{"user_id":"%s"} +}`, + id, role, organizationID, userID)), + Method: http.MethodPatch, + Path: "/v1/organizations/" + organizationID + "/memberships/" + userID, + }, + } + client := NewClient(config) + membership, err := client.Update(context.Background(), &UpdateParams{ + Role: clerk.String(role), + OrganizationID: organizationID, + UserID: userID, + }) + require.NoError(t, err) + require.Equal(t, id, membership.ID) + require.Equal(t, role, membership.Role) + require.Equal(t, organizationID, membership.Organization.ID) + require.Equal(t, userID, membership.PublicUserData.UserID) +} + +func TestOrganizationMembershipClientUpdate_Error(t *testing.T) { + t.Parallel() + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Status: http.StatusBadRequest, + Out: json.RawMessage(`{ + "errors":[{ + "code":"update-error-code" + }], + "clerk_trace_id":"update-trace-id" +}`), + }, + } + client := NewClient(config) + _, err := client.Update(context.Background(), &UpdateParams{}) + require.Error(t, err) + apiErr, ok := err.(*clerk.APIErrorResponse) + require.True(t, ok) + require.Equal(t, "update-trace-id", apiErr.TraceID) + require.Equal(t, 1, len(apiErr.Errors)) + require.Equal(t, "update-error-code", apiErr.Errors[0].Code) +} + +func TestOrganizationMembershipClientDelete(t *testing.T) { + t.Parallel() + id := "orgmem_123" + organizationID := "org_123" + userID := "user_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{ +"id":"%s", +"organization":{"id":"%s"}, +"public_user_data":{"user_id":"%s"} +}`, + id, organizationID, userID)), + Method: http.MethodDelete, + Path: "/v1/organizations/" + organizationID + "/memberships/" + userID, + }, + } + client := NewClient(config) + membership, err := client.Delete(context.Background(), &DeleteParams{ + UserID: userID, + OrganizationID: organizationID, + }) + require.NoError(t, err) + require.Equal(t, id, membership.ID) + require.Equal(t, organizationID, membership.Organization.ID) + require.Equal(t, userID, membership.PublicUserData.UserID) +} + +func TestOrganizationMembershipClientList(t *testing.T) { + t.Parallel() + id := "orgmem_123" + organizationID := "org_123" + userID := "user_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{ +"data": [{ + "id":"%s", + "organization":{"id":"%s"}, + "public_user_data":{"user_id":"%s"} +}], +"total_count": 1 +}`, + id, organizationID, userID)), + Method: http.MethodGet, + Path: "/v1/organizations/" + organizationID + "/memberships", + Query: &url.Values{ + "limit": []string{"1"}, + "offset": []string{"2"}, + "role": []string{"admin", "member"}, + "order_by": []string{"-created_at"}, + }, + }, + } + client := NewClient(config) + params := &ListParams{ + OrganizationID: organizationID, + OrderBy: clerk.String("-created_at"), + Roles: []string{"admin", "member"}, + } + params.Limit = clerk.Int64(1) + params.Offset = clerk.Int64(2) + list, err := client.List(context.Background(), params) + require.NoError(t, err) + require.Equal(t, id, list.OrganizationMemberships[0].ID) + require.Equal(t, organizationID, list.OrganizationMemberships[0].Organization.ID) + require.Equal(t, userID, list.OrganizationMemberships[0].PublicUserData.UserID) +}