service backend
This commit is contained in:
144
api/pkg/db/internal/mongo/indexable/README.md
Normal file
144
api/pkg/db/internal/mongo/indexable/README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# Indexable Implementation (Refactored)
|
||||
|
||||
## Overview
|
||||
|
||||
This package provides a refactored implementation of the `indexable.DB` interface that uses `mutil.GetObjects` for better consistency with the existing codebase. The implementation has been moved to the mongo folder and includes a factory for project indexable in the pkg/db folder.
|
||||
|
||||
## Structure
|
||||
|
||||
### 1. `api/pkg/db/internal/mongo/indexable/indexable.go`
|
||||
- **`ReorderTemplate[T]`**: Generic template function that uses `mutil.GetObjects` for fetching objects
|
||||
- **`IndexableDB`**: Base struct for creating concrete implementations
|
||||
- **Type-safe implementation**: Uses Go generics with proper type constraints
|
||||
|
||||
### 2. `api/pkg/db/project_indexable.go`
|
||||
- **`ProjectIndexableDB`**: Factory implementation for Project objects
|
||||
- **`NewProjectIndexableDB`**: Constructor function
|
||||
- **`ReorderTemplate`**: Duplicate of the mongo version for convenience
|
||||
|
||||
## Key Changes from Previous Implementation
|
||||
|
||||
### 1. **Uses `mutil.GetObjects`**
|
||||
```go
|
||||
// Old implementation (manual cursor handling)
|
||||
err = repo.FindManyByFilter(ctx, filter, func(cursor *mongo.Cursor) error {
|
||||
var obj T
|
||||
if err := cursor.Decode(&obj); err != nil {
|
||||
return err
|
||||
}
|
||||
objects = append(objects, obj)
|
||||
return nil
|
||||
})
|
||||
|
||||
// New implementation (using mutil.GetObjects)
|
||||
objects, err := mutil.GetObjects[T](
|
||||
ctx,
|
||||
logger,
|
||||
filterFunc().
|
||||
And(
|
||||
repository.IndexOpFilter(minIdx, builder.Gte),
|
||||
repository.IndexOpFilter(maxIdx, builder.Lte),
|
||||
),
|
||||
nil, nil, nil, // limit, offset, isArchived
|
||||
repo,
|
||||
)
|
||||
```
|
||||
|
||||
### 2. **Moved to Mongo Folder**
|
||||
- Location: `api/pkg/db/internal/mongo/indexable/`
|
||||
- Consistent with other mongo implementations
|
||||
- Better organization within the codebase
|
||||
|
||||
### 3. **Added Factory in pkg/db**
|
||||
- Location: `api/pkg/db/project_indexable.go`
|
||||
- Provides easy access to project indexable functionality
|
||||
- Includes logger parameter for better error handling
|
||||
|
||||
## Usage
|
||||
|
||||
### Using the Factory (Recommended)
|
||||
|
||||
```go
|
||||
import "github.com/tech/sendico/pkg/db"
|
||||
|
||||
// Create a project indexable DB
|
||||
projectDB := db.NewProjectIndexableDB(repo, logger, organizationRef)
|
||||
|
||||
// Reorder a project
|
||||
err := projectDB.Reorder(ctx, projectID, newIndex)
|
||||
if err != nil {
|
||||
// Handle error
|
||||
}
|
||||
```
|
||||
|
||||
### Using the Template Directly
|
||||
|
||||
```go
|
||||
import "github.com/tech/sendico/pkg/db/internal/mongo/indexable"
|
||||
|
||||
// Define helper functions
|
||||
getIndexable := func(p *model.Project) *model.Indexable {
|
||||
return &p.Indexable
|
||||
}
|
||||
|
||||
updateIndexable := func(p *model.Project, newIndex int) {
|
||||
p.Index = newIndex
|
||||
}
|
||||
|
||||
createEmpty := func() *model.Project {
|
||||
return &model.Project{}
|
||||
}
|
||||
|
||||
filterFunc := func() builder.Query {
|
||||
return repository.OrgFilter(organizationRef)
|
||||
}
|
||||
|
||||
// Use the template
|
||||
err := indexable.ReorderTemplate(
|
||||
ctx,
|
||||
logger,
|
||||
repo,
|
||||
objectRef,
|
||||
newIndex,
|
||||
filterFunc,
|
||||
getIndexable,
|
||||
updateIndexable,
|
||||
createEmpty,
|
||||
)
|
||||
```
|
||||
|
||||
## Benefits of Refactoring
|
||||
|
||||
1. **Consistency**: Uses `mutil.GetObjects` like other parts of the codebase
|
||||
2. **Better Error Handling**: Includes logger parameter for proper error logging
|
||||
3. **Organization**: Moved to appropriate folder structure
|
||||
4. **Factory Pattern**: Easy-to-use factory for common use cases
|
||||
5. **Type Safety**: Maintains compile-time type checking
|
||||
6. **Performance**: Leverages existing optimized `mutil.GetObjects` implementation
|
||||
|
||||
## Testing
|
||||
|
||||
### Mongo Implementation Tests
|
||||
```bash
|
||||
go test ./db/internal/mongo/indexable -v
|
||||
```
|
||||
|
||||
### Factory Tests
|
||||
```bash
|
||||
go test ./db -v
|
||||
```
|
||||
|
||||
## Integration
|
||||
|
||||
The refactored implementation is ready for integration with existing project reordering APIs. The factory pattern makes it easy to add reordering functionality to any service that needs to reorder projects within an organization.
|
||||
|
||||
## Migration from Old Implementation
|
||||
|
||||
If you were using the old implementation:
|
||||
|
||||
1. **Update imports**: Change from `api/pkg/db/internal/indexable` to `api/pkg/db`
|
||||
2. **Use factory**: Replace manual template usage with `NewProjectIndexableDB`
|
||||
3. **Add logger**: Include a logger parameter in your constructor calls
|
||||
4. **Update tests**: Use the new test structure if needed
|
||||
|
||||
The API remains the same, so existing code should work with minimal changes.
|
||||
174
api/pkg/db/internal/mongo/indexable/USAGE.md
Normal file
174
api/pkg/db/internal/mongo/indexable/USAGE.md
Normal file
@@ -0,0 +1,174 @@
|
||||
# Indexable Usage Guide
|
||||
|
||||
## Generic Implementation for Any Indexable Struct
|
||||
|
||||
The implementation is now **generic** and supports **any struct that embeds `model.Indexable`**!
|
||||
|
||||
- **Interface**: `api/pkg/db/indexable.go` - defines the contract
|
||||
- **Implementation**: `api/pkg/db/internal/mongo/indexable/` - generic implementation
|
||||
- **Factory**: `api/pkg/db/project_indexable.go` - convenient factory for projects
|
||||
|
||||
## Usage
|
||||
|
||||
### 1. Using the Generic Implementation Directly
|
||||
|
||||
```go
|
||||
import "github.com/tech/sendico/pkg/db/internal/mongo/indexable"
|
||||
|
||||
// For any type that embeds model.Indexable, define helper functions:
|
||||
createEmpty := func() *YourType {
|
||||
return &YourType{}
|
||||
}
|
||||
|
||||
getIndexable := func(obj *YourType) *model.Indexable {
|
||||
return &obj.Indexable
|
||||
}
|
||||
|
||||
// Create generic IndexableDB
|
||||
indexableDB := indexable.NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
|
||||
// Use with single filter parameter
|
||||
err := indexableDB.Reorder(ctx, objectID, newIndex, filter)
|
||||
```
|
||||
|
||||
### 2. Using the Project Factory (Recommended for Projects)
|
||||
|
||||
```go
|
||||
import "github.com/tech/sendico/pkg/db"
|
||||
|
||||
// Create project indexable DB (automatically applies org filter)
|
||||
projectDB := db.NewProjectIndexableDB(repo, logger, organizationRef)
|
||||
|
||||
// Reorder project (org filter applied automatically)
|
||||
err := projectDB.Reorder(ctx, projectID, newIndex, repository.Query())
|
||||
|
||||
// Reorder with additional filters (combined with org filter)
|
||||
additionalFilter := repository.Query().Comparison(repository.Field("state"), builder.Eq, "active")
|
||||
err := projectDB.Reorder(ctx, projectID, newIndex, additionalFilter)
|
||||
```
|
||||
|
||||
## Examples for Different Types
|
||||
|
||||
### Project IndexableDB
|
||||
```go
|
||||
createEmpty := func() *model.Project {
|
||||
return &model.Project{}
|
||||
}
|
||||
|
||||
getIndexable := func(p *model.Project) *model.Indexable {
|
||||
return &p.Indexable
|
||||
}
|
||||
|
||||
projectDB := indexable.NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
orgFilter := repository.OrgFilter(organizationRef)
|
||||
projectDB.Reorder(ctx, projectID, 2, orgFilter)
|
||||
```
|
||||
|
||||
### Status IndexableDB
|
||||
```go
|
||||
createEmpty := func() *model.Status {
|
||||
return &model.Status{}
|
||||
}
|
||||
|
||||
getIndexable := func(s *model.Status) *model.Indexable {
|
||||
return &s.Indexable
|
||||
}
|
||||
|
||||
statusDB := indexable.NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
projectFilter := repository.Query().Comparison(repository.Field("projectRef"), builder.Eq, projectRef)
|
||||
statusDB.Reorder(ctx, statusID, 1, projectFilter)
|
||||
```
|
||||
|
||||
### Task IndexableDB
|
||||
```go
|
||||
createEmpty := func() *model.Task {
|
||||
return &model.Task{}
|
||||
}
|
||||
|
||||
getIndexable := func(t *model.Task) *model.Indexable {
|
||||
return &t.Indexable
|
||||
}
|
||||
|
||||
taskDB := indexable.NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
statusFilter := repository.Query().Comparison(repository.Field("statusRef"), builder.Eq, statusRef)
|
||||
taskDB.Reorder(ctx, taskID, 3, statusFilter)
|
||||
```
|
||||
|
||||
### Priority IndexableDB
|
||||
```go
|
||||
createEmpty := func() *model.Priority {
|
||||
return &model.Priority{}
|
||||
}
|
||||
|
||||
getIndexable := func(p *model.Priority) *model.Indexable {
|
||||
return &p.Indexable
|
||||
}
|
||||
|
||||
priorityDB := indexable.NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
orgFilter := repository.OrgFilter(organizationRef)
|
||||
priorityDB.Reorder(ctx, priorityID, 0, orgFilter)
|
||||
```
|
||||
|
||||
### Global Reordering (No Filter)
|
||||
```go
|
||||
createEmpty := func() *model.Project {
|
||||
return &model.Project{}
|
||||
}
|
||||
|
||||
getIndexable := func(p *model.Project) *model.Indexable {
|
||||
return &p.Indexable
|
||||
}
|
||||
|
||||
globalDB := indexable.NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
// Reorders all items globally (empty filter)
|
||||
globalDB.Reorder(ctx, objectID, 5, repository.Query())
|
||||
```
|
||||
|
||||
## Key Features
|
||||
|
||||
### ✅ **Generic Support**
|
||||
- Works with **any struct** that embeds `model.Indexable`
|
||||
- Type-safe with compile-time checking
|
||||
- No hardcoded types
|
||||
|
||||
### ✅ **Single Filter Parameter**
|
||||
- **Simple**: Single `builder.Query` parameter instead of variadic `interface{}`
|
||||
- **Flexible**: Can incorporate any combination of filters
|
||||
- **Type-safe**: No runtime type assertions needed
|
||||
|
||||
### ✅ **Clean Architecture**
|
||||
- Interface separated from implementation
|
||||
- Generic implementation in internal package
|
||||
- Easy-to-use factories for common types
|
||||
|
||||
## How It Works
|
||||
|
||||
### Generic Algorithm
|
||||
1. **Get current index** using type-specific helper function
|
||||
2. **If no change needed** → return early
|
||||
3. **Apply filter** to scope affected items
|
||||
4. **Shift affected items** using `PatchMany` with `$inc`
|
||||
5. **Update target object** using `Patch` with `$set`
|
||||
|
||||
### Type-Safe Implementation
|
||||
```go
|
||||
type IndexableDB[T storable.Storable] struct {
|
||||
repo repository.Repository
|
||||
logger mlogger.Logger
|
||||
createEmpty func() T
|
||||
getIndexable func(T) *model.Indexable
|
||||
}
|
||||
|
||||
// Single filter parameter - clean and simple
|
||||
func (db *IndexableDB[T]) Reorder(ctx context.Context, objectRef primitive.ObjectID, newIndex int, filter builder.Query) error
|
||||
```
|
||||
|
||||
## Benefits
|
||||
|
||||
✅ **Generic** - Works with any Indexable struct
|
||||
✅ **Type Safe** - Compile-time type checking
|
||||
✅ **Simple** - Single filter parameter instead of variadic interface{}
|
||||
✅ **Efficient** - Uses patches, not full updates
|
||||
✅ **Clean** - Interface separated from implementation
|
||||
|
||||
That's it! **Generic, type-safe, and simple** reordering for any Indexable struct with a single filter parameter.
|
||||
69
api/pkg/db/internal/mongo/indexable/examples.go
Normal file
69
api/pkg/db/internal/mongo/indexable/examples.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package indexable
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// Example usage of the generic IndexableDB with different types
|
||||
|
||||
// Example 1: Using with Project
|
||||
func ExampleProjectIndexableDB(repo repository.Repository, logger mlogger.Logger, organizationRef primitive.ObjectID) {
|
||||
// Define helper functions for Project
|
||||
createEmpty := func() *model.Project {
|
||||
return &model.Project{}
|
||||
}
|
||||
|
||||
getIndexable := func(p *model.Project) *model.Indexable {
|
||||
return &p.Indexable
|
||||
}
|
||||
|
||||
// Create generic IndexableDB for Project
|
||||
projectDB := NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
|
||||
// Use with organization filter
|
||||
orgFilter := repository.OrgFilter(organizationRef)
|
||||
projectDB.Reorder(context.Background(), primitive.NewObjectID(), 2, orgFilter)
|
||||
}
|
||||
|
||||
// Example 3: Using with Task
|
||||
func ExampleTaskIndexableDB(repo repository.Repository, logger mlogger.Logger, statusRef primitive.ObjectID) {
|
||||
// Define helper functions for Task
|
||||
createEmpty := func() *model.Task {
|
||||
return &model.Task{}
|
||||
}
|
||||
|
||||
getIndexable := func(t *model.Task) *model.Indexable {
|
||||
return &t.Indexable
|
||||
}
|
||||
|
||||
// Create generic IndexableDB for Task
|
||||
taskDB := NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
|
||||
// Use with status filter
|
||||
statusFilter := repository.Query().Comparison(repository.Field("statusRef"), builder.Eq, statusRef)
|
||||
taskDB.Reorder(context.Background(), primitive.NewObjectID(), 3, statusFilter)
|
||||
}
|
||||
|
||||
// Example 5: Using without any filter (global reordering)
|
||||
func ExampleGlobalIndexableDB(repo repository.Repository, logger mlogger.Logger) {
|
||||
// Define helper functions for any Indexable type
|
||||
createEmpty := func() *model.Project {
|
||||
return &model.Project{}
|
||||
}
|
||||
|
||||
getIndexable := func(p *model.Project) *model.Indexable {
|
||||
return &p.Indexable
|
||||
}
|
||||
|
||||
// Create generic IndexableDB without filters
|
||||
globalDB := NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
|
||||
// Use without any filter - reorders all items globally
|
||||
globalDB.Reorder(context.Background(), primitive.NewObjectID(), 5, repository.Query())
|
||||
}
|
||||
122
api/pkg/db/internal/mongo/indexable/indexable.go
Normal file
122
api/pkg/db/internal/mongo/indexable/indexable.go
Normal file
@@ -0,0 +1,122 @@
|
||||
package indexable
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/db/repository/builder"
|
||||
"github.com/tech/sendico/pkg/db/storable"
|
||||
"github.com/tech/sendico/pkg/mlogger"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// IndexableDB implements db.IndexableDB interface with generic support
|
||||
type IndexableDB[T storable.Storable] struct {
|
||||
repo repository.Repository
|
||||
logger mlogger.Logger
|
||||
createEmpty func() T
|
||||
getIndexable func(T) *model.Indexable
|
||||
}
|
||||
|
||||
// NewIndexableDB creates a new IndexableDB instance
|
||||
func NewIndexableDB[T storable.Storable](
|
||||
repo repository.Repository,
|
||||
logger mlogger.Logger,
|
||||
createEmpty func() T,
|
||||
getIndexable func(T) *model.Indexable,
|
||||
) *IndexableDB[T] {
|
||||
return &IndexableDB[T]{
|
||||
repo: repo,
|
||||
logger: logger,
|
||||
createEmpty: createEmpty,
|
||||
getIndexable: getIndexable,
|
||||
}
|
||||
}
|
||||
|
||||
// Reorder implements the db.IndexableDB interface with single filter parameter
|
||||
func (db *IndexableDB[T]) Reorder(ctx context.Context, objectRef primitive.ObjectID, newIndex int, filter builder.Query) error {
|
||||
// Get current object to find its index
|
||||
obj := db.createEmpty()
|
||||
err := db.repo.Get(ctx, objectRef, obj)
|
||||
if err != nil {
|
||||
db.logger.Error("Failed to get object for reordering",
|
||||
zap.Error(err),
|
||||
zap.String("object_ref", objectRef.Hex()),
|
||||
zap.Int("new_index", newIndex))
|
||||
return err
|
||||
}
|
||||
|
||||
// Extract index from the object
|
||||
indexable := db.getIndexable(obj)
|
||||
currentIndex := indexable.Index
|
||||
if currentIndex == newIndex {
|
||||
db.logger.Debug("No reordering needed - same index",
|
||||
zap.String("object_ref", objectRef.Hex()),
|
||||
zap.Int("current_index", currentIndex),
|
||||
zap.Int("new_index", newIndex))
|
||||
return nil // No change needed
|
||||
}
|
||||
|
||||
// Simple reordering logic
|
||||
if currentIndex < newIndex {
|
||||
// Moving down: shift items between currentIndex+1 and newIndex up by -1
|
||||
patch := repository.Patch().Inc(repository.IndexField(), -1)
|
||||
reorderFilter := filter.
|
||||
And(repository.IndexOpFilter(currentIndex+1, builder.Gte)).
|
||||
And(repository.IndexOpFilter(newIndex, builder.Lte))
|
||||
|
||||
updatedCount, err := db.repo.PatchMany(ctx, reorderFilter, patch)
|
||||
if err != nil {
|
||||
db.logger.Error("Failed to shift objects during reordering (moving down)",
|
||||
zap.Error(err),
|
||||
zap.String("object_ref", objectRef.Hex()),
|
||||
zap.Int("current_index", currentIndex),
|
||||
zap.Int("new_index", newIndex),
|
||||
zap.Int("updated_count", updatedCount))
|
||||
return err
|
||||
}
|
||||
db.logger.Debug("Successfully shifted objects (moving down)",
|
||||
zap.String("object_ref", objectRef.Hex()),
|
||||
zap.Int("updated_count", updatedCount))
|
||||
} else {
|
||||
// Moving up: shift items between newIndex and currentIndex-1 down by +1
|
||||
patch := repository.Patch().Inc(repository.IndexField(), 1)
|
||||
reorderFilter := filter.
|
||||
And(repository.IndexOpFilter(newIndex, builder.Gte)).
|
||||
And(repository.IndexOpFilter(currentIndex-1, builder.Lte))
|
||||
|
||||
updatedCount, err := db.repo.PatchMany(ctx, reorderFilter, patch)
|
||||
if err != nil {
|
||||
db.logger.Error("Failed to shift objects during reordering (moving up)",
|
||||
zap.Error(err),
|
||||
zap.String("object_ref", objectRef.Hex()),
|
||||
zap.Int("current_index", currentIndex),
|
||||
zap.Int("new_index", newIndex),
|
||||
zap.Int("updated_count", updatedCount))
|
||||
return err
|
||||
}
|
||||
db.logger.Debug("Successfully shifted objects (moving up)",
|
||||
zap.String("object_ref", objectRef.Hex()),
|
||||
zap.Int("updated_count", updatedCount))
|
||||
}
|
||||
|
||||
// Update the target object to new index
|
||||
patch := repository.Patch().Set(repository.IndexField(), newIndex)
|
||||
err = db.repo.Patch(ctx, objectRef, patch)
|
||||
if err != nil {
|
||||
db.logger.Error("Failed to update target object index",
|
||||
zap.Error(err),
|
||||
zap.String("object_ref", objectRef.Hex()),
|
||||
zap.Int("current_index", currentIndex),
|
||||
zap.Int("new_index", newIndex))
|
||||
return err
|
||||
}
|
||||
|
||||
db.logger.Info("Successfully reordered object",
|
||||
zap.String("object_ref", objectRef.Hex()),
|
||||
zap.Int("old_index", currentIndex),
|
||||
zap.Int("new_index", newIndex))
|
||||
return nil
|
||||
}
|
||||
314
api/pkg/db/internal/mongo/indexable/indexable_test.go
Normal file
314
api/pkg/db/internal/mongo/indexable/indexable_test.go
Normal file
@@ -0,0 +1,314 @@
|
||||
//go:build integration
|
||||
// +build integration
|
||||
|
||||
package indexable
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/tech/sendico/pkg/db/repository"
|
||||
"github.com/tech/sendico/pkg/model"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/testcontainers/testcontainers-go"
|
||||
"github.com/testcontainers/testcontainers-go/modules/mongodb"
|
||||
"github.com/testcontainers/testcontainers-go/wait"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func setupTestDB(t *testing.T) (repository.Repository, func()) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
mongoContainer, err := mongodb.Run(ctx,
|
||||
"mongo:latest",
|
||||
mongodb.WithUsername("root"),
|
||||
mongodb.WithPassword("password"),
|
||||
testcontainers.WithWaitStrategy(wait.ForLog("Waiting for connections")),
|
||||
)
|
||||
require.NoError(t, err, "failed to start MongoDB container")
|
||||
|
||||
mongoURI, err := mongoContainer.ConnectionString(ctx)
|
||||
require.NoError(t, err, "failed to get MongoDB connection string")
|
||||
|
||||
clientOptions := options.Client().ApplyURI(mongoURI)
|
||||
client, err := mongo.Connect(ctx, clientOptions)
|
||||
require.NoError(t, err, "failed to connect to MongoDB")
|
||||
|
||||
db := client.Database("testdb")
|
||||
repo := repository.CreateMongoRepository(db, "projects")
|
||||
|
||||
cleanup := func() {
|
||||
disconnect(ctx, t, client)
|
||||
terminate(ctx, t, mongoContainer)
|
||||
}
|
||||
|
||||
return repo, cleanup
|
||||
}
|
||||
|
||||
func disconnect(ctx context.Context, t *testing.T, client *mongo.Client) {
|
||||
if err := client.Disconnect(ctx); err != nil {
|
||||
t.Logf("failed to disconnect from MongoDB: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func terminate(ctx context.Context, t *testing.T, container testcontainers.Container) {
|
||||
if err := container.Terminate(ctx); err != nil {
|
||||
t.Logf("failed to terminate MongoDB container: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIndexableDB_Reorder(t *testing.T) {
|
||||
repo, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
organizationRef := primitive.NewObjectID()
|
||||
logger := zap.NewNop()
|
||||
|
||||
// Create test projects with different indices
|
||||
projects := []*model.Project{
|
||||
{
|
||||
ProjectBase: model.ProjectBase{
|
||||
PermissionBound: model.PermissionBound{
|
||||
OrganizationBoundBase: model.OrganizationBoundBase{
|
||||
OrganizationRef: organizationRef,
|
||||
},
|
||||
},
|
||||
Describable: model.Describable{Name: "Project A"},
|
||||
Indexable: model.Indexable{Index: 0},
|
||||
Mnemonic: "A",
|
||||
State: model.ProjectStateActive,
|
||||
},
|
||||
},
|
||||
{
|
||||
ProjectBase: model.ProjectBase{
|
||||
PermissionBound: model.PermissionBound{
|
||||
OrganizationBoundBase: model.OrganizationBoundBase{
|
||||
OrganizationRef: organizationRef,
|
||||
},
|
||||
},
|
||||
Describable: model.Describable{Name: "Project B"},
|
||||
Indexable: model.Indexable{Index: 1},
|
||||
Mnemonic: "B",
|
||||
State: model.ProjectStateActive,
|
||||
},
|
||||
},
|
||||
{
|
||||
ProjectBase: model.ProjectBase{
|
||||
PermissionBound: model.PermissionBound{
|
||||
OrganizationBoundBase: model.OrganizationBoundBase{
|
||||
OrganizationRef: organizationRef,
|
||||
},
|
||||
},
|
||||
Describable: model.Describable{Name: "Project C"},
|
||||
Indexable: model.Indexable{Index: 2},
|
||||
Mnemonic: "C",
|
||||
State: model.ProjectStateActive,
|
||||
},
|
||||
},
|
||||
{
|
||||
ProjectBase: model.ProjectBase{
|
||||
PermissionBound: model.PermissionBound{
|
||||
OrganizationBoundBase: model.OrganizationBoundBase{
|
||||
OrganizationRef: organizationRef,
|
||||
},
|
||||
},
|
||||
Describable: model.Describable{Name: "Project D"},
|
||||
Indexable: model.Indexable{Index: 3},
|
||||
Mnemonic: "D",
|
||||
State: model.ProjectStateActive,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Insert projects into database
|
||||
for _, project := range projects {
|
||||
project.ID = primitive.NewObjectID()
|
||||
err := repo.Insert(ctx, project, nil)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Create helper functions for Project type
|
||||
createEmpty := func() *model.Project {
|
||||
return &model.Project{}
|
||||
}
|
||||
|
||||
getIndexable := func(p *model.Project) *model.Indexable {
|
||||
return &p.Indexable
|
||||
}
|
||||
|
||||
indexableDB := NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
|
||||
t.Run("Reorder_NoChange", func(t *testing.T) {
|
||||
// Test reordering to the same position (should be no-op)
|
||||
err := indexableDB.Reorder(ctx, projects[1].ID, 1, repository.Query())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify indices haven't changed
|
||||
var result model.Project
|
||||
err = repo.Get(ctx, projects[0].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Index)
|
||||
|
||||
err = repo.Get(ctx, projects[1].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, result.Index)
|
||||
})
|
||||
|
||||
t.Run("Reorder_MoveDown", func(t *testing.T) {
|
||||
// Move Project A (index 0) to index 2
|
||||
err := indexableDB.Reorder(ctx, projects[0].ID, 2, repository.Query())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the reordering:
|
||||
// Project A should now be at index 2
|
||||
// Project B should be at index 0
|
||||
// Project C should be at index 1
|
||||
// Project D should remain at index 3
|
||||
|
||||
var result model.Project
|
||||
|
||||
// Check Project A (moved to index 2)
|
||||
err = repo.Get(ctx, projects[0].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, result.Index)
|
||||
|
||||
// Check Project B (shifted to index 0)
|
||||
err = repo.Get(ctx, projects[1].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Index)
|
||||
|
||||
// Check Project C (shifted to index 1)
|
||||
err = repo.Get(ctx, projects[2].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, result.Index)
|
||||
|
||||
// Check Project D (unchanged)
|
||||
err = repo.Get(ctx, projects[3].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, result.Index)
|
||||
})
|
||||
|
||||
t.Run("Reorder_MoveUp", func(t *testing.T) {
|
||||
// Reset indices for this test
|
||||
for i, project := range projects {
|
||||
project.Index = i
|
||||
err := repo.Update(ctx, project)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Move Project C (index 2) to index 0
|
||||
err := indexableDB.Reorder(ctx, projects[2].ID, 0, repository.Query())
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the reordering:
|
||||
// Project C should now be at index 0
|
||||
// Project A should be at index 1
|
||||
// Project B should be at index 2
|
||||
// Project D should remain at index 3
|
||||
|
||||
var result model.Project
|
||||
|
||||
// Check Project C (moved to index 0)
|
||||
err = repo.Get(ctx, projects[2].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Index)
|
||||
|
||||
// Check Project A (shifted to index 1)
|
||||
err = repo.Get(ctx, projects[0].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, result.Index)
|
||||
|
||||
// Check Project B (shifted to index 2)
|
||||
err = repo.Get(ctx, projects[1].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, result.Index)
|
||||
|
||||
// Check Project D (unchanged)
|
||||
err = repo.Get(ctx, projects[3].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 3, result.Index)
|
||||
})
|
||||
|
||||
t.Run("Reorder_WithFilter", func(t *testing.T) {
|
||||
// Reset indices for this test
|
||||
for i, project := range projects {
|
||||
project.Index = i
|
||||
err := repo.Update(ctx, project)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
// Test reordering with organization filter
|
||||
orgFilter := repository.OrgFilter(organizationRef)
|
||||
err := indexableDB.Reorder(ctx, projects[0].ID, 2, orgFilter)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Verify the reordering worked with filter
|
||||
var result model.Project
|
||||
err = repo.Get(ctx, projects[0].ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, result.Index)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIndexableDB_EdgeCases(t *testing.T) {
|
||||
repo, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := context.Background()
|
||||
organizationRef := primitive.NewObjectID()
|
||||
logger := zap.NewNop()
|
||||
|
||||
// Create a single project for edge case testing
|
||||
project := &model.Project{
|
||||
ProjectBase: model.ProjectBase{
|
||||
PermissionBound: model.PermissionBound{
|
||||
OrganizationBoundBase: model.OrganizationBoundBase{
|
||||
OrganizationRef: organizationRef,
|
||||
},
|
||||
},
|
||||
Describable: model.Describable{Name: "Test Project"},
|
||||
Indexable: model.Indexable{Index: 0},
|
||||
Mnemonic: "TEST",
|
||||
State: model.ProjectStateActive,
|
||||
},
|
||||
}
|
||||
project.ID = primitive.NewObjectID()
|
||||
err := repo.Insert(ctx, project, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create helper functions for Project type
|
||||
createEmpty := func() *model.Project {
|
||||
return &model.Project{}
|
||||
}
|
||||
|
||||
getIndexable := func(p *model.Project) *model.Indexable {
|
||||
return &p.Indexable
|
||||
}
|
||||
|
||||
indexableDB := NewIndexableDB(repo, logger, createEmpty, getIndexable)
|
||||
|
||||
t.Run("Reorder_SingleItem", func(t *testing.T) {
|
||||
// Test reordering a single item (should work but have no effect)
|
||||
err := indexableDB.Reorder(ctx, project.ID, 0, repository.Query())
|
||||
require.NoError(t, err)
|
||||
|
||||
var result model.Project
|
||||
err = repo.Get(ctx, project.ID, &result)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 0, result.Index)
|
||||
})
|
||||
|
||||
t.Run("Reorder_InvalidObjectID", func(t *testing.T) {
|
||||
// Test reordering with an invalid object ID
|
||||
invalidID := primitive.NewObjectID()
|
||||
err := indexableDB.Reorder(ctx, invalidID, 1, repository.Query())
|
||||
require.Error(t, err) // Should fail because object doesn't exist
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user