registry/internal/database/memory.go
adam jones 9d9a02c57f
Database implementation cleanup (#335)
## Motivation and Context

The database logic was getting a bit unwieldy and fragile - especially
because business logic that should really have lived at the registry
service level was being pushed into the persistence level.

This PR tightens the interface, simplifies the code there, as well as
rips out a bunch of mocks etc. we don't need.

## How Has This Been Tested?

Locally everything works

## Breaking Changes

The list endpoint was surprisingly always filtering for isLatest only,
which seems like a bug? I have removed this filter.
2025-09-02 13:37:28 +01:00

200 lines
4.5 KiB
Go

package database
import (
"context"
"fmt"
"sort"
"sync"
apiv0 "github.com/modelcontextprotocol/registry/pkg/api/v0"
)
// MemoryDB is an in-memory implementation of the Database interface
type MemoryDB struct {
entries map[string]*apiv0.ServerJSON // maps registry metadata ID to ServerJSON
mu sync.RWMutex
}
func NewMemoryDB() *MemoryDB {
// Convert input ServerJSON entries to have proper metadata
serverRecords := make(map[string]*apiv0.ServerJSON)
return &MemoryDB{
entries: serverRecords,
}
}
func (db *MemoryDB) List(
ctx context.Context,
filter *ServerFilter,
cursor string,
limit int,
) ([]*apiv0.ServerJSON, string, error) {
if ctx.Err() != nil {
return nil, "", ctx.Err()
}
if limit <= 0 {
limit = 10 // Default limit
}
db.mu.RLock()
defer db.mu.RUnlock()
// Convert all entries to a slice for pagination
var allEntries []*apiv0.ServerJSON
for _, entry := range db.entries {
allEntries = append(allEntries, entry)
}
// Apply filtering and sorting
filteredEntries := db.filterAndSort(allEntries, filter)
// Find starting point for cursor-based pagination
startIdx := 0
if cursor != "" {
for i, entry := range filteredEntries {
if db.getRegistryID(entry) == cursor {
startIdx = i + 1 // Start after the cursor
break
}
}
}
// Apply pagination
endIdx := min(startIdx+limit, len(filteredEntries))
var result []*apiv0.ServerJSON
if startIdx < len(filteredEntries) {
result = filteredEntries[startIdx:endIdx]
} else {
result = []*apiv0.ServerJSON{}
}
// Determine next cursor
nextCursor := ""
if endIdx < len(filteredEntries) && len(result) > 0 {
nextCursor = db.getRegistryID(result[len(result)-1])
}
return result, nextCursor, nil
}
func (db *MemoryDB) GetByID(ctx context.Context, id string) (*apiv0.ServerJSON, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
db.mu.RLock()
defer db.mu.RUnlock()
// Find entry by registry metadata ID
if entry, exists := db.entries[id]; exists {
// Return a copy of the ServerRecord
entryCopy := *entry
return &entryCopy, nil
}
return nil, ErrNotFound
}
func (db *MemoryDB) CreateServer(ctx context.Context, server *apiv0.ServerJSON) (*apiv0.ServerJSON, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
// Get the ID from the registry metadata
if server.Meta == nil || server.Meta.IOModelContextProtocolRegistry == nil {
return nil, fmt.Errorf("server must have registry metadata with ID")
}
id := server.Meta.IOModelContextProtocolRegistry.ID
db.mu.Lock()
defer db.mu.Unlock()
// Store the record using registry metadata ID
db.entries[id] = server
return server, nil
}
func (db *MemoryDB) UpdateServer(ctx context.Context, id string, server *apiv0.ServerJSON) (*apiv0.ServerJSON, error) {
if ctx.Err() != nil {
return nil, ctx.Err()
}
db.mu.Lock()
defer db.mu.Unlock()
_, exists := db.entries[id]
if !exists {
return nil, ErrNotFound
}
// Update the server
db.entries[id] = server
// Return the updated record
return server, nil
}
// For an in-memory database, this is a no-op
func (db *MemoryDB) Close() error {
return nil
}
// filterAndSort applies filtering and sorting to the entries
func (db *MemoryDB) filterAndSort(allEntries []*apiv0.ServerJSON, filter *ServerFilter) []*apiv0.ServerJSON {
// Apply filtering
var filteredEntries []*apiv0.ServerJSON
for _, entry := range allEntries {
if db.matchesFilter(entry, filter) {
filteredEntries = append(filteredEntries, entry)
}
}
// Sort by registry metadata ID for consistent pagination
sort.Slice(filteredEntries, func(i, j int) bool {
iID := db.getRegistryID(filteredEntries[i])
jID := db.getRegistryID(filteredEntries[j])
return iID < jID
})
return filteredEntries
}
// matchesFilter checks if an entry matches the provided filter
func (db *MemoryDB) matchesFilter(entry *apiv0.ServerJSON, filter *ServerFilter) bool {
if filter == nil {
return true
}
// Check name filter
if filter.Name != nil && entry.Name != *filter.Name {
return false
}
// Check remote URL filter
if filter.RemoteURL != nil {
found := false
for _, remote := range entry.Remotes {
if remote.URL == *filter.RemoteURL {
found = true
break
}
}
if !found {
return false
}
}
return true
}
// getRegistryID safely extracts the registry ID from an entry
func (db *MemoryDB) getRegistryID(entry *apiv0.ServerJSON) string {
if entry.Meta != nil && entry.Meta.IOModelContextProtocolRegistry != nil {
return entry.Meta.IOModelContextProtocolRegistry.ID
}
return ""
}