Skip to content
This repository was archived by the owner on Mar 27, 2024. It is now read-only.

Commit ac003b2

Browse files
authored
feat: Added GetBulkAsRawMap function to the MongoDB API (#259)
GetBulkAsRawMap returns a slice of 'raw' maps, one for each of the specified keys. Also exposed PrepareFilter and CreateMongoDBFindOptions so that clients may prepare queries for use with the QueryCustom function. Signed-off-by: Bob Stasyszyn <Bob.Stasyszyn@securekey.com>
1 parent 55b4fab commit ac003b2

File tree

2 files changed

+138
-13
lines changed

2 files changed

+138
-13
lines changed

component/storage/mongodb/store.go

Lines changed: 91 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -674,6 +674,39 @@ func (s *Store) GetBulk(keys ...string) ([][]byte, error) {
674674
return allValues, nil
675675
}
676676

677+
// GetBulkAsRawMap fetches the values associated with the given keys and returns the documents (as maps).
678+
// If no data exists under a given key, then nil is returned for that value. It is not considered an error.
679+
// Depending on the implementation, this method may be faster than calling Get for each key individually.
680+
// If any of the given keys are empty, then an error will be returned.
681+
func (s *Store) GetBulkAsRawMap(keys ...string) ([]map[string]interface{}, error) {
682+
if len(keys) == 0 {
683+
return nil, errors.New("keys slice must contain at least one key")
684+
}
685+
686+
for _, key := range keys {
687+
if key == "" {
688+
return nil, errors.New("key cannot be empty")
689+
}
690+
}
691+
692+
ctxWithTimeout, cancel := context.WithTimeout(context.Background(), s.timeout)
693+
defer cancel()
694+
695+
cursor, err := s.coll.Find(ctxWithTimeout, bson.M{"_id": bson.D{
696+
{Key: "$in", Value: keys},
697+
}})
698+
if err != nil {
699+
return nil, fmt.Errorf("failed to run Find command in MongoDB: %w", err)
700+
}
701+
702+
allValues, err := s.collectBulkGetResultsAsRawMap(keys, cursor)
703+
if err != nil {
704+
return nil, err
705+
}
706+
707+
return allValues, nil
708+
}
709+
677710
// Query does a query for data as defined by the documentation in storage.Store (the interface).
678711
// This implementation also supports querying for data tagged with multiple tag name + value pairs (using AND logic).
679712
// To do this, separate the tag name + value pairs using &&. You can still omit one or both of the tag values
@@ -691,12 +724,12 @@ func (s *Store) Query(expression string, options ...storage.QueryOption) (storag
691724
return &Iterator{}, errInvalidQueryExpressionFormat
692725
}
693726

694-
filter, err := prepareFilter(strings.Split(expression, "&&"), false)
727+
filter, err := PrepareFilter(strings.Split(expression, "&&"), false)
695728
if err != nil {
696729
return nil, err
697730
}
698731

699-
findOptions := s.createMongoDBFindOptions(options)
732+
findOptions := s.CreateMongoDBFindOptions(options)
700733

701734
ctxWithTimeout, cancel := context.WithTimeout(context.Background(), s.timeout)
702735
defer cancel()
@@ -933,6 +966,30 @@ func (s *Store) collectBulkGetResults(keys []string, cursor *mongo.Cursor) ([][]
933966
return allValues, nil
934967
}
935968

969+
func (s *Store) collectBulkGetResultsAsRawMap(keys []string, cursor *mongo.Cursor) ([]map[string]interface{}, error) {
970+
allValues := make([]map[string]interface{}, len(keys))
971+
972+
ctxWithTimeout, cancel := context.WithTimeout(context.Background(), s.timeout)
973+
defer cancel()
974+
975+
for cursor.Next(ctxWithTimeout) {
976+
key, value, err := getKeyAndRawMapFromMongoDBResult(cursor)
977+
if err != nil {
978+
return nil, fmt.Errorf("failed to get value from MongoDB result: %w", err)
979+
}
980+
981+
for i := 0; i < len(keys); i++ {
982+
if key == keys[i] {
983+
allValues[i] = value
984+
985+
break
986+
}
987+
}
988+
}
989+
990+
return allValues, nil
991+
}
992+
936993
func (s *Store) executeBulkWriteCommand(models []mongo.WriteModel, atLeastOneInsertOneModel bool) error {
937994
var attemptsMade int
938995

@@ -993,7 +1050,8 @@ func (s *Store) executeBulkWriteCommand(models []mongo.WriteModel, atLeastOneIns
9931050
}, backoff.WithMaxRetries(backoff.NewConstantBackOff(s.timeBetweenRetries), s.maxRetries))
9941051
}
9951052

996-
func (s *Store) createMongoDBFindOptions(options []storage.QueryOption) *mongooptions.FindOptions {
1053+
// CreateMongoDBFindOptions converts the given storage options into MongoDB options.
1054+
func (s *Store) CreateMongoDBFindOptions(options []storage.QueryOption) *mongooptions.FindOptions {
9971055
queryOptions := getQueryOptions(options)
9981056

9991057
findOptions := mongooptions.Find()
@@ -1261,6 +1319,25 @@ func getKeyAndValueFromMongoDBResult(decoder decoder) (key string, value []byte,
12611319
return data.Key, valueBytes, nil
12621320
}
12631321

1322+
func getKeyAndRawMapFromMongoDBResult(decoder decoder) (key string, doc map[string]interface{}, err error) {
1323+
doc, errGetDataWrapper := getValueAsRawMapFromMongoDBResult(decoder)
1324+
if errGetDataWrapper != nil {
1325+
return "", nil, fmt.Errorf("failed to get data wrapper from MongoDB result: %w", errGetDataWrapper)
1326+
}
1327+
1328+
id, ok := doc["_id"]
1329+
if !ok {
1330+
return "", nil, fmt.Errorf("no _id field in document")
1331+
}
1332+
1333+
key, ok = id.(string)
1334+
if !ok {
1335+
return "", nil, fmt.Errorf("_id field in document is not a string")
1336+
}
1337+
1338+
return key, doc, nil
1339+
}
1340+
12641341
func getTagsFromMongoDBResult(decoder decoder) ([]storage.Tag, error) {
12651342
data, err := getDataWrapperFromMongoDBResult(decoder)
12661343
if err != nil {
@@ -1307,7 +1384,8 @@ func getQueryOptions(options []storage.QueryOption) storage.QueryOptions {
13071384
return queryOptions
13081385
}
13091386

1310-
func prepareFilter(expressions []string, isJSONQuery bool) (bson.D, error) {
1387+
// PrepareFilter converts the expression into a MongoDB filter.
1388+
func PrepareFilter(expressions []string, isJSONQuery bool) (bson.D, error) {
13111389
operands := make(bson.D, len(expressions))
13121390

13131391
for i, exp := range expressions {
@@ -1332,6 +1410,14 @@ func prepareSingleOperand(expression string, isJSONQuery bool) (bson.E, error) {
13321410
return bson.E{}, err
13331411
}
13341412

1413+
var key string
1414+
1415+
if isJSONQuery {
1416+
key = splitExpression[0]
1417+
} else {
1418+
key = fmt.Sprintf("tags.%s", splitExpression[0])
1419+
}
1420+
13351421
if operator == "$lt" || operator == "$lte" || operator == "$gt" || operator == "$gte" {
13361422
value, err := strconv.Atoi(splitExpression[1])
13371423
if err != nil {
@@ -1343,14 +1429,6 @@ func prepareSingleOperand(expression string, isJSONQuery bool) (bson.E, error) {
13431429
{Key: operator, Value: value},
13441430
}
13451431

1346-
var key string
1347-
1348-
if isJSONQuery {
1349-
key = splitExpression[0]
1350-
} else {
1351-
key = fmt.Sprintf("tags.%s", splitExpression[0])
1352-
}
1353-
13541432
operand := bson.E{
13551433
Key: key,
13561434
Value: filterValue,
@@ -1368,7 +1446,7 @@ func prepareSingleOperand(expression string, isJSONQuery bool) (bson.E, error) {
13681446
}
13691447

13701448
operand := bson.E{
1371-
Key: fmt.Sprintf("tags.%s", splitExpression[0]),
1449+
Key: key,
13721450
Value: filterValue,
13731451
}
13741452

component/storage/mongodb/store_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,7 @@ func doAllTests(t *testing.T, connString string) {
294294
testBatchIsNewKeyError(t, connString)
295295
testPing(t, connString)
296296
testGetAsRawMap(t, connString)
297+
testGetBulkAsRawMap(t, connString)
297298
testCustomIndexAndQuery(t, connString)
298299
testDocumentReplacementAndMarshalling(t, connString)
299300
}
@@ -1397,6 +1398,52 @@ func testGetAsRawMap(t *testing.T, connString string) {
13971398
"unexpected retrieved test data")
13981399
}
13991400

1401+
func testGetBulkAsRawMap(t *testing.T, connString string) {
1402+
t.Helper()
1403+
1404+
provider, err := mongodb.NewProvider(connString)
1405+
require.NoError(t, err)
1406+
1407+
storeName := randomStoreName()
1408+
1409+
store, err := provider.OpenStore(storeName)
1410+
require.NoError(t, err)
1411+
1412+
var ok bool
1413+
1414+
mongoDBStore, ok := store.(*mongodb.Store)
1415+
require.True(t, ok)
1416+
1417+
_, err = mongoDBStore.GetBulkAsRawMap("TestKey1", "")
1418+
require.EqualError(t, err, "key cannot be empty")
1419+
1420+
testData1 := map[string]interface{}{
1421+
"field1": "value1",
1422+
"field2": int64(2),
1423+
"field3": true,
1424+
}
1425+
1426+
testData2 := map[string]interface{}{
1427+
"field1": "value1",
1428+
"field2": int64(2),
1429+
"field3": true,
1430+
}
1431+
1432+
require.NoError(t, mongoDBStore.PutAsJSON("TestKey1", testData1))
1433+
require.NoError(t, mongoDBStore.PutAsJSON("TestKey2", testData2))
1434+
1435+
retrievedTestData, err := mongoDBStore.GetBulkAsRawMap("TestKey1", "TestKey2")
1436+
require.NoError(t, err)
1437+
require.Len(t, retrievedTestData, 2)
1438+
1439+
// The retrieved test data should be the same as the input test data, except that there's an _id field now.
1440+
testData1["_id"] = "TestKey1"
1441+
testData2["_id"] = "TestKey2"
1442+
1443+
require.True(t, reflect.DeepEqual(testData1, retrievedTestData[0]), "unexpected retrieved test data")
1444+
require.True(t, reflect.DeepEqual(testData2, retrievedTestData[1]), "unexpected retrieved test data")
1445+
}
1446+
14001447
func testCustomIndexAndQuery(t *testing.T, connString string) {
14011448
t.Helper()
14021449
t.Run("Using individual PutAsJSON calls", func(t *testing.T) {

0 commit comments

Comments
 (0)