diff --git a/cli/docs/outcli_collection.md b/cli/docs/outcli_collection.md index b221ace..ba16010 100644 --- a/cli/docs/outcli_collection.md +++ b/cli/docs/outcli_collection.md @@ -26,4 +26,5 @@ If you have to work with collection in any case, use this command * [outcli collection docs](outcli_collection_docs.md) - Get document structure * [outcli collection info](outcli_collection_info.md) - Get collection info * [outcli collection list](outcli_collection_list.md) - List all collections +* [outcli collection update](outcli_collection_update.md) - Update an existing collection diff --git a/cli/docs/outcli_collection_update.md b/cli/docs/outcli_collection_update.md new file mode 100644 index 0000000..90d834d --- /dev/null +++ b/cli/docs/outcli_collection_update.md @@ -0,0 +1,34 @@ +## outcli collection update + +Update an existing collection + +### Synopsis + +Update an existing collection's name, description etc. properties + +``` +outcli collection update [flags] +``` + +### Options + +``` + --color string The color of the collection. Should be in the format of #AABBCC + --description string The description of the collection + -h, --help help for update + --name string The name of the collection + --permission-read Change the permission to read only + --permission-read-write Change the permission to read write +``` + +### Options inherited from parent commands + +``` + --key string The outline api key + --server string The outline API server url +``` + +### SEE ALSO + +* [outcli collection](outcli_collection.md) - Work with collections + diff --git a/collections.go b/collections.go index e889052..bc4b6af 100644 --- a/collections.go +++ b/collections.go @@ -37,6 +37,12 @@ func (cl *CollectionsClient) Create(name string) *CollectionsCreateClient { return newCollectionsCreateClient(cl.sl, name) } +// Update returns a client for updating a collection. +// API reference: https://www.getoutline.com/developers#tag/Collections/paths/~1collections.update/post +func (cl *CollectionsClient) Update(id CollectionID) *CollectionsUpdateClient { + return newCollectionsUpdateClient(cl.sl, id) +} + type CollectionsDocumentStructureClient struct { sl *rsling.Sling } @@ -230,3 +236,67 @@ func (cl *CollectionsCreateClient) Do(ctx context.Context) (*Collection, error) return success.Data, nil } + +// collectionsUpdateParams represents the Outline Collections.update parameters +type collectionsUpdateParams struct { + ID CollectionID `json:"id"` + Name string `json:"name"` + Permission string `json:"permission,omitempty"` + Description string `json:"description,omitempty"` + Color string `json:"color,omitempty"` +} + +type CollectionsUpdateClient struct { + sl *rsling.Sling + params collectionsUpdateParams +} + +func newCollectionsUpdateClient(sl *rsling.Sling, id CollectionID) *CollectionsUpdateClient { + copy := sl.New() + params := collectionsUpdateParams{ID: id} + return &CollectionsUpdateClient{sl: copy, params: params} +} + +func (cl *CollectionsUpdateClient) Name(name string) *CollectionsUpdateClient { + cl.params.Name = name + return cl +} + +func (cl *CollectionsUpdateClient) PermissionRead() *CollectionsUpdateClient { + cl.params.Permission = "read" + return cl +} + +func (cl *CollectionsUpdateClient) PermissionReadWrite() *CollectionsUpdateClient { + cl.params.Permission = "read_write" + return cl +} + +func (cl *CollectionsUpdateClient) Color(color string) *CollectionsUpdateClient { + cl.params.Color = color + return cl +} + +func (cl *CollectionsUpdateClient) Description(desc string) *CollectionsUpdateClient { + cl.params.Description = desc + return cl +} + +// Do makes the actual request for updating the collection. +func (cl *CollectionsUpdateClient) Do(ctx context.Context) (*Collection, error) { + cl.sl.Post(common.CollectionsUpdateEndpoint()).BodyJSON(&cl.params) + + success := &struct { + Data *Collection `json:"data"` + }{} + + br, err := request(ctx, cl.sl, success) + if err != nil { + return nil, fmt.Errorf("failed making HTTP request: %w", err) + } + if br != nil { + return nil, fmt.Errorf("bad response: %w", &apiError{br: *br}) + } + + return success.Data, nil +} diff --git a/internal/cli/cli.go b/internal/cli/cli.go index 44004e7..f68a3fb 100644 --- a/internal/cli/cli.go +++ b/internal/cli/cli.go @@ -121,6 +121,7 @@ func Command() *cobra.Command { collectionCmd.AddCommand(collectionCreateCmd) collectionCmd.AddCommand(collectionDocumentsCmd) collectionCmd.AddCommand(collectionListCmd) + collectionCmd.AddCommand(collectionUpdate()) rootCmd.AddCommand(documentCmd) documentCmd.AddCommand(documentCreateCmd) documentCmd.AddCommand(documentGetCmd) @@ -200,6 +201,68 @@ func collectionList(serverUrl string, apiKey string) error { return nil } +func collectionUpdate() *cobra.Command { + var name, description, color string + var permissionRead, permissionReadWrite bool + + cmd := &cobra.Command{ + Use: "update", + Short: "Update an existing collection", + Long: "Update an existing collection's name, description etc. properties", + Args: cobra.MinimumNArgs(1), + RunE: func(c *cobra.Command, args []string) error { + id := args[0] + errBase := fmt.Sprintf("failed updating collection with ID '%s'", id) + + // Extract value of global flags + key, err := c.Flags().GetString(flagApiKey) + if err != nil { + return fmt.Errorf("%s: %w", errBase, err) + } + url, err := c.Flags().GetString(flagServerURL) + if err != nil { + return fmt.Errorf("%s: %w", errBase, err) + } + + cl := outline.New(url, &http.Client{}, key). + Collections(). + Update(outline.CollectionID(id)). + Name(name). + Description(description). + Color(color) + + if permissionRead { + cl.PermissionRead() + } + if permissionReadWrite { + cl.PermissionReadWrite() + } + + doc, err := cl.Do(context.Background()) + if err != nil { + return fmt.Errorf("%s: %w", errBase, err) + } + + b, err := json.MarshalIndent(doc, "", " ") + if err != nil { + return fmt.Errorf("failed marshalling collection with ID '%s': %w", id, err) + } + fmt.Println(string(b)) + + return nil + }, + } + + cmd.Flags().StringVar(&name, "name", "", "The name of the collection") + cmd.Flags().StringVar(&description, "description", "", "The description of the collection") + cmd.Flags().StringVar(&color, "color", "", "The color of the collection. Should be in the format of #AABBCC") + cmd.Flags().BoolVar(&permissionRead, "permission-read", false, "Change the permission to read only") + cmd.Flags().BoolVar(&permissionReadWrite, "permission-read-write", false, "Change the permission to read write") + cmd.MarkFlagsMutuallyExclusive("permission-read", "permission-read-write") + + return cmd +} + func documentCreate(serverUrl string, apiKey string, name string, collectionId outline.CollectionID) error { oc := outline.New(serverUrl, &http.Client{}, apiKey) doc, err := oc.Documents().Create(name, collectionId).Do(context.Background()) diff --git a/internal/common/common.go b/internal/common/common.go index cb4b274..7702134 100644 --- a/internal/common/common.go +++ b/internal/common/common.go @@ -34,6 +34,10 @@ func CollectionsCreateEndpoint() string { return "collections.create" } +func CollectionsUpdateEndpoint() string { + return "collections.update" +} + func DocumentsGetEndpoint() string { return "documents.info" } diff --git a/package_test.go b/package_test.go index d0f6c4b..c48a6ce 100644 --- a/package_test.go +++ b/package_test.go @@ -203,6 +203,99 @@ func TestClientCollectionsGet(t *testing.T) { assert.Equal(t, &expected.Data, got) } +func TestClientCollectionsUpdate_failed(t *testing.T) { + tests := map[string]struct { + isTemporary bool + rt http.RoundTripper + }{ + "HTTP request failed": { + isTemporary: false, + rt: &testutils.MockRoundTripper{ + RoundTripFn: func(r *http.Request) (*http.Response, error) { + return nil, &net.DNSError{} + }, + }, + }, + "server side error": { + isTemporary: true, + rt: &testutils.MockRoundTripper{ + RoundTripFn: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + Request: r, + StatusCode: http.StatusServiceUnavailable, + ContentLength: -1, + Body: io.NopCloser(strings.NewReader("service unavailable")), + }, nil + }, + }, + }, + "client side error": { + isTemporary: false, + rt: &testutils.MockRoundTripper{ + RoundTripFn: func(r *http.Request) (*http.Response, error) { + return &http.Response{ + Request: r, + ContentLength: -1, + StatusCode: http.StatusUnauthorized, + Body: io.NopCloser(strings.NewReader("unauthorized key")), + }, nil + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + hc := &http.Client{} + hc.Transport = test.rt + cl := outline.New(testServerURL, hc, testApiKey) + col, err := cl.Collections().Update("collection id").Do(context.Background()) + assert.Nil(t, col) + require.NotNil(t, err) + assert.Equal(t, test.isTemporary, outline.IsTemporary(err)) + }) + } +} + +func TestClientCollectionsUpdate(t *testing.T) { + testResponse := exampleCollectionsUpdateResponse + + // Prepare HTTP client with mocked transport. + hc := &http.Client{} + hc.Transport = &testutils.MockRoundTripper{RoundTripFn: func(r *http.Request) (*http.Response, error) { + // Assert request method and URL. + assert.Equal(t, http.MethodPost, r.Method) + u, err := url.JoinPath(common.BaseURL(testServerURL), common.CollectionsUpdateEndpoint()) + require.NoError(t, err) + assert.Equal(t, u, r.URL.String()) + + testAssertHeaders(t, r.Header) + testAssertBody(t, r, fmt.Sprintf(`{"id":"%s", "name":"%s", "description":"%s"}`, "collection id", "NewName", "New Desc")) + + return &http.Response{ + Request: r, + ContentLength: -1, + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(testResponse)), + }, nil + }} + + cl := outline.New(testServerURL, hc, testApiKey) + got, err := cl.Collections(). + Update("collection id"). + Name("NewName"). + Description("New Desc"). + Do(context.Background()) + require.NoError(t, err) + + // Manually unmarshal test response and see if we get same object via the API. + expected := &struct { + Data outline.Collection `json:"data"` + }{} + require.NoError(t, json.Unmarshal([]byte(testResponse), expected)) + assert.Equal(t, &expected.Data, got) +} + func TestClientCollectionsList(t *testing.T) { requestCount := atomic.Uint32{} hc := &http.Client{} @@ -709,6 +802,8 @@ const exampleCollectionsGetResponse string = `{ } }` +const exampleCollectionsUpdateResponse string = exampleCollectionsGetResponse + const exampleCollectionsListResponse_2collections string = ` { "data": [