Initial cloud-services repo - gateway service + pkg modules

This commit is contained in:
Chris Rai
2026-01-30 23:14:52 -05:00
commit fbb820d7b3
1037 changed files with 171318 additions and 0 deletions

130
pkg/mongo/client.go Normal file
View File

@@ -0,0 +1,130 @@
package mongo
import (
"context"
"sync"
"time"
"fiskerinc.com/modules/logger"
"fiskerinc.com/modules/utils/envtool"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
mongotrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/go.mongodb.org/mongo-driver/mongo"
)
var (
conn_str = envtool.GetEnv("MONGO_CONN_STR", "mongodb://REPLACE_ME:REPLACE_ME@localhost:27017/?authSource=admin&w=majority")
StandardDB = envtool.GetEnv("MONGO_DB_NAME", "db")
ODXDB = envtool.GetEnv("MONGO_ODX_DB_NAME", "odx_db")
timeout = time.Duration(envtool.GetEnvInt("MONGO_CLIENT_TIMEOUT", 60)) * time.Second
)
// Supply the database to connect to, either mongo.StandardDB or mongo.ODXDB
func NewClient(dbToConnectTo string) (Client, error) {
var conn Client
client, database, err := NewMongoConnection(dbToConnectTo)
if err != nil {
return conn, err
}
conn = &Conn{
conn: client,
database: database,
}
return conn, nil
}
// Sets up a mongo connection, if connection string is empty, defaults to the env connection string
func NewMongoConnection(databaseName string) (client *mongo.Client, db *mongo.Database, err error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
logger.Info().Msgf("Connection string %s", conn_str)
serverAPIOptions := options.ServerAPI(options.ServerAPIVersion1)
opts := options.Client().ApplyURI(conn_str).SetServerAPIOptions(serverAPIOptions)
opts.Monitor = mongotrace.NewMonitor()
client, err = mongo.Connect(ctx, opts)
if err != nil {
return nil, nil, errors.WithStack(err)
}
db = client.Database(databaseName)
return
}
type Client interface {
GetVehicles() VehiclesCollectionInterface
SetVehicles(v VehiclesCollectionInterface)
GetFleets() FleetsCollectionInterface
SetFleets(f FleetsCollectionInterface)
Collection(name string) *mongo.Collection
Close() error
Ping(ctx context.Context) error
}
type Conn struct {
conn *mongo.Client
database *mongo.Database
vehicles VehiclesCollectionInterface
vehiclesOnce sync.Once
fleets FleetsCollectionInterface
fleetsOnce sync.Once
}
func (c *Conn) GetVehicles() VehiclesCollectionInterface {
c.vehiclesOnce.Do(func() {
if c.vehicles == nil {
c.vehicles = NewVehiclesCollection(
NewCollection(c.Collection("vehicles")),
)
}
})
return c.vehicles
}
func (c *Conn) SetVehicles(v VehiclesCollectionInterface) {
c.vehicles = v
}
func (c *Conn) GetFleets() FleetsCollectionInterface {
c.fleetsOnce.Do(func() {
if c.fleets == nil {
c.fleets = NewFleetsCollection(
NewCollection(c.Collection("fleets")),
)
}
})
return c.fleets
}
func (c *Conn) SetFleets(f FleetsCollectionInterface) {
c.fleets = f
}
func (c *Conn) Collection(name string) *mongo.Collection {
return c.database.Collection(name)
}
func (c *Conn) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
err := c.conn.Disconnect(ctx)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *Conn) Ping(ctx context.Context) error {
return c.conn.Ping(ctx, nil)
}

81
pkg/mongo/client_test.go Normal file
View File

@@ -0,0 +1,81 @@
package mongo_test
import (
"testing"
"fiskerinc.com/modules/mongo"
"fiskerinc.com/modules/testhelper"
)
func TestNewClient(t *testing.T) {
_, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewClient", nil, err)
}
}
func TestClientGetVehicles(t *testing.T) {
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestClientGetVehicles", nil, err)
}
client.GetVehicles()
}
func TestClientSetVehicles(t *testing.T) {
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestClientSetVehicles", nil, err)
}
client.SetVehicles(mongo.NewVehiclesCollection(
mongo.NewCollection(client.Collection("vehicles")),
))
}
func TestClientGetFleets(t *testing.T) {
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestClientGetFleets", nil, err)
}
client.GetFleets()
}
func TestClientSetFleets(t *testing.T) {
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestClientSetFleets", nil, err)
}
client.SetFleets(mongo.NewFleetsCollection(
mongo.NewCollection(client.Collection("fleets")),
))
}
func TestClientCollection(t *testing.T) {
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestClientCollection", nil, err)
return
}
c := client.Collection("test")
if c == nil {
t.Errorf(testhelper.TestErrorTemplate, "TestClientCollection", "*mongo.Collection", c)
}
}
func TestClientClose(t *testing.T) {
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestClientClose", nil, err)
return
}
err = client.Close()
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestClientClose", nil, err)
}
}

177
pkg/mongo/collection.go Normal file
View File

@@ -0,0 +1,177 @@
package mongo
import (
"context"
"fiskerinc.com/modules/db/queries"
e "fiskerinc.com/modules/mongo/error"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/mongo"
"go.mongodb.org/mongo-driver/mongo/options"
)
func NewCollection(collection *mongo.Collection) CollectionInterface {
return &Collection{
collection: collection,
}
}
type CollectionInterface interface {
InsertOne(document interface{}) (interface{}, error)
FindOne(filter interface{}, object interface{}, projection interface{}) error
ReplaceOne(filter interface{}, update interface{}) error
DeleteOne(document interface{}) error
UpdateOne(filter interface{}, update interface{}) (*mongo.UpdateResult, error)
UpdateMany(filter interface{}, update interface{}) error
Find(filter interface{}, objects interface{}, pq *queries.PageQueryOptions) error
Count(filter interface{}) (int64, error)
Aggregate(pipeline mongo.Pipeline, object interface{}) error
}
type Collection struct {
collection *mongo.Collection
}
// InsertOne is an abstraction over Mongo's library
func (c *Collection) InsertOne(document interface{}) (interface{}, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
r, err := c.collection.InsertOne(ctx, document)
if err != nil {
return r, errors.WithStack(err)
}
return r.InsertedID, nil
}
// FindOne is an abstraction over Mongo's library
func (c *Collection) FindOne(filter interface{}, object interface{}, projection interface{}) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
opts := options.FindOne().SetProjection(projection)
err := c.collection.FindOne(ctx, filter, opts).Decode(object)
if err != nil {
return errors.WithStack(err)
}
return nil
}
// ReplaceOne is an abstraction over Mongo's library
func (c *Collection) ReplaceOne(filter interface{}, update interface{}) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
r, err := c.collection.ReplaceOne(ctx, filter, update)
if err != nil {
return errors.WithStack(err)
} else if r.MatchedCount == 0 {
return e.ErrInvalidNumberOfDocs
}
return nil
}
// DeleteOne is an abstraction over Mongo's library
func (c *Collection) DeleteOne(document interface{}) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
r, err := c.collection.DeleteOne(ctx, document)
if err != nil {
return errors.WithStack(err)
} else if r.DeletedCount != 1 {
return e.ErrUnableToDelete
}
return nil
}
func (c *Collection) UpdateOne(filter interface{}, update interface{}) (*mongo.UpdateResult, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
r, err := c.collection.UpdateOne(ctx, filter, update)
if err != nil {
return nil, errors.WithStack(err)
} else if r.MatchedCount == 0 {
return nil, e.ErrInvalidNumberOfDocs
}
return r, nil
}
func (c *Collection) UpdateMany(filter interface{}, update interface{}) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
r, err := c.collection.UpdateMany(ctx, filter, update)
if err != nil {
return errors.WithStack(err)
} else if r.MatchedCount == 0 {
return e.ErrInvalidNumberOfDocs
}
return nil
}
func (c *Collection) Find(filter interface{}, objects interface{}, pq *queries.PageQueryOptions) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
f, err := c.collection.Find(
ctx,
filter,
options.Find().SetLimit(int64(pq.Limit)),
options.Find().SetSkip(int64(pq.Offset)),
options.Find().SetSort(getOrder(pq.Order)),
options.Find().SetProjection(getFieldsToIgnore(pq.Ignore)),
)
if err != nil {
return errors.WithStack(err)
}
err = f.All(ctx, objects)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (c *Collection) Count(filter interface{}) (int64, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
t, err := c.collection.CountDocuments(ctx, filter)
if err != nil {
return 0, errors.WithStack(err)
}
return t, nil
}
// Aggregate when all else fails
func (c *Collection) Aggregate(pipeline mongo.Pipeline, object interface{}) error {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
a, err := c.collection.Aggregate(ctx, pipeline)
if err != nil {
return errors.WithStack(err)
}
err = a.All(ctx, object)
if err != nil {
return errors.WithStack(err)
}
return nil
}

View File

@@ -0,0 +1,213 @@
package mongo_test
import (
"errors"
"testing"
e "fiskerinc.com/modules/mongo/error"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/mongo"
"fiskerinc.com/modules/testhelper"
"go.mongodb.org/mongo-driver/bson"
"fiskerinc.com/modules/utils/elptr"
)
func TestNewCollection(t *testing.T) {
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewCollection", nil, err)
return
}
mongo.NewCollection(client.Collection("vehicles"))
}
func TestCollectionInsertOneIntegration(t *testing.T) {
t.Skip()
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionInsertOneIntegration", nil, err)
return
}
collection := mongo.NewCollection(client.Collection("vehicles"))
v := mongo.Vehicle{
VIN: "TESTVIN123",
}
_, err = collection.InsertOne(v)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionInsertOneIntegration", nil, err)
}
f := mongo.Fleet{
Name: "testfleet",
}
_, err = collection.InsertOne(f)
if !errors.Is(err, e.ErrInvalidStruct) {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionInsertOneIntegration", e.ErrInvalidStruct, err)
}
}
func TestCollectionFindOneIntegration(t *testing.T) {
t.Skip()
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionFindOneIntegration", nil, err)
return
}
collection := mongo.NewCollection(client.Collection("vehicles"))
var vehicle *mongo.Vehicle
vin := "TESTVIN123"
err = collection.FindOne(vin, vehicle, nil)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionFindOneIntegration", nil, err)
}
vin = "TESTVIN456"
err = collection.FindOne(vin, vehicle, nil)
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionFindOneIntegration", "error", err)
}
}
func TestCollectionReplaceOneIntegration(t *testing.T) {
t.Skip()
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionReplaceOneIntegration", nil, err)
return
}
collection := mongo.NewCollection(client.Collection("vehicles"))
f := bson.M{"vin": "TESTVIN123"}
u := bson.M{"$set": bson.M{"fleets": bson.A{}}}
_, err = collection.UpdateOne(f, u)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionReplaceOneIntegration", nil, err)
}
f = bson.M{"vin": "TESTVIN456"}
u = bson.M{"$set": bson.M{"fleets": bson.A{}}}
_, err = collection.UpdateOne(f, u)
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionReplaceOneIntegration", "error", err)
}
}
func TestCollectionDeleteOneIntegration(t *testing.T) {
t.Skip()
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionDeleteOneIntegration", nil, err)
return
}
collection := mongo.NewCollection(client.Collection("vehicles"))
f := bson.M{"vin": "TESTVIN123"}
err = collection.DeleteOne(f)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionDeleteOneIntegration", nil, err)
}
f = bson.M{"vin": "TESTVIN456"}
err = collection.DeleteOne(f)
if err == nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionDeleteOneIntegration", "error", err)
}
}
func TestCollectionUpdateOneIntegration(t *testing.T) {
t.Skip()
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionUpdateOneIntegration", nil, err)
return
}
collection := mongo.NewCollection(client.Collection("vehicles"))
m := bson.M{"vin": "TESTVIN123"}
u := bson.M{"$addToSet": bson.M{"canbus.filters": []common.CANFilter{{CANID: "123", Interval: elptr.ElPtr(100)}}}}
_, err = collection.UpdateOne(m, u)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionUpdateOneIntegration", nil, err)
}
}
func TestCollectionUpdateManyIntegration(t *testing.T) {
t.Skip()
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionUpdateManyIntegration", nil, err)
return
}
collection := mongo.NewCollection(client.Collection("vehicles"))
m := bson.M{}
u := bson.M{"$addToSet": bson.M{"canbus.filters": []common.CANFilter{{CANID: "123", Interval: elptr.ElPtr(100)}}}}
err = collection.UpdateMany(m, u)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionUpdateManyIntegration", nil, err)
}
}
func TestCollectionFindIntegration(t *testing.T) {
t.Skip()
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionFindIntegration", nil, err)
return
}
collection := mongo.NewCollection(client.Collection("vehicles"))
m := bson.M{"vin": "TESTVIN123"}
vehicles := []mongo.Vehicle{}
err = collection.Find(m, &vehicles, nil)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionFindIntegration", nil, err)
}
}
func TestCollectionCountIntegration(t *testing.T) {
t.Skip()
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionCountIntegration", nil, err)
return
}
collection := mongo.NewCollection(client.Collection("vehicles"))
m := bson.M{}
_, err = collection.Count(m)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionCountIntegration", nil, err)
}
}
func TestCollectionAggregateIntegration(t *testing.T) {
t.Skip()
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionAggregateIntegration", nil, err)
return
}
collection := mongo.NewCollection(client.Collection("vehicles"))
pipeline := []bson.D{}
vehicles := []mongo.Vehicle{}
err = collection.Aggregate(pipeline, &vehicles)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestCollectionAggregateIntegration", nil, err)
}
}

154
pkg/mongo/dtc_codes.go Normal file
View File

@@ -0,0 +1,154 @@
package mongo
import (
"context"
"strconv"
"strings"
"sync"
"time"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/logger"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
type DTCCollectionInterface interface {
GetDTCDefinitionByHexString(troubleCodeHex string) (info common.DTCInformation, err error)
GetLatestPDXVersions() (latestVersion PDXVersion, err error)
}
var (
pdxMongodbOnce sync.Once
pdxMongodbInstance *DTCCollection
)
type DTCCollection struct {
CollectionInterface
Client *mongo.Client
DB *mongo.Database
}
func GetPDXMongoClient() (*DTCCollection, error) {
var err error
pdxMongodbOnce.Do(func() {
logger.Info().Msg("init MongoDB instance")
pdxMongodbInstance = &DTCCollection{}
var cl *mongo.Client
var db *mongo.Database
cl, db, err = NewMongoConnection(ODXDB)
pdxMongodbInstance.Client = cl
pdxMongodbInstance.DB = db
})
return pdxMongodbInstance, err
}
// Given a Hex String for 3 bytes, get the dtc information from mongo
func (coll *DTCCollection) GetDTCDefinitionByHexString(troubleCodeHex string, ecuSource string) (info *common.DTCInformation, err error) {
if replacement, exist := common.CarDTCLookupInverse[ecuSource]; exist {
ecuSource = replacement
}
collect := coll.DB.Collection("dtc_lookup")
//db.dtc_lookup.aggregate({"$match": {"ecuName": "ECU", "DtcUniqueId": "V2.6.0"}}, {"$unwind": "$dtcData.DtcList"}, {"$match": {"dtcData.DtcList.TroubleCodeHex":"800156"}}, {"$project":{"_id": 0, "Obj": "$dtcData.DtcList"}})
pdxVersion, err := coll.GetLatestPDXVersions()
if err != nil {
return
}
a := bson.D{}
if ecuSource == "AMP" {
a = bson.D{{"$match", bson.D{{"ecuName", bson.M{"$in": []string{"AMPSO", "AMPHA"}}}, {"DtcUniqueId", pdxVersion.PDXVersion}}}}
} else {
a = bson.D{{"$match", bson.D{{"ecuName", ecuSource}, {"DtcUniqueId", pdxVersion.PDXVersion}}}}
}
b := bson.D{{"$unwind", "$dtcData.DtcList"}}
c := bson.D{{"$match", bson.D{{"dtcData.DtcList.TroubleCodeHex", troubleCodeHex}}}}
d := bson.D{{"$project", bson.D{{"_id", 0}, {"Obj", "$dtcData.DtcList"}}}}
cursor, err := collect.Aggregate(context.Background(), mongo.Pipeline{a, b, c, d})
if err != nil {
err = errors.WithStack(err)
logger.Err(err).Msg("Failed to call aggregate in GetDTCDefinitionByHexString")
return
}
var obj DTCOuterObj
for cursor.Next(context.Background()) {
err = cursor.Decode(&obj)
if err != nil {
err = errors.WithStack(err)
logger.Err(err).Msg("Failed to decode mongo object into DTCInformation")
return
}
info = &obj.DTCInformation
}
return
}
// This value should be cached for a period of time
func (coll *DTCCollection) GetLatestPDXVersions() (latestVersion PDXVersion, err error) {
//db.vod_lookup.aggregate({"$project": {"VodUniqueId": 1, "createDate": 1, "updateDate": 1}})
collect := coll.DB.Collection("vod_lookup")
a := bson.D{{"$project", bson.D{{"VodUniqueId", 1}, {"createDate", 1}, {"updateDate", 1}, {"_id", 0}}}}
cursor, err := collect.Aggregate(context.Background(), mongo.Pipeline{a})
if err != nil {
err = errors.WithStack(err)
logger.Err(err).Msg("")
return
}
// Need to determine the latest version
for cursor.Next(context.Background()) {
var ag PDXVersion
err = cursor.Decode(&ag)
if err != nil {
err = errors.WithStack(err)
logger.Err(err).Msg("")
return
}
if isVersionNewer(latestVersion.PDXVersion, ag.PDXVersion) {
latestVersion = ag
}
}
return
}
// Give the one we have as the newest, and then the new value to compare to
func isVersionNewer(lastNewest, newPossibleNewest string) (isNewer bool) {
// Better algorithm, turn V1.3.2.1 => 1321 and add any 0's on the end if the other number is longer
if lastNewest == "" {
return true
}
// Remove the V
lastNewest = lastNewest[1:]
newPossibleNewest = newPossibleNewest[1:]
last := strings.Split(lastNewest, ".")
new := strings.Split(newPossibleNewest, ".")
for x := 0; x < len(last); x++ {
// We have gotten to the end of the second string without being less, so we are greater, like a hotfix
if x >= len(new) {
return false
}
l, _ := strconv.Atoi(last[x])
n, _ := strconv.Atoi(new[x])
if n > l {
return true
} else if l > n {
return false
}
}
return len(new) > len(last)
}
type DTCOuterObj struct {
common.DTCInformation `bson:"Obj"`
}
type PDXVersion struct {
PDXVersion string `bson:"VodUniqueId"`
CreatedTime time.Time `bson:"createDate"`
UpdatedTime time.Time `bson:"updateDate"`
}

View File

@@ -0,0 +1,64 @@
package mongo
import (
"encoding/json"
"testing"
)
// A test to see if twe get the response form the database at all
func TestGetDTCFromMongo(t *testing.T){
t.Skip()
mg, err := GetPDXMongoClient()
if err != nil{
t.Error(err)
}
info, err := mg.GetDTCDefinitionByHexString("800156", "ACU")
if err != nil {
t.Error(err)
}
b, _ := json.Marshal(info)
t.Log(string(b))
}
func TestGetLatestPDXVersions(t *testing.T){
t.Skip()
mg, err := GetPDXMongoClient()
if err != nil{
t.Error(err)
}
version, err := mg.GetLatestPDXVersions()
if err != nil {
t.Error(err)
}
if version.PDXVersion == ""{
t.Fail()
}
t.Log(version)
}
func TestIsVersionNewer(t *testing.T){
if !isVersionNewer("V1.2.3", "V2.3.4"){
t.Log("V1.2.3", "V2.3.4")
t.Fail()
}
if !isVersionNewer("V1.2.3", "V1.2.3.4"){
t.Log("V1.2.3", "V1.2.3.4")
t.Fail()
}
if isVersionNewer("V1.2.3.4", "V1.2.3"){
t.Log("V1.2.3.4", "V1.2.3")
t.Fail()
}
if isVersionNewer("V1.3.4", "V1.3.3.3"){
t.Log("V1.3.4", "V1.3.3.3")
t.Fail()
}
}

View File

@@ -0,0 +1,9 @@
package error
import "github.com/pkg/errors"
var ErrInvalidStruct = errors.New("invalid struct passed through parameters")
var ErrInvalidNumberOfDocs = errors.New("invalid number of docs returned from db")
var ErrUnableToUpdate = errors.New("unable to edit doc in db")
var ErrUnableToDelete = errors.New("unable to delete doc in db")

15
pkg/mongo/fields.go Normal file
View File

@@ -0,0 +1,15 @@
package mongo
import (
"go.mongodb.org/mongo-driver/bson"
)
func getFieldsToIgnore(fields []string) bson.M {
projection := bson.M{}
for _, field := range fields {
projection[field] = 0
}
return projection
}

21
pkg/mongo/fields_test.go Normal file
View File

@@ -0,0 +1,21 @@
package mongo
import (
"fmt"
"testing"
"go.mongodb.org/mongo-driver/bson"
)
func TestGetFieldsToIgnore(t *testing.T) {
got := getFieldsToIgnore([]string{"field1", "field2"})
expected := fmt.Sprintf("%+v", bson.M{
"field1": 0,
"field2": 0,
})
if fmt.Sprintf("%+v", got) != expected {
t.Errorf("getFieldsToIgnore = %+v; want %s", got, expected)
}
}

455
pkg/mongo/fleets.go Normal file
View File

@@ -0,0 +1,455 @@
package mongo
import (
"strings"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"go.mongodb.org/mongo-driver/bson/primitive"
"fiskerinc.com/modules/utils/elptr"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
// NewFleetsCollection is the constructor for Fleets and utilizes the same mongo connection
func NewFleetsCollection(c CollectionInterface) FleetsCollectionInterface {
return &FleetsCollection{c}
}
type FleetsCollectionInterface interface {
AddFleet(fleet *Fleet) error
FindFleet(filter *Fleet) (*Fleet, error)
UpdateFleet(filter *Fleet, fleet *Fleet) error
DeleteFleet(fleet *Fleet) error
SelectFleets(filter *Fleet, options *queries.PageQueryOptions) ([]Fleet, error)
GetFleetCount(filter *Fleet) (int64, error)
GetVehiclesForFleet(name, search string, options *queries.PageQueryOptions) ([]string, error)
GetVehiclesForFleetCount(name, search string) (int64, error)
AddVehiclesToFleet(name string, vins []string) error
DeleteVehiclesFromFleet(name string, vins []string) error
GetFiltersForFleet(name string, options *queries.PageQueryOptions) ([]common.CANFilter, error)
GetFiltersForFleetCount(name string) (int64, error)
AddFilterToFleet(name string, filter *common.CANFilter) error
UpdateFilterForFleet(name string, id string, filter *common.CANFilter) error
DeleteFilterFromFleet(name string, id string) error
GetCANBusForVehicle(vin string) (*Fleet, error)
CollectionInterface
}
// FleetsCollection is the client for the collection
type FleetsCollection struct {
CollectionInterface
}
// Fleet is an embedded object within Vehicle
type Fleet struct {
Name string `json:"name" bson:"name"`
LogLevel common.LogLevel `json:"log_level" bson:"log_level,omitempty"`
CANBus common.CANBus `json:"canbus" bson:"canbus,omitempty"`
IDPSEnabled bool `json:"idps_enabled" bson:"idps_enabled"`
DebugMask string `json:"debug_mask,omitempty" bson:"debug_mask,omitempty"`
Tags []string `json:"tags" bson:"tags,omitempty"`
Vehicles []string `json:"vehicles" bson:"vehicles,omitempty"`
VehiclesCount int `json:"vehicles_count" bson:"vehicles_count,omitempty"`
search string `json:"-" bson:"-"`
}
func (f *Fleet) SetSearchQuery(search string) {
f.search = search
}
func (f *Fleet) SearchQuery() string {
return f.search
}
func (f *Fleet) AsExpr() bson.M {
if f == nil || len(strings.Trim(f.search, " ")) == 0 {
return bson.M{}
}
return bson.M{"$text": bson.M{"$search": f.search}}
}
func (c *FleetsCollection) AddFleet(fleet *Fleet) error {
_, err := c.InsertOne(fleet)
if err != nil {
return err
}
return nil
}
func (c *FleetsCollection) FindFleet(filter *Fleet) (*Fleet, error) {
m := bson.M{"name": filter.Name}
projection := bson.D{{"vehicles", 0}}
f := &Fleet{}
err := c.FindOne(m, f, projection)
if err != nil {
if errors.Is(err, mongo.ErrNoDocuments) {
return nil, nil
}
return nil, err
}
return f, nil
}
func (c *FleetsCollection) UpdateFleet(filter *Fleet, fleet *Fleet) error {
if fleet.CANBus.DTCEnabled == nil {
fleet.CANBus.DTCEnabled = elptr.ElPtr(false)
}
m := bson.M{"name": filter.Name}
u := bson.M{"$set": bson.M{
"name": fleet.Name,
"canbus.enabled": fleet.CANBus.Enabled,
"canbus.data_logger": fleet.CANBus.DataLogger,
"canbus.dtc_enabled": fleet.CANBus.DTCEnabled,
"canbus.mem_buff_size": fleet.CANBus.MemBuffSize,
"canbus.disk_buff_size": fleet.CANBus.DiskBuffSize,
"idps_enabled": fleet.IDPSEnabled,
"log_level": fleet.LogLevel,
"debug_mask": fleet.DebugMask,
}}
_, err := c.UpdateOne(m, u)
if err != nil {
return err
}
return nil
}
func (c *FleetsCollection) DeleteFleet(fleet *Fleet) error {
m := bson.M{"name": fleet.Name}
err := c.DeleteOne(m)
if err != nil {
return err
}
return nil
}
func (c *FleetsCollection) SelectFleets(filter *Fleet, options *queries.PageQueryOptions) ([]Fleet, error) {
m, f := filter.AsExpr(), make([]Fleet, 0, 0)
if err := c.Find(m, &f, options); err != nil {
return nil, err
}
for x := range f {
if f[x].CANBus.DTCEnabled == nil {
f[x].CANBus.DTCEnabled = elptr.ElPtr(false)
}
}
return f, nil
}
func (c *FleetsCollection) GetFleetCount(filter *Fleet) (int64, error) {
m := filter.AsExpr()
if len(filter.Tags) > 0 {
m["tags"] = bson.M{"$all": filter.Tags}
}
return c.Count(m)
}
func (c *FleetsCollection) GetVehiclesForFleet(name, search string, options *queries.PageQueryOptions) ([]string, error) {
preProc := mongo.Pipeline{
{{"$match", bson.D{{"name", name}}}},
{{"$project", bson.D{{
"vin", bson.D{{
"$cond", bson.A{bson.D{{"$isArray", "$vehicles"}}, "$vehicles", []string{}},
}},
}}}},
{{"$unwind", "$vin"}},
}
if search != "" {
preProc = append(preProc, bson.D{
{"$match", bson.M{"vin": primitive.Regex{
Pattern: search,
Options: "i",
}}}})
}
preProc = append(preProc, bson.D{{"$sort", bson.D{{"vin", 1}}}})
postProc := mongo.Pipeline{
{{"$group", bson.D{{"_id", "$_id"}, {"vehicles", bson.D{{"$push", "$vin"}}}}}},
}
pipe := mongo.Pipeline{}
pipe = append(pipe, preProc...)
if options.Offset > 0 {
pipe = append(pipe, bson.D{{"$skip", options.Offset}})
}
if options.Limit > 0 {
pipe = append(pipe, bson.D{{"$limit", options.Limit}})
}
pipe = append(pipe, postProc...)
var fleets []Fleet
err := c.Aggregate(pipe, &fleets)
if err != nil {
return nil, err
}
if len(fleets) != 1 {
return []string{}, nil
}
return fleets[0].Vehicles, nil
}
type count struct {
Total int64 `bson:"total"`
}
func (c *FleetsCollection) GetVehiclesForFleetCount(name, search string) (int64, error) {
pipe := mongo.Pipeline{
{{"$match", bson.D{{"name", name}}}},
}
if search == "" {
pipe = append(pipe, bson.D{{"$project", bson.D{{
"total", bson.D{{
"$size", bson.D{{
"$cond", bson.A{
bson.D{{"$isArray", "$vehicles"}}, "$vehicles", []string{}},
}},
}}}},
}})
} else {
pipe = append(pipe,
bson.D{{"$project",
bson.D{{"vin",
bson.D{{"$cond",
bson.A{
bson.D{{"$isArray", "$vehicles"}}, "$vehicles", []string{}}}}}}}},
bson.D{{"$unwind", "$vin"}},
bson.D{{"$match", bson.M{"vin": primitive.Regex{
Pattern: search,
Options: "i",
}}}},
bson.D{{"$count", "total"}},
)
}
var counter []count
err := c.Aggregate(pipe, &counter)
if err != nil {
return 0, err
}
if len(counter) != 1 {
return 0, nil
}
return counter[0].Total, nil
}
// Creates a Mongodb pipeline to accomplish several steps
// 1. Get document by name
// 2. Merge vins as sets
// 3. Calculate an accurate count
// 4. Store result in db
func createPipelineForFleetVehicles(name string, vins []string, mergeStrategy string) mongo.Pipeline {
return mongo.Pipeline{
{{"$match", bson.D{{"name", name}}}},
{{"$addFields", bson.D{
{"vehicles", bson.D{
{mergeStrategy, bson.A{
bson.D{{"$ifNull", bson.A{"$vehicles", bson.A{}}}},
vins,
}},
}},
}}},
{{"$set", bson.D{
{"vehicles_count", bson.D{
{"$size", bson.D{
{"$ifNull", bson.A{"$vehicles", bson.A{}}},
}},
}},
}}},
{{"$merge", bson.D{{"into", "fleets"}}}},
}
}
func (c *FleetsCollection) AddVehiclesToFleet(name string, vins []string) error {
if len(vins) == 0 {
return errors.Errorf("No VINs to add to fleet")
}
pipe := createPipelineForFleetVehicles(name, vins, "$setUnion")
var result []interface{}
err := c.Aggregate(pipe, &result)
return err
}
func (c *FleetsCollection) DeleteVehiclesFromFleet(name string, vins []string) error {
if len(vins) == 0 {
return errors.Errorf("No VINs to remove from fleet")
}
pipe := createPipelineForFleetVehicles(name, vins, "$setDifference")
var result []interface{}
err := c.Aggregate(pipe, &result)
return err
}
func (c *FleetsCollection) GetFiltersForFleet(name string, options *queries.PageQueryOptions) ([]common.CANFilter, error) {
preProc := mongo.Pipeline{
{{"$match", bson.D{{"name", name}}}},
{{"$project", bson.D{{
"canbus.filters", bson.D{{
"$cond", bson.A{bson.D{{"$isArray", "$canbus.filters"}}, "$canbus.filters", []common.CANFilter{}},
}},
}}}},
{{"$unwind", "$canbus.filters"}},
{{"$sort", bson.D{{"canbus.filters.can_id", 1}}}},
}
postProc := mongo.Pipeline{
{{"$group", bson.D{{"_id", "$_id"}, {"filters", bson.D{{"$push", "$canbus.filters"}}}}}},
{{"$project", bson.D{{"canbus.filters", "$filters"}}}},
}
pipe := mongo.Pipeline{}
pipe = append(pipe, preProc...)
if options.Offset > 0 {
pipe = append(pipe, bson.D{{"$skip", options.Offset}})
}
if options.Limit > 0 {
pipe = append(pipe, bson.D{{"$limit", options.Limit}})
}
pipe = append(pipe, postProc...)
var fleets []Fleet
err := c.Aggregate(pipe, &fleets)
if err != nil {
return nil, err
}
if len(fleets) != 1 {
return []common.CANFilter{}, nil
}
return fleets[0].CANBus.Filters, nil
}
func (c *FleetsCollection) GetFiltersForFleetCount(name string) (int64, error) {
pipe := mongo.Pipeline{
{{"$match", bson.D{{"name", name}}}},
{{"$project", bson.D{{
"total", bson.D{{
"$size", bson.D{{
"$cond", bson.A{bson.D{{"$isArray", "$canbus.filters"}}, "$canbus.filters", []common.CANBus{}},
}},
}}}},
}},
}
var counter []count
err := c.Aggregate(pipe, &counter)
if err != nil {
return 0, err
}
if len(counter) != 1 {
return 0, nil
}
return counter[0].Total, nil
}
func (c *FleetsCollection) AddFilterToFleet(name string, filter *common.CANFilter) error {
m := bson.M{"name": name}
u := bson.M{"$addToSet": bson.M{"canbus.filters": filter}}
_, err := c.UpdateOne(m, u)
if err != nil {
return err
}
return nil
}
func (c *FleetsCollection) UpdateFilterForFleet(name string, id string, filter *common.CANFilter) error {
m := bson.M{
"name": name,
"canbus.filters.can_id": id,
}
u := bson.M{"$set": bson.M{
"canbus.filters.$.interval": filter.Interval,
"canbus.filters.$.edge_mask": filter.EdgeMask}}
_, err := c.UpdateOne(m, u)
if err != nil {
return err
}
return nil
}
func (c *FleetsCollection) DeleteFilterFromFleet(name string, id string) error {
m := bson.M{"name": name}
u := bson.M{"$pull": bson.M{"canbus.filters": bson.M{"can_id": id}}}
_, err := c.UpdateOne(m, u)
if err != nil {
return err
}
return nil
}
func (c *FleetsCollection) GetCANBusForVehicle(vin string) (*Fleet, error) {
pipe := mongo.Pipeline{
{{"$match", bson.D{{"vehicles", vin}}}},
{{"$unwind", bson.D{{"path", "$canbus.filters"}, {"preserveNullAndEmptyArrays", true}}}},
{{"$group", bson.D{
{"_id", nil},
{"filters", bson.D{{"$push", "$canbus.filters"}}},
{"enabled", bson.D{{"$max", "$canbus.enabled"}}},
{"data_logger", bson.D{{"$max", "$canbus.data_logger"}}},
{"mem_buff_size", bson.D{{"$min", "$canbus.mem_buff_size"}}},
{"disk_buff_size", bson.D{{"$min", "$canbus.disk_buff_size"}}},
}}},
{{"$project", bson.D{
{"canbus.filters", "$filters"},
{"canbus.enabled", "$enabled"},
{"canbus.data_logger", "$data_logger"},
{"canbus.mem_buff_size", "$mem_buff_size"},
{"canbus.disk_buff_size", "$disk_buff_size"},
{"canbus.dtc_enabled", "$dtc_enabled"},
}}},
}
var fleets []Fleet
err := c.Aggregate(pipe, &fleets)
if err != nil {
return nil, err
}
if len(fleets) != 1 {
return nil, nil
}
theFleet := &fleets[0]
if theFleet.CANBus.DTCEnabled == nil {
theFleet.CANBus.DTCEnabled = elptr.ElPtr(false)
}
return theFleet, nil
}

201
pkg/mongo/fleets_test.go Normal file
View File

@@ -0,0 +1,201 @@
package mongo_test
import (
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/mongo"
"fiskerinc.com/modules/testhelper"
"fiskerinc.com/modules/utils/elptr"
)
func TestNewFleetsCollection(t *testing.T) {
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewFleetsCollection", nil, err)
return
}
mongo.NewFleetsCollection(mongo.NewCollection(client.Collection("fleets")))
}
func TestFleetsAddFleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
err := collection.AddFleet(&mongo.Fleet{
Name: "TEST-FLEET",
LogLevel: 2,
CANBus: common.CANBus{
Enabled: true,
DataLogger: true,
DTCEnabled: elptr.ElPtr(true),
MemBuffSize: 1 * 1024 * 1024,
DiskBuffSize: 10 * 1024 * 1024,
},
})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsAddFleet", nil, err)
}
}
func TestFleetsFindFleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
_, err := collection.FindFleet(&mongo.Fleet{Name: "TEST-FLEET"})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsFindFleet", nil, err)
}
}
func TestFleetsUpdateFleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
fleet := mongo.Fleet{Name: "TEST-FlEET", CANBus: common.CANBus{Enabled: true, DataLogger: true, DTCEnabled: elptr.ElPtr(true)}, DebugMask: "12", IDPSEnabled: true}
err := collection.UpdateFleet(&mongo.Fleet{Name: "TEST-FLEET"}, &fleet)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsUpdateFleet", nil, err)
}
}
func TestFleetsDeleteFleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
err := collection.AddFleet(&mongo.Fleet{Name: "TEST-FLEET"})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsDeleteFleet", nil, err)
}
}
func TestFleetsSelectFleets(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
_, err := collection.SelectFleets(&mongo.Fleet{Name: "TEST-FLEET"}, &queries.PageQueryOptions{Offset: 1, Limit: 1})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsSelectFleets", nil, err)
}
}
func TestFleetsGetFleetCount(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
_, err := collection.GetFleetCount(&mongo.Fleet{Name: "TEST-FLEET"})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsGetFleetCount", nil, err)
}
}
func TestFleetsGetVehiclesForFleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
_, err := collection.GetVehiclesForFleet("TEST-FLEET", "", &queries.PageQueryOptions{Offset: 1, Limit: 1})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsGetVehiclesForFleet", nil, err)
}
}
func TestFleetsGetVehiclesForFleetCount(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{
AggregateObject: []struct{ Total int64 }{{1}},
},
}
_, err := collection.GetVehiclesForFleetCount("TEST-FLEET", "")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsGetVehiclesForFleetCount", nil, err)
}
}
func TestFleetsAddVehiclesToFleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
err := collection.AddVehiclesToFleet("TEST-FLEET", []string{"TESTVIN123"})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsAddVehiclesToFleet", nil, err)
}
}
func TestFleetsRemoveVehicleFromFleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
err := collection.DeleteVehiclesFromFleet("TEST-FLEET", []string{"TESTVIN123"})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsRemoveVehicleFromFleet", nil, err)
}
}
func TestFleetsGetFiltersForFleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
_, err := collection.GetFiltersForFleet("TEST-FLEET", &queries.PageQueryOptions{Offset: 1, Limit: 1})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsGetFiltersForFleet", nil, err)
}
}
func TestFleetsAddFilterToFleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
err := collection.AddFilterToFleet("TEST-FLEET",
&common.CANFilter{CANID: "123", Interval: elptr.ElPtr(456)})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsAddFilterToFleet", nil, err)
}
}
func TestFleetsUpdateFilterForFleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
err := collection.UpdateFilterForFleet("TEST-FLEET", "123",
&common.CANFilter{CANID: "123", Interval: elptr.ElPtr(789)})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsUpdateFilterForFleet", nil, err)
}
}
func TestFleetsDeleteFilterForleet(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
err := collection.DeleteFilterFromFleet("TEST-FLEET", "123")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsDeleteFilterForleet", nil, err)
}
}
func TestFleetsGetFiltersForVehicle(t *testing.T) {
collection := mongo.FleetsCollection{
&mongo.MockCollection{},
}
_, err := collection.GetCANBusForVehicle("TESTVIN123")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestFleetsGetFiltersForVehicle", nil, err)
}
}

139
pkg/mongo/mock.go Normal file
View File

@@ -0,0 +1,139 @@
package mongo
import (
"context"
"encoding/json"
"fiskerinc.com/modules/db/queries"
"github.com/pkg/errors"
"go.mongodb.org/mongo-driver/mongo"
)
func NewMockClient() Client {
c := &Conn{}
c.SetVehicles(&VehiclesCollection{&MockCollection{}})
c.SetFleets(&FleetsCollection{&MockCollection{}})
return &mockClient{c}
}
type mockClient struct {
*Conn
}
func (m *mockClient) Ping(ctx context.Context) error {
return nil
}
// MockCollection is used as an embedded struct for mock tests
type MockCollection struct {
AggregateObject interface{}
FindObject interface{}
}
func (m *MockCollection) InsertOne(document interface{}) (interface{}, error) {
return nil, nil
}
func (m *MockCollection) FindOne(filter interface{}, object interface{}, projection interface{}) error {
return nil
}
func (m *MockCollection) ReplaceOne(filter interface{}, update interface{}) error {
return nil
}
func (m *MockCollection) DeleteOne(document interface{}) error {
return nil
}
func (m *MockCollection) UpdateOne(filter interface{}, update interface{}) (*mongo.UpdateResult, error) {
return &mongo.UpdateResult{
MatchedCount: 0,
ModifiedCount: 1,
UpsertedCount: 0,
UpsertedID: []string{},
}, nil
}
func (m *MockCollection) UpdateMany(filter interface{}, update interface{}) error {
return nil
}
func (m *MockCollection) Find(filter interface{}, objects interface{}, options *queries.PageQueryOptions) error {
data, err := json.Marshal(m.FindObject)
if err != nil {
return errors.WithStack(err)
}
err = json.Unmarshal(data, objects)
if err != nil {
return errors.WithStack(err)
}
return nil
}
func (m *MockCollection) Count(filter interface{}) (int64, error) {
return 0, nil
}
func (m *MockCollection) Aggregate(pipeline mongo.Pipeline, object interface{}) error {
data, err := json.Marshal(m.AggregateObject)
if err != nil {
return errors.WithStack(err)
}
err = json.Unmarshal(data, object)
if err != nil {
return errors.WithStack(err)
}
return nil
}
// A separate client where mock collection have bunch of methods overwritten
// specifically for certain unit tests like the one to validate DebugMask enablement.
func NewMockMongoClient() Client {
c := &Conn{}
m := newMockVehicleCollection()
c.SetVehicles(&VehiclesCollection{&m})
c.SetFleets(&FleetsCollection{&MockCollection{}})
return &mockMongoClient{c}
}
type mockMongoClient struct {
*Conn
}
// It carries a specific vehicle inserted for mocking.
type MockVehicleCollection struct {
VehiclesCollection
vehicle Vehicle
}
func newMockVehicleCollection() MockVehicleCollection {
return MockVehicleCollection{}
}
func (m *MockVehicleCollection) InsertOne(document interface{}) (interface{}, error) {
vh := document.(*Vehicle)
m.vehicle = *vh
return vh, nil
}
func (m *MockVehicleCollection) FindOne(filter interface{}, object interface{}, projection interface{}) error {
argVh := object.(*Vehicle)
argVh.VIN = m.vehicle.VIN
argVh.DebugMask = m.vehicle.DebugMask
return nil
}
func (m *MockVehicleCollection) FindVehicle(vf *Vehicle) (*Vehicle, error) {
v := m.vehicle
return &v, nil
}
func (m *MockVehicleCollection) GetVehicles() VehiclesCollectionInterface {
return m
}

30
pkg/mongo/sort.go Normal file
View File

@@ -0,0 +1,30 @@
package mongo
import (
"strings"
"go.mongodb.org/mongo-driver/bson"
)
func AdaptOrder(orderString string, adaptMap map[string]string) string {
for old, new := range adaptMap {
orderString = strings.Replace(orderString, old, new, -1)
}
return orderString
}
func getOrder(queryOrder string) bson.M {
order := bson.M{"_id": -1}
orderSlice := strings.Split(queryOrder, " ")
if len(orderSlice) > 1 {
asc := 1
if strings.ToLower(orderSlice[1]) == "desc" {
asc = -1
}
order = bson.M{orderSlice[0]: asc}
}
return order
}

421
pkg/mongo/vehicles.go Normal file
View File

@@ -0,0 +1,421 @@
package mongo
import (
"strings"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/utils/elptr"
"go.mongodb.org/mongo-driver/bson"
"go.mongodb.org/mongo-driver/mongo"
)
// Vehicle serves as the schema for Vehicles collection
type Vehicle struct {
VIN string `json:"vin" bson:"vin"`
LogLevel common.LogLevel `json:"log_level" bson:"log_level"`
DLTEnabled bool `json:"dlt_enabled" bson:"dlt_enabled"`
DLTLevel int `json:"dlt_level,omitempty" bson:"dlt_level,omitempty"`
CANBus common.CANBus `json:"canbus" bson:"canbus"`
IDPSEnabled bool `json:"idps_enabled" bson:"idps_enabled"`
DebugMask string `json:"debug_mask,omitempty" bson:"debug_mask,omitempty"`
Fleets []Fleet `json:"fleets" bson:"fleets,omitempty"`
search string `json:"-" bson:"-"`
}
func (f *Vehicle) SetSearchQuery(search string) {
f.search = search
}
func (f *Vehicle) SearchQuery() string {
return f.search
}
type VehicleCANBusWithFleet struct {
CANBus struct {
Filters []common.CANFilterWithFleet `json:"filters,omitempty" bson:"filters,omitempty"`
} `json:"canbus" bson:"canbus"`
}
// NewVehiclesCollection is the construct for Vehicles and utilizes the same mongo connection
func NewVehiclesCollection(c CollectionInterface) VehiclesCollectionInterface {
return &VehiclesCollection{c}
}
type VehiclesCollectionInterface interface {
AddVehicle(vehicle *Vehicle) error
FindVehicle(filter *Vehicle) (*Vehicle, error)
UpdateVehicle(vehicle *Vehicle) error
DeleteVehicle(filter *Vehicle) error
AddFilterToVehicle(vin string, filter *common.CANFilter) error
GetFiltersForVehicle(vin string, options *queries.PageQueryOptions) ([]common.CANFilterWithFleet, error)
UpdateFilterForVehicle(vin string, id string, filter *common.CANFilter) error
DeleteFilterForVehicle(vin string, id string) error
GetFilterCountForVehicle(vin string) (int64, error)
UpdateFiltersForVehicle(vin string, filters []common.CANFilter) error
GetFleetsForVehicle(
vin, fleetName string,
options *queries.PageQueryOptions,
) ([]string, error)
GetFleetCountForVehicle(
vin, fleetName string,
) (int64, error)
CollectionInterface
}
// VehiclesCollection is the client for the collection
type VehiclesCollection struct {
CollectionInterface
}
func (c *VehiclesCollection) AddVehicle(vehicle *Vehicle) error {
vehicle.VIN = strings.ToUpper(vehicle.VIN)
_, err := c.InsertOne(vehicle)
if err != nil {
return err
}
return nil
}
func (c *VehiclesCollection) FindVehicle(filter *Vehicle) (*Vehicle, error) {
m := bson.M{"vin": filter.VIN}
v := &Vehicle{}
err := c.FindOne(m, v, nil)
if err != nil {
return nil, err
}
if v.CANBus.DTCEnabled == nil {
v.CANBus.DTCEnabled = elptr.ElPtr(false)
}
return v, nil
}
func (c *VehiclesCollection) UpdateVehicle(vehicle *Vehicle) error {
m := bson.M{"vin": vehicle.VIN}
u := bson.M{"$set": bson.M{
"canbus.enabled": vehicle.CANBus.Enabled,
"canbus.data_logger": vehicle.CANBus.DataLogger,
"canbus.dtc_enabled": vehicle.CANBus.DTCEnabled,
"canbus.mem_buff_size": vehicle.CANBus.MemBuffSize,
"canbus.disk_buff_size": vehicle.CANBus.DiskBuffSize,
"log_level": vehicle.LogLevel,
"debug_mask": vehicle.DebugMask,
"dlt_enabled": vehicle.DLTEnabled,
"dlt_level": vehicle.DLTLevel,
"idps_enabled": vehicle.IDPSEnabled,
}}
_, err := c.UpdateOne(m, u)
if err != nil {
return err
}
return nil
}
func (c *VehiclesCollection) DeleteVehicle(vehicle *Vehicle) error {
m := bson.M{"vin": vehicle.VIN}
err := c.DeleteOne(m)
if err != nil {
return err
}
return nil
}
func (c *VehiclesCollection) AddFilterToVehicle(vin string, filter *common.CANFilter) error {
m := bson.M{"vin": vin}
u := bson.M{"$addToSet": bson.M{"canbus.filters": filter}}
_, err := c.UpdateOne(m, u)
if err != nil {
return err
}
return nil
}
func (c *VehiclesCollection) filtersForVehiclePipe(
vin string,
) mongo.Pipeline {
// This pipeline works in a following way:
// 1. matches the vin from vehicles.
// 2. checks whether the canbus.filters is array, if not creates an empty one.
// 3. does lookup in fleets collection
// 3.1. checks whether fleets.vehicles is array and
// 3.2. matches a current vin with the vins in fleets.vehicles
// 3.3. checks if the canbus.filters of the matching fleet is array, if not creates an empty one
// 3.4. names it as matches
// 4. creates transformed matches as cfilters
// 4.1. goes through every canbus filter of the matches
// 4.2. concatenates them through creating a new object with a "fleet" field representing fleet name
// 4.3. eventually named as cfilters
// 5. merges cfilters and canbus.filters selected as "filters" eventually
// 6. unwinds filters
// 7. sorts the unwinded through filters.can_id
return mongo.Pipeline{
{{"$match", bson.D{{"vin", vin}}}},
{{"$project", bson.D{
{"canbus.filters", bson.D{
{"$cond",
bson.A{bson.D{{"$isArray", "$canbus.filters"}}, "$canbus.filters", bson.A{}},
},
}},
{"vin", 1}}}},
{{"$lookup",
bson.D{
{"from", "fleets"},
{"let", bson.D{{"vin", "$vin"}}},
{"pipeline", bson.A{bson.D{
{"$match", bson.D{
{"$expr", bson.D{
{"$and", bson.A{
bson.D{{"$isArray", "$vehicles"}},
bson.D{{"$in", bson.A{"$$vin", "$vehicles"}}},
}}}}}}}, bson.D{
{"$project", bson.D{
{"canbus.filters", bson.D{
{"$cond", bson.A{
bson.D{{"$isArray", "$canbus.filters"}},
"$canbus.filters",
bson.A{},
}}}},
{"name", 1},
}}}}},
{"as", "matches"}}}},
{{"$addFields", bson.D{
{"cfilters", bson.D{
{"$reduce", bson.D{
{"input", "$matches"},
{"initialValue", bson.A{}},
{"in", bson.D{
{"$concatArrays", bson.A{
"$$value", bson.D{
{"$map", bson.D{
{"input", "$$this.canbus.filters"},
{"as", "cfilter"},
{"in", bson.D{
{"fleet", "$$this.name"},
{"can_id", "$$cfilter.can_id"},
{"interval", "$$cfilter.interval"},
{"edge_mask", "$$cfilter.edge_mask"}}},
}}}}}}}}}}}}}},
{{"$project", bson.D{
{"filters", bson.D{
{"$concatArrays", bson.A{
"$canbus.filters",
"$cfilters",
}}}}}}},
{{"$unwind", "$filters"}},
{{"$sort", bson.D{{"filters.can_id", 1}}}},
}
}
func (c *VehiclesCollection) GetFiltersForVehicle(
vin string,
options *queries.PageQueryOptions,
) ([]common.CANFilterWithFleet, error) {
preProc := c.filtersForVehiclePipe(vin)
postProc := mongo.Pipeline{{
{"$group", bson.D{
{"_id", "$_id"},
{"filters", bson.D{{"$push", "$filters"}}}}}},
{{"$project", bson.D{{"canbus.filters", "$filters"}}}}}
pipe := mongo.Pipeline{}
pipe = append(pipe, preProc...)
if options.Offset > 0 {
pipe = append(pipe, bson.D{{"$skip", options.Offset}})
}
if options.Limit > 0 {
pipe = append(pipe, bson.D{{"$limit", options.Limit}})
}
pipe = append(pipe, postProc...)
var vehicles []VehicleCANBusWithFleet
err := c.Aggregate(pipe, &vehicles)
if err != nil {
return nil, err
}
if len(vehicles) != 1 {
return []common.CANFilterWithFleet{}, nil
}
return vehicles[0].CANBus.Filters, nil
}
func (c *VehiclesCollection) UpdateFilterForVehicle(vin string, id string, filter *common.CANFilter) error {
m := bson.M{
"vin": vin,
"canbus.filters.can_id": id,
}
u := bson.M{"$set": bson.M{
"canbus.filters.$.interval": filter.Interval,
"canbus.filters.$.edge_mask": filter.EdgeMask}}
_, err := c.UpdateOne(m, u)
if err != nil {
return err
}
return nil
}
func (c *VehiclesCollection) DeleteFilterForVehicle(vin string, id string) error {
m := bson.M{"vin": vin}
u := bson.M{"$pull": bson.M{"canbus.filters": bson.M{"can_id": id}}}
_, err := c.UpdateOne(m, u)
if err != nil {
return err
}
return nil
}
func (c *VehiclesCollection) GetFilterCountForVehicle(vin string) (int64, error) {
pipe := append(c.filtersForVehiclePipe(vin), mongo.Pipeline{
{{"$group", bson.D{
{"_id", "$_id"},
{"filters", bson.D{{"$push", "$filters"}}}}}},
{{"$project", bson.D{{"canbus.filters", "$filters"}}}},
{{"$project", bson.D{{
"total", bson.D{{
"$size", bson.D{{
"$cond", bson.A{bson.D{{"$isArray", "$canbus.filters"}}, "$canbus.filters", []common.CANBus{}},
}},
}}}},
}},
}...)
var counter []count
err := c.Aggregate(pipe, &counter)
if err != nil {
return 0, err
}
if len(counter) != 1 {
return 0, nil
}
return counter[0].Total, nil
}
func (c *VehiclesCollection) UpdateFiltersForVehicle(vin string, filters []common.CANFilter) error {
m := bson.M{"vin": vin}
u := bson.M{"$set": bson.M{"canbus.filters": filters}}
_, err := c.UpdateOne(m, u)
if err != nil {
return err
}
return nil
}
func (c *VehiclesCollection) getFleetsForVehiclePipe(vin, fleetName string) mongo.Pipeline {
ofFleet := bson.E{}
if fleetName != "" {
ofFleet = bson.E{"name", fleetName}
}
return mongo.Pipeline{
{{"$match", bson.D{{"vin", vin}}}},
{{"$project", bson.D{{"vin", 1}}}},
{{"$lookup", bson.D{
{"from", "fleets"},
{"let", bson.D{{"vin", "$vin"}}},
{"pipeline", bson.A{bson.D{
{"$match", bson.D{
{"$expr", bson.D{
{"$and", bson.A{
bson.D{{"$isArray", "$vehicles"}},
bson.D{{"$in", bson.A{"$$vin", "$vehicles"}}},
}}}}, ofFleet,
}}}, bson.D{
{"$project", bson.D{{"name", 1}}}}}},
{"as", "fleets"}}}},
{{"$project", bson.D{
{"fleets", bson.D{
{"$map", bson.D{
{"input", "$fleets"},
{"as", "fleet"},
{"in", "$$fleet.name"}}}}}}}},
{{"$unwind", "$fleets"}}}
}
func (c *VehiclesCollection) GetFleetCountForVehicle(
vin, fleetName string,
) (int64, error) {
pipe := append(c.getFleetsForVehiclePipe(vin, fleetName), mongo.Pipeline{
{{"$group", bson.D{
{"_id", "$_id"},
{"fleets", bson.D{{"$push", "$fleets"}}}}}},
{{"$project", bson.D{{
"total", bson.D{{
"$size", bson.D{{
"$cond", bson.A{bson.D{{"$isArray", "$fleets"}}, "$fleets", []common.CANBus{}},
}},
}}}},
}},
}...)
var counter []count
err := c.Aggregate(pipe, &counter)
if err != nil {
return 0, err
}
if len(counter) != 1 {
return 0, nil
}
return counter[0].Total, nil
}
func (c *VehiclesCollection) GetFleetsForVehicle(
vin, fleetName string,
options *queries.PageQueryOptions,
) ([]string, error) {
preProc := c.getFleetsForVehiclePipe(vin, fleetName)
postProc := mongo.Pipeline{{
{"$group", bson.D{
{"_id", "$_id"},
{"fleets", bson.D{{"$push", "$fleets"}}}}}}}
pipe := mongo.Pipeline{}
pipe = append(pipe, preProc...)
if options.Offset > 0 {
pipe = append(pipe, bson.D{{"$skip", options.Offset}})
}
if options.Limit > 0 {
pipe = append(pipe, bson.D{{"$limit", options.Limit}})
}
pipe = append(pipe, postProc...)
var fleets []struct {
Fleets []string `bson:"fleets"`
}
err := c.Aggregate(pipe, &fleets)
if err != nil {
return nil, err
}
if len(fleets) != 1 {
return []string{}, nil
}
return fleets[0].Fleets, nil
}

164
pkg/mongo/vehicles_test.go Normal file
View File

@@ -0,0 +1,164 @@
package mongo_test
import (
"testing"
"fiskerinc.com/modules/common"
"fiskerinc.com/modules/db/queries"
"fiskerinc.com/modules/mongo"
"fiskerinc.com/modules/testhelper"
"fiskerinc.com/modules/utils/elptr"
)
func TestNewVehiclesCollection(t *testing.T) {
client, err := mongo.NewClient(mongo.StandardDB)
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestNewVehiclesCollection", nil, err)
return
}
mongo.NewVehiclesCollection(mongo.NewCollection(client.Collection("vehicles")))
}
func TestVehiclesAddVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{},
}
err := collection.AddVehicle(&mongo.Vehicle{VIN: "TESTVIN123", CANBus: common.CANBus{Enabled: true, DataLogger: true, DTCEnabled: elptr.ElPtr(true)}})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesAddVehicle", nil, err)
}
}
func TestVehiclesFindVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{},
}
_, err := collection.FindVehicle(&mongo.Vehicle{VIN: "TESTVIN123"})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesFindVehicle", nil, err)
}
}
func TestVehiclesUpdateVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{},
}
err := collection.AddVehicle(&mongo.Vehicle{VIN: "TESTVIN123", CANBus: common.CANBus{Enabled: true, DataLogger: true, DTCEnabled: elptr.ElPtr(false)}, DebugMask: "E"})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesUpdateVehicle", nil, err)
}
}
func TestVehiclesDeleteVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{},
}
err := collection.DeleteVehicle(&mongo.Vehicle{VIN: "TESTVIN123"})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesDeleteVehicle", nil, err)
}
}
func TestVehiclesAddFilterToVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{},
}
err := collection.AddFilterToVehicle("TESTVIN123",
&common.CANFilter{CANID: "123", Interval: elptr.ElPtr(100)})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestAddFilterToVehicle", nil, err)
}
}
func TestVehiclesGetFiltersForVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{
AggregateObject: []mongo.Vehicle{{VIN: "TESTVIN123"}},
},
}
_, err := collection.GetFiltersForVehicle("TESTVIN123", &queries.PageQueryOptions{Offset: 1, Limit: 1})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesGetFiltersForVehicle", nil, err)
}
}
func TestVehiclesGetFleetsForVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{
AggregateObject: []struct {
Fleets []string `bson:"fleets"`
}{{Fleets: []string{"fleet1", "fleet2"}}}}}
_, err := collection.GetFleetsForVehicle(
"TESTVIN123", "", &queries.PageQueryOptions{Offset: 1, Limit: 1})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesGetFleetsForVehicle", nil, err)
}
}
func TestVehiclesGetFleetsCountForVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{
AggregateObject: []struct{ Total int64 }{{1}},
},
}
_, err := collection.GetFleetCountForVehicle("TESTVIN123", "")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesGetFleetsCountForVehicle", nil, err)
}
}
func TestVehiclesUpdateFilterForVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{},
}
err := collection.UpdateFilterForVehicle("TESTVIN123", "123",
&common.CANFilter{Interval: elptr.ElPtr(100)})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesUpdateFilterForVehicle", nil, err)
}
}
func TestVehiclesDeleteFilterForVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{},
}
err := collection.DeleteFilterForVehicle("TESTVIN123", "123")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesDeleteFilterForVehicle", nil, err)
}
}
func TestVehiclesGetFilterCountForVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{
AggregateObject: []struct{ Total int64 }{{1}},
},
}
_, err := collection.GetFilterCountForVehicle("TESTVIN123")
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesGetFilterCountForVehicle", nil, err)
}
}
func TestVehiclesUpdateFiltersForVehicle(t *testing.T) {
collection := mongo.VehiclesCollection{
&mongo.MockCollection{},
}
err := collection.UpdateFiltersForVehicle("TESTVIN123", []common.CANFilter{{CANID: "123",
Interval: elptr.ElPtr(100)}})
if err != nil {
t.Errorf(testhelper.TestErrorTemplate, "TestVehiclesUpdateFiltersForVehicle", nil, err)
}
}