Skip to content

Commit 8f3d28c

Browse files
authored
feat(metadata): impl endpoint for updating metadata (#9)
1 parent b1074a6 commit 8f3d28c

File tree

5 files changed

+169
-15
lines changed

5 files changed

+169
-15
lines changed

http/api.go

+52
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package http
44
import (
55
"bytes"
66
"fmt"
7+
"strings"
78

89
"github.com/z5labs/sakuin"
910
"github.com/z5labs/sakuin/http/middleware/logger"
@@ -57,6 +58,7 @@ func NewServer(s *sakuin.Service, cfg ...fiber.Config) *fiber.App {
5758

5859
// Metadata
5960
app.Get("/index/:id/metadata", NewGetMetadataHandler(s))
61+
app.Put("/index/:id/metadata", NewUpdateMetadataHandler(s))
6062

6163
// Indexing
6264
app.Post("/index", NewIndexHandler(s))
@@ -163,6 +165,56 @@ func NewGetMetadataHandler(s *sakuin.Service) fiber.Handler {
163165
}
164166
}
165167

168+
// NewUpdateMetadataHandler godoc
169+
// @Summary Update object metadata by id. This will override and merge metadata fields.
170+
// @Tags Metadata
171+
// @Accept json
172+
// @Success 200 "Successfully updated object metadata."
173+
// @Failure 500 {object} APIError
174+
// @Param id path string true "Object ID"
175+
// @Router /index/{id}/metadata [put]
176+
func NewUpdateMetadataHandler(s *sakuin.Service) fiber.Handler {
177+
return func(c *fiber.Ctx) error {
178+
if contentType := c.Get("Content-Type"); !strings.Contains(contentType, "application/json") {
179+
zap.L().Warn("received invalid content type", zap.String("content-type", contentType))
180+
181+
return c.Status(fiber.StatusBadRequest).
182+
JSON(APIError{
183+
Message: "content type must be: application/json",
184+
})
185+
}
186+
187+
var m map[string]interface{}
188+
err := c.BodyParser(&m)
189+
if err != nil {
190+
zap.L().Error("unexpected error when unmarshalling request body", zap.Error(err))
191+
return c.Status(fiber.StatusInternalServerError).
192+
JSON(APIError{
193+
Message: err.Error(),
194+
})
195+
}
196+
197+
id := c.Params("id")
198+
199+
_, err = s.UpdateMetadata(c.Context(), &sakuin.UpdateMetadataRequest{
200+
ID: id,
201+
Metadata: m,
202+
})
203+
if _, ok := err.(sakuin.DocumentDoesNotExistErr); ok {
204+
zap.L().Error("metadata does not exist", zap.String("id", id))
205+
return c.SendStatus(fiber.StatusNotFound)
206+
}
207+
if err != nil {
208+
zap.L().Error("unexpected error when updating metadata", zap.Error(err))
209+
return c.Status(fiber.StatusInternalServerError).JSON(APIError{
210+
Message: err.Error(),
211+
})
212+
}
213+
214+
return c.SendStatus(fiber.StatusOK)
215+
}
216+
}
217+
166218
// NewIndexHandler godoc
167219
// @Summary index a new object along with its metadata
168220
// @Tags Index

http/metadata_test.go

+82
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package http
22

33
import (
4+
"bytes"
45
"fmt"
56
"net/http"
67
"testing"
78

89
"github.com/z5labs/sakuin"
10+
11+
"github.com/stretchr/testify/assert"
912
)
1013

1114
const getMetadataEndpointFmt = "http://%s/index/%s/metadata"
@@ -77,3 +80,82 @@ func TestGetMetadataHandler(t *testing.T) {
7780
}
7881
})
7982
}
83+
84+
func TestUpdateMetadataHandler(t *testing.T) {
85+
t.Run("should fail if req content type isn't json", func(subT *testing.T) {
86+
addr, err := startTestServer(subT)
87+
if err != nil {
88+
subT.Error(err)
89+
return
90+
}
91+
92+
uri := fmt.Sprintf(getMetadataEndpointFmt, addr, "metadataDoesNotExistID")
93+
req, err := http.NewRequest(http.MethodPut, uri, bytes.NewReader([]byte("{}")))
94+
if err != nil {
95+
subT.Error(err)
96+
return
97+
}
98+
99+
resp, err := http.DefaultClient.Do(req)
100+
if err != nil {
101+
subT.Error(err)
102+
return
103+
}
104+
105+
assert.Equal(subT, http.StatusBadRequest, resp.StatusCode)
106+
})
107+
108+
t.Run("should fail if metadata doesn't exist", func(subT *testing.T) {
109+
addr, err := startTestServer(subT)
110+
if err != nil {
111+
subT.Error(err)
112+
return
113+
}
114+
115+
uri := fmt.Sprintf(getMetadataEndpointFmt, addr, "metadataDoesNotExistID")
116+
req, err := http.NewRequest(http.MethodPut, uri, bytes.NewReader([]byte("{}")))
117+
if err != nil {
118+
subT.Error(err)
119+
return
120+
}
121+
req.Header.Set("Content-Type", "application/json")
122+
123+
resp, err := http.DefaultClient.Do(req)
124+
if err != nil {
125+
subT.Error(err)
126+
return
127+
}
128+
129+
assert.Equal(subT, http.StatusNotFound, resp.StatusCode)
130+
})
131+
132+
t.Run("should succeed if metadata does exist", func(subT *testing.T) {
133+
testDocID := "test"
134+
testMetadata := map[string]interface{}{"hello": "world"}
135+
136+
docStore := sakuin.NewInMemoryDocumentStore().
137+
WithDocument(testDocID, testMetadata)
138+
139+
addr, err := startTestServer(subT, withDocumentStore(docStore))
140+
if err != nil {
141+
subT.Error(err)
142+
return
143+
}
144+
145+
uri := fmt.Sprintf(getMetadataEndpointFmt, addr, testDocID)
146+
req, err := http.NewRequest(http.MethodPut, uri, bytes.NewReader([]byte(`{"good": "bye"}`)))
147+
if err != nil {
148+
subT.Error(err)
149+
return
150+
}
151+
req.Header.Set("Content-Type", "application/json")
152+
153+
resp, err := http.DefaultClient.Do(req)
154+
if err != nil {
155+
subT.Error(err)
156+
return
157+
}
158+
159+
assert.Equal(subT, http.StatusOK, resp.StatusCode)
160+
})
161+
}

http/object_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@ import (
66
"net/http"
77
"testing"
88

9-
"github.com/stretchr/testify/assert"
109
"github.com/z5labs/sakuin"
10+
11+
"github.com/stretchr/testify/assert"
1112
)
1213

1314
const getObjectEndpointFmt = "http://%s/index/%s/object"

index.go

+19
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,25 @@ func (s *Service) GetMetadata(ctx context.Context, req *GetMetadataRequest) (*Ge
7676
return &GetMetadataResponse{Metadata: doc}, nil
7777
}
7878

79+
type UpdateMetadataRequest struct {
80+
ID string
81+
Metadata map[string]interface{}
82+
}
83+
84+
type UpdateMetadataResponse struct{}
85+
86+
func (s *Service) UpdateMetadata(ctx context.Context, req *UpdateMetadataRequest) (*UpdateMetadataResponse, error) {
87+
stats, err := s.docDB.Stat(ctx, req.ID)
88+
if err != nil {
89+
return nil, err
90+
}
91+
if !stats.Exists {
92+
return nil, DocumentDoesNotExistErr{ID: req.ID}
93+
}
94+
95+
return nil, s.docDB.Upsert(ctx, req.ID, req.Metadata)
96+
}
97+
7998
type IndexRequest struct {
8099
Metadata map[string]interface{}
81100
Object []byte

storage.go

+14-14
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,6 @@ func RunObjectStorageTests(t TestingT, objStore ObjectStore) {
5858
})
5959
}
6060

61-
type DocumentStore interface {
62-
Stat(ctx context.Context, id string) (*StatInfo, error)
63-
Get(ctx context.Context, id string) (map[string]interface{}, error)
64-
Upsert(ctx context.Context, id string, b map[string]interface{}) error
65-
}
66-
67-
func RunDocumentStorageTests(t TestingT, docStore DocumentStore) {
68-
t.Run("should fail with DocumentDoesNotExistErr if document doesn't exist", func(subT TestingT) {
69-
var docErr DocumentDoesNotExistErr
70-
_, err := docStore.Get(context.Background(), "")
71-
assert.ErrorAs(subT, err, &docErr, "expected and DocumentDoesNotExistErr")
72-
})
73-
}
74-
7561
type InMemoryObjectStore struct {
7662
mu sync.Mutex
7763
objects map[string][]byte
@@ -137,6 +123,20 @@ func (s *InMemoryObjectStore) NumOfObects() int {
137123
return len(s.objects)
138124
}
139125

126+
type DocumentStore interface {
127+
Stat(ctx context.Context, id string) (*StatInfo, error)
128+
Get(ctx context.Context, id string) (map[string]interface{}, error)
129+
Upsert(ctx context.Context, id string, b map[string]interface{}) error
130+
}
131+
132+
func RunDocumentStorageTests(t TestingT, docStore DocumentStore) {
133+
t.Run("should fail with DocumentDoesNotExistErr if document doesn't exist", func(subT TestingT) {
134+
var docErr DocumentDoesNotExistErr
135+
_, err := docStore.Get(context.Background(), "")
136+
assert.ErrorAs(subT, err, &docErr, "expected and DocumentDoesNotExistErr")
137+
})
138+
}
139+
140140
type InMemoryDocumentStore struct {
141141
mu sync.Mutex
142142
docs map[string]map[string]interface{}

0 commit comments

Comments
 (0)