• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / inventory / 777635886

pending completion
777635886

push

gitlab-ci

GitHub
Merge pull request #371 from tranchitella/me-50

7 of 7 new or added lines in 1 file covered. (100.0%)

12 existing lines in 1 file now uncovered.

3091 of 3375 relevant lines covered (91.59%)

183.09 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.62
/store/mongo/datastore_mongo.go
1
// Copyright 2023 Northern.tech AS
2
//
3
//    Licensed under the Apache License, Version 2.0 (the "License");
4
//    you may not use this file except in compliance with the License.
5
//    You may obtain a copy of the License at
6
//
7
//        http://www.apache.org/licenses/LICENSE-2.0
8
//
9
//    Unless required by applicable law or agreed to in writing, software
10
//    distributed under the License is distributed on an "AS IS" BASIS,
11
//    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
//    See the License for the specific language governing permissions and
13
//    limitations under the License.
14

15
package mongo
16

17
import (
18
        "context"
19
        "crypto/tls"
20
        "fmt"
21
        "io"
22
        "strings"
23
        "sync"
24
        "time"
25

26
        "go.mongodb.org/mongo-driver/bson"
27
        "go.mongodb.org/mongo-driver/bson/primitive"
28
        "go.mongodb.org/mongo-driver/mongo"
29
        mopts "go.mongodb.org/mongo-driver/mongo/options"
30

31
        "github.com/google/uuid"
32
        "github.com/pkg/errors"
33

34
        "github.com/mendersoftware/go-lib-micro/log"
35
        mstore "github.com/mendersoftware/go-lib-micro/store"
36

37
        "github.com/mendersoftware/inventory/model"
38
        "github.com/mendersoftware/inventory/store"
39
        "github.com/mendersoftware/inventory/utils"
40
)
41

42
const (
43
        DbVersion = "1.1.0"
44

45
        DbName        = "inventory"
46
        DbDevicesColl = "devices"
47

48
        DbDevId              = "_id"
49
        DbDevAttributes      = "attributes"
50
        DbDevGroup           = "group"
51
        DbDevRevision        = "revision"
52
        DbDevUpdatedTs       = "updated_ts"
53
        DbDevAttributesText  = "text"
54
        DbDevAttributesTs    = "timestamp"
55
        DbDevAttributesDesc  = "description"
56
        DbDevAttributesValue = "value"
57
        DbDevAttributesScope = "scope"
58
        DbDevAttributesName  = "name"
59
        DbDevAttributesGroup = DbDevAttributes + "." +
60
                model.AttrScopeSystem + "-" + model.AttrNameGroup
61
        DbDevAttributesGroupValue = DbDevAttributesGroup + "." +
62
                DbDevAttributesValue
63

64
        DbScopeInventory = "inventory"
65

66
        FiltersAttributesLimit = 500
67

68
        attrIdentityStatus = "identity-status"
69
)
70

71
var (
72
        //with offcial mongodb supported driver we keep client
73
        clientGlobal *mongo.Client
74

75
        // once ensures client is created only once
76
        once sync.Once
77

78
        ErrNotFound = errors.New("mongo: no documents in result")
79
)
80

81
type DataStoreMongoConfig struct {
82
        // connection string
83
        ConnectionString string
84

85
        // SSL support
86
        SSL           bool
87
        SSLSkipVerify bool
88

89
        // Overwrites credentials provided in connection string if provided
90
        Username string
91
        Password string
92
}
93

94
type DataStoreMongo struct {
95
        client      *mongo.Client
96
        automigrate bool
97
}
98

99
func NewDataStoreMongoWithSession(client *mongo.Client) store.DataStore {
379✔
100
        return &DataStoreMongo{client: client}
379✔
101
}
379✔
102

103
// config.ConnectionString must contain a valid
104
func NewDataStoreMongo(config DataStoreMongoConfig) (store.DataStore, error) {
6✔
105
        //init master session
6✔
106
        var err error
6✔
107
        once.Do(func() {
11✔
108
                if !strings.Contains(config.ConnectionString, "://") {
10✔
109
                        config.ConnectionString = "mongodb://" + config.ConnectionString
5✔
110
                }
5✔
111
                clientOptions := mopts.Client().ApplyURI(config.ConnectionString)
5✔
112

5✔
113
                if config.Username != "" {
5✔
114
                        clientOptions.SetAuth(mopts.Credential{
×
115
                                Username: config.Username,
×
116
                                Password: config.Password,
×
117
                        })
×
118
                }
×
119

120
                if config.SSL {
5✔
121
                        tlsConfig := &tls.Config{}
×
122
                        tlsConfig.InsecureSkipVerify = config.SSLSkipVerify
×
123
                        clientOptions.SetTLSConfig(tlsConfig)
×
124
                }
×
125

126
                ctx := context.Background()
5✔
127
                l := log.FromContext(ctx)
5✔
128
                clientGlobal, err = mongo.Connect(ctx, clientOptions)
5✔
129
                if err != nil {
5✔
130
                        l.Errorf("mongo: error connecting to mongo '%s'", err.Error())
×
131
                        return
×
132
                }
×
133
                if clientGlobal == nil {
5✔
134
                        l.Errorf("mongo: client is nil. wow.")
×
135
                        return
×
136
                }
×
137
                // from: https://www.mongodb.com/blog/post/mongodb-go-driver-tutorial
138
                /*
139
                        It is best practice to keep a client that is connected to MongoDB around so that the
140
                        application can make use of connection pooling - you don't want to open and close a
141
                        connection for each query. However, if your application no longer requires a connection,
142
                        the connection can be closed with client.Disconnect() like so:
143
                */
144
                err = clientGlobal.Ping(ctx, nil)
5✔
145
                if err != nil {
7✔
146
                        clientGlobal = nil
2✔
147
                        l.Errorf("mongo: error pinging mongo '%s'", err.Error())
2✔
148
                        return
2✔
149
                }
2✔
150
                if clientGlobal == nil {
3✔
151
                        l.Errorf("mongo: global instance of client is nil.")
×
152
                        return
×
153
                }
×
154
        })
155

156
        if clientGlobal == nil {
8✔
157
                return nil, errors.New("failed to open mongo-driver session")
2✔
158
        }
2✔
159
        db := &DataStoreMongo{client: clientGlobal}
4✔
160

4✔
161
        return db, nil
4✔
162
}
163

164
func (db *DataStoreMongo) Ping(ctx context.Context) error {
2✔
165
        res := db.client.Database(DbName).RunCommand(ctx, bson.M{"ping": 1})
2✔
166
        return res.Err()
2✔
167
}
2✔
168

169
func (db *DataStoreMongo) GetDevices(
170
        ctx context.Context,
171
        q store.ListQuery,
172
) ([]model.Device, int, error) {
79✔
173
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
79✔
174

79✔
175
        queryFilters := make([]bson.M, 0)
79✔
176
        for _, filter := range q.Filters {
91✔
177
                op := mongoOperator(filter.Operator)
12✔
178
                name := fmt.Sprintf(
12✔
179
                        "%s-%s",
12✔
180
                        filter.AttrScope,
12✔
181
                        model.GetDeviceAttributeNameReplacer().Replace(filter.AttrName),
12✔
182
                )
12✔
183
                field := fmt.Sprintf("%s.%s.%s", DbDevAttributes, name, DbDevAttributesValue)
12✔
184
                switch filter.Operator {
12✔
185
                default:
12✔
186
                        if filter.ValueFloat != nil {
18✔
187
                                queryFilters = append(queryFilters, bson.M{"$or": []bson.M{
6✔
188
                                        {field: bson.M{op: filter.Value}},
6✔
189
                                        {field: bson.M{op: filter.ValueFloat}},
6✔
190
                                }})
6✔
191
                        } else if filter.ValueTime != nil {
14✔
192
                                queryFilters = append(queryFilters, bson.M{"$or": []bson.M{
2✔
193
                                        {field: bson.M{op: filter.Value}},
2✔
194
                                        {field: bson.M{op: filter.ValueTime}},
2✔
195
                                }})
2✔
196
                        } else {
6✔
197
                                queryFilters = append(queryFilters, bson.M{field: bson.M{op: filter.Value}})
4✔
198
                        }
4✔
199
                }
200
        }
201
        if q.GroupName != "" {
125✔
202
                groupFilter := bson.M{DbDevAttributesGroupValue: q.GroupName}
46✔
203
                queryFilters = append(queryFilters, groupFilter)
46✔
204
        }
46✔
205
        if q.GroupName != "" {
125✔
206
                groupFilter := bson.M{DbDevAttributesGroupValue: q.GroupName}
46✔
207
                queryFilters = append(queryFilters, groupFilter)
46✔
208
        }
46✔
209
        if q.HasGroup != nil {
130✔
210
                groupExistenceFilter := bson.M{
51✔
211
                        DbDevAttributesGroup: bson.M{
51✔
212
                                "$exists": *q.HasGroup,
51✔
213
                        },
51✔
214
                }
51✔
215
                queryFilters = append(queryFilters, groupExistenceFilter)
51✔
216
        }
51✔
217

218
        findQuery := bson.M{}
79✔
219
        if len(queryFilters) > 0 {
142✔
220
                findQuery["$and"] = queryFilters
63✔
221
        }
63✔
222

223
        findOptions := mopts.Find()
79✔
224
        if q.Skip > 0 {
97✔
225
                findOptions.SetSkip(int64(q.Skip))
18✔
226
        }
18✔
227
        if q.Limit > 0 {
148✔
228
                findOptions.SetLimit(int64(q.Limit))
69✔
229
        }
69✔
230
        if q.Sort != nil {
85✔
231
                name := fmt.Sprintf(
6✔
232
                        "%s-%s",
6✔
233
                        q.Sort.AttrScope,
6✔
234
                        model.GetDeviceAttributeNameReplacer().Replace(q.Sort.AttrName),
6✔
235
                )
6✔
236
                sortField := fmt.Sprintf("%s.%s.%s", DbDevAttributes, name, DbDevAttributesValue)
6✔
237
                sortFieldQuery := bson.D{{Key: sortField, Value: 1}}
6✔
238
                if !q.Sort.Ascending {
10✔
239
                        sortFieldQuery[0].Value = -1
4✔
240
                }
4✔
241
                findOptions.SetSort(sortFieldQuery)
6✔
242
        }
243

244
        cursor, err := c.Find(ctx, findQuery, findOptions)
79✔
245
        if err != nil {
79✔
246
                return nil, -1, errors.Wrap(err, "failed to search devices")
×
247
        }
×
248
        defer cursor.Close(ctx)
79✔
249

79✔
250
        devices := []model.Device{}
79✔
251
        if err = cursor.All(ctx, &devices); err != nil {
79✔
252
                return nil, -1, errors.Wrap(err, "failed to search devices")
×
253
        }
×
254

255
        count, err := c.CountDocuments(ctx, findQuery)
79✔
256
        if err != nil {
79✔
257
                return nil, -1, errors.Wrap(err, "failed to count devices")
×
258
        }
×
259

260
        return devices, int(count), nil
79✔
261
}
262

263
func (db *DataStoreMongo) GetDevice(
264
        ctx context.Context,
265
        id model.DeviceID,
266
) (*model.Device, error) {
48✔
267
        var res model.Device
48✔
268
        c := db.client.
48✔
269
                Database(mstore.DbFromContext(ctx, DbName)).
48✔
270
                Collection(DbDevicesColl)
48✔
271
        l := log.FromContext(ctx)
48✔
272

48✔
273
        if id == model.NilDeviceID {
52✔
274
                return nil, nil
4✔
275
        }
4✔
276
        if err := c.FindOne(ctx, bson.M{DbDevId: id}).Decode(&res); err != nil {
52✔
277
                switch err {
8✔
278
                case mongo.ErrNoDocuments:
8✔
279
                        return nil, nil
8✔
280
                default:
×
281
                        l.Errorf("GetDevice: %v", err)
×
282
                        return nil, errors.Wrap(err, "failed to fetch device")
×
283
                }
284
        }
285
        return &res, nil
36✔
286
}
287

288
// AddDevice inserts a new device, initializing the inventory data.
289
func (db *DataStoreMongo) AddDevice(ctx context.Context, dev *model.Device) error {
435✔
290
        if dev.Group != "" {
515✔
291
                dev.Attributes = append(dev.Attributes, model.DeviceAttribute{
80✔
292
                        Scope: model.AttrScopeSystem,
80✔
293
                        Name:  model.AttrNameGroup,
80✔
294
                        Value: dev.Group,
80✔
295
                })
80✔
296
        }
80✔
297
        _, err := db.UpsertDevicesAttributesWithUpdated(
435✔
298
                ctx, []model.DeviceID{dev.ID}, dev.Attributes, "", "",
435✔
299
        )
435✔
300
        if err != nil {
435✔
301
                return errors.Wrap(err, "failed to store device")
×
302
        }
×
303
        return nil
435✔
304
}
305

306
func (db *DataStoreMongo) UpsertDevicesAttributesWithRevision(
307
        ctx context.Context,
308
        devices []model.DeviceUpdate,
309
        attrs model.DeviceAttributes,
310
) (*model.UpdateResult, error) {
6✔
311
        return db.upsertAttributes(ctx, devices, attrs, false, true, "", "")
6✔
312
}
6✔
313

314
func (db *DataStoreMongo) UpsertDevicesAttributesWithUpdated(
315
        ctx context.Context,
316
        ids []model.DeviceID,
317
        attrs model.DeviceAttributes,
318
        scope string,
319
        etag string,
320
) (*model.UpdateResult, error) {
472✔
321
        return db.upsertAttributes(ctx, makeDevsWithIds(ids), attrs, true, false, scope, etag)
472✔
322
}
472✔
323

324
func (db *DataStoreMongo) UpsertDevicesAttributes(
325
        ctx context.Context,
326
        ids []model.DeviceID,
327
        attrs model.DeviceAttributes,
328
) (*model.UpdateResult, error) {
32✔
329
        return db.upsertAttributes(ctx, makeDevsWithIds(ids), attrs, false, false, "", "")
32✔
330
}
32✔
331

332
func makeDevsWithIds(ids []model.DeviceID) []model.DeviceUpdate {
504✔
333
        devices := make([]model.DeviceUpdate, len(ids))
504✔
334
        for i, id := range ids {
1,008✔
335
                devices[i].Id = id
504✔
336
        }
504✔
337
        return devices
504✔
338
}
339

340
func (db *DataStoreMongo) upsertAttributes(
341
        ctx context.Context,
342
        devices []model.DeviceUpdate,
343
        attrs model.DeviceAttributes,
344
        withUpdated bool,
345
        withRevision bool,
346
        scope string,
347
        etag string,
348
) (*model.UpdateResult, error) {
510✔
349
        const systemScope = DbDevAttributes + "." + model.AttrScopeSystem
510✔
350
        const createdField = systemScope + "-" + model.AttrNameCreated
510✔
351
        const etagField = model.AttrNameTagsEtag
510✔
352
        var (
510✔
353
                result *model.UpdateResult
510✔
354
                filter interface{}
510✔
355
                err    error
510✔
356
        )
510✔
357

510✔
358
        c := db.client.
510✔
359
                Database(mstore.DbFromContext(ctx, DbName)).
510✔
360
                Collection(DbDevicesColl)
510✔
361

510✔
362
        update, err := makeAttrUpsert(attrs)
510✔
363
        if err != nil {
514✔
364
                return nil, err
4✔
365
        }
4✔
366

367
        now := time.Now()
506✔
368
        oninsert := bson.M{
506✔
369
                createdField: model.DeviceAttribute{
506✔
370
                        Scope: model.AttrScopeSystem,
506✔
371
                        Name:  model.AttrNameCreated,
506✔
372
                        Value: now,
506✔
373
                },
506✔
374
        }
506✔
375
        if !withRevision {
1,006✔
376
                oninsert["revision"] = 0
500✔
377
        }
500✔
378

379
        const updatedField = systemScope + "-" + model.AttrNameUpdated
506✔
380
        if withUpdated {
976✔
381
                update[updatedField] = model.DeviceAttribute{
470✔
382
                        Scope: model.AttrScopeSystem,
470✔
383
                        Name:  model.AttrNameUpdated,
470✔
384
                        Value: now,
470✔
385
                }
470✔
386
        } else {
506✔
387
                oninsert[updatedField] = model.DeviceAttribute{
36✔
388
                        Scope: model.AttrScopeSystem,
36✔
389
                        Name:  model.AttrNameUpdated,
36✔
390
                        Value: now,
36✔
391
                }
36✔
392
        }
36✔
393

394
        switch len(devices) {
506✔
395
        case 0:
4✔
396
                return &model.UpdateResult{}, nil
4✔
397
        case 1:
496✔
398
                filter := bson.M{
496✔
399
                        "_id": devices[0].Id,
496✔
400
                }
496✔
401
                updateOpts := mopts.FindOneAndUpdate().
496✔
402
                        SetUpsert(true).
496✔
403
                        SetReturnDocument(mopts.After)
496✔
404

496✔
405
                if withRevision {
500✔
406
                        filter[DbDevRevision] = bson.M{"$lt": devices[0].Revision}
4✔
407
                        update[DbDevRevision] = devices[0].Revision
4✔
408
                }
4✔
409
                if scope == model.AttrScopeTags {
501✔
410
                        update[etagField] = uuid.New().String()
5✔
411
                        updateOpts = mopts.FindOneAndUpdate().
5✔
412
                                SetUpsert(false).
5✔
413
                                SetReturnDocument(mopts.After)
5✔
414
                }
5✔
415
                if etag != "" {
497✔
416
                        filter[etagField] = bson.M{"$eq": etag}
1✔
417
                }
1✔
418

419
                update = bson.M{
496✔
420
                        "$set":         update,
496✔
421
                        "$setOnInsert": oninsert,
496✔
422
                }
496✔
423

496✔
424
                device := &model.Device{}
496✔
425
                res := c.FindOneAndUpdate(ctx, filter, update, updateOpts)
496✔
426
                err = res.Decode(device)
496✔
427
                if err != nil {
499✔
428
                        if mongo.IsDuplicateKeyError(err) {
5✔
429
                                return nil, store.ErrWriteConflict
2✔
430
                        } else if err == mongo.ErrNoDocuments {
4✔
431
                                return &model.UpdateResult{}, nil
1✔
432
                        } else {
1✔
433
                                return nil, err
×
434
                        }
×
435
                }
436
                result = &model.UpdateResult{
493✔
437
                        MatchedCount: 1,
493✔
438
                        CreatedCount: 0,
493✔
439
                        Devices:      []*model.Device{device},
493✔
440
                }
493✔
441
        default:
6✔
442
                var bres *mongo.BulkWriteResult
6✔
443
                // Perform single bulk-write operation
6✔
444
                // NOTE: Can't use UpdateMany as $in query operator does not
6✔
445
                //       upsert missing devices.
6✔
446

6✔
447
                models := make([]mongo.WriteModel, len(devices))
6✔
448
                for i, dev := range devices {
24✔
449
                        umod := mongo.NewUpdateOneModel()
18✔
450
                        if withRevision {
24✔
451
                                filter = bson.M{
6✔
452
                                        "_id":         dev.Id,
6✔
453
                                        DbDevRevision: bson.M{"$lt": dev.Revision},
6✔
454
                                }
6✔
455
                                update[DbDevRevision] = dev.Revision
6✔
456
                                umod.Update = bson.M{
6✔
457
                                        "$set":         update,
6✔
458
                                        "$setOnInsert": oninsert,
6✔
459
                                }
6✔
460
                        } else {
18✔
461
                                filter = map[string]interface{}{"_id": dev.Id}
12✔
462
                                umod.Update = bson.M{
12✔
463
                                        "$set":         update,
12✔
464
                                        "$setOnInsert": oninsert,
12✔
465
                                }
12✔
466
                        }
12✔
467
                        umod.Filter = filter
18✔
468
                        umod.SetUpsert(true)
18✔
469
                        models[i] = umod
18✔
470
                }
471
                bres, err = c.BulkWrite(
6✔
472
                        ctx, models, mopts.BulkWrite().SetOrdered(false),
6✔
473
                )
6✔
474
                if err != nil {
8✔
475
                        if mongo.IsDuplicateKeyError(err) {
4✔
476
                                // bulk mode, swallow the error as we already updated the other devices
2✔
477
                                // and the Matchedcount and CreatedCount values will tell the caller if
2✔
478
                                // all the operations succeeded or not
2✔
479
                                err = nil
2✔
480
                        } else {
2✔
481
                                return nil, err
×
482
                        }
×
483
                }
484
                result = &model.UpdateResult{
6✔
485
                        MatchedCount: bres.MatchedCount,
6✔
486
                        CreatedCount: bres.UpsertedCount,
6✔
487
                }
6✔
488
        }
489
        return result, err
499✔
490
}
491

492
// makeAttrField is a convenience function for composing attribute field names.
493
func makeAttrField(attrName, attrScope string, subFields ...string) string {
6,143✔
494
        field := fmt.Sprintf(
6,143✔
495
                "%s.%s-%s",
6,143✔
496
                DbDevAttributes,
6,143✔
497
                attrScope,
6,143✔
498
                model.GetDeviceAttributeNameReplacer().Replace(attrName),
6,143✔
499
        )
6,143✔
500
        if len(subFields) > 0 {
12,284✔
501
                field = strings.Join(
6,141✔
502
                        append([]string{field}, subFields...), ".",
6,141✔
503
                )
6,141✔
504
        }
6,141✔
505
        return field
6,143✔
506
}
507

508
// makeAttrUpsert creates a new upsert document for the given attributes.
509
func makeAttrUpsert(attrs model.DeviceAttributes) (bson.M, error) {
558✔
510
        var fieldName string
558✔
511
        upsert := make(bson.M)
558✔
512

558✔
513
        for i := range attrs {
2,361✔
514
                if attrs[i].Name == "" {
1,809✔
515
                        return nil, store.ErrNoAttrName
6✔
516
                }
6✔
517
                if attrs[i].Scope == "" {
1,809✔
518
                        // Default to inventory scope
12✔
519
                        attrs[i].Scope = model.AttrScopeInventory
12✔
520
                }
12✔
521

522
                fieldName = makeAttrField(
1,797✔
523
                        attrs[i].Name,
1,797✔
524
                        attrs[i].Scope,
1,797✔
525
                        DbDevAttributesScope,
1,797✔
526
                )
1,797✔
527
                upsert[fieldName] = attrs[i].Scope
1,797✔
528

1,797✔
529
                fieldName = makeAttrField(
1,797✔
530
                        attrs[i].Name,
1,797✔
531
                        attrs[i].Scope,
1,797✔
532
                        DbDevAttributesName,
1,797✔
533
                )
1,797✔
534
                upsert[fieldName] = attrs[i].Name
1,797✔
535

1,797✔
536
                if attrs[i].Value != nil {
3,584✔
537
                        fieldName = makeAttrField(
1,787✔
538
                                attrs[i].Name,
1,787✔
539
                                attrs[i].Scope,
1,787✔
540
                                DbDevAttributesValue,
1,787✔
541
                        )
1,787✔
542
                        upsert[fieldName] = attrs[i].Value
1,787✔
543
                }
1,787✔
544

545
                if attrs[i].Description != nil {
2,545✔
546
                        fieldName = makeAttrField(
748✔
547
                                attrs[i].Name,
748✔
548
                                attrs[i].Scope,
748✔
549
                                DbDevAttributesDesc,
748✔
550
                        )
748✔
551
                        upsert[fieldName] = attrs[i].Description
748✔
552
                }
748✔
553

554
                if attrs[i].Timestamp != nil {
1,809✔
555
                        fieldName = makeAttrField(
12✔
556
                                attrs[i].Name,
12✔
557
                                attrs[i].Scope,
12✔
558
                                DbDevAttributesTs,
12✔
559
                        )
12✔
560
                        upsert[fieldName] = attrs[i].Timestamp
12✔
561
                }
12✔
562
        }
563
        return upsert, nil
552✔
564
}
565

566
// makeAttrRemove creates a new unset document to remove attributes
567
func makeAttrRemove(attrs model.DeviceAttributes) (bson.M, error) {
46✔
568
        var fieldName string
46✔
569
        remove := make(bson.M)
46✔
570

46✔
571
        for i := range attrs {
48✔
572
                if attrs[i].Name == "" {
2✔
573
                        return nil, store.ErrNoAttrName
×
574
                }
×
575
                if attrs[i].Scope == "" {
2✔
576
                        // Default to inventory scope
×
577
                        attrs[i].Scope = model.AttrScopeInventory
×
578
                }
×
579
                fieldName = makeAttrField(
2✔
580
                        attrs[i].Name,
2✔
581
                        attrs[i].Scope,
2✔
582
                )
2✔
583
                remove[fieldName] = true
2✔
584
        }
585
        return remove, nil
46✔
586
}
587

588
func mongoOperator(co store.ComparisonOperator) string {
12✔
589
        switch co {
12✔
590
        case store.Eq:
12✔
591
                return "$eq"
12✔
592
        }
593
        return ""
×
594
}
595

596
func (db *DataStoreMongo) UpsertRemoveDeviceAttributes(
597
        ctx context.Context,
598
        id model.DeviceID,
599
        updateAttrs model.DeviceAttributes,
600
        removeAttrs model.DeviceAttributes,
601
        scope string,
602
        etag string,
603
) (*model.UpdateResult, error) {
48✔
604
        const systemScope = DbDevAttributes + "." + model.AttrScopeSystem
48✔
605
        const updatedField = systemScope + "-" + model.AttrNameUpdated
48✔
606
        const createdField = systemScope + "-" + model.AttrNameCreated
48✔
607
        const etagField = model.AttrNameTagsEtag
48✔
608
        var (
48✔
609
                err error
48✔
610
        )
48✔
611

48✔
612
        c := db.client.
48✔
613
                Database(mstore.DbFromContext(ctx, DbName)).
48✔
614
                Collection(DbDevicesColl)
48✔
615

48✔
616
        update, err := makeAttrUpsert(updateAttrs)
48✔
617
        if err != nil {
50✔
618
                return nil, err
2✔
619
        }
2✔
620
        remove, err := makeAttrRemove(removeAttrs)
46✔
621
        if err != nil {
46✔
622
                return nil, err
×
623
        }
×
624
        filter := bson.M{"_id": id}
46✔
625
        if etag != "" {
53✔
626
                filter[etagField] = bson.M{"$eq": etag}
7✔
627
        }
7✔
628

629
        updateOpts := mopts.FindOneAndUpdate().
46✔
630
                SetUpsert(true).
46✔
631
                SetReturnDocument(mopts.After)
46✔
632
        if scope == model.AttrScopeTags {
64✔
633
                update[etagField] = uuid.New().String()
18✔
634
                updateOpts = updateOpts.SetUpsert(false)
18✔
635
        }
18✔
636
        now := time.Now()
46✔
637
        if scope != model.AttrScopeTags {
74✔
638
                update[updatedField] = model.DeviceAttribute{
28✔
639
                        Scope: model.AttrScopeSystem,
28✔
640
                        Name:  model.AttrNameUpdated,
28✔
641
                        Value: now,
28✔
642
                }
28✔
643
        }
28✔
644
        update = bson.M{
46✔
645
                "$set": update,
46✔
646
                "$setOnInsert": bson.M{
46✔
647
                        createdField: model.DeviceAttribute{
46✔
648
                                Scope: model.AttrScopeSystem,
46✔
649
                                Name:  model.AttrNameCreated,
46✔
650
                                Value: now,
46✔
651
                        },
46✔
652
                },
46✔
653
        }
46✔
654
        if len(remove) > 0 {
48✔
655
                update["$unset"] = remove
2✔
656
        }
2✔
657

658
        device := &model.Device{}
46✔
659
        res := c.FindOneAndUpdate(ctx, filter, update, updateOpts)
46✔
660
        err = res.Decode(device)
46✔
661
        if err == mongo.ErrNoDocuments {
49✔
662
                return &model.UpdateResult{
3✔
663
                        MatchedCount: 0,
3✔
664
                        CreatedCount: 0,
3✔
665
                        Devices:      []*model.Device{},
3✔
666
                }, nil
3✔
667
        } else if err == nil {
89✔
668
                return &model.UpdateResult{
43✔
669
                        MatchedCount: 1,
43✔
670
                        CreatedCount: 0,
43✔
671
                        Devices:      []*model.Device{device},
43✔
672
                }, nil
43✔
673
        }
43✔
674
        return nil, err
×
675
}
676

677
func (db *DataStoreMongo) UpdateDevicesGroup(
678
        ctx context.Context,
679
        devIDs []model.DeviceID,
680
        group model.GroupName,
681
) (*model.UpdateResult, error) {
72✔
682
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
72✔
683
        collDevs := database.Collection(DbDevicesColl)
72✔
684

72✔
685
        var filter = bson.M{}
72✔
686
        switch len(devIDs) {
72✔
687
        case 0:
6✔
688
                return &model.UpdateResult{}, nil
6✔
689
        case 1:
58✔
690
                filter[DbDevId] = devIDs[0]
58✔
691
        default:
8✔
692
                filter[DbDevId] = bson.M{"$in": devIDs}
8✔
693
        }
694
        update := bson.M{
66✔
695
                "$set": bson.M{
66✔
696
                        DbDevAttributesGroup: model.DeviceAttribute{
66✔
697
                                Scope: model.AttrScopeSystem,
66✔
698
                                Name:  DbDevGroup,
66✔
699
                                Value: group,
66✔
700
                        },
66✔
701
                },
66✔
702
        }
66✔
703
        res, err := collDevs.UpdateMany(ctx, filter, update)
66✔
704
        if err != nil {
66✔
705
                return nil, err
×
706
        }
×
707
        return &model.UpdateResult{
66✔
708
                MatchedCount: res.MatchedCount,
66✔
709
                UpdatedCount: res.ModifiedCount,
66✔
710
        }, nil
66✔
711
}
712

713
// UpdateDeviceText updates the device text field
714
func (db *DataStoreMongo) UpdateDeviceText(
715
        ctx context.Context,
716
        deviceID model.DeviceID,
717
        text string,
718
) error {
37✔
719
        filter := bson.M{
37✔
720
                DbDevId: deviceID.String(),
37✔
721
        }
37✔
722

37✔
723
        update := bson.M{
37✔
724
                "$set": bson.M{
37✔
725
                        DbDevAttributesText: text,
37✔
726
                },
37✔
727
        }
37✔
728

37✔
729
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
37✔
730
        collDevs := database.Collection(DbDevicesColl)
37✔
731

37✔
732
        _, err := collDevs.UpdateOne(ctx, filter, update)
37✔
733
        return err
37✔
734
}
37✔
735

736
func (db *DataStoreMongo) GetFiltersAttributes(
737
        ctx context.Context,
738
) ([]model.FilterAttribute, error) {
4✔
739
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
4✔
740
        collDevs := database.Collection(DbDevicesColl)
4✔
741

4✔
742
        const DbCount = "count"
4✔
743

4✔
744
        cur, err := collDevs.Aggregate(ctx, []bson.M{
4✔
745
                {
4✔
746
                        "$project": bson.M{
4✔
747
                                "attributes": bson.M{
4✔
748
                                        "$objectToArray": "$" + DbDevAttributes,
4✔
749
                                },
4✔
750
                        },
4✔
751
                },
4✔
752
                {
4✔
753
                        "$unwind": "$" + DbDevAttributes,
4✔
754
                },
4✔
755
                {
4✔
756
                        "$project": bson.M{
4✔
757
                                DbDevAttributesName:  "$" + DbDevAttributes + ".v." + DbDevAttributesName,
4✔
758
                                DbDevAttributesScope: "$" + DbDevAttributes + ".v." + DbDevAttributesScope,
4✔
759
                        },
4✔
760
                },
4✔
761
                {
4✔
762
                        "$group": bson.M{
4✔
763
                                DbDevId: bson.M{
4✔
764
                                        DbDevAttributesName:  "$" + DbDevAttributesName,
4✔
765
                                        DbDevAttributesScope: "$" + DbDevAttributesScope,
4✔
766
                                },
4✔
767
                                DbCount: bson.M{
4✔
768
                                        "$sum": 1,
4✔
769
                                },
4✔
770
                        },
4✔
771
                },
4✔
772
                {
4✔
773
                        "$project": bson.M{
4✔
774
                                DbDevId:              0,
4✔
775
                                DbDevAttributesName:  "$" + DbDevId + "." + DbDevAttributesName,
4✔
776
                                DbDevAttributesScope: "$" + DbDevId + "." + DbDevAttributesScope,
4✔
777
                                DbCount:              "$" + DbCount,
4✔
778
                        },
4✔
779
                },
4✔
780
                {
4✔
781
                        "$sort": bson.D{
4✔
782
                                {Key: DbCount, Value: -1},
4✔
783
                                {Key: DbDevAttributesScope, Value: 1},
4✔
784
                                {Key: DbDevAttributesName, Value: 1},
4✔
785
                        },
4✔
786
                },
4✔
787
                {
4✔
788
                        "$limit": FiltersAttributesLimit,
4✔
789
                },
4✔
790
        })
4✔
791
        if err != nil {
4✔
792
                return nil, err
×
793
        }
×
794
        defer cur.Close(ctx)
4✔
795

4✔
796
        var attributes []model.FilterAttribute
4✔
797
        err = cur.All(ctx, &attributes)
4✔
798
        if err != nil {
4✔
799
                return nil, err
×
800
        }
×
801

802
        return attributes, nil
4✔
803
}
804

805
func (db *DataStoreMongo) DeleteGroup(
806
        ctx context.Context,
807
        group model.GroupName,
808
) (chan model.DeviceID, error) {
2✔
809
        deviceIDs := make(chan model.DeviceID)
2✔
810

2✔
811
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
2✔
812
        collDevs := database.Collection(DbDevicesColl)
2✔
813

2✔
814
        filter := bson.M{DbDevAttributesGroupValue: group}
2✔
815

2✔
816
        const batchMaxSize = 100
2✔
817
        batchSize := int32(batchMaxSize)
2✔
818
        findOptions := &mopts.FindOptions{
2✔
819
                Projection: bson.M{DbDevId: 1},
2✔
820
                BatchSize:  &batchSize,
2✔
821
        }
2✔
822
        cursor, err := collDevs.Find(ctx, filter, findOptions)
2✔
823
        if err != nil {
2✔
824
                return nil, err
×
825
        }
×
826

827
        go func() {
4✔
828
                defer cursor.Close(ctx)
2✔
829
                batch := make([]model.DeviceID, batchMaxSize)
2✔
830
                batchSize := 0
2✔
831

2✔
832
                update := bson.M{"$unset": bson.M{DbDevAttributesGroup: 1}}
2✔
833
                device := &model.Device{}
2✔
834
                defer close(deviceIDs)
2✔
835

2✔
836
        next:
2✔
837
                for {
10✔
838
                        hasNext := cursor.Next(ctx)
8✔
839
                        if !hasNext {
12✔
840
                                if batchSize > 0 {
6✔
841
                                        break
2✔
842
                                }
843
                                return
2✔
844
                        }
845
                        if err = cursor.Decode(&device); err == nil {
8✔
846
                                batch[batchSize] = device.ID
4✔
847
                                batchSize++
4✔
848
                                if len(batch) == batchSize {
4✔
849
                                        break
×
850
                                }
851
                        }
852
                }
853

854
                _, _ = collDevs.UpdateMany(ctx, bson.M{DbDevId: bson.M{"$in": batch[:batchSize]}}, update)
2✔
855
                for _, item := range batch[:batchSize] {
6✔
856
                        deviceIDs <- item
4✔
857
                }
4✔
858
                batchSize = 0
2✔
859
                goto next
2✔
860
        }()
861

862
        return deviceIDs, nil
2✔
863
}
864

865
func (db *DataStoreMongo) UnsetDevicesGroup(
866
        ctx context.Context,
867
        deviceIDs []model.DeviceID,
868
        group model.GroupName,
869
) (*model.UpdateResult, error) {
24✔
870
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
24✔
871
        collDevs := database.Collection(DbDevicesColl)
24✔
872

24✔
873
        var filter bson.D
24✔
874
        // Add filter on device id (either $in or direct indexing)
24✔
875
        switch len(deviceIDs) {
24✔
876
        case 0:
2✔
877
                return &model.UpdateResult{}, nil
2✔
878
        case 1:
16✔
879
                filter = bson.D{{Key: DbDevId, Value: deviceIDs[0]}}
16✔
880
        default:
6✔
881
                filter = bson.D{{Key: DbDevId, Value: bson.M{"$in": deviceIDs}}}
6✔
882
        }
883
        // Append filter on group
884
        filter = append(
22✔
885
                filter,
22✔
886
                bson.E{Key: DbDevAttributesGroupValue, Value: group},
22✔
887
        )
22✔
888
        // Create unset operation on group attribute
22✔
889
        update := bson.M{
22✔
890
                "$unset": bson.M{
22✔
891
                        DbDevAttributesGroup: "",
22✔
892
                },
22✔
893
        }
22✔
894
        res, err := collDevs.UpdateMany(ctx, filter, update)
22✔
895
        if err != nil {
22✔
896
                return nil, err
×
897
        }
×
898
        return &model.UpdateResult{
22✔
899
                MatchedCount: res.MatchedCount,
22✔
900
                UpdatedCount: res.ModifiedCount,
22✔
901
        }, nil
22✔
902
}
903

904
func predicateToQuery(pred model.FilterPredicate) (bson.D, error) {
4✔
905
        if err := pred.Validate(); err != nil {
6✔
906
                return nil, err
2✔
907
        }
2✔
908
        name := fmt.Sprintf(
2✔
909
                "%s.%s-%s.value",
2✔
910
                DbDevAttributes,
2✔
911
                pred.Scope,
2✔
912
                model.GetDeviceAttributeNameReplacer().Replace(pred.Attribute),
2✔
913
        )
2✔
914
        return bson.D{{
2✔
915
                Key: name, Value: bson.D{{Key: pred.Type, Value: pred.Value}},
2✔
916
        }}, nil
2✔
917
}
918

919
func (db *DataStoreMongo) ListGroups(
920
        ctx context.Context,
921
        filters []model.FilterPredicate,
922
) ([]model.GroupName, error) {
18✔
923
        c := db.client.
18✔
924
                Database(mstore.DbFromContext(ctx, DbName)).
18✔
925
                Collection(DbDevicesColl)
18✔
926

18✔
927
        fltr := bson.D{{
18✔
928
                Key: DbDevAttributesGroupValue, Value: bson.M{"$exists": true},
18✔
929
        }}
18✔
930
        if len(fltr) > 0 {
36✔
931
                for _, p := range filters {
22✔
932
                        q, err := predicateToQuery(p)
4✔
933
                        if err != nil {
6✔
934
                                return nil, errors.Wrap(
2✔
935
                                        err, "store: bad filter predicate",
2✔
936
                                )
2✔
937
                        }
2✔
938
                        fltr = append(fltr, q...)
2✔
939
                }
940
        }
941
        results, err := c.Distinct(
16✔
942
                ctx, DbDevAttributesGroupValue, fltr,
16✔
943
        )
16✔
944
        if err != nil {
16✔
945
                return nil, err
×
946
        }
×
947

948
        groups := make([]model.GroupName, len(results))
16✔
949
        for i, d := range results {
57✔
950
                groups[i] = model.GroupName(d.(string))
41✔
951
        }
41✔
952
        return groups, nil
16✔
953
}
954

955
func (db *DataStoreMongo) GetDevicesByGroup(
956
        ctx context.Context,
957
        group model.GroupName,
958
        skip,
959
        limit int,
960
) ([]model.DeviceID, int, error) {
52✔
961
        c := db.client.
52✔
962
                Database(mstore.DbFromContext(ctx, DbName)).
52✔
963
                Collection(DbDevicesColl)
52✔
964

52✔
965
        filter := bson.M{DbDevAttributesGroupValue: group}
52✔
966
        result := c.FindOne(ctx, filter)
52✔
967
        if result == nil {
52✔
968
                return nil, -1, store.ErrGroupNotFound
×
969
        }
×
970

971
        var dev model.Device
52✔
972
        err := result.Decode(&dev)
52✔
973
        if err != nil {
60✔
974
                return nil, -1, store.ErrGroupNotFound
8✔
975
        }
8✔
976

977
        hasGroup := group != ""
44✔
978
        devices, totalDevices, e := db.GetDevices(ctx,
44✔
979
                store.ListQuery{
44✔
980
                        Skip:      skip,
44✔
981
                        Limit:     limit,
44✔
982
                        Filters:   nil,
44✔
983
                        Sort:      nil,
44✔
984
                        HasGroup:  &hasGroup,
44✔
985
                        GroupName: string(group)})
44✔
986
        if e != nil {
44✔
987
                return nil, -1, errors.Wrap(e, "failed to get device list for group")
×
988
        }
×
989

990
        resIds := make([]model.DeviceID, len(devices))
44✔
991
        for i, d := range devices {
119✔
992
                resIds[i] = d.ID
75✔
993
        }
75✔
994
        return resIds, totalDevices, nil
44✔
995
}
996

997
func (db *DataStoreMongo) GetDeviceGroup(
998
        ctx context.Context,
999
        id model.DeviceID,
1000
) (model.GroupName, error) {
12✔
1001
        dev, err := db.GetDevice(ctx, id)
12✔
1002
        if err != nil || dev == nil {
16✔
1003
                return "", store.ErrDevNotFound
4✔
1004
        }
4✔
1005

1006
        return dev.Group, nil
8✔
1007
}
1008

1009
func (db *DataStoreMongo) DeleteDevices(
1010
        ctx context.Context, ids []model.DeviceID,
1011
) (*model.UpdateResult, error) {
6✔
1012
        var filter = bson.M{}
6✔
1013
        database := db.client.Database(mstore.DbFromContext(ctx, DbName))
6✔
1014
        collDevs := database.Collection(DbDevicesColl)
6✔
1015

6✔
1016
        switch len(ids) {
6✔
1017
        case 0:
×
1018
                // This is a no-op, don't bother requesting mongo.
×
1019
                return &model.UpdateResult{DeletedCount: 0}, nil
×
1020
        case 1:
4✔
1021
                filter[DbDevId] = ids[0]
4✔
1022
        default:
2✔
1023
                filter[DbDevId] = bson.M{"$in": ids}
2✔
1024
        }
1025
        res, err := collDevs.DeleteMany(ctx, filter)
6✔
1026
        if err != nil {
6✔
1027
                return nil, err
×
1028
        }
×
1029
        return &model.UpdateResult{
6✔
1030
                DeletedCount: res.DeletedCount,
6✔
1031
        }, nil
6✔
1032
}
1033

1034
func (db *DataStoreMongo) GetAllAttributeNames(ctx context.Context) ([]string, error) {
57✔
1035
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
57✔
1036

57✔
1037
        project := bson.M{
57✔
1038
                "$project": bson.M{
57✔
1039
                        "arrayofkeyvalue": bson.M{
57✔
1040
                                "$objectToArray": "$$ROOT.attributes",
57✔
1041
                        },
57✔
1042
                },
57✔
1043
        }
57✔
1044

57✔
1045
        unwind := bson.M{
57✔
1046
                "$unwind": "$arrayofkeyvalue",
57✔
1047
        }
57✔
1048

57✔
1049
        group := bson.M{
57✔
1050
                "$group": bson.M{
57✔
1051
                        "_id": nil,
57✔
1052
                        "allkeys": bson.M{
57✔
1053
                                "$addToSet": "$arrayofkeyvalue.v.name",
57✔
1054
                        },
57✔
1055
                },
57✔
1056
        }
57✔
1057

57✔
1058
        l := log.FromContext(ctx)
57✔
1059
        cursor, err := c.Aggregate(ctx, []bson.M{
57✔
1060
                project,
57✔
1061
                unwind,
57✔
1062
                group,
57✔
1063
        })
57✔
1064
        if err != nil {
57✔
1065
                return nil, err
×
1066
        }
×
1067
        defer cursor.Close(ctx)
57✔
1068

57✔
1069
        cursor.Next(ctx)
57✔
1070
        elem := &bson.D{}
57✔
1071
        err = cursor.Decode(elem)
57✔
1072
        if err != nil {
80✔
1073
                if err != io.EOF {
23✔
1074
                        return nil, errors.Wrap(err, "failed to get attributes")
×
1075
                } else {
23✔
1076
                        return make([]string, 0), nil
23✔
1077
                }
23✔
1078
        }
1079
        m := elem.Map()
34✔
1080
        results := m["allkeys"].(primitive.A)
34✔
1081
        attributeNames := make([]string, len(results))
34✔
1082
        for i, d := range results {
152✔
1083
                attributeNames[i] = d.(string)
118✔
1084
                l.Debugf("GetAllAttributeNames got: '%v'", d)
118✔
1085
        }
118✔
1086

1087
        return attributeNames, nil
34✔
1088
}
1089

1090
func (db *DataStoreMongo) SearchDevices(
1091
        ctx context.Context,
1092
        searchParams model.SearchParams,
1093
) ([]model.Device, int, error) {
30✔
1094
        c := db.client.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
30✔
1095

30✔
1096
        queryFilters := make([]bson.M, 0)
30✔
1097
        for _, filter := range searchParams.Filters {
58✔
1098
                op := filter.Type
28✔
1099
                var field string
28✔
1100
                if filter.Scope == model.AttrScopeIdentity && filter.Attribute == model.AttrNameID {
32✔
1101
                        field = DbDevId
4✔
1102
                } else {
28✔
1103
                        name := fmt.Sprintf(
24✔
1104
                                "%s-%s",
24✔
1105
                                filter.Scope,
24✔
1106
                                model.GetDeviceAttributeNameReplacer().Replace(filter.Attribute),
24✔
1107
                        )
24✔
1108
                        field = fmt.Sprintf("%s.%s.%s", DbDevAttributes, name, DbDevAttributesValue)
24✔
1109
                }
24✔
1110
                queryFilters = append(queryFilters, bson.M{field: bson.M{op: filter.Value}})
28✔
1111
        }
1112

1113
        // FIXME: remove after migrating ids to attributes
1114
        if len(searchParams.DeviceIDs) > 0 {
32✔
1115
                queryFilters = append(queryFilters, bson.M{"_id": bson.M{"$in": searchParams.DeviceIDs}})
2✔
1116
        }
2✔
1117

1118
        if searchParams.Text != "" {
32✔
1119
                queryFilters = append(queryFilters, bson.M{
2✔
1120
                        "$text": bson.M{
2✔
1121
                                "$search": utils.TextToKeywords(searchParams.Text),
2✔
1122
                        },
2✔
1123
                })
2✔
1124
        }
2✔
1125

1126
        findQuery := bson.M{}
30✔
1127
        if len(queryFilters) > 0 {
56✔
1128
                findQuery["$and"] = queryFilters
26✔
1129
        }
26✔
1130

1131
        findOptions := mopts.Find()
30✔
1132
        findOptions.SetSkip(int64((searchParams.Page - 1) * searchParams.PerPage))
30✔
1133
        findOptions.SetLimit(int64(searchParams.PerPage))
30✔
1134

30✔
1135
        if len(searchParams.Attributes) > 0 {
34✔
1136
                name := fmt.Sprintf(
4✔
1137
                        "%s-%s",
4✔
1138
                        model.AttrScopeSystem,
4✔
1139
                        model.GetDeviceAttributeNameReplacer().Replace(DbDevUpdatedTs),
4✔
1140
                )
4✔
1141
                field := fmt.Sprintf("%s.%s", DbDevAttributes, name)
4✔
1142
                projection := bson.M{field: 1}
4✔
1143
                for _, attribute := range searchParams.Attributes {
10✔
1144
                        name := fmt.Sprintf(
6✔
1145
                                "%s-%s",
6✔
1146
                                attribute.Scope,
6✔
1147
                                model.GetDeviceAttributeNameReplacer().Replace(attribute.Attribute),
6✔
1148
                        )
6✔
1149
                        field := fmt.Sprintf("%s.%s", DbDevAttributes, name)
6✔
1150
                        projection[field] = 1
6✔
1151
                }
6✔
1152
                findOptions.SetProjection(projection)
4✔
1153
        }
1154

1155
        if searchParams.Text != "" {
32✔
1156
                findOptions.SetSort(bson.M{"score": bson.M{"$meta": "textScore"}})
2✔
1157
        } else if len(searchParams.Sort) > 0 {
38✔
1158
                sortField := make(bson.D, len(searchParams.Sort))
8✔
1159
                for i, sortQ := range searchParams.Sort {
18✔
1160
                        var field string
10✔
1161
                        if sortQ.Scope == model.AttrScopeIdentity && sortQ.Attribute == model.AttrNameID {
12✔
1162
                                field = DbDevId
2✔
1163
                        } else {
10✔
1164
                                name := fmt.Sprintf(
8✔
1165
                                        "%s-%s",
8✔
1166
                                        sortQ.Scope,
8✔
1167
                                        model.GetDeviceAttributeNameReplacer().Replace(sortQ.Attribute),
8✔
1168
                                )
8✔
1169
                                field = fmt.Sprintf("%s.%s", DbDevAttributes, name)
8✔
1170
                        }
8✔
1171
                        sortField[i] = bson.E{Key: field, Value: 1}
10✔
1172
                        if sortQ.Order == "desc" {
16✔
1173
                                sortField[i].Value = -1
6✔
1174
                        }
6✔
1175
                }
1176
                findOptions.SetSort(sortField)
8✔
1177
        }
1178

1179
        cursor, err := c.Find(ctx, findQuery, findOptions)
30✔
1180
        if err != nil {
32✔
1181
                return nil, -1, errors.Wrap(err, "failed to search devices")
2✔
1182
        }
2✔
1183
        defer cursor.Close(ctx)
28✔
1184

28✔
1185
        devices := []model.Device{}
28✔
1186

28✔
1187
        if err = cursor.All(ctx, &devices); err != nil {
28✔
UNCOV
1188
                return nil, -1, errors.Wrap(err, "failed to search devices")
×
UNCOV
1189
        }
×
1190

1191
        count, err := c.CountDocuments(ctx, findQuery)
28✔
1192
        if err != nil {
28✔
UNCOV
1193
                return nil, -1, errors.Wrap(err, "failed to search devices")
×
1194
        }
×
1195

1196
        return devices, int(count), nil
28✔
1197
}
1198

1199
func indexAttr(s *mongo.Client, ctx context.Context, attr string) error {
116✔
1200
        l := log.FromContext(ctx)
116✔
1201
        c := s.Database(mstore.DbFromContext(ctx, DbName)).Collection(DbDevicesColl)
116✔
1202

116✔
1203
        indexView := c.Indexes()
116✔
1204
        keys := bson.D{
116✔
1205
                {Key: indexAttrName(attrIdentityStatus), Value: 1},
116✔
1206
                {Key: indexAttrName(attr), Value: 1},
116✔
1207
        }
116✔
1208
        _, err := indexView.CreateOne(ctx, mongo.IndexModel{Keys: keys, Options: &mopts.IndexOptions{
116✔
1209
                Name: &attr,
116✔
1210
        }})
116✔
1211

116✔
1212
        if err != nil {
116✔
UNCOV
1213
                if isTooManyIndexes(err) {
×
UNCOV
1214
                        l.Warnf(
×
UNCOV
1215
                                "failed to index attr %s in db %s: too many indexes",
×
UNCOV
1216
                                attr,
×
UNCOV
1217
                                mstore.DbFromContext(ctx, DbName),
×
UNCOV
1218
                        )
×
1219
                } else {
×
1220
                        return errors.Wrapf(
×
1221
                                err,
×
1222
                                "failed to index attr %s in db %s",
×
1223
                                attr,
×
1224
                                mstore.DbFromContext(ctx, DbName),
×
1225
                        )
×
1226
                }
×
1227
        }
1228

1229
        return nil
116✔
1230
}
1231

1232
func indexAttrName(attr string) string {
264✔
1233
        return fmt.Sprintf("attributes.%s.value", attr)
264✔
1234
}
264✔
1235

UNCOV
1236
func isTooManyIndexes(e error) bool {
×
UNCOV
1237
        return strings.HasPrefix(e.Error(), "add index fails, too many indexes for inventory.devices")
×
UNCOV
1238
}
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc