Repository: meilisearch/meilisearch-go

Issue:
[v1.17.0] Add support for webhook API
## Description

We want to add the necessary methods in this SDK to interact with the **Webhook API**. This feature is introduced in Meilisearch 1.17.0, [see releases](https://github.com/meilisearch/meilisearch/releases).

For more context, please see:
- [the usage guide](https://www.notion.so/meilisearch/Usage-for-webhook-API-23f4b06b651f8054b6fed3ca02641770)
- the related issue in Meilisearch: https://github.com/meilisearch/meilisearch/issues/5792

## Tasks

- [ ] Add the new methods to interact with the Meilisearch API
- [ ] Add new tests cases
- [ ] Add code samples in [.code-samples.meilisearch.yaml](../blob/main/.code-samples.meilisearch.yaml) for the following keys: `webhooks_get_1`, `webhooks_get_single_1`, `webhooks_post_1`, `webhooks_patch_1`, `webhooks_delete_1`. They should perform the same action as the curl calls in [Meilisearch docs](https://github.com/meilisearch/documentation/pull/3336/files#diff-65d25ef75b16ef426e9e9d93d4ab97e174f75dd16ca8e4c0d7f4b7ca03173516)

Discussion:

Solution patch:
diff --git a/.code-samples.meilisearch.yaml b/.code-samples.meilisearch.yaml
index e08ed64..cf3e972 100644
--- a/.code-samples.meilisearch.yaml
+++ b/.code-samples.meilisearch.yaml
@@ -1016,3 +1016,23 @@ search_parameter_reference_media_1: |-
       },
     },
   });
+webhooks_get_1: |-
+  client.ListWebhooks();
+webhooks_get_single_1: |-
+  client.GetWebhook("WEBHOOK_UUID");
+webhooks_post_1: |-
+  client.AddWebhook(&meilisearch.AddWebhookRequest{
+    URL: "WEBHOOK_TARGET_URL",
+    Headers: map[string]string{
+      "authorization": "SECURITY_KEY",
+      "referer": "https://example.com"
+    },
+  });
+webhooks_patch_1: |-
+  client.UpdateWebhook("WEBHOOK_UUID", &meilisearch.UpdateWebhookRequest{
+    Header: map[string]string{
+      "referer": ""
+    },
+  });
+webhooks_delete_1: |-
+  client.DeleteWebhook("WEBHOOK_UUID");
diff --git a/meilisearch.go b/meilisearch.go
index afa3996..f1cfd9d 100644
--- a/meilisearch.go
+++ b/meilisearch.go
@@ -77,6 +77,10 @@ func (m *meilisearch) KeyReader() KeyReader {
 	return m
 }
 
+func (m *meilisearch) WebhookManager() WebhookManager { return m }
+
+func (m *meilisearch) WebhookReader() WebhookReader { return m }
+
 func (m *meilisearch) ChatManager() ChatManager { return m }
 
 func (m *meilisearch) ChatReader() ChatReader { return m }
diff --git a/meilisearch_interface.go b/meilisearch_interface.go
index 4dccc17..99c9fee 100644
--- a/meilisearch_interface.go
+++ b/meilisearch_interface.go
@@ -11,6 +11,7 @@ type ServiceManager interface {
 	TaskManager
 	ChatManager
 	ChatReader
+	WebhookManager
 
 	ServiceReader() ServiceReader
 
@@ -23,6 +24,9 @@ type ServiceManager interface {
 	ChatManager() ChatManager
 	ChatReader() ChatReader
 
+	WebhookManager() WebhookManager
+	WebhookReader() WebhookReader
+
 	// CreateIndex creates a new index.
 	CreateIndex(config *IndexConfig) (*TaskInfo, error)
 
diff --git a/types.go b/types.go
index 578c434..b943afc 100644
--- a/types.go
+++ b/types.go
@@ -14,6 +14,27 @@ const (
 	nullBody                 = "null"
 )
 
+type UpdateWebhookRequest struct {
+	URL     string            `json:"url,omitempty"`
+	Headers map[string]string `json:"headers,omitempty"`
+}
+
+type Webhook struct {
+	UUID       string            `json:"uuid"`
+	IsEditable bool              `json:"isEditable"`
+	URL        string            `json:"url"`
+	Headers    map[string]string `json:"headers"`
+}
+
+type WebhookResults struct {
+	Result []*Webhook `json:"results"`
+}
+
+type AddWebhookRequest struct {
+	URL     string            `json:"url"`
+	Headers map[string]string `json:"headers,omitempty"`
+}
+
 type IndexConfig struct {
 	// Uid is the unique identifier of a given index.
 	Uid string
diff --git a/webhooks.go b/webhooks.go
new file mode 100644
index 0000000..5dc76b9
--- /dev/null
+++ b/webhooks.go
@@ -0,0 +1,105 @@
+package meilisearch
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+)
+
+func (m *meilisearch) AddWebhook(params *AddWebhookRequest) (*Webhook, error) {
+	return m.AddWebhookWithContext(context.Background(), params)
+}
+
+func (m *meilisearch) AddWebhookWithContext(ctx context.Context, params *AddWebhookRequest) (*Webhook, error) {
+	resp := new(Webhook)
+	req := &internalRequest{
+		endpoint:            "/webhooks",
+		method:              http.MethodPost,
+		withRequest:         params,
+		contentType:         contentTypeJSON,
+		withResponse:        resp,
+		acceptedStatusCodes: []int{http.StatusCreated, http.StatusOK},
+		functionName:        "AddWebhook",
+	}
+	if err := m.client.executeRequest(ctx, req); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func (m *meilisearch) ListWebhooks() (*WebhookResults, error) {
+	return m.ListWebhooksWithContext(context.Background())
+}
+
+func (m *meilisearch) ListWebhooksWithContext(ctx context.Context) (*WebhookResults, error) {
+	resp := new(WebhookResults)
+	req := &internalRequest{
+		endpoint:            "/webhooks",
+		method:              http.MethodGet,
+		withRequest:         nil,
+		withResponse:        resp,
+		acceptedStatusCodes: []int{http.StatusOK},
+		functionName:        "ListWebhooks",
+	}
+	if err := m.client.executeRequest(ctx, req); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func (m *meilisearch) GetWebhook(uuid string) (*Webhook, error) {
+	return m.GetWebhookWithContext(context.Background(), uuid)
+}
+
+func (m *meilisearch) GetWebhookWithContext(ctx context.Context, uuid string) (*Webhook, error) {
+	resp := new(Webhook)
+	req := &internalRequest{
+		endpoint:            fmt.Sprintf("/webhooks/%s", uuid),
+		method:              http.MethodGet,
+		withRequest:         nil,
+		withResponse:        resp,
+		acceptedStatusCodes: []int{http.StatusOK},
+		functionName:        "GetWebhook",
+	}
+	if err := m.client.executeRequest(ctx, req); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func (m *meilisearch) UpdateWebhook(uuid string, params *UpdateWebhookRequest) (*Webhook, error) {
+	return m.UpdateWebhookWithContext(context.Background(), uuid, params)
+}
+
+func (m *meilisearch) UpdateWebhookWithContext(ctx context.Context, uuid string, params *UpdateWebhookRequest) (*Webhook, error) {
+	resp := new(Webhook)
+	req := &internalRequest{
+		endpoint:            fmt.Sprintf("/webhooks/%s", uuid),
+		method:              http.MethodPatch,
+		withRequest:         params,
+		contentType:         contentTypeJSON,
+		withResponse:        resp,
+		acceptedStatusCodes: []int{http.StatusOK},
+		functionName:        "UpdateWebhook",
+	}
+	if err := m.client.executeRequest(ctx, req); err != nil {
+		return nil, err
+	}
+	return resp, nil
+}
+
+func (m *meilisearch) DeleteWebhook(uuid string) error {
+	return m.DeleteWebhookWithContext(context.Background(), uuid)
+}
+
+func (m *meilisearch) DeleteWebhookWithContext(ctx context.Context, uuid string) error {
+	req := &internalRequest{
+		endpoint:            fmt.Sprintf("/webhooks/%s", uuid),
+		method:              http.MethodDelete,
+		withRequest:         nil,
+		withResponse:        nil,
+		acceptedStatusCodes: []int{http.StatusNoContent},
+		functionName:        "DeleteWebhook",
+	}
+	return m.client.executeRequest(ctx, req)
+}
diff --git a/webhooks_interface.go b/webhooks_interface.go
new file mode 100644
index 0000000..5bced28
--- /dev/null
+++ b/webhooks_interface.go
@@ -0,0 +1,39 @@
+package meilisearch
+
+import "context"
+
+type WebhookManager interface {
+	WebhookReader
+
+	// AddWebhook add a new webhook to meilisearch.
+	AddWebhook(params *AddWebhookRequest) (*Webhook, error)
+	// AddWebhookWithContext add a new webhook to meilisearch with a context.
+	AddWebhookWithContext(ctx context.Context, params *AddWebhookRequest) (*Webhook, error)
+
+	// UpdateWebhook modifies a previously existing webhook.
+	// If the webhook has isEditable to false the HTTP call returns an error.
+	UpdateWebhook(uuid string, params *UpdateWebhookRequest) (*Webhook, error)
+	// UpdateWebhookWithContext modifies a previously existing webhook with a context.
+	// If the webhook has isEditable to false the HTTP call returns an error.
+	UpdateWebhookWithContext(ctx context.Context, uuid string, params *UpdateWebhookRequest) (*Webhook, error)
+
+	// DeleteWebhook deletes an existing webhook. Will also fail when the webhook doesn’t exist.
+	// If the webhook has isEditable to false the HTTP call returns an error.
+	DeleteWebhook(uuid string) error
+
+	// DeleteWebhookWithContext deletes an existing webhook with a context. Will also fail when the webhook doesn’t exist.
+	// If the webhook has isEditable to false the HTTP call returns an error.
+	DeleteWebhookWithContext(ctx context.Context, uuid string) error
+}
+
+type WebhookReader interface {
+	// ListWebhooks lists all the webhooks.
+	ListWebhooks() (*WebhookResults, error)
+	// ListWebhooksWithContext lists all the webhooks with context.
+	ListWebhooksWithContext(ctx context.Context) (*WebhookResults, error)
+
+	// GetWebhook gets a webhook by uuid.
+	GetWebhook(uuid string) (*Webhook, error)
+	// GetWebhookWithContext gets a webhook by uuid with a context.
+	GetWebhookWithContext(ctx context.Context, uuid string) (*Webhook, error)
+}



Test patch:
diff --git a/integration/main_test.go b/integration/main_test.go
index 8bf55c3..8679205 100644
--- a/integration/main_test.go
+++ b/integration/main_test.go
@@ -114,6 +114,14 @@ func cleanupChat(services ...meilisearch.ChatManager) func() {
 	}
 }
 
+func cleanupWebhook(services ...meilisearch.WebhookManager) func() {
+	return func() {
+		for _, s := range services {
+			_, _ = deleteAllWebhooks(s)
+		}
+	}
+}
+
 func getPrivateKey(sv meilisearch.ServiceManager) (key string) {
 	list, err := sv.GetKeys(nil)
 	if err != nil {
@@ -157,6 +165,19 @@ func deleteAllIndexes(sv meilisearch.ServiceManager) (ok bool, err error) {
 	return true, nil
 }
 
+func deleteAllWebhooks(webhookMgr meilisearch.WebhookManager) (ok bool, err error) {
+	list, err := webhookMgr.ListWebhooks()
+	if err != nil {
+		return false, err
+	}
+	for _, webhook := range list.Result {
+		if err := webhookMgr.DeleteWebhook(webhook.UUID); err != nil {
+			return false, err
+		}
+	}
+	return true, nil
+}
+
 func deleteAllChatWorkspaces(chatMgr meilisearch.ChatManager) (ok bool, err error) {
 	list, err := chatMgr.ListChatWorkspaces(&meilisearch.ListChatWorkSpaceQuery{
 		Limit:  1000,
diff --git a/integration/webhook_test.go b/integration/webhook_test.go
new file mode 100644
index 0000000..c28cf7f
--- /dev/null
+++ b/integration/webhook_test.go
@@ -0,0 +1,91 @@
+package integration
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/meilisearch/meilisearch-go"
+	"github.com/stretchr/testify/require"
+)
+
+func Test_AddWebhook(t *testing.T) {
+	sv := setup(t, "")
+	t.Cleanup(cleanupWebhook(sv))
+
+	webhook := &meilisearch.AddWebhookRequest{
+		URL:     "http://example.com",
+		Headers: map[string]string{"FOO": "BAR", "BAR": ""},
+	}
+	result, err := sv.AddWebhook(webhook)
+	require.NoError(t, err)
+	require.Equal(t, webhook.URL, result.URL)
+	require.Equal(t, webhook.Headers, result.Headers)
+	require.True(t, result.IsEditable)
+	require.NotZero(t, result.UUID)
+}
+
+func Test_GetWebhook(t *testing.T) {
+	sv := setup(t, "")
+	t.Cleanup(cleanupWebhook(sv))
+
+	wb, err := sv.AddWebhook(&meilisearch.AddWebhookRequest{
+		URL:     "http://example.com",
+		Headers: map[string]string{"FOO": "BAR"},
+	})
+	require.NoError(t, err)
+	got, err := sv.GetWebhook(wb.UUID)
+	require.NoError(t, err)
+	require.Equal(t, wb, got)
+}
+
+func Test_UpdateWebhook(t *testing.T) {
+	sv := setup(t, "")
+	t.Cleanup(cleanupWebhook(sv))
+
+	wb, err := sv.AddWebhook(&meilisearch.AddWebhookRequest{
+		URL:     "http://example.com",
+		Headers: map[string]string{"FOO": "BAR"},
+	})
+	require.NoError(t, err)
+
+	updatedWb, err := sv.UpdateWebhook(wb.UUID, &meilisearch.UpdateWebhookRequest{
+		URL:     "http://update.com",
+		Headers: map[string]string{"FOO": "UPDATED", "BAR": ""},
+	})
+	require.NoError(t, err)
+
+	got, err := sv.GetWebhook(wb.UUID)
+	require.NoError(t, err)
+	require.Equal(t, updatedWb, got)
+}
+
+func Test_ListWebhooks(t *testing.T) {
+	sv := setup(t, "")
+	t.Cleanup(cleanupWebhook(sv))
+
+	n := 5
+	for i := 0; i < n; i++ {
+		_, err := sv.AddWebhook(&meilisearch.AddWebhookRequest{
+			URL:     fmt.Sprintf("http://example_%d.com", i),
+			Headers: map[string]string{"FOO": "BAR"},
+		})
+		require.NoError(t, err)
+	}
+	result, err := sv.ListWebhooks()
+	require.NoError(t, err)
+	require.Len(t, result.Result, n)
+}
+
+func Test_DeleteWebhook(t *testing.T) {
+	sv := setup(t, "")
+	t.Cleanup(cleanupWebhook(sv))
+
+	wb, err := sv.AddWebhook(&meilisearch.AddWebhookRequest{
+		URL:     "http://example.com",
+		Headers: map[string]string{"FOO": "BAR"},
+	})
+	require.NoError(t, err)
+
+	err = sv.DeleteWebhook(wb.UUID)
+	require.NoError(t, err)
+}