Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,6 +503,21 @@ Update the generated files,
go generate ./...
```

Run all tests

```bash
# In Windows Powershell
# $env:TYPESENSE_API_KEY="xyz"
# $env:TYPESENSE_URL="http://localhost:8108"

export TYPESENSE_URL="http://localhost:8108"
export TYPESENSE_API_KEY="xyz"

go test ./... -tags=integration -v
```



## License

`typesense-go` is distributed under the Apache 2 license.
2 changes: 1 addition & 1 deletion typesense/api/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package api
type ImportDocumentResponse struct {
Success bool `json:"success"`
Error string `json:"error"`
Document string `json:"document"`
Document any `json:"document"` // on success: map[string]interface{}; on error: string
Id string `json:"id"`
}

Expand Down
14 changes: 9 additions & 5 deletions typesense/documents.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package typesense

import (
"bufio"
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"

Expand Down Expand Up @@ -141,18 +143,20 @@ func (d *documents) Import(ctx context.Context, documents []interface{}, params
if err != nil {
return nil, err
}
defer response.Close()

var result []*api.ImportDocumentResponse
jsonDecoder := json.NewDecoder(response)
for jsonDecoder.More() {
scanner := bufio.NewScanner(response)
scanner.Split(bufio.ScanLines)
for scanner.Scan() {
var docResult *api.ImportDocumentResponse
if err := jsonDecoder.Decode(&docResult); err != nil {
return result, errors.New("failed to decode result")
if err := json.Unmarshal(scanner.Bytes(), &docResult); err != nil {
return result, fmt.Errorf("failed to decode result: %w", err)
}
result = append(result, docResult)
}

return result, nil
return result, scanner.Err()
}

func (d *documents) ImportJsonl(ctx context.Context, body io.Reader, params *api.ImportDocumentsParams) (io.ReadCloser, error) {
Expand Down
44 changes: 26 additions & 18 deletions typesense/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"errors"
"io"
"net/http"
"reflect"
"strings"
"testing"

Expand All @@ -30,15 +29,15 @@ func eqReader(r io.Reader) gomock.Matcher {
}

func (m *eqReaderMatcher) Matches(x interface{}) bool {
if _, ok := x.(io.Reader); !ok {
r, ok := x.(io.Reader)
if !ok {
return false
}
r := x.(io.Reader)
allBytes, err := io.ReadAll(r)
if err != nil {
panic(err)
}
return reflect.DeepEqual(allBytes, m.readerBytes)
return bytes.Equal(allBytes, m.readerBytes)
}

func (m *eqReaderMatcher) String() string {
Expand Down Expand Up @@ -197,26 +196,38 @@ func TestDocumentsImportOnHttpStatusErrorCodeReturnsError(t *testing.T) {
assert.NotNil(t, err)
}

func TestDocumentsImportWithTwoDocuments(t *testing.T) {
expectedParams := &api.ImportDocumentsParams{
func TestDocumentsImportWithTwoSuccessesAndOneFailure(t *testing.T) {
params := &api.ImportDocumentsParams{
Action: pointer.Any(api.Create),
BatchSize: pointer.Int(40),
}
expectedBody := strings.NewReader(`{"id":"123","companyName":"Stark Industries","numEmployees":5215,"country":"USA"}` +
"\n" + `{"id":"125","companyName":"Stark Industries","numEmployees":5215,"country":"USA"}` + "\n")
expectedResultString := `{"success": true, "id":"123"}` + "\n" + `{"success": false, "id":"125", "error": "Bad JSON.", "document": "[bad doc"}`
ReturnId: pointer.Any(true),
ReturnDoc: pointer.Any(true),
}
expectedBody := `{"id":"123","companyName":"Stark Industries","numEmployees":5215,"country":"USA"}
{"id":"125","companyName":"Stark Industries","numEmployees":5215,"country":"USA"}
"[bad doc"
`
expectedResultString := `{"success": true, "id":"123","document": {"id":"123","companyName":"Stark Industries","numEmployees":5215,"country":"USA"}}
{"success": true, "id":"125", "document": {"id":"125","companyName":"Stark Industries","numEmployees":5215,"country":"USA"}}
{"success": false, "id":"", "error": "Bad JSON.", "document": "[bad doc"}
`
expectedResult := []*api.ImportDocumentResponse{
{Success: true, Id: "123"},
{Success: false, Id: "125", Error: "Bad JSON.", Document: "[bad doc"},
{Success: true, Id: "123", Document: map[string]interface{}{"id": "123", "companyName": "Stark Industries", "numEmployees": float64(5215), "country": "USA"}},
{Success: true, Id: "125", Document: map[string]interface{}{"id": "125", "companyName": "Stark Industries", "numEmployees": float64(5215), "country": "USA"}},
{Success: false, Id: "", Error: "Bad JSON.", Document: "[bad doc"},
}

ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockAPIClient := mocks.NewMockAPIClientInterface(ctrl)

mockAPIClient.EXPECT().
ImportDocumentsWithBody(gomock.Not(gomock.Nil()),
"companies", expectedParams, "application/octet-stream", eqReader(expectedBody)).
ImportDocumentsWithBody(
gomock.Not(gomock.Nil()),
"companies",
params,
"application/octet-stream",
eqReader(strings.NewReader(expectedBody))).
Return(&http.Response{
StatusCode: http.StatusOK,
Body: io.NopCloser(strings.NewReader(expectedResultString)),
Expand All @@ -227,10 +238,7 @@ func TestDocumentsImportWithTwoDocuments(t *testing.T) {
documents := []interface{}{
createNewDocument("123"),
createNewDocument("125"),
}
params := &api.ImportDocumentsParams{
Action: pointer.Any(api.Create),
BatchSize: pointer.Int(40),
"[bad doc",
}
result, err := client.Collection("companies").Documents().Import(context.Background(), documents, params)

Expand Down
18 changes: 15 additions & 3 deletions typesense/test/import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,26 @@ func TestDocumentsImport(t *testing.T) {
newDocument("123"),
newDocument("125", withCompanyName("Company2")),
newDocument("127", withCompanyName("Company3")),
// Bad doc
map[string]interface{}{"bad_doc": true, "content": map[string]interface{}{"bad_field": "bad_value"}},
"[Bad string",
}

params := &api.ImportDocumentsParams{Action: pointer.Any(api.Create), DirtyValues: pointer.Any(api.CoerceOrDrop)}
params := &api.ImportDocumentsParams{Action: pointer.Any(api.Create), DirtyValues: pointer.Any(api.CoerceOrDrop), ReturnDoc: pointer.True(), ReturnId: pointer.True()}
responses, err := typesenseClient.Collection(collectionName).Documents().Import(context.Background(), documents, params)

require.NoError(t, err)
for _, response := range responses {
require.True(t, response.Success, "document import failed")
for i, response := range responses {
if i < 3 {
require.True(t, response.Success, "document import failed")

} else if i == 3 {
require.False(t, response.Success, "failed to handle bad document")
require.Equal(t, `{"bad_doc":true,"content":{"bad_field":"bad_value"}}`, response.Document)
} else {
require.False(t, response.Success, "failed to handle bad string")
require.Equal(t, `"[Bad string"`, response.Document)
}
}

results := retrieveDocuments(t, collectionName, "123", "125", "127")
Expand Down