Files
backend/internal/store/memory.go
Andriy Oblivantsev 59c9a719e0
CI / test (push) Successful in 3s
Add public GeoJSON features API and load public 3D objects on maps.
Expose GET /v1/features/public (optional kind filter) and update Leaflet/MapLibre demos to render all public 3D assets globally, while still merging owner collections after login.

Made-with: Cursor
2026-03-02 22:21:21 +00:00

353 lines
7.4 KiB
Go

package store
import (
"errors"
"sync"
"time"
)
var (
ErrNotFound = errors.New("not found")
ErrAlreadyExists = errors.New("already exists")
)
type MemoryStore struct {
mu sync.RWMutex
users map[string]User
challenges map[string]Challenge
sessions map[string]Session
invitations map[string]Invitation
collections map[string]Collection
features map[string]Feature
assets map[string]Asset
featureRefs map[string]map[string]FeatureAsset
}
func NewMemoryStore() *MemoryStore {
return &MemoryStore{
users: make(map[string]User),
challenges: make(map[string]Challenge),
sessions: make(map[string]Session),
invitations: make(map[string]Invitation),
collections: make(map[string]Collection),
features: make(map[string]Feature),
assets: make(map[string]Asset),
featureRefs: make(map[string]map[string]FeatureAsset),
}
}
func (s *MemoryStore) UpsertUser(user User) {
s.mu.Lock()
defer s.mu.Unlock()
s.users[user.PublicKey] = user
}
func (s *MemoryStore) GetUser(publicKey string) (User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[publicKey]
if !ok {
return User{}, ErrNotFound
}
return user, nil
}
func (s *MemoryStore) CreateChallenge(ch Challenge) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.challenges[ch.Nonce]; exists {
return ErrAlreadyExists
}
s.challenges[ch.Nonce] = ch
return nil
}
func (s *MemoryStore) GetChallenge(nonce string) (Challenge, error) {
s.mu.RLock()
defer s.mu.RUnlock()
ch, ok := s.challenges[nonce]
if !ok {
return Challenge{}, ErrNotFound
}
return ch, nil
}
func (s *MemoryStore) MarkChallengeUsed(nonce string) error {
s.mu.Lock()
defer s.mu.Unlock()
ch, ok := s.challenges[nonce]
if !ok {
return ErrNotFound
}
ch.Used = true
s.challenges[nonce] = ch
return nil
}
func (s *MemoryStore) SaveSession(session Session) {
s.mu.Lock()
defer s.mu.Unlock()
s.sessions[session.Token] = session
}
func (s *MemoryStore) GetSession(token string) (Session, error) {
s.mu.RLock()
defer s.mu.RUnlock()
session, ok := s.sessions[token]
if !ok {
return Session{}, ErrNotFound
}
return session, nil
}
func (s *MemoryStore) SaveInvitation(inv Invitation) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, exists := s.invitations[inv.JTI]; exists {
return ErrAlreadyExists
}
s.invitations[inv.JTI] = inv
return nil
}
func (s *MemoryStore) GetInvitation(jti string) (Invitation, error) {
s.mu.RLock()
defer s.mu.RUnlock()
inv, ok := s.invitations[jti]
if !ok {
return Invitation{}, ErrNotFound
}
return inv, nil
}
func (s *MemoryStore) IncrementInvitationUsage(jti string) error {
s.mu.Lock()
defer s.mu.Unlock()
inv, ok := s.invitations[jti]
if !ok {
return ErrNotFound
}
inv.UsedCount++
s.invitations[jti] = inv
return nil
}
func (s *MemoryStore) SaveCollection(c Collection) {
s.mu.Lock()
defer s.mu.Unlock()
s.collections[c.ID] = c
}
func (s *MemoryStore) ListCollectionsByOwner(owner string) []Collection {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Collection, 0)
for _, c := range s.collections {
if c.OwnerKey == owner {
result = append(result, c)
}
}
return result
}
func (s *MemoryStore) GetCollection(id string) (Collection, error) {
s.mu.RLock()
defer s.mu.RUnlock()
c, ok := s.collections[id]
if !ok {
return Collection{}, ErrNotFound
}
return c, nil
}
func (s *MemoryStore) DeleteCollection(id string) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.collections[id]; !ok {
return ErrNotFound
}
for fid, f := range s.features {
if f.CollectionID == id {
delete(s.features, fid)
delete(s.featureRefs, fid)
}
}
delete(s.collections, id)
return nil
}
func (s *MemoryStore) SaveFeature(f Feature) {
s.mu.Lock()
defer s.mu.Unlock()
s.features[f.ID] = f
}
func (s *MemoryStore) ListFeaturesByCollection(collectionID string) []Feature {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Feature, 0)
for _, f := range s.features {
if f.CollectionID == collectionID {
result = append(result, f)
}
}
return result
}
func (s *MemoryStore) ListFeaturesAll() []Feature {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]Feature, 0, len(s.features))
for _, f := range s.features {
result = append(result, f)
}
return result
}
func (s *MemoryStore) GetFeature(featureID string) (Feature, error) {
s.mu.RLock()
defer s.mu.RUnlock()
f, ok := s.features[featureID]
if !ok {
return Feature{}, ErrNotFound
}
return f, nil
}
func (s *MemoryStore) DeleteFeature(featureID string) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.features[featureID]; !ok {
return ErrNotFound
}
delete(s.features, featureID)
delete(s.featureRefs, featureID)
return nil
}
func (s *MemoryStore) SaveAsset(a Asset) {
s.mu.Lock()
defer s.mu.Unlock()
s.assets[a.ID] = a
}
func (s *MemoryStore) GetAsset(assetID string) (Asset, error) {
s.mu.RLock()
defer s.mu.RUnlock()
a, ok := s.assets[assetID]
if !ok {
return Asset{}, ErrNotFound
}
return a, nil
}
func (s *MemoryStore) GetAssetByOwnerChecksumExt(ownerKey, checksum, ext string) (Asset, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, a := range s.assets {
if a.OwnerKey == ownerKey && a.Checksum == checksum && a.Ext == ext {
return a, nil
}
}
return Asset{}, ErrNotFound
}
func (s *MemoryStore) SetAssetPublic(assetID string, isPublic bool) error {
s.mu.Lock()
defer s.mu.Unlock()
a, ok := s.assets[assetID]
if !ok {
return ErrNotFound
}
a.IsPublic = isPublic
a.UpdatedAt = time.Now().UTC()
s.assets[assetID] = a
return nil
}
func (s *MemoryStore) LinkAssetToFeature(featureID, assetID, name, description string) error {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.features[featureID]; !ok {
return ErrNotFound
}
a, ok := s.assets[assetID]
if !ok {
return ErrNotFound
}
if _, ok := s.featureRefs[featureID]; !ok {
s.featureRefs[featureID] = make(map[string]FeatureAsset)
}
if existing, exists := s.featureRefs[featureID][assetID]; exists {
existing.Name = name
existing.Description = description
s.featureRefs[featureID][assetID] = existing
return nil
}
s.featureRefs[featureID][assetID] = FeatureAsset{
Asset: a,
FeatureID: featureID,
Name: name,
Description: description,
LinkedAt: time.Now().UTC(),
}
return nil
}
func (s *MemoryStore) UnlinkAssetFromFeature(featureID, assetID string) error {
s.mu.Lock()
defer s.mu.Unlock()
links, ok := s.featureRefs[featureID]
if !ok {
return ErrNotFound
}
if _, exists := links[assetID]; !exists {
return ErrNotFound
}
delete(links, assetID)
return nil
}
func (s *MemoryStore) ListAssetsByFeature(featureID string) []FeatureAsset {
s.mu.RLock()
defer s.mu.RUnlock()
links, ok := s.featureRefs[featureID]
if !ok {
return []FeatureAsset{}
}
result := make([]FeatureAsset, 0, len(links))
for assetID, fa := range links {
if updated, exists := s.assets[assetID]; exists {
fa.Asset = updated
}
result = append(result, fa)
}
return result
}
func (s *MemoryStore) PruneExpired(now time.Time) {
s.mu.Lock()
defer s.mu.Unlock()
for nonce, ch := range s.challenges {
if ch.ExpiresAt.Before(now) {
delete(s.challenges, nonce)
}
}
for token, sess := range s.sessions {
if sess.ExpiresAt.Before(now) {
delete(s.sessions, token)
}
}
for jti, inv := range s.invitations {
if inv.ExpiresAt.Before(now) {
delete(s.invitations, jti)
}
}
}
func (s *MemoryStore) SaveUserLogin(ul UserLogin) {
// In-memory store: no-op for login history (persistence only in Postgres)
}