Skip to content

Commit

Permalink
Support for updating collections via API and CLI (#70)
Browse files Browse the repository at this point in the history
  • Loading branch information
StefMa authored Feb 14, 2024
1 parent 0f888da commit f1302fa
Show file tree
Hide file tree
Showing 6 changed files with 267 additions and 0 deletions.
1 change: 1 addition & 0 deletions cli/docs/outcli_collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

34 changes: 34 additions & 0 deletions cli/docs/outcli_collection_update.md
Original file line number Diff line number Diff line change
@@ -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

70 changes: 70 additions & 0 deletions collections.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
63 changes: 63 additions & 0 deletions internal/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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())
Expand Down
4 changes: 4 additions & 0 deletions internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ func CollectionsCreateEndpoint() string {
return "collections.create"
}

func CollectionsUpdateEndpoint() string {
return "collections.update"
}

func DocumentsGetEndpoint() string {
return "documents.info"
}
Expand Down
95 changes: 95 additions & 0 deletions package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down Expand Up @@ -709,6 +802,8 @@ const exampleCollectionsGetResponse string = `{
}
}`

const exampleCollectionsUpdateResponse string = exampleCollectionsGetResponse

const exampleCollectionsListResponse_2collections string = `
{
"data": [
Expand Down

0 comments on commit f1302fa

Please sign in to comment.