Repository: meilisearch/meilisearch-go

Issue:
[v1.19.0] Add sharding support
## Context

We want to update the SDK to support [Meilisearch 1.19](https://github.com/meilisearch/meilisearch/releases/tag/v1.19.0) features.

## Description

Meilisearch now supports sharding to enable horizontal scaling. [Read the docs](https://www.meilisearch.com/docs/learn/multi_search/implement_sharding#implement-sharding-with-remote-federated-search-) for more context on implementation.

The following changes in Meilisearch API need to be reflected in this SDK:
- Network API (first introduced in [Meilisearch 1.13](https://www.meilisearch.com/blog/meilisearch-1-13)) now allows enabling sharding
- Network API responses now include `writeApiKey` in the `remotes` objects
- Tasks API responses now include `remotes` objects for tasks

## Tasks

- [ ] Update the Network methods to accept sending the `sharding` parameter
- [ ] Update the Network methods to include `remotes.[remoteName].writeApiKey` in the responses
- [ ] Update the Tasks methods to include `remotes` objects in the tasks reponse index update method to allow renaming
- [ ] Add new test cases to test implementation

Discussion:

Solution patch:
diff --git a/types.go b/types.go
index e49e7da..e40e3e0 100644
--- a/types.go
+++ b/types.go
@@ -18,8 +18,9 @@ const (
 // Each field is wrapped in an Opt so it can be explicitly included,
 // set to JSON null, or omitted entirely.
 type Network struct {
-	Self    Opt[string]                 `json:"self,omitempty"`
-	Remotes Opt[map[string]Opt[Remote]] `json:"remotes,omitempty"`
+	Self     Opt[string]                 `json:"self,omitempty"`
+	Remotes  Opt[map[string]Opt[Remote]] `json:"remotes,omitempty"`
+	Sharding Opt[bool]                   `json:"sharding,omitempty"`
 }
 
 func (n Network) MarshalJSON() ([]byte, error) {
@@ -37,6 +38,12 @@ func (n Network) MarshalJSON() ([]byte, error) {
 		m["remotes"] = nil
 	}
 
+	if n.Sharding.Valid() {
+		m["sharding"] = n.Sharding.Value
+	} else if n.Sharding.Null() {
+		m["sharding"] = nil
+	}
+
 	return json.Marshal(m)
 }
 
@@ -46,6 +53,7 @@ func (n Network) MarshalJSON() ([]byte, error) {
 type Remote struct {
 	URL          Opt[string] `json:"url"`
 	SearchAPIKey Opt[string] `json:"searchApiKey,omitempty"`
+	WriteAPIKey  Opt[string] `json:"writeApiKey,omitempty"`
 }
 
 func (r Remote) MarshalJSON() ([]byte, error) {
@@ -63,6 +71,12 @@ func (r Remote) MarshalJSON() ([]byte, error) {
 		m["searchApiKey"] = nil
 	}
 
+	if r.WriteAPIKey.Valid() {
+		m["writeApiKey"] = r.WriteAPIKey.Value
+	} else if r.WriteAPIKey.Null() {
+		m["writeApiKey"] = nil
+	}
+
 	return json.Marshal(m)
 }
 
@@ -287,18 +301,37 @@ type Stats struct {
 //
 // Documentation: https://www.meilisearch.com/docs/learn/advanced/asynchronous_operations
 type Task struct {
-	Status     TaskStatus          `json:"status"`
-	UID        int64               `json:"uid,omitempty"`
-	TaskUID    int64               `json:"taskUid,omitempty"`
-	IndexUID   string              `json:"indexUid"`
-	Type       TaskType            `json:"type"`
-	Error      meilisearchApiError `json:"error,omitempty"`
-	Duration   string              `json:"duration,omitempty"`
-	EnqueuedAt time.Time           `json:"enqueuedAt"`
-	StartedAt  time.Time           `json:"startedAt,omitempty"`
-	FinishedAt time.Time           `json:"finishedAt,omitempty"`
-	Details    Details             `json:"details,omitempty"`
-	CanceledBy int64               `json:"canceledBy,omitempty"`
+	Status      TaskStatus          `json:"status"`
+	UID         int64               `json:"uid,omitempty"`
+	TaskUID     int64               `json:"taskUid,omitempty"`
+	IndexUID    string              `json:"indexUid"`
+	Type        TaskType            `json:"type"`
+	Error       meilisearchApiError `json:"error,omitempty"`
+	TaskNetwork TaskNetwork         `json:"network,omitempty"`
+	Duration    string              `json:"duration,omitempty"`
+	EnqueuedAt  time.Time           `json:"enqueuedAt"`
+	StartedAt   time.Time           `json:"startedAt,omitempty"`
+	FinishedAt  time.Time           `json:"finishedAt,omitempty"`
+	Details     Details             `json:"details,omitempty"`
+	CanceledBy  int64               `json:"canceledBy,omitempty"`
+}
+
+// TaskNetwork indicates information about a task network
+//
+// Documentation: https://www.meilisearch.com/docs/reference/api/tasks#network
+type TaskNetwork struct {
+	Origin  *Origin                `json:"origin,omitempty"`
+	Remotes map[string]*TaskRemote `json:"remotes,omitempty"`
+}
+
+type Origin struct {
+	RemoteName string `json:"remoteName,omitempty"`
+	TaskUID    string `json:"taskUid,omitempty"`
+}
+
+type TaskRemote struct {
+	TaskUID *string `json:"task_uid,omitempty"`
+	Error   *string `json:"error,omitempty"`
 }
 
 // TaskInfo indicates information regarding a task returned by an asynchronous method



Test patch:
diff --git a/integration/network_test.go b/integration/network_test.go
index 8a0e7ee..37130a2 100644
--- a/integration/network_test.go
+++ b/integration/network_test.go
@@ -30,10 +30,11 @@ func Test_UpdateNetwork(t *testing.T) {
 	require.True(t, experimentalFeatures.Network)
 
 	tests := []struct {
-		name        string
-		input       *meilisearch.Network
-		wantSelf    string
-		wantRemotes map[string]meilisearch.Opt[meilisearch.Remote]
+		name         string
+		input        *meilisearch.Network
+		wantSelf     string
+		wantSharding bool
+		wantRemotes  map[string]meilisearch.Opt[meilisearch.Remote]
 	}{
 		{
 			name: "set initial network",
@@ -46,11 +47,13 @@ func Test_UpdateNetwork(t *testing.T) {
 					}),
 				}),
 			},
-			wantSelf: "TEST",
+			wantSelf:     "TEST",
+			wantSharding: false,
 			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
 				"ms-00": meilisearch.NewOpt(meilisearch.Remote{
 					URL:          meilisearch.String("https://example.com"),
 					SearchAPIKey: meilisearch.String("TEST"),
+					WriteAPIKey:  meilisearch.Null[string](),
 				}),
 			},
 		},
@@ -59,11 +62,13 @@ func Test_UpdateNetwork(t *testing.T) {
 			input: &meilisearch.Network{
 				Self: meilisearch.NewOpt("NEW-SELF"),
 			},
-			wantSelf: "NEW-SELF",
+			wantSelf:     "NEW-SELF",
+			wantSharding: false,
 			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
 				"ms-00": meilisearch.NewOpt(meilisearch.Remote{
 					URL:          meilisearch.String("https://example.com"),
 					SearchAPIKey: meilisearch.String("TEST"),
+					WriteAPIKey:  meilisearch.Null[string](),
 				}),
 			},
 		},
@@ -72,15 +77,18 @@ func Test_UpdateNetwork(t *testing.T) {
 			input: &meilisearch.Network{
 				Remotes: meilisearch.NewOpt(map[string]meilisearch.Opt[meilisearch.Remote]{
 					"ms-00": meilisearch.NewOpt(meilisearch.Remote{
-						URL: meilisearch.String("https://updated.com"),
+						URL:         meilisearch.String("https://updated.com"),
+						WriteAPIKey: meilisearch.Null[string](),
 					}),
 				}),
 			},
-			wantSelf: "NEW-SELF",
+			wantSelf:     "NEW-SELF",
+			wantSharding: false,
 			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
 				"ms-00": meilisearch.NewOpt(meilisearch.Remote{
 					URL:          meilisearch.String("https://updated.com"),
 					SearchAPIKey: meilisearch.String("TEST"), // unchanged
+					WriteAPIKey:  meilisearch.Null[string](),
 				}),
 			},
 		},
@@ -90,14 +98,17 @@ func Test_UpdateNetwork(t *testing.T) {
 				Remotes: meilisearch.NewOpt(map[string]meilisearch.Opt[meilisearch.Remote]{
 					"ms-00": meilisearch.NewOpt(meilisearch.Remote{
 						SearchAPIKey: meilisearch.String("UPDATED_API_KEY"),
+						WriteAPIKey:  meilisearch.Null[string](),
 					}),
 				}),
 			},
-			wantSelf: "NEW-SELF",
+			wantSelf:     "NEW-SELF",
+			wantSharding: false,
 			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
 				"ms-00": meilisearch.NewOpt(meilisearch.Remote{
 					URL:          meilisearch.String("https://updated.com"),
 					SearchAPIKey: meilisearch.String("UPDATED_API_KEY"), // unchanged
+					WriteAPIKey:  meilisearch.Null[string](),
 				}),
 			},
 		},
@@ -107,14 +118,17 @@ func Test_UpdateNetwork(t *testing.T) {
 				Remotes: meilisearch.NewOpt(map[string]meilisearch.Opt[meilisearch.Remote]{
 					"ms-00": meilisearch.NewOpt(meilisearch.Remote{
 						SearchAPIKey: meilisearch.Null[string](),
+						WriteAPIKey:  meilisearch.Null[string](),
 					}),
 				}),
 			},
-			wantSelf: "NEW-SELF",
+			wantSelf:     "NEW-SELF",
+			wantSharding: false,
 			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
 				"ms-00": meilisearch.NewOpt(meilisearch.Remote{
 					URL:          meilisearch.String("https://updated.com"),
 					SearchAPIKey: meilisearch.Null[string](),
+					WriteAPIKey:  meilisearch.Null[string](),
 				}),
 			},
 		},
@@ -125,8 +139,9 @@ func Test_UpdateNetwork(t *testing.T) {
 					"ms-00": meilisearch.Null[meilisearch.Remote](),
 				}),
 			},
-			wantSelf:    "NEW-SELF",
-			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{},
+			wantSharding: false,
+			wantSelf:     "NEW-SELF",
+			wantRemotes:  map[string]meilisearch.Opt[meilisearch.Remote]{},
 		},
 		{
 			name: "add new remote",
@@ -138,11 +153,106 @@ func Test_UpdateNetwork(t *testing.T) {
 					}),
 				}),
 			},
-			wantSelf: "NEW-SELF",
+			wantSelf:     "NEW-SELF",
+			wantSharding: false,
 			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
 				"ms-01": meilisearch.NewOpt(meilisearch.Remote{
 					URL:          meilisearch.String("https://new-remote.com"),
 					SearchAPIKey: meilisearch.NewOpt("NEW-REMOTE-KEY"),
+					WriteAPIKey:  meilisearch.Null[string](),
+				}),
+			},
+		},
+		{
+			name: "add writeApiKey",
+			input: &meilisearch.Network{
+				Remotes: meilisearch.NewOpt(map[string]meilisearch.Opt[meilisearch.Remote]{
+					"ms-01": meilisearch.NewOpt(meilisearch.Remote{
+						URL:          meilisearch.String("https://new-remote.com"),
+						SearchAPIKey: meilisearch.NewOpt("NEW-REMOTE-KEY"),
+						WriteAPIKey:  meilisearch.String("WRITE-API-KEY"),
+					}),
+				}),
+			},
+			wantSelf:     "NEW-SELF",
+			wantSharding: false,
+			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
+				"ms-01": meilisearch.NewOpt(meilisearch.Remote{
+					URL:          meilisearch.String("https://new-remote.com"),
+					SearchAPIKey: meilisearch.NewOpt("NEW-REMOTE-KEY"),
+					WriteAPIKey:  meilisearch.String("WRITE-API-KEY"),
+				}),
+			},
+		},
+		{
+			name: "set writeApikey to empty",
+			input: &meilisearch.Network{
+				Remotes: meilisearch.NewOpt(map[string]meilisearch.Opt[meilisearch.Remote]{
+					"ms-01": meilisearch.NewOpt(meilisearch.Remote{
+						URL:          meilisearch.String("https://new-remote.com"),
+						SearchAPIKey: meilisearch.NewOpt("NEW-REMOTE-KEY"),
+						WriteAPIKey:  meilisearch.String(""),
+					}),
+				}),
+			},
+			wantSelf:     "NEW-SELF",
+			wantSharding: false,
+			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
+				"ms-01": meilisearch.NewOpt(meilisearch.Remote{
+					URL:          meilisearch.String("https://new-remote.com"),
+					SearchAPIKey: meilisearch.NewOpt("NEW-REMOTE-KEY"),
+					WriteAPIKey:  meilisearch.String(""),
+				}),
+			},
+		},
+		{
+			name: "set writeApikey to null",
+			input: &meilisearch.Network{
+				Remotes: meilisearch.NewOpt(map[string]meilisearch.Opt[meilisearch.Remote]{
+					"ms-01": meilisearch.NewOpt(meilisearch.Remote{
+						URL:          meilisearch.String("https://new-remote.com"),
+						SearchAPIKey: meilisearch.NewOpt("NEW-REMOTE-KEY"),
+						WriteAPIKey:  meilisearch.Null[string](),
+					}),
+				}),
+			},
+			wantSelf:     "NEW-SELF",
+			wantSharding: false,
+			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
+				"ms-01": meilisearch.NewOpt(meilisearch.Remote{
+					URL:          meilisearch.String("https://new-remote.com"),
+					SearchAPIKey: meilisearch.NewOpt("NEW-REMOTE-KEY"),
+					WriteAPIKey:  meilisearch.Null[string](),
+				}),
+			},
+		},
+		{
+			name: "enable sharding",
+			input: &meilisearch.Network{
+				Sharding: meilisearch.Bool(true),
+			},
+			wantSelf:     "NEW-SELF",
+			wantSharding: true,
+			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
+				"ms-01": meilisearch.NewOpt(meilisearch.Remote{
+					URL:          meilisearch.String("https://new-remote.com"),
+					SearchAPIKey: meilisearch.NewOpt("NEW-REMOTE-KEY"),
+					WriteAPIKey:  meilisearch.Null[string](),
+				}),
+			},
+		},
+		{
+			name: "disable sharding",
+			input: &meilisearch.Network{
+				Sharding: meilisearch.Bool(false),
+			},
+			wantSharding: false,
+			wantSelf:     "NEW-SELF",
+			wantRemotes: map[string]meilisearch.Opt[meilisearch.Remote]{
+				"ms-01": meilisearch.NewOpt(meilisearch.Remote{
+					URL:          meilisearch.String("https://new-remote.com"),
+					SearchAPIKey: meilisearch.NewOpt("NEW-REMOTE-KEY"),
+					WriteAPIKey:  meilisearch.Null[string](),
 				}),
 			},
 		},
@@ -155,6 +265,7 @@ func Test_UpdateNetwork(t *testing.T) {
 
 			require.Equal(t, tt.wantSelf, network.Self.Value)
 			require.Equal(t, tt.wantRemotes, network.Remotes.Value)
+			require.Equal(t, tt.wantSharding, network.Sharding.Value)
 		})
 	}
 }
diff --git a/types_test.go b/types_test.go
index ca27f7e..4f92fae 100644
--- a/types_test.go
+++ b/types_test.go
@@ -188,6 +188,20 @@ func TestNetwork_MarshalJSON(t *testing.T) {
 			},
 			wantJSON: `{"self":"primary","remotes":null}`,
 		},
+		{
+			name: "sharding explicitly null",
+			in: Network{
+				Sharding: Null[bool](),
+			},
+			wantJSON: `{"sharding": null}`,
+		},
+		{
+			name: "sharding set",
+			in: Network{
+				Sharding: Bool(true),
+			},
+			wantJSON: `{"sharding": true}`,
+		},
 	}
 
 	for _, tt := range tests {
@@ -232,6 +246,16 @@ func TestRemote_MarshalJSON(t *testing.T) {
 			in:       Remote{URL: String("https://east.example.com"), SearchAPIKey: String("sek_abc")},
 			wantJSON: `{"url":"https://east.example.com","searchApiKey":"sek_abc"}`,
 		},
+		{
+			name:     "writeApiKey set",
+			in:       Remote{WriteAPIKey: String("TEST-API-KEY")},
+			wantJSON: `{"writeApiKey": "TEST-API-KEY"}`,
+		},
+		{
+			name:     "writeApiKey null",
+			in:       Remote{WriteAPIKey: Null[string]()},
+			wantJSON: `{"writeApiKey": null}`,
+		},
 	}
 
 	for _, tt := range tests {