diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
deleted file mode 100644
index 5b0f1dd..0000000
--- a/.github/workflows/deploy.yml
+++ /dev/null
@@ -1,22 +0,0 @@
-name: Deploy
-
-on:
- workflow_run:
- workflows: ["Build and Publish Docker Image"]
- types:
- - completed
- branches:
- - main
-
-jobs:
- deploy:
- runs-on: blacksmith-2vcpu-ubuntu-2404
- if: ${{ github.event.workflow_run.conclusion == 'success' }}
- steps:
- - name: Deploy to Dokku
- uses: appleboy/ssh-action@master
- with:
- host: ${{ secrets.DOKKU_HOST }}
- username: deploy
- key: ${{ secrets.DEPLOY_KEY }}
- script: echo deploy
\ No newline at end of file
diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go
index c4df76e..992d2a7 100644
--- a/backend/cmd/server/main.go
+++ b/backend/cmd/server/main.go
@@ -5,6 +5,7 @@ import (
"net/http"
"os"
"os/signal"
+ "strings"
"syscall"
"time"
@@ -27,7 +28,11 @@ import (
func main() {
godotenv.Load("../.env", ".env")
- database, err := db.New(getEnv("DATABASE_URL", "margin.db"))
+ dsn := os.Getenv("DATABASE_URL")
+ if dsn == "" {
+ logger.Fatal("DATABASE_URL environment variable is required")
+ }
+ database, err := db.New(dsn)
if err != nil {
logger.Fatal("Failed to connect to database: %v", err)
}
@@ -70,29 +75,51 @@ func main() {
firehose.RelayURL = getEnv("BLOCK_RELAY_URL", "wss://jetstream2.us-east.bsky.network/subscribe")
logger.Info("Firehose URL: %s", firehose.RelayURL)
+ backfillCtx, backfillCancel := context.WithCancel(context.Background())
+ defer backfillCancel()
+
if recService.IsEnabled() {
ingester.SetOnAnnotation(recService.OnAnnotation)
ingester.SetOnDocument(recService.OnDocument)
- go func() {
- logger.Info("Starting recommendation backfill...")
- if err := recService.BackfillDocumentEmbeddings(200); err != nil {
- logger.Error("Document embedding backfill error: %v", err)
- }
- annCount, err := recService.BackfillAnnotationEmbeddings(200)
- if err != nil {
- logger.Error("Annotation embedding backfill error: %v", err)
- }
- hlCount, err := recService.BackfillHighlightEmbeddings(200)
- if err != nil {
- logger.Error("Highlight embedding backfill error: %v", err)
- }
- profileCount, err := recService.RebuildAllProfiles()
- if err != nil {
- logger.Error("Profile rebuild error: %v", err)
- }
- logger.Info("Recommendation backfill complete (annotations: %d, highlights: %d, profiles: %d)", annCount, hlCount, profileCount)
- }()
+ if getEnv("DISABLE_BACKFILL", "") == "" {
+ go func() {
+ time.Sleep(5 * time.Second)
+ select {
+ case <-backfillCtx.Done():
+ return
+ default:
+ }
+ logger.Info("Starting recommendation backfill...")
+ if err := recService.BackfillDocumentEmbeddings(200); err != nil {
+ logger.Error("Document embedding backfill error: %v", err)
+ }
+ if backfillCtx.Err() != nil {
+ return
+ }
+ annCount, err := recService.BackfillAnnotationEmbeddings(200)
+ if err != nil {
+ logger.Error("Annotation embedding backfill error: %v", err)
+ }
+ if backfillCtx.Err() != nil {
+ return
+ }
+ hlCount, err := recService.BackfillHighlightEmbeddings(200)
+ if err != nil {
+ logger.Error("Highlight embedding backfill error: %v", err)
+ }
+ if backfillCtx.Err() != nil {
+ return
+ }
+ profileCount, err := recService.RebuildAllProfiles()
+ if err != nil {
+ logger.Error("Profile rebuild error: %v", err)
+ }
+ logger.Info("Recommendation backfill complete (annotations: %d, highlights: %d, profiles: %d)", annCount, hlCount, profileCount)
+ }()
+ } else {
+ logger.Info("Recommendation backfill disabled (DISABLE_BACKFILL is set)")
+ }
}
go func() {
@@ -111,7 +138,17 @@ func main() {
r.Use(middleware.Throttle(100))
r.Use(cors.Handler(cors.Options{
- AllowedOrigins: []string{"https://*", "http://*", "chrome-extension://*"},
+ AllowOriginFunc: func(r *http.Request, origin string) bool {
+ if strings.HasPrefix(origin, "chrome-extension://") ||
+ strings.HasPrefix(origin, "moz-extension://") ||
+ strings.HasPrefix(origin, "safari-web-extension://") {
+ return true
+ }
+ if baseURL := os.Getenv("BASE_URL"); baseURL != "" {
+ return origin == baseURL
+ }
+ return false
+ },
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token", "X-Session-Token"},
ExposedHeaders: []string{"Link"},
@@ -174,6 +211,7 @@ func main() {
<-quit
logger.Infoln("Shutting down server...")
+ backfillCancel()
ingester.Stop()
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
diff --git a/backend/go.mod b/backend/go.mod
index ce8eeb5..69778a7 100644
--- a/backend/go.mod
+++ b/backend/go.mod
@@ -11,7 +11,6 @@ require (
github.com/ipfs/go-cid v0.6.0
github.com/joho/godotenv v1.5.1
github.com/lib/pq v1.10.9
- github.com/mattn/go-sqlite3 v1.14.22
github.com/multiformats/go-multihash v0.2.3
)
diff --git a/backend/go.sum b/backend/go.sum
index 9bdd503..84dca23 100644
--- a/backend/go.sum
+++ b/backend/go.sum
@@ -21,8 +21,6 @@ github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBF
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
-github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
-github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
diff --git a/backend/internal/api/apikey.go b/backend/internal/api/apikey.go
index af99c9b..016e65d 100644
--- a/backend/internal/api/apikey.go
+++ b/backend/internal/api/apikey.go
@@ -482,13 +482,13 @@ func (h *APIKeyHandler) authenticateAPIKey(r *http.Request) (*db.APIKey, error)
}
func (h *APIKeyHandler) getSessionByDID(did string) (*SessionData, error) {
- rows, err := h.db.Query(h.db.Rebind(`
+ rows, err := h.db.Query(`
SELECT id, did, handle, access_token, refresh_token, COALESCE(dpop_key, '')
FROM sessions
- WHERE did = ? AND expires_at > ?
+ WHERE did = $1 AND expires_at > $2
ORDER BY created_at DESC
LIMIT 1
- `), did, time.Now())
+ `, did, time.Now())
if err != nil {
return nil, err
}
diff --git a/backend/internal/api/handler.go b/backend/internal/api/handler.go
index 43455ad..1ef9580 100644
--- a/backend/internal/api/handler.go
+++ b/backend/internal/api/handler.go
@@ -10,6 +10,7 @@ import (
"sort"
"strconv"
"strings"
+ "sync"
"time"
"github.com/go-chi/chi/v5"
@@ -22,6 +23,59 @@ import (
"margin.at/internal/xrpc"
)
+type urlMetaCacheEntry struct {
+ data map[string]string
+ expiresAt time.Time
+}
+
+type urlMetaCache struct {
+ mu sync.RWMutex
+ entries map[string]urlMetaCacheEntry
+ inflight sync.Map
+}
+
+type singleflight struct {
+ wg sync.WaitGroup
+ data map[string]string
+ err error
+}
+
+func newURLMetaCache() *urlMetaCache {
+ c := &urlMetaCache{entries: make(map[string]urlMetaCacheEntry)}
+ go c.evictLoop()
+ return c
+}
+
+func (c *urlMetaCache) get(key string) (map[string]string, bool) {
+ c.mu.RLock()
+ defer c.mu.RUnlock()
+ e, ok := c.entries[key]
+ if !ok || time.Now().After(e.expiresAt) {
+ return nil, false
+ }
+ return e.data, true
+}
+
+func (c *urlMetaCache) set(key string, data map[string]string, ttl time.Duration) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ c.entries[key] = urlMetaCacheEntry{data: data, expiresAt: time.Now().Add(ttl)}
+}
+
+func (c *urlMetaCache) evictLoop() {
+ ticker := time.NewTicker(5 * time.Minute)
+ for range ticker.C {
+ c.mu.Lock()
+ now := time.Now()
+ for k, e := range c.entries {
+ if now.After(e.expiresAt) {
+ delete(c.entries, k)
+ }
+ }
+ c.mu.Unlock()
+ }
+}
+
type Handler struct {
db *db.DB
annotationService *AnnotationService
@@ -30,6 +84,8 @@ type Handler struct {
syncService *internal_sync.Service
moderation *ModerationHandler
recommendations *recommendations.Service
+ metaCache *urlMetaCache
+ metaSem chan struct{}
}
func NewHandler(database *db.DB, annotationService *AnnotationService, refresher *TokenRefresher, syncService *internal_sync.Service, recService *recommendations.Service) *Handler {
@@ -41,6 +97,8 @@ func NewHandler(database *db.DB, annotationService *AnnotationService, refresher
syncService: syncService,
moderation: NewModerationHandler(database, refresher),
recommendations: recService,
+ metaCache: newURLMetaCache(),
+ metaSem: make(chan struct{}, 5),
}
}
@@ -346,9 +404,47 @@ func (h *Handler) GetFeed(w http.ResponseWriter, r *http.Request) {
}
}
- authAnnos, _ := hydrateAnnotations(h.db, annotations, viewerDID)
- authHighs, _ := hydrateHighlights(h.db, highlights, viewerDID)
- authBooks, _ := hydrateBookmarks(h.db, bookmarks, viewerDID)
+ allDIDs := make(map[string]bool)
+ for _, a := range annotations {
+ allDIDs[a.AuthorDID] = true
+ }
+ for _, h := range highlights {
+ allDIDs[h.AuthorDID] = true
+ }
+ for _, b := range bookmarks {
+ allDIDs[b.AuthorDID] = true
+ }
+ for _, ci := range collectionItems {
+ allDIDs[ci.AuthorDID] = true
+ }
+ didSlice := make([]string, 0, len(allDIDs))
+ for did := range allDIDs {
+ didSlice = append(didSlice, did)
+ }
+ profiles := fetchProfilesForDIDs(h.db, didSlice)
+ shared := &hydrationData{profiles: profiles}
+
+ var (
+ authAnnos []APIAnnotation
+ authHighs []APIHighlight
+ authBooks []APIBookmark
+ authCollectionItems []APICollectionItem
+ wg sync.WaitGroup
+ )
+
+ wg.Add(3)
+ go func() {
+ defer wg.Done()
+ authAnnos, _ = hydrateAnnotationsWithData(h.db, annotations, viewerDID, shared)
+ }()
+ go func() {
+ defer wg.Done()
+ authHighs, _ = hydrateHighlightsWithData(h.db, highlights, viewerDID, shared)
+ }()
+ go func() {
+ defer wg.Done()
+ authBooks, _ = hydrateBookmarksWithData(h.db, bookmarks, viewerDID, shared)
+ }()
if len(collectionItems) > 0 {
var sembleURIs []string
@@ -362,9 +458,14 @@ func (h *Handler) GetFeed(w http.ResponseWriter, r *http.Request) {
defer cancel()
ensureSembleCardsIndexed(ctx, h.db, sembleURIs)
}
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ authCollectionItems, _ = hydrateCollectionItemsWithData(h.db, collectionItems, viewerDID, shared)
+ }()
}
- authCollectionItems, _ := hydrateCollectionItems(h.db, collectionItems, viewerDID)
+ wg.Wait()
collectionItemURIs := make(map[string]string)
for _, ci := range authCollectionItems {
@@ -1194,22 +1295,92 @@ func (h *Handler) GetURLMetadata(w http.ResponseWriter, r *http.Request) {
return
}
- client := &http.Client{Timeout: 10 * time.Second}
- resp, err := client.Get(targetURL)
- if err != nil {
+ if cached, ok := h.metaCache.get(targetURL); ok {
w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{"title": "", "error": "failed to fetch"})
+ w.Header().Set("X-Cache", "HIT")
+ json.NewEncoder(w).Encode(cached)
return
}
- defer resp.Body.Close()
- body, err := io.ReadAll(io.LimitReader(resp.Body, 500*1024))
- if err != nil {
+ sfVal, loaded := h.metaCache.inflight.LoadOrStore(targetURL, &singleflight{})
+ sf := sfVal.(*singleflight)
+ if loaded {
+ sf.wg.Wait()
w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(map[string]string{"title": ""})
+ w.Header().Set("X-Cache", "DEDUP")
+ if sf.data != nil {
+ json.NewEncoder(w).Encode(sf.data)
+ } else {
+ json.NewEncoder(w).Encode(map[string]string{"title": "", "error": "failed to fetch"})
+ }
return
}
+ sf.wg.Add(1)
+ defer func() {
+ sf.wg.Done()
+ go func() {
+ time.Sleep(100 * time.Millisecond)
+ h.metaCache.inflight.Delete(targetURL)
+ }()
+ }()
+
+ select {
+ case h.metaSem <- struct{}{}:
+ defer func() { <-h.metaSem }()
+ case <-r.Context().Done():
+ sf.data = map[string]string{"title": "", "error": "timeout"}
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(sf.data)
+ return
+ }
+
+ data := h.fetchURLMetadata(r.Context(), targetURL)
+ sf.data = data
+
+ ttl := 1 * time.Hour
+ if data["title"] == "" && data["error"] != "" {
+ ttl = 2 * time.Minute
+ }
+ h.metaCache.set(targetURL, data, ttl)
+
+ w.Header().Set("Content-Type", "application/json")
+ w.Header().Set("Cache-Control", "public, max-age=3600")
+ json.NewEncoder(w).Encode(data)
+}
+
+func (h *Handler) fetchURLMetadata(ctx context.Context, targetURL string) map[string]string {
+ ctx, cancel := context.WithTimeout(ctx, 4*time.Second)
+ defer cancel()
+
+ req, err := http.NewRequestWithContext(ctx, "GET", targetURL, nil)
+ if err != nil {
+ return map[string]string{"title": "", "error": "invalid url"}
+ }
+ req.Header.Set("User-Agent", "Margin/1.0 (metadata fetcher)")
+ req.Header.Set("Accept", "text/html")
+
+ client := &http.Client{
+ Timeout: 4 * time.Second,
+ CheckRedirect: func(req *http.Request, via []*http.Request) error {
+ if len(via) >= 3 {
+ return fmt.Errorf("too many redirects")
+ }
+ return nil
+ },
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return map[string]string{"title": "", "error": "failed to fetch"}
+ }
+ defer resp.Body.Close()
+
+ body, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024))
+ if err != nil {
+ return map[string]string{"title": ""}
+ }
+
content := string(body)
extract := func(key string) string {
@@ -1310,15 +1481,12 @@ func (h *Handler) GetURLMetadata(w http.ResponseWriter, r *http.Request) {
}
}
- data := map[string]string{
+ return map[string]string{
"title": title,
"description": description,
"image": image,
"icon": favicon,
}
-
- w.Header().Set("Content-Type", "application/json")
- json.NewEncoder(w).Encode(data)
}
func (h *Handler) GetNotifications(w http.ResponseWriter, r *http.Request) {
diff --git a/backend/internal/api/hydration.go b/backend/internal/api/hydration.go
index 53697e9..21a7516 100644
--- a/backend/internal/api/hydration.go
+++ b/backend/internal/api/hydration.go
@@ -19,6 +19,8 @@ import (
var (
Cache ProfileCache = NewInMemoryCache(5 * time.Minute)
ConstellationClient *constellation.Client = constellation.NewClient() // Enabled by default
+
+ bskyHTTPClient = &http.Client{Timeout: 5 * time.Second}
)
func init() {
@@ -168,6 +170,11 @@ type APINotification struct {
ReadAt *time.Time `json:"readAt,omitempty"`
}
+type hydrationData struct {
+ profiles map[string]Author
+ subscribedLabelers []string
+}
+
func fetchCounts(ctx context.Context, database *db.DB, uris []string, viewerDID string) (likeCounts, replyCounts map[string]int, viewerLikes map[string]bool) {
likeCounts = make(map[string]int)
replyCounts = make(map[string]int)
@@ -177,12 +184,39 @@ func fetchCounts(ctx context.Context, database *db.DB, uris []string, viewerDID
return
}
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+
if database != nil {
- likeCounts, _ = database.GetLikeCounts(uris)
- replyCounts, _ = database.GetReplyCounts(uris)
+ wg.Add(2)
+ go func() {
+ defer wg.Done()
+ if lc, err := database.GetLikeCounts(uris); err == nil {
+ mu.Lock()
+ likeCounts = lc
+ mu.Unlock()
+ }
+ }()
+ go func() {
+ defer wg.Done()
+ if rc, err := database.GetReplyCounts(uris); err == nil {
+ mu.Lock()
+ replyCounts = rc
+ mu.Unlock()
+ }
+ }()
if viewerDID != "" {
- viewerLikes, _ = database.GetViewerLikes(viewerDID, uris)
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if vl, err := database.GetViewerLikes(viewerDID, uris); err == nil {
+ mu.Lock()
+ viewerLikes = vl
+ mu.Unlock()
+ }
+ }()
}
+ wg.Wait()
}
if ConstellationClient != nil && len(uris) <= 5 {
@@ -205,28 +239,99 @@ func fetchCounts(ctx context.Context, database *db.DB, uris []string, viewerDID
return
}
+func fetchEngagementData(database *db.DB, uris []string, authorDIDs []string, viewerDID string) (
+ likeCounts, replyCounts map[string]int,
+ viewerLikes map[string]bool,
+ uriLabels, didLabels map[string][]db.ContentLabel,
+ editTimes map[string]time.Time,
+) {
+ likeCounts = make(map[string]int)
+ replyCounts = make(map[string]int)
+ viewerLikes = make(map[string]bool)
+ uriLabels = make(map[string][]db.ContentLabel)
+ didLabels = make(map[string][]db.ContentLabel)
+ editTimes = make(map[string]time.Time)
+
+ if len(uris) == 0 {
+ return
+ }
+
+ subscribedLabelers := getSubscribedLabelers(database, viewerDID)
+ labelerDIDs := appendUnique(subscribedLabelers, authorDIDs)
+
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
+ defer cancel()
+ lc, rc, vl := fetchCounts(ctx, database, uris, viewerDID)
+ mu.Lock()
+ likeCounts = lc
+ replyCounts = rc
+ viewerLikes = vl
+ mu.Unlock()
+ }()
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if ul, err := database.GetContentLabelsForURIs(uris, labelerDIDs); err == nil {
+ mu.Lock()
+ uriLabels = ul
+ mu.Unlock()
+ }
+ }()
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if dl, err := database.GetContentLabelsForDIDs(authorDIDs, labelerDIDs); err == nil {
+ mu.Lock()
+ didLabels = dl
+ mu.Unlock()
+ }
+ }()
+
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ if et, err := database.GetLatestEditTimes(uris); err == nil {
+ mu.Lock()
+ editTimes = et
+ mu.Unlock()
+ }
+ }()
+
+ wg.Wait()
+ return
+}
+
func hydrateAnnotations(database *db.DB, annotations []db.Annotation, viewerDID string) ([]APIAnnotation, error) {
+ return hydrateAnnotationsWithData(database, annotations, viewerDID, nil)
+}
+
+func hydrateAnnotationsWithData(database *db.DB, annotations []db.Annotation, viewerDID string, shared *hydrationData) ([]APIAnnotation, error) {
if len(annotations) == 0 {
return []APIAnnotation{}, nil
}
- profiles := fetchProfilesForDIDs(database, collectDIDs(annotations, func(a db.Annotation) string { return a.AuthorDID }))
+ var profiles map[string]Author
+ if shared != nil && shared.profiles != nil {
+ profiles = shared.profiles
+ } else {
+ profiles = fetchProfilesForDIDs(database, collectDIDs(annotations, func(a db.Annotation) string { return a.AuthorDID }))
+ }
uris := make([]string, len(annotations))
for i, a := range annotations {
uris[i] = a.URI
}
-
- ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
- defer cancel()
- likeCounts, replyCounts, viewerLikes := fetchCounts(ctx, database, uris, viewerDID)
-
- subscribedLabelers := getSubscribedLabelers(database, viewerDID)
authorDIDs := collectDIDs(annotations, func(a db.Annotation) string { return a.AuthorDID })
- labelerDIDs := appendUnique(subscribedLabelers, authorDIDs)
- uriLabels, _ := database.GetContentLabelsForURIs(uris, labelerDIDs)
- didLabels, _ := database.GetContentLabelsForDIDs(authorDIDs, labelerDIDs)
- editTimes, _ := database.GetLatestEditTimes(uris)
+
+ likeCounts, replyCounts, viewerLikes, uriLabels, didLabels, editTimes := fetchEngagementData(database, uris, authorDIDs, viewerDID)
result := make([]APIAnnotation, len(annotations))
for i, a := range annotations {
@@ -303,27 +408,28 @@ func hydrateAnnotations(database *db.DB, annotations []db.Annotation, viewerDID
}
func hydrateHighlights(database *db.DB, highlights []db.Highlight, viewerDID string) ([]APIHighlight, error) {
+ return hydrateHighlightsWithData(database, highlights, viewerDID, nil)
+}
+
+func hydrateHighlightsWithData(database *db.DB, highlights []db.Highlight, viewerDID string, shared *hydrationData) ([]APIHighlight, error) {
if len(highlights) == 0 {
return []APIHighlight{}, nil
}
- profiles := fetchProfilesForDIDs(database, collectDIDs(highlights, func(h db.Highlight) string { return h.AuthorDID }))
+ var profiles map[string]Author
+ if shared != nil && shared.profiles != nil {
+ profiles = shared.profiles
+ } else {
+ profiles = fetchProfilesForDIDs(database, collectDIDs(highlights, func(h db.Highlight) string { return h.AuthorDID }))
+ }
uris := make([]string, len(highlights))
for i, h := range highlights {
uris[i] = h.URI
}
-
- ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
- defer cancel()
- likeCounts, replyCounts, viewerLikes := fetchCounts(ctx, database, uris, viewerDID)
-
- subscribedLabelers := getSubscribedLabelers(database, viewerDID)
authorDIDs := collectDIDs(highlights, func(h db.Highlight) string { return h.AuthorDID })
- labelerDIDs := appendUnique(subscribedLabelers, authorDIDs)
- uriLabels, _ := database.GetContentLabelsForURIs(uris, labelerDIDs)
- didLabels, _ := database.GetContentLabelsForDIDs(authorDIDs, labelerDIDs)
- editTimes, _ := database.GetLatestEditTimes(uris)
+
+ likeCounts, replyCounts, viewerLikes, uriLabels, didLabels, editTimes := fetchEngagementData(database, uris, authorDIDs, viewerDID)
result := make([]APIHighlight, len(highlights))
for i, h := range highlights {
@@ -385,27 +491,28 @@ func hydrateHighlights(database *db.DB, highlights []db.Highlight, viewerDID str
}
func hydrateBookmarks(database *db.DB, bookmarks []db.Bookmark, viewerDID string) ([]APIBookmark, error) {
+ return hydrateBookmarksWithData(database, bookmarks, viewerDID, nil)
+}
+
+func hydrateBookmarksWithData(database *db.DB, bookmarks []db.Bookmark, viewerDID string, shared *hydrationData) ([]APIBookmark, error) {
if len(bookmarks) == 0 {
return []APIBookmark{}, nil
}
- profiles := fetchProfilesForDIDs(database, collectDIDs(bookmarks, func(b db.Bookmark) string { return b.AuthorDID }))
+ var profiles map[string]Author
+ if shared != nil && shared.profiles != nil {
+ profiles = shared.profiles
+ } else {
+ profiles = fetchProfilesForDIDs(database, collectDIDs(bookmarks, func(b db.Bookmark) string { return b.AuthorDID }))
+ }
uris := make([]string, len(bookmarks))
for i, b := range bookmarks {
uris[i] = b.URI
}
-
- ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
- defer cancel()
- likeCounts, replyCounts, viewerLikes := fetchCounts(ctx, database, uris, viewerDID)
-
- subscribedLabelers := getSubscribedLabelers(database, viewerDID)
authorDIDs := collectDIDs(bookmarks, func(b db.Bookmark) string { return b.AuthorDID })
- labelerDIDs := appendUnique(subscribedLabelers, authorDIDs)
- uriLabels, _ := database.GetContentLabelsForURIs(uris, labelerDIDs)
- didLabels, _ := database.GetContentLabelsForDIDs(authorDIDs, labelerDIDs)
- editTimes, _ := database.GetLatestEditTimes(uris)
+
+ likeCounts, replyCounts, viewerLikes, uriLabels, didLabels, editTimes := fetchEngagementData(database, uris, authorDIDs, viewerDID)
result := make([]APIBookmark, len(bookmarks))
for i, b := range bookmarks {
@@ -504,8 +611,11 @@ func collectDIDs[T any](items []T, getDID func(T) string) []string {
func fetchProfilesForDIDs(database *db.DB, dids []string) map[string]Author {
profiles := make(map[string]Author)
- missingDIDs := make([]string, 0)
+ if len(dids) == 0 {
+ return profiles
+ }
+ missingDIDs := make([]string, 0)
for _, did := range dids {
if author, ok := Cache.Get(did); ok {
profiles[did] = author
@@ -514,6 +624,35 @@ func fetchProfilesForDIDs(database *db.DB, dids []string) map[string]Author {
}
}
+ if len(missingDIDs) == 0 {
+ return profiles
+ }
+
+ if database != nil {
+ marginProfiles, err := database.GetProfilesByDIDs(missingDIDs)
+ if err == nil {
+ for did, mp := range marginProfiles {
+ author := Author{DID: did}
+ if mp.DisplayName != nil && *mp.DisplayName != "" {
+ author.DisplayName = *mp.DisplayName
+ }
+ if mp.Avatar != nil && *mp.Avatar != "" {
+ author.Avatar = getProxiedAvatarURL(did, *mp.Avatar)
+ }
+ profiles[did] = author
+ Cache.Set(did, author)
+ }
+ }
+
+ stillMissing := make([]string, 0)
+ for _, did := range missingDIDs {
+ if _, ok := profiles[did]; !ok {
+ stillMissing = append(stillMissing, did)
+ }
+ }
+ missingDIDs = stillMissing
+ }
+
if len(missingDIDs) > 0 {
batchSize := 25
var wg sync.WaitGroup
@@ -532,10 +671,11 @@ func fetchProfilesForDIDs(database *db.DB, dids []string) map[string]Author {
fetched, err := fetchProfiles(actors)
if err == nil {
mu.Lock()
- defer mu.Unlock()
for k, v := range fetched {
profiles[k] = v
+ Cache.Set(k, v)
}
+ mu.Unlock()
}
}(batch)
}
@@ -548,11 +688,8 @@ func fetchProfilesForDIDs(database *db.DB, dids []string) map[string]Author {
for did, mp := range marginProfiles {
author, exists := profiles[did]
if !exists {
- author = Author{
- DID: did,
- }
+ author = Author{DID: did}
}
-
if mp.DisplayName != nil && *mp.DisplayName != "" {
author.DisplayName = *mp.DisplayName
}
@@ -560,7 +697,6 @@ func fetchProfilesForDIDs(database *db.DB, dids []string) map[string]Author {
author.Avatar = getProxiedAvatarURL(did, *mp.Avatar)
}
profiles[did] = author
-
Cache.Set(did, author)
}
}
@@ -579,7 +715,7 @@ func fetchProfiles(dids []string) (map[string]Author, error) {
q.Add("actors", did)
}
- resp, err := http.Get(config.Get().BskyGetProfilesURL() + "?" + q.Encode())
+ resp, err := bskyHTTPClient.Get(config.Get().BskyGetProfilesURL() + "?" + q.Encode())
if err != nil {
logger.Error("Hydration fetch error: %v", err)
return nil, err
@@ -618,11 +754,20 @@ func fetchProfiles(dids []string) (map[string]Author, error) {
}
func hydrateCollectionItems(database *db.DB, items []db.CollectionItem, viewerDID string) ([]APICollectionItem, error) {
+ return hydrateCollectionItemsWithData(database, items, viewerDID, nil)
+}
+
+func hydrateCollectionItemsWithData(database *db.DB, items []db.CollectionItem, viewerDID string, shared *hydrationData) ([]APICollectionItem, error) {
if len(items) == 0 {
return []APICollectionItem{}, nil
}
- profiles := fetchProfilesForDIDs(database, collectDIDs(items, func(i db.CollectionItem) string { return i.AuthorDID }))
+ var profiles map[string]Author
+ if shared != nil && shared.profiles != nil {
+ profiles = shared.profiles
+ } else {
+ profiles = fetchProfilesForDIDs(database, collectDIDs(items, func(i db.CollectionItem) string { return i.AuthorDID }))
+ }
var collectionURIs []string
var annotationURIs []string
@@ -647,7 +792,6 @@ func hydrateCollectionItems(database *db.DB, items []db.CollectionItem, viewerDI
if len(collectionURIs) > 0 {
colls, err := database.GetCollectionsByURIs(collectionURIs)
if err == nil {
- collProfiles := fetchProfilesForDIDs(database, collectDIDs(colls, func(c db.Collection) string { return c.AuthorDID }))
for _, coll := range colls {
icon := ""
if coll.Icon != nil {
@@ -662,7 +806,7 @@ func hydrateCollectionItems(database *db.DB, items []db.CollectionItem, viewerDI
Name: coll.Name,
Description: desc,
Icon: icon,
- Creator: collProfiles[coll.AuthorDID],
+ Creator: profiles[coll.AuthorDID],
CreatedAt: coll.CreatedAt,
IndexedAt: coll.IndexedAt,
}
@@ -670,39 +814,66 @@ func hydrateCollectionItems(database *db.DB, items []db.CollectionItem, viewerDI
}
}
- annotationsMap := make(map[string]APIAnnotation)
+ var (
+ annotationsMap = make(map[string]APIAnnotation)
+ highlightsMap = make(map[string]APIHighlight)
+ bookmarksMap = make(map[string]APIBookmark)
+ wg sync.WaitGroup
+ mu sync.Mutex
+ )
+
+ nestedShared := &hydrationData{profiles: profiles}
+
if len(annotationURIs) > 0 {
- rawAnnos, err := database.GetAnnotationsByURIs(annotationURIs)
- if err == nil {
- hydrated, _ := hydrateAnnotations(database, rawAnnos, viewerDID)
- for _, a := range hydrated {
- annotationsMap[a.ID] = a
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ rawAnnos, err := database.GetAnnotationsByURIs(annotationURIs)
+ if err == nil {
+ hydrated, _ := hydrateAnnotationsWithData(database, rawAnnos, viewerDID, nestedShared)
+ mu.Lock()
+ for _, a := range hydrated {
+ annotationsMap[a.ID] = a
+ }
+ mu.Unlock()
}
- }
+ }()
}
- highlightsMap := make(map[string]APIHighlight)
if len(highlightURIs) > 0 {
- rawHighlights, err := database.GetHighlightsByURIs(highlightURIs)
- if err == nil {
- hydrated, _ := hydrateHighlights(database, rawHighlights, viewerDID)
- for _, h := range hydrated {
- highlightsMap[h.ID] = h
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ rawHighlights, err := database.GetHighlightsByURIs(highlightURIs)
+ if err == nil {
+ hydrated, _ := hydrateHighlightsWithData(database, rawHighlights, viewerDID, nestedShared)
+ mu.Lock()
+ for _, h := range hydrated {
+ highlightsMap[h.ID] = h
+ }
+ mu.Unlock()
}
- }
+ }()
}
- bookmarksMap := make(map[string]APIBookmark)
if len(bookmarkURIs) > 0 {
- rawBookmarks, err := database.GetBookmarksByURIs(bookmarkURIs)
- if err == nil {
- hydrated, _ := hydrateBookmarks(database, rawBookmarks, viewerDID)
- for _, b := range hydrated {
- bookmarksMap[b.ID] = b
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ rawBookmarks, err := database.GetBookmarksByURIs(bookmarkURIs)
+ if err == nil {
+ hydrated, _ := hydrateBookmarksWithData(database, rawBookmarks, viewerDID, nestedShared)
+ mu.Lock()
+ for _, b := range hydrated {
+ bookmarksMap[b.ID] = b
+ }
+ mu.Unlock()
}
- }
+ }()
}
+ wg.Wait()
+
var result []APICollectionItem
for _, item := range items {
apiItem := APICollectionItem{
diff --git a/backend/internal/api/pds.go b/backend/internal/api/pds.go
index abe5d90..0601bb5 100644
--- a/backend/internal/api/pds.go
+++ b/backend/internal/api/pds.go
@@ -11,6 +11,8 @@ import (
"margin.at/internal/xrpc"
)
+var pdsClient = &http.Client{Timeout: 10 * time.Second}
+
func (h *Handler) FetchLatestUserRecords(r *http.Request, did string, collection string, limit int) ([]interface{}, error) {
session, err := h.refresher.GetSessionWithAutoRefresh(r)
if err != nil {
@@ -25,7 +27,7 @@ func (h *Handler) FetchLatestUserRecords(r *http.Request, did string, collection
req, _ := http.NewRequestWithContext(r.Context(), "GET", url, nil)
req.Header.Set("Authorization", "Bearer "+client.AccessToken)
- resp, err := http.DefaultClient.Do(req)
+ resp, err := pdsClient.Do(req)
if err != nil {
return fmt.Errorf("failed to fetch %s: %w", collection, err)
}
diff --git a/backend/internal/db/db.go b/backend/internal/db/db.go
index eb5b6a7..c3a1635 100644
--- a/backend/internal/db/db.go
+++ b/backend/internal/db/db.go
@@ -8,12 +8,10 @@ import (
"time"
_ "github.com/lib/pq"
- _ "github.com/mattn/go-sqlite3"
)
type DB struct {
*sql.DB
- driver string
}
type Annotation struct {
@@ -204,48 +202,28 @@ type ContentLabel struct {
}
func New(dsn string) (*DB, error) {
- driver := "sqlite3"
- if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") {
- driver = "postgres"
+ if !strings.HasPrefix(dsn, "postgres://") && !strings.HasPrefix(dsn, "postgresql://") {
+ return nil, fmt.Errorf("only PostgreSQL is supported, DSN must start with postgres:// or postgresql://")
}
- db, err := sql.Open(driver, dsn)
+ db, err := sql.Open("postgres", dsn)
if err != nil {
return nil, fmt.Errorf("failed to open database connection: %w", err)
}
- if driver == "sqlite3" {
- if _, err := db.Exec("PRAGMA journal_mode=WAL;"); err != nil {
- return nil, fmt.Errorf("failed to set WAL mode: %w", err)
- }
- db.Exec("PRAGMA synchronous=NORMAL;")
- db.Exec("PRAGMA busy_timeout=5000;")
- db.Exec("PRAGMA cache_size=-2000;")
- db.Exec("PRAGMA foreign_keys=ON;")
-
- db.SetMaxOpenConns(25)
- db.SetMaxIdleConns(25)
- db.SetConnMaxLifetime(5 * time.Minute)
- } else {
- db.SetMaxOpenConns(50)
- db.SetMaxIdleConns(25)
- db.SetConnMaxLifetime(10 * time.Minute)
- }
+ db.SetMaxOpenConns(25)
+ db.SetMaxIdleConns(10)
+ db.SetConnMaxLifetime(5 * time.Minute)
+ db.SetConnMaxIdleTime(2 * time.Minute)
if err := db.Ping(); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
- return &DB{DB: db, driver: driver}, nil
+ return &DB{DB: db}, nil
}
func (db *DB) Migrate() error {
-
- dateType := "DATETIME"
- if db.driver == "postgres" {
- dateType = "TIMESTAMP"
- }
-
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS annotations (
uri TEXT PRIMARY KEY,
@@ -259,8 +237,8 @@ func (db *DB) Migrate() error {
target_title TEXT,
selector_json TEXT,
tags_json TEXT,
- created_at ` + dateType + ` NOT NULL,
- indexed_at ` + dateType + ` NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ indexed_at TIMESTAMP NOT NULL,
cid TEXT
)`)
if err != nil {
@@ -272,6 +250,8 @@ func (db *DB) Migrate() error {
db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_author_did ON annotations(author_did)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_motivation ON annotations(motivation)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_created_at ON annotations(created_at DESC)`)
+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_author_created ON annotations(author_did, created_at DESC)`)
+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_uri_pattern ON annotations(uri text_pattern_ops)`)
db.Exec(`CREATE TABLE IF NOT EXISTS highlights (
uri TEXT PRIMARY KEY,
@@ -282,13 +262,15 @@ func (db *DB) Migrate() error {
selector_json TEXT,
color TEXT,
tags_json TEXT,
- created_at ` + dateType + ` NOT NULL,
- indexed_at ` + dateType + ` NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ indexed_at TIMESTAMP NOT NULL,
cid TEXT
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_target_hash ON highlights(target_hash)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_author_did ON highlights(author_did)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_created_at ON highlights(created_at DESC)`)
+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_author_created ON highlights(author_did, created_at DESC)`)
+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_uri_pattern ON highlights(uri text_pattern_ops)`)
db.Exec(`CREATE TABLE IF NOT EXISTS bookmarks (
uri TEXT PRIMARY KEY,
@@ -298,13 +280,15 @@ func (db *DB) Migrate() error {
title TEXT,
description TEXT,
tags_json TEXT,
- created_at ` + dateType + ` NOT NULL,
- indexed_at ` + dateType + ` NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ indexed_at TIMESTAMP NOT NULL,
cid TEXT
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_source_hash ON bookmarks(source_hash)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_author_did ON bookmarks(author_did)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_created_at ON bookmarks(created_at DESC)`)
+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_author_created ON bookmarks(author_did, created_at DESC)`)
+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_uri_pattern ON bookmarks(uri text_pattern_ops)`)
db.Exec(`CREATE TABLE IF NOT EXISTS replies (
uri TEXT PRIMARY KEY,
@@ -313,20 +297,21 @@ func (db *DB) Migrate() error {
root_uri TEXT NOT NULL,
text TEXT NOT NULL,
format TEXT DEFAULT 'text/plain',
- created_at ` + dateType + ` NOT NULL,
- indexed_at ` + dateType + ` NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ indexed_at TIMESTAMP NOT NULL,
cid TEXT
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_parent_uri ON replies(parent_uri)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_root_uri ON replies(root_uri)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_created_at ON replies(created_at DESC)`)
+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_author_did ON replies(author_did)`)
db.Exec(`CREATE TABLE IF NOT EXISTS likes (
uri TEXT PRIMARY KEY,
author_did TEXT NOT NULL,
subject_uri TEXT NOT NULL,
- created_at ` + dateType + ` NOT NULL,
- indexed_at ` + dateType + ` NOT NULL
+ created_at TIMESTAMP NOT NULL,
+ indexed_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_subject_uri ON likes(subject_uri)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_did ON likes(author_did)`)
@@ -338,8 +323,8 @@ func (db *DB) Migrate() error {
name TEXT NOT NULL,
description TEXT,
icon TEXT,
- created_at ` + dateType + ` NOT NULL,
- indexed_at ` + dateType + ` NOT NULL
+ created_at TIMESTAMP NOT NULL,
+ indexed_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_collections_author_did ON collections(author_did)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_collections_created_at ON collections(created_at DESC)`)
@@ -350,8 +335,8 @@ func (db *DB) Migrate() error {
collection_uri TEXT NOT NULL,
annotation_uri TEXT NOT NULL,
position INTEGER DEFAULT 0,
- created_at ` + dateType + ` NOT NULL,
- indexed_at ` + dateType + ` NOT NULL
+ created_at TIMESTAMP NOT NULL,
+ indexed_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_collection_items_collection ON collection_items(collection_uri)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_collection_items_annotation ON collection_items(annotation_uri)`)
@@ -364,49 +349,46 @@ func (db *DB) Migrate() error {
access_token TEXT NOT NULL,
refresh_token TEXT NOT NULL,
dpop_key TEXT,
- created_at ` + dateType + ` NOT NULL,
- expires_at ` + dateType + ` NOT NULL
+ created_at TIMESTAMP NOT NULL,
+ expires_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_sessions_did ON sessions(did)`)
-
- autoInc := "INTEGER PRIMARY KEY AUTOINCREMENT"
- if db.driver == "postgres" {
- autoInc = "SERIAL PRIMARY KEY"
- }
+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)`)
db.Exec(`CREATE TABLE IF NOT EXISTS edit_history (
- id ` + autoInc + `,
+ id SERIAL PRIMARY KEY,
uri TEXT NOT NULL,
record_type TEXT NOT NULL,
previous_content TEXT NOT NULL,
previous_cid TEXT,
- edited_at ` + dateType + ` NOT NULL
+ edited_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_edit_history_uri ON edit_history(uri)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_edit_history_edited_at ON edit_history(edited_at DESC)`)
db.Exec(`CREATE TABLE IF NOT EXISTS notifications (
- id ` + autoInc + `,
+ id SERIAL PRIMARY KEY,
recipient_did TEXT NOT NULL,
actor_did TEXT NOT NULL,
type TEXT NOT NULL,
subject_uri TEXT NOT NULL,
- created_at ` + dateType + ` NOT NULL,
- read_at ` + dateType + `
+ created_at TIMESTAMP NOT NULL,
+ read_at TIMESTAMP
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_did)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at DESC)`)
+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(recipient_did) WHERE read_at IS NULL`)
db.Exec(`CREATE TABLE IF NOT EXISTS api_keys (
id TEXT PRIMARY KEY,
owner_did TEXT NOT NULL,
name TEXT NOT NULL,
key_hash TEXT NOT NULL,
- created_at ` + dateType + ` NOT NULL,
- last_used_at ` + dateType + `,
+ created_at TIMESTAMP NOT NULL,
+ last_used_at TIMESTAMP,
uri TEXT,
cid TEXT,
- indexed_at ` + dateType + ` DEFAULT CURRENT_TIMESTAMP
+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_owner ON api_keys(owner_did)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)`)
@@ -419,8 +401,8 @@ func (db *DB) Migrate() error {
bio TEXT,
website TEXT,
links_json TEXT,
- created_at ` + dateType + ` NOT NULL,
- indexed_at ` + dateType + ` NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ indexed_at TIMESTAMP NOT NULL,
cid TEXT
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_profiles_author_did ON profiles(author_did)`)
@@ -432,8 +414,8 @@ func (db *DB) Migrate() error {
subscribed_labelers TEXT,
label_preferences TEXT,
disable_external_link_warning BOOLEAN,
- created_at ` + dateType + ` NOT NULL,
- indexed_at ` + dateType + ` NOT NULL,
+ created_at TIMESTAMP NOT NULL,
+ indexed_at TIMESTAMP NOT NULL,
cid TEXT
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_preferences_author_did ON preferences(author_did)`)
@@ -443,39 +425,39 @@ func (db *DB) Migrate() error {
db.Exec(`CREATE TABLE IF NOT EXISTS cursors (
id TEXT PRIMARY KEY,
last_cursor BIGINT NOT NULL,
- updated_at ` + dateType + ` NOT NULL
+ updated_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE TABLE IF NOT EXISTS blocks (
- id ` + autoInc + `,
+ id SERIAL PRIMARY KEY,
actor_did TEXT NOT NULL,
subject_did TEXT NOT NULL,
- created_at ` + dateType + ` NOT NULL,
+ created_at TIMESTAMP NOT NULL,
UNIQUE(actor_did, subject_did)
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_blocks_actor ON blocks(actor_did)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_blocks_subject ON blocks(subject_did)`)
db.Exec(`CREATE TABLE IF NOT EXISTS mutes (
- id ` + autoInc + `,
+ id SERIAL PRIMARY KEY,
actor_did TEXT NOT NULL,
subject_did TEXT NOT NULL,
- created_at ` + dateType + ` NOT NULL,
+ created_at TIMESTAMP NOT NULL,
UNIQUE(actor_did, subject_did)
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_mutes_actor ON mutes(actor_did)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_mutes_subject ON mutes(subject_did)`)
db.Exec(`CREATE TABLE IF NOT EXISTS moderation_reports (
- id ` + autoInc + `,
+ id SERIAL PRIMARY KEY,
reporter_did TEXT NOT NULL,
subject_did TEXT NOT NULL,
subject_uri TEXT,
reason_type TEXT NOT NULL,
reason_text TEXT,
status TEXT NOT NULL DEFAULT 'pending',
- created_at ` + dateType + ` NOT NULL,
- resolved_at ` + dateType + `,
+ created_at TIMESTAMP NOT NULL,
+ resolved_at TIMESTAMP,
resolved_by TEXT
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_status ON moderation_reports(status)`)
@@ -483,23 +465,23 @@ func (db *DB) Migrate() error {
db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_reporter ON moderation_reports(reporter_did)`)
db.Exec(`CREATE TABLE IF NOT EXISTS moderation_actions (
- id ` + autoInc + `,
+ id SERIAL PRIMARY KEY,
report_id INTEGER NOT NULL,
actor_did TEXT NOT NULL,
action TEXT NOT NULL,
comment TEXT,
- created_at ` + dateType + ` NOT NULL
+ created_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_actions_report ON moderation_actions(report_id)`)
db.Exec(`CREATE TABLE IF NOT EXISTS content_labels (
- id ` + autoInc + `,
+ id SERIAL PRIMARY KEY,
src TEXT NOT NULL,
uri TEXT NOT NULL,
val TEXT NOT NULL,
neg INTEGER NOT NULL DEFAULT 0,
created_by TEXT NOT NULL,
- created_at ` + dateType + ` NOT NULL
+ created_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_uri ON content_labels(uri)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_src ON content_labels(src)`)
@@ -511,7 +493,7 @@ func (db *DB) Migrate() error {
name TEXT NOT NULL,
description TEXT,
show_in_discover BOOLEAN NOT NULL DEFAULT true,
- indexed_at ` + dateType + ` NOT NULL
+ indexed_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_publications_author ON publications(author_did)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_publications_url ON publications(url)`)
@@ -526,8 +508,8 @@ func (db *DB) Migrate() error {
text_content TEXT,
tags_json TEXT,
canonical_url TEXT,
- published_at ` + dateType + ` NOT NULL,
- indexed_at ` + dateType + ` NOT NULL
+ published_at TIMESTAMP NOT NULL,
+ indexed_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_documents_author ON documents(author_did)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_documents_site ON documents(site)`)
@@ -544,26 +526,14 @@ func (db *DB) GetProfilesByDIDs(dids []string) (map[string]*Profile, error) {
return nil, nil
}
- query := `SELECT uri, author_did, display_name, bio, avatar, website, links_json, created_at, indexed_at FROM profiles WHERE author_did IN (`
- args := make([]interface{}, len(dids))
placeholders := make([]string, len(dids))
-
+ args := make([]interface{}, len(dids))
for i, did := range dids {
placeholders[i] = fmt.Sprintf("$%d", i+1)
args[i] = did
}
- query += strings.Join(placeholders, ",") + ")"
-
- if db.driver == "sqlite3" {
- query = strings.ReplaceAll(query, "$", "?")
-
- placeholders = make([]string, len(dids))
- for i := range dids {
- placeholders[i] = "?"
- }
- query = `SELECT uri, author_did, display_name, bio, avatar, website, links_json, created_at, indexed_at FROM profiles WHERE author_did IN (` + strings.Join(placeholders, ",") + ")"
- }
+ query := `SELECT uri, author_did, display_name, bio, avatar, website, links_json, created_at, indexed_at FROM profiles WHERE author_did IN (` + strings.Join(placeholders, ",") + ")"
rows, err := db.Query(query, args...)
if err != nil {
@@ -597,10 +567,10 @@ func (db *DB) GetCursor(id string) (int64, error) {
func (db *DB) SetCursor(id string, cursor int64) error {
query := `
- INSERT INTO cursors (id, last_cursor, updated_at)
- VALUES ($1, $2, $3)
- ON CONFLICT(id) DO UPDATE SET
- last_cursor = EXCLUDED.last_cursor,
+ INSERT INTO cursors (id, last_cursor, updated_at)
+ VALUES ($1, $2, $3)
+ ON CONFLICT(id) DO UPDATE SET
+ last_cursor = EXCLUDED.last_cursor,
updated_at = EXCLUDED.updated_at
`
_, err := db.Exec(query, id, cursor, time.Now())
@@ -623,17 +593,17 @@ func (db *DB) GetProfile(did string) (*Profile, error) {
func (db *DB) UpsertProfile(p *Profile) error {
query := `
- INSERT INTO profiles (uri, author_did, display_name, avatar, bio, website, links_json, created_at, indexed_at)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
- ON CONFLICT(uri) DO UPDATE SET
+ INSERT INTO profiles (uri, author_did, display_name, avatar, bio, website, links_json, created_at, indexed_at)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
+ ON CONFLICT(uri) DO UPDATE SET
display_name = EXCLUDED.display_name,
avatar = EXCLUDED.avatar,
- bio = EXCLUDED.bio,
+ bio = EXCLUDED.bio,
website = EXCLUDED.website,
links_json = EXCLUDED.links_json,
indexed_at = EXCLUDED.indexed_at
`
- _, err := db.Exec(db.Rebind(query), p.URI, p.AuthorDID, p.DisplayName, p.Avatar, p.Bio, p.Website, p.LinksJSON, p.CreatedAt, p.IndexedAt)
+ _, err := db.Exec(query, p.URI, p.AuthorDID, p.DisplayName, p.Avatar, p.Bio, p.Website, p.LinksJSON, p.CreatedAt, p.IndexedAt)
return err
}
@@ -672,9 +642,9 @@ func (db *DB) GetPreferences(did string) (*Preferences, error) {
func (db *DB) UpsertPreferences(p *Preferences) error {
query := `
- INSERT INTO preferences (uri, author_did, external_link_skipped_hostnames, subscribed_labelers, label_preferences, disable_external_link_warning, created_at, indexed_at, cid)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
- ON CONFLICT(uri) DO UPDATE SET
+ INSERT INTO preferences (uri, author_did, external_link_skipped_hostnames, subscribed_labelers, label_preferences, disable_external_link_warning, created_at, indexed_at, cid)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
+ ON CONFLICT(uri) DO UPDATE SET
external_link_skipped_hostnames = EXCLUDED.external_link_skipped_hostnames,
subscribed_labelers = EXCLUDED.subscribed_labelers,
label_preferences = EXCLUDED.label_preferences,
@@ -682,7 +652,7 @@ func (db *DB) UpsertPreferences(p *Preferences) error {
indexed_at = EXCLUDED.indexed_at,
cid = EXCLUDED.cid
`
- _, err := db.Exec(db.Rebind(query), p.URI, p.AuthorDID, p.ExternalLinkSkippedHostnames, p.SubscribedLabelers, p.LabelPreferences, p.DisableExternalLinkWarning, p.CreatedAt, p.IndexedAt, p.CID)
+ _, err := db.Exec(query, p.URI, p.AuthorDID, p.ExternalLinkSkippedHostnames, p.SubscribedLabelers, p.LabelPreferences, p.DisableExternalLinkWarning, p.CreatedAt, p.IndexedAt, p.CID)
return err
}
@@ -697,7 +667,7 @@ func (db *DB) DeletePreferences(uri string) error {
}
func (db *DB) GetAPIKeyURIs(ownerDID string) ([]string, error) {
- rows, err := db.Query(db.Rebind("SELECT uri FROM api_keys WHERE owner_did = ? AND uri IS NOT NULL AND uri != ''"), ownerDID)
+ rows, err := db.Query("SELECT uri FROM api_keys WHERE owner_did = $1 AND uri IS NOT NULL AND uri != ''", ownerDID)
if err != nil {
return nil, err
}
@@ -714,7 +684,7 @@ func (db *DB) GetAPIKeyURIs(ownerDID string) ([]string, error) {
}
func (db *DB) GetPreferenceURIs(did string) ([]string, error) {
- rows, err := db.Query(db.Rebind("SELECT uri FROM preferences WHERE author_did = ? AND uri IS NOT NULL AND uri != ''"), did)
+ rows, err := db.Query("SELECT uri FROM preferences WHERE author_did = $1 AND uri IS NOT NULL AND uri != ''", did)
if err != nil {
return nil, err
}
@@ -731,22 +701,18 @@ func (db *DB) GetPreferenceURIs(did string) ([]string, error) {
}
func (db *DB) runMigrations() {
- dateType := "DATETIME"
- if db.driver == "postgres" {
- dateType = "TIMESTAMP"
- }
- db.Exec(`ALTER TABLE sessions ADD COLUMN dpop_key TEXT`)
-
- db.Exec(`ALTER TABLE annotations ADD COLUMN motivation TEXT`)
- db.Exec(`ALTER TABLE annotations ADD COLUMN body_value TEXT`)
- db.Exec(`ALTER TABLE annotations ADD COLUMN body_format TEXT DEFAULT 'text/plain'`)
- db.Exec(`ALTER TABLE annotations ADD COLUMN body_uri TEXT`)
- db.Exec(`ALTER TABLE annotations ADD COLUMN target_source TEXT`)
- db.Exec(`ALTER TABLE annotations ADD COLUMN target_hash TEXT`)
- db.Exec(`ALTER TABLE annotations ADD COLUMN target_title TEXT`)
- db.Exec(`ALTER TABLE annotations ADD COLUMN selector_json TEXT`)
- db.Exec(`ALTER TABLE annotations ADD COLUMN tags_json TEXT`)
- db.Exec(`ALTER TABLE annotations ADD COLUMN cid TEXT`)
+ db.Exec(`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS dpop_key TEXT`)
+
+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS motivation TEXT`)
+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_value TEXT`)
+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_format TEXT DEFAULT 'text/plain'`)
+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_uri TEXT`)
+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_source TEXT`)
+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_hash TEXT`)
+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_title TEXT`)
+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS selector_json TEXT`)
+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS tags_json TEXT`)
+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS cid TEXT`)
db.Exec(`UPDATE annotations SET target_source = url WHERE target_source IS NULL AND url IS NOT NULL`)
db.Exec(`UPDATE annotations SET target_hash = url_hash WHERE target_hash IS NULL AND url_hash IS NOT NULL`)
@@ -754,46 +720,39 @@ func (db *DB) runMigrations() {
db.Exec(`UPDATE annotations SET target_title = title WHERE target_title IS NULL AND title IS NOT NULL`)
db.Exec(`UPDATE annotations SET motivation = 'commenting' WHERE motivation IS NULL`)
- db.Exec(`ALTER TABLE profiles ADD COLUMN website TEXT`)
- db.Exec(`ALTER TABLE profiles ADD COLUMN display_name TEXT`)
- db.Exec(`ALTER TABLE profiles ADD COLUMN avatar TEXT`)
+ db.Exec(`ALTER TABLE profiles ADD COLUMN IF NOT EXISTS website TEXT`)
+ db.Exec(`ALTER TABLE profiles ADD COLUMN IF NOT EXISTS display_name TEXT`)
+ db.Exec(`ALTER TABLE profiles ADD COLUMN IF NOT EXISTS avatar TEXT`)
- if db.driver == "postgres" {
- db.Exec(`ALTER TABLE cursors ALTER COLUMN last_cursor TYPE BIGINT`)
- }
+ db.Exec(`ALTER TABLE cursors ALTER COLUMN last_cursor TYPE BIGINT`)
- db.Exec(`ALTER TABLE api_keys ADD COLUMN uri TEXT`)
- db.Exec(`ALTER TABLE api_keys ADD COLUMN cid TEXT`)
- db.Exec(`ALTER TABLE api_keys ADD COLUMN indexed_at ` + dateType + ` DEFAULT CURRENT_TIMESTAMP`)
+ db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS uri TEXT`)
+ db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS cid TEXT`)
+ db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`)
- db.migrateModeration(dateType)
+ db.migrateModeration()
- db.Exec(`ALTER TABLE preferences ADD COLUMN subscribed_labelers TEXT`)
- db.Exec(`ALTER TABLE preferences ADD COLUMN label_preferences TEXT`)
- db.Exec(`ALTER TABLE preferences ADD COLUMN disable_external_link_warning BOOLEAN`)
+ db.Exec(`ALTER TABLE preferences ADD COLUMN IF NOT EXISTS subscribed_labelers TEXT`)
+ db.Exec(`ALTER TABLE preferences ADD COLUMN IF NOT EXISTS label_preferences TEXT`)
+ db.Exec(`ALTER TABLE preferences ADD COLUMN IF NOT EXISTS disable_external_link_warning BOOLEAN`)
}
-func (db *DB) migrateModeration(dateType string) {
+func (db *DB) migrateModeration() {
_, err := db.Exec(`SELECT subject_did FROM moderation_reports LIMIT 0`)
if err != nil {
db.Exec(`DROP TABLE IF EXISTS moderation_reports`)
db.Exec(`DROP TABLE IF EXISTS moderation_actions`)
- autoInc := "INTEGER PRIMARY KEY AUTOINCREMENT"
- if db.driver == "postgres" {
- autoInc = "SERIAL PRIMARY KEY"
- }
-
db.Exec(`CREATE TABLE IF NOT EXISTS moderation_reports (
- id ` + autoInc + `,
+ id SERIAL PRIMARY KEY,
reporter_did TEXT NOT NULL,
subject_did TEXT NOT NULL,
subject_uri TEXT,
reason_type TEXT NOT NULL,
reason_text TEXT,
status TEXT NOT NULL DEFAULT 'pending',
- created_at ` + dateType + ` NOT NULL,
- resolved_at ` + dateType + `,
+ created_at TIMESTAMP NOT NULL,
+ resolved_at TIMESTAMP,
resolved_by TEXT
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_status ON moderation_reports(status)`)
@@ -801,28 +760,24 @@ func (db *DB) migrateModeration(dateType string) {
db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_reporter ON moderation_reports(reporter_did)`)
db.Exec(`CREATE TABLE IF NOT EXISTS moderation_actions (
- id ` + autoInc + `,
+ id SERIAL PRIMARY KEY,
report_id INTEGER NOT NULL,
actor_did TEXT NOT NULL,
action TEXT NOT NULL,
comment TEXT,
- created_at ` + dateType + ` NOT NULL
+ created_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_actions_report ON moderation_actions(report_id)`)
}
- autoInc := "INTEGER PRIMARY KEY AUTOINCREMENT"
- if db.driver == "postgres" {
- autoInc = "SERIAL PRIMARY KEY"
- }
db.Exec(`CREATE TABLE IF NOT EXISTS content_labels (
- id ` + autoInc + `,
+ id SERIAL PRIMARY KEY,
src TEXT NOT NULL,
uri TEXT NOT NULL,
val TEXT NOT NULL,
neg INTEGER NOT NULL DEFAULT 0,
created_by TEXT NOT NULL,
- created_at ` + dateType + ` NOT NULL
+ created_at TIMESTAMP NOT NULL
)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_uri ON content_labels(uri)`)
db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_src ON content_labels(src)`)
@@ -832,30 +787,6 @@ func (db *DB) Close() error {
return db.DB.Close()
}
-func (db *DB) Rebind(query string) string {
- if db.driver != "postgres" {
- return query
- }
-
- if !strings.Contains(query, "?") {
- return query
- }
-
- var builder strings.Builder
- builder.Grow(len(query) + 20)
-
- paramCount := 1
- for _, r := range query {
- if r == '?' {
- fmt.Fprintf(&builder, "$%d", paramCount)
- paramCount++
- } else {
- builder.WriteRune(r)
- }
- }
- return builder.String()
-}
-
func ParseSelector(selectorJSON *string) (*Selector, error) {
if selectorJSON == nil || *selectorJSON == "" {
return nil, nil
diff --git a/backend/internal/db/pg_helpers.go b/backend/internal/db/pg_helpers.go
new file mode 100644
index 0000000..06b92bb
--- /dev/null
+++ b/backend/internal/db/pg_helpers.go
@@ -0,0 +1,22 @@
+package db
+
+import (
+ "database/sql/driver"
+ "fmt"
+ "strings"
+)
+
+type pqStringArray []string
+
+func (a pqStringArray) Value() (driver.Value, error) {
+ if a == nil {
+ return "{}", nil
+ }
+ parts := make([]string, len(a))
+ for i, s := range a {
+ escaped := strings.ReplaceAll(s, "\\", "\\\\")
+ escaped = strings.ReplaceAll(escaped, "\"", "\\\"")
+ parts[i] = fmt.Sprintf(`"%s"`, escaped)
+ }
+ return "{" + strings.Join(parts, ",") + "}", nil
+}
diff --git a/backend/internal/db/queries.go b/backend/internal/db/queries.go
index 78c95d6..f470d0e 100644
--- a/backend/internal/db/queries.go
+++ b/backend/internal/db/queries.go
@@ -35,9 +35,9 @@ func scanAnnotations(rows interface {
}
func (db *DB) AnnotationExists(uri string) bool {
- var count int
- db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM annotations WHERE uri = ?`), uri).Scan(&count)
- return count > 0
+ var exists bool
+ db.QueryRow(`SELECT EXISTS(SELECT 1 FROM annotations WHERE uri = $1)`, uri).Scan(&exists)
+ return exists
}
func HashURL(rawURL string) string {
@@ -71,17 +71,17 @@ func ToJSON(v interface{}) string {
func (db *DB) GetAuthorByURI(uri string) (string, error) {
var authorDID string
- err := db.QueryRow(db.Rebind(`SELECT author_did FROM annotations WHERE uri = ?`), uri).Scan(&authorDID)
+ err := db.QueryRow(`SELECT author_did FROM annotations WHERE uri = $1`, uri).Scan(&authorDID)
if err == nil {
return authorDID, nil
}
- err = db.QueryRow(db.Rebind(`SELECT author_did FROM highlights WHERE uri = ?`), uri).Scan(&authorDID)
+ err = db.QueryRow(`SELECT author_did FROM highlights WHERE uri = $1`, uri).Scan(&authorDID)
if err == nil {
return authorDID, nil
}
- err = db.QueryRow(db.Rebind(`SELECT author_did FROM bookmarks WHERE uri = ?`), uri).Scan(&authorDID)
+ err = db.QueryRow(`SELECT author_did FROM bookmarks WHERE uri = $1`, uri).Scan(&authorDID)
if err == nil {
return authorDID, nil
}
@@ -89,13 +89,13 @@ func (db *DB) GetAuthorByURI(uri string) (string, error) {
return "", fmt.Errorf("uri not found or no author")
}
-func buildPlaceholders(n int) string {
+func buildPlaceholders(n, startAt int) string {
if n == 0 {
return ""
}
placeholders := make([]string, n)
for i := range placeholders {
- placeholders[i] = "?"
+ placeholders[i] = fmt.Sprintf("$%d", startAt+i)
}
return strings.Join(placeholders, ", ")
}
diff --git a/backend/internal/db/queries_annotations.go b/backend/internal/db/queries_annotations.go
index 60a9cc9..d2f7790 100644
--- a/backend/internal/db/queries_annotations.go
+++ b/backend/internal/db/queries_annotations.go
@@ -5,32 +5,32 @@ import (
)
func (db *DB) CreateAnnotation(a *Annotation) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO annotations (uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
ON CONFLICT(uri) DO UPDATE SET
- motivation = excluded.motivation,
- body_value = excluded.body_value,
- body_format = excluded.body_format,
- body_uri = excluded.body_uri,
- target_source = excluded.target_source,
- target_hash = excluded.target_hash,
- target_title = excluded.target_title,
- selector_json = excluded.selector_json,
- tags_json = excluded.tags_json,
- indexed_at = excluded.indexed_at,
- cid = excluded.cid
- `), a.URI, a.AuthorDID, a.Motivation, a.BodyValue, a.BodyFormat, a.BodyURI, a.TargetSource, a.TargetHash, a.TargetTitle, a.SelectorJSON, a.TagsJSON, a.CreatedAt, a.IndexedAt, a.CID)
+ motivation = EXCLUDED.motivation,
+ body_value = EXCLUDED.body_value,
+ body_format = EXCLUDED.body_format,
+ body_uri = EXCLUDED.body_uri,
+ target_source = EXCLUDED.target_source,
+ target_hash = EXCLUDED.target_hash,
+ target_title = EXCLUDED.target_title,
+ selector_json = EXCLUDED.selector_json,
+ tags_json = EXCLUDED.tags_json,
+ indexed_at = EXCLUDED.indexed_at,
+ cid = EXCLUDED.cid
+ `, a.URI, a.AuthorDID, a.Motivation, a.BodyValue, a.BodyFormat, a.BodyURI, a.TargetSource, a.TargetHash, a.TargetTitle, a.SelectorJSON, a.TagsJSON, a.CreatedAt, a.IndexedAt, a.CID)
return err
}
func (db *DB) GetAnnotationByURI(uri string) (*Annotation, error) {
var a Annotation
- err := db.QueryRow(db.Rebind(`
+ err := db.QueryRow(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE uri = ?
- `), uri).Scan(&a.URI, &a.AuthorDID, &a.Motivation, &a.BodyValue, &a.BodyFormat, &a.BodyURI, &a.TargetSource, &a.TargetHash, &a.TargetTitle, &a.SelectorJSON, &a.TagsJSON, &a.CreatedAt, &a.IndexedAt, &a.CID)
+ WHERE uri = $1
+ `, uri).Scan(&a.URI, &a.AuthorDID, &a.Motivation, &a.BodyValue, &a.BodyFormat, &a.BodyURI, &a.TargetSource, &a.TargetHash, &a.TargetTitle, &a.SelectorJSON, &a.TagsJSON, &a.CreatedAt, &a.IndexedAt, &a.CID)
if err != nil {
return nil, err
}
@@ -38,13 +38,13 @@ func (db *DB) GetAnnotationByURI(uri string) (*Annotation, error) {
}
func (db *DB) GetAnnotationsByTargetHash(targetHash string, limit, offset int) ([]Annotation, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE target_hash = ?
+ WHERE target_hash = $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), targetHash, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, targetHash, limit, offset)
if err != nil {
return nil, err
}
@@ -54,13 +54,13 @@ func (db *DB) GetAnnotationsByTargetHash(targetHash string, limit, offset int) (
}
func (db *DB) GetAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE author_did = ?
+ WHERE author_did = $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, authorDID, limit, offset)
if err != nil {
return nil, err
}
@@ -70,13 +70,13 @@ func (db *DB) GetAnnotationsByAuthor(authorDID string, limit, offset int) ([]Ann
}
func (db *DB) GetMarginAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, authorDID, limit, offset)
if err != nil {
return nil, err
}
@@ -86,13 +86,13 @@ func (db *DB) GetMarginAnnotationsByAuthor(authorDID string, limit, offset int)
}
func (db *DB) GetSembleAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE author_did = ? AND uri LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, authorDID, limit, offset)
if err != nil {
return nil, err
}
@@ -102,13 +102,13 @@ func (db *DB) GetSembleAnnotationsByAuthor(authorDID string, limit, offset int)
}
func (db *DB) GetAnnotationsByMotivation(motivation string, limit, offset int) ([]Annotation, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE motivation = ?
+ WHERE motivation = $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), motivation, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, motivation, limit, offset)
if err != nil {
return nil, err
}
@@ -118,12 +118,12 @@ func (db *DB) GetAnnotationsByMotivation(motivation string, limit, offset int) (
}
func (db *DB) GetRecentAnnotations(limit, offset int) ([]Annotation, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
@@ -134,22 +134,22 @@ func (db *DB) GetRecentAnnotations(limit, offset int) ([]Annotation, error) {
func (db *DB) GetPopularAnnotations(limit, offset int) ([]Annotation, error) {
since := time.Now().AddDate(0, 0, -14)
- rows, err := db.Query(db.Rebind(`
- SELECT
- a.uri, a.author_did, a.motivation, a.body_value, a.body_format,
- a.body_uri, a.target_source, a.target_hash, a.target_title,
+ rows, err := db.Query(`
+ SELECT
+ a.uri, a.author_did, a.motivation, a.body_value, a.body_format,
+ a.body_uri, a.target_source, a.target_hash, a.target_title,
a.selector_json, a.tags_json, a.created_at, a.indexed_at, a.cid
FROM annotations a
- LEFT JOIN (
- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
- ) l ON l.subject_uri = a.uri
- LEFT JOIN (
- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
- ) r ON r.root_uri = a.uri
- WHERE a.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) > 0
- ORDER BY (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) DESC, a.created_at DESC
- LIMIT ? OFFSET ?
- `), since, limit, offset)
+ LEFT JOIN LATERAL (
+ SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = a.uri
+ ) l ON true
+ LEFT JOIN LATERAL (
+ SELECT COUNT(*) as cnt FROM replies WHERE root_uri = a.uri
+ ) r ON true
+ WHERE a.created_at > $1 AND (l.cnt + r.cnt) > 0
+ ORDER BY (l.cnt + r.cnt) DESC, a.created_at DESC
+ LIMIT $2 OFFSET $3
+ `, since, limit, offset)
if err != nil {
return nil, err
}
@@ -161,22 +161,18 @@ func (db *DB) GetPopularAnnotations(limit, offset int) ([]Annotation, error) {
func (db *DB) GetShelvedAnnotations(limit, offset int) ([]Annotation, error) {
olderThan := time.Now().AddDate(0, 0, -1)
since := time.Now().AddDate(0, 0, -14)
- rows, err := db.Query(db.Rebind(`
- SELECT
- a.uri, a.author_did, a.motivation, a.body_value, a.body_format,
- a.body_uri, a.target_source, a.target_hash, a.target_title,
+ rows, err := db.Query(`
+ SELECT
+ a.uri, a.author_did, a.motivation, a.body_value, a.body_format,
+ a.body_uri, a.target_source, a.target_hash, a.target_title,
a.selector_json, a.tags_json, a.created_at, a.indexed_at, a.cid
FROM annotations a
- LEFT JOIN (
- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
- ) l ON l.subject_uri = a.uri
- LEFT JOIN (
- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
- ) r ON r.root_uri = a.uri
- WHERE a.created_at < ? AND a.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) = 0
+ WHERE a.created_at < $1 AND a.created_at > $2
+ AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = a.uri)
+ AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = a.uri)
ORDER BY RANDOM()
- LIMIT ? OFFSET ?
- `), olderThan, since, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, olderThan, since, limit, offset)
if err != nil {
return nil, err
}
@@ -186,13 +182,13 @@ func (db *DB) GetShelvedAnnotations(limit, offset int) ([]Annotation, error) {
}
func (db *DB) GetMarginAnnotations(limit, offset int) ([]Annotation, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
WHERE uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
@@ -202,13 +198,13 @@ func (db *DB) GetMarginAnnotations(limit, offset int) ([]Annotation, error) {
}
func (db *DB) GetSembleAnnotations(limit, offset int) ([]Annotation, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
WHERE uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
@@ -218,14 +214,13 @@ func (db *DB) GetSembleAnnotations(limit, offset int) ([]Annotation, error) {
}
func (db *DB) GetAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE tags_json LIKE ?
+ WHERE tags_json::jsonb ? $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), pattern, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, tag, limit, offset)
if err != nil {
return nil, err
}
@@ -235,14 +230,13 @@ func (db *DB) GetAnnotationsByTag(tag string, limit, offset int) ([]Annotation,
}
func (db *DB) GetMarginAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
+ WHERE tags_json::jsonb ? $1 AND uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), pattern, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, tag, limit, offset)
if err != nil {
return nil, err
}
@@ -252,14 +246,13 @@ func (db *DB) GetMarginAnnotationsByTag(tag string, limit, offset int) ([]Annota
}
func (db *DB) GetSembleAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%'
+ WHERE tags_json::jsonb ? $1 AND uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), pattern, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, tag, limit, offset)
if err != nil {
return nil, err
}
@@ -269,28 +262,27 @@ func (db *DB) GetSembleAnnotationsByTag(tag string, limit, offset int) ([]Annota
}
func (db *DB) DeleteAnnotation(uri string) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM annotations WHERE uri = ?`), uri)
+ _, err := db.Exec(`DELETE FROM annotations WHERE uri = $1`, uri)
return err
}
func (db *DB) UpdateAnnotation(uri, bodyValue, tagsJSON, cid string) error {
- _, err := db.Exec(db.Rebind(`
- UPDATE annotations
- SET body_value = ?, tags_json = ?, cid = ?, indexed_at = ?
- WHERE uri = ?
- `), bodyValue, tagsJSON, cid, time.Now(), uri)
+ _, err := db.Exec(`
+ UPDATE annotations
+ SET body_value = $1, tags_json = $2, cid = $3, indexed_at = $4
+ WHERE uri = $5
+ `, bodyValue, tagsJSON, cid, time.Now(), uri)
return err
}
func (db *DB) GetAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE author_did = ? AND tags_json LIKE ?
+ WHERE author_did = $1 AND tags_json::jsonb ? $2
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, pattern, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, tag, limit, offset)
if err != nil {
return nil, err
}
@@ -300,14 +292,13 @@ func (db *DB) GetAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset
}
func (db *DB) GetMarginAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, pattern, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, tag, limit, offset)
if err != nil {
return nil, err
}
@@ -317,14 +308,13 @@ func (db *DB) GetMarginAnnotationsByTagAndAuthor(tag, authorDID string, limit, o
}
func (db *DB) GetSembleAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, pattern, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, tag, limit, offset)
if err != nil {
return nil, err
}
@@ -334,13 +324,13 @@ func (db *DB) GetSembleAnnotationsByTagAndAuthor(tag, authorDID string, limit, o
}
func (db *DB) GetAnnotationsByAuthorAndTargetHash(authorDID, targetHash string, limit, offset int) ([]Annotation, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE author_did = ? AND target_hash = ?
+ WHERE author_did = $1 AND target_hash = $2
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, targetHash, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, targetHash, limit, offset)
if err != nil {
return nil, err
}
@@ -354,18 +344,13 @@ func (db *DB) GetAnnotationsByURIs(uris []string) ([]Annotation, error) {
return []Annotation{}, nil
}
- query := db.Rebind(`
+ query := `
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE uri IN (` + buildPlaceholders(len(uris)) + `)
- `)
+ WHERE uri = ANY($1)
+ `
- args := make([]interface{}, len(uris))
- for i, uri := range uris {
- args[i] = uri
- }
-
- rows, err := db.Query(query, args...)
+ rows, err := db.Query(query, pqStringArray(uris))
if err != nil {
return nil, err
}
@@ -375,9 +360,9 @@ func (db *DB) GetAnnotationsByURIs(uris []string) ([]Annotation, error) {
}
func (db *DB) GetAnnotationURIs(authorDID string) ([]string, error) {
- rows, err := db.Query(db.Rebind(`
- SELECT uri FROM annotations WHERE author_did = ?
- `), authorDID)
+ rows, err := db.Query(`
+ SELECT uri FROM annotations WHERE author_did = $1
+ `, authorDID)
if err != nil {
return nil, err
}
diff --git a/backend/internal/db/queries_bookmarks.go b/backend/internal/db/queries_bookmarks.go
index ae1ce56..edfe814 100644
--- a/backend/internal/db/queries_bookmarks.go
+++ b/backend/internal/db/queries_bookmarks.go
@@ -5,28 +5,28 @@ import (
)
func (db *DB) CreateBookmark(b *Bookmark) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO bookmarks (uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
ON CONFLICT(uri) DO UPDATE SET
- source = excluded.source,
- source_hash = excluded.source_hash,
- title = excluded.title,
- description = excluded.description,
- tags_json = excluded.tags_json,
- indexed_at = excluded.indexed_at,
- cid = excluded.cid
- `), b.URI, b.AuthorDID, b.Source, b.SourceHash, b.Title, b.Description, b.TagsJSON, b.CreatedAt, b.IndexedAt, b.CID)
+ source = EXCLUDED.source,
+ source_hash = EXCLUDED.source_hash,
+ title = EXCLUDED.title,
+ description = EXCLUDED.description,
+ tags_json = EXCLUDED.tags_json,
+ indexed_at = EXCLUDED.indexed_at,
+ cid = EXCLUDED.cid
+ `, b.URI, b.AuthorDID, b.Source, b.SourceHash, b.Title, b.Description, b.TagsJSON, b.CreatedAt, b.IndexedAt, b.CID)
return err
}
func (db *DB) GetBookmarkByURI(uri string) (*Bookmark, error) {
var b Bookmark
- err := db.QueryRow(db.Rebind(`
+ err := db.QueryRow(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE uri = ?
- `), uri).Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID)
+ WHERE uri = $1
+ `, uri).Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID)
if err != nil {
return nil, err
}
@@ -34,376 +34,254 @@ func (db *DB) GetBookmarkByURI(uri string) (*Bookmark, error) {
}
func (db *DB) GetRecentBookmarks(limit, offset int) ([]Bookmark, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetPopularBookmarks(limit, offset int) ([]Bookmark, error) {
since := time.Now().AddDate(0, 0, -14)
- rows, err := db.Query(db.Rebind(`
- SELECT
- b.uri, b.author_did, b.source, b.source_hash, b.title,
+ rows, err := db.Query(`
+ SELECT
+ b.uri, b.author_did, b.source, b.source_hash, b.title,
b.description, b.tags_json, b.created_at, b.indexed_at, b.cid
FROM bookmarks b
- LEFT JOIN (
- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
- ) l ON l.subject_uri = b.uri
- LEFT JOIN (
- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
- ) r ON r.root_uri = b.uri
- WHERE b.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) > 0
- ORDER BY (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) DESC, b.created_at DESC
- LIMIT ? OFFSET ?
- `), since, limit, offset)
+ LEFT JOIN LATERAL (
+ SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = b.uri
+ ) l ON true
+ LEFT JOIN LATERAL (
+ SELECT COUNT(*) as cnt FROM replies WHERE root_uri = b.uri
+ ) r ON true
+ WHERE b.created_at > $1 AND (l.cnt + r.cnt) > 0
+ ORDER BY (l.cnt + r.cnt) DESC, b.created_at DESC
+ LIMIT $2 OFFSET $3
+ `, since, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetShelvedBookmarks(limit, offset int) ([]Bookmark, error) {
olderThan := time.Now().AddDate(0, 0, -1)
since := time.Now().AddDate(0, 0, -14)
- rows, err := db.Query(db.Rebind(`
- SELECT
- b.uri, b.author_did, b.source, b.source_hash, b.title,
+ rows, err := db.Query(`
+ SELECT
+ b.uri, b.author_did, b.source, b.source_hash, b.title,
b.description, b.tags_json, b.created_at, b.indexed_at, b.cid
FROM bookmarks b
- LEFT JOIN (
- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
- ) l ON l.subject_uri = b.uri
- LEFT JOIN (
- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
- ) r ON r.root_uri = b.uri
- WHERE b.created_at < ? AND b.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) = 0
+ WHERE b.created_at < $1 AND b.created_at > $2
+ AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = b.uri)
+ AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = b.uri)
ORDER BY RANDOM()
- LIMIT ? OFFSET ?
- `), olderThan, since, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, olderThan, since, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetMarginBookmarks(limit, offset int) ([]Bookmark, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
WHERE uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetSembleBookmarks(limit, offset int) ([]Bookmark, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
WHERE uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE tags_json LIKE ?
+ WHERE tags_json::jsonb ? $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), pattern, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetMarginBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
+ WHERE tags_json::jsonb ? $1 AND uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), pattern, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetSembleBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%'
+ WHERE tags_json::jsonb ? $1 AND uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), pattern, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE author_did = ? AND tags_json LIKE ?
+ WHERE author_did = $1 AND tags_json::jsonb ? $2
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, pattern, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetMarginBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, pattern, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetSembleBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, pattern, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE author_did = ?
+ WHERE author_did = $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, authorDID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetMarginBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, authorDID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetSembleBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE author_did = ? AND uri LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, authorDID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) DeleteBookmark(uri string) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM bookmarks WHERE uri = ?`), uri)
+ _, err := db.Exec(`DELETE FROM bookmarks WHERE uri = $1`, uri)
return err
}
func (db *DB) UpdateBookmark(uri, title, description, tagsJSON, cid string) error {
- _, err := db.Exec(db.Rebind(`
- UPDATE bookmarks
- SET title = ?, description = ?, tags_json = ?, cid = ?, indexed_at = ?
- WHERE uri = ?
- `), title, description, tagsJSON, cid, time.Now(), uri)
+ _, err := db.Exec(`
+ UPDATE bookmarks
+ SET title = $1, description = $2, tags_json = $3, cid = $4, indexed_at = $5
+ WHERE uri = $6
+ `, title, description, tagsJSON, cid, time.Now(), uri)
return err
}
@@ -412,38 +290,23 @@ func (db *DB) GetBookmarksByURIs(uris []string) ([]Bookmark, error) {
return []Bookmark{}, nil
}
- query := db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE uri IN (` + buildPlaceholders(len(uris)) + `)
- `)
-
- args := make([]interface{}, len(uris))
- for i, uri := range uris {
- args[i] = uri
- }
-
- rows, err := db.Query(query, args...)
+ WHERE uri = ANY($1)
+ `, pqStringArray(uris))
if err != nil {
return nil, err
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
func (db *DB) GetBookmarkURIs(authorDID string) ([]string, error) {
- rows, err := db.Query(db.Rebind(`
- SELECT uri FROM bookmarks WHERE author_did = ?
- `), authorDID)
+ rows, err := db.Query(`
+ SELECT uri FROM bookmarks WHERE author_did = $1
+ `, authorDID)
if err != nil {
return nil, err
}
@@ -461,18 +324,25 @@ func (db *DB) GetBookmarkURIs(authorDID string) ([]string, error) {
}
func (db *DB) GetBookmarksByTargetHash(targetHash string, limit, offset int) ([]Bookmark, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE source_hash = ?
+ WHERE source_hash = $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), targetHash, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, targetHash, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
+ return scanBookmarks(rows)
+}
+
+func scanBookmarks(rows interface {
+ Next() bool
+ Scan(...interface{}) error
+}) ([]Bookmark, error) {
var bookmarks []Bookmark
for rows.Next() {
var b Bookmark
diff --git a/backend/internal/db/queries_collections.go b/backend/internal/db/queries_collections.go
index 91d2bfd..e8dbfbf 100644
--- a/backend/internal/db/queries_collections.go
+++ b/backend/internal/db/queries_collections.go
@@ -3,25 +3,25 @@ package db
import "time"
func (db *DB) CreateCollection(c *Collection) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO collections (uri, author_did, name, description, icon, created_at, indexed_at)
- VALUES (?, ?, ?, ?, ?, ?, ?)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT(uri) DO UPDATE SET
- name = excluded.name,
- description = excluded.description,
- icon = excluded.icon,
- indexed_at = excluded.indexed_at
- `), c.URI, c.AuthorDID, c.Name, c.Description, c.Icon, c.CreatedAt, c.IndexedAt)
+ name = EXCLUDED.name,
+ description = EXCLUDED.description,
+ icon = EXCLUDED.icon,
+ indexed_at = EXCLUDED.indexed_at
+ `, c.URI, c.AuthorDID, c.Name, c.Description, c.Icon, c.CreatedAt, c.IndexedAt)
return err
}
func (db *DB) GetCollectionsByAuthor(authorDID string) ([]Collection, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, name, description, icon, created_at, indexed_at
FROM collections
- WHERE author_did = ?
+ WHERE author_did = $1
ORDER BY created_at DESC
- `), authorDID)
+ `, authorDID)
if err != nil {
return nil, err
}
@@ -40,11 +40,11 @@ func (db *DB) GetCollectionsByAuthor(authorDID string) ([]Collection, error) {
func (db *DB) GetCollectionByURI(uri string) (*Collection, error) {
var c Collection
- err := db.QueryRow(db.Rebind(`
+ err := db.QueryRow(`
SELECT uri, author_did, name, description, icon, created_at, indexed_at
FROM collections
- WHERE uri = ?
- `), uri).Scan(&c.URI, &c.AuthorDID, &c.Name, &c.Description, &c.Icon, &c.CreatedAt, &c.IndexedAt)
+ WHERE uri = $1
+ `, uri).Scan(&c.URI, &c.AuthorDID, &c.Name, &c.Description, &c.Icon, &c.CreatedAt, &c.IndexedAt)
if err != nil {
return nil, err
}
@@ -52,30 +52,29 @@ func (db *DB) GetCollectionByURI(uri string) (*Collection, error) {
}
func (db *DB) DeleteCollection(uri string) error {
-
- db.Exec(db.Rebind(`DELETE FROM collection_items WHERE collection_uri = ?`), uri)
- _, err := db.Exec(db.Rebind(`DELETE FROM collections WHERE uri = ?`), uri)
+ db.Exec(`DELETE FROM collection_items WHERE collection_uri = $1`, uri)
+ _, err := db.Exec(`DELETE FROM collections WHERE uri = $1`, uri)
return err
}
func (db *DB) AddToCollection(item *CollectionItem) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO collection_items (uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at)
- VALUES (?, ?, ?, ?, ?, ?, ?)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT(uri) DO UPDATE SET
- position = excluded.position,
- indexed_at = excluded.indexed_at
- `), item.URI, item.AuthorDID, item.CollectionURI, item.AnnotationURI, item.Position, item.CreatedAt, item.IndexedAt)
+ position = EXCLUDED.position,
+ indexed_at = EXCLUDED.indexed_at
+ `, item.URI, item.AuthorDID, item.CollectionURI, item.AnnotationURI, item.Position, item.CreatedAt, item.IndexedAt)
return err
}
func (db *DB) GetCollectionItems(collectionURI string) ([]CollectionItem, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at
FROM collection_items
- WHERE collection_uri = ?
+ WHERE collection_uri = $1
ORDER BY position ASC, created_at DESC
- `), collectionURI)
+ `, collectionURI)
if err != nil {
return nil, err
}
@@ -93,127 +92,91 @@ func (db *DB) GetCollectionItems(collectionURI string) ([]CollectionItem, error)
}
func (db *DB) RemoveFromCollection(uri string) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM collection_items WHERE uri = ?`), uri)
+ _, err := db.Exec(`DELETE FROM collection_items WHERE uri = $1`, uri)
return err
}
func (db *DB) GetRecentCollectionItems(limit, offset int) ([]CollectionItem, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at
FROM collection_items
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var items []CollectionItem
- for rows.Next() {
- var item CollectionItem
- if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil {
- return nil, err
- }
- items = append(items, item)
- }
- return items, nil
+ return scanCollectionItems(rows)
}
func (db *DB) GetPopularCollectionItems(limit, offset int) ([]CollectionItem, error) {
since := time.Now().AddDate(0, 0, -14)
- rows, err := db.Query(db.Rebind(`
- SELECT
- c.uri, c.author_did, c.collection_uri, c.annotation_uri,
+ rows, err := db.Query(`
+ SELECT
+ c.uri, c.author_did, c.collection_uri, c.annotation_uri,
c.position, c.created_at, c.indexed_at
FROM collection_items c
- LEFT JOIN (
- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
- ) l ON l.subject_uri = c.annotation_uri
- LEFT JOIN (
- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
- ) r ON r.root_uri = c.annotation_uri
- WHERE c.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) > 0
- ORDER BY (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) DESC, c.created_at DESC
- LIMIT ? OFFSET ?
- `), since, limit, offset)
+ LEFT JOIN LATERAL (
+ SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = c.annotation_uri
+ ) l ON true
+ LEFT JOIN LATERAL (
+ SELECT COUNT(*) as cnt FROM replies WHERE root_uri = c.annotation_uri
+ ) r ON true
+ WHERE c.created_at > $1 AND (l.cnt + r.cnt) > 0
+ ORDER BY (l.cnt + r.cnt) DESC, c.created_at DESC
+ LIMIT $2 OFFSET $3
+ `, since, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var items []CollectionItem
- for rows.Next() {
- var item CollectionItem
- if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil {
- return nil, err
- }
- items = append(items, item)
- }
- return items, nil
+ return scanCollectionItems(rows)
}
func (db *DB) GetShelvedCollectionItems(limit, offset int) ([]CollectionItem, error) {
olderThan := time.Now().AddDate(0, 0, -1)
since := time.Now().AddDate(0, 0, -14)
- rows, err := db.Query(db.Rebind(`
- SELECT
- c.uri, c.author_did, c.collection_uri, c.annotation_uri,
+ rows, err := db.Query(`
+ SELECT
+ c.uri, c.author_did, c.collection_uri, c.annotation_uri,
c.position, c.created_at, c.indexed_at
FROM collection_items c
- LEFT JOIN (
- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
- ) l ON l.subject_uri = c.annotation_uri
- LEFT JOIN (
- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
- ) r ON r.root_uri = c.annotation_uri
- WHERE c.created_at < ? AND c.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) = 0
+ WHERE c.created_at < $1 AND c.created_at > $2
+ AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = c.annotation_uri)
+ AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = c.annotation_uri)
ORDER BY RANDOM()
- LIMIT ? OFFSET ?
- `), olderThan, since, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, olderThan, since, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var items []CollectionItem
- for rows.Next() {
- var item CollectionItem
- if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil {
- return nil, err
- }
- items = append(items, item)
- }
- return items, nil
+ return scanCollectionItems(rows)
}
func (db *DB) GetCollectionItemsByAuthor(authorDID string) ([]CollectionItem, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at
FROM collection_items
- WHERE author_did = ?
+ WHERE author_did = $1
ORDER BY created_at DESC
- `), authorDID)
+ `, authorDID)
if err != nil {
return nil, err
}
defer rows.Close()
- var items []CollectionItem
- for rows.Next() {
- var item CollectionItem
- if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil {
- return nil, err
- }
- items = append(items, item)
- }
- return items, nil
+ return scanCollectionItems(rows)
}
func (db *DB) GetCollectionURIsForAnnotation(annotationURI string) ([]string, error) {
- rows, err := db.Query(db.Rebind(`
- SELECT collection_uri FROM collection_items WHERE annotation_uri = ?
- `), annotationURI)
+ rows, err := db.Query(`
+ SELECT collection_uri FROM collection_items WHERE annotation_uri = $1
+ `, annotationURI)
if err != nil {
return nil, err
}
@@ -235,19 +198,12 @@ func (db *DB) GetCollectionItemCounts(uris []string) (map[string]int, error) {
return map[string]int{}, nil
}
- query := db.Rebind(`
+ rows, err := db.Query(`
SELECT collection_uri, COUNT(*)
FROM collection_items
- WHERE collection_uri IN (` + buildPlaceholders(len(uris)) + `)
+ WHERE collection_uri = ANY($1)
GROUP BY collection_uri
- `)
-
- args := make([]interface{}, len(uris))
- for i, uri := range uris {
- args[i] = uri
- }
-
- rows, err := db.Query(query, args...)
+ `, pqStringArray(uris))
if err != nil {
return nil, err
}
@@ -270,18 +226,11 @@ func (db *DB) GetCollectionsByURIs(uris []string) ([]Collection, error) {
return []Collection{}, nil
}
- query := db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, name, description, icon, created_at, indexed_at
FROM collections
- WHERE uri IN (` + buildPlaceholders(len(uris)) + `)
- `)
-
- args := make([]interface{}, len(uris))
- for i, uri := range uris {
- args[i] = uri
- }
-
- rows, err := db.Query(query, args...)
+ WHERE uri = ANY($1)
+ `, pqStringArray(uris))
if err != nil {
return nil, err
}
@@ -297,3 +246,18 @@ func (db *DB) GetCollectionsByURIs(uris []string) ([]Collection, error) {
}
return collections, nil
}
+
+func scanCollectionItems(rows interface {
+ Next() bool
+ Scan(...interface{}) error
+}) ([]CollectionItem, error) {
+ var items []CollectionItem
+ for rows.Next() {
+ var item CollectionItem
+ if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil {
+ return nil, err
+ }
+ items = append(items, item)
+ }
+ return items, nil
+}
diff --git a/backend/internal/db/queries_highlights.go b/backend/internal/db/queries_highlights.go
index 6cdcf8a..bdd755a 100644
--- a/backend/internal/db/queries_highlights.go
+++ b/backend/internal/db/queries_highlights.go
@@ -5,29 +5,29 @@ import (
)
func (db *DB) CreateHighlight(h *Highlight) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO highlights (uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
ON CONFLICT(uri) DO UPDATE SET
- target_source = excluded.target_source,
- target_hash = excluded.target_hash,
- target_title = excluded.target_title,
- selector_json = excluded.selector_json,
- color = excluded.color,
- tags_json = excluded.tags_json,
- indexed_at = excluded.indexed_at,
- cid = excluded.cid
- `), h.URI, h.AuthorDID, h.TargetSource, h.TargetHash, h.TargetTitle, h.SelectorJSON, h.Color, h.TagsJSON, h.CreatedAt, h.IndexedAt, h.CID)
+ target_source = EXCLUDED.target_source,
+ target_hash = EXCLUDED.target_hash,
+ target_title = EXCLUDED.target_title,
+ selector_json = EXCLUDED.selector_json,
+ color = EXCLUDED.color,
+ tags_json = EXCLUDED.tags_json,
+ indexed_at = EXCLUDED.indexed_at,
+ cid = EXCLUDED.cid
+ `, h.URI, h.AuthorDID, h.TargetSource, h.TargetHash, h.TargetTitle, h.SelectorJSON, h.Color, h.TagsJSON, h.CreatedAt, h.IndexedAt, h.CID)
return err
}
func (db *DB) GetHighlightByURI(uri string) (*Highlight, error) {
var h Highlight
- err := db.QueryRow(db.Rebind(`
+ err := db.QueryRow(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE uri = ?
- `), uri).Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID)
+ WHERE uri = $1
+ `, uri).Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID)
if err != nil {
return nil, err
}
@@ -35,424 +35,286 @@ func (db *DB) GetHighlightByURI(uri string) (*Highlight, error) {
}
func (db *DB) GetRecentHighlights(limit, offset int) ([]Highlight, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetPopularHighlights(limit, offset int) ([]Highlight, error) {
since := time.Now().AddDate(0, 0, -14)
- rows, err := db.Query(db.Rebind(`
- SELECT
- h.uri, h.author_did, h.target_source, h.target_hash, h.target_title,
+ rows, err := db.Query(`
+ SELECT
+ h.uri, h.author_did, h.target_source, h.target_hash, h.target_title,
h.selector_json, h.color, h.tags_json, h.created_at, h.indexed_at, h.cid
FROM highlights h
- LEFT JOIN (
- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
- ) l ON l.subject_uri = h.uri
- LEFT JOIN (
- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
- ) r ON r.root_uri = h.uri
- WHERE h.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) > 0
- ORDER BY (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) DESC, h.created_at DESC
- LIMIT ? OFFSET ?
- `), since, limit, offset)
+ LEFT JOIN LATERAL (
+ SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = h.uri
+ ) l ON true
+ LEFT JOIN LATERAL (
+ SELECT COUNT(*) as cnt FROM replies WHERE root_uri = h.uri
+ ) r ON true
+ WHERE h.created_at > $1 AND (l.cnt + r.cnt) > 0
+ ORDER BY (l.cnt + r.cnt) DESC, h.created_at DESC
+ LIMIT $2 OFFSET $3
+ `, since, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetShelvedHighlights(limit, offset int) ([]Highlight, error) {
olderThan := time.Now().AddDate(0, 0, -1)
since := time.Now().AddDate(0, 0, -14)
- rows, err := db.Query(db.Rebind(`
- SELECT
- h.uri, h.author_did, h.target_source, h.target_hash, h.target_title,
+ rows, err := db.Query(`
+ SELECT
+ h.uri, h.author_did, h.target_source, h.target_hash, h.target_title,
h.selector_json, h.color, h.tags_json, h.created_at, h.indexed_at, h.cid
FROM highlights h
- LEFT JOIN (
- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
- ) l ON l.subject_uri = h.uri
- LEFT JOIN (
- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
- ) r ON r.root_uri = h.uri
- WHERE h.created_at < ? AND h.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) = 0
+ WHERE h.created_at < $1 AND h.created_at > $2
+ AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = h.uri)
+ AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = h.uri)
ORDER BY RANDOM()
- LIMIT ? OFFSET ?
- `), olderThan, since, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, olderThan, since, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetMarginHighlights(limit, offset int) ([]Highlight, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
WHERE uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetSembleHighlights(limit, offset int) ([]Highlight, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
WHERE uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE tags_json LIKE ?
+ WHERE tags_json::jsonb ? $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), pattern, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetMarginHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
+ WHERE tags_json::jsonb ? $1 AND uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), pattern, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetSembleHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%'
+ WHERE tags_json::jsonb ? $1 AND uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), pattern, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE author_did = ? AND tags_json LIKE ?
+ WHERE author_did = $1 AND tags_json::jsonb ? $2
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, pattern, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetMarginHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, pattern, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetSembleHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
- pattern := "%\"" + tag + "\"%"
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, pattern, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, tag, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetHighlightsByTargetHash(targetHash string, limit, offset int) ([]Highlight, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE target_hash = ?
+ WHERE target_hash = $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), targetHash, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, targetHash, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE author_did = ?
+ WHERE author_did = $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, authorDID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetMarginHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND uri NOT LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, authorDID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetSembleHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE author_did = ? AND uri LIKE '%network.cosmik%'
+ WHERE author_did = $1 AND uri LIKE '%network.cosmik%'
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, authorDID, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetHighlightsByAuthorAndTargetHash(authorDID, targetHash string, limit, offset int) ([]Highlight, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE author_did = ? AND target_hash = ?
+ WHERE author_did = $1 AND target_hash = $2
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), authorDID, targetHash, limit, offset)
+ LIMIT $3 OFFSET $4
+ `, authorDID, targetHash, limit, offset)
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) DeleteHighlight(uri string) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM highlights WHERE uri = ?`), uri)
+ _, err := db.Exec(`DELETE FROM highlights WHERE uri = $1`, uri)
return err
}
func (db *DB) UpdateHighlight(uri, color, tagsJSON, cid string) error {
- _, err := db.Exec(db.Rebind(`
- UPDATE highlights
- SET color = ?, tags_json = ?, cid = ?, indexed_at = ?
- WHERE uri = ?
- `), color, tagsJSON, cid, time.Now(), uri)
+ _, err := db.Exec(`
+ UPDATE highlights
+ SET color = $1, tags_json = $2, cid = $3, indexed_at = $4
+ WHERE uri = $5
+ `, color, tagsJSON, cid, time.Now(), uri)
return err
}
@@ -461,38 +323,23 @@ func (db *DB) GetHighlightsByURIs(uris []string) ([]Highlight, error) {
return []Highlight{}, nil
}
- query := db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE uri IN (` + buildPlaceholders(len(uris)) + `)
- `)
-
- args := make([]interface{}, len(uris))
- for i, uri := range uris {
- args[i] = uri
- }
-
- rows, err := db.Query(query, args...)
+ WHERE uri = ANY($1)
+ `, pqStringArray(uris))
if err != nil {
return nil, err
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) GetHighlightURIs(authorDID string) ([]string, error) {
- rows, err := db.Query(db.Rebind(`
- SELECT uri FROM highlights WHERE author_did = ?
- `), authorDID)
+ rows, err := db.Query(`
+ SELECT uri FROM highlights WHERE author_did = $1
+ `, authorDID)
if err != nil {
return nil, err
}
@@ -508,3 +355,18 @@ func (db *DB) GetHighlightURIs(authorDID string) ([]string, error) {
}
return uris, nil
}
+
+func scanHighlights(rows interface {
+ Next() bool
+ Scan(...interface{}) error
+}) ([]Highlight, error) {
+ var highlights []Highlight
+ for rows.Next() {
+ var h Highlight
+ if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
+ return nil, err
+ }
+ highlights = append(highlights, h)
+ }
+ return highlights, nil
+}
diff --git a/backend/internal/db/queries_history.go b/backend/internal/db/queries_history.go
index 254a0d2..55eba4e 100644
--- a/backend/internal/db/queries_history.go
+++ b/backend/internal/db/queries_history.go
@@ -7,20 +7,20 @@ import (
)
func (db *DB) SaveEditHistory(uri, recordType, previousContent string, previousCID *string) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO edit_history (uri, record_type, previous_content, previous_cid, edited_at)
- VALUES (?, ?, ?, ?, ?)
- `), uri, recordType, previousContent, previousCID, time.Now())
+ VALUES ($1, $2, $3, $4, $5)
+ `, uri, recordType, previousContent, previousCID, time.Now())
return err
}
func (db *DB) GetEditHistory(uri string) ([]EditHistory, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT id, uri, record_type, previous_content, previous_cid, edited_at
FROM edit_history
- WHERE uri = ?
+ WHERE uri = $1
ORDER BY edited_at DESC
- `), uri)
+ `, uri)
if err != nil {
return nil, err
}
@@ -29,28 +29,9 @@ func (db *DB) GetEditHistory(uri string) ([]EditHistory, error) {
var history []EditHistory
for rows.Next() {
var h EditHistory
- var editedAt interface{}
- if err := rows.Scan(&h.ID, &h.URI, &h.RecordType, &h.PreviousContent, &h.PreviousCID, &editedAt); err != nil {
+ if err := rows.Scan(&h.ID, &h.URI, &h.RecordType, &h.PreviousContent, &h.PreviousCID, &h.EditedAt); err != nil {
return nil, err
}
-
- switch v := editedAt.(type) {
- case time.Time:
- h.EditedAt = v
- case []byte:
- parsed, err := parseTime(string(v))
- if err != nil {
- return nil, err
- }
- h.EditedAt = parsed
- case string:
- parsed, err := parseTime(v)
- if err != nil {
- return nil, err
- }
- h.EditedAt = parsed
- }
-
history = append(history, h)
}
return history, nil
@@ -61,33 +42,21 @@ func (db *DB) GetLatestEditTimes(uris []string) (map[string]time.Time, error) {
return nil, nil
}
- query := `
- SELECT uri, MAX(edited_at) as edited_at
- FROM edit_history
- WHERE uri IN (`
- args := make([]interface{}, len(uris))
placeholders := make([]string, len(uris))
-
+ args := make([]interface{}, len(uris))
for i, uri := range uris {
placeholders[i] = fmt.Sprintf("$%d", i+1)
args[i] = uri
}
- query += strings.Join(placeholders, ",") + ") GROUP BY uri"
-
- if db.driver == "sqlite3" {
- query = strings.ReplaceAll(query, "$", "?")
- placeholders = make([]string, len(uris))
- for i := range uris {
- placeholders[i] = "?"
- }
- query = `
+ query := `
SELECT uri, MAX(edited_at) as edited_at
FROM edit_history
- WHERE uri IN (` + strings.Join(placeholders, ",") + ") GROUP BY uri"
- }
+ WHERE uri IN (` + strings.Join(placeholders, ",") + `)
+ GROUP BY uri
+ `
- rows, err := db.Query(db.Rebind(query), args...)
+ rows, err := db.Query(query, args...)
if err != nil {
return nil, err
}
@@ -96,49 +65,12 @@ func (db *DB) GetLatestEditTimes(uris []string) (map[string]time.Time, error) {
result := make(map[string]time.Time)
for rows.Next() {
var uri string
- var editedAt interface{}
+ var editedAt time.Time
if err := rows.Scan(&uri, &editedAt); err != nil {
continue
}
-
- var finalTime time.Time
- switch v := editedAt.(type) {
- case time.Time:
- finalTime = v
- case []byte:
- parsed, err := parseTime(string(v))
- if err != nil {
- continue
- }
- finalTime = parsed
- case string:
- parsed, err := parseTime(v)
- if err != nil {
- continue
- }
- finalTime = parsed
- default:
- continue
- }
-
- result[uri] = finalTime
+ result[uri] = editedAt
}
return result, nil
}
-
-func parseTime(s string) (time.Time, error) {
- formats := []string{
- time.RFC3339,
- time.RFC3339Nano,
- "2006-01-02 15:04:05.999999999-07:00",
- "2006-01-02 15:04:05",
- }
-
- for _, f := range formats {
- if t, err := time.Parse(f, s); err == nil {
- return t, nil
- }
- }
- return time.Time{}, fmt.Errorf("could not parse time: %s", s)
-}
diff --git a/backend/internal/db/queries_keys.go b/backend/internal/db/queries_keys.go
index c0c8990..325ce1a 100644
--- a/backend/internal/db/queries_keys.go
+++ b/backend/internal/db/queries_keys.go
@@ -5,25 +5,25 @@ import (
)
func (db *DB) CreateAPIKey(key *APIKey) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO api_keys (id, owner_did, name, key_hash, created_at, uri, cid)
- VALUES (?, ?, ?, ?, ?, ?, ?)
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (id) DO UPDATE SET
name = EXCLUDED.name,
key_hash = EXCLUDED.key_hash,
uri = EXCLUDED.uri,
cid = EXCLUDED.cid
- `), key.ID, key.OwnerDID, key.Name, key.KeyHash, key.CreatedAt, key.URI, key.CID)
+ `, key.ID, key.OwnerDID, key.Name, key.KeyHash, key.CreatedAt, key.URI, key.CID)
return err
}
func (db *DB) GetAPIKeysByOwner(ownerDID string) ([]APIKey, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT id, owner_did, name, key_hash, created_at, last_used_at
FROM api_keys
- WHERE owner_did = ?
+ WHERE owner_did = $1
ORDER BY created_at DESC
- `), ownerDID)
+ `, ownerDID)
if err != nil {
return nil, err
}
@@ -42,11 +42,11 @@ func (db *DB) GetAPIKeysByOwner(ownerDID string) ([]APIKey, error) {
func (db *DB) GetAPIKeyByHash(keyHash string) (*APIKey, error) {
var k APIKey
- err := db.QueryRow(db.Rebind(`
+ err := db.QueryRow(`
SELECT id, owner_did, name, key_hash, created_at, last_used_at
FROM api_keys
- WHERE key_hash = ?
- `), keyHash).Scan(&k.ID, &k.OwnerDID, &k.Name, &k.KeyHash, &k.CreatedAt, &k.LastUsedAt)
+ WHERE key_hash = $1
+ `, keyHash).Scan(&k.ID, &k.OwnerDID, &k.Name, &k.KeyHash, &k.CreatedAt, &k.LastUsedAt)
if err != nil {
return nil, err
}
@@ -54,6 +54,6 @@ func (db *DB) GetAPIKeyByHash(keyHash string) (*APIKey, error) {
}
func (db *DB) UpdateAPIKeyLastUsed(id string) error {
- _, err := db.Exec(db.Rebind(`UPDATE api_keys SET last_used_at = ? WHERE id = ?`), time.Now(), id)
+ _, err := db.Exec(`UPDATE api_keys SET last_used_at = $1 WHERE id = $2`, time.Now(), id)
return err
}
diff --git a/backend/internal/db/queries_likes.go b/backend/internal/db/queries_likes.go
index d38be4b..33b8b6a 100644
--- a/backend/internal/db/queries_likes.go
+++ b/backend/internal/db/queries_likes.go
@@ -1,26 +1,28 @@
package db
+import "fmt"
+
func (db *DB) CreateLike(l *Like) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO likes (uri, author_did, subject_uri, created_at, indexed_at)
- VALUES (?, ?, ?, ?, ?)
+ VALUES ($1, $2, $3, $4, $5)
ON CONFLICT(uri) DO NOTHING
- `), l.URI, l.AuthorDID, l.SubjectURI, l.CreatedAt, l.IndexedAt)
+ `, l.URI, l.AuthorDID, l.SubjectURI, l.CreatedAt, l.IndexedAt)
return err
}
func (db *DB) DeleteLike(uri string) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM likes WHERE uri = ?`), uri)
+ _, err := db.Exec(`DELETE FROM likes WHERE uri = $1`, uri)
return err
}
func (db *DB) GetLikesByAuthor(authorDID string) ([]Like, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, subject_uri, created_at, indexed_at
FROM likes
- WHERE author_did = ?
+ WHERE author_did = $1
ORDER BY created_at DESC
- `), authorDID)
+ `, authorDID)
if err != nil {
return nil, err
}
@@ -39,17 +41,17 @@ func (db *DB) GetLikesByAuthor(authorDID string) ([]Like, error) {
func (db *DB) GetLikeCount(subjectURI string) (int, error) {
var count int
- err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM likes WHERE subject_uri = ?`), subjectURI).Scan(&count)
+ err := db.QueryRow(`SELECT COUNT(*) FROM likes WHERE subject_uri = $1`, subjectURI).Scan(&count)
return count, err
}
func (db *DB) GetLikeByUserAndSubject(userDID, subjectURI string) (*Like, error) {
var like Like
- err := db.QueryRow(db.Rebind(`
+ err := db.QueryRow(`
SELECT uri, author_did, subject_uri, created_at, indexed_at
FROM likes
- WHERE author_did = ? AND subject_uri = ?
- `), userDID, subjectURI).Scan(&like.URI, &like.AuthorDID, &like.SubjectURI, &like.CreatedAt, &like.IndexedAt)
+ WHERE author_did = $1 AND subject_uri = $2
+ `, userDID, subjectURI).Scan(&like.URI, &like.AuthorDID, &like.SubjectURI, &like.CreatedAt, &like.IndexedAt)
if err != nil {
return nil, err
}
@@ -61,12 +63,12 @@ func (db *DB) GetLikeCounts(subjectURIs []string) (map[string]int, error) {
return map[string]int{}, nil
}
- query := db.Rebind(`
- SELECT subject_uri, COUNT(*)
- FROM likes
- WHERE subject_uri IN (` + buildPlaceholders(len(subjectURIs)) + `)
+ query := `
+ SELECT subject_uri, COUNT(*)
+ FROM likes
+ WHERE subject_uri IN (` + buildPlaceholders(len(subjectURIs), 1) + `)
GROUP BY subject_uri
- `)
+ `
args := make([]interface{}, len(subjectURIs))
for i, uri := range subjectURIs {
@@ -97,11 +99,11 @@ func (db *DB) GetViewerLikes(viewerDID string, subjectURIs []string) (map[string
return map[string]bool{}, nil
}
- query := db.Rebind(`
- SELECT subject_uri
- FROM likes
- WHERE author_did = ? AND subject_uri IN (` + buildPlaceholders(len(subjectURIs)) + `)
- `)
+ query := fmt.Sprintf(`
+ SELECT subject_uri
+ FROM likes
+ WHERE author_did = $1 AND subject_uri IN (%s)
+ `, buildPlaceholders(len(subjectURIs), 2))
args := make([]interface{}, len(subjectURIs)+1)
args[0] = viewerDID
diff --git a/backend/internal/db/queries_moderation.go b/backend/internal/db/queries_moderation.go
index ebb911c..b058394 100644
--- a/backend/internal/db/queries_moderation.go
+++ b/backend/internal/db/queries_moderation.go
@@ -1,21 +1,26 @@
package db
-import "time"
+import (
+ "fmt"
+ "strings"
+ "time"
+)
func (db *DB) CreateBlock(actorDID, subjectDID string) error {
- query := `INSERT INTO blocks (actor_did, subject_did, created_at) VALUES (?, ?, ?)
- ON CONFLICT(actor_did, subject_did) DO NOTHING`
- _, err := db.Exec(db.Rebind(query), actorDID, subjectDID, time.Now())
+ _, err := db.Exec(`
+ INSERT INTO blocks (actor_did, subject_did, created_at) VALUES ($1, $2, $3)
+ ON CONFLICT(actor_did, subject_did) DO NOTHING
+ `, actorDID, subjectDID, time.Now())
return err
}
func (db *DB) DeleteBlock(actorDID, subjectDID string) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM blocks WHERE actor_did = ? AND subject_did = ?`), actorDID, subjectDID)
+ _, err := db.Exec(`DELETE FROM blocks WHERE actor_did = $1 AND subject_did = $2`, actorDID, subjectDID)
return err
}
func (db *DB) GetBlocks(actorDID string) ([]Block, error) {
- rows, err := db.Query(db.Rebind(`SELECT id, actor_did, subject_did, created_at FROM blocks WHERE actor_did = ? ORDER BY created_at DESC`), actorDID)
+ rows, err := db.Query(`SELECT id, actor_did, subject_did, created_at FROM blocks WHERE actor_did = $1 ORDER BY created_at DESC`, actorDID)
if err != nil {
return nil, err
}
@@ -33,19 +38,19 @@ func (db *DB) GetBlocks(actorDID string) ([]Block, error) {
}
func (db *DB) IsBlocked(actorDID, subjectDID string) (bool, error) {
- var count int
- err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM blocks WHERE actor_did = ? AND subject_did = ?`), actorDID, subjectDID).Scan(&count)
- return count > 0, err
+ var exists bool
+ err := db.QueryRow(`SELECT EXISTS(SELECT 1 FROM blocks WHERE actor_did = $1 AND subject_did = $2)`, actorDID, subjectDID).Scan(&exists)
+ return exists, err
}
func (db *DB) IsBlockedEither(did1, did2 string) (bool, error) {
- var count int
- err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM blocks WHERE (actor_did = ? AND subject_did = ?) OR (actor_did = ? AND subject_did = ?)`), did1, did2, did2, did1).Scan(&count)
- return count > 0, err
+ var exists bool
+ err := db.QueryRow(`SELECT EXISTS(SELECT 1 FROM blocks WHERE (actor_did = $1 AND subject_did = $2) OR (actor_did = $2 AND subject_did = $1))`, did1, did2).Scan(&exists)
+ return exists, err
}
func (db *DB) GetBlockedDIDs(actorDID string) ([]string, error) {
- rows, err := db.Query(db.Rebind(`SELECT subject_did FROM blocks WHERE actor_did = ?`), actorDID)
+ rows, err := db.Query(`SELECT subject_did FROM blocks WHERE actor_did = $1`, actorDID)
if err != nil {
return nil, err
}
@@ -63,7 +68,7 @@ func (db *DB) GetBlockedDIDs(actorDID string) ([]string, error) {
}
func (db *DB) GetBlockedByDIDs(actorDID string) ([]string, error) {
- rows, err := db.Query(db.Rebind(`SELECT actor_did FROM blocks WHERE subject_did = ?`), actorDID)
+ rows, err := db.Query(`SELECT actor_did FROM blocks WHERE subject_did = $1`, actorDID)
if err != nil {
return nil, err
}
@@ -81,19 +86,20 @@ func (db *DB) GetBlockedByDIDs(actorDID string) ([]string, error) {
}
func (db *DB) CreateMute(actorDID, subjectDID string) error {
- query := `INSERT INTO mutes (actor_did, subject_did, created_at) VALUES (?, ?, ?)
- ON CONFLICT(actor_did, subject_did) DO NOTHING`
- _, err := db.Exec(db.Rebind(query), actorDID, subjectDID, time.Now())
+ _, err := db.Exec(`
+ INSERT INTO mutes (actor_did, subject_did, created_at) VALUES ($1, $2, $3)
+ ON CONFLICT(actor_did, subject_did) DO NOTHING
+ `, actorDID, subjectDID, time.Now())
return err
}
func (db *DB) DeleteMute(actorDID, subjectDID string) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM mutes WHERE actor_did = ? AND subject_did = ?`), actorDID, subjectDID)
+ _, err := db.Exec(`DELETE FROM mutes WHERE actor_did = $1 AND subject_did = $2`, actorDID, subjectDID)
return err
}
func (db *DB) GetMutes(actorDID string) ([]Mute, error) {
- rows, err := db.Query(db.Rebind(`SELECT id, actor_did, subject_did, created_at FROM mutes WHERE actor_did = ? ORDER BY created_at DESC`), actorDID)
+ rows, err := db.Query(`SELECT id, actor_did, subject_did, created_at FROM mutes WHERE actor_did = $1 ORDER BY created_at DESC`, actorDID)
if err != nil {
return nil, err
}
@@ -111,13 +117,13 @@ func (db *DB) GetMutes(actorDID string) ([]Mute, error) {
}
func (db *DB) IsMuted(actorDID, subjectDID string) (bool, error) {
- var count int
- err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM mutes WHERE actor_did = ? AND subject_did = ?`), actorDID, subjectDID).Scan(&count)
- return count > 0, err
+ var exists bool
+ err := db.QueryRow(`SELECT EXISTS(SELECT 1 FROM mutes WHERE actor_did = $1 AND subject_did = $2)`, actorDID, subjectDID).Scan(&exists)
+ return exists, err
}
func (db *DB) GetMutedDIDs(actorDID string) ([]string, error) {
- rows, err := db.Query(db.Rebind(`SELECT subject_did FROM mutes WHERE actor_did = ?`), actorDID)
+ rows, err := db.Query(`SELECT subject_did FROM mutes WHERE actor_did = $1`, actorDID)
if err != nil {
return nil, err
}
@@ -187,32 +193,31 @@ func (db *DB) GetViewerRelationship(viewerDID, subjectDID string) (blocked bool,
}
func (db *DB) CreateReport(reporterDID, subjectDID string, subjectURI *string, reasonType string, reasonText *string) (int, error) {
- query := `INSERT INTO moderation_reports (reporter_did, subject_did, subject_uri, reason_type, reason_text, status, created_at)
- VALUES (?, ?, ?, ?, ?, 'pending', ?)`
-
- result, err := db.Exec(db.Rebind(query), reporterDID, subjectDID, subjectURI, reasonType, reasonText, time.Now())
- if err != nil {
- return 0, err
- }
-
- id, err := result.LastInsertId()
- return int(id), err
+ var id int
+ err := db.QueryRow(`
+ INSERT INTO moderation_reports (reporter_did, subject_did, subject_uri, reason_type, reason_text, status, created_at)
+ VALUES ($1, $2, $3, $4, $5, 'pending', $6)
+ RETURNING id
+ `, reporterDID, subjectDID, subjectURI, reasonType, reasonText, time.Now()).Scan(&id)
+ return id, err
}
func (db *DB) GetReports(status string, limit, offset int) ([]ModerationReport, error) {
query := `SELECT id, reporter_did, subject_did, subject_uri, reason_type, reason_text, status, created_at, resolved_at, resolved_by
FROM moderation_reports`
args := []interface{}{}
+ paramIdx := 1
if status != "" {
- query += ` WHERE status = ?`
+ query += ` WHERE status = $1`
args = append(args, status)
+ paramIdx = 2
}
- query += ` ORDER BY created_at DESC LIMIT ? OFFSET ?`
+ query += ` ORDER BY created_at DESC LIMIT $` + itoa(paramIdx) + ` OFFSET $` + itoa(paramIdx+1)
args = append(args, limit, offset)
- rows, err := db.Query(db.Rebind(query), args...)
+ rows, err := db.Query(query, args...)
if err != nil {
return nil, err
}
@@ -231,7 +236,7 @@ func (db *DB) GetReports(status string, limit, offset int) ([]ModerationReport,
func (db *DB) GetReport(id int) (*ModerationReport, error) {
var r ModerationReport
- err := db.QueryRow(db.Rebind(`SELECT id, reporter_did, subject_did, subject_uri, reason_type, reason_text, status, created_at, resolved_at, resolved_by FROM moderation_reports WHERE id = ?`), id).Scan(
+ err := db.QueryRow(`SELECT id, reporter_did, subject_did, subject_uri, reason_type, reason_text, status, created_at, resolved_at, resolved_by FROM moderation_reports WHERE id = $1`, id).Scan(
&r.ID, &r.ReporterDID, &r.SubjectDID, &r.SubjectURI, &r.ReasonType, &r.ReasonText, &r.Status, &r.CreatedAt, &r.ResolvedAt, &r.ResolvedBy,
)
if err != nil {
@@ -241,18 +246,17 @@ func (db *DB) GetReport(id int) (*ModerationReport, error) {
}
func (db *DB) ResolveReport(id int, resolvedBy string, status string) error {
- _, err := db.Exec(db.Rebind(`UPDATE moderation_reports SET status = ?, resolved_at = ?, resolved_by = ? WHERE id = ?`), status, time.Now(), resolvedBy, id)
+ _, err := db.Exec(`UPDATE moderation_reports SET status = $1, resolved_at = $2, resolved_by = $3 WHERE id = $4`, status, time.Now(), resolvedBy, id)
return err
}
func (db *DB) CreateModerationAction(reportID int, actorDID, action string, comment *string) error {
- query := `INSERT INTO moderation_actions (report_id, actor_did, action, comment, created_at) VALUES (?, ?, ?, ?, ?)`
- _, err := db.Exec(db.Rebind(query), reportID, actorDID, action, comment, time.Now())
+ _, err := db.Exec(`INSERT INTO moderation_actions (report_id, actor_did, action, comment, created_at) VALUES ($1, $2, $3, $4, $5)`, reportID, actorDID, action, comment, time.Now())
return err
}
func (db *DB) GetReportActions(reportID int) ([]ModerationAction, error) {
- rows, err := db.Query(db.Rebind(`SELECT id, report_id, actor_did, action, comment, created_at FROM moderation_actions WHERE report_id = ? ORDER BY created_at DESC`), reportID)
+ rows, err := db.Query(`SELECT id, report_id, actor_did, action, comment, created_at FROM moderation_actions WHERE report_id = $1 ORDER BY created_at DESC`, reportID)
if err != nil {
return nil, err
}
@@ -273,22 +277,21 @@ func (db *DB) GetReportCount(status string) (int, error) {
query := `SELECT COUNT(*) FROM moderation_reports`
args := []interface{}{}
if status != "" {
- query += ` WHERE status = ?`
+ query += ` WHERE status = $1`
args = append(args, status)
}
var count int
- err := db.QueryRow(db.Rebind(query), args...).Scan(&count)
+ err := db.QueryRow(query, args...).Scan(&count)
return count, err
}
func (db *DB) CreateContentLabel(src, uri, val, createdBy string) error {
- query := `INSERT INTO content_labels (src, uri, val, neg, created_by, created_at) VALUES (?, ?, ?, 0, ?, ?)`
- _, err := db.Exec(db.Rebind(query), src, uri, val, createdBy, time.Now())
+ _, err := db.Exec(`INSERT INTO content_labels (src, uri, val, neg, created_by, created_at) VALUES ($1, $2, $3, 0, $4, $5)`, src, uri, val, createdBy, time.Now())
return err
}
func (db *DB) SyncSelfLabels(authorDID, uri string, labels []string) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM content_labels WHERE src = ? AND uri = ? AND created_by = ?`), authorDID, uri, authorDID)
+ _, err := db.Exec(`DELETE FROM content_labels WHERE src = $1 AND uri = $2 AND created_by = $3`, authorDID, uri, authorDID)
if err != nil {
return err
}
@@ -301,12 +304,12 @@ func (db *DB) SyncSelfLabels(authorDID, uri string, labels []string) error {
}
func (db *DB) NegateContentLabel(id int) error {
- _, err := db.Exec(db.Rebind(`UPDATE content_labels SET neg = 1 WHERE id = ?`), id)
+ _, err := db.Exec(`UPDATE content_labels SET neg = 1 WHERE id = $1`, id)
return err
}
func (db *DB) DeleteContentLabel(id int) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM content_labels WHERE id = ?`), id)
+ _, err := db.Exec(`DELETE FROM content_labels WHERE id = $1`, id)
return err
}
@@ -316,28 +319,18 @@ func (db *DB) GetContentLabelsForURIs(uris []string, labelerDIDs []string) (map[
return result, nil
}
- placeholders := make([]string, len(uris))
- args := make([]interface{}, len(uris))
- for i, uri := range uris {
- placeholders[i] = "?"
- args[i] = uri
- }
-
query := `SELECT id, src, uri, val, neg, created_by, created_at FROM content_labels
- WHERE uri IN (` + joinStrings(placeholders, ",") + `) AND neg = 0`
+ WHERE uri = ANY($1) AND neg = 0`
+ args := []interface{}{pqStringArray(uris)}
if len(labelerDIDs) > 0 {
- srcPlaceholders := make([]string, len(labelerDIDs))
- for i, did := range labelerDIDs {
- srcPlaceholders[i] = "?"
- args = append(args, did)
- }
- query += ` AND src IN (` + joinStrings(srcPlaceholders, ",") + `)`
+ query += ` AND src = ANY($2)`
+ args = append(args, pqStringArray(labelerDIDs))
}
query += ` ORDER BY created_at DESC`
- rows, err := db.Query(db.Rebind(query), args...)
+ rows, err := db.Query(query, args...)
if err != nil {
return result, err
}
@@ -359,28 +352,18 @@ func (db *DB) GetContentLabelsForDIDs(dids []string, labelerDIDs []string) (map[
return result, nil
}
- placeholders := make([]string, len(dids))
- args := make([]interface{}, len(dids))
- for i, did := range dids {
- placeholders[i] = "?"
- args[i] = did
- }
-
query := `SELECT id, src, uri, val, neg, created_by, created_at FROM content_labels
- WHERE uri IN (` + joinStrings(placeholders, ",") + `) AND neg = 0`
+ WHERE uri = ANY($1) AND neg = 0`
+ args := []interface{}{pqStringArray(dids)}
if len(labelerDIDs) > 0 {
- srcPlaceholders := make([]string, len(labelerDIDs))
- for i, did := range labelerDIDs {
- srcPlaceholders[i] = "?"
- args = append(args, did)
- }
- query += ` AND src IN (` + joinStrings(srcPlaceholders, ",") + `)`
+ query += ` AND src = ANY($2)`
+ args = append(args, pqStringArray(labelerDIDs))
}
query += ` ORDER BY created_at DESC`
- rows, err := db.Query(db.Rebind(query), args...)
+ rows, err := db.Query(query, args...)
if err != nil {
return result, err
}
@@ -397,7 +380,7 @@ func (db *DB) GetContentLabelsForDIDs(dids []string, labelerDIDs []string) (map[
}
func (db *DB) GetAllContentLabels(limit, offset int) ([]ContentLabel, error) {
- rows, err := db.Query(db.Rebind(`SELECT id, src, uri, val, neg, created_by, created_at FROM content_labels ORDER BY created_at DESC LIMIT ? OFFSET ?`), limit, offset)
+ rows, err := db.Query(`SELECT id, src, uri, val, neg, created_by, created_at FROM content_labels ORDER BY created_at DESC LIMIT $1 OFFSET $2`, limit, offset)
if err != nil {
return nil, err
}
@@ -414,13 +397,6 @@ func (db *DB) GetAllContentLabels(limit, offset int) ([]ContentLabel, error) {
return labels, nil
}
-func joinStrings(strs []string, sep string) string {
- result := ""
- for i, s := range strs {
- if i > 0 {
- result += sep
- }
- result += s
- }
- return result
+func itoa(i int) string {
+ return strings.Repeat("", 0) + fmt.Sprintf("%d", i)
}
diff --git a/backend/internal/db/queries_notifications.go b/backend/internal/db/queries_notifications.go
index 1e72f42..c5cc4b9 100644
--- a/backend/internal/db/queries_notifications.go
+++ b/backend/internal/db/queries_notifications.go
@@ -5,21 +5,21 @@ import (
)
func (db *DB) CreateNotification(n *Notification) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO notifications (recipient_did, actor_did, type, subject_uri, created_at)
- VALUES (?, ?, ?, ?, ?)
- `), n.RecipientDID, n.ActorDID, n.Type, n.SubjectURI, n.CreatedAt)
+ VALUES ($1, $2, $3, $4, $5)
+ `, n.RecipientDID, n.ActorDID, n.Type, n.SubjectURI, n.CreatedAt)
return err
}
func (db *DB) GetNotifications(recipientDID string, limit, offset int) ([]Notification, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT id, recipient_did, actor_did, type, subject_uri, created_at, read_at
FROM notifications
- WHERE recipient_did = ?
+ WHERE recipient_did = $1
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `), recipientDID, limit, offset)
+ LIMIT $2 OFFSET $3
+ `, recipientDID, limit, offset)
if err != nil {
return nil, err
}
@@ -38,15 +38,15 @@ func (db *DB) GetNotifications(recipientDID string, limit, offset int) ([]Notifi
func (db *DB) GetUnreadNotificationCount(recipientDID string) (int, error) {
var count int
- err := db.QueryRow(db.Rebind(`
- SELECT COUNT(*) FROM notifications WHERE recipient_did = ? AND read_at IS NULL
- `), recipientDID).Scan(&count)
+ err := db.QueryRow(`
+ SELECT COUNT(*) FROM notifications WHERE recipient_did = $1 AND read_at IS NULL
+ `, recipientDID).Scan(&count)
return count, err
}
func (db *DB) MarkNotificationsRead(recipientDID string) error {
- _, err := db.Exec(db.Rebind(`
- UPDATE notifications SET read_at = ? WHERE recipient_did = ? AND read_at IS NULL
- `), time.Now(), recipientDID)
+ _, err := db.Exec(`
+ UPDATE notifications SET read_at = $1 WHERE recipient_did = $2 AND read_at IS NULL
+ `, time.Now(), recipientDID)
return err
}
diff --git a/backend/internal/db/queries_recommendations.go b/backend/internal/db/queries_recommendations.go
index 32b7a69..b5b4829 100644
--- a/backend/internal/db/queries_recommendations.go
+++ b/backend/internal/db/queries_recommendations.go
@@ -55,16 +55,11 @@ type UserProfile struct {
}
func (db *DB) MigrateRecommendations() error {
- dateType := "TIMESTAMP"
- if db.driver == "sqlite3" {
- dateType = "DATETIME"
- }
-
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS document_embeddings (
document_uri TEXT PRIMARY KEY,
embedding TEXT NOT NULL,
- updated_at ` + dateType + ` NOT NULL
+ updated_at TIMESTAMP NOT NULL
)`)
if err != nil {
return fmt.Errorf("create document_embeddings table: %w", err)
@@ -76,7 +71,7 @@ func (db *DB) MigrateRecommendations() error {
author_did TEXT NOT NULL,
document_uri TEXT,
embedding TEXT NOT NULL,
- updated_at ` + dateType + ` NOT NULL
+ updated_at TIMESTAMP NOT NULL
)`)
if err != nil {
return fmt.Errorf("create annotation_embeddings table: %w", err)
@@ -90,7 +85,7 @@ func (db *DB) MigrateRecommendations() error {
embedding TEXT NOT NULL,
tag_affinities TEXT DEFAULT '{}',
annotation_count INTEGER NOT NULL DEFAULT 0,
- updated_at ` + dateType + ` NOT NULL
+ updated_at TIMESTAMP NOT NULL
)`)
if err != nil {
return fmt.Errorf("create user_profiles table: %w", err)
@@ -154,7 +149,7 @@ func (db *DB) DeleteDocument(uri string) error {
func (db *DB) GetDocumentByCanonicalURL(canonicalURL string) (*Document, error) {
var d Document
err := db.QueryRow(
- `SELECT uri, author_did, site, path, title, description, text_content, tags_json, canonical_url, published_at, indexed_at
+ `SELECT uri, author_did, site, path, title, description, text_content, tags_json, canonical_url, published_at, indexed_at
FROM documents WHERE canonical_url = $1`,
canonicalURL,
).Scan(&d.URI, &d.AuthorDID, &d.Site, &d.Path, &d.Title, &d.Description, &d.TextContent, &d.TagsJSON, &d.CanonicalURL, &d.PublishedAt, &d.IndexedAt)
@@ -167,7 +162,7 @@ func (db *DB) GetDocumentByCanonicalURL(canonicalURL string) (*Document, error)
func (db *DB) GetDocumentByURI(uri string) (*Document, error) {
var d Document
err := db.QueryRow(
- `SELECT uri, author_did, site, path, title, description, text_content, tags_json, canonical_url, published_at, indexed_at
+ `SELECT uri, author_did, site, path, title, description, text_content, tags_json, canonical_url, published_at, indexed_at
FROM documents WHERE uri = $1`,
uri,
).Scan(&d.URI, &d.AuthorDID, &d.Site, &d.Path, &d.Title, &d.Description, &d.TextContent, &d.TagsJSON, &d.CanonicalURL, &d.PublishedAt, &d.IndexedAt)
@@ -178,14 +173,14 @@ func (db *DB) GetDocumentByURI(uri string) (*Document, error) {
}
func (db *DB) GetDocumentsWithoutEmbeddings(limit int) ([]Document, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT d.uri, d.author_did, d.site, d.path, d.title, d.description, d.text_content, d.tags_json, d.canonical_url, d.published_at, d.indexed_at
FROM documents d
LEFT JOIN document_embeddings de ON d.uri = de.document_uri
WHERE de.document_uri IS NULL
ORDER BY d.indexed_at DESC
- LIMIT ?
- `), limit)
+ LIMIT $1
+ `, limit)
if err != nil {
return nil, err
}
@@ -194,14 +189,14 @@ func (db *DB) GetDocumentsWithoutEmbeddings(limit int) ([]Document, error) {
}
func (db *DB) GetAnnotationsWithoutEmbeddings(limit int) ([]Annotation, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT a.uri, a.author_did, a.motivation, a.body_value, a.body_format, a.body_uri, a.target_source, a.target_hash, a.target_title, a.selector_json, a.tags_json, a.created_at, a.indexed_at, a.cid
FROM annotations a
LEFT JOIN annotation_embeddings ae ON a.uri = ae.annotation_uri
WHERE ae.annotation_uri IS NULL AND a.motivation IN ('commenting', 'highlighting')
ORDER BY a.created_at DESC
- LIMIT ?
- `), limit)
+ LIMIT $1
+ `, limit)
if err != nil {
return nil, err
}
@@ -219,14 +214,14 @@ type HighlightForEmbedding struct {
}
func (db *DB) GetHighlightsWithoutEmbeddings(limit int) ([]HighlightForEmbedding, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT h.uri, h.author_did, h.target_source, h.target_title, h.selector_json, h.tags_json
FROM highlights h
LEFT JOIN annotation_embeddings ae ON h.uri = ae.annotation_uri
WHERE ae.annotation_uri IS NULL
ORDER BY h.created_at DESC
- LIMIT ?
- `), limit)
+ LIMIT $1
+ `, limit)
if err != nil {
return nil, err
}
@@ -276,12 +271,12 @@ func scanDocuments(rows interface {
}
func (db *DB) GetRecentDocuments(limit, offset int) ([]Document, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, site, path, title, description, text_content, tags_json, canonical_url, published_at, indexed_at
FROM documents
ORDER BY published_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
@@ -290,14 +285,14 @@ func (db *DB) GetRecentDocuments(limit, offset int) ([]Document, error) {
}
func (db *DB) GetPopularDocuments(limit, offset int) ([]Document, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT d.uri, d.author_did, d.site, d.path, d.title, d.description, d.text_content, d.tags_json, d.canonical_url, d.published_at, d.indexed_at
FROM documents d
LEFT JOIN annotations a ON a.target_source = d.canonical_url
GROUP BY d.uri
ORDER BY COUNT(a.uri) DESC, d.published_at DESC
- LIMIT ? OFFSET ?
- `), limit, offset)
+ LIMIT $1 OFFSET $2
+ `, limit, offset)
if err != nil {
return nil, err
}
@@ -386,7 +381,7 @@ func (db *DB) GetAnnotationEmbeddingsByAuthor(authorDID string) ([]AnnotationEmb
func (db *DB) GetRecentAnnotationEmbeddingsByAuthor(authorDID string, limit int) ([]AnnotationEmbedding, error) {
rows, err := db.Query(
- db.Rebind(`SELECT annotation_uri, author_did, document_uri, embedding, updated_at FROM annotation_embeddings WHERE author_did = ? ORDER BY updated_at DESC LIMIT ?`),
+ `SELECT annotation_uri, author_did, document_uri, embedding, updated_at FROM annotation_embeddings WHERE author_did = $1 ORDER BY updated_at DESC LIMIT $2`,
authorDID, limit,
)
if err != nil {
@@ -422,11 +417,8 @@ type CandidateDocument struct {
}
func (db *DB) GetCandidateDocuments(userDID string, limit int) ([]CandidateDocument, error) {
- // Note: We use NOT LIKE instead of !~* for cross-database compatibility and performance.
- // The engagement count sub-select is also constrained to recent elements if possible, but
- // for now we just optimize the regex and exact grouping.
- rows, err := db.Query(db.Rebind(`
- SELECT
+ rows, err := db.Query(`
+ SELECT
d.uri, d.author_did, d.site, d.path, d.title, d.description, d.tags_json,
d.canonical_url, d.published_at, de.embedding,
COALESCE(eng.cnt, 0) AS engagement
@@ -439,7 +431,7 @@ func (db *DB) GetCandidateDocuments(userDID string, limit int) ([]CandidateDocum
GROUP BY document_uri
) eng ON eng.document_uri = d.uri
LEFT JOIN publications p ON d.site = p.uri OR d.site = p.url
- WHERE d.author_did != ?
+ WHERE d.author_did != $1
AND (p.show_in_discover IS NULL OR p.show_in_discover = true)
AND LENGTH(d.title) > 15
AND (LENGTH(COALESCE(d.description, '')) >= 30 OR LENGTH(COALESCE(d.text_content, '')) >= 100)
@@ -453,11 +445,11 @@ func (db *DB) GetCandidateDocuments(userDID string, limit int) ([]CandidateDocum
AND LOWER(d.title) NOT LIKE '%placeholder%'
AND d.uri NOT IN (
SELECT DISTINCT document_uri FROM annotation_embeddings
- WHERE author_did = ? AND document_uri IS NOT NULL
+ WHERE author_did = $2 AND document_uri IS NOT NULL
)
ORDER BY d.published_at DESC
- LIMIT ?
- `), userDID, userDID, limit)
+ LIMIT $3
+ `, userDID, userDID, limit)
if err != nil {
return nil, fmt.Errorf("candidate query: %w", err)
}
diff --git a/backend/internal/db/queries_replies.go b/backend/internal/db/queries_replies.go
index d528bce..7f154f9 100644
--- a/backend/internal/db/queries_replies.go
+++ b/backend/internal/db/queries_replies.go
@@ -1,48 +1,40 @@
package db
func (db *DB) CreateReply(r *Reply) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO replies (uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT(uri) DO UPDATE SET
- text = excluded.text,
- format = excluded.format,
- indexed_at = excluded.indexed_at,
- cid = excluded.cid
- `), r.URI, r.AuthorDID, r.ParentURI, r.RootURI, r.Text, r.Format, r.CreatedAt, r.IndexedAt, r.CID)
+ text = EXCLUDED.text,
+ format = EXCLUDED.format,
+ indexed_at = EXCLUDED.indexed_at,
+ cid = EXCLUDED.cid
+ `, r.URI, r.AuthorDID, r.ParentURI, r.RootURI, r.Text, r.Format, r.CreatedAt, r.IndexedAt, r.CID)
return err
}
func (db *DB) GetRepliesByRoot(rootURI string) ([]Reply, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid
FROM replies
- WHERE root_uri = ?
+ WHERE root_uri = $1
ORDER BY created_at ASC
- `), rootURI)
+ `, rootURI)
if err != nil {
return nil, err
}
defer rows.Close()
- var replies []Reply
- for rows.Next() {
- var r Reply
- if err := rows.Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID); err != nil {
- return nil, err
- }
- replies = append(replies, r)
- }
- return replies, nil
+ return scanReplies(rows)
}
func (db *DB) GetReplyByURI(uri string) (*Reply, error) {
var r Reply
- err := db.QueryRow(db.Rebind(`
+ err := db.QueryRow(`
SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid
FROM replies
- WHERE uri = ?
- `), uri).Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID)
+ WHERE uri = $1
+ `, uri).Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID)
if err != nil {
return nil, err
}
@@ -50,59 +42,43 @@ func (db *DB) GetReplyByURI(uri string) (*Reply, error) {
}
func (db *DB) DeleteReply(uri string) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM replies WHERE uri = ?`), uri)
+ _, err := db.Exec(`DELETE FROM replies WHERE uri = $1`, uri)
return err
}
func (db *DB) GetRepliesByAuthor(authorDID string) ([]Reply, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid
FROM replies
- WHERE author_did = ?
+ WHERE author_did = $1
ORDER BY created_at DESC
- `), authorDID)
+ `, authorDID)
if err != nil {
return nil, err
}
defer rows.Close()
- var replies []Reply
- for rows.Next() {
- var r Reply
- if err := rows.Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID); err != nil {
- return nil, err
- }
- replies = append(replies, r)
- }
- return replies, nil
+ return scanReplies(rows)
}
func (db *DB) GetOrphanedRepliesByAuthor(authorDID string) ([]Reply, error) {
- rows, err := db.Query(db.Rebind(`
+ rows, err := db.Query(`
SELECT r.uri, r.author_did, r.parent_uri, r.root_uri, r.text, r.format, r.created_at, r.indexed_at, r.cid
FROM replies r
LEFT JOIN annotations a ON r.root_uri = a.uri
- WHERE r.author_did = ? AND a.uri IS NULL
- `), authorDID)
+ WHERE r.author_did = $1 AND a.uri IS NULL
+ `, authorDID)
if err != nil {
return nil, err
}
defer rows.Close()
- var replies []Reply
- for rows.Next() {
- var r Reply
- if err := rows.Scan(&r.URI, &r.AuthorDID, &r.ParentURI, &r.RootURI, &r.Text, &r.Format, &r.CreatedAt, &r.IndexedAt, &r.CID); err != nil {
- return nil, err
- }
- replies = append(replies, r)
- }
- return replies, nil
+ return scanReplies(rows)
}
func (db *DB) GetReplyCount(rootURI string) (int, error) {
var count int
- err := db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM replies WHERE root_uri = ?`), rootURI).Scan(&count)
+ err := db.QueryRow(`SELECT COUNT(*) FROM replies WHERE root_uri = $1`, rootURI).Scan(&count)
return count, err
}
@@ -111,19 +87,14 @@ func (db *DB) GetReplyCounts(rootURIs []string) (map[string]int, error) {
return map[string]int{}, nil
}
- query := db.Rebind(`
- SELECT root_uri, COUNT(*)
- FROM replies
- WHERE root_uri IN (` + buildPlaceholders(len(rootURIs)) + `)
+ query := `
+ SELECT root_uri, COUNT(*)
+ FROM replies
+ WHERE root_uri = ANY($1)
GROUP BY root_uri
- `)
-
- args := make([]interface{}, len(rootURIs))
- for i, uri := range rootURIs {
- args[i] = uri
- }
+ `
- rows, err := db.Query(query, args...)
+ rows, err := db.Query(query, pqStringArray(rootURIs))
if err != nil {
return nil, err
}
@@ -147,23 +118,23 @@ func (db *DB) GetRepliesByURIs(uris []string) ([]Reply, error) {
return []Reply{}, nil
}
- query := db.Rebind(`
+ rows, err := db.Query(`
SELECT uri, author_did, parent_uri, root_uri, text, format, created_at, indexed_at, cid
FROM replies
- WHERE uri IN (` + buildPlaceholders(len(uris)) + `)
- `)
-
- args := make([]interface{}, len(uris))
- for i, uri := range uris {
- args[i] = uri
- }
-
- rows, err := db.Query(query, args...)
+ WHERE uri = ANY($1)
+ `, pqStringArray(uris))
if err != nil {
return nil, err
}
defer rows.Close()
+ return scanReplies(rows)
+}
+
+func scanReplies(rows interface {
+ Next() bool
+ Scan(...interface{}) error
+}) ([]Reply, error) {
var replies []Reply
for rows.Next() {
var r Reply
diff --git a/backend/internal/db/queries_search.go b/backend/internal/db/queries_search.go
index fa74ff1..0c89060 100644
--- a/backend/internal/db/queries_search.go
+++ b/backend/internal/db/queries_search.go
@@ -1,29 +1,38 @@
package db
+import "strings"
+
+func escapeLike(s string) string {
+ s = strings.ReplaceAll(s, "\\", "\\\\")
+ s = strings.ReplaceAll(s, "%", "\\%")
+ s = strings.ReplaceAll(s, "_", "\\_")
+ return s
+}
+
func (db *DB) SearchAnnotations(query string, authorDID string, limit, offset int) ([]Annotation, error) {
- pattern := "%" + query + "%"
+ pattern := "%" + escapeLike(query) + "%"
var baseQuery string
var args []interface{}
if authorDID != "" {
- baseQuery = db.Rebind(`
+ baseQuery = `
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE author_did = ?
- AND (body_value LIKE ? OR target_source LIKE ? OR target_title LIKE ? OR tags_json LIKE ? OR selector_json LIKE ?)
+ WHERE author_did = $1
+ AND (body_value ILIKE $2 OR target_source ILIKE $3 OR target_title ILIKE $4 OR tags_json ILIKE $5 OR selector_json ILIKE $6)
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `)
+ LIMIT $7 OFFSET $8
+ `
args = []interface{}{authorDID, pattern, pattern, pattern, pattern, pattern, limit, offset}
} else {
- baseQuery = db.Rebind(`
+ baseQuery = `
SELECT uri, author_did, motivation, body_value, body_format, body_uri, target_source, target_hash, target_title, selector_json, tags_json, created_at, indexed_at, cid
FROM annotations
- WHERE body_value LIKE ? OR target_source LIKE ? OR target_title LIKE ? OR tags_json LIKE ? OR selector_json LIKE ?
+ WHERE body_value ILIKE $1 OR target_source ILIKE $2 OR target_title ILIKE $3 OR tags_json ILIKE $4 OR selector_json ILIKE $5
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `)
+ LIMIT $6 OFFSET $7
+ `
args = []interface{}{pattern, pattern, pattern, pattern, pattern, limit, offset}
}
@@ -37,29 +46,29 @@ func (db *DB) SearchAnnotations(query string, authorDID string, limit, offset in
}
func (db *DB) SearchHighlights(query string, authorDID string, limit, offset int) ([]Highlight, error) {
- pattern := "%" + query + "%"
+ pattern := "%" + escapeLike(query) + "%"
var baseQuery string
var args []interface{}
if authorDID != "" {
- baseQuery = db.Rebind(`
+ baseQuery = `
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE author_did = ?
- AND (target_source LIKE ? OR target_title LIKE ? OR selector_json LIKE ? OR tags_json LIKE ?)
+ WHERE author_did = $1
+ AND (target_source ILIKE $2 OR target_title ILIKE $3 OR selector_json ILIKE $4 OR tags_json ILIKE $5)
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `)
+ LIMIT $6 OFFSET $7
+ `
args = []interface{}{authorDID, pattern, pattern, pattern, pattern, limit, offset}
} else {
- baseQuery = db.Rebind(`
+ baseQuery = `
SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
FROM highlights
- WHERE target_source LIKE ? OR target_title LIKE ? OR selector_json LIKE ? OR tags_json LIKE ?
+ WHERE target_source ILIKE $1 OR target_title ILIKE $2 OR selector_json ILIKE $3 OR tags_json ILIKE $4
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `)
+ LIMIT $5 OFFSET $6
+ `
args = []interface{}{pattern, pattern, pattern, pattern, limit, offset}
}
@@ -69,41 +78,33 @@ func (db *DB) SearchHighlights(query string, authorDID string, limit, offset int
}
defer rows.Close()
- var highlights []Highlight
- for rows.Next() {
- var h Highlight
- if err := rows.Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID); err != nil {
- return nil, err
- }
- highlights = append(highlights, h)
- }
- return highlights, nil
+ return scanHighlights(rows)
}
func (db *DB) SearchBookmarks(query string, authorDID string, limit, offset int) ([]Bookmark, error) {
- pattern := "%" + query + "%"
+ pattern := "%" + escapeLike(query) + "%"
var baseQuery string
var args []interface{}
if authorDID != "" {
- baseQuery = db.Rebind(`
+ baseQuery = `
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE author_did = ?
- AND (source LIKE ? OR title LIKE ? OR description LIKE ? OR tags_json LIKE ?)
+ WHERE author_did = $1
+ AND (source ILIKE $2 OR title ILIKE $3 OR description ILIKE $4 OR tags_json ILIKE $5)
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `)
+ LIMIT $6 OFFSET $7
+ `
args = []interface{}{authorDID, pattern, pattern, pattern, pattern, limit, offset}
} else {
- baseQuery = db.Rebind(`
+ baseQuery = `
SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
FROM bookmarks
- WHERE source LIKE ? OR title LIKE ? OR description LIKE ? OR tags_json LIKE ?
+ WHERE source ILIKE $1 OR title ILIKE $2 OR description ILIKE $3 OR tags_json ILIKE $4
ORDER BY created_at DESC
- LIMIT ? OFFSET ?
- `)
+ LIMIT $5 OFFSET $6
+ `
args = []interface{}{pattern, pattern, pattern, pattern, limit, offset}
}
@@ -113,13 +114,5 @@ func (db *DB) SearchBookmarks(query string, authorDID string, limit, offset int)
}
defer rows.Close()
- var bookmarks []Bookmark
- for rows.Next() {
- var b Bookmark
- if err := rows.Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID); err != nil {
- return nil, err
- }
- bookmarks = append(bookmarks, b)
- }
- return bookmarks, nil
+ return scanBookmarks(rows)
}
diff --git a/backend/internal/db/queries_sessions.go b/backend/internal/db/queries_sessions.go
index d3d9a57..f5cd8f1 100644
--- a/backend/internal/db/queries_sessions.go
+++ b/backend/internal/db/queries_sessions.go
@@ -5,33 +5,33 @@ import (
)
func (db *DB) SaveSession(id, did, handle, accessToken, refreshToken, dpopKey string, expiresAt time.Time) error {
- _, err := db.Exec(db.Rebind(`
+ _, err := db.Exec(`
INSERT INTO sessions (id, did, handle, access_token, refresh_token, dpop_key, created_at, expires_at)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT(id) DO UPDATE SET
- access_token = excluded.access_token,
- refresh_token = excluded.refresh_token,
- dpop_key = excluded.dpop_key,
- expires_at = excluded.expires_at
- `), id, did, handle, accessToken, refreshToken, dpopKey, time.Now(), expiresAt)
+ access_token = EXCLUDED.access_token,
+ refresh_token = EXCLUDED.refresh_token,
+ dpop_key = EXCLUDED.dpop_key,
+ expires_at = EXCLUDED.expires_at
+ `, id, did, handle, accessToken, refreshToken, dpopKey, time.Now(), expiresAt)
return err
}
func (db *DB) GetSession(id string) (did, handle, accessToken, refreshToken, dpopKey string, err error) {
- err = db.QueryRow(db.Rebind(`
+ err = db.QueryRow(`
SELECT did, handle, access_token, refresh_token, COALESCE(dpop_key, '')
FROM sessions
- WHERE id = ? AND expires_at > ?
- `), id, time.Now()).Scan(&did, &handle, &accessToken, &refreshToken, &dpopKey)
+ WHERE id = $1 AND expires_at > $2
+ `, id, time.Now()).Scan(&did, &handle, &accessToken, &refreshToken, &dpopKey)
return
}
func (db *DB) DeleteSession(id string) error {
- _, err := db.Exec(db.Rebind(`DELETE FROM sessions WHERE id = ?`), id)
+ _, err := db.Exec(`DELETE FROM sessions WHERE id = $1`, id)
return err
}
func (db *DB) DeleteExpiredSessions() error {
- _, err := db.Exec(db.Rebind(`DELETE FROM sessions WHERE expires_at <= ?`), time.Now())
+ _, err := db.Exec(`DELETE FROM sessions WHERE expires_at <= $1`, time.Now())
return err
}
diff --git a/backend/internal/db/tags.go b/backend/internal/db/tags.go
index 225b4fe..8152a0e 100644
--- a/backend/internal/db/tags.go
+++ b/backend/internal/db/tags.go
@@ -1,69 +1,30 @@
package db
-import "database/sql"
-
-type TrendingTag struct {
- Tag string `json:"tag"`
- Count int `json:"count"`
-}
-
func (db *DB) GetTrendingTags(limit int) ([]TrendingTag, error) {
- var query string
- if db.driver == "postgres" {
- query = `
- SELECT tag, COUNT(*) as count FROM (
- SELECT value as tag, author_did
- FROM annotations, json_array_elements_text(tags_json::json) as value
- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- AND created_at > NOW() - INTERVAL '14 days'
- UNION ALL
- SELECT value as tag, author_did
- FROM highlights, json_array_elements_text(tags_json::json) as value
- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- AND created_at > NOW() - INTERVAL '14 days'
- UNION ALL
- SELECT value as tag, author_did
- FROM bookmarks, json_array_elements_text(tags_json::json) as value
- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- AND created_at > NOW() - INTERVAL '14 days'
- ) combined
- GROUP BY tag
- HAVING COUNT(DISTINCT author_did) >= 3
- ORDER BY count DESC
- LIMIT $1
- `
- } else {
- query = `
- SELECT tag, COUNT(*) as count FROM (
- SELECT json_each.value as tag, author_did
- FROM annotations, json_each(annotations.tags_json)
- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- AND created_at > datetime('now', '-14 days')
- UNION ALL
- SELECT json_each.value as tag, author_did
- FROM highlights, json_each(highlights.tags_json)
- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- AND created_at > datetime('now', '-14 days')
- UNION ALL
- SELECT json_each.value as tag, author_did
- FROM bookmarks, json_each(bookmarks.tags_json)
- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- AND created_at > datetime('now', '-14 days')
- ) combined
- GROUP BY tag
- HAVING COUNT(DISTINCT author_did) >= 3
- ORDER BY count DESC
- LIMIT ?
- `
- }
+ query := `
+ SELECT tag, COUNT(*) as count FROM (
+ SELECT value as tag, author_did
+ FROM annotations, json_array_elements_text(tags_json::json) as value
+ WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
+ AND created_at > NOW() - INTERVAL '14 days'
+ UNION ALL
+ SELECT value as tag, author_did
+ FROM highlights, json_array_elements_text(tags_json::json) as value
+ WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
+ AND created_at > NOW() - INTERVAL '14 days'
+ UNION ALL
+ SELECT value as tag, author_did
+ FROM bookmarks, json_array_elements_text(tags_json::json) as value
+ WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
+ AND created_at > NOW() - INTERVAL '14 days'
+ ) combined
+ GROUP BY tag
+ HAVING COUNT(DISTINCT author_did) >= 3
+ ORDER BY count DESC
+ LIMIT $1
+ `
- var rows *sql.Rows
- var err error
- if db.driver == "postgres" {
- rows, err = db.Query(query, limit)
- } else {
- rows, err = db.Query(db.Rebind(query), limit)
- }
+ rows, err := db.Query(query, limit)
if err != nil {
return nil, err
}
@@ -90,60 +51,29 @@ func (db *DB) GetTrendingTags(limit int) ([]TrendingTag, error) {
}
func (db *DB) GetUserTags(did string, limit int) ([]TrendingTag, error) {
- var query string
- if db.driver == "postgres" {
- query = `
- SELECT tag, SUM(cnt) as count FROM (
- SELECT value as tag, COUNT(*) as cnt
- FROM annotations, json_array_elements_text(tags_json::json) as value
- WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- GROUP BY tag
- UNION ALL
- SELECT value as tag, COUNT(*) as cnt
- FROM highlights, json_array_elements_text(tags_json::json) as value
- WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- GROUP BY tag
- UNION ALL
- SELECT value as tag, COUNT(*) as cnt
- FROM bookmarks, json_array_elements_text(tags_json::json) as value
- WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- GROUP BY tag
- ) combined
+ query := `
+ SELECT tag, SUM(cnt) as count FROM (
+ SELECT value as tag, COUNT(*) as cnt
+ FROM annotations, json_array_elements_text(tags_json::json) as value
+ WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
GROUP BY tag
- ORDER BY count DESC
- LIMIT $2
- `
- } else {
- query = `
- SELECT tag, SUM(cnt) as count FROM (
- SELECT json_each.value as tag, COUNT(*) as cnt
- FROM annotations, json_each(annotations.tags_json)
- WHERE author_did = ? AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- GROUP BY tag
- UNION ALL
- SELECT json_each.value as tag, COUNT(*) as cnt
- FROM highlights, json_each(highlights.tags_json)
- WHERE author_did = ? AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- GROUP BY tag
- UNION ALL
- SELECT json_each.value as tag, COUNT(*) as cnt
- FROM bookmarks, json_each(bookmarks.tags_json)
- WHERE author_did = ? AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
- GROUP BY tag
- ) combined
+ UNION ALL
+ SELECT value as tag, COUNT(*) as cnt
+ FROM highlights, json_array_elements_text(tags_json::json) as value
+ WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
GROUP BY tag
- ORDER BY count DESC
- LIMIT ?
- `
- }
+ UNION ALL
+ SELECT value as tag, COUNT(*) as cnt
+ FROM bookmarks, json_array_elements_text(tags_json::json) as value
+ WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
+ GROUP BY tag
+ ) combined
+ GROUP BY tag
+ ORDER BY count DESC
+ LIMIT $2
+ `
- var rows *sql.Rows
- var err error
- if db.driver == "postgres" {
- rows, err = db.Query(query, did, limit)
- } else {
- rows, err = db.Query(db.Rebind(query), did, did, did, limit)
- }
+ rows, err := db.Query(query, did, limit)
if err != nil {
return nil, err
}
@@ -168,3 +98,8 @@ func (db *DB) GetUserTags(did string, limit int) ([]TrendingTag, error) {
return tags, nil
}
+
+type TrendingTag struct {
+ Tag string `json:"tag"`
+ Count int `json:"count"`
+}
diff --git a/backend/internal/firehose/ingester.go b/backend/internal/firehose/ingester.go
index 752a7ba..b2e716c 100644
--- a/backend/internal/firehose/ingester.go
+++ b/backend/internal/firehose/ingester.go
@@ -57,15 +57,26 @@ type Ingester struct {
currentRelayIdx int
onAnnotation AnnotationCallback
onDocument DocumentCallback
+ workerPool chan func()
}
type RecordHandler func(event *FirehoseEvent)
func NewIngester(database *db.DB, syncService *internal_sync.Service) *Ingester {
+ pool := make(chan func(), 256)
+ for range 10 {
+ go func() {
+ for fn := range pool {
+ fn()
+ }
+ }()
+ }
+
i := &Ingester{
- db: database,
- sync: syncService,
- handlers: make(map[string]RecordHandler),
+ db: database,
+ sync: syncService,
+ handlers: make(map[string]RecordHandler),
+ workerPool: pool,
}
i.RegisterHandler(CollectionAnnotation, i.handleAnnotation)
@@ -237,7 +248,11 @@ func (i *Ingester) handleCommit(event JetstreamEvent) {
i.dispatchToHandler(firehoseEvent)
- go i.triggerLazySync(event.Did)
+ did := event.Did
+ select {
+ case i.workerPool <- func() { i.triggerLazySync(did) }:
+ default:
+ }
}
case "delete":
i.handleDelete(commit.Collection, uri)
@@ -266,7 +281,9 @@ func (i *Ingester) triggerLazySync(did string) {
return
}
- _, err = i.sync.PerformSync(context.Background(), did, func(ctx context.Context, _ string) (*xrpc.Client, error) {
+ syncCtx, syncCancel := context.WithTimeout(context.Background(), 15*time.Second)
+ defer syncCancel()
+ _, err = i.sync.PerformSync(syncCtx, did, func(ctx context.Context, _ string) (*xrpc.Client, error) {
return &xrpc.Client{
PDS: pds,
}, nil
@@ -434,7 +451,11 @@ func (i *Ingester) handleAnnotation(event *FirehoseEvent) {
} else {
logger.Info("Indexed annotation from %s on %s", event.Repo, targetSource)
if i.onAnnotation != nil {
- go i.onAnnotation(uri, event.Repo, targetSource, bodyValuePtr, selectorJSONPtr, targetTitlePtr, tagsJSONPtr)
+ cb := i.onAnnotation
+ select {
+ case i.workerPool <- func() { cb(uri, event.Repo, targetSource, bodyValuePtr, selectorJSONPtr, targetTitlePtr, tagsJSONPtr) }:
+ default:
+ }
}
}
}
diff --git a/backend/internal/recommendations/service.go b/backend/internal/recommendations/service.go
index f69f547..a428864 100644
--- a/backend/internal/recommendations/service.go
+++ b/backend/internal/recommendations/service.go
@@ -374,6 +374,8 @@ func (s *Service) BackfillDocumentEmbeddings(batchSize int) error {
if len(docs) < batchSize {
break
}
+
+ time.Sleep(2 * time.Second)
}
if total > 0 {
@@ -429,6 +431,8 @@ func (s *Service) BackfillAnnotationEmbeddings(batchSize int) (int, error) {
if len(anns) < batchSize {
break
}
+
+ time.Sleep(2 * time.Second)
}
if total > 0 {
@@ -484,6 +488,8 @@ func (s *Service) BackfillHighlightEmbeddings(batchSize int) (int, error) {
if len(highlights) < batchSize {
break
}
+
+ time.Sleep(2 * time.Second)
}
if total > 0 {
diff --git a/backend/internal/verification/verify.go b/backend/internal/verification/verify.go
index 8b99b47..822a78c 100644
--- a/backend/internal/verification/verify.go
+++ b/backend/internal/verification/verify.go
@@ -22,6 +22,8 @@ var client = &http.Client{
},
}
+var verifySem = make(chan struct{}, 3)
+
var linkTagPattern = regexp.MustCompile(`]+rel=["']site\.standard\.document["'][^>]+href=["']([^"']+)["'][^>]*/?>|]+href=["']([^"']+)["'][^>]+rel=["']site\.standard\.document["'][^>]*/?>`)
func VerifyPublication(pubURL, expectedURI string) error {
@@ -84,7 +86,7 @@ func VerifyDocument(docURL, expectedURI string) error {
return fmt.Errorf("document URL returned %d", resp.StatusCode)
}
- body, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024))
+ body, err := io.ReadAll(io.LimitReader(resp.Body, 16*1024))
if err != nil {
return fmt.Errorf("failed to read document: %w", err)
}
@@ -107,6 +109,9 @@ func VerifyDocument(docURL, expectedURI string) error {
func VerifyPublicationAsync(pubURL, uri string, onVerified func(string)) {
go func() {
+ verifySem <- struct{}{}
+ defer func() { <-verifySem }()
+
if err := VerifyPublication(pubURL, uri); err != nil {
return
}
@@ -119,6 +124,9 @@ func VerifyPublicationAsync(pubURL, uri string, onVerified func(string)) {
func VerifyDocumentAsync(docURL, uri string, onVerified func(string)) {
go func() {
+ verifySem <- struct{}{}
+ defer func() { <-verifySem }()
+
if err := VerifyDocument(docURL, uri); err != nil {
return
}
diff --git a/extension/src/utils/overlay.ts b/extension/src/utils/overlay.ts
index a5c9601..82aadcc 100644
--- a/extension/src/utils/overlay.ts
+++ b/extension/src/utils/overlay.ts
@@ -765,9 +765,9 @@ export async function initContentScript(ctx: { onInvalidated: (cb: () => void) =
const marginLeft = i === 0 ? '0' : '-8px';
if (avatar) {
- return ``;
+ return `
`;
} else {
- return `
@@ -140,12 +139,12 @@ export default function ProfileHoverCard({
)} - View Profile - +diff --git a/web/src/components/common/RichText.tsx b/web/src/components/common/RichText.tsx index f53ca2c..1f9468d 100644 --- a/web/src/components/common/RichText.tsx +++ b/web/src/components/common/RichText.tsx @@ -1,5 +1,4 @@ import React from "react"; -import { Link } from "react-router-dom"; import ExternalLinkModal from "../modals/ExternalLinkModal"; import { useStore } from "@nanostores/react"; import { $preferences } from "../../store/preferences"; @@ -138,14 +137,14 @@ export default function RichText({ text, className }: RichTextProps) { } finalParts.push( - e.stopPropagation()} > @{handle} - , + , ); lastMentionIndex = startIndex + fullMatch.length; diff --git a/web/src/components/modals/SignUpModal.tsx b/web/src/components/modals/SignUpModal.tsx index 135bb48..4f91331 100644 --- a/web/src/components/modals/SignUpModal.tsx +++ b/web/src/components/modals/SignUpModal.tsx @@ -195,6 +195,9 @@ export default function SignUpModal({ onClose }: SignUpModalProps) { try { const result = await startSignup(serviceUrl); if (result.authorizationUrl) { + const url = new URL(result.authorizationUrl); + if (url.protocol !== "https:") + throw new Error("Invalid authorization URL"); window.location.href = result.authorizationUrl; } } catch (err) { diff --git a/web/src/components/navigation/MobileNav.tsx b/web/src/components/navigation/MobileNav.tsx index 6610589..c2a6e41 100644 --- a/web/src/components/navigation/MobileNav.tsx +++ b/web/src/components/navigation/MobileNav.tsx @@ -15,22 +15,43 @@ import { X, } from "lucide-react"; import React, { useEffect, useState } from "react"; -import { Link, useLocation } from "react-router-dom"; import { getUnreadNotificationCount } from "../../api/client"; import { $user, logout } from "../../store/auth"; +import type { UserProfile } from "../../types"; import { AppleIcon } from "../common/Icons"; -export default function MobileNav() { - const user = useStore($user); - const location = useLocation(); +interface MobileNavProps { + initialUser?: UserProfile | null; + currentPath?: string; +} + +export default function MobileNav({ + initialUser, + currentPath: initialPath, +}: MobileNavProps) { + const storeUser = useStore($user); + const user = storeUser || initialUser || null; + const [currentPath, setCurrentPath] = useState(initialPath || "/"); const [isMenuOpen, setIsMenuOpen] = useState(false); const [unreadCount, setUnreadCount] = useState(0); const isAuthenticated = !!user; + useEffect(() => { + if (initialUser && !storeUser) { + $user.set(initialUser); + } + }, [initialUser, storeUser]); + + useEffect(() => { + const handler = () => setCurrentPath(window.location.pathname); + document.addEventListener("astro:page-load", handler); + return () => document.removeEventListener("astro:page-load", handler); + }, []); + const isActive = (path: string) => { - if (path === "/") return location.pathname === "/"; - return location.pathname.startsWith(path); + if (path === "/") return currentPath === "/"; + return currentPath.startsWith(path); }; useEffect(() => { @@ -57,8 +78,8 @@ export default function MobileNav() {