diff --git a/cmd/gen/main.go b/cmd/gen/main.go index c988b987..9079394b 100644 --- a/cmd/gen/main.go +++ b/cmd/gen/main.go @@ -59,7 +59,7 @@ func main() { // Gather all comments, they might be a method's // godoc. if strings.HasPrefix(line, "//") { - comments.WriteString(line) + comments.WriteString("\n" + line) } if line == "" { comments.Reset() diff --git a/organization.go b/organization.go new file mode 100644 index 00000000..289dc719 --- /dev/null +++ b/organization.go @@ -0,0 +1,28 @@ +package clerk + +import "encoding/json" + +type Organization struct { + APIResource + Object string `json:"object"` + ID string `json:"id"` + Name string `json:"name"` + Slug string `json:"slug"` + ImageURL *string `json:"image_url"` + HasImage bool `json:"has_image"` + MembersCount *int64 `json:"members_count,omitempty"` + PendingInvitationsCount *int64 `json:"pending_invitations_count,omitempty"` + MaxAllowedMemberships int64 `json:"max_allowed_memberships"` + AdminDeleteEnabled bool `json:"admin_delete_enabled"` + PublicMetadata json.RawMessage `json:"public_metadata"` + PrivateMetadata json.RawMessage `json:"private_metadata"` + CreatedBy string `json:"created_by"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} + +type OrganizationList struct { + APIResource + Organizations []*Organization `json:"data"` + TotalCount int64 `json:"total_count"` +} diff --git a/organization/api.go b/organization/api.go new file mode 100644 index 00000000..aa42e87d --- /dev/null +++ b/organization/api.go @@ -0,0 +1,53 @@ +// 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 07:09:19.854305486 +0000 UTC +package organization + +import ( + "context" + + "github.com/clerk/clerk-sdk-go/v2" +) + +// Create creates a new organization. +func Create(ctx context.Context, params *CreateParams) (*clerk.Organization, error) { + return getClient().Create(ctx, params) +} + +// Get retrieves details for an organization. +// The organization can be fetched by either the ID or its slug. +func Get(ctx context.Context, idOrSlug string) (*clerk.Organization, error) { + return getClient().Get(ctx, idOrSlug) +} + +// Update updates an organization. +func Update(ctx context.Context, id string, params *UpdateParams) (*clerk.Organization, error) { + return getClient().Update(ctx, id, params) +} + +// UpdateMetadata updates the organization's metadata by merging the +// provided values with the existing ones. +func UpdateMetadata(ctx context.Context, id string, params *UpdateMetadataParams) (*clerk.Organization, error) { + return getClient().UpdateMetadata(ctx, id, params) +} + +// Delete deletes an organization. +func Delete(ctx context.Context, id string) (*clerk.DeletedResource, error) { + return getClient().Delete(ctx, id) +} + +// DeleteLogo removes the organization's logo. +func DeleteLogo(ctx context.Context, id string) (*clerk.Organization, error) { + return getClient().DeleteLogo(ctx, id) +} + +// List returns a list of organizations. +func List(ctx context.Context, params *ListParams) (*clerk.OrganizationList, error) { + return getClient().List(ctx, params) +} + +func getClient() *Client { + return &Client{ + Backend: clerk.GetBackend(), + } +} diff --git a/organization/client.go b/organization/client.go new file mode 100644 index 00000000..51ae2b10 --- /dev/null +++ b/organization/client.go @@ -0,0 +1,167 @@ +// Package organization provides the Organizations API. +package organization + +import ( + "context" + "encoding/json" + "net/http" + "net/url" + "strconv" + + "github.com/clerk/clerk-sdk-go/v2" +) + +//go:generate go run ../cmd/gen/main.go + +const path = "/organizations" + +// Client is used to invoke the Organizations 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 + Name *string `json:"name,omitempty"` + Slug *string `json:"slug,omitempty"` + CreatedBy *string `json:"created_by,omitempty"` + MaxAllowedMemberships *int64 `json:"max_allowed_memberships,omitempty"` + PublicMetadata *json.RawMessage `json:"public_metadata,omitempty"` + PrivateMetadata *json.RawMessage `json:"private_metadata,omitempty"` +} + +// Create creates a new organization. +func (c *Client) Create(ctx context.Context, params *CreateParams) (*clerk.Organization, error) { + req := clerk.NewAPIRequest(http.MethodPost, path) + req.SetParams(params) + organization := &clerk.Organization{} + err := c.Backend.Call(ctx, req, organization) + return organization, err +} + +// Get retrieves details for an organization. +// The organization can be fetched by either the ID or its slug. +func (c *Client) Get(ctx context.Context, idOrSlug string) (*clerk.Organization, error) { + path, err := clerk.JoinPath(path, idOrSlug) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodGet, path) + organization := &clerk.Organization{} + err = c.Backend.Call(ctx, req, organization) + return organization, err +} + +type UpdateParams struct { + clerk.APIParams + Name *string `json:"name,omitempty"` + Slug *string `json:"slug,omitempty"` + MaxAllowedMemberships *int64 `json:"max_allowed_memberships,omitempty"` + PublicMetadata *json.RawMessage `json:"public_metadata,omitempty"` + PrivateMetadata *json.RawMessage `json:"private_metadata,omitempty"` + AdminDeleteEnabled *bool `json:"admin_delete_enabled,omitempty"` +} + +// Update updates an organization. +func (c *Client) Update(ctx context.Context, id string, params *UpdateParams) (*clerk.Organization, error) { + path, err := clerk.JoinPath(path, id) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPatch, path) + req.SetParams(params) + organization := &clerk.Organization{} + err = c.Backend.Call(ctx, req, organization) + return organization, err +} + +type UpdateMetadataParams struct { + clerk.APIParams + PublicMetadata *json.RawMessage `json:"public_metadata,omitempty"` + PrivateMetadata *json.RawMessage `json:"private_metadata,omitempty"` +} + +// UpdateMetadata updates the organization's metadata by merging the +// provided values with the existing ones. +func (c *Client) UpdateMetadata(ctx context.Context, id string, params *UpdateMetadataParams) (*clerk.Organization, error) { + path, err := clerk.JoinPath(path, id, "/metadata") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodPatch, path) + req.SetParams(params) + organization := &clerk.Organization{} + err = c.Backend.Call(ctx, req, organization) + return organization, err +} + +// Delete deletes an organization. +func (c *Client) Delete(ctx context.Context, id string) (*clerk.DeletedResource, error) { + path, err := clerk.JoinPath(path, id) + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodDelete, path) + organization := &clerk.DeletedResource{} + err = c.Backend.Call(ctx, req, organization) + return organization, err +} + +// DeleteLogo removes the organization's logo. +func (c *Client) DeleteLogo(ctx context.Context, id string) (*clerk.Organization, error) { + path, err := clerk.JoinPath(path, id, "/logo") + if err != nil { + return nil, err + } + req := clerk.NewAPIRequest(http.MethodDelete, path) + organization := &clerk.Organization{} + err = c.Backend.Call(ctx, req, organization) + return organization, err +} + +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"` +} + +// ToQuery returns query string values from the params. +func (params *ListParams) ToQuery() url.Values { + q := params.ListParams.ToQuery() + if params.IncludeMembersCount != nil { + q.Set("include_members_count", strconv.FormatBool(*params.IncludeMembersCount)) + } + if params.OrderBy != nil { + q.Set("order_by", *params.OrderBy) + } + if params.Query != nil { + q.Set("query", *params.Query) + } + if params.UserIDs != nil { + q["user_id"] = params.UserIDs + } + return q +} + +// List returns a list of organizations. +func (c *Client) List(ctx context.Context, params *ListParams) (*clerk.OrganizationList, error) { + req := clerk.NewAPIRequest(http.MethodGet, path) + req.SetParams(params) + list := &clerk.OrganizationList{} + err := c.Backend.Call(ctx, req, list) + return list, err +} diff --git a/organization/client_test.go b/organization/client_test.go new file mode 100644 index 00000000..f6503006 --- /dev/null +++ b/organization/client_test.go @@ -0,0 +1,226 @@ +package organization + +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 TestOrganizationClientCreate(t *testing.T) { + t.Parallel() + id := "org_123" + name := "Acme Inc" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"name":"%s"}`, name)), + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","name":"%s"}`, id, name)), + Method: http.MethodPost, + Path: "/v1/organizations", + }, + } + client := NewClient(config) + organization, err := client.Create(context.Background(), &CreateParams{ + Name: clerk.String(name), + }) + require.NoError(t, err) + require.Equal(t, id, organization.ID) + require.Equal(t, name, organization.Name) +} + +func TestOrganizationClientCreate_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 TestOrganizationClientGet(t *testing.T) { + t.Parallel() + id := "org_123" + name := "Acme Inc" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","name":"%s"}`, id, name)), + Method: http.MethodGet, + Path: "/v1/organizations/" + id, + }, + } + client := NewClient(config) + organization, err := client.Get(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, organization.ID) + require.Equal(t, name, organization.Name) +} + +func TestOrganizationClientUpdate(t *testing.T) { + t.Parallel() + id := "org_123" + name := "Acme Inc" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"name":"%s"}`, name)), + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","name":"%s"}`, id, name)), + Method: http.MethodPatch, + Path: "/v1/organizations/" + id, + }, + } + client := NewClient(config) + organization, err := client.Update(context.Background(), id, &UpdateParams{ + Name: clerk.String(name), + }) + require.NoError(t, err) + require.Equal(t, id, organization.ID) + require.Equal(t, name, organization.Name) +} + +func TestOrganizationClientUpdate_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(), "org_123", &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 TestOrganizationClientDelete(t *testing.T) { + t.Parallel() + id := "org_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","deleted":true}`, id)), + Method: http.MethodDelete, + Path: "/v1/organizations/" + id, + }, + } + client := NewClient(config) + organization, err := client.Delete(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, organization.ID) + require.True(t, organization.Deleted) +} + +func TestOrganizationClientList(t *testing.T) { + t.Parallel() + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(`{ +"data": [{"id":"org_123","name":"Acme Inc"}], +"total_count": 1 +}`), + Method: http.MethodGet, + Path: "/v1/organizations", + Query: &url.Values{ + "limit": []string{"1"}, + "offset": []string{"2"}, + "order_by": []string{"-created_at"}, + "query": []string{"Acme"}, + }, + }, + } + client := NewClient(config) + params := &ListParams{ + OrderBy: clerk.String("-created_at"), + Query: clerk.String("Acme"), + } + params.Limit = clerk.Int64(1) + params.Offset = clerk.Int64(2) + list, err := client.List(context.Background(), params) + require.NoError(t, err) + require.Equal(t, int64(1), list.TotalCount) + require.Equal(t, 1, len(list.Organizations)) + require.Equal(t, "org_123", list.Organizations[0].ID) + require.Equal(t, "Acme Inc", list.Organizations[0].Name) +} + +func TestOrganizationClientDeleteLogo(t *testing.T) { + t.Parallel() + id := "org_123" + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s"}`, id)), + Method: http.MethodDelete, + Path: "/v1/organizations/" + id + "/logo", + }, + } + client := NewClient(config) + organization, err := client.DeleteLogo(context.Background(), id) + require.NoError(t, err) + require.Equal(t, id, organization.ID) +} + +func TestOrganizationClientUpdateMetadata(t *testing.T) { + t.Parallel() + id := "org_123" + metadata := `{"foo":"bar"}` + config := &ClientConfig{} + config.HTTPClient = &http.Client{ + Transport: &clerktest.RoundTripper{ + T: t, + In: json.RawMessage(fmt.Sprintf(`{"private_metadata":%s}`, metadata)), + Out: json.RawMessage(fmt.Sprintf(`{"id":"%s","private_metadata":%s}`, id, metadata)), + Method: http.MethodPatch, + Path: "/v1/organizations/" + id + "/metadata", + }, + } + client := NewClient(config) + metadataParam := json.RawMessage(metadata) + organization, err := client.UpdateMetadata(context.Background(), id, &UpdateMetadataParams{ + PrivateMetadata: &metadataParam, + }) + require.NoError(t, err) + require.Equal(t, id, organization.ID) + require.JSONEq(t, metadata, string(organization.PrivateMetadata)) +}