···88 "time"
991010 _ "github.com/lib/pq"
1111- _ "github.com/mattn/go-sqlite3"
1211)
13121413type DB struct {
1514 *sql.DB
1616- driver string
1715}
18161917type Annotation struct {
···204202}
205203206204func New(dsn string) (*DB, error) {
207207- driver := "sqlite3"
208208- if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") {
209209- driver = "postgres"
205205+ if !strings.HasPrefix(dsn, "postgres://") && !strings.HasPrefix(dsn, "postgresql://") {
206206+ return nil, fmt.Errorf("only PostgreSQL is supported, DSN must start with postgres:// or postgresql://")
210207 }
211208212212- db, err := sql.Open(driver, dsn)
209209+ db, err := sql.Open("postgres", dsn)
213210 if err != nil {
214211 return nil, fmt.Errorf("failed to open database connection: %w", err)
215212 }
216213217217- if driver == "sqlite3" {
218218- if _, err := db.Exec("PRAGMA journal_mode=WAL;"); err != nil {
219219- return nil, fmt.Errorf("failed to set WAL mode: %w", err)
220220- }
221221- db.Exec("PRAGMA synchronous=NORMAL;")
222222- db.Exec("PRAGMA busy_timeout=5000;")
223223- db.Exec("PRAGMA cache_size=-2000;")
224224- db.Exec("PRAGMA foreign_keys=ON;")
225225-226226- db.SetMaxOpenConns(25)
227227- db.SetMaxIdleConns(25)
228228- db.SetConnMaxLifetime(5 * time.Minute)
229229- } else {
230230- db.SetMaxOpenConns(50)
231231- db.SetMaxIdleConns(25)
232232- db.SetConnMaxLifetime(10 * time.Minute)
233233- }
214214+ db.SetMaxOpenConns(25)
215215+ db.SetMaxIdleConns(10)
216216+ db.SetConnMaxLifetime(5 * time.Minute)
217217+ db.SetConnMaxIdleTime(2 * time.Minute)
234218235219 if err := db.Ping(); err != nil {
236220 return nil, fmt.Errorf("failed to ping database: %w", err)
237221 }
238222239239- return &DB{DB: db, driver: driver}, nil
223223+ return &DB{DB: db}, nil
240224}
241225242226func (db *DB) Migrate() error {
243243-244244- dateType := "DATETIME"
245245- if db.driver == "postgres" {
246246- dateType = "TIMESTAMP"
247247- }
248248-249227 _, err := db.Exec(`
250228 CREATE TABLE IF NOT EXISTS annotations (
251229 uri TEXT PRIMARY KEY,
···259237 target_title TEXT,
260238 selector_json TEXT,
261239 tags_json TEXT,
262262- created_at ` + dateType + ` NOT NULL,
263263- indexed_at ` + dateType + ` NOT NULL,
240240+ created_at TIMESTAMP NOT NULL,
241241+ indexed_at TIMESTAMP NOT NULL,
264242 cid TEXT
265243 )`)
266244 if err != nil {
···272250 db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_author_did ON annotations(author_did)`)
273251 db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_motivation ON annotations(motivation)`)
274252 db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_created_at ON annotations(created_at DESC)`)
253253+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_author_created ON annotations(author_did, created_at DESC)`)
254254+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_annotations_uri_pattern ON annotations(uri text_pattern_ops)`)
275255276256 db.Exec(`CREATE TABLE IF NOT EXISTS highlights (
277257 uri TEXT PRIMARY KEY,
···282262 selector_json TEXT,
283263 color TEXT,
284264 tags_json TEXT,
285285- created_at ` + dateType + ` NOT NULL,
286286- indexed_at ` + dateType + ` NOT NULL,
265265+ created_at TIMESTAMP NOT NULL,
266266+ indexed_at TIMESTAMP NOT NULL,
287267 cid TEXT
288268 )`)
289269 db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_target_hash ON highlights(target_hash)`)
290270 db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_author_did ON highlights(author_did)`)
291271 db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_created_at ON highlights(created_at DESC)`)
272272+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_author_created ON highlights(author_did, created_at DESC)`)
273273+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_highlights_uri_pattern ON highlights(uri text_pattern_ops)`)
292274293275 db.Exec(`CREATE TABLE IF NOT EXISTS bookmarks (
294276 uri TEXT PRIMARY KEY,
···298280 title TEXT,
299281 description TEXT,
300282 tags_json TEXT,
301301- created_at ` + dateType + ` NOT NULL,
302302- indexed_at ` + dateType + ` NOT NULL,
283283+ created_at TIMESTAMP NOT NULL,
284284+ indexed_at TIMESTAMP NOT NULL,
303285 cid TEXT
304286 )`)
305287 db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_source_hash ON bookmarks(source_hash)`)
306288 db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_author_did ON bookmarks(author_did)`)
307289 db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_created_at ON bookmarks(created_at DESC)`)
290290+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_author_created ON bookmarks(author_did, created_at DESC)`)
291291+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_bookmarks_uri_pattern ON bookmarks(uri text_pattern_ops)`)
308292309293 db.Exec(`CREATE TABLE IF NOT EXISTS replies (
310294 uri TEXT PRIMARY KEY,
···313297 root_uri TEXT NOT NULL,
314298 text TEXT NOT NULL,
315299 format TEXT DEFAULT 'text/plain',
316316- created_at ` + dateType + ` NOT NULL,
317317- indexed_at ` + dateType + ` NOT NULL,
300300+ created_at TIMESTAMP NOT NULL,
301301+ indexed_at TIMESTAMP NOT NULL,
318302 cid TEXT
319303 )`)
320304 db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_parent_uri ON replies(parent_uri)`)
321305 db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_root_uri ON replies(root_uri)`)
322306 db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_created_at ON replies(created_at DESC)`)
307307+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_replies_author_did ON replies(author_did)`)
323308324309 db.Exec(`CREATE TABLE IF NOT EXISTS likes (
325310 uri TEXT PRIMARY KEY,
326311 author_did TEXT NOT NULL,
327312 subject_uri TEXT NOT NULL,
328328- created_at ` + dateType + ` NOT NULL,
329329- indexed_at ` + dateType + ` NOT NULL
313313+ created_at TIMESTAMP NOT NULL,
314314+ indexed_at TIMESTAMP NOT NULL
330315 )`)
331316 db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_subject_uri ON likes(subject_uri)`)
332317 db.Exec(`CREATE INDEX IF NOT EXISTS idx_likes_author_did ON likes(author_did)`)
···338323 name TEXT NOT NULL,
339324 description TEXT,
340325 icon TEXT,
341341- created_at ` + dateType + ` NOT NULL,
342342- indexed_at ` + dateType + ` NOT NULL
326326+ created_at TIMESTAMP NOT NULL,
327327+ indexed_at TIMESTAMP NOT NULL
343328 )`)
344329 db.Exec(`CREATE INDEX IF NOT EXISTS idx_collections_author_did ON collections(author_did)`)
345330 db.Exec(`CREATE INDEX IF NOT EXISTS idx_collections_created_at ON collections(created_at DESC)`)
···350335 collection_uri TEXT NOT NULL,
351336 annotation_uri TEXT NOT NULL,
352337 position INTEGER DEFAULT 0,
353353- created_at ` + dateType + ` NOT NULL,
354354- indexed_at ` + dateType + ` NOT NULL
338338+ created_at TIMESTAMP NOT NULL,
339339+ indexed_at TIMESTAMP NOT NULL
355340 )`)
356341 db.Exec(`CREATE INDEX IF NOT EXISTS idx_collection_items_collection ON collection_items(collection_uri)`)
357342 db.Exec(`CREATE INDEX IF NOT EXISTS idx_collection_items_annotation ON collection_items(annotation_uri)`)
···364349 access_token TEXT NOT NULL,
365350 refresh_token TEXT NOT NULL,
366351 dpop_key TEXT,
367367- created_at ` + dateType + ` NOT NULL,
368368- expires_at ` + dateType + ` NOT NULL
352352+ created_at TIMESTAMP NOT NULL,
353353+ expires_at TIMESTAMP NOT NULL
369354 )`)
370355 db.Exec(`CREATE INDEX IF NOT EXISTS idx_sessions_did ON sessions(did)`)
371371-372372- autoInc := "INTEGER PRIMARY KEY AUTOINCREMENT"
373373- if db.driver == "postgres" {
374374- autoInc = "SERIAL PRIMARY KEY"
375375- }
356356+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at)`)
376357377358 db.Exec(`CREATE TABLE IF NOT EXISTS edit_history (
378378- id ` + autoInc + `,
359359+ id SERIAL PRIMARY KEY,
379360 uri TEXT NOT NULL,
380361 record_type TEXT NOT NULL,
381362 previous_content TEXT NOT NULL,
382363 previous_cid TEXT,
383383- edited_at ` + dateType + ` NOT NULL
364364+ edited_at TIMESTAMP NOT NULL
384365 )`)
385366 db.Exec(`CREATE INDEX IF NOT EXISTS idx_edit_history_uri ON edit_history(uri)`)
386367 db.Exec(`CREATE INDEX IF NOT EXISTS idx_edit_history_edited_at ON edit_history(edited_at DESC)`)
387368388369 db.Exec(`CREATE TABLE IF NOT EXISTS notifications (
389389- id ` + autoInc + `,
370370+ id SERIAL PRIMARY KEY,
390371 recipient_did TEXT NOT NULL,
391372 actor_did TEXT NOT NULL,
392373 type TEXT NOT NULL,
393374 subject_uri TEXT NOT NULL,
394394- created_at ` + dateType + ` NOT NULL,
395395- read_at ` + dateType + `
375375+ created_at TIMESTAMP NOT NULL,
376376+ read_at TIMESTAMP
396377 )`)
397378 db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON notifications(recipient_did)`)
398379 db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_created_at ON notifications(created_at DESC)`)
380380+ db.Exec(`CREATE INDEX IF NOT EXISTS idx_notifications_unread ON notifications(recipient_did) WHERE read_at IS NULL`)
399381400382 db.Exec(`CREATE TABLE IF NOT EXISTS api_keys (
401383 id TEXT PRIMARY KEY,
402384 owner_did TEXT NOT NULL,
403385 name TEXT NOT NULL,
404386 key_hash TEXT NOT NULL,
405405- created_at ` + dateType + ` NOT NULL,
406406- last_used_at ` + dateType + `,
387387+ created_at TIMESTAMP NOT NULL,
388388+ last_used_at TIMESTAMP,
407389 uri TEXT,
408390 cid TEXT,
409409- indexed_at ` + dateType + ` DEFAULT CURRENT_TIMESTAMP
391391+ indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
410392 )`)
411393 db.Exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_owner ON api_keys(owner_did)`)
412394 db.Exec(`CREATE INDEX IF NOT EXISTS idx_api_keys_hash ON api_keys(key_hash)`)
···419401 bio TEXT,
420402 website TEXT,
421403 links_json TEXT,
422422- created_at ` + dateType + ` NOT NULL,
423423- indexed_at ` + dateType + ` NOT NULL,
404404+ created_at TIMESTAMP NOT NULL,
405405+ indexed_at TIMESTAMP NOT NULL,
424406 cid TEXT
425407 )`)
426408 db.Exec(`CREATE INDEX IF NOT EXISTS idx_profiles_author_did ON profiles(author_did)`)
···432414 subscribed_labelers TEXT,
433415 label_preferences TEXT,
434416 disable_external_link_warning BOOLEAN,
435435- created_at ` + dateType + ` NOT NULL,
436436- indexed_at ` + dateType + ` NOT NULL,
417417+ created_at TIMESTAMP NOT NULL,
418418+ indexed_at TIMESTAMP NOT NULL,
437419 cid TEXT
438420 )`)
439421 db.Exec(`CREATE INDEX IF NOT EXISTS idx_preferences_author_did ON preferences(author_did)`)
···443425 db.Exec(`CREATE TABLE IF NOT EXISTS cursors (
444426 id TEXT PRIMARY KEY,
445427 last_cursor BIGINT NOT NULL,
446446- updated_at ` + dateType + ` NOT NULL
428428+ updated_at TIMESTAMP NOT NULL
447429 )`)
448430449431 db.Exec(`CREATE TABLE IF NOT EXISTS blocks (
450450- id ` + autoInc + `,
432432+ id SERIAL PRIMARY KEY,
451433 actor_did TEXT NOT NULL,
452434 subject_did TEXT NOT NULL,
453453- created_at ` + dateType + ` NOT NULL,
435435+ created_at TIMESTAMP NOT NULL,
454436 UNIQUE(actor_did, subject_did)
455437 )`)
456438 db.Exec(`CREATE INDEX IF NOT EXISTS idx_blocks_actor ON blocks(actor_did)`)
457439 db.Exec(`CREATE INDEX IF NOT EXISTS idx_blocks_subject ON blocks(subject_did)`)
458440459441 db.Exec(`CREATE TABLE IF NOT EXISTS mutes (
460460- id ` + autoInc + `,
442442+ id SERIAL PRIMARY KEY,
461443 actor_did TEXT NOT NULL,
462444 subject_did TEXT NOT NULL,
463463- created_at ` + dateType + ` NOT NULL,
445445+ created_at TIMESTAMP NOT NULL,
464446 UNIQUE(actor_did, subject_did)
465447 )`)
466448 db.Exec(`CREATE INDEX IF NOT EXISTS idx_mutes_actor ON mutes(actor_did)`)
467449 db.Exec(`CREATE INDEX IF NOT EXISTS idx_mutes_subject ON mutes(subject_did)`)
468450469451 db.Exec(`CREATE TABLE IF NOT EXISTS moderation_reports (
470470- id ` + autoInc + `,
452452+ id SERIAL PRIMARY KEY,
471453 reporter_did TEXT NOT NULL,
472454 subject_did TEXT NOT NULL,
473455 subject_uri TEXT,
474456 reason_type TEXT NOT NULL,
475457 reason_text TEXT,
476458 status TEXT NOT NULL DEFAULT 'pending',
477477- created_at ` + dateType + ` NOT NULL,
478478- resolved_at ` + dateType + `,
459459+ created_at TIMESTAMP NOT NULL,
460460+ resolved_at TIMESTAMP,
479461 resolved_by TEXT
480462 )`)
481463 db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_status ON moderation_reports(status)`)
···483465 db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_reporter ON moderation_reports(reporter_did)`)
484466485467 db.Exec(`CREATE TABLE IF NOT EXISTS moderation_actions (
486486- id ` + autoInc + `,
468468+ id SERIAL PRIMARY KEY,
487469 report_id INTEGER NOT NULL,
488470 actor_did TEXT NOT NULL,
489471 action TEXT NOT NULL,
490472 comment TEXT,
491491- created_at ` + dateType + ` NOT NULL
473473+ created_at TIMESTAMP NOT NULL
492474 )`)
493475 db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_actions_report ON moderation_actions(report_id)`)
494476495477 db.Exec(`CREATE TABLE IF NOT EXISTS content_labels (
496496- id ` + autoInc + `,
478478+ id SERIAL PRIMARY KEY,
497479 src TEXT NOT NULL,
498480 uri TEXT NOT NULL,
499481 val TEXT NOT NULL,
500482 neg INTEGER NOT NULL DEFAULT 0,
501483 created_by TEXT NOT NULL,
502502- created_at ` + dateType + ` NOT NULL
484484+ created_at TIMESTAMP NOT NULL
503485 )`)
504486 db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_uri ON content_labels(uri)`)
505487 db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_src ON content_labels(src)`)
···511493 name TEXT NOT NULL,
512494 description TEXT,
513495 show_in_discover BOOLEAN NOT NULL DEFAULT true,
514514- indexed_at ` + dateType + ` NOT NULL
496496+ indexed_at TIMESTAMP NOT NULL
515497 )`)
516498 db.Exec(`CREATE INDEX IF NOT EXISTS idx_publications_author ON publications(author_did)`)
517499 db.Exec(`CREATE INDEX IF NOT EXISTS idx_publications_url ON publications(url)`)
···526508 text_content TEXT,
527509 tags_json TEXT,
528510 canonical_url TEXT,
529529- published_at ` + dateType + ` NOT NULL,
530530- indexed_at ` + dateType + ` NOT NULL
511511+ published_at TIMESTAMP NOT NULL,
512512+ indexed_at TIMESTAMP NOT NULL
531513 )`)
532514 db.Exec(`CREATE INDEX IF NOT EXISTS idx_documents_author ON documents(author_did)`)
533515 db.Exec(`CREATE INDEX IF NOT EXISTS idx_documents_site ON documents(site)`)
···544526 return nil, nil
545527 }
546528547547- query := `SELECT uri, author_did, display_name, bio, avatar, website, links_json, created_at, indexed_at FROM profiles WHERE author_did IN (`
548548- args := make([]interface{}, len(dids))
549529 placeholders := make([]string, len(dids))
550550-530530+ args := make([]interface{}, len(dids))
551531 for i, did := range dids {
552532 placeholders[i] = fmt.Sprintf("$%d", i+1)
553533 args[i] = did
554534 }
555535556556- query += strings.Join(placeholders, ",") + ")"
557557-558558- if db.driver == "sqlite3" {
559559- query = strings.ReplaceAll(query, "$", "?")
560560-561561- placeholders = make([]string, len(dids))
562562- for i := range dids {
563563- placeholders[i] = "?"
564564- }
565565- 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, ",") + ")"
566566- }
536536+ 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, ",") + ")"
567537568538 rows, err := db.Query(query, args...)
569539 if err != nil {
···597567598568func (db *DB) SetCursor(id string, cursor int64) error {
599569 query := `
600600- INSERT INTO cursors (id, last_cursor, updated_at)
601601- VALUES ($1, $2, $3)
602602- ON CONFLICT(id) DO UPDATE SET
603603- last_cursor = EXCLUDED.last_cursor,
570570+ INSERT INTO cursors (id, last_cursor, updated_at)
571571+ VALUES ($1, $2, $3)
572572+ ON CONFLICT(id) DO UPDATE SET
573573+ last_cursor = EXCLUDED.last_cursor,
604574 updated_at = EXCLUDED.updated_at
605575 `
606576 _, err := db.Exec(query, id, cursor, time.Now())
···623593624594func (db *DB) UpsertProfile(p *Profile) error {
625595 query := `
626626- INSERT INTO profiles (uri, author_did, display_name, avatar, bio, website, links_json, created_at, indexed_at)
627627- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
628628- ON CONFLICT(uri) DO UPDATE SET
596596+ INSERT INTO profiles (uri, author_did, display_name, avatar, bio, website, links_json, created_at, indexed_at)
597597+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
598598+ ON CONFLICT(uri) DO UPDATE SET
629599 display_name = EXCLUDED.display_name,
630600 avatar = EXCLUDED.avatar,
631631- bio = EXCLUDED.bio,
601601+ bio = EXCLUDED.bio,
632602 website = EXCLUDED.website,
633603 links_json = EXCLUDED.links_json,
634604 indexed_at = EXCLUDED.indexed_at
635605 `
636636- _, err := db.Exec(db.Rebind(query), p.URI, p.AuthorDID, p.DisplayName, p.Avatar, p.Bio, p.Website, p.LinksJSON, p.CreatedAt, p.IndexedAt)
606606+ _, err := db.Exec(query, p.URI, p.AuthorDID, p.DisplayName, p.Avatar, p.Bio, p.Website, p.LinksJSON, p.CreatedAt, p.IndexedAt)
637607 return err
638608}
639609···672642673643func (db *DB) UpsertPreferences(p *Preferences) error {
674644 query := `
675675- INSERT INTO preferences (uri, author_did, external_link_skipped_hostnames, subscribed_labelers, label_preferences, disable_external_link_warning, created_at, indexed_at, cid)
676676- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
677677- ON CONFLICT(uri) DO UPDATE SET
645645+ INSERT INTO preferences (uri, author_did, external_link_skipped_hostnames, subscribed_labelers, label_preferences, disable_external_link_warning, created_at, indexed_at, cid)
646646+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
647647+ ON CONFLICT(uri) DO UPDATE SET
678648 external_link_skipped_hostnames = EXCLUDED.external_link_skipped_hostnames,
679649 subscribed_labelers = EXCLUDED.subscribed_labelers,
680650 label_preferences = EXCLUDED.label_preferences,
···682652 indexed_at = EXCLUDED.indexed_at,
683653 cid = EXCLUDED.cid
684654 `
685685- _, err := db.Exec(db.Rebind(query), p.URI, p.AuthorDID, p.ExternalLinkSkippedHostnames, p.SubscribedLabelers, p.LabelPreferences, p.DisableExternalLinkWarning, p.CreatedAt, p.IndexedAt, p.CID)
655655+ _, err := db.Exec(query, p.URI, p.AuthorDID, p.ExternalLinkSkippedHostnames, p.SubscribedLabelers, p.LabelPreferences, p.DisableExternalLinkWarning, p.CreatedAt, p.IndexedAt, p.CID)
686656 return err
687657}
688658···697667}
698668699669func (db *DB) GetAPIKeyURIs(ownerDID string) ([]string, error) {
700700- rows, err := db.Query(db.Rebind("SELECT uri FROM api_keys WHERE owner_did = ? AND uri IS NOT NULL AND uri != ''"), ownerDID)
670670+ rows, err := db.Query("SELECT uri FROM api_keys WHERE owner_did = $1 AND uri IS NOT NULL AND uri != ''", ownerDID)
701671 if err != nil {
702672 return nil, err
703673 }
···714684}
715685716686func (db *DB) GetPreferenceURIs(did string) ([]string, error) {
717717- rows, err := db.Query(db.Rebind("SELECT uri FROM preferences WHERE author_did = ? AND uri IS NOT NULL AND uri != ''"), did)
687687+ rows, err := db.Query("SELECT uri FROM preferences WHERE author_did = $1 AND uri IS NOT NULL AND uri != ''", did)
718688 if err != nil {
719689 return nil, err
720690 }
···731701}
732702733703func (db *DB) runMigrations() {
734734- dateType := "DATETIME"
735735- if db.driver == "postgres" {
736736- dateType = "TIMESTAMP"
737737- }
738738- db.Exec(`ALTER TABLE sessions ADD COLUMN dpop_key TEXT`)
704704+ db.Exec(`ALTER TABLE sessions ADD COLUMN IF NOT EXISTS dpop_key TEXT`)
739705740740- db.Exec(`ALTER TABLE annotations ADD COLUMN motivation TEXT`)
741741- db.Exec(`ALTER TABLE annotations ADD COLUMN body_value TEXT`)
742742- db.Exec(`ALTER TABLE annotations ADD COLUMN body_format TEXT DEFAULT 'text/plain'`)
743743- db.Exec(`ALTER TABLE annotations ADD COLUMN body_uri TEXT`)
744744- db.Exec(`ALTER TABLE annotations ADD COLUMN target_source TEXT`)
745745- db.Exec(`ALTER TABLE annotations ADD COLUMN target_hash TEXT`)
746746- db.Exec(`ALTER TABLE annotations ADD COLUMN target_title TEXT`)
747747- db.Exec(`ALTER TABLE annotations ADD COLUMN selector_json TEXT`)
748748- db.Exec(`ALTER TABLE annotations ADD COLUMN tags_json TEXT`)
749749- db.Exec(`ALTER TABLE annotations ADD COLUMN cid TEXT`)
706706+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS motivation TEXT`)
707707+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_value TEXT`)
708708+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_format TEXT DEFAULT 'text/plain'`)
709709+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS body_uri TEXT`)
710710+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_source TEXT`)
711711+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_hash TEXT`)
712712+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS target_title TEXT`)
713713+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS selector_json TEXT`)
714714+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS tags_json TEXT`)
715715+ db.Exec(`ALTER TABLE annotations ADD COLUMN IF NOT EXISTS cid TEXT`)
750716751717 db.Exec(`UPDATE annotations SET target_source = url WHERE target_source IS NULL AND url IS NOT NULL`)
752718 db.Exec(`UPDATE annotations SET target_hash = url_hash WHERE target_hash IS NULL AND url_hash IS NOT NULL`)
···754720 db.Exec(`UPDATE annotations SET target_title = title WHERE target_title IS NULL AND title IS NOT NULL`)
755721 db.Exec(`UPDATE annotations SET motivation = 'commenting' WHERE motivation IS NULL`)
756722757757- db.Exec(`ALTER TABLE profiles ADD COLUMN website TEXT`)
758758- db.Exec(`ALTER TABLE profiles ADD COLUMN display_name TEXT`)
759759- db.Exec(`ALTER TABLE profiles ADD COLUMN avatar TEXT`)
723723+ db.Exec(`ALTER TABLE profiles ADD COLUMN IF NOT EXISTS website TEXT`)
724724+ db.Exec(`ALTER TABLE profiles ADD COLUMN IF NOT EXISTS display_name TEXT`)
725725+ db.Exec(`ALTER TABLE profiles ADD COLUMN IF NOT EXISTS avatar TEXT`)
760726761761- if db.driver == "postgres" {
762762- db.Exec(`ALTER TABLE cursors ALTER COLUMN last_cursor TYPE BIGINT`)
763763- }
727727+ db.Exec(`ALTER TABLE cursors ALTER COLUMN last_cursor TYPE BIGINT`)
764728765765- db.Exec(`ALTER TABLE api_keys ADD COLUMN uri TEXT`)
766766- db.Exec(`ALTER TABLE api_keys ADD COLUMN cid TEXT`)
767767- db.Exec(`ALTER TABLE api_keys ADD COLUMN indexed_at ` + dateType + ` DEFAULT CURRENT_TIMESTAMP`)
729729+ db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS uri TEXT`)
730730+ db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS cid TEXT`)
731731+ db.Exec(`ALTER TABLE api_keys ADD COLUMN IF NOT EXISTS indexed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP`)
768732769769- db.migrateModeration(dateType)
733733+ db.migrateModeration()
770734771771- db.Exec(`ALTER TABLE preferences ADD COLUMN subscribed_labelers TEXT`)
772772- db.Exec(`ALTER TABLE preferences ADD COLUMN label_preferences TEXT`)
773773- db.Exec(`ALTER TABLE preferences ADD COLUMN disable_external_link_warning BOOLEAN`)
735735+ db.Exec(`ALTER TABLE preferences ADD COLUMN IF NOT EXISTS subscribed_labelers TEXT`)
736736+ db.Exec(`ALTER TABLE preferences ADD COLUMN IF NOT EXISTS label_preferences TEXT`)
737737+ db.Exec(`ALTER TABLE preferences ADD COLUMN IF NOT EXISTS disable_external_link_warning BOOLEAN`)
774738}
775739776776-func (db *DB) migrateModeration(dateType string) {
740740+func (db *DB) migrateModeration() {
777741 _, err := db.Exec(`SELECT subject_did FROM moderation_reports LIMIT 0`)
778742 if err != nil {
779743 db.Exec(`DROP TABLE IF EXISTS moderation_reports`)
780744 db.Exec(`DROP TABLE IF EXISTS moderation_actions`)
781745782782- autoInc := "INTEGER PRIMARY KEY AUTOINCREMENT"
783783- if db.driver == "postgres" {
784784- autoInc = "SERIAL PRIMARY KEY"
785785- }
786786-787746 db.Exec(`CREATE TABLE IF NOT EXISTS moderation_reports (
788788- id ` + autoInc + `,
747747+ id SERIAL PRIMARY KEY,
789748 reporter_did TEXT NOT NULL,
790749 subject_did TEXT NOT NULL,
791750 subject_uri TEXT,
792751 reason_type TEXT NOT NULL,
793752 reason_text TEXT,
794753 status TEXT NOT NULL DEFAULT 'pending',
795795- created_at ` + dateType + ` NOT NULL,
796796- resolved_at ` + dateType + `,
754754+ created_at TIMESTAMP NOT NULL,
755755+ resolved_at TIMESTAMP,
797756 resolved_by TEXT
798757 )`)
799758 db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_status ON moderation_reports(status)`)
···801760 db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_reports_reporter ON moderation_reports(reporter_did)`)
802761803762 db.Exec(`CREATE TABLE IF NOT EXISTS moderation_actions (
804804- id ` + autoInc + `,
763763+ id SERIAL PRIMARY KEY,
805764 report_id INTEGER NOT NULL,
806765 actor_did TEXT NOT NULL,
807766 action TEXT NOT NULL,
808767 comment TEXT,
809809- created_at ` + dateType + ` NOT NULL
768768+ created_at TIMESTAMP NOT NULL
810769 )`)
811770 db.Exec(`CREATE INDEX IF NOT EXISTS idx_mod_actions_report ON moderation_actions(report_id)`)
812771 }
813772814814- autoInc := "INTEGER PRIMARY KEY AUTOINCREMENT"
815815- if db.driver == "postgres" {
816816- autoInc = "SERIAL PRIMARY KEY"
817817- }
818773 db.Exec(`CREATE TABLE IF NOT EXISTS content_labels (
819819- id ` + autoInc + `,
774774+ id SERIAL PRIMARY KEY,
820775 src TEXT NOT NULL,
821776 uri TEXT NOT NULL,
822777 val TEXT NOT NULL,
823778 neg INTEGER NOT NULL DEFAULT 0,
824779 created_by TEXT NOT NULL,
825825- created_at ` + dateType + ` NOT NULL
780780+ created_at TIMESTAMP NOT NULL
826781 )`)
827782 db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_uri ON content_labels(uri)`)
828783 db.Exec(`CREATE INDEX IF NOT EXISTS idx_content_labels_src ON content_labels(src)`)
···830785831786func (db *DB) Close() error {
832787 return db.DB.Close()
833833-}
834834-835835-func (db *DB) Rebind(query string) string {
836836- if db.driver != "postgres" {
837837- return query
838838- }
839839-840840- if !strings.Contains(query, "?") {
841841- return query
842842- }
843843-844844- var builder strings.Builder
845845- builder.Grow(len(query) + 20)
846846-847847- paramCount := 1
848848- for _, r := range query {
849849- if r == '?' {
850850- fmt.Fprintf(&builder, "$%d", paramCount)
851851- paramCount++
852852- } else {
853853- builder.WriteRune(r)
854854- }
855855- }
856856- return builder.String()
857788}
858789859790func ParseSelector(selectorJSON *string) (*Selector, error) {
+22
backend/internal/db/pg_helpers.go
···11+package db
22+33+import (
44+ "database/sql/driver"
55+ "fmt"
66+ "strings"
77+)
88+99+type pqStringArray []string
1010+1111+func (a pqStringArray) Value() (driver.Value, error) {
1212+ if a == nil {
1313+ return "{}", nil
1414+ }
1515+ parts := make([]string, len(a))
1616+ for i, s := range a {
1717+ escaped := strings.ReplaceAll(s, "\\", "\\\\")
1818+ escaped = strings.ReplaceAll(escaped, "\"", "\\\"")
1919+ parts[i] = fmt.Sprintf(`"%s"`, escaped)
2020+ }
2121+ return "{" + strings.Join(parts, ",") + "}", nil
2222+}
+8-8
backend/internal/db/queries.go
···3535}
36363737func (db *DB) AnnotationExists(uri string) bool {
3838- var count int
3939- db.QueryRow(db.Rebind(`SELECT COUNT(*) FROM annotations WHERE uri = ?`), uri).Scan(&count)
4040- return count > 0
3838+ var exists bool
3939+ db.QueryRow(`SELECT EXISTS(SELECT 1 FROM annotations WHERE uri = $1)`, uri).Scan(&exists)
4040+ return exists
4141}
42424343func HashURL(rawURL string) string {
···71717272func (db *DB) GetAuthorByURI(uri string) (string, error) {
7373 var authorDID string
7474- err := db.QueryRow(db.Rebind(`SELECT author_did FROM annotations WHERE uri = ?`), uri).Scan(&authorDID)
7474+ err := db.QueryRow(`SELECT author_did FROM annotations WHERE uri = $1`, uri).Scan(&authorDID)
7575 if err == nil {
7676 return authorDID, nil
7777 }
78787979- err = db.QueryRow(db.Rebind(`SELECT author_did FROM highlights WHERE uri = ?`), uri).Scan(&authorDID)
7979+ err = db.QueryRow(`SELECT author_did FROM highlights WHERE uri = $1`, uri).Scan(&authorDID)
8080 if err == nil {
8181 return authorDID, nil
8282 }
83838484- err = db.QueryRow(db.Rebind(`SELECT author_did FROM bookmarks WHERE uri = ?`), uri).Scan(&authorDID)
8484+ err = db.QueryRow(`SELECT author_did FROM bookmarks WHERE uri = $1`, uri).Scan(&authorDID)
8585 if err == nil {
8686 return authorDID, nil
8787 }
···8989 return "", fmt.Errorf("uri not found or no author")
9090}
91919292-func buildPlaceholders(n int) string {
9292+func buildPlaceholders(n, startAt int) string {
9393 if n == 0 {
9494 return ""
9595 }
9696 placeholders := make([]string, n)
9797 for i := range placeholders {
9898- placeholders[i] = "?"
9898+ placeholders[i] = fmt.Sprintf("$%d", startAt+i)
9999 }
100100 return strings.Join(placeholders, ", ")
101101}
+110-125
backend/internal/db/queries_annotations.go
···55)
6677func (db *DB) CreateAnnotation(a *Annotation) error {
88- _, err := db.Exec(db.Rebind(`
88+ _, err := db.Exec(`
99 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)
1010- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1010+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
1111 ON CONFLICT(uri) DO UPDATE SET
1212- motivation = excluded.motivation,
1313- body_value = excluded.body_value,
1414- body_format = excluded.body_format,
1515- body_uri = excluded.body_uri,
1616- target_source = excluded.target_source,
1717- target_hash = excluded.target_hash,
1818- target_title = excluded.target_title,
1919- selector_json = excluded.selector_json,
2020- tags_json = excluded.tags_json,
2121- indexed_at = excluded.indexed_at,
2222- cid = excluded.cid
2323- `), 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)
1212+ motivation = EXCLUDED.motivation,
1313+ body_value = EXCLUDED.body_value,
1414+ body_format = EXCLUDED.body_format,
1515+ body_uri = EXCLUDED.body_uri,
1616+ target_source = EXCLUDED.target_source,
1717+ target_hash = EXCLUDED.target_hash,
1818+ target_title = EXCLUDED.target_title,
1919+ selector_json = EXCLUDED.selector_json,
2020+ tags_json = EXCLUDED.tags_json,
2121+ indexed_at = EXCLUDED.indexed_at,
2222+ cid = EXCLUDED.cid
2323+ `, 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)
2424 return err
2525}
26262727func (db *DB) GetAnnotationByURI(uri string) (*Annotation, error) {
2828 var a Annotation
2929- err := db.QueryRow(db.Rebind(`
2929+ err := db.QueryRow(`
3030 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
3131 FROM annotations
3232- WHERE uri = ?
3333- `), 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)
3232+ WHERE uri = $1
3333+ `, 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)
3434 if err != nil {
3535 return nil, err
3636 }
···3838}
39394040func (db *DB) GetAnnotationsByTargetHash(targetHash string, limit, offset int) ([]Annotation, error) {
4141- rows, err := db.Query(db.Rebind(`
4141+ rows, err := db.Query(`
4242 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
4343 FROM annotations
4444- WHERE target_hash = ?
4444+ WHERE target_hash = $1
4545 ORDER BY created_at DESC
4646- LIMIT ? OFFSET ?
4747- `), targetHash, limit, offset)
4646+ LIMIT $2 OFFSET $3
4747+ `, targetHash, limit, offset)
4848 if err != nil {
4949 return nil, err
5050 }
···5454}
55555656func (db *DB) GetAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
5757- rows, err := db.Query(db.Rebind(`
5757+ rows, err := db.Query(`
5858 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
5959 FROM annotations
6060- WHERE author_did = ?
6060+ WHERE author_did = $1
6161 ORDER BY created_at DESC
6262- LIMIT ? OFFSET ?
6363- `), authorDID, limit, offset)
6262+ LIMIT $2 OFFSET $3
6363+ `, authorDID, limit, offset)
6464 if err != nil {
6565 return nil, err
6666 }
···7070}
71717272func (db *DB) GetMarginAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
7373- rows, err := db.Query(db.Rebind(`
7373+ rows, err := db.Query(`
7474 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
7575 FROM annotations
7676- WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%'
7676+ WHERE author_did = $1 AND uri NOT LIKE '%network.cosmik%'
7777 ORDER BY created_at DESC
7878- LIMIT ? OFFSET ?
7979- `), authorDID, limit, offset)
7878+ LIMIT $2 OFFSET $3
7979+ `, authorDID, limit, offset)
8080 if err != nil {
8181 return nil, err
8282 }
···8686}
87878888func (db *DB) GetSembleAnnotationsByAuthor(authorDID string, limit, offset int) ([]Annotation, error) {
8989- rows, err := db.Query(db.Rebind(`
8989+ rows, err := db.Query(`
9090 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
9191 FROM annotations
9292- WHERE author_did = ? AND uri LIKE '%network.cosmik%'
9292+ WHERE author_did = $1 AND uri LIKE '%network.cosmik%'
9393 ORDER BY created_at DESC
9494- LIMIT ? OFFSET ?
9595- `), authorDID, limit, offset)
9494+ LIMIT $2 OFFSET $3
9595+ `, authorDID, limit, offset)
9696 if err != nil {
9797 return nil, err
9898 }
···102102}
103103104104func (db *DB) GetAnnotationsByMotivation(motivation string, limit, offset int) ([]Annotation, error) {
105105- rows, err := db.Query(db.Rebind(`
105105+ rows, err := db.Query(`
106106 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
107107 FROM annotations
108108- WHERE motivation = ?
108108+ WHERE motivation = $1
109109 ORDER BY created_at DESC
110110- LIMIT ? OFFSET ?
111111- `), motivation, limit, offset)
110110+ LIMIT $2 OFFSET $3
111111+ `, motivation, limit, offset)
112112 if err != nil {
113113 return nil, err
114114 }
···118118}
119119120120func (db *DB) GetRecentAnnotations(limit, offset int) ([]Annotation, error) {
121121- rows, err := db.Query(db.Rebind(`
121121+ rows, err := db.Query(`
122122 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
123123 FROM annotations
124124 ORDER BY created_at DESC
125125- LIMIT ? OFFSET ?
126126- `), limit, offset)
125125+ LIMIT $1 OFFSET $2
126126+ `, limit, offset)
127127 if err != nil {
128128 return nil, err
129129 }
···134134135135func (db *DB) GetPopularAnnotations(limit, offset int) ([]Annotation, error) {
136136 since := time.Now().AddDate(0, 0, -14)
137137- rows, err := db.Query(db.Rebind(`
138138- SELECT
139139- a.uri, a.author_did, a.motivation, a.body_value, a.body_format,
140140- a.body_uri, a.target_source, a.target_hash, a.target_title,
137137+ rows, err := db.Query(`
138138+ SELECT
139139+ a.uri, a.author_did, a.motivation, a.body_value, a.body_format,
140140+ a.body_uri, a.target_source, a.target_hash, a.target_title,
141141 a.selector_json, a.tags_json, a.created_at, a.indexed_at, a.cid
142142 FROM annotations a
143143- LEFT JOIN (
144144- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
145145- ) l ON l.subject_uri = a.uri
146146- LEFT JOIN (
147147- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
148148- ) r ON r.root_uri = a.uri
149149- WHERE a.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) > 0
150150- ORDER BY (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) DESC, a.created_at DESC
151151- LIMIT ? OFFSET ?
152152- `), since, limit, offset)
143143+ LEFT JOIN LATERAL (
144144+ SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = a.uri
145145+ ) l ON true
146146+ LEFT JOIN LATERAL (
147147+ SELECT COUNT(*) as cnt FROM replies WHERE root_uri = a.uri
148148+ ) r ON true
149149+ WHERE a.created_at > $1 AND (l.cnt + r.cnt) > 0
150150+ ORDER BY (l.cnt + r.cnt) DESC, a.created_at DESC
151151+ LIMIT $2 OFFSET $3
152152+ `, since, limit, offset)
153153 if err != nil {
154154 return nil, err
155155 }
···161161func (db *DB) GetShelvedAnnotations(limit, offset int) ([]Annotation, error) {
162162 olderThan := time.Now().AddDate(0, 0, -1)
163163 since := time.Now().AddDate(0, 0, -14)
164164- rows, err := db.Query(db.Rebind(`
165165- SELECT
166166- a.uri, a.author_did, a.motivation, a.body_value, a.body_format,
167167- a.body_uri, a.target_source, a.target_hash, a.target_title,
164164+ rows, err := db.Query(`
165165+ SELECT
166166+ a.uri, a.author_did, a.motivation, a.body_value, a.body_format,
167167+ a.body_uri, a.target_source, a.target_hash, a.target_title,
168168 a.selector_json, a.tags_json, a.created_at, a.indexed_at, a.cid
169169 FROM annotations a
170170- LEFT JOIN (
171171- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
172172- ) l ON l.subject_uri = a.uri
173173- LEFT JOIN (
174174- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
175175- ) r ON r.root_uri = a.uri
176176- WHERE a.created_at < ? AND a.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) = 0
170170+ WHERE a.created_at < $1 AND a.created_at > $2
171171+ AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = a.uri)
172172+ AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = a.uri)
177173 ORDER BY RANDOM()
178178- LIMIT ? OFFSET ?
179179- `), olderThan, since, limit, offset)
174174+ LIMIT $3 OFFSET $4
175175+ `, olderThan, since, limit, offset)
180176 if err != nil {
181177 return nil, err
182178 }
···186182}
187183188184func (db *DB) GetMarginAnnotations(limit, offset int) ([]Annotation, error) {
189189- rows, err := db.Query(db.Rebind(`
185185+ rows, err := db.Query(`
190186 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
191187 FROM annotations
192188 WHERE uri NOT LIKE '%network.cosmik%'
193189 ORDER BY created_at DESC
194194- LIMIT ? OFFSET ?
195195- `), limit, offset)
190190+ LIMIT $1 OFFSET $2
191191+ `, limit, offset)
196192 if err != nil {
197193 return nil, err
198194 }
···202198}
203199204200func (db *DB) GetSembleAnnotations(limit, offset int) ([]Annotation, error) {
205205- rows, err := db.Query(db.Rebind(`
201201+ rows, err := db.Query(`
206202 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
207203 FROM annotations
208204 WHERE uri LIKE '%network.cosmik%'
209205 ORDER BY created_at DESC
210210- LIMIT ? OFFSET ?
211211- `), limit, offset)
206206+ LIMIT $1 OFFSET $2
207207+ `, limit, offset)
212208 if err != nil {
213209 return nil, err
214210 }
···218214}
219215220216func (db *DB) GetAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
221221- pattern := "%\"" + tag + "\"%"
222222- rows, err := db.Query(db.Rebind(`
217217+ rows, err := db.Query(`
223218 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
224219 FROM annotations
225225- WHERE tags_json LIKE ?
220220+ WHERE tags_json::jsonb ? $1
226221 ORDER BY created_at DESC
227227- LIMIT ? OFFSET ?
228228- `), pattern, limit, offset)
222222+ LIMIT $2 OFFSET $3
223223+ `, tag, limit, offset)
229224 if err != nil {
230225 return nil, err
231226 }
···235230}
236231237232func (db *DB) GetMarginAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
238238- pattern := "%\"" + tag + "\"%"
239239- rows, err := db.Query(db.Rebind(`
233233+ rows, err := db.Query(`
240234 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
241235 FROM annotations
242242- WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
236236+ WHERE tags_json::jsonb ? $1 AND uri NOT LIKE '%network.cosmik%'
243237 ORDER BY created_at DESC
244244- LIMIT ? OFFSET ?
245245- `), pattern, limit, offset)
238238+ LIMIT $2 OFFSET $3
239239+ `, tag, limit, offset)
246240 if err != nil {
247241 return nil, err
248242 }
···252246}
253247254248func (db *DB) GetSembleAnnotationsByTag(tag string, limit, offset int) ([]Annotation, error) {
255255- pattern := "%\"" + tag + "\"%"
256256- rows, err := db.Query(db.Rebind(`
249249+ rows, err := db.Query(`
257250 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
258251 FROM annotations
259259- WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%'
252252+ WHERE tags_json::jsonb ? $1 AND uri LIKE '%network.cosmik%'
260253 ORDER BY created_at DESC
261261- LIMIT ? OFFSET ?
262262- `), pattern, limit, offset)
254254+ LIMIT $2 OFFSET $3
255255+ `, tag, limit, offset)
263256 if err != nil {
264257 return nil, err
265258 }
···269262}
270263271264func (db *DB) DeleteAnnotation(uri string) error {
272272- _, err := db.Exec(db.Rebind(`DELETE FROM annotations WHERE uri = ?`), uri)
265265+ _, err := db.Exec(`DELETE FROM annotations WHERE uri = $1`, uri)
273266 return err
274267}
275268276269func (db *DB) UpdateAnnotation(uri, bodyValue, tagsJSON, cid string) error {
277277- _, err := db.Exec(db.Rebind(`
278278- UPDATE annotations
279279- SET body_value = ?, tags_json = ?, cid = ?, indexed_at = ?
280280- WHERE uri = ?
281281- `), bodyValue, tagsJSON, cid, time.Now(), uri)
270270+ _, err := db.Exec(`
271271+ UPDATE annotations
272272+ SET body_value = $1, tags_json = $2, cid = $3, indexed_at = $4
273273+ WHERE uri = $5
274274+ `, bodyValue, tagsJSON, cid, time.Now(), uri)
282275 return err
283276}
284277285278func (db *DB) GetAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
286286- pattern := "%\"" + tag + "\"%"
287287- rows, err := db.Query(db.Rebind(`
279279+ rows, err := db.Query(`
288280 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
289281 FROM annotations
290290- WHERE author_did = ? AND tags_json LIKE ?
282282+ WHERE author_did = $1 AND tags_json::jsonb ? $2
291283 ORDER BY created_at DESC
292292- LIMIT ? OFFSET ?
293293- `), authorDID, pattern, limit, offset)
284284+ LIMIT $3 OFFSET $4
285285+ `, authorDID, tag, limit, offset)
294286 if err != nil {
295287 return nil, err
296288 }
···300292}
301293302294func (db *DB) GetMarginAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
303303- pattern := "%\"" + tag + "\"%"
304304- rows, err := db.Query(db.Rebind(`
295295+ rows, err := db.Query(`
305296 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
306297 FROM annotations
307307- WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
298298+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri NOT LIKE '%network.cosmik%'
308299 ORDER BY created_at DESC
309309- LIMIT ? OFFSET ?
310310- `), authorDID, pattern, limit, offset)
300300+ LIMIT $3 OFFSET $4
301301+ `, authorDID, tag, limit, offset)
311302 if err != nil {
312303 return nil, err
313304 }
···317308}
318309319310func (db *DB) GetSembleAnnotationsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Annotation, error) {
320320- pattern := "%\"" + tag + "\"%"
321321- rows, err := db.Query(db.Rebind(`
311311+ rows, err := db.Query(`
322312 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
323313 FROM annotations
324324- WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%'
314314+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri LIKE '%network.cosmik%'
325315 ORDER BY created_at DESC
326326- LIMIT ? OFFSET ?
327327- `), authorDID, pattern, limit, offset)
316316+ LIMIT $3 OFFSET $4
317317+ `, authorDID, tag, limit, offset)
328318 if err != nil {
329319 return nil, err
330320 }
···334324}
335325336326func (db *DB) GetAnnotationsByAuthorAndTargetHash(authorDID, targetHash string, limit, offset int) ([]Annotation, error) {
337337- rows, err := db.Query(db.Rebind(`
327327+ rows, err := db.Query(`
338328 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
339329 FROM annotations
340340- WHERE author_did = ? AND target_hash = ?
330330+ WHERE author_did = $1 AND target_hash = $2
341331 ORDER BY created_at DESC
342342- LIMIT ? OFFSET ?
343343- `), authorDID, targetHash, limit, offset)
332332+ LIMIT $3 OFFSET $4
333333+ `, authorDID, targetHash, limit, offset)
344334 if err != nil {
345335 return nil, err
346336 }
···354344 return []Annotation{}, nil
355345 }
356346357357- query := db.Rebind(`
347347+ query := `
358348 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
359349 FROM annotations
360360- WHERE uri IN (` + buildPlaceholders(len(uris)) + `)
361361- `)
350350+ WHERE uri = ANY($1)
351351+ `
362352363363- args := make([]interface{}, len(uris))
364364- for i, uri := range uris {
365365- args[i] = uri
366366- }
367367-368368- rows, err := db.Query(query, args...)
353353+ rows, err := db.Query(query, pqStringArray(uris))
369354 if err != nil {
370355 return nil, err
371356 }
···375360}
376361377362func (db *DB) GetAnnotationURIs(authorDID string) ([]string, error) {
378378- rows, err := db.Query(db.Rebind(`
379379- SELECT uri FROM annotations WHERE author_did = ?
380380- `), authorDID)
363363+ rows, err := db.Query(`
364364+ SELECT uri FROM annotations WHERE author_did = $1
365365+ `, authorDID)
381366 if err != nil {
382367 return nil, err
383368 }
+117-247
backend/internal/db/queries_bookmarks.go
···55)
6677func (db *DB) CreateBookmark(b *Bookmark) error {
88- _, err := db.Exec(db.Rebind(`
88+ _, err := db.Exec(`
99 INSERT INTO bookmarks (uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid)
1010- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1010+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
1111 ON CONFLICT(uri) DO UPDATE SET
1212- source = excluded.source,
1313- source_hash = excluded.source_hash,
1414- title = excluded.title,
1515- description = excluded.description,
1616- tags_json = excluded.tags_json,
1717- indexed_at = excluded.indexed_at,
1818- cid = excluded.cid
1919- `), b.URI, b.AuthorDID, b.Source, b.SourceHash, b.Title, b.Description, b.TagsJSON, b.CreatedAt, b.IndexedAt, b.CID)
1212+ source = EXCLUDED.source,
1313+ source_hash = EXCLUDED.source_hash,
1414+ title = EXCLUDED.title,
1515+ description = EXCLUDED.description,
1616+ tags_json = EXCLUDED.tags_json,
1717+ indexed_at = EXCLUDED.indexed_at,
1818+ cid = EXCLUDED.cid
1919+ `, b.URI, b.AuthorDID, b.Source, b.SourceHash, b.Title, b.Description, b.TagsJSON, b.CreatedAt, b.IndexedAt, b.CID)
2020 return err
2121}
22222323func (db *DB) GetBookmarkByURI(uri string) (*Bookmark, error) {
2424 var b Bookmark
2525- err := db.QueryRow(db.Rebind(`
2525+ err := db.QueryRow(`
2626 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
2727 FROM bookmarks
2828- WHERE uri = ?
2929- `), uri).Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID)
2828+ WHERE uri = $1
2929+ `, uri).Scan(&b.URI, &b.AuthorDID, &b.Source, &b.SourceHash, &b.Title, &b.Description, &b.TagsJSON, &b.CreatedAt, &b.IndexedAt, &b.CID)
3030 if err != nil {
3131 return nil, err
3232 }
···3434}
35353636func (db *DB) GetRecentBookmarks(limit, offset int) ([]Bookmark, error) {
3737- rows, err := db.Query(db.Rebind(`
3737+ rows, err := db.Query(`
3838 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
3939 FROM bookmarks
4040 ORDER BY created_at DESC
4141- LIMIT ? OFFSET ?
4242- `), limit, offset)
4141+ LIMIT $1 OFFSET $2
4242+ `, limit, offset)
4343 if err != nil {
4444 return nil, err
4545 }
4646 defer rows.Close()
47474848- var bookmarks []Bookmark
4949- for rows.Next() {
5050- var b Bookmark
5151- 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 {
5252- return nil, err
5353- }
5454- bookmarks = append(bookmarks, b)
5555- }
5656- return bookmarks, nil
4848+ return scanBookmarks(rows)
5749}
58505951func (db *DB) GetPopularBookmarks(limit, offset int) ([]Bookmark, error) {
6052 since := time.Now().AddDate(0, 0, -14)
6161- rows, err := db.Query(db.Rebind(`
6262- SELECT
6363- b.uri, b.author_did, b.source, b.source_hash, b.title,
5353+ rows, err := db.Query(`
5454+ SELECT
5555+ b.uri, b.author_did, b.source, b.source_hash, b.title,
6456 b.description, b.tags_json, b.created_at, b.indexed_at, b.cid
6557 FROM bookmarks b
6666- LEFT JOIN (
6767- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
6868- ) l ON l.subject_uri = b.uri
6969- LEFT JOIN (
7070- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
7171- ) r ON r.root_uri = b.uri
7272- WHERE b.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) > 0
7373- ORDER BY (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) DESC, b.created_at DESC
7474- LIMIT ? OFFSET ?
7575- `), since, limit, offset)
5858+ LEFT JOIN LATERAL (
5959+ SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = b.uri
6060+ ) l ON true
6161+ LEFT JOIN LATERAL (
6262+ SELECT COUNT(*) as cnt FROM replies WHERE root_uri = b.uri
6363+ ) r ON true
6464+ WHERE b.created_at > $1 AND (l.cnt + r.cnt) > 0
6565+ ORDER BY (l.cnt + r.cnt) DESC, b.created_at DESC
6666+ LIMIT $2 OFFSET $3
6767+ `, since, limit, offset)
7668 if err != nil {
7769 return nil, err
7870 }
7971 defer rows.Close()
80728181- var bookmarks []Bookmark
8282- for rows.Next() {
8383- var b Bookmark
8484- 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 {
8585- return nil, err
8686- }
8787- bookmarks = append(bookmarks, b)
8888- }
8989- return bookmarks, nil
7373+ return scanBookmarks(rows)
9074}
91759276func (db *DB) GetShelvedBookmarks(limit, offset int) ([]Bookmark, error) {
9377 olderThan := time.Now().AddDate(0, 0, -1)
9478 since := time.Now().AddDate(0, 0, -14)
9595- rows, err := db.Query(db.Rebind(`
9696- SELECT
9797- b.uri, b.author_did, b.source, b.source_hash, b.title,
7979+ rows, err := db.Query(`
8080+ SELECT
8181+ b.uri, b.author_did, b.source, b.source_hash, b.title,
9882 b.description, b.tags_json, b.created_at, b.indexed_at, b.cid
9983 FROM bookmarks b
100100- LEFT JOIN (
101101- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
102102- ) l ON l.subject_uri = b.uri
103103- LEFT JOIN (
104104- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
105105- ) r ON r.root_uri = b.uri
106106- WHERE b.created_at < ? AND b.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) = 0
8484+ WHERE b.created_at < $1 AND b.created_at > $2
8585+ AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = b.uri)
8686+ AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = b.uri)
10787 ORDER BY RANDOM()
108108- LIMIT ? OFFSET ?
109109- `), olderThan, since, limit, offset)
8888+ LIMIT $3 OFFSET $4
8989+ `, olderThan, since, limit, offset)
11090 if err != nil {
11191 return nil, err
11292 }
11393 defer rows.Close()
11494115115- var bookmarks []Bookmark
116116- for rows.Next() {
117117- var b Bookmark
118118- 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 {
119119- return nil, err
120120- }
121121- bookmarks = append(bookmarks, b)
122122- }
123123- return bookmarks, nil
9595+ return scanBookmarks(rows)
12496}
1259712698func (db *DB) GetMarginBookmarks(limit, offset int) ([]Bookmark, error) {
127127- rows, err := db.Query(db.Rebind(`
9999+ rows, err := db.Query(`
128100 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
129101 FROM bookmarks
130102 WHERE uri NOT LIKE '%network.cosmik%'
131103 ORDER BY created_at DESC
132132- LIMIT ? OFFSET ?
133133- `), limit, offset)
104104+ LIMIT $1 OFFSET $2
105105+ `, limit, offset)
134106 if err != nil {
135107 return nil, err
136108 }
137109 defer rows.Close()
138110139139- var bookmarks []Bookmark
140140- for rows.Next() {
141141- var b Bookmark
142142- 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 {
143143- return nil, err
144144- }
145145- bookmarks = append(bookmarks, b)
146146- }
147147- return bookmarks, nil
111111+ return scanBookmarks(rows)
148112}
149113150114func (db *DB) GetSembleBookmarks(limit, offset int) ([]Bookmark, error) {
151151- rows, err := db.Query(db.Rebind(`
115115+ rows, err := db.Query(`
152116 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
153117 FROM bookmarks
154118 WHERE uri LIKE '%network.cosmik%'
155119 ORDER BY created_at DESC
156156- LIMIT ? OFFSET ?
157157- `), limit, offset)
120120+ LIMIT $1 OFFSET $2
121121+ `, limit, offset)
158122 if err != nil {
159123 return nil, err
160124 }
161125 defer rows.Close()
162126163163- var bookmarks []Bookmark
164164- for rows.Next() {
165165- var b Bookmark
166166- 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 {
167167- return nil, err
168168- }
169169- bookmarks = append(bookmarks, b)
170170- }
171171- return bookmarks, nil
127127+ return scanBookmarks(rows)
172128}
173129174130func (db *DB) GetBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
175175- pattern := "%\"" + tag + "\"%"
176176- rows, err := db.Query(db.Rebind(`
131131+ rows, err := db.Query(`
177132 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
178133 FROM bookmarks
179179- WHERE tags_json LIKE ?
134134+ WHERE tags_json::jsonb ? $1
180135 ORDER BY created_at DESC
181181- LIMIT ? OFFSET ?
182182- `), pattern, limit, offset)
136136+ LIMIT $2 OFFSET $3
137137+ `, tag, limit, offset)
183138 if err != nil {
184139 return nil, err
185140 }
186141 defer rows.Close()
187142188188- var bookmarks []Bookmark
189189- for rows.Next() {
190190- var b Bookmark
191191- 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 {
192192- return nil, err
193193- }
194194- bookmarks = append(bookmarks, b)
195195- }
196196- return bookmarks, nil
143143+ return scanBookmarks(rows)
197144}
198145199146func (db *DB) GetMarginBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
200200- pattern := "%\"" + tag + "\"%"
201201- rows, err := db.Query(db.Rebind(`
147147+ rows, err := db.Query(`
202148 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
203149 FROM bookmarks
204204- WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
150150+ WHERE tags_json::jsonb ? $1 AND uri NOT LIKE '%network.cosmik%'
205151 ORDER BY created_at DESC
206206- LIMIT ? OFFSET ?
207207- `), pattern, limit, offset)
152152+ LIMIT $2 OFFSET $3
153153+ `, tag, limit, offset)
208154 if err != nil {
209155 return nil, err
210156 }
211157 defer rows.Close()
212158213213- var bookmarks []Bookmark
214214- for rows.Next() {
215215- var b Bookmark
216216- 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 {
217217- return nil, err
218218- }
219219- bookmarks = append(bookmarks, b)
220220- }
221221- return bookmarks, nil
159159+ return scanBookmarks(rows)
222160}
223161224162func (db *DB) GetSembleBookmarksByTag(tag string, limit, offset int) ([]Bookmark, error) {
225225- pattern := "%\"" + tag + "\"%"
226226- rows, err := db.Query(db.Rebind(`
163163+ rows, err := db.Query(`
227164 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
228165 FROM bookmarks
229229- WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%'
166166+ WHERE tags_json::jsonb ? $1 AND uri LIKE '%network.cosmik%'
230167 ORDER BY created_at DESC
231231- LIMIT ? OFFSET ?
232232- `), pattern, limit, offset)
168168+ LIMIT $2 OFFSET $3
169169+ `, tag, limit, offset)
233170 if err != nil {
234171 return nil, err
235172 }
236173 defer rows.Close()
237174238238- var bookmarks []Bookmark
239239- for rows.Next() {
240240- var b Bookmark
241241- 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 {
242242- return nil, err
243243- }
244244- bookmarks = append(bookmarks, b)
245245- }
246246- return bookmarks, nil
175175+ return scanBookmarks(rows)
247176}
248177249178func (db *DB) GetBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
250250- pattern := "%\"" + tag + "\"%"
251251- rows, err := db.Query(db.Rebind(`
179179+ rows, err := db.Query(`
252180 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
253181 FROM bookmarks
254254- WHERE author_did = ? AND tags_json LIKE ?
182182+ WHERE author_did = $1 AND tags_json::jsonb ? $2
255183 ORDER BY created_at DESC
256256- LIMIT ? OFFSET ?
257257- `), authorDID, pattern, limit, offset)
184184+ LIMIT $3 OFFSET $4
185185+ `, authorDID, tag, limit, offset)
258186 if err != nil {
259187 return nil, err
260188 }
261189 defer rows.Close()
262190263263- var bookmarks []Bookmark
264264- for rows.Next() {
265265- var b Bookmark
266266- 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 {
267267- return nil, err
268268- }
269269- bookmarks = append(bookmarks, b)
270270- }
271271- return bookmarks, nil
191191+ return scanBookmarks(rows)
272192}
273193274194func (db *DB) GetMarginBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
275275- pattern := "%\"" + tag + "\"%"
276276- rows, err := db.Query(db.Rebind(`
195195+ rows, err := db.Query(`
277196 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
278197 FROM bookmarks
279279- WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
198198+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri NOT LIKE '%network.cosmik%'
280199 ORDER BY created_at DESC
281281- LIMIT ? OFFSET ?
282282- `), authorDID, pattern, limit, offset)
200200+ LIMIT $3 OFFSET $4
201201+ `, authorDID, tag, limit, offset)
283202 if err != nil {
284203 return nil, err
285204 }
286205 defer rows.Close()
287206288288- var bookmarks []Bookmark
289289- for rows.Next() {
290290- var b Bookmark
291291- 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 {
292292- return nil, err
293293- }
294294- bookmarks = append(bookmarks, b)
295295- }
296296- return bookmarks, nil
207207+ return scanBookmarks(rows)
297208}
298209299210func (db *DB) GetSembleBookmarksByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Bookmark, error) {
300300- pattern := "%\"" + tag + "\"%"
301301- rows, err := db.Query(db.Rebind(`
211211+ rows, err := db.Query(`
302212 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
303213 FROM bookmarks
304304- WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%'
214214+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri LIKE '%network.cosmik%'
305215 ORDER BY created_at DESC
306306- LIMIT ? OFFSET ?
307307- `), authorDID, pattern, limit, offset)
216216+ LIMIT $3 OFFSET $4
217217+ `, authorDID, tag, limit, offset)
308218 if err != nil {
309219 return nil, err
310220 }
311221 defer rows.Close()
312222313313- var bookmarks []Bookmark
314314- for rows.Next() {
315315- var b Bookmark
316316- 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 {
317317- return nil, err
318318- }
319319- bookmarks = append(bookmarks, b)
320320- }
321321- return bookmarks, nil
223223+ return scanBookmarks(rows)
322224}
323225324226func (db *DB) GetBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
325325- rows, err := db.Query(db.Rebind(`
227227+ rows, err := db.Query(`
326228 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
327229 FROM bookmarks
328328- WHERE author_did = ?
230230+ WHERE author_did = $1
329231 ORDER BY created_at DESC
330330- LIMIT ? OFFSET ?
331331- `), authorDID, limit, offset)
232232+ LIMIT $2 OFFSET $3
233233+ `, authorDID, limit, offset)
332234 if err != nil {
333235 return nil, err
334236 }
335237 defer rows.Close()
336238337337- var bookmarks []Bookmark
338338- for rows.Next() {
339339- var b Bookmark
340340- 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 {
341341- return nil, err
342342- }
343343- bookmarks = append(bookmarks, b)
344344- }
345345- return bookmarks, nil
239239+ return scanBookmarks(rows)
346240}
347241348242func (db *DB) GetMarginBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
349349- rows, err := db.Query(db.Rebind(`
243243+ rows, err := db.Query(`
350244 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
351245 FROM bookmarks
352352- WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%'
246246+ WHERE author_did = $1 AND uri NOT LIKE '%network.cosmik%'
353247 ORDER BY created_at DESC
354354- LIMIT ? OFFSET ?
355355- `), authorDID, limit, offset)
248248+ LIMIT $2 OFFSET $3
249249+ `, authorDID, limit, offset)
356250 if err != nil {
357251 return nil, err
358252 }
359253 defer rows.Close()
360254361361- var bookmarks []Bookmark
362362- for rows.Next() {
363363- var b Bookmark
364364- 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 {
365365- return nil, err
366366- }
367367- bookmarks = append(bookmarks, b)
368368- }
369369- return bookmarks, nil
255255+ return scanBookmarks(rows)
370256}
371257372258func (db *DB) GetSembleBookmarksByAuthor(authorDID string, limit, offset int) ([]Bookmark, error) {
373373- rows, err := db.Query(db.Rebind(`
259259+ rows, err := db.Query(`
374260 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
375261 FROM bookmarks
376376- WHERE author_did = ? AND uri LIKE '%network.cosmik%'
262262+ WHERE author_did = $1 AND uri LIKE '%network.cosmik%'
377263 ORDER BY created_at DESC
378378- LIMIT ? OFFSET ?
379379- `), authorDID, limit, offset)
264264+ LIMIT $2 OFFSET $3
265265+ `, authorDID, limit, offset)
380266 if err != nil {
381267 return nil, err
382268 }
383269 defer rows.Close()
384270385385- var bookmarks []Bookmark
386386- for rows.Next() {
387387- var b Bookmark
388388- 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 {
389389- return nil, err
390390- }
391391- bookmarks = append(bookmarks, b)
392392- }
393393- return bookmarks, nil
271271+ return scanBookmarks(rows)
394272}
395273396274func (db *DB) DeleteBookmark(uri string) error {
397397- _, err := db.Exec(db.Rebind(`DELETE FROM bookmarks WHERE uri = ?`), uri)
275275+ _, err := db.Exec(`DELETE FROM bookmarks WHERE uri = $1`, uri)
398276 return err
399277}
400278401279func (db *DB) UpdateBookmark(uri, title, description, tagsJSON, cid string) error {
402402- _, err := db.Exec(db.Rebind(`
403403- UPDATE bookmarks
404404- SET title = ?, description = ?, tags_json = ?, cid = ?, indexed_at = ?
405405- WHERE uri = ?
406406- `), title, description, tagsJSON, cid, time.Now(), uri)
280280+ _, err := db.Exec(`
281281+ UPDATE bookmarks
282282+ SET title = $1, description = $2, tags_json = $3, cid = $4, indexed_at = $5
283283+ WHERE uri = $6
284284+ `, title, description, tagsJSON, cid, time.Now(), uri)
407285 return err
408286}
409287···412290 return []Bookmark{}, nil
413291 }
414292415415- query := db.Rebind(`
293293+ rows, err := db.Query(`
416294 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
417295 FROM bookmarks
418418- WHERE uri IN (` + buildPlaceholders(len(uris)) + `)
419419- `)
420420-421421- args := make([]interface{}, len(uris))
422422- for i, uri := range uris {
423423- args[i] = uri
424424- }
425425-426426- rows, err := db.Query(query, args...)
296296+ WHERE uri = ANY($1)
297297+ `, pqStringArray(uris))
427298 if err != nil {
428299 return nil, err
429300 }
430301 defer rows.Close()
431302432432- var bookmarks []Bookmark
433433- for rows.Next() {
434434- var b Bookmark
435435- 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 {
436436- return nil, err
437437- }
438438- bookmarks = append(bookmarks, b)
439439- }
440440- return bookmarks, nil
303303+ return scanBookmarks(rows)
441304}
442305443306func (db *DB) GetBookmarkURIs(authorDID string) ([]string, error) {
444444- rows, err := db.Query(db.Rebind(`
445445- SELECT uri FROM bookmarks WHERE author_did = ?
446446- `), authorDID)
307307+ rows, err := db.Query(`
308308+ SELECT uri FROM bookmarks WHERE author_did = $1
309309+ `, authorDID)
447310 if err != nil {
448311 return nil, err
449312 }
···461324}
462325463326func (db *DB) GetBookmarksByTargetHash(targetHash string, limit, offset int) ([]Bookmark, error) {
464464- rows, err := db.Query(db.Rebind(`
327327+ rows, err := db.Query(`
465328 SELECT uri, author_did, source, source_hash, title, description, tags_json, created_at, indexed_at, cid
466329 FROM bookmarks
467467- WHERE source_hash = ?
330330+ WHERE source_hash = $1
468331 ORDER BY created_at DESC
469469- LIMIT ? OFFSET ?
470470- `), targetHash, limit, offset)
332332+ LIMIT $2 OFFSET $3
333333+ `, targetHash, limit, offset)
471334 if err != nil {
472335 return nil, err
473336 }
474337 defer rows.Close()
475338339339+ return scanBookmarks(rows)
340340+}
341341+342342+func scanBookmarks(rows interface {
343343+ Next() bool
344344+ Scan(...interface{}) error
345345+}) ([]Bookmark, error) {
476346 var bookmarks []Bookmark
477347 for rows.Next() {
478348 var b Bookmark
+79-115
backend/internal/db/queries_collections.go
···33import "time"
4455func (db *DB) CreateCollection(c *Collection) error {
66- _, err := db.Exec(db.Rebind(`
66+ _, err := db.Exec(`
77 INSERT INTO collections (uri, author_did, name, description, icon, created_at, indexed_at)
88- VALUES (?, ?, ?, ?, ?, ?, ?)
88+ VALUES ($1, $2, $3, $4, $5, $6, $7)
99 ON CONFLICT(uri) DO UPDATE SET
1010- name = excluded.name,
1111- description = excluded.description,
1212- icon = excluded.icon,
1313- indexed_at = excluded.indexed_at
1414- `), c.URI, c.AuthorDID, c.Name, c.Description, c.Icon, c.CreatedAt, c.IndexedAt)
1010+ name = EXCLUDED.name,
1111+ description = EXCLUDED.description,
1212+ icon = EXCLUDED.icon,
1313+ indexed_at = EXCLUDED.indexed_at
1414+ `, c.URI, c.AuthorDID, c.Name, c.Description, c.Icon, c.CreatedAt, c.IndexedAt)
1515 return err
1616}
17171818func (db *DB) GetCollectionsByAuthor(authorDID string) ([]Collection, error) {
1919- rows, err := db.Query(db.Rebind(`
1919+ rows, err := db.Query(`
2020 SELECT uri, author_did, name, description, icon, created_at, indexed_at
2121 FROM collections
2222- WHERE author_did = ?
2222+ WHERE author_did = $1
2323 ORDER BY created_at DESC
2424- `), authorDID)
2424+ `, authorDID)
2525 if err != nil {
2626 return nil, err
2727 }
···40404141func (db *DB) GetCollectionByURI(uri string) (*Collection, error) {
4242 var c Collection
4343- err := db.QueryRow(db.Rebind(`
4343+ err := db.QueryRow(`
4444 SELECT uri, author_did, name, description, icon, created_at, indexed_at
4545 FROM collections
4646- WHERE uri = ?
4747- `), uri).Scan(&c.URI, &c.AuthorDID, &c.Name, &c.Description, &c.Icon, &c.CreatedAt, &c.IndexedAt)
4646+ WHERE uri = $1
4747+ `, uri).Scan(&c.URI, &c.AuthorDID, &c.Name, &c.Description, &c.Icon, &c.CreatedAt, &c.IndexedAt)
4848 if err != nil {
4949 return nil, err
5050 }
···5252}
53535454func (db *DB) DeleteCollection(uri string) error {
5555-5656- db.Exec(db.Rebind(`DELETE FROM collection_items WHERE collection_uri = ?`), uri)
5757- _, err := db.Exec(db.Rebind(`DELETE FROM collections WHERE uri = ?`), uri)
5555+ db.Exec(`DELETE FROM collection_items WHERE collection_uri = $1`, uri)
5656+ _, err := db.Exec(`DELETE FROM collections WHERE uri = $1`, uri)
5857 return err
5958}
60596160func (db *DB) AddToCollection(item *CollectionItem) error {
6262- _, err := db.Exec(db.Rebind(`
6161+ _, err := db.Exec(`
6362 INSERT INTO collection_items (uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at)
6464- VALUES (?, ?, ?, ?, ?, ?, ?)
6363+ VALUES ($1, $2, $3, $4, $5, $6, $7)
6564 ON CONFLICT(uri) DO UPDATE SET
6666- position = excluded.position,
6767- indexed_at = excluded.indexed_at
6868- `), item.URI, item.AuthorDID, item.CollectionURI, item.AnnotationURI, item.Position, item.CreatedAt, item.IndexedAt)
6565+ position = EXCLUDED.position,
6666+ indexed_at = EXCLUDED.indexed_at
6767+ `, item.URI, item.AuthorDID, item.CollectionURI, item.AnnotationURI, item.Position, item.CreatedAt, item.IndexedAt)
6968 return err
7069}
71707271func (db *DB) GetCollectionItems(collectionURI string) ([]CollectionItem, error) {
7373- rows, err := db.Query(db.Rebind(`
7272+ rows, err := db.Query(`
7473 SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at
7574 FROM collection_items
7676- WHERE collection_uri = ?
7575+ WHERE collection_uri = $1
7776 ORDER BY position ASC, created_at DESC
7878- `), collectionURI)
7777+ `, collectionURI)
7978 if err != nil {
8079 return nil, err
8180 }
···9392}
94939594func (db *DB) RemoveFromCollection(uri string) error {
9696- _, err := db.Exec(db.Rebind(`DELETE FROM collection_items WHERE uri = ?`), uri)
9595+ _, err := db.Exec(`DELETE FROM collection_items WHERE uri = $1`, uri)
9796 return err
9897}
999810099func (db *DB) GetRecentCollectionItems(limit, offset int) ([]CollectionItem, error) {
101101- rows, err := db.Query(db.Rebind(`
100100+ rows, err := db.Query(`
102101 SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at
103102 FROM collection_items
104103 ORDER BY created_at DESC
105105- LIMIT ? OFFSET ?
106106- `), limit, offset)
104104+ LIMIT $1 OFFSET $2
105105+ `, limit, offset)
107106 if err != nil {
108107 return nil, err
109108 }
110109 defer rows.Close()
111110112112- var items []CollectionItem
113113- for rows.Next() {
114114- var item CollectionItem
115115- if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil {
116116- return nil, err
117117- }
118118- items = append(items, item)
119119- }
120120- return items, nil
111111+ return scanCollectionItems(rows)
121112}
122113123114func (db *DB) GetPopularCollectionItems(limit, offset int) ([]CollectionItem, error) {
124115 since := time.Now().AddDate(0, 0, -14)
125125- rows, err := db.Query(db.Rebind(`
126126- SELECT
127127- c.uri, c.author_did, c.collection_uri, c.annotation_uri,
116116+ rows, err := db.Query(`
117117+ SELECT
118118+ c.uri, c.author_did, c.collection_uri, c.annotation_uri,
128119 c.position, c.created_at, c.indexed_at
129120 FROM collection_items c
130130- LEFT JOIN (
131131- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
132132- ) l ON l.subject_uri = c.annotation_uri
133133- LEFT JOIN (
134134- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
135135- ) r ON r.root_uri = c.annotation_uri
136136- WHERE c.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) > 0
137137- ORDER BY (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) DESC, c.created_at DESC
138138- LIMIT ? OFFSET ?
139139- `), since, limit, offset)
121121+ LEFT JOIN LATERAL (
122122+ SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = c.annotation_uri
123123+ ) l ON true
124124+ LEFT JOIN LATERAL (
125125+ SELECT COUNT(*) as cnt FROM replies WHERE root_uri = c.annotation_uri
126126+ ) r ON true
127127+ WHERE c.created_at > $1 AND (l.cnt + r.cnt) > 0
128128+ ORDER BY (l.cnt + r.cnt) DESC, c.created_at DESC
129129+ LIMIT $2 OFFSET $3
130130+ `, since, limit, offset)
140131 if err != nil {
141132 return nil, err
142133 }
143134 defer rows.Close()
144135145145- var items []CollectionItem
146146- for rows.Next() {
147147- var item CollectionItem
148148- if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil {
149149- return nil, err
150150- }
151151- items = append(items, item)
152152- }
153153- return items, nil
136136+ return scanCollectionItems(rows)
154137}
155138156139func (db *DB) GetShelvedCollectionItems(limit, offset int) ([]CollectionItem, error) {
157140 olderThan := time.Now().AddDate(0, 0, -1)
158141 since := time.Now().AddDate(0, 0, -14)
159159- rows, err := db.Query(db.Rebind(`
160160- SELECT
161161- c.uri, c.author_did, c.collection_uri, c.annotation_uri,
142142+ rows, err := db.Query(`
143143+ SELECT
144144+ c.uri, c.author_did, c.collection_uri, c.annotation_uri,
162145 c.position, c.created_at, c.indexed_at
163146 FROM collection_items c
164164- LEFT JOIN (
165165- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
166166- ) l ON l.subject_uri = c.annotation_uri
167167- LEFT JOIN (
168168- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
169169- ) r ON r.root_uri = c.annotation_uri
170170- WHERE c.created_at < ? AND c.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) = 0
147147+ WHERE c.created_at < $1 AND c.created_at > $2
148148+ AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = c.annotation_uri)
149149+ AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = c.annotation_uri)
171150 ORDER BY RANDOM()
172172- LIMIT ? OFFSET ?
173173- `), olderThan, since, limit, offset)
151151+ LIMIT $3 OFFSET $4
152152+ `, olderThan, since, limit, offset)
174153 if err != nil {
175154 return nil, err
176155 }
177156 defer rows.Close()
178157179179- var items []CollectionItem
180180- for rows.Next() {
181181- var item CollectionItem
182182- if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil {
183183- return nil, err
184184- }
185185- items = append(items, item)
186186- }
187187- return items, nil
158158+ return scanCollectionItems(rows)
188159}
189160190161func (db *DB) GetCollectionItemsByAuthor(authorDID string) ([]CollectionItem, error) {
191191- rows, err := db.Query(db.Rebind(`
162162+ rows, err := db.Query(`
192163 SELECT uri, author_did, collection_uri, annotation_uri, position, created_at, indexed_at
193164 FROM collection_items
194194- WHERE author_did = ?
165165+ WHERE author_did = $1
195166 ORDER BY created_at DESC
196196- `), authorDID)
167167+ `, authorDID)
197168 if err != nil {
198169 return nil, err
199170 }
200171 defer rows.Close()
201172202202- var items []CollectionItem
203203- for rows.Next() {
204204- var item CollectionItem
205205- if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil {
206206- return nil, err
207207- }
208208- items = append(items, item)
209209- }
210210- return items, nil
173173+ return scanCollectionItems(rows)
211174}
212175213176func (db *DB) GetCollectionURIsForAnnotation(annotationURI string) ([]string, error) {
214214- rows, err := db.Query(db.Rebind(`
215215- SELECT collection_uri FROM collection_items WHERE annotation_uri = ?
216216- `), annotationURI)
177177+ rows, err := db.Query(`
178178+ SELECT collection_uri FROM collection_items WHERE annotation_uri = $1
179179+ `, annotationURI)
217180 if err != nil {
218181 return nil, err
219182 }
···235198 return map[string]int{}, nil
236199 }
237200238238- query := db.Rebind(`
201201+ rows, err := db.Query(`
239202 SELECT collection_uri, COUNT(*)
240203 FROM collection_items
241241- WHERE collection_uri IN (` + buildPlaceholders(len(uris)) + `)
204204+ WHERE collection_uri = ANY($1)
242205 GROUP BY collection_uri
243243- `)
244244-245245- args := make([]interface{}, len(uris))
246246- for i, uri := range uris {
247247- args[i] = uri
248248- }
249249-250250- rows, err := db.Query(query, args...)
206206+ `, pqStringArray(uris))
251207 if err != nil {
252208 return nil, err
253209 }
···270226 return []Collection{}, nil
271227 }
272228273273- query := db.Rebind(`
229229+ rows, err := db.Query(`
274230 SELECT uri, author_did, name, description, icon, created_at, indexed_at
275231 FROM collections
276276- WHERE uri IN (` + buildPlaceholders(len(uris)) + `)
277277- `)
278278-279279- args := make([]interface{}, len(uris))
280280- for i, uri := range uris {
281281- args[i] = uri
282282- }
283283-284284- rows, err := db.Query(query, args...)
232232+ WHERE uri = ANY($1)
233233+ `, pqStringArray(uris))
285234 if err != nil {
286235 return nil, err
287236 }
···297246 }
298247 return collections, nil
299248}
249249+250250+func scanCollectionItems(rows interface {
251251+ Next() bool
252252+ Scan(...interface{}) error
253253+}) ([]CollectionItem, error) {
254254+ var items []CollectionItem
255255+ for rows.Next() {
256256+ var item CollectionItem
257257+ if err := rows.Scan(&item.URI, &item.AuthorDID, &item.CollectionURI, &item.AnnotationURI, &item.Position, &item.CreatedAt, &item.IndexedAt); err != nil {
258258+ return nil, err
259259+ }
260260+ items = append(items, item)
261261+ }
262262+ return items, nil
263263+}
+132-270
backend/internal/db/queries_highlights.go
···55)
6677func (db *DB) CreateHighlight(h *Highlight) error {
88- _, err := db.Exec(db.Rebind(`
88+ _, err := db.Exec(`
99 INSERT INTO highlights (uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid)
1010- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1010+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
1111 ON CONFLICT(uri) DO UPDATE SET
1212- target_source = excluded.target_source,
1313- target_hash = excluded.target_hash,
1414- target_title = excluded.target_title,
1515- selector_json = excluded.selector_json,
1616- color = excluded.color,
1717- tags_json = excluded.tags_json,
1818- indexed_at = excluded.indexed_at,
1919- cid = excluded.cid
2020- `), h.URI, h.AuthorDID, h.TargetSource, h.TargetHash, h.TargetTitle, h.SelectorJSON, h.Color, h.TagsJSON, h.CreatedAt, h.IndexedAt, h.CID)
1212+ target_source = EXCLUDED.target_source,
1313+ target_hash = EXCLUDED.target_hash,
1414+ target_title = EXCLUDED.target_title,
1515+ selector_json = EXCLUDED.selector_json,
1616+ color = EXCLUDED.color,
1717+ tags_json = EXCLUDED.tags_json,
1818+ indexed_at = EXCLUDED.indexed_at,
1919+ cid = EXCLUDED.cid
2020+ `, h.URI, h.AuthorDID, h.TargetSource, h.TargetHash, h.TargetTitle, h.SelectorJSON, h.Color, h.TagsJSON, h.CreatedAt, h.IndexedAt, h.CID)
2121 return err
2222}
23232424func (db *DB) GetHighlightByURI(uri string) (*Highlight, error) {
2525 var h Highlight
2626- err := db.QueryRow(db.Rebind(`
2626+ err := db.QueryRow(`
2727 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
2828 FROM highlights
2929- WHERE uri = ?
3030- `), uri).Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID)
2929+ WHERE uri = $1
3030+ `, uri).Scan(&h.URI, &h.AuthorDID, &h.TargetSource, &h.TargetHash, &h.TargetTitle, &h.SelectorJSON, &h.Color, &h.TagsJSON, &h.CreatedAt, &h.IndexedAt, &h.CID)
3131 if err != nil {
3232 return nil, err
3333 }
···3535}
36363737func (db *DB) GetRecentHighlights(limit, offset int) ([]Highlight, error) {
3838- rows, err := db.Query(db.Rebind(`
3838+ rows, err := db.Query(`
3939 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
4040 FROM highlights
4141 ORDER BY created_at DESC
4242- LIMIT ? OFFSET ?
4343- `), limit, offset)
4242+ LIMIT $1 OFFSET $2
4343+ `, limit, offset)
4444 if err != nil {
4545 return nil, err
4646 }
4747 defer rows.Close()
48484949- var highlights []Highlight
5050- for rows.Next() {
5151- var h Highlight
5252- 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 {
5353- return nil, err
5454- }
5555- highlights = append(highlights, h)
5656- }
5757- return highlights, nil
4949+ return scanHighlights(rows)
5850}
59516052func (db *DB) GetPopularHighlights(limit, offset int) ([]Highlight, error) {
6153 since := time.Now().AddDate(0, 0, -14)
6262- rows, err := db.Query(db.Rebind(`
6363- SELECT
6464- h.uri, h.author_did, h.target_source, h.target_hash, h.target_title,
5454+ rows, err := db.Query(`
5555+ SELECT
5656+ h.uri, h.author_did, h.target_source, h.target_hash, h.target_title,
6557 h.selector_json, h.color, h.tags_json, h.created_at, h.indexed_at, h.cid
6658 FROM highlights h
6767- LEFT JOIN (
6868- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
6969- ) l ON l.subject_uri = h.uri
7070- LEFT JOIN (
7171- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
7272- ) r ON r.root_uri = h.uri
7373- WHERE h.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) > 0
7474- ORDER BY (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) DESC, h.created_at DESC
7575- LIMIT ? OFFSET ?
7676- `), since, limit, offset)
5959+ LEFT JOIN LATERAL (
6060+ SELECT COUNT(*) as cnt FROM likes WHERE subject_uri = h.uri
6161+ ) l ON true
6262+ LEFT JOIN LATERAL (
6363+ SELECT COUNT(*) as cnt FROM replies WHERE root_uri = h.uri
6464+ ) r ON true
6565+ WHERE h.created_at > $1 AND (l.cnt + r.cnt) > 0
6666+ ORDER BY (l.cnt + r.cnt) DESC, h.created_at DESC
6767+ LIMIT $2 OFFSET $3
6868+ `, since, limit, offset)
7769 if err != nil {
7870 return nil, err
7971 }
8072 defer rows.Close()
81738282- var highlights []Highlight
8383- for rows.Next() {
8484- var h Highlight
8585- 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 {
8686- return nil, err
8787- }
8888- highlights = append(highlights, h)
8989- }
9090- return highlights, nil
7474+ return scanHighlights(rows)
9175}
92769377func (db *DB) GetShelvedHighlights(limit, offset int) ([]Highlight, error) {
9478 olderThan := time.Now().AddDate(0, 0, -1)
9579 since := time.Now().AddDate(0, 0, -14)
9696- rows, err := db.Query(db.Rebind(`
9797- SELECT
9898- h.uri, h.author_did, h.target_source, h.target_hash, h.target_title,
8080+ rows, err := db.Query(`
8181+ SELECT
8282+ h.uri, h.author_did, h.target_source, h.target_hash, h.target_title,
9983 h.selector_json, h.color, h.tags_json, h.created_at, h.indexed_at, h.cid
10084 FROM highlights h
101101- LEFT JOIN (
102102- SELECT subject_uri, COUNT(*) as cnt FROM likes GROUP BY subject_uri
103103- ) l ON l.subject_uri = h.uri
104104- LEFT JOIN (
105105- SELECT root_uri, COUNT(*) as cnt FROM replies GROUP BY root_uri
106106- ) r ON r.root_uri = h.uri
107107- WHERE h.created_at < ? AND h.created_at > ? AND (COALESCE(l.cnt, 0) + COALESCE(r.cnt, 0)) = 0
8585+ WHERE h.created_at < $1 AND h.created_at > $2
8686+ AND NOT EXISTS (SELECT 1 FROM likes WHERE subject_uri = h.uri)
8787+ AND NOT EXISTS (SELECT 1 FROM replies WHERE root_uri = h.uri)
10888 ORDER BY RANDOM()
109109- LIMIT ? OFFSET ?
110110- `), olderThan, since, limit, offset)
8989+ LIMIT $3 OFFSET $4
9090+ `, olderThan, since, limit, offset)
11191 if err != nil {
11292 return nil, err
11393 }
11494 defer rows.Close()
11595116116- var highlights []Highlight
117117- for rows.Next() {
118118- var h Highlight
119119- 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 {
120120- return nil, err
121121- }
122122- highlights = append(highlights, h)
123123- }
124124- return highlights, nil
9696+ return scanHighlights(rows)
12597}
1269812799func (db *DB) GetMarginHighlights(limit, offset int) ([]Highlight, error) {
128128- rows, err := db.Query(db.Rebind(`
100100+ rows, err := db.Query(`
129101 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
130102 FROM highlights
131103 WHERE uri NOT LIKE '%network.cosmik%'
132104 ORDER BY created_at DESC
133133- LIMIT ? OFFSET ?
134134- `), limit, offset)
105105+ LIMIT $1 OFFSET $2
106106+ `, limit, offset)
135107 if err != nil {
136108 return nil, err
137109 }
138110 defer rows.Close()
139111140140- var highlights []Highlight
141141- for rows.Next() {
142142- var h Highlight
143143- 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 {
144144- return nil, err
145145- }
146146- highlights = append(highlights, h)
147147- }
148148- return highlights, nil
112112+ return scanHighlights(rows)
149113}
150114151115func (db *DB) GetSembleHighlights(limit, offset int) ([]Highlight, error) {
152152- rows, err := db.Query(db.Rebind(`
116116+ rows, err := db.Query(`
153117 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
154118 FROM highlights
155119 WHERE uri LIKE '%network.cosmik%'
156120 ORDER BY created_at DESC
157157- LIMIT ? OFFSET ?
158158- `), limit, offset)
121121+ LIMIT $1 OFFSET $2
122122+ `, limit, offset)
159123 if err != nil {
160124 return nil, err
161125 }
162126 defer rows.Close()
163127164164- var highlights []Highlight
165165- for rows.Next() {
166166- var h Highlight
167167- 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 {
168168- return nil, err
169169- }
170170- highlights = append(highlights, h)
171171- }
172172- return highlights, nil
128128+ return scanHighlights(rows)
173129}
174130175131func (db *DB) GetHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
176176- pattern := "%\"" + tag + "\"%"
177177- rows, err := db.Query(db.Rebind(`
132132+ rows, err := db.Query(`
178133 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
179134 FROM highlights
180180- WHERE tags_json LIKE ?
135135+ WHERE tags_json::jsonb ? $1
181136 ORDER BY created_at DESC
182182- LIMIT ? OFFSET ?
183183- `), pattern, limit, offset)
137137+ LIMIT $2 OFFSET $3
138138+ `, tag, limit, offset)
184139 if err != nil {
185140 return nil, err
186141 }
187142 defer rows.Close()
188143189189- var highlights []Highlight
190190- for rows.Next() {
191191- var h Highlight
192192- 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 {
193193- return nil, err
194194- }
195195- highlights = append(highlights, h)
196196- }
197197- return highlights, nil
144144+ return scanHighlights(rows)
198145}
199146200147func (db *DB) GetMarginHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
201201- pattern := "%\"" + tag + "\"%"
202202- rows, err := db.Query(db.Rebind(`
148148+ rows, err := db.Query(`
203149 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
204150 FROM highlights
205205- WHERE tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
151151+ WHERE tags_json::jsonb ? $1 AND uri NOT LIKE '%network.cosmik%'
206152 ORDER BY created_at DESC
207207- LIMIT ? OFFSET ?
208208- `), pattern, limit, offset)
153153+ LIMIT $2 OFFSET $3
154154+ `, tag, limit, offset)
209155 if err != nil {
210156 return nil, err
211157 }
212158 defer rows.Close()
213159214214- var highlights []Highlight
215215- for rows.Next() {
216216- var h Highlight
217217- 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 {
218218- return nil, err
219219- }
220220- highlights = append(highlights, h)
221221- }
222222- return highlights, nil
160160+ return scanHighlights(rows)
223161}
224162225163func (db *DB) GetSembleHighlightsByTag(tag string, limit, offset int) ([]Highlight, error) {
226226- pattern := "%\"" + tag + "\"%"
227227- rows, err := db.Query(db.Rebind(`
164164+ rows, err := db.Query(`
228165 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
229166 FROM highlights
230230- WHERE tags_json LIKE ? AND uri LIKE '%network.cosmik%'
167167+ WHERE tags_json::jsonb ? $1 AND uri LIKE '%network.cosmik%'
231168 ORDER BY created_at DESC
232232- LIMIT ? OFFSET ?
233233- `), pattern, limit, offset)
169169+ LIMIT $2 OFFSET $3
170170+ `, tag, limit, offset)
234171 if err != nil {
235172 return nil, err
236173 }
237174 defer rows.Close()
238175239239- var highlights []Highlight
240240- for rows.Next() {
241241- var h Highlight
242242- 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 {
243243- return nil, err
244244- }
245245- highlights = append(highlights, h)
246246- }
247247- return highlights, nil
176176+ return scanHighlights(rows)
248177}
249178250179func (db *DB) GetHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
251251- pattern := "%\"" + tag + "\"%"
252252- rows, err := db.Query(db.Rebind(`
180180+ rows, err := db.Query(`
253181 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
254182 FROM highlights
255255- WHERE author_did = ? AND tags_json LIKE ?
183183+ WHERE author_did = $1 AND tags_json::jsonb ? $2
256184 ORDER BY created_at DESC
257257- LIMIT ? OFFSET ?
258258- `), authorDID, pattern, limit, offset)
185185+ LIMIT $3 OFFSET $4
186186+ `, authorDID, tag, limit, offset)
259187 if err != nil {
260188 return nil, err
261189 }
262190 defer rows.Close()
263191264264- var highlights []Highlight
265265- for rows.Next() {
266266- var h Highlight
267267- 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 {
268268- return nil, err
269269- }
270270- highlights = append(highlights, h)
271271- }
272272- return highlights, nil
192192+ return scanHighlights(rows)
273193}
274194275195func (db *DB) GetMarginHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
276276- pattern := "%\"" + tag + "\"%"
277277- rows, err := db.Query(db.Rebind(`
196196+ rows, err := db.Query(`
278197 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
279198 FROM highlights
280280- WHERE author_did = ? AND tags_json LIKE ? AND uri NOT LIKE '%network.cosmik%'
199199+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri NOT LIKE '%network.cosmik%'
281200 ORDER BY created_at DESC
282282- LIMIT ? OFFSET ?
283283- `), authorDID, pattern, limit, offset)
201201+ LIMIT $3 OFFSET $4
202202+ `, authorDID, tag, limit, offset)
284203 if err != nil {
285204 return nil, err
286205 }
287206 defer rows.Close()
288207289289- var highlights []Highlight
290290- for rows.Next() {
291291- var h Highlight
292292- 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 {
293293- return nil, err
294294- }
295295- highlights = append(highlights, h)
296296- }
297297- return highlights, nil
208208+ return scanHighlights(rows)
298209}
299210300211func (db *DB) GetSembleHighlightsByTagAndAuthor(tag, authorDID string, limit, offset int) ([]Highlight, error) {
301301- pattern := "%\"" + tag + "\"%"
302302- rows, err := db.Query(db.Rebind(`
212212+ rows, err := db.Query(`
303213 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
304214 FROM highlights
305305- WHERE author_did = ? AND tags_json LIKE ? AND uri LIKE '%network.cosmik%'
215215+ WHERE author_did = $1 AND tags_json::jsonb ? $2 AND uri LIKE '%network.cosmik%'
306216 ORDER BY created_at DESC
307307- LIMIT ? OFFSET ?
308308- `), authorDID, pattern, limit, offset)
217217+ LIMIT $3 OFFSET $4
218218+ `, authorDID, tag, limit, offset)
309219 if err != nil {
310220 return nil, err
311221 }
312222 defer rows.Close()
313223314314- var highlights []Highlight
315315- for rows.Next() {
316316- var h Highlight
317317- 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 {
318318- return nil, err
319319- }
320320- highlights = append(highlights, h)
321321- }
322322- return highlights, nil
224224+ return scanHighlights(rows)
323225}
324226325227func (db *DB) GetHighlightsByTargetHash(targetHash string, limit, offset int) ([]Highlight, error) {
326326- rows, err := db.Query(db.Rebind(`
228228+ rows, err := db.Query(`
327229 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
328230 FROM highlights
329329- WHERE target_hash = ?
231231+ WHERE target_hash = $1
330232 ORDER BY created_at DESC
331331- LIMIT ? OFFSET ?
332332- `), targetHash, limit, offset)
233233+ LIMIT $2 OFFSET $3
234234+ `, targetHash, limit, offset)
333235 if err != nil {
334236 return nil, err
335237 }
336238 defer rows.Close()
337239338338- var highlights []Highlight
339339- for rows.Next() {
340340- var h Highlight
341341- 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 {
342342- return nil, err
343343- }
344344- highlights = append(highlights, h)
345345- }
346346- return highlights, nil
240240+ return scanHighlights(rows)
347241}
348242349243func (db *DB) GetHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
350350- rows, err := db.Query(db.Rebind(`
244244+ rows, err := db.Query(`
351245 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
352246 FROM highlights
353353- WHERE author_did = ?
247247+ WHERE author_did = $1
354248 ORDER BY created_at DESC
355355- LIMIT ? OFFSET ?
356356- `), authorDID, limit, offset)
249249+ LIMIT $2 OFFSET $3
250250+ `, authorDID, limit, offset)
357251 if err != nil {
358252 return nil, err
359253 }
360254 defer rows.Close()
361255362362- var highlights []Highlight
363363- for rows.Next() {
364364- var h Highlight
365365- 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 {
366366- return nil, err
367367- }
368368- highlights = append(highlights, h)
369369- }
370370- return highlights, nil
256256+ return scanHighlights(rows)
371257}
372258373259func (db *DB) GetMarginHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
374374- rows, err := db.Query(db.Rebind(`
260260+ rows, err := db.Query(`
375261 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
376262 FROM highlights
377377- WHERE author_did = ? AND uri NOT LIKE '%network.cosmik%'
263263+ WHERE author_did = $1 AND uri NOT LIKE '%network.cosmik%'
378264 ORDER BY created_at DESC
379379- LIMIT ? OFFSET ?
380380- `), authorDID, limit, offset)
265265+ LIMIT $2 OFFSET $3
266266+ `, authorDID, limit, offset)
381267 if err != nil {
382268 return nil, err
383269 }
384270 defer rows.Close()
385271386386- var highlights []Highlight
387387- for rows.Next() {
388388- var h Highlight
389389- 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 {
390390- return nil, err
391391- }
392392- highlights = append(highlights, h)
393393- }
394394- return highlights, nil
272272+ return scanHighlights(rows)
395273}
396274397275func (db *DB) GetSembleHighlightsByAuthor(authorDID string, limit, offset int) ([]Highlight, error) {
398398- rows, err := db.Query(db.Rebind(`
276276+ rows, err := db.Query(`
399277 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
400278 FROM highlights
401401- WHERE author_did = ? AND uri LIKE '%network.cosmik%'
279279+ WHERE author_did = $1 AND uri LIKE '%network.cosmik%'
402280 ORDER BY created_at DESC
403403- LIMIT ? OFFSET ?
404404- `), authorDID, limit, offset)
281281+ LIMIT $2 OFFSET $3
282282+ `, authorDID, limit, offset)
405283 if err != nil {
406284 return nil, err
407285 }
408286 defer rows.Close()
409287410410- var highlights []Highlight
411411- for rows.Next() {
412412- var h Highlight
413413- 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 {
414414- return nil, err
415415- }
416416- highlights = append(highlights, h)
417417- }
418418- return highlights, nil
288288+ return scanHighlights(rows)
419289}
420290421291func (db *DB) GetHighlightsByAuthorAndTargetHash(authorDID, targetHash string, limit, offset int) ([]Highlight, error) {
422422- rows, err := db.Query(db.Rebind(`
292292+ rows, err := db.Query(`
423293 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
424294 FROM highlights
425425- WHERE author_did = ? AND target_hash = ?
295295+ WHERE author_did = $1 AND target_hash = $2
426296 ORDER BY created_at DESC
427427- LIMIT ? OFFSET ?
428428- `), authorDID, targetHash, limit, offset)
297297+ LIMIT $3 OFFSET $4
298298+ `, authorDID, targetHash, limit, offset)
429299 if err != nil {
430300 return nil, err
431301 }
432302 defer rows.Close()
433303434434- var highlights []Highlight
435435- for rows.Next() {
436436- var h Highlight
437437- 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 {
438438- return nil, err
439439- }
440440- highlights = append(highlights, h)
441441- }
442442- return highlights, nil
304304+ return scanHighlights(rows)
443305}
444306445307func (db *DB) DeleteHighlight(uri string) error {
446446- _, err := db.Exec(db.Rebind(`DELETE FROM highlights WHERE uri = ?`), uri)
308308+ _, err := db.Exec(`DELETE FROM highlights WHERE uri = $1`, uri)
447309 return err
448310}
449311450312func (db *DB) UpdateHighlight(uri, color, tagsJSON, cid string) error {
451451- _, err := db.Exec(db.Rebind(`
452452- UPDATE highlights
453453- SET color = ?, tags_json = ?, cid = ?, indexed_at = ?
454454- WHERE uri = ?
455455- `), color, tagsJSON, cid, time.Now(), uri)
313313+ _, err := db.Exec(`
314314+ UPDATE highlights
315315+ SET color = $1, tags_json = $2, cid = $3, indexed_at = $4
316316+ WHERE uri = $5
317317+ `, color, tagsJSON, cid, time.Now(), uri)
456318 return err
457319}
458320···461323 return []Highlight{}, nil
462324 }
463325464464- query := db.Rebind(`
326326+ rows, err := db.Query(`
465327 SELECT uri, author_did, target_source, target_hash, target_title, selector_json, color, tags_json, created_at, indexed_at, cid
466328 FROM highlights
467467- WHERE uri IN (` + buildPlaceholders(len(uris)) + `)
468468- `)
469469-470470- args := make([]interface{}, len(uris))
471471- for i, uri := range uris {
472472- args[i] = uri
473473- }
474474-475475- rows, err := db.Query(query, args...)
329329+ WHERE uri = ANY($1)
330330+ `, pqStringArray(uris))
476331 if err != nil {
477332 return nil, err
478333 }
479334 defer rows.Close()
480335481481- var highlights []Highlight
482482- for rows.Next() {
483483- var h Highlight
484484- 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 {
485485- return nil, err
486486- }
487487- highlights = append(highlights, h)
488488- }
489489- return highlights, nil
336336+ return scanHighlights(rows)
490337}
491338492339func (db *DB) GetHighlightURIs(authorDID string) ([]string, error) {
493493- rows, err := db.Query(db.Rebind(`
494494- SELECT uri FROM highlights WHERE author_did = ?
495495- `), authorDID)
340340+ rows, err := db.Query(`
341341+ SELECT uri FROM highlights WHERE author_did = $1
342342+ `, authorDID)
496343 if err != nil {
497344 return nil, err
498345 }
···508355 }
509356 return uris, nil
510357}
358358+359359+func scanHighlights(rows interface {
360360+ Next() bool
361361+ Scan(...interface{}) error
362362+}) ([]Highlight, error) {
363363+ var highlights []Highlight
364364+ for rows.Next() {
365365+ var h Highlight
366366+ 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 {
367367+ return nil, err
368368+ }
369369+ highlights = append(highlights, h)
370370+ }
371371+ return highlights, nil
372372+}
+15-83
backend/internal/db/queries_history.go
···77)
8899func (db *DB) SaveEditHistory(uri, recordType, previousContent string, previousCID *string) error {
1010- _, err := db.Exec(db.Rebind(`
1010+ _, err := db.Exec(`
1111 INSERT INTO edit_history (uri, record_type, previous_content, previous_cid, edited_at)
1212- VALUES (?, ?, ?, ?, ?)
1313- `), uri, recordType, previousContent, previousCID, time.Now())
1212+ VALUES ($1, $2, $3, $4, $5)
1313+ `, uri, recordType, previousContent, previousCID, time.Now())
1414 return err
1515}
16161717func (db *DB) GetEditHistory(uri string) ([]EditHistory, error) {
1818- rows, err := db.Query(db.Rebind(`
1818+ rows, err := db.Query(`
1919 SELECT id, uri, record_type, previous_content, previous_cid, edited_at
2020 FROM edit_history
2121- WHERE uri = ?
2121+ WHERE uri = $1
2222 ORDER BY edited_at DESC
2323- `), uri)
2323+ `, uri)
2424 if err != nil {
2525 return nil, err
2626 }
···2929 var history []EditHistory
3030 for rows.Next() {
3131 var h EditHistory
3232- var editedAt interface{}
3333- if err := rows.Scan(&h.ID, &h.URI, &h.RecordType, &h.PreviousContent, &h.PreviousCID, &editedAt); err != nil {
3232+ if err := rows.Scan(&h.ID, &h.URI, &h.RecordType, &h.PreviousContent, &h.PreviousCID, &h.EditedAt); err != nil {
3433 return nil, err
3534 }
3636-3737- switch v := editedAt.(type) {
3838- case time.Time:
3939- h.EditedAt = v
4040- case []byte:
4141- parsed, err := parseTime(string(v))
4242- if err != nil {
4343- return nil, err
4444- }
4545- h.EditedAt = parsed
4646- case string:
4747- parsed, err := parseTime(v)
4848- if err != nil {
4949- return nil, err
5050- }
5151- h.EditedAt = parsed
5252- }
5353-5435 history = append(history, h)
5536 }
5637 return history, nil
···6142 return nil, nil
6243 }
63446464- query := `
6565- SELECT uri, MAX(edited_at) as edited_at
6666- FROM edit_history
6767- WHERE uri IN (`
6868- args := make([]interface{}, len(uris))
6945 placeholders := make([]string, len(uris))
7070-4646+ args := make([]interface{}, len(uris))
7147 for i, uri := range uris {
7248 placeholders[i] = fmt.Sprintf("$%d", i+1)
7349 args[i] = uri
7450 }
75517676- query += strings.Join(placeholders, ",") + ") GROUP BY uri"
7777-7878- if db.driver == "sqlite3" {
7979- query = strings.ReplaceAll(query, "$", "?")
8080- placeholders = make([]string, len(uris))
8181- for i := range uris {
8282- placeholders[i] = "?"
8383- }
8484- query = `
5252+ query := `
8553 SELECT uri, MAX(edited_at) as edited_at
8654 FROM edit_history
8787- WHERE uri IN (` + strings.Join(placeholders, ",") + ") GROUP BY uri"
8888- }
5555+ WHERE uri IN (` + strings.Join(placeholders, ",") + `)
5656+ GROUP BY uri
5757+ `
89589090- rows, err := db.Query(db.Rebind(query), args...)
5959+ rows, err := db.Query(query, args...)
9160 if err != nil {
9261 return nil, err
9362 }
···9665 result := make(map[string]time.Time)
9766 for rows.Next() {
9867 var uri string
9999- var editedAt interface{}
6868+ var editedAt time.Time
10069 if err := rows.Scan(&uri, &editedAt); err != nil {
10170 continue
10271 }
103103-104104- var finalTime time.Time
105105- switch v := editedAt.(type) {
106106- case time.Time:
107107- finalTime = v
108108- case []byte:
109109- parsed, err := parseTime(string(v))
110110- if err != nil {
111111- continue
112112- }
113113- finalTime = parsed
114114- case string:
115115- parsed, err := parseTime(v)
116116- if err != nil {
117117- continue
118118- }
119119- finalTime = parsed
120120- default:
121121- continue
122122- }
123123-124124- result[uri] = finalTime
7272+ result[uri] = editedAt
12573 }
1267412775 return result, nil
12876}
129129-130130-func parseTime(s string) (time.Time, error) {
131131- formats := []string{
132132- time.RFC3339,
133133- time.RFC3339Nano,
134134- "2006-01-02 15:04:05.999999999-07:00",
135135- "2006-01-02 15:04:05",
136136- }
137137-138138- for _, f := range formats {
139139- if t, err := time.Parse(f, s); err == nil {
140140- return t, nil
141141- }
142142- }
143143- return time.Time{}, fmt.Errorf("could not parse time: %s", s)
144144-}
···11package db
2233-import "database/sql"
44-55-type TrendingTag struct {
66- Tag string `json:"tag"`
77- Count int `json:"count"`
88-}
99-103func (db *DB) GetTrendingTags(limit int) ([]TrendingTag, error) {
1111- var query string
1212- if db.driver == "postgres" {
1313- query = `
1414- SELECT tag, COUNT(*) as count FROM (
1515- SELECT value as tag, author_did
1616- FROM annotations, json_array_elements_text(tags_json::json) as value
1717- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
1818- AND created_at > NOW() - INTERVAL '14 days'
1919- UNION ALL
2020- SELECT value as tag, author_did
2121- FROM highlights, json_array_elements_text(tags_json::json) as value
2222- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
2323- AND created_at > NOW() - INTERVAL '14 days'
2424- UNION ALL
2525- SELECT value as tag, author_did
2626- FROM bookmarks, json_array_elements_text(tags_json::json) as value
2727- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
2828- AND created_at > NOW() - INTERVAL '14 days'
2929- ) combined
3030- GROUP BY tag
3131- HAVING COUNT(DISTINCT author_did) >= 3
3232- ORDER BY count DESC
3333- LIMIT $1
3434- `
3535- } else {
3636- query = `
3737- SELECT tag, COUNT(*) as count FROM (
3838- SELECT json_each.value as tag, author_did
3939- FROM annotations, json_each(annotations.tags_json)
4040- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
4141- AND created_at > datetime('now', '-14 days')
4242- UNION ALL
4343- SELECT json_each.value as tag, author_did
4444- FROM highlights, json_each(highlights.tags_json)
4545- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
4646- AND created_at > datetime('now', '-14 days')
4747- UNION ALL
4848- SELECT json_each.value as tag, author_did
4949- FROM bookmarks, json_each(bookmarks.tags_json)
5050- WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
5151- AND created_at > datetime('now', '-14 days')
5252- ) combined
5353- GROUP BY tag
5454- HAVING COUNT(DISTINCT author_did) >= 3
5555- ORDER BY count DESC
5656- LIMIT ?
5757- `
5858- }
44+ query := `
55+ SELECT tag, COUNT(*) as count FROM (
66+ SELECT value as tag, author_did
77+ FROM annotations, json_array_elements_text(tags_json::json) as value
88+ WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
99+ AND created_at > NOW() - INTERVAL '14 days'
1010+ UNION ALL
1111+ SELECT value as tag, author_did
1212+ FROM highlights, json_array_elements_text(tags_json::json) as value
1313+ WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
1414+ AND created_at > NOW() - INTERVAL '14 days'
1515+ UNION ALL
1616+ SELECT value as tag, author_did
1717+ FROM bookmarks, json_array_elements_text(tags_json::json) as value
1818+ WHERE tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
1919+ AND created_at > NOW() - INTERVAL '14 days'
2020+ ) combined
2121+ GROUP BY tag
2222+ HAVING COUNT(DISTINCT author_did) >= 3
2323+ ORDER BY count DESC
2424+ LIMIT $1
2525+ `
59266060- var rows *sql.Rows
6161- var err error
6262- if db.driver == "postgres" {
6363- rows, err = db.Query(query, limit)
6464- } else {
6565- rows, err = db.Query(db.Rebind(query), limit)
6666- }
2727+ rows, err := db.Query(query, limit)
6728 if err != nil {
6829 return nil, err
6930 }
···9051}
91529253func (db *DB) GetUserTags(did string, limit int) ([]TrendingTag, error) {
9393- var query string
9494- if db.driver == "postgres" {
9595- query = `
9696- SELECT tag, SUM(cnt) as count FROM (
9797- SELECT value as tag, COUNT(*) as cnt
9898- FROM annotations, json_array_elements_text(tags_json::json) as value
9999- WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
100100- GROUP BY tag
101101- UNION ALL
102102- SELECT value as tag, COUNT(*) as cnt
103103- FROM highlights, json_array_elements_text(tags_json::json) as value
104104- WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
105105- GROUP BY tag
106106- UNION ALL
107107- SELECT value as tag, COUNT(*) as cnt
108108- FROM bookmarks, json_array_elements_text(tags_json::json) as value
109109- WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
110110- GROUP BY tag
111111- ) combined
5454+ query := `
5555+ SELECT tag, SUM(cnt) as count FROM (
5656+ SELECT value as tag, COUNT(*) as cnt
5757+ FROM annotations, json_array_elements_text(tags_json::json) as value
5858+ WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
11259 GROUP BY tag
113113- ORDER BY count DESC
114114- LIMIT $2
115115- `
116116- } else {
117117- query = `
118118- SELECT tag, SUM(cnt) as count FROM (
119119- SELECT json_each.value as tag, COUNT(*) as cnt
120120- FROM annotations, json_each(annotations.tags_json)
121121- WHERE author_did = ? AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
122122- GROUP BY tag
123123- UNION ALL
124124- SELECT json_each.value as tag, COUNT(*) as cnt
125125- FROM highlights, json_each(highlights.tags_json)
126126- WHERE author_did = ? AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
127127- GROUP BY tag
128128- UNION ALL
129129- SELECT json_each.value as tag, COUNT(*) as cnt
130130- FROM bookmarks, json_each(bookmarks.tags_json)
131131- WHERE author_did = ? AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
132132- GROUP BY tag
133133- ) combined
6060+ UNION ALL
6161+ SELECT value as tag, COUNT(*) as cnt
6262+ FROM highlights, json_array_elements_text(tags_json::json) as value
6363+ WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
6464+ GROUP BY tag
6565+ UNION ALL
6666+ SELECT value as tag, COUNT(*) as cnt
6767+ FROM bookmarks, json_array_elements_text(tags_json::json) as value
6868+ WHERE author_did = $1 AND tags_json IS NOT NULL AND tags_json != '' AND tags_json != '[]'
13469 GROUP BY tag
135135- ORDER BY count DESC
136136- LIMIT ?
137137- `
138138- }
7070+ ) combined
7171+ GROUP BY tag
7272+ ORDER BY count DESC
7373+ LIMIT $2
7474+ `
13975140140- var rows *sql.Rows
141141- var err error
142142- if db.driver == "postgres" {
143143- rows, err = db.Query(query, did, limit)
144144- } else {
145145- rows, err = db.Query(db.Rebind(query), did, did, did, limit)
146146- }
7676+ rows, err := db.Query(query, did, limit)
14777 if err != nil {
14878 return nil, err
14979 }
···1689816999 return tags, nil
170100}
101101+102102+type TrendingTag struct {
103103+ Tag string `json:"tag"`
104104+ Count int `json:"count"`
105105+}
···11----
22-export const prerender = false;
33-44-import BaseLayout from '../layouts/BaseLayout.astro';
55-import App from '../App';
66----
77-88-<BaseLayout title="Margin" description="Annotate the web using the AT Protocol">
99- <App client:only="react" />
1010-</BaseLayout>
+7-4
web/src/pages/[handle]/annotation/[rkey].astro
···11---
22-export const prerender = false;
3244-import OGLayout from '../../../layouts/OGLayout.astro';
33+import AppLayout from '../../../layouts/AppLayout.astro';
44+import AnnotationDetail from '../../../views/content/AnnotationDetail';
55import { resolveHandle, fetchOGForRoute } from '../../../lib/og';
6677const { handle, rkey } = Astro.params;
88+const user = Astro.locals.user;
8999-let title = 'Margin';
1010+let title = 'Annotation - Margin';
1011let description = 'Annotate the web';
1112let image = 'https://margin.at/og.png';
1213···2728}
2829---
29303030-<OGLayout title={title} description={description} image={image} />
3131+<AppLayout title={title} description={description} image={image} user={user}>
3232+ <AnnotationDetail client:load handle={handle} rkey={rkey} type="annotation" />
3333+</AppLayout>
+7-4
web/src/pages/[handle]/bookmark/[rkey].astro
···11---
22-export const prerender = false;
3244-import OGLayout from '../../../layouts/OGLayout.astro';
33+import AppLayout from '../../../layouts/AppLayout.astro';
44+import AnnotationDetail from '../../../views/content/AnnotationDetail';
55import { resolveHandle, fetchOGForRoute } from '../../../lib/og';
6677const { handle, rkey } = Astro.params;
88+const user = Astro.locals.user;
8999-let title = 'Margin';
1010+let title = 'Bookmark - Margin';
1011let description = 'Annotate the web';
1112let image = 'https://margin.at/og.png';
1213···2728}
2829---
29303030-<OGLayout title={title} description={description} image={image} />
3131+<AppLayout title={title} description={description} image={image} user={user}>
3232+ <AnnotationDetail client:load handle={handle} rkey={rkey} type="bookmark" />
3333+</AppLayout>
+7-4
web/src/pages/[handle]/collection/[rkey].astro
···11---
22-export const prerender = false;
3244-import OGLayout from '../../../layouts/OGLayout.astro';
33+import AppLayout from '../../../layouts/AppLayout.astro';
44+import CollectionDetail from '../../../views/collections/CollectionDetail';
55import { resolveHandle, fetchCollectionOG } from '../../../lib/og';
6677const { handle, rkey } = Astro.params;
88+const user = Astro.locals.user;
8999-let title = 'Margin';
1010+let title = 'Collection - Margin';
1011let description = 'Annotate the web';
1112let image = 'https://margin.at/og.png';
1213···2829}
2930---
30313131-<OGLayout title={title} description={description} image={image} />
3232+<AppLayout title={title} description={description} image={image} user={user}>
3333+ <CollectionDetail client:load handle={handle} rkey={rkey} />
3434+</AppLayout>
+7-4
web/src/pages/[handle]/highlight/[rkey].astro
···11---
22-export const prerender = false;
3244-import OGLayout from '../../../layouts/OGLayout.astro';
33+import AppLayout from '../../../layouts/AppLayout.astro';
44+import AnnotationDetail from '../../../views/content/AnnotationDetail';
55import { resolveHandle, fetchOGForRoute } from '../../../lib/og';
6677const { handle, rkey } = Astro.params;
88+const user = Astro.locals.user;
8999-let title = 'Margin';
1010+let title = 'Highlight - Margin';
1011let description = 'Annotate the web';
1112let image = 'https://margin.at/og.png';
1213···2728}
2829---
29303030-<OGLayout title={title} description={description} image={image} />
3131+<AppLayout title={title} description={description} image={image} user={user}>
3232+ <AnnotationDetail client:load handle={handle} rkey={rkey} type="highlight" />
3333+</AppLayout>
+9
web/src/pages/about.astro
···11+---
22+33+import BaseLayout from '../layouts/BaseLayout.astro';
44+import About from '../views/About';
55+---
66+77+<BaseLayout title="About - Margin" description="Annotate the web using the AT Protocol">
88+ <About client:load />
99+</BaseLayout>
···11+---
22+33+import AppLayout from '../../layouts/AppLayout.astro';
44+import Collections from '../../views/collections/Collections';
55+66+const user = Astro.locals.user;
77+---
88+99+<AppLayout title="Collections - Margin" user={user}>
1010+ <Collections client:load />
1111+</AppLayout>
+11
web/src/pages/discover.astro
···11+---
22+33+import AppLayout from '../layouts/AppLayout.astro';
44+import Discover from '../views/core/Discover';
55+66+const user = Astro.locals.user;
77+---
88+99+<AppLayout title="Discover - Margin" user={user}>
1010+ <Discover client:load />
1111+</AppLayout>
+12
web/src/pages/highlights.astro
···11+---
22+33+import AppLayout from '../layouts/AppLayout.astro';
44+import Feed from '../views/core/Feed';
55+66+const user = Astro.locals.user;
77+const tag = Astro.url.searchParams.get('tag') || undefined;
88+---
99+1010+<AppLayout title="Highlights - Margin" user={user}>
1111+ <Feed client:load initialType="all" motivation="highlighting" showTabs={false} initialTag={tag} initialUser={user} />
1212+</AppLayout>
+12
web/src/pages/home.astro
···11+---
22+33+import AppLayout from '../layouts/AppLayout.astro';
44+import Feed from '../views/core/Feed';
55+66+const user = Astro.locals.user;
77+const tag = Astro.url.searchParams.get('tag') || undefined;
88+---
99+1010+<AppLayout title="Home - Margin" user={user}>
1111+ <Feed client:load initialType="all" initialTag={tag} initialUser={user} />
1212+</AppLayout>
+8-2
web/src/pages/index.astro
···11---
22+23import BaseLayout from '../layouts/BaseLayout.astro';
33-import App from '../App';
44+import About from '../views/About';
55+66+const user = Astro.locals.user;
77+if (user) {
88+ return Astro.redirect('/home');
99+}
410---
511612<BaseLayout title="Margin" description="Annotate the web using the AT Protocol">
77- <App client:only="react" />
1313+ <About client:load />
814</BaseLayout>
+16
web/src/pages/login.astro
···11+---
22+33+import BaseLayout from '../layouts/BaseLayout.astro';
44+import Login from '../views/auth/Login';
55+66+const user = Astro.locals.user;
77+if (user) {
88+ return Astro.redirect('/home');
99+}
1010+1111+const error = Astro.url.searchParams.get('error') || undefined;
1212+---
1313+1414+<BaseLayout title="Sign In - Margin" description="Sign in to Margin">
1515+ <Login client:load initialError={error} />
1616+</BaseLayout>