Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NOISSUE - Search by metadata #849

Merged
merged 27 commits into from
Sep 17, 2019
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
2 changes: 1 addition & 1 deletion bootstrap/mocks/things.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ func (svc *mainfluxThings) UpdateKey(context.Context, string, string, string) er
panic("not implemented")
}

func (svc *mainfluxThings) ListThings(context.Context, string, uint64, uint64, string) (things.ThingsPage, error) {
func (svc *mainfluxThings) ListThings(context.Context, string, uint64, uint64, string, things.ThingMetadata) (things.ThingsPage, error) {
panic("not implemented")
}

Expand Down
7 changes: 7 additions & 0 deletions docs/provisioning.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,13 @@ group of things. In that case, your request should look like:
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/things?offset=0&limit=5
```

You can specify `name` and/or `metadata` parameters in order to fetch specific
group of things. When specifiying metadata you can specify just a part of the metadata json you want to match

```
curl -s -S -i --cacert docker/ssl/certs/mainflux-server.crt --insecure -H "Authorization: <user_auth_token>" https://localhost/things?offset=0&limit=5&metadata={"serial":"123456"}
```

If you don't provide them, default values will be used instead: 0 for `offset`,
and 10 for `limit`. Note that `limit` cannot be set to values greater than 100. Providing
invalid values will be considered malformed request.
Expand Down
4 changes: 2 additions & 2 deletions things/api/logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ func (lm *loggingMiddleware) ViewThing(ctx context.Context, token, id string) (t
return lm.svc.ViewThing(ctx, token, id)
}

func (lm *loggingMiddleware) ListThings(ctx context.Context, token string, offset, limit uint64, name string) (_ things.ThingsPage, err error) {
func (lm *loggingMiddleware) ListThings(ctx context.Context, token string, offset, limit uint64, name string, metadata things.ThingMetadata) (_ things.ThingsPage, err error) {
defer func(begin time.Time) {
nlog := ""
if name != "" {
Expand All @@ -96,7 +96,7 @@ func (lm *loggingMiddleware) ListThings(ctx context.Context, token string, offse
lm.logger.Info(fmt.Sprintf("%s without errors.", message))
}(time.Now())

return lm.svc.ListThings(ctx, token, offset, limit, name)
return lm.svc.ListThings(ctx, token, offset, limit, name, metadata)
}

func (lm *loggingMiddleware) ListThingsByChannel(ctx context.Context, token, id string, offset, limit uint64) (_ things.ThingsPage, err error) {
Expand Down
4 changes: 2 additions & 2 deletions things/api/metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,13 @@ func (ms *metricsMiddleware) ViewThing(ctx context.Context, token, id string) (t
return ms.svc.ViewThing(ctx, token, id)
}

func (ms *metricsMiddleware) ListThings(ctx context.Context, token string, offset, limit uint64, name string) (things.ThingsPage, error) {
func (ms *metricsMiddleware) ListThings(ctx context.Context, token string, offset, limit uint64, name string, metadata things.ThingMetadata) (things.ThingsPage, error) {
defer func(begin time.Time) {
ms.counter.With("method", "list_things").Add(1)
ms.latency.With("method", "list_things").Observe(time.Since(begin).Seconds())
}(time.Now())

return ms.svc.ListThings(ctx, token, offset, limit, name)
return ms.svc.ListThings(ctx, token, offset, limit, name, metadata)
}

func (ms *metricsMiddleware) ListThingsByChannel(ctx context.Context, token, id string, offset, limit uint64) (things.ThingsPage, error) {
Expand Down
2 changes: 1 addition & 1 deletion things/api/things/http/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ func listThingsEndpoint(svc things.Service) endpoint.Endpoint {
return nil, err
}

page, err := svc.ListThings(ctx, req.token, req.offset, req.limit, req.name)
page, err := svc.ListThings(ctx, req.token, req.offset, req.limit, req.name, req.metadata)
if err != nil {
return nil, err
}
Expand Down
9 changes: 5 additions & 4 deletions things/api/things/http/requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,10 +135,11 @@ func (req viewResourceReq) validate() error {
}

type listResourcesReq struct {
token string
offset uint64
limit uint64
name string
token string
offset uint64
limit uint64
name string
metadata map[string]interface{}
}

func (req *listResourcesReq) validate() error {
Expand Down
34 changes: 30 additions & 4 deletions things/api/things/http/transport.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const (
offset = "offset"
limit = "limit"
name = "name"
metadata = "metadata"

defOffset = 0
defLimit = 10
Expand Down Expand Up @@ -258,11 +259,17 @@ func decodeList(_ context.Context, r *http.Request) (interface{}, error) {
return nil, err
}

m, err := readMetadataQuery(r, "metadata")
if err != nil {
return nil, err
}

req := listResourcesReq{
token: r.Header.Get("Authorization"),
offset: o,
limit: l,
name: n,
token: r.Header.Get("Authorization"),
offset: o,
limit: l,
name: n,
metadata: m,
}

return req, nil
Expand Down Expand Up @@ -380,3 +387,22 @@ func readStringQuery(r *http.Request, key string) (string, error) {

return vals[0], nil
}

func readMetadataQuery(r *http.Request, key string) (map[string]interface{}, error) {
vals := bone.GetQuery(r, key)
if len(vals) > 1 {
return nil, errInvalidQueryParams
}

if len(vals) == 0 {
return nil, nil
}
drasko marked this conversation as resolved.
Show resolved Hide resolved

m := make(map[string]interface{})
err := json.Unmarshal([]byte(vals[0]), &m)
if err != nil {
return nil, err
}

return m, nil
}
2 changes: 1 addition & 1 deletion things/mocks/things.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func (trm *thingRepositoryMock) RetrieveByID(_ context.Context, owner, id string
return things.Thing{}, things.ErrNotFound
}

func (trm *thingRepositoryMock) RetrieveAll(_ context.Context, owner string, offset, limit uint64, name string) (things.ThingsPage, error) {
func (trm *thingRepositoryMock) RetrieveAll(_ context.Context, owner string, offset, limit uint64, name string, metadata things.ThingMetadata) (things.ThingsPage, error) {
trm.mu.Lock()
defer trm.mu.Unlock()

Expand Down
8 changes: 8 additions & 0 deletions things/postgres/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,14 @@ func migrateDB(db *sqlx.DB) error {
"DROP TABLE channels",
},
},
{
Id: "things_3",
Up: []string{
`ALTER TABLE IF EXISTS things ALTER COLUMN
metadata TYPE JSONB using metadata::text::jsonb
`,
},
},
},
}

Expand Down
22 changes: 16 additions & 6 deletions things/postgres/things.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,22 +171,32 @@ func (tr thingRepository) RetrieveByKey(_ context.Context, key string) (string,
return id, nil
}

func (tr thingRepository) RetrieveAll(_ context.Context, owner string, offset, limit uint64, name string) (things.ThingsPage, error) {
func (tr thingRepository) RetrieveAll(_ context.Context, owner string, offset, limit uint64, name string, metadata things.ThingMetadata) (things.ThingsPage, error) {
name = strings.ToLower(name)
nq := ""
if name != "" {
name = fmt.Sprintf(`%%%s%%`, name)
nq = `AND LOWER(name) LIKE :name`
}
mq := ""
if len(metadata) > 0 {
mq = `metadata @> :metadata AND`
}

m, err := json.Marshal(metadata)
if err != nil {
return things.ThingsPage{}, err
}

q := fmt.Sprintf(`SELECT id, name, key, metadata FROM things
WHERE owner = :owner %s ORDER BY id LIMIT :limit OFFSET :offset;`, nq)
WHERE %s owner = :owner %s ORDER BY id LIMIT :limit OFFSET :offset;`, mq, nq)
drasko marked this conversation as resolved.
Show resolved Hide resolved

params := map[string]interface{}{
"owner": owner,
"limit": limit,
"offset": offset,
"name": name,
"owner": owner,
"limit": limit,
"offset": offset,
"name": name,
"metadata": m,
}

rows, err := tr.db.NamedQuery(q, params)
Expand Down
33 changes: 23 additions & 10 deletions things/postgres/things_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,9 @@ func TestThingRetrieveByKey(t *testing.T) {
func TestMultiThingRetrieval(t *testing.T) {
email := "thing-multi-retrieval@example.com"
name := "mainflux"
metadata := make(map[string]interface{})
metadata["serial"] = "123456"
metadata["type"] = "test"
idp := uuid.New()
thingRepo := postgres.NewThingRepository(db)

Expand All @@ -375,9 +378,10 @@ func TestMultiThingRetrieval(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("got unexpected error: %s", err))

th := things.Thing{
Owner: email,
ID: thid,
Key: thkey,
Owner: email,
ID: thid,
Key: thkey,
Metadata: metadata,
}

// Create first two Things with name.
Expand All @@ -389,12 +393,13 @@ func TestMultiThingRetrieval(t *testing.T) {
}

cases := map[string]struct {
owner string
offset uint64
limit uint64
name string
size uint64
total uint64
owner string
offset uint64
limit uint64
name string
size uint64
total uint64
metadata map[string]interface{}
}{
"retrieve all things with existing owner": {
owner: email,
Expand Down Expand Up @@ -433,10 +438,18 @@ func TestMultiThingRetrieval(t *testing.T) {
size: 0,
total: 0,
},
"retrieve things with metadata": {
owner: email,
offset: 0,
limit: n,
size: n,
total: n,
metadata: metadata,
},
}

for desc, tc := range cases {
page, err := thingRepo.RetrieveAll(context.Background(), tc.owner, tc.offset, tc.limit, tc.name)
page, err := thingRepo.RetrieveAll(context.Background(), tc.owner, tc.offset, tc.limit, tc.name, tc.metadata)
size := uint64(len(page.Things))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.Equal(t, tc.total, page.Total, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.total, page.Total))
Expand Down
4 changes: 2 additions & 2 deletions things/redis/streams.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ func (es eventStore) ViewThing(ctx context.Context, token, id string) (things.Th
return es.svc.ViewThing(ctx, token, id)
}

func (es eventStore) ListThings(ctx context.Context, token string, offset, limit uint64, name string) (things.ThingsPage, error) {
return es.svc.ListThings(ctx, token, offset, limit, name)
func (es eventStore) ListThings(ctx context.Context, token string, offset, limit uint64, name string, metadata things.ThingMetadata) (things.ThingsPage, error) {
return es.svc.ListThings(ctx, token, offset, limit, name, metadata)
}

func (es eventStore) ListThingsByChannel(ctx context.Context, token, id string, offset, limit uint64) (things.ThingsPage, error) {
Expand Down
4 changes: 2 additions & 2 deletions things/redis/streams_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,8 +187,8 @@ func TestListThings(t *testing.T) {
require.Nil(t, err, fmt.Sprintf("unexpected error %s", err))

essvc := redis.NewEventStoreMiddleware(svc, redisClient)
esths, eserr := essvc.ListThings(context.Background(), token, 0, 10, "")
ths, err := svc.ListThings(context.Background(), token, 0, 10, "")
esths, eserr := essvc.ListThings(context.Background(), token, 0, 10, "", nil)
ths, err := svc.ListThings(context.Background(), token, 0, 10, "", nil)
assert.Equal(t, ths, esths, fmt.Sprintf("event sourcing changed service behaviour: expected %v got %v", ths, esths))
assert.Equal(t, err, eserr, fmt.Sprintf("event sourcing changed service behaviour: expected %v got %v", err, eserr))
}
Expand Down
6 changes: 3 additions & 3 deletions things/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ type Service interface {

// ListThings retrieves data about subset of things that belongs to the
// user identified by the provided key.
ListThings(context.Context, string, uint64, uint64, string) (ThingsPage, error)
ListThings(context.Context, string, uint64, uint64, string, ThingMetadata) (ThingsPage, error)

// ListThingsByChannel retrieves data about subset of things that are
// connected to specified channel and belong to the user identified by
Expand Down Expand Up @@ -196,13 +196,13 @@ func (ts *thingsService) ViewThing(ctx context.Context, token, id string) (Thing
return ts.things.RetrieveByID(ctx, res.GetValue(), id)
}

func (ts *thingsService) ListThings(ctx context.Context, token string, offset, limit uint64, name string) (ThingsPage, error) {
func (ts *thingsService) ListThings(ctx context.Context, token string, offset, limit uint64, name string, metadata ThingMetadata) (ThingsPage, error) {
res, err := ts.users.Identify(ctx, &mainflux.Token{Value: token})
if err != nil {
return ThingsPage{}, ErrUnauthorizedAccess
}

return ts.things.RetrieveAll(ctx, res.GetValue(), offset, limit, name)
return ts.things.RetrieveAll(ctx, res.GetValue(), offset, limit, name, metadata)
}

func (ts *thingsService) ListThingsByChannel(ctx context.Context, token, channel string, offset, limit uint64) (ThingsPage, error) {
Expand Down
27 changes: 20 additions & 7 deletions things/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,18 +186,23 @@ func TestViewThing(t *testing.T) {
func TestListThings(t *testing.T) {
svc := newService(map[string]string{token: email})

m := make(map[string]interface{})
m["serial"] = "123456"
thing.Metadata = m

n := uint64(10)
for i := uint64(0); i < n; i++ {
svc.AddThing(context.Background(), token, thing)
}

cases := map[string]struct {
token string
offset uint64
limit uint64
name string
size uint64
err error
token string
offset uint64
limit uint64
name string
size uint64
metadata map[string]interface{}
err error
}{
"list all things": {
token: token,
Expand Down Expand Up @@ -241,10 +246,18 @@ func TestListThings(t *testing.T) {
size: 0,
err: things.ErrUnauthorizedAccess,
},
"list with metadata": {
token: token,
offset: 0,
limit: n,
size: n,
err: nil,
metadata: m,
},
}

for desc, tc := range cases {
page, err := svc.ListThings(context.Background(), tc.token, tc.offset, tc.limit, tc.name)
page, err := svc.ListThings(context.Background(), tc.token, tc.offset, tc.limit, tc.name, tc.metadata)
size := uint64(len(page.Things))
assert.Equal(t, tc.size, size, fmt.Sprintf("%s: expected %d got %d\n", desc, tc.size, size))
assert.Equal(t, tc.err, err, fmt.Sprintf("%s: expected %s got %s\n", desc, tc.err, err))
Expand Down
8 changes: 8 additions & 0 deletions things/swagger.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ paths:
- $ref: "#/parameters/Limit"
- $ref: "#/parameters/Offset"
- $ref: "#/parameters/Name"
- $ref: "#/parameters/Metadata"
responses:
200:
description: Data retrieved.
Expand Down Expand Up @@ -502,6 +503,13 @@ parameters:
type: string
minimum: 0
required: false
Metadata
name: metadata
descripton: Metadata filter. Filtering is performed matching the parametar with metadata on top level. Parametar is json.
in: query
type: string
minimum: 0
required: false

responses:
ServiceError:
Expand Down
Loading