initial commit
This commit is contained in:
124
data/storage/mock.go
Normal file
124
data/storage/mock.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-platform/saved-search/data/types"
|
||||
)
|
||||
|
||||
// MockStorage implements Storage interface for testing
|
||||
type MockStorage struct {
|
||||
mu sync.RWMutex
|
||||
data map[string]*types.SavedSearch
|
||||
counter int
|
||||
}
|
||||
|
||||
// NewMockStorage creates a new mock storage instance
|
||||
func NewMockStorage() *MockStorage {
|
||||
return &MockStorage{
|
||||
data: make(map[string]*types.SavedSearch),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockStorage) CreateSavedSearch(ctx context.Context, search types.SavedSearch) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
search.CreatedAt = now
|
||||
search.UpdatedAt = now
|
||||
|
||||
m.data[search.ID] = &search
|
||||
m.counter++
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockStorage) GetSavedSearch(ctx context.Context, id string) (*types.SavedSearch, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
search, exists := m.data[id]
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("saved search not found")
|
||||
}
|
||||
|
||||
// Return a copy to avoid mutation issues
|
||||
searchCopy := *search
|
||||
return &searchCopy, nil
|
||||
}
|
||||
|
||||
func (m *MockStorage) GetSavedSearchesByOwner(ctx context.Context, ownerID string) ([]*types.SavedSearch, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var searches []*types.SavedSearch
|
||||
for _, search := range m.data {
|
||||
if search.OwnerID == ownerID {
|
||||
searchCopy := *search
|
||||
searches = append(searches, &searchCopy)
|
||||
}
|
||||
}
|
||||
|
||||
return searches, nil
|
||||
}
|
||||
|
||||
func (m *MockStorage) UpdateSavedSearch(ctx context.Context, search types.SavedSearch) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
existing, exists := m.data[search.ID]
|
||||
if !exists {
|
||||
return fmt.Errorf("saved search not found")
|
||||
}
|
||||
|
||||
search.CreatedAt = existing.CreatedAt // Preserve creation time
|
||||
search.UpdatedAt = time.Now()
|
||||
m.data[search.ID] = &search
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockStorage) DeleteSavedSearch(ctx context.Context, id string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if _, exists := m.data[id]; !exists {
|
||||
return fmt.Errorf("saved search not found")
|
||||
}
|
||||
|
||||
delete(m.data, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockStorage) ListSavedSearches(ctx context.Context, ownerID string, limit, offset int) ([]*types.SavedSearch, int64, error) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
|
||||
var allSearches []*types.SavedSearch
|
||||
for _, search := range m.data {
|
||||
if ownerID == "" || search.OwnerID == ownerID {
|
||||
searchCopy := *search
|
||||
allSearches = append(allSearches, &searchCopy)
|
||||
}
|
||||
}
|
||||
|
||||
total := int64(len(allSearches))
|
||||
|
||||
// Apply pagination
|
||||
start := offset
|
||||
end := offset + limit
|
||||
|
||||
if start > len(allSearches) {
|
||||
return []*types.SavedSearch{}, total, nil
|
||||
}
|
||||
|
||||
if end > len(allSearches) {
|
||||
end = len(allSearches)
|
||||
}
|
||||
|
||||
return allSearches[start:end], total, nil
|
||||
}
|
||||
219
data/storage/mongodb.go
Normal file
219
data/storage/mongodb.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.coopgo.io/coopgo-platform/saved-search/data/types"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// MongoDBStorage implements Storage interface using MongoDB
|
||||
type MongoDBStorage struct {
|
||||
client *mongo.Client
|
||||
database string
|
||||
collection string
|
||||
}
|
||||
|
||||
// NewMongoDBStorage creates a new MongoDB storage instance
|
||||
func NewMongoDBStorage(client *mongo.Client, database, collection string) (*MongoDBStorage, error) {
|
||||
storage := &MongoDBStorage{
|
||||
client: client,
|
||||
database: database,
|
||||
collection: collection,
|
||||
}
|
||||
|
||||
// Create indexes
|
||||
if err := storage.createIndexes(); err != nil {
|
||||
return nil, fmt.Errorf("failed to create indexes: %w", err)
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// NewMongoDBStorageFromConfig creates a new MongoDB storage from config following solidarity transport pattern
|
||||
func NewMongoDBStorageFromConfig(cfg *viper.Viper) (Storage, error) {
|
||||
var (
|
||||
mongodb_uri = cfg.GetString("storage.db.mongodb.uri")
|
||||
mongodb_host = cfg.GetString("storage.db.mongodb.host")
|
||||
mongodb_port = cfg.GetString("storage.db.mongodb.port")
|
||||
mongodb_dbname = cfg.GetString("storage.db.mongodb.db_name")
|
||||
mongodb_collection = cfg.GetString("storage.db.mongodb.collections.saved_searches")
|
||||
)
|
||||
|
||||
if mongodb_uri == "" {
|
||||
mongodb_uri = fmt.Sprintf("mongodb://%s:%s/%s", mongodb_host, mongodb_port, mongodb_dbname)
|
||||
}
|
||||
|
||||
client, err := mongo.Connect(context.Background(), options.Client().ApplyURI(mongodb_uri))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Info().Str("collection", mongodb_collection).Msg("mongodb storage initialized")
|
||||
|
||||
return NewMongoDBStorage(client, mongodb_dbname, mongodb_collection)
|
||||
}
|
||||
|
||||
func (s *MongoDBStorage) createIndexes() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
coll := s.client.Database(s.database).Collection(s.collection)
|
||||
|
||||
// Create index on owner_id for efficient querying
|
||||
ownerIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "owner_id", Value: 1}},
|
||||
}
|
||||
|
||||
// Create compound index on owner_id and created_at for pagination
|
||||
paginationIndex := mongo.IndexModel{
|
||||
Keys: bson.D{{Key: "owner_id", Value: 1}, {Key: "created_at", Value: -1}},
|
||||
}
|
||||
|
||||
_, err := coll.Indexes().CreateMany(ctx, []mongo.IndexModel{ownerIndex, paginationIndex})
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *MongoDBStorage) CreateSavedSearch(ctx context.Context, search types.SavedSearch) error {
|
||||
coll := s.client.Database(s.database).Collection(s.collection)
|
||||
|
||||
now := time.Now()
|
||||
search.CreatedAt = now
|
||||
search.UpdatedAt = now
|
||||
|
||||
_, err := coll.InsertOne(ctx, search)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("CreateSavedSearch error")
|
||||
return fmt.Errorf("failed to create saved search: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MongoDBStorage) GetSavedSearch(ctx context.Context, id string) (*types.SavedSearch, error) {
|
||||
coll := s.client.Database(s.database).Collection(s.collection)
|
||||
|
||||
var search types.SavedSearch
|
||||
err := coll.FindOne(ctx, bson.M{"_id": id}).Decode(&search)
|
||||
if err != nil {
|
||||
if err == mongo.ErrNoDocuments {
|
||||
return nil, fmt.Errorf("saved search not found")
|
||||
}
|
||||
log.Error().Err(err).Msg("GetSavedSearch error")
|
||||
return nil, fmt.Errorf("failed to get saved search: %w", err)
|
||||
}
|
||||
|
||||
return &search, nil
|
||||
}
|
||||
|
||||
func (s *MongoDBStorage) GetSavedSearchesByOwner(ctx context.Context, ownerID string) ([]*types.SavedSearch, error) {
|
||||
coll := s.client.Database(s.database).Collection(s.collection)
|
||||
|
||||
// Filter to only include future searches
|
||||
filter := bson.M{
|
||||
"owner_id": ownerID,
|
||||
"datetime": bson.M{"$gt": time.Now()},
|
||||
}
|
||||
cursor, err := coll.Find(ctx, filter)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("GetSavedSearchesByOwner error")
|
||||
return nil, fmt.Errorf("failed to get saved searches: %w", err)
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var searches []*types.SavedSearch
|
||||
for cursor.Next(ctx) {
|
||||
var search types.SavedSearch
|
||||
if err := cursor.Decode(&search); err != nil {
|
||||
log.Error().Err(err).Msg("decode saved search error")
|
||||
continue
|
||||
}
|
||||
searches = append(searches, &search)
|
||||
}
|
||||
|
||||
return searches, nil
|
||||
}
|
||||
|
||||
func (s *MongoDBStorage) UpdateSavedSearch(ctx context.Context, search types.SavedSearch) error {
|
||||
coll := s.client.Database(s.database).Collection(s.collection)
|
||||
|
||||
search.UpdatedAt = time.Now()
|
||||
|
||||
filter := bson.M{"_id": search.ID}
|
||||
update := bson.M{"$set": search}
|
||||
|
||||
result, err := coll.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("UpdateSavedSearch error")
|
||||
return fmt.Errorf("failed to update saved search: %w", err)
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return fmt.Errorf("saved search not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MongoDBStorage) DeleteSavedSearch(ctx context.Context, id string) error {
|
||||
coll := s.client.Database(s.database).Collection(s.collection)
|
||||
|
||||
result, err := coll.DeleteOne(ctx, bson.M{"_id": id})
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("DeleteSavedSearch error")
|
||||
return fmt.Errorf("failed to delete saved search: %w", err)
|
||||
}
|
||||
|
||||
if result.DeletedCount == 0 {
|
||||
return fmt.Errorf("saved search not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MongoDBStorage) ListSavedSearches(ctx context.Context, ownerID string, limit, offset int) ([]*types.SavedSearch, int64, error) {
|
||||
coll := s.client.Database(s.database).Collection(s.collection)
|
||||
|
||||
filter := bson.M{}
|
||||
if ownerID != "" {
|
||||
filter["owner_id"] = ownerID
|
||||
}
|
||||
|
||||
// Get total count
|
||||
total, err := coll.CountDocuments(ctx, filter)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("ListSavedSearches count error")
|
||||
return nil, 0, fmt.Errorf("failed to count saved searches: %w", err)
|
||||
}
|
||||
|
||||
// Get paginated results
|
||||
opts := options.Find().
|
||||
SetSort(bson.D{{Key: "created_at", Value: -1}}).
|
||||
SetLimit(int64(limit)).
|
||||
SetSkip(int64(offset))
|
||||
|
||||
cursor, err := coll.Find(ctx, filter, opts)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msg("ListSavedSearches find error")
|
||||
return nil, 0, fmt.Errorf("failed to list saved searches: %w", err)
|
||||
}
|
||||
defer cursor.Close(ctx)
|
||||
|
||||
var searches []*types.SavedSearch
|
||||
for cursor.Next(ctx) {
|
||||
var search types.SavedSearch
|
||||
if err := cursor.Decode(&search); err != nil {
|
||||
log.Error().Err(err).Msg("decode saved search error")
|
||||
continue
|
||||
}
|
||||
searches = append(searches, &search)
|
||||
}
|
||||
|
||||
return searches, total, nil
|
||||
}
|
||||
28
data/storage/storage.go
Normal file
28
data/storage/storage.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"git.coopgo.io/coopgo-platform/saved-search/data/types"
|
||||
)
|
||||
|
||||
// Storage interface defines the methods for saved search storage
|
||||
type Storage interface {
|
||||
// CreateSavedSearch creates a new saved search
|
||||
CreateSavedSearch(ctx context.Context, search types.SavedSearch) error
|
||||
|
||||
// GetSavedSearch retrieves a saved search by ID
|
||||
GetSavedSearch(ctx context.Context, id string) (*types.SavedSearch, error)
|
||||
|
||||
// GetSavedSearchesByOwner retrieves all saved searches for a specific owner
|
||||
GetSavedSearchesByOwner(ctx context.Context, ownerID string) ([]*types.SavedSearch, error)
|
||||
|
||||
// UpdateSavedSearch updates an existing saved search
|
||||
UpdateSavedSearch(ctx context.Context, search types.SavedSearch) error
|
||||
|
||||
// DeleteSavedSearch deletes a saved search by ID
|
||||
DeleteSavedSearch(ctx context.Context, id string) error
|
||||
|
||||
// ListSavedSearches retrieves paginated saved searches with optional filtering
|
||||
ListSavedSearches(ctx context.Context, ownerID string, limit, offset int) ([]*types.SavedSearch, int64, error)
|
||||
}
|
||||
20
data/storage/storage_factory.go
Normal file
20
data/storage/storage_factory.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
func NewStorage(cfg *viper.Viper) (Storage, error) {
|
||||
storage_type := cfg.GetString("storage.db.type")
|
||||
|
||||
switch storage_type {
|
||||
case "mongodb":
|
||||
return NewMongoDBStorageFromConfig(cfg)
|
||||
case "mock":
|
||||
return NewMockStorage(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("storage type %v is not supported", storage_type)
|
||||
}
|
||||
}
|
||||
19
data/types/saved_search.go
Normal file
19
data/types/saved_search.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/paulmach/orb/geojson"
|
||||
)
|
||||
|
||||
// SavedSearch represents a saved journey search
|
||||
type SavedSearch struct {
|
||||
ID string `json:"id" bson:"_id"`
|
||||
OwnerID string `json:"owner_id" bson:"owner_id"`
|
||||
Departure *geojson.Feature `json:"departure" bson:"departure"`
|
||||
Destination *geojson.Feature `json:"destination" bson:"destination"`
|
||||
DateTime time.Time `json:"datetime" bson:"datetime"`
|
||||
Data map[string]interface{} `json:"data" bson:"data"`
|
||||
CreatedAt time.Time `json:"created_at" bson:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at" bson:"updated_at"`
|
||||
}
|
||||
Reference in New Issue
Block a user