package store import ( "context" "database/sql" "encoding/json" "fmt" "time" "github.com/google/uuid" ) // Space operations func (r *sqliteRepo) CreateSpace(ctx context.Context, space *Space) (*Space, error) { now := utcNow() space.ID = uuid.NewString() space.CreatedAt = now space.UpdatedAt = now _, err := r.db.ExecContext(ctx, `INSERT INTO spaces (id, slug, name, description, is_public, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, space.ID, space.Slug, space.Name, space.Description, space.IsPublic, space.CreatedAt, space.UpdatedAt) if err != nil { return nil, fmt.Errorf("store: create space: %w", err) } return space, nil } func (r *sqliteRepo) GetSpaceBySlug(ctx context.Context, slug string) (*Space, error) { row := r.db.QueryRowContext(ctx, `SELECT id, slug, name, description, is_public, created_at, updated_at FROM spaces WHERE slug = ?`, slug) space := &Space{} if err := row.Scan(&space.ID, &space.Slug, &space.Name, &space.Description, &space.IsPublic, &space.CreatedAt, &space.UpdatedAt); err != nil { return nil, fmt.Errorf("store: get space by slug: %w", err) } return space, nil } func (r *sqliteRepo) UpdateSpace(ctx context.Context, space *Space) (*Space, error) { space.UpdatedAt = utcNow() _, err := r.db.ExecContext(ctx, `UPDATE spaces SET name = ?, description = ?, is_public = ?, updated_at = ? WHERE id = ?`, space.Name, space.Description, space.IsPublic, space.UpdatedAt, space.ID) if err != nil { return nil, fmt.Errorf("store: update space: %w", err) } return space, nil } func (r *sqliteRepo) DeleteSpace(ctx context.Context, id string) error { _, err := r.db.ExecContext(ctx, `DELETE FROM spaces WHERE id = ?`, id) if err != nil { return fmt.Errorf("store: delete space: %w", err) } return nil } func (r *sqliteRepo) ListPublicSpaces(ctx context.Context) ([]*Space, error) { rows, err := r.db.QueryContext(ctx, `SELECT id, slug, name, description, is_public, created_at, updated_at FROM spaces WHERE is_public = TRUE ORDER BY created_at DESC`) if err != nil { return nil, fmt.Errorf("store: list public spaces: %w", err) } defer rows.Close() var spaces []*Space for rows.Next() { space := &Space{} if err := rows.Scan(&space.ID, &space.Slug, &space.Name, &space.Description, &space.IsPublic, &space.CreatedAt, &space.UpdatedAt); err != nil { return nil, fmt.Errorf("store: scan space: %w", err) } spaces = append(spaces, space) } return spaces, rows.Err() } // Photo operations func (r *sqliteRepo) CreatePhoto(ctx context.Context, photo *Photo) (*Photo, error) { photo.ID = uuid.NewString() photo.CreatedAt = utcNow() _, err := r.db.ExecContext(ctx, `INSERT INTO photos (id, space_id, url, thumbnail_url, face_count, metadata, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, photo.ID, photo.SpaceID, photo.URL, photo.ThumbnailURL, photo.FaceCount, photo.Metadata, photo.CreatedAt) if err != nil { return nil, fmt.Errorf("store: create photo: %w", err) } return photo, nil } func (r *sqliteRepo) GetPhoto(ctx context.Context, id string) (*Photo, error) { row := r.db.QueryRowContext(ctx, `SELECT id, space_id, url, thumbnail_url, face_count, metadata, created_at FROM photos WHERE id = ?`, id) photo := &Photo{} if err := row.Scan(&photo.ID, &photo.SpaceID, &photo.URL, &photo.ThumbnailURL, &photo.FaceCount, &photo.Metadata, &photo.CreatedAt); err != nil { return nil, fmt.Errorf("store: get photo: %w", err) } return photo, nil } func (r *sqliteRepo) ListPhotosBySpace(ctx context.Context, spaceID string, limit int) ([]*Photo, error) { if limit <= 0 { limit = 100 } rows, err := r.db.QueryContext(ctx, `SELECT id, space_id, url, thumbnail_url, face_count, metadata, created_at FROM photos WHERE space_id = ? ORDER BY created_at DESC LIMIT ?`, spaceID, limit) if err != nil { return nil, fmt.Errorf("store: list photos by space: %w", err) } defer rows.Close() var photos []*Photo for rows.Next() { photo := &Photo{} if err := rows.Scan(&photo.ID, &photo.SpaceID, &photo.URL, &photo.ThumbnailURL, &photo.FaceCount, &photo.Metadata, &photo.CreatedAt); err != nil { return nil, fmt.Errorf("store: scan photo: %w", err) } photos = append(photos, photo) } return photos, rows.Err() } func (r *sqliteRepo) DeletePhoto(ctx context.Context, id string) error { _, err := r.db.ExecContext(ctx, `DELETE FROM photos WHERE id = ?`, id) if err != nil { return fmt.Errorf("store: delete photo: %w", err) } return nil } // Face embedding operations func (r *sqliteRepo) CreateFaceEmbedding(ctx context.Context, embedding *FaceEmbedding) (*FaceEmbedding, error) { embedding.ID = uuid.NewString() embedding.CreatedAt = utcNow() // Convert embedding to JSON embeddingJSON, err := json.Marshal(embedding.Embedding) if err != nil { return nil, fmt.Errorf("store: marshal face embedding: %w", err) } _, err = r.db.ExecContext(ctx, `INSERT INTO face_embeddings (id, photo_id, space_id, embedding, bounding_box, created_at) VALUES (?, ?, ?, ?, ?, ?)`, embedding.ID, embedding.PhotoID, embedding.SpaceID, embeddingJSON, embedding.BoundingBox, embedding.CreatedAt) if err != nil { return nil, fmt.Errorf("store: create face embedding: %w", err) } return embedding, nil } func (r *sqliteRepo) ListFaceEmbeddingsBySpace(ctx context.Context, spaceID string) ([]*FaceEmbedding, error) { rows, err := r.db.QueryContext(ctx, `SELECT id, photo_id, space_id, embedding, bounding_box, created_at FROM face_embeddings WHERE space_id = ?`, spaceID) if err != nil { return nil, fmt.Errorf("store: list face embeddings by space: %w", err) } defer rows.Close() var embeddings []*FaceEmbedding for rows.Next() { embedding := &FaceEmbedding{} var embeddingJSON []byte if err := rows.Scan(&embedding.ID, &embedding.PhotoID, &embedding.SpaceID, &embeddingJSON, &embedding.BoundingBox, &embedding.CreatedAt); err != nil { return nil, fmt.Errorf("store: scan face embedding: %w", err) } if err := json.Unmarshal(embeddingJSON, &embedding.Embedding); err != nil { return nil, fmt.Errorf("store: unmarshal face embedding: %w", err) } embeddings = append(embeddings, embedding) } return embeddings, rows.Err() } func (r *sqliteRepo) SearchFaceEmbeddings(ctx context.Context, spaceID string, embedding []float32, limit int) ([]*FaceEmbedding, error) { if limit <= 0 { limit = 10 } // For now, return all embeddings in the space (cosine similarity search would be implemented here) // In a real implementation, you'd compute cosine similarity between the query embedding and all embeddings embeddings, err := r.ListFaceEmbeddingsBySpace(ctx, spaceID) if err != nil { return nil, err } // Return up to limit results if len(embeddings) > limit { embeddings = embeddings[:limit] } return embeddings, nil } // Rate limiting operations func (r *sqliteRepo) CheckRateLimit(ctx context.Context, ipAddress, spaceID string, limit, windowMinutes int) (bool, error) { // Calculate window start time windowStart := time.Now().UTC().Add(-time.Duration(windowMinutes) * time.Minute).Format("2006-01-02T15:04:05.000Z07:00") var count int err := r.db.QueryRowContext(ctx, `SELECT COALESCE(SUM(request_count), 0) FROM rate_limits WHERE ip_address = ? AND space_id = ? AND window_start >= ?`, ipAddress, spaceID, windowStart).Scan(&count) if err != nil { return false, fmt.Errorf("store: check rate limit: %w", err) } return count < limit, nil } func (r *sqliteRepo) IncrementRateLimit(ctx context.Context, ipAddress, spaceID string, windowMinutes int) error { // Calculate window start time (rounded to the nearest window) now := time.Now().UTC() windowStart := now.Add(-time.Duration(now.Minute()%windowMinutes) * time.Minute).Format("2006-01-02T15:04:05.000Z07:00") // Check if there's an existing record for this IP, space, and window var id string err := r.db.QueryRowContext(ctx, `SELECT id FROM rate_limits WHERE ip_address = ? AND space_id = ? AND window_start = ?`, ipAddress, spaceID, windowStart).Scan(&id) if err == sql.ErrNoRows { // Create new record id = uuid.NewString() _, err = r.db.ExecContext(ctx, `INSERT INTO rate_limits (id, ip_address, space_id, request_count, window_start, created_at) VALUES (?, ?, ?, 1, ?, ?)`, id, ipAddress, spaceID, windowStart, utcNow()) if err != nil { return fmt.Errorf("store: create rate limit: %w", err) } } else if err == nil { // Update existing record _, err = r.db.ExecContext(ctx, `UPDATE rate_limits SET request_count = request_count + 1 WHERE id = ?`, id) if err != nil { return fmt.Errorf("store: increment rate limit: %w", err) } } else { return fmt.Errorf("store: get rate limit: %w", err) } return nil }