NOISSUE - Add name field for Bootstrap Config (#564)
* Add name field to Config Enable search by name. Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com> * Create separate response for unknown Configs Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com> * Use meaningful names for filters Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com> * Add name search tests Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com> * Update API docs Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com> * Break mocks check into multiple lines Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com> * Create new instances in a consistent way Reformat `return` statements. Signed-off-by: Dusan Borovcanin <dusan.borovcanin@mainflux.com>
This commit is contained in:
parent
8966a13760
commit
1df4dcd7b7
|
@ -30,6 +30,7 @@ func addEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
|||
ExternalID: req.ExternalID,
|
||||
ExternalKey: req.ExternalKey,
|
||||
MFChannels: channels,
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
}
|
||||
|
||||
|
@ -75,6 +76,7 @@ func viewEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
|||
Channels: channels,
|
||||
ExternalID: config.ExternalID,
|
||||
ExternalKey: config.ExternalKey,
|
||||
Name: config.Name,
|
||||
Content: config.Content,
|
||||
State: config.State,
|
||||
}
|
||||
|
@ -99,6 +101,7 @@ func updateEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
|||
config := bootstrap.Config{
|
||||
MFThing: req.id,
|
||||
MFChannels: channels,
|
||||
Name: req.Name,
|
||||
Content: req.Content,
|
||||
State: req.State,
|
||||
}
|
||||
|
@ -128,34 +131,46 @@ func listEndpoint(svc bootstrap.Service) endpoint.Endpoint {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := listRes{
|
||||
Configs: []viewRes{},
|
||||
}
|
||||
|
||||
for _, cfg := range configs {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.MFChannels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
switch {
|
||||
case req.filter.Unknown:
|
||||
res := listUnknownRes{}
|
||||
for _, cfg := range configs {
|
||||
res.Configs = append(res.Configs, unknownRes{
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
})
|
||||
}
|
||||
|
||||
view := viewRes{
|
||||
MFThing: cfg.MFThing,
|
||||
MFKey: cfg.MFKey,
|
||||
Channels: channels,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Content: cfg.Content,
|
||||
State: cfg.State,
|
||||
return res, nil
|
||||
default:
|
||||
res := listRes{
|
||||
Configs: []viewRes{},
|
||||
}
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
for _, cfg := range configs {
|
||||
var channels []channelRes
|
||||
for _, ch := range cfg.MFChannels {
|
||||
channels = append(channels, channelRes{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
view := viewRes{
|
||||
MFThing: cfg.MFThing,
|
||||
MFKey: cfg.MFKey,
|
||||
Channels: channels,
|
||||
ExternalID: cfg.ExternalID,
|
||||
ExternalKey: cfg.ExternalKey,
|
||||
Name: cfg.Name,
|
||||
Content: cfg.Content,
|
||||
State: cfg.State,
|
||||
}
|
||||
res.Configs = append(res.Configs, view)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,7 @@ const (
|
|||
metadata = `{"meta": "data"}`
|
||||
addExternalID = "external-id"
|
||||
addExternalKey = "external-key"
|
||||
addName = "name"
|
||||
addContent = "config"
|
||||
)
|
||||
|
||||
|
@ -49,12 +50,14 @@ var (
|
|||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Channels []string `json:"channels"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}{
|
||||
ExternalID: addExternalID,
|
||||
ExternalKey: addExternalKey,
|
||||
Channels: addChannels,
|
||||
Content: addContent,
|
||||
ExternalID: "external-id",
|
||||
ExternalKey: "external-key",
|
||||
Channels: []string{"1"},
|
||||
Name: "name",
|
||||
Content: "config",
|
||||
}
|
||||
|
||||
updateReq = struct {
|
||||
|
@ -82,6 +85,7 @@ func newConfig(channels []bootstrap.Channel) bootstrap.Config {
|
|||
ExternalID: addExternalID,
|
||||
ExternalKey: addExternalKey,
|
||||
MFChannels: channels,
|
||||
Name: addName,
|
||||
Content: addContent,
|
||||
}
|
||||
}
|
||||
|
@ -282,6 +286,7 @@ func TestView(t *testing.T) {
|
|||
Channels: channels,
|
||||
ExternalID: saved.ExternalID,
|
||||
ExternalKey: saved.ExternalKey,
|
||||
Name: saved.Name,
|
||||
Content: saved.Content,
|
||||
})
|
||||
|
||||
|
@ -464,7 +469,8 @@ func TestList(t *testing.T) {
|
|||
for i := 0; i < configNum; i++ {
|
||||
c.ExternalID = strconv.Itoa(i)
|
||||
c.MFKey = c.ExternalID
|
||||
c.ExternalKey = fmt.Sprintf("%s%s", c.ExternalKey, strconv.Itoa(i))
|
||||
c.Name = fmt.Sprintf("%s-%d", addName, i)
|
||||
c.ExternalKey = fmt.Sprintf("%s%s", addExternalKey, strconv.Itoa(i))
|
||||
saved, err := svc.Add(validToken, c)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
var channels []channel
|
||||
|
@ -477,6 +483,7 @@ func TestList(t *testing.T) {
|
|||
Channels: channels,
|
||||
ExternalID: saved.ExternalID,
|
||||
ExternalKey: saved.ExternalKey,
|
||||
Name: saved.Name,
|
||||
Content: saved.Content,
|
||||
State: saved.State,
|
||||
}
|
||||
|
@ -527,6 +534,13 @@ func TestList(t *testing.T) {
|
|||
status: http.StatusOK,
|
||||
res: list[0:1],
|
||||
},
|
||||
{
|
||||
desc: "view list searching by name",
|
||||
auth: validToken,
|
||||
url: fmt.Sprintf("%s/configs?offset=%d&limit=%d&name=%s", bs.URL, 0, 100, "95"),
|
||||
status: http.StatusOK,
|
||||
res: list[95:96],
|
||||
},
|
||||
{
|
||||
desc: "view last page",
|
||||
auth: validToken,
|
||||
|
@ -1001,5 +1015,6 @@ type config struct {
|
|||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name"`
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ type addReq struct {
|
|||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key"`
|
||||
Channels []string `json:"channels"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
|
@ -46,6 +47,7 @@ type updateReq struct {
|
|||
key string
|
||||
id string
|
||||
Channels []string `json:"channels"`
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
|
|
@ -78,6 +78,7 @@ type viewRes struct {
|
|||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
State bootstrap.State `json:"state"`
|
||||
}
|
||||
|
||||
|
@ -93,6 +94,27 @@ func (res viewRes) Empty() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
type unknownRes struct {
|
||||
ExternalID string `json:"external_id"`
|
||||
ExternalKey string `json:"external_key,omitempty"`
|
||||
}
|
||||
|
||||
type listUnknownRes struct {
|
||||
Configs []unknownRes `json:"configs"`
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Code() int {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Headers() map[string]string {
|
||||
return map[string]string{}
|
||||
}
|
||||
|
||||
func (res listUnknownRes) Empty() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type listRes struct {
|
||||
Configs []viewRes `json:"configs"`
|
||||
}
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
|
||||
|
@ -34,7 +35,8 @@ const (
|
|||
var (
|
||||
errUnsupportedContentType = errors.New("unsupported content type")
|
||||
errInvalidQueryParams = errors.New("invalid query params")
|
||||
validParams = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
|
||||
fullMatch = []string{"state", "external_id", "mainflux_id", "mainflux_key"}
|
||||
partialMatch = []string{"name"}
|
||||
)
|
||||
|
||||
// MakeHandler returns a HTTP handler for API endpoints.
|
||||
|
@ -138,7 +140,7 @@ func decodeUnknownRequest(_ context.Context, r *http.Request) (interface{}, erro
|
|||
|
||||
req := listReq{
|
||||
key: r.Header.Get("Authorization"),
|
||||
filter: bootstrap.Filter{"unknown": "true"},
|
||||
filter: bootstrap.Filter{Unknown: true},
|
||||
offset: offset,
|
||||
limit: limit,
|
||||
}
|
||||
|
@ -257,6 +259,7 @@ func parseUint(s string) (uint64, error) {
|
|||
if err != nil {
|
||||
return 0, errInvalidQueryParams
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
|
@ -285,12 +288,19 @@ func parsePagePrams(q url.Values) (uint64, uint64, error) {
|
|||
}
|
||||
|
||||
func parseFilter(values url.Values) bootstrap.Filter {
|
||||
ret := bootstrap.Filter{}
|
||||
ret := bootstrap.Filter{
|
||||
FullMatch: make(map[string]string),
|
||||
PartialMatch: make(map[string]string),
|
||||
}
|
||||
for k := range values {
|
||||
if contains(validParams, k) {
|
||||
ret[k] = values.Get(k)
|
||||
if contains(fullMatch, k) {
|
||||
ret.FullMatch[k] = values.Get(k)
|
||||
}
|
||||
if contains(partialMatch, k) {
|
||||
ret.PartialMatch[k] = strings.ToLower(values.Get(k))
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ const (
|
|||
type Config struct {
|
||||
MFThing string
|
||||
Owner string
|
||||
Name string
|
||||
MFKey string
|
||||
MFChannels []Channel
|
||||
ExternalID string
|
||||
|
@ -38,7 +39,11 @@ type Channel struct {
|
|||
}
|
||||
|
||||
// Filter is used for the search filters.
|
||||
type Filter map[string]string
|
||||
type Filter struct {
|
||||
Unknown bool
|
||||
FullMatch map[string]string
|
||||
PartialMatch map[string]string
|
||||
}
|
||||
|
||||
// ConfigRepository specifies a Config persistence API.
|
||||
type ConfigRepository interface {
|
||||
|
|
|
@ -10,6 +10,7 @@ package mocks
|
|||
import (
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/mainflux/mainflux/bootstrap"
|
||||
|
@ -73,15 +74,22 @@ func (crm *configRepositoryMock) RetrieveAll(key string, filter bootstrap.Filter
|
|||
first := uint64(offset) + 1
|
||||
last := first + uint64(limit)
|
||||
var state bootstrap.State = -1
|
||||
if s, ok := filter["state"]; ok {
|
||||
var name string
|
||||
if s, ok := filter.FullMatch["state"]; ok {
|
||||
val, _ := strconv.Atoi(s)
|
||||
state = bootstrap.State(val)
|
||||
}
|
||||
|
||||
if s, ok := filter.PartialMatch["name"]; ok {
|
||||
name = strings.ToLower(s)
|
||||
}
|
||||
|
||||
for _, v := range crm.configs {
|
||||
id, _ := strconv.ParseUint(v.MFThing, 10, 64)
|
||||
if id >= first && id < last {
|
||||
if (state == -1 || v.State == state) && v.Owner == key {
|
||||
if (state == -1 || v.State == state) &&
|
||||
(name == "" || strings.Index(strings.ToLower(v.Name), name) != -1) &&
|
||||
v.Owner == key {
|
||||
configs = append(configs, v)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ type configRepository struct {
|
|||
log logger.Logger
|
||||
}
|
||||
|
||||
type writeCh struct {
|
||||
type dbChannel struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Metadata interface{} `json:"metadata"`
|
||||
|
@ -57,17 +57,18 @@ func nullString(s string) sql.NullString {
|
|||
}
|
||||
|
||||
func (cr configRepository) Save(cfg bootstrap.Config) (string, error) {
|
||||
q := `INSERT INTO configs (mainflux_thing, owner, mainflux_key, external_id, external_key, content, state, mainflux_channels)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`
|
||||
q := `INSERT INTO configs (mainflux_thing, owner, name, mainflux_key, external_id, external_key, content, state, mainflux_channels)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`
|
||||
|
||||
channels := toDBChannels(cfg.MFChannels)
|
||||
|
||||
jsn, err := json.Marshal(channels)
|
||||
if err != nil {
|
||||
return "", bootstrap.ErrMalformedEntity
|
||||
}
|
||||
content := nullString(cfg.Content)
|
||||
name := nullString(cfg.Name)
|
||||
|
||||
if _, err := cr.db.Exec(q, cfg.MFThing, cfg.Owner, cfg.MFKey, cfg.ExternalID, cfg.ExternalKey, cfg.Content, cfg.State, jsn); err != nil {
|
||||
if _, err := cr.db.Exec(q, cfg.MFThing, cfg.Owner, name, cfg.MFKey, cfg.ExternalID, cfg.ExternalKey, content, cfg.State, jsn); err != nil {
|
||||
if pqErr, ok := err.(*pq.Error); ok && pqErr.Code.Name() == duplicateErr {
|
||||
return "", bootstrap.ErrConflict
|
||||
}
|
||||
|
@ -78,12 +79,12 @@ func (cr configRepository) Save(cfg bootstrap.Config) (string, error) {
|
|||
}
|
||||
|
||||
func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error) {
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, content, state, mainflux_channels FROM configs WHERE mainflux_thing = $1 AND owner = $2`
|
||||
q := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state, mainflux_channels FROM configs WHERE mainflux_thing = $1 AND owner = $2`
|
||||
cfg := bootstrap.Config{MFThing: id, Owner: key, MFChannels: []bootstrap.Channel{}}
|
||||
var content sql.NullString
|
||||
var name, content sql.NullString
|
||||
var chs []byte
|
||||
if err := cr.db.QueryRow(q, id, key).
|
||||
Scan(&cfg.MFThing, &cfg.MFKey, &cfg.ExternalID, &cfg.ExternalKey, &content, &cfg.State, &chs); err != nil {
|
||||
Scan(&cfg.MFThing, &cfg.MFKey, &cfg.ExternalID, &cfg.ExternalKey, &name, &content, &cfg.State, &chs); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, bootstrap.ErrNotFound
|
||||
|
@ -96,6 +97,7 @@ func (cr configRepository) RetrieveByID(key, id string) (bootstrap.Config, error
|
|||
}
|
||||
|
||||
cfg.Content = content.String
|
||||
cfg.Name = name.String
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
@ -108,13 +110,13 @@ func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offs
|
|||
}
|
||||
defer rows.Close()
|
||||
|
||||
var content sql.NullString
|
||||
var name, content sql.NullString
|
||||
configs := []bootstrap.Config{}
|
||||
|
||||
for rows.Next() {
|
||||
var chs []byte
|
||||
c := bootstrap.Config{Owner: key}
|
||||
if err := rows.Scan(&c.MFThing, &c.MFKey, &c.ExternalID, &c.ExternalKey, &content, &c.State, &chs); err != nil {
|
||||
if err := rows.Scan(&c.MFThing, &c.MFKey, &c.ExternalID, &c.ExternalKey, &name, &content, &c.State, &chs); err != nil {
|
||||
cr.log.Error(fmt.Sprintf("Failed to read retrieved config due to %s", err))
|
||||
return []bootstrap.Config{}
|
||||
}
|
||||
|
@ -124,6 +126,7 @@ func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offs
|
|||
return []bootstrap.Config{}
|
||||
}
|
||||
|
||||
c.Name = name.String
|
||||
c.Content = content.String
|
||||
configs = append(configs, c)
|
||||
}
|
||||
|
@ -132,8 +135,10 @@ func (cr configRepository) RetrieveAll(key string, filter bootstrap.Filter, offs
|
|||
}
|
||||
|
||||
func (cr configRepository) RetrieveByExternalID(externalKey, externalID string) (bootstrap.Config, error) {
|
||||
q := `SELECT mainflux_thing, owner, mainflux_key, content, state, mainflux_channels FROM configs WHERE external_key = $1 AND external_id = $2`
|
||||
var content sql.NullString
|
||||
q := `SELECT mainflux_thing, owner, mainflux_key, name, content, state, mainflux_channels
|
||||
FROM configs WHERE external_key = $1 AND external_id = $2`
|
||||
|
||||
var name, content sql.NullString
|
||||
cfg := bootstrap.Config{
|
||||
ExternalID: externalID,
|
||||
ExternalKey: externalKey,
|
||||
|
@ -141,7 +146,7 @@ func (cr configRepository) RetrieveByExternalID(externalKey, externalID string)
|
|||
|
||||
var chs []byte
|
||||
if err := cr.db.QueryRow(q, externalKey, externalID).
|
||||
Scan(&cfg.MFThing, &cfg.Owner, &cfg.MFKey, &content, &cfg.State, &chs); err != nil {
|
||||
Scan(&cfg.MFThing, &cfg.Owner, &cfg.MFKey, &name, &content, &cfg.State, &chs); err != nil {
|
||||
empty := bootstrap.Config{}
|
||||
if err == sql.ErrNoRows {
|
||||
return empty, bootstrap.ErrNotFound
|
||||
|
@ -159,7 +164,7 @@ func (cr configRepository) RetrieveByExternalID(externalKey, externalID string)
|
|||
}
|
||||
|
||||
func (cr configRepository) Update(cfg bootstrap.Config) error {
|
||||
q := `UPDATE configs SET content = $1, state = $2, mainflux_channels = $3 WHERE mainflux_thing = $4 AND owner = $5`
|
||||
q := `UPDATE configs SET name = $1, content = $2, state = $3, mainflux_channels = $4 WHERE mainflux_thing = $5 AND owner = $6`
|
||||
|
||||
channels := toDBChannels(cfg.MFChannels)
|
||||
|
||||
|
@ -169,7 +174,10 @@ func (cr configRepository) Update(cfg bootstrap.Config) error {
|
|||
return bootstrap.ErrMalformedEntity
|
||||
}
|
||||
|
||||
res, err := cr.db.Exec(q, cfg.Content, cfg.State, jsn, cfg.MFThing, cfg.Owner)
|
||||
name := nullString(cfg.Name)
|
||||
content := nullString(cfg.Content)
|
||||
|
||||
res, err := cr.db.Exec(q, name, content, cfg.State, jsn, cfg.MFThing, cfg.Owner)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -189,6 +197,7 @@ func (cr configRepository) Update(cfg bootstrap.Config) error {
|
|||
func (cr configRepository) Remove(key, id string) error {
|
||||
q := `DELETE FROM configs WHERE mainflux_thing = $1 AND owner = $2`
|
||||
cr.db.Exec(q, id, key)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -251,31 +260,45 @@ func (cr configRepository) RetrieveUnknown(offset, limit uint64) []bootstrap.Con
|
|||
func (cr configRepository) RemoveUnknown(key, id string) error {
|
||||
q := `DELETE FROM unknown_configs WHERE external_id = $1 AND external_key = $2`
|
||||
_, err := cr.db.Exec(q, id, key)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (cr configRepository) retrieveAll(key string, filter bootstrap.Filter, offset, limit uint64) (*sql.Rows, error) {
|
||||
template := `SELECT mainflux_thing, mainflux_key, external_id, external_key, content, state, mainflux_channels FROM configs WHERE owner = $1 %s ORDER BY mainflux_thing LIMIT $2 OFFSET $3`
|
||||
template := `SELECT mainflux_thing, mainflux_key, external_id, external_key, name, content, state, mainflux_channels
|
||||
FROM configs WHERE owner = $1 %s ORDER BY mainflux_thing LIMIT $2 OFFSET $3`
|
||||
params := []interface{}{key, limit, offset}
|
||||
// One empty string so that strings Join works if only one filter is applied.
|
||||
queries := []string{""}
|
||||
// Since key = 1, limit = 2, offset = 3, the next one is 4.
|
||||
counter := len(params) + 1
|
||||
for k, v := range filter {
|
||||
for k, v := range filter.FullMatch {
|
||||
queries = append(queries, fmt.Sprintf("%s = $%d", k, counter))
|
||||
params = append(params, v)
|
||||
counter++
|
||||
}
|
||||
for k, v := range filter.PartialMatch {
|
||||
queries = append(queries, fmt.Sprintf("LOWER(%s) LIKE '%%' || $%d || '%%'", k, counter))
|
||||
params = append(params, v)
|
||||
counter++
|
||||
}
|
||||
|
||||
f := strings.Join(queries, " AND ")
|
||||
|
||||
return cr.db.Query(fmt.Sprintf(template, f), params...)
|
||||
}
|
||||
|
||||
func toDBChannels(channels []bootstrap.Channel) []writeCh {
|
||||
ret := []writeCh{}
|
||||
func toDBChannels(channels []bootstrap.Channel) []dbChannel {
|
||||
ret := []dbChannel{}
|
||||
for _, ch := range channels {
|
||||
c := writeCh{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata}
|
||||
c := dbChannel{
|
||||
ID: ch.ID,
|
||||
Name: ch.Name,
|
||||
Metadata: ch.Metadata,
|
||||
}
|
||||
|
||||
ret = append(ret, c)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
|
|
@ -114,6 +114,7 @@ func TestRetrieveAll(t *testing.T) {
|
|||
// Use UUID to prevent conflict errors.
|
||||
id := uuid.NewV4().String()
|
||||
c.ExternalID = id
|
||||
c.Name = fmt.Sprintf("name %d", i)
|
||||
c.MFThing = id
|
||||
c.MFKey = id
|
||||
if i%2 == 0 {
|
||||
|
@ -156,9 +157,17 @@ func TestRetrieveAll(t *testing.T) {
|
|||
owner: config.Owner,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{"state": bootstrap.Active.String()},
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
size: numConfigs / 2,
|
||||
},
|
||||
{
|
||||
desc: "retrieve search by name",
|
||||
owner: config.Owner,
|
||||
offset: 0,
|
||||
limit: uint64(numConfigs),
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "1"}},
|
||||
size: 1,
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
ret := repo.RetrieveAll(tc.owner, tc.filter, tc.offset, tc.limit)
|
||||
|
|
|
@ -54,7 +54,8 @@ func migrateDB(db *sql.DB) error {
|
|||
Up: []string{
|
||||
`CREATE TABLE IF NOT EXISTS configs (
|
||||
mainflux_thing TEXT UNIQUE NOT NULL,
|
||||
owner VARCHAR(254),
|
||||
owner VARCHAR(254),
|
||||
name TEXT,
|
||||
mainflux_key CHAR(36) UNIQUE NOT NULL,
|
||||
mainflux_channels jsonb,
|
||||
external_id TEXT UNIQUE NOT NULL,
|
||||
|
|
|
@ -59,11 +59,13 @@ func (r reader) ReadConfig(cfg Config) (mainflux.Response, error) {
|
|||
for _, ch := range cfg.MFChannels {
|
||||
channels = append(channels, channelRes{ID: ch.ID, Name: ch.Name, Metadata: ch.Metadata})
|
||||
}
|
||||
|
||||
res := bootstrapRes{
|
||||
MFKey: cfg.MFKey,
|
||||
MFThing: cfg.MFThing,
|
||||
MFChannels: channels,
|
||||
Content: cfg.Content,
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
|
|
@ -135,6 +135,7 @@ func (bs bootstrapService) Add(key string, cfg Config) (Config, error) {
|
|||
bs.configs.RemoveUnknown(cfg.ExternalKey, cfg.ExternalID)
|
||||
|
||||
cfg.MFThing = id
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
|
@ -143,6 +144,7 @@ func (bs bootstrapService) View(key, id string) (Config, error) {
|
|||
if err != nil {
|
||||
return Config{}, err
|
||||
}
|
||||
|
||||
return bs.configs.RetrieveByID(owner, id)
|
||||
}
|
||||
|
||||
|
@ -208,10 +210,8 @@ func (bs bootstrapService) List(key string, filter Filter, offset, limit uint64)
|
|||
if err != nil {
|
||||
return []Config{}, err
|
||||
}
|
||||
if filter == nil {
|
||||
return []Config{}, ErrMalformedEntity
|
||||
}
|
||||
if _, ok := filter["unknown"]; ok {
|
||||
|
||||
if filter.Unknown {
|
||||
return bs.configs.RetrieveUnknown(offset, limit), nil
|
||||
}
|
||||
|
||||
|
|
|
@ -250,6 +250,7 @@ func TestList(t *testing.T) {
|
|||
id := uuid.NewV4().String()
|
||||
c.ExternalID = id
|
||||
c.ExternalKey = id
|
||||
c.Name = fmt.Sprintf("%s-%d", config.Name, i)
|
||||
s, err := svc.Add(validToken, c)
|
||||
saved = append(saved, s)
|
||||
require.Nil(t, err, fmt.Sprintf("Saving config expected to succeed: %s.\n", err))
|
||||
|
@ -274,7 +275,7 @@ func TestList(t *testing.T) {
|
|||
err error
|
||||
}{
|
||||
{
|
||||
desc: "list config",
|
||||
desc: "list configs",
|
||||
config: saved[0:10],
|
||||
filter: bootstrap.Filter{},
|
||||
key: validToken,
|
||||
|
@ -283,7 +284,16 @@ func TestList(t *testing.T) {
|
|||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list config unauthorized",
|
||||
desc: "list configs with specified name",
|
||||
config: saved[95:96],
|
||||
filter: bootstrap.Filter{PartialMatch: map[string]string{"name": "95"}},
|
||||
key: validToken,
|
||||
offset: 0,
|
||||
limit: 100,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list configs unauthorized",
|
||||
config: []bootstrap.Config{},
|
||||
filter: bootstrap.Filter{},
|
||||
key: invalidToken,
|
||||
|
@ -291,15 +301,6 @@ func TestList(t *testing.T) {
|
|||
limit: 10,
|
||||
err: bootstrap.ErrUnauthorizedAccess,
|
||||
},
|
||||
{
|
||||
desc: "list config with invalid filter",
|
||||
config: []bootstrap.Config{},
|
||||
filter: nil,
|
||||
key: validToken,
|
||||
offset: 0,
|
||||
limit: 10,
|
||||
err: bootstrap.ErrMalformedEntity,
|
||||
},
|
||||
{
|
||||
desc: "list last page",
|
||||
config: saved[95:],
|
||||
|
@ -310,18 +311,18 @@ func TestList(t *testing.T) {
|
|||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list config with Active staate",
|
||||
desc: "list configs with Active staate",
|
||||
config: []bootstrap.Config{saved[41]},
|
||||
filter: bootstrap.Filter{"state": bootstrap.Active.String()},
|
||||
filter: bootstrap.Filter{FullMatch: map[string]string{"state": bootstrap.Active.String()}},
|
||||
key: validToken,
|
||||
offset: 35,
|
||||
limit: 20,
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
desc: "list unknown config",
|
||||
desc: "list unknown configs",
|
||||
config: []bootstrap.Config{unknownConfig},
|
||||
filter: bootstrap.Filter{"unknown": "true"},
|
||||
filter: bootstrap.Filter{Unknown: true},
|
||||
key: validToken,
|
||||
offset: 0,
|
||||
limit: 20,
|
||||
|
|
|
@ -53,6 +53,7 @@ paths:
|
|||
- $ref: "#/parameters/Limit"
|
||||
- $ref: "#/parameters/Offset"
|
||||
- $ref: "#/parameters/State"
|
||||
- $ref: "#/parameters/Name"
|
||||
responses:
|
||||
200:
|
||||
description: Data retrieved.
|
||||
|
@ -263,6 +264,12 @@ parameters:
|
|||
- inactive
|
||||
- active
|
||||
required: false
|
||||
Name:
|
||||
name: name
|
||||
description: Name of the config. Search by name is partial-match and case-insensitive.
|
||||
in: query
|
||||
type: string
|
||||
required: false
|
||||
|
||||
responses:
|
||||
ServiceError:
|
||||
|
|
Loading…
Reference in New Issue