A community based topic aggregation platform built on atproto

Merge pull request #2 from BrettM86/feature/repository

Refactor: ATProto Repository Storage with Indigo Carstore Integration

authored by Bretton May and committed by GitHub 44f6ce79 f5ef3bbc

+12
.env.test.example
··· 1 + # Test Environment Configuration 2 + # This file contains environment variables for running tests 3 + # Copy this file to .env.test and update with your actual values 4 + 5 + # Test Database Configuration 6 + TEST_DATABASE_URL=postgres://your_test_user:your_test_password@localhost:5434/coves_test?sslmode=disable 7 + 8 + # Test Server Configuration (if needed) 9 + TEST_PORT=8081 10 + 11 + # Test CAR Storage Directory 12 + TEST_CAR_STORAGE_DIR=/tmp/coves_test_carstore
+45
.gitignore
··· 1 + # Binaries 2 + *.exe 3 + *.dll 4 + *.so 5 + *.dylib 6 + /main 7 + /server 8 + 9 + # Test binary, built with go test -c 10 + *.test 11 + 12 + # Output of the go coverage tool 13 + *.out 14 + 15 + # Go workspace file 16 + go.work 17 + 18 + # Environment files 19 + .env 20 + .env.local 21 + .env.development 22 + .env.production 23 + .env.test 24 + 25 + # IDE 26 + .idea/ 27 + .vscode/ 28 + *.swp 29 + *.swo 30 + 31 + # OS 32 + .DS_Store 33 + Thumbs.db 34 + 35 + # Application data 36 + /data/ 37 + /local_dev_data/ 38 + /test_db_data/ 39 + 40 + # Logs 41 + *.log 42 + 43 + # Temporary files 44 + *.tmp 45 + *.temp
+3
CLAUDE.md
··· 19 19 - DB: PostgreSQL 20 20 - atProto for federation & user identities 21 21 22 + ## atProto Guidelines 23 + - Attempt to utilize bsky built indigo packages before building atProto layer functions from scratch 24 + 22 25 # Architecture Guidelines 23 26 24 27 ## Required Layered Architecture
+70
TESTING_SUMMARY.md
··· 1 + # Repository Testing Summary 2 + 3 + ## Test Infrastructure Setup 4 + - Created Docker Compose configuration for isolated test database on port 5434 5 + - Test database is completely separate from development (5433) and production (5432) 6 + - Configuration location: `/internal/db/test_db_compose/docker-compose.yml` 7 + 8 + ## Repository Service Implementation 9 + Successfully integrated Indigo's carstore for ATProto repository management: 10 + 11 + ### Key Components: 12 + 1. **CarStore Wrapper** (`/internal/atproto/carstore/carstore.go`) 13 + - Wraps Indigo's carstore implementation 14 + - Manages CAR file storage with PostgreSQL metadata 15 + 16 + 2. **RepoStore** (`/internal/atproto/carstore/repo_store.go`) 17 + - Combines CarStore with UserMapping for DID-based access 18 + - Handles DID to UID conversions transparently 19 + 20 + 3. **UserMapping** (`/internal/atproto/carstore/user_mapping.go`) 21 + - Maps ATProto DIDs to numeric UIDs (required by Indigo) 22 + - Auto-creates user_maps table via GORM 23 + 24 + 4. **Repository Service** (`/internal/core/repository/service.go`) 25 + - Updated to use Indigo's carstore instead of custom implementation 26 + - Handles empty repositories gracefully 27 + - Placeholder CID for empty repos until records are added 28 + 29 + ## Test Results 30 + All repository tests passing: 31 + - ✅ CreateRepository - Creates user mapping and repository record 32 + - ✅ ImportExport - Handles empty CAR data correctly 33 + - ✅ DeleteRepository - Removes repository and carstore data 34 + - ✅ CompactRepository - Runs garbage collection 35 + - ✅ UserMapping - DID to UID mapping works correctly 36 + 37 + ## Implementation Notes 38 + 1. **Empty Repositories**: Since Indigo's carstore expects actual CAR data, we handle empty repositories by: 39 + - Creating user mapping only 40 + - Using placeholder CID 41 + - Returning empty byte array on export 42 + - Actual CAR data will be created when records are added 43 + 44 + 2. **Database Tables**: Indigo's carstore auto-creates: 45 + - `user_maps` (DID ↔ UID mapping) 46 + - `car_shards` (CAR file metadata) 47 + - `block_refs` (IPLD block references) 48 + 49 + 3. **Migration**: Created migration to drop our custom block_refs table to avoid conflicts 50 + 51 + ## Next Steps 52 + To fully utilize the carstore, implement: 53 + 1. Record CRUD operations using carstore's DeltaSession 54 + 2. Proper CAR file generation when adding records 55 + 3. Commit tracking with proper signatures 56 + 4. Repository versioning and history 57 + 58 + ## Running Tests 59 + ```bash 60 + # Start test database 61 + cd internal/db/test_db_compose 62 + docker-compose up -d 63 + 64 + # Run repository tests 65 + TEST_DATABASE_URL="postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" \ 66 + go test -v ./internal/core/repository/... 67 + 68 + # Stop test database 69 + docker-compose down 70 + ```
+33 -4
cmd/server/main.go
··· 11 11 "github.com/go-chi/chi/v5/middleware" 12 12 _ "github.com/lib/pq" 13 13 "github.com/pressly/goose/v3" 14 + "gorm.io/driver/postgres" 15 + "gorm.io/gorm" 14 16 15 17 "Coves/internal/api/routes" 18 + "Coves/internal/atproto/carstore" 19 + "Coves/internal/core/repository" 16 20 "Coves/internal/core/users" 17 - "Coves/internal/db/postgres" 21 + postgresRepo "Coves/internal/db/postgres" 18 22 ) 19 23 20 24 func main() { ··· 47 51 r.Use(middleware.Recoverer) 48 52 r.Use(middleware.RequestID) 49 53 50 - userRepo := postgres.NewUserRepository(db) 51 - userService := users.NewUserService(userRepo) 54 + // Initialize GORM 55 + gormDB, err := gorm.Open(postgres.New(postgres.Config{ 56 + Conn: db, 57 + }), &gorm.Config{ 58 + DisableForeignKeyConstraintWhenMigrating: true, 59 + PrepareStmt: false, 60 + }) 61 + if err != nil { 62 + log.Fatal("Failed to initialize GORM:", err) 63 + } 64 + 65 + // Initialize repositories 66 + userRepo := postgresRepo.NewUserRepository(db) 67 + _ = users.NewUserService(userRepo) // TODO: Use when UserRoutes is fixed 68 + 69 + // Initialize carstore for ATProto repository storage 70 + carDirs := []string{"./data/carstore"} 71 + repoStore, err := carstore.NewRepoStore(gormDB, carDirs) 72 + if err != nil { 73 + log.Fatal("Failed to initialize repo store:", err) 74 + } 75 + 76 + repositoryRepo := postgresRepo.NewRepositoryRepo(db) 77 + repositoryService := repository.NewService(repositoryRepo, repoStore) 52 78 53 - r.Mount("/api/users", routes.UserRoutes(userService)) 79 + // Mount routes 80 + // TODO: Fix UserRoutes to accept *UserService 81 + // r.Mount("/api/users", routes.UserRoutes(userService)) 82 + r.Mount("/", routes.RepositoryRoutes(repositoryService)) 54 83 55 84 r.Get("/health", func(w http.ResponseWriter, r *http.Request) { 56 85 w.WriteHeader(http.StatusOK)
+31 -8
go.mod
··· 3 3 go 1.24 4 4 5 5 require ( 6 + github.com/bluesky-social/indigo v0.0.0-20250621010046-488d1b91889b 6 7 github.com/go-chi/chi/v5 v5.2.1 8 + github.com/ipfs/go-cid v0.4.1 9 + github.com/ipfs/go-ipld-cbor v0.1.0 10 + github.com/ipfs/go-ipld-format v0.6.0 11 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 7 12 github.com/lib/pq v1.10.9 8 13 github.com/pressly/goose/v3 v3.22.1 14 + github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e 9 15 ) 10 16 11 17 require ( 12 18 github.com/beorn7/perks v1.0.1 // indirect 13 - github.com/bluesky-social/indigo v0.0.0-20250621010046-488d1b91889b // indirect 14 19 github.com/carlmjohnson/versioninfo v0.22.5 // indirect 15 20 github.com/cespare/xxhash/v2 v2.2.0 // indirect 16 21 github.com/felixge/httpsnoop v1.0.4 // indirect 17 22 github.com/go-logr/logr v1.4.1 // indirect 18 23 github.com/go-logr/stdr v1.2.2 // indirect 24 + github.com/gocql/gocql v1.7.0 // indirect 19 25 github.com/gogo/protobuf v1.3.2 // indirect 26 + github.com/golang/snappy v0.0.4 // indirect 20 27 github.com/google/uuid v1.6.0 // indirect 28 + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed // indirect 21 29 github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 22 30 github.com/hashicorp/go-retryablehttp v0.7.5 // indirect 23 31 github.com/hashicorp/golang-lru v1.0.2 // indirect 24 32 github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect 25 33 github.com/ipfs/bbloom v0.0.4 // indirect 26 34 github.com/ipfs/go-block-format v0.2.0 // indirect 27 - github.com/ipfs/go-cid v0.4.1 // indirect 35 + github.com/ipfs/go-blockservice v0.5.2 // indirect 28 36 github.com/ipfs/go-datastore v0.6.0 // indirect 29 37 github.com/ipfs/go-ipfs-blockstore v1.3.1 // indirect 30 38 github.com/ipfs/go-ipfs-ds-help v1.1.1 // indirect 39 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 // indirect 31 40 github.com/ipfs/go-ipfs-util v0.0.3 // indirect 32 - github.com/ipfs/go-ipld-cbor v0.1.0 // indirect 33 - github.com/ipfs/go-ipld-format v0.6.0 // indirect 41 + github.com/ipfs/go-ipld-legacy v0.2.1 // indirect 42 + github.com/ipfs/go-libipfs v0.7.0 // indirect 34 43 github.com/ipfs/go-log v1.0.5 // indirect 35 44 github.com/ipfs/go-log/v2 v2.5.1 // indirect 45 + github.com/ipfs/go-merkledag v0.11.0 // indirect 36 46 github.com/ipfs/go-metrics-interface v0.0.1 // indirect 47 + github.com/ipfs/go-verifcid v0.0.3 // indirect 48 + github.com/ipld/go-codec-dagpb v1.6.0 // indirect 49 + github.com/ipld/go-ipld-prime v0.21.0 // indirect 50 + github.com/jackc/pgpassfile v1.0.0 // indirect 51 + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect 52 + github.com/jackc/pgx/v5 v5.7.1 // indirect 53 + github.com/jackc/puddle/v2 v2.2.2 // indirect 37 54 github.com/jbenet/goprocess v0.1.4 // indirect 55 + github.com/jinzhu/inflection v1.0.0 // indirect 56 + github.com/jinzhu/now v1.1.5 // indirect 38 57 github.com/klauspost/cpuid/v2 v2.2.7 // indirect 39 58 github.com/mattn/go-isatty v0.0.20 // indirect 59 + github.com/mattn/go-sqlite3 v1.14.22 // indirect 40 60 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect 41 61 github.com/mfridman/interpolate v0.0.2 // indirect 42 62 github.com/minio/sha256-simd v1.0.1 // indirect ··· 55 75 github.com/rivo/uniseg v0.1.0 // indirect 56 76 github.com/sethvargo/go-retry v0.3.0 // indirect 57 77 github.com/spaolacci/murmur3 v1.1.0 // indirect 58 - github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 59 78 gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect 60 79 gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect 61 80 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect ··· 65 84 go.uber.org/atomic v1.11.0 // indirect 66 85 go.uber.org/multierr v1.11.0 // indirect 67 86 go.uber.org/zap v1.26.0 // indirect 68 - golang.org/x/crypto v0.27.0 // indirect 69 - golang.org/x/sync v0.8.0 // indirect 70 - golang.org/x/sys v0.25.0 // indirect 87 + golang.org/x/crypto v0.31.0 // indirect 88 + golang.org/x/sync v0.10.0 // indirect 89 + golang.org/x/sys v0.28.0 // indirect 90 + golang.org/x/text v0.21.0 // indirect 71 91 golang.org/x/time v0.3.0 // indirect 72 92 golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect 73 93 google.golang.org/protobuf v1.33.0 // indirect 94 + gopkg.in/inf.v0 v0.9.1 // indirect 95 + gorm.io/driver/postgres v1.6.0 // indirect 96 + gorm.io/gorm v1.30.0 // indirect 74 97 lukechampine.com/blake3 v1.2.1 // indirect 75 98 )
+165
go.sum
··· 1 1 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 2 github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 + github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= 4 + github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= 3 5 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 4 6 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 7 + github.com/bitly/go-hostpool v0.0.0-20171023180738-a3a6125de932/go.mod h1:NOuUCSz6Q9T7+igc/hlvDOUdtWKryOrtFyIVABv/p7k= 5 8 github.com/bluesky-social/indigo v0.0.0-20250621010046-488d1b91889b h1:QniihTdfvYFr8oJZgltN0VyWSWa28v/0DiIVFHy6nfg= 6 9 github.com/bluesky-social/indigo v0.0.0-20250621010046-488d1b91889b/go.mod h1:8FlFpF5cIq3DQG0kEHqyTkPV/5MDQoaWLcVwza5ZPJU= 10 + github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= 7 11 github.com/carlmjohnson/versioninfo v0.22.5 h1:O00sjOLUAFxYQjlN/bzYTuZiS0y6fWDQjMRvwtKgwwc= 8 12 github.com/carlmjohnson/versioninfo v0.22.5/go.mod h1:QT9mph3wcVfISUKd0i9sZfVrPviHuSF+cUtLjm2WSf8= 9 13 github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= 10 14 github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 11 15 github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= 16 + github.com/cskr/pubsub v1.0.2 h1:vlOzMhl6PFn60gRlTQQsIfVwaPB/B/8MziK8FhEPt/0= 17 + github.com/cskr/pubsub v1.0.2/go.mod h1:/8MzYXk/NJAz782G8RPkFzXTZVu63VotefPnR9TIRis= 12 18 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 19 github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 20 github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 21 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0 h1:8UrgZ3GkP4i/CLijOJx79Yu+etlyjdBU4sfcs2WYQMs= 22 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.2.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= 15 23 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 16 24 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 17 25 github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 18 26 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 27 + github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 28 + github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 19 29 github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8= 20 30 github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= 21 31 github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= ··· 24 34 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 25 35 github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 26 36 github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0= 37 + github.com/gocql/gocql v1.7.0 h1:O+7U7/1gSN7QTEAaMEsJc1Oq2QHXvCWoF3DFK9HDHus= 38 + github.com/gocql/gocql v1.7.0/go.mod h1:vnlvXyFZeLBF0Wy+RS8hrOdbn0UWsWtdg07XJnFxZ+4= 27 39 github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 28 40 github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 41 + github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 42 + github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= 43 + github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= 44 + github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 45 + github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 46 + github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8= 47 + github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 29 48 github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 30 49 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 31 50 github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 51 + github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= 32 52 github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= 53 + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed h1:5upAirOpQc1Q53c0bnx2ufif5kANL7bfZWcc6VJWJd8= 54 + github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4= 33 55 github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 34 56 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 57 + github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI= 35 58 github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= 36 59 github.com/hashicorp/go-retryablehttp v0.7.5 h1:bJj+Pj19UZMIweq/iie+1u5YCdGrnxCT9yvm0e+Nd5M= 37 60 github.com/hashicorp/go-retryablehttp v0.7.5/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= ··· 39 62 github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= 40 63 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= 41 64 github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= 65 + github.com/huin/goupnp v1.0.3 h1:N8No57ls+MnjlB+JPiCVSOyy/ot7MJTqlo7rn+NYSqQ= 66 + github.com/huin/goupnp v1.0.3/go.mod h1:ZxNlw5WqJj6wSsRK5+YfflQGXYfccj5VgQsMNixHM7Y= 42 67 github.com/ipfs/bbloom v0.0.4 h1:Gi+8EGJ2y5qiD5FbsbpX/TMNcJw8gSqr7eyjHa4Fhvs= 43 68 github.com/ipfs/bbloom v0.0.4/go.mod h1:cS9YprKXpoZ9lT0n/Mw/a6/aFV6DTjTLYHeA+gyqMG0= 69 + github.com/ipfs/go-bitswap v0.11.0 h1:j1WVvhDX1yhG32NTC9xfxnqycqYIlhzEzLXG/cU1HyQ= 70 + github.com/ipfs/go-bitswap v0.11.0/go.mod h1:05aE8H3XOU+LXpTedeAS0OZpcO1WFsj5niYQH9a1Tmk= 44 71 github.com/ipfs/go-block-format v0.2.0 h1:ZqrkxBA2ICbDRbK8KJs/u0O3dlp6gmAuuXUJNiW1Ycs= 45 72 github.com/ipfs/go-block-format v0.2.0/go.mod h1:+jpL11nFx5A/SPpsoBn6Bzkra/zaArfSmsknbPMYgzM= 73 + github.com/ipfs/go-blockservice v0.5.2 h1:in9Bc+QcXwd1apOVM7Un9t8tixPKdaHQFdLSUM1Xgk8= 74 + github.com/ipfs/go-blockservice v0.5.2/go.mod h1:VpMblFEqG67A/H2sHKAemeH9vlURVavlysbdUI632yk= 46 75 github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s= 47 76 github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 48 77 github.com/ipfs/go-datastore v0.6.0 h1:JKyz+Gvz1QEZw0LsX1IBn+JFCJQH4SJVFtM4uWU0Myk= 49 78 github.com/ipfs/go-datastore v0.6.0/go.mod h1:rt5M3nNbSO/8q1t4LNkLyUwRs8HupMeN/8O4Vn9YAT8= 79 + github.com/ipfs/go-detect-race v0.0.1 h1:qX/xay2W3E4Q1U7d9lNs1sU9nvguX0a7319XbyQ6cOk= 80 + github.com/ipfs/go-detect-race v0.0.1/go.mod h1:8BNT7shDZPo99Q74BpGMK+4D8Mn4j46UU0LZ723meps= 50 81 github.com/ipfs/go-ipfs-blockstore v1.3.1 h1:cEI9ci7V0sRNivqaOr0elDsamxXFxJMMMy7PTTDQNsQ= 51 82 github.com/ipfs/go-ipfs-blockstore v1.3.1/go.mod h1:KgtZyc9fq+P2xJUiCAzbRdhhqJHvsw8u2Dlqy2MyRTE= 83 + github.com/ipfs/go-ipfs-blocksutil v0.0.1 h1:Eh/H4pc1hsvhzsQoMEP3Bke/aW5P5rVM1IWFJMcGIPQ= 84 + github.com/ipfs/go-ipfs-blocksutil v0.0.1/go.mod h1:Yq4M86uIOmxmGPUHv/uI7uKqZNtLb449gwKqXjIsnRk= 85 + github.com/ipfs/go-ipfs-delay v0.0.1 h1:r/UXYyRcddO6thwOnhiznIAiSvxMECGgtv35Xs1IeRQ= 86 + github.com/ipfs/go-ipfs-delay v0.0.1/go.mod h1:8SP1YXK1M1kXuc4KJZINY3TQQ03J2rwBG9QfXmbRPrw= 52 87 github.com/ipfs/go-ipfs-ds-help v1.1.1 h1:B5UJOH52IbcfS56+Ul+sv8jnIV10lbjLF5eOO0C66Nw= 53 88 github.com/ipfs/go-ipfs-ds-help v1.1.1/go.mod h1:75vrVCkSdSFidJscs8n4W+77AtTpCIAdDGAwjitJMIo= 89 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1 h1:jMzo2VhLKSHbVe+mHNzYgs95n0+t0Q69GQ5WhRDZV/s= 90 + github.com/ipfs/go-ipfs-exchange-interface v0.2.1/go.mod h1:MUsYn6rKbG6CTtsDp+lKJPmVt3ZrCViNyH3rfPGsZ2E= 91 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0 h1:c/Dg8GDPzixGd0MC8Jh6mjOwU57uYokgWRFidfvEkuA= 92 + github.com/ipfs/go-ipfs-exchange-offline v0.3.0/go.mod h1:MOdJ9DChbb5u37M1IcbrRB02e++Z7521fMxqCNRrz9s= 93 + github.com/ipfs/go-ipfs-pq v0.0.2 h1:e1vOOW6MuOwG2lqxcLA+wEn93i/9laCY8sXAw76jFOY= 94 + github.com/ipfs/go-ipfs-pq v0.0.2/go.mod h1:LWIqQpqfRG3fNc5XsnIhz/wQ2XXGyugQwls7BgUmUfY= 95 + github.com/ipfs/go-ipfs-routing v0.3.0 h1:9W/W3N+g+y4ZDeffSgqhgo7BsBSJwPMcyssET9OWevc= 96 + github.com/ipfs/go-ipfs-routing v0.3.0/go.mod h1:dKqtTFIql7e1zYsEuWLyuOU+E0WJWW8JjbTPLParDWo= 54 97 github.com/ipfs/go-ipfs-util v0.0.3 h1:2RFdGez6bu2ZlZdI+rWfIdbQb1KudQp3VGwPtdNCmE0= 55 98 github.com/ipfs/go-ipfs-util v0.0.3/go.mod h1:LHzG1a0Ig4G+iZ26UUOMjHd+lfM84LZCrn17xAKWBvs= 56 99 github.com/ipfs/go-ipld-cbor v0.1.0 h1:dx0nS0kILVivGhfWuB6dUpMa/LAwElHPw1yOGYopoYs= 57 100 github.com/ipfs/go-ipld-cbor v0.1.0/go.mod h1:U2aYlmVrJr2wsUBU67K4KgepApSZddGRDWBYR0H4sCk= 58 101 github.com/ipfs/go-ipld-format v0.6.0 h1:VEJlA2kQ3LqFSIm5Vu6eIlSxD/Ze90xtc4Meten1F5U= 59 102 github.com/ipfs/go-ipld-format v0.6.0/go.mod h1:g4QVMTn3marU3qXchwjpKPKgJv+zF+OlaKMyhJ4LHPg= 103 + github.com/ipfs/go-ipld-legacy v0.2.1 h1:mDFtrBpmU7b//LzLSypVrXsD8QxkEWxu5qVxN99/+tk= 104 + github.com/ipfs/go-ipld-legacy v0.2.1/go.mod h1:782MOUghNzMO2DER0FlBR94mllfdCJCkTtDtPM51otM= 105 + github.com/ipfs/go-libipfs v0.7.0 h1:Mi54WJTODaOL2/ZSm5loi3SwI3jI2OuFWUrQIkJ5cpM= 106 + github.com/ipfs/go-libipfs v0.7.0/go.mod h1:KsIf/03CqhICzyRGyGo68tooiBE2iFbI/rXW7FhAYr0= 60 107 github.com/ipfs/go-log v1.0.5 h1:2dOuUCB1Z7uoczMWgAyDck5JLb72zHzrMnGnCNNbvY8= 61 108 github.com/ipfs/go-log v1.0.5/go.mod h1:j0b8ZoR+7+R99LD9jZ6+AJsrzkPbSXbZfGakb5JPtIo= 62 109 github.com/ipfs/go-log/v2 v2.1.3/go.mod h1:/8d0SH3Su5Ooc31QlL1WysJhvyOTDCjcCZ9Axpmri6g= 63 110 github.com/ipfs/go-log/v2 v2.5.1 h1:1XdUzF7048prq4aBjDQQ4SL5RxftpRGdXhNRwKSAlcY= 64 111 github.com/ipfs/go-log/v2 v2.5.1/go.mod h1:prSpmC1Gpllc9UYWxDiZDreBYw7zp4Iqp1kOLU9U5UI= 112 + github.com/ipfs/go-merkledag v0.11.0 h1:DgzwK5hprESOzS4O1t/wi6JDpyVQdvm9Bs59N/jqfBY= 113 + github.com/ipfs/go-merkledag v0.11.0/go.mod h1:Q4f/1ezvBiJV0YCIXvt51W/9/kqJGH4I1LsA7+djsM4= 65 114 github.com/ipfs/go-metrics-interface v0.0.1 h1:j+cpbjYvu4R8zbleSs36gvB7jR+wsL2fGD6n0jO4kdg= 66 115 github.com/ipfs/go-metrics-interface v0.0.1/go.mod h1:6s6euYU4zowdslK0GKHmqaIZ3j/b/tL7HTWtJ4VPgWY= 116 + github.com/ipfs/go-peertaskqueue v0.8.0 h1:JyNO144tfu9bx6Hpo119zvbEL9iQ760FHOiJYsUjqaU= 117 + github.com/ipfs/go-peertaskqueue v0.8.0/go.mod h1:cz8hEnnARq4Du5TGqiWKgMr/BOSQ5XOgMOh1K5YYKKM= 118 + github.com/ipfs/go-verifcid v0.0.3 h1:gmRKccqhWDocCRkC+a59g5QW7uJw5bpX9HWBevXa0zs= 119 + github.com/ipfs/go-verifcid v0.0.3/go.mod h1:gcCtGniVzelKrbk9ooUSX/pM3xlH73fZZJDzQJRvOUw= 120 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4 h1:oFo19cBmcP0Cmg3XXbrr0V/c+xU9U1huEZp8+OgBzdI= 121 + github.com/ipld/go-car v0.6.1-0.20230509095817-92d28eb23ba4/go.mod h1:6nkFF8OmR5wLKBzRKi7/YFJpyYR7+oEn1DX+mMWnlLA= 122 + github.com/ipld/go-car/v2 v2.13.1 h1:KnlrKvEPEzr5IZHKTXLAEub+tPrzeAFQVRlSQvuxBO4= 123 + github.com/ipld/go-car/v2 v2.13.1/go.mod h1:QkdjjFNGit2GIkpQ953KBwowuoukoM75nP/JI1iDJdo= 124 + github.com/ipld/go-codec-dagpb v1.6.0 h1:9nYazfyu9B1p3NAgfVdpRco3Fs2nFC72DqVsMj6rOcc= 125 + github.com/ipld/go-codec-dagpb v1.6.0/go.mod h1:ANzFhfP2uMJxRBr8CE+WQWs5UsNa0pYtmKZ+agnUw9s= 126 + github.com/ipld/go-ipld-prime v0.21.0 h1:n4JmcpOlPDIxBcY037SVfpd1G+Sj1nKZah0m6QH9C2E= 127 + github.com/ipld/go-ipld-prime v0.21.0/go.mod h1:3RLqy//ERg/y5oShXXdx5YIp50cFGOanyMctpPjsvxQ= 128 + github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= 129 + github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= 130 + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= 131 + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= 132 + github.com/jackc/pgx/v5 v5.7.1 h1:x7SYsPBYDkHDksogeSmZZ5xzThcTgRz++I5E+ePFUcs= 133 + github.com/jackc/pgx/v5 v5.7.1/go.mod h1:e7O26IywZZ+naJtWWos6i6fvWK+29etgITqrqHLfoZA= 134 + github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= 135 + github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= 136 + github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= 137 + github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= 67 138 github.com/jbenet/go-cienv v0.1.0/go.mod h1:TqNnHUmJgXau0nCzC7kXWeotg3J9W34CUv5Djy1+FlA= 68 139 github.com/jbenet/goprocess v0.1.4 h1:DRGOFReOMqqDNXwW70QkacFW0YN9QnwLV0Vqk+3oU0o= 69 140 github.com/jbenet/goprocess v0.1.4/go.mod h1:5yspPrukOVuOLORacaBi858NqyClJPQxYZlqdZVfqY4= 141 + github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 142 + github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 143 + github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= 144 + github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 145 + github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= 70 146 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= 71 147 github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 72 148 github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 73 149 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 74 150 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 151 + github.com/koron/go-ssdp v0.0.3 h1:JivLMY45N76b4p/vsWGOKewBQu6uf39y8l+AQ7sDKx8= 152 + github.com/koron/go-ssdp v0.0.3/go.mod h1:b2MxI6yh02pKrsyNoQUsk4+YNikaGhe4894J+Q5lDvA= 75 153 github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 154 + github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 155 + github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 76 156 github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 77 157 github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 158 + github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 159 + github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 78 160 github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 79 161 github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 162 + github.com/libp2p/go-buffer-pool v0.1.0 h1:oK4mSFcQz7cTQIfqbe4MIj9gLW+mnanjyFtc6cdF0Y8= 163 + github.com/libp2p/go-buffer-pool v0.1.0/go.mod h1:N+vh8gMqimBzdKkSMVuydVDq+UV5QTWy5HSiZacSbPg= 164 + github.com/libp2p/go-cidranger v1.1.0 h1:ewPN8EZ0dd1LSnrtuwd4709PXVcITVeuwbag38yPW7c= 165 + github.com/libp2p/go-cidranger v1.1.0/go.mod h1:KWZTfSr+r9qEo9OkI9/SIEeAtw+NNoU0dXIXt15Okic= 166 + github.com/libp2p/go-libp2p v0.22.0 h1:2Tce0kHOp5zASFKJbNzRElvh0iZwdtG5uZheNW8chIw= 167 + github.com/libp2p/go-libp2p v0.22.0/go.mod h1:UDolmweypBSjQb2f7xutPnwZ/fxioLbMBxSjRksxxU4= 168 + github.com/libp2p/go-libp2p-asn-util v0.2.0 h1:rg3+Os8jbnO5DxkC7K/Utdi+DkY3q/d1/1q+8WeNAsw= 169 + github.com/libp2p/go-libp2p-asn-util v0.2.0/go.mod h1:WoaWxbHKBymSN41hWSq/lGKJEca7TNm58+gGJi2WsLI= 170 + github.com/libp2p/go-libp2p-record v0.2.0 h1:oiNUOCWno2BFuxt3my4i1frNrt7PerzB3queqa1NkQ0= 171 + github.com/libp2p/go-libp2p-record v0.2.0/go.mod h1:I+3zMkvvg5m2OcSdoL0KPljyJyvNDFGKX7QdlpYUcwk= 172 + github.com/libp2p/go-libp2p-testing v0.12.0 h1:EPvBb4kKMWO29qP4mZGyhVzUyR25dvfUIK5WDu6iPUA= 173 + github.com/libp2p/go-libp2p-testing v0.12.0/go.mod h1:KcGDRXyN7sQCllucn1cOOS+Dmm7ujhfEyXQL5lvkcPg= 174 + github.com/libp2p/go-msgio v0.2.0 h1:W6shmB+FeynDrUVl2dgFQvzfBZcXiyqY4VmpQLu9FqU= 175 + github.com/libp2p/go-msgio v0.2.0/go.mod h1:dBVM1gW3Jk9XqHkU4eKdGvVHdLa51hoGfll6jMJMSlY= 176 + github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg= 177 + github.com/libp2p/go-nat v0.1.0/go.mod h1:X7teVkwRHNInVNWQiO/tAiAVRwSr5zoRz4YSTC3uRBM= 178 + github.com/libp2p/go-netroute v0.2.0 h1:0FpsbsvuSnAhXFnCY0VLFbJOzaK0VnP0r1QT/o4nWRE= 179 + github.com/libp2p/go-netroute v0.2.0/go.mod h1:Vio7LTzZ+6hoT4CMZi5/6CpY3Snzh2vgZhWgxMNwlQI= 180 + github.com/libp2p/go-openssl v0.1.0 h1:LBkKEcUv6vtZIQLVTegAil8jbNpJErQ9AnT+bWV+Ooo= 181 + github.com/libp2p/go-openssl v0.1.0/go.mod h1:OiOxwPpL3n4xlenjx2h7AwSGaFSC/KZvf6gNdOBQMtc= 80 182 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 81 183 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 82 184 github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 185 + github.com/mattn/go-pointer v0.0.1 h1:n+XhsuGeVO6MEAp7xyEukFINEa+Quek5psIR/ylA6o0= 186 + github.com/mattn/go-pointer v0.0.1/go.mod h1:2zXcozF6qYGgmsG+SeTZz3oAbFLdD3OWqnUbNvJZAlc= 187 + github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU= 188 + github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= 83 189 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= 84 190 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= 85 191 github.com/mfridman/interpolate v0.0.2 h1:pnuTK7MQIxxFz1Gr+rjSIx9u7qVjf5VOoM/u6BbAxPY= 86 192 github.com/mfridman/interpolate v0.0.2/go.mod h1:p+7uk6oE07mpE/Ik1b8EckO0O4ZXiGAfshKBWLUM9Xg= 193 + github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= 194 + github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= 87 195 github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM= 88 196 github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8= 89 197 github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o= ··· 92 200 github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI= 93 201 github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0= 94 202 github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4= 203 + github.com/multiformats/go-multiaddr v0.7.0 h1:gskHcdaCyPtp9XskVwtvEeQOG465sCohbQIirSyqxrc= 204 + github.com/multiformats/go-multiaddr v0.7.0/go.mod h1:Fs50eBDWvZu+l3/9S6xAE7ZYj6yhxlvaVZjakWN7xRs= 205 + github.com/multiformats/go-multiaddr-dns v0.3.1 h1:QgQgR+LQVt3NPTjbrLLpsaT2ufAA2y0Mkk+QRVJbW3A= 206 + github.com/multiformats/go-multiaddr-dns v0.3.1/go.mod h1:G/245BRQ6FJGmryJCrOuTdB37AMA5AMOVuO6NY3JwTk= 207 + github.com/multiformats/go-multiaddr-fmt v0.1.0 h1:WLEFClPycPkp4fnIzoFoV9FVd49/eQsuaL3/CWe167E= 208 + github.com/multiformats/go-multiaddr-fmt v0.1.0/go.mod h1:hGtDIW4PU4BqJ50gW2quDuPVjyWNZxToGUh/HwTZYJo= 95 209 github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g= 96 210 github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk= 211 + github.com/multiformats/go-multicodec v0.9.0 h1:pb/dlPnzee/Sxv/j4PmkDRxCOi3hXTz3IbPKOXWJkmg= 212 + github.com/multiformats/go-multicodec v0.9.0/go.mod h1:L3QTQvMIaVBkXOXXtVmYE+LI16i14xuaojr/H7Ai54k= 97 213 github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U= 98 214 github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM= 215 + github.com/multiformats/go-multistream v0.3.3 h1:d5PZpjwRgVlbwfdTDjife7XszfZd8KYWfROYFlGcR8o= 216 + github.com/multiformats/go-multistream v0.3.3/go.mod h1:ODRoqamLUsETKS9BNcII4gcRsJBU5VAwRIv7O39cEXg= 99 217 github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8= 100 218 github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU= 101 219 github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= 102 220 github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= 103 221 github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= 104 222 github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= 223 + github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk= 224 + github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw= 105 225 github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 106 226 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 107 227 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= ··· 122 242 github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= 123 243 github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 124 244 github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 245 + github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= 246 + github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= 125 247 github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 126 248 github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= 127 249 github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= 128 250 github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= 251 + github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= 129 252 github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= 253 + github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= 130 254 github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= 255 + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572 h1:RC6RW7j+1+HkWaX/Yh71Ee5ZHaHYt7ZP4sQgUrm6cDU= 256 + github.com/spacemonkeygo/spacelog v0.0.0-20180420211403-2296661a0572/go.mod h1:w0SWMsp6j9O/dk4/ZpIhL+3CkG8ofA2vuv7k+ltqUMc= 131 257 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= 132 258 github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= 133 259 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= ··· 138 264 github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 139 265 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 140 266 github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= 267 + github.com/warpfork/go-testmark v0.12.1 h1:rMgCpJfwy1sJ50x0M0NgyphxYYPMOODIJHhsXyEHU0s= 268 + github.com/warpfork/go-testmark v0.12.1/go.mod h1:kHwy7wfvGSPh1rQJYKayD4AbtNaeyZdcGi9tNJTaa5Y= 269 + github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0 h1:GDDkbFiaK8jsSDJfjId/PEGEShv6ugrt4kYsC5UIDaQ= 141 270 github.com/warpfork/go-wish v0.0.0-20220906213052-39a1cc7a02d0/go.mod h1:x6AKhvSSexNrVSrViXSHUEbICjmGXhtgABaHIySUSGw= 271 + github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11 h1:5HZfQkwe0mIfyDmc1Em5GqlNRzcdtlv4HTNmdpt7XH0= 272 + github.com/whyrusleeping/cbor v0.0.0-20171005072247-63513f603b11/go.mod h1:Wlo/SzPmxVp6vXpGt/zaXhHH0fn4IxgqZc82aKg6bpQ= 142 273 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4= 143 274 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so= 144 275 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= ··· 161 292 go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= 162 293 go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= 163 294 go.uber.org/goleak v1.1.11-0.20210813005559-691160354723/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= 295 + go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk= 296 + go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo= 164 297 go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 165 298 go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 166 299 go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= ··· 176 309 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 177 310 golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= 178 311 golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= 312 + golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 313 + golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 314 + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= 315 + golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= 179 316 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 180 317 golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 181 318 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 182 319 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 183 320 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 321 + golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 322 + golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 184 323 golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 185 324 golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 186 325 golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 187 326 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 188 327 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 189 328 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= 329 + golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= 330 + golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= 190 331 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 191 332 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 192 333 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 193 334 golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 194 335 golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= 195 336 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 337 + golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 338 + golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 196 339 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 197 340 golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 198 341 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= ··· 204 347 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 205 348 golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= 206 349 golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 350 + golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 351 + golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 207 352 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 208 353 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 209 354 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 355 + golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= 356 + golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 357 + golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 358 + golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 210 359 golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= 211 360 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= 212 361 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= ··· 219 368 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 220 369 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 221 370 golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= 371 + golang.org/x/tools v0.15.0 h1:zdAyfUGbYmuVokhzVmghFl2ZJh5QhcfebBgmVPFYA+8= 372 + golang.org/x/tools v0.15.0/go.mod h1:hpksKq4dtpQWS1uQ61JkdqWM3LscIS6Slf+VVkm+wQk= 222 373 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 223 374 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 224 375 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= ··· 229 380 google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 230 381 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 231 382 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 383 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 384 + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 232 385 gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 386 + gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 387 + gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 233 388 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 234 389 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 390 + gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 391 + gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 235 392 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 236 393 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 237 394 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 238 395 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 396 + gorm.io/driver/postgres v1.5.7 h1:8ptbNJTDbEmhdr62uReG5BGkdQyeasu/FZHxI0IMGnM= 397 + gorm.io/driver/postgres v1.5.7/go.mod h1:3e019WlBaYI5o5LIdNV+LyxCMNtLOQETBXL2h4chKpA= 398 + gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= 399 + gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= 400 + gorm.io/gorm v1.25.9 h1:wct0gxZIELDk8+ZqF/MVnHLkA1rvYlBWUMv2EdsK1g8= 401 + gorm.io/gorm v1.25.9/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= 402 + gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= 403 + gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= 239 404 honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 240 405 lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI= 241 406 lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+469
internal/api/handlers/repository_handler.go
··· 1 + package handlers 2 + 3 + import ( 4 + "encoding/json" 5 + "fmt" 6 + "io" 7 + "net/http" 8 + "strings" 9 + 10 + "Coves/internal/core/repository" 11 + "github.com/ipfs/go-cid" 12 + cbornode "github.com/ipfs/go-ipld-cbor" 13 + ) 14 + 15 + // RepositoryHandler handles HTTP requests for repository operations 16 + type RepositoryHandler struct { 17 + service repository.RepositoryService 18 + } 19 + 20 + // NewRepositoryHandler creates a new repository handler 21 + func NewRepositoryHandler(service repository.RepositoryService) *RepositoryHandler { 22 + return &RepositoryHandler{ 23 + service: service, 24 + } 25 + } 26 + 27 + // AT Protocol XRPC request/response types 28 + 29 + // CreateRecordRequest represents a request to create a record 30 + type CreateRecordRequest struct { 31 + Repo string `json:"repo"` // DID of the repository 32 + Collection string `json:"collection"` // NSID of the collection 33 + RKey string `json:"rkey,omitempty"` // Optional record key 34 + Validate bool `json:"validate"` // Whether to validate against lexicon 35 + Record json.RawMessage `json:"record"` // The record data 36 + } 37 + 38 + // CreateRecordResponse represents the response after creating a record 39 + type CreateRecordResponse struct { 40 + URI string `json:"uri"` // AT-URI of the created record 41 + CID string `json:"cid"` // CID of the record 42 + } 43 + 44 + // GetRecordRequest represents a request to get a record 45 + type GetRecordRequest struct { 46 + Repo string `json:"repo"` // DID of the repository 47 + Collection string `json:"collection"` // NSID of the collection 48 + RKey string `json:"rkey"` // Record key 49 + } 50 + 51 + // GetRecordResponse represents the response when getting a record 52 + type GetRecordResponse struct { 53 + URI string `json:"uri"` // AT-URI of the record 54 + CID string `json:"cid"` // CID of the record 55 + Value json.RawMessage `json:"value"` // The record data 56 + } 57 + 58 + // PutRecordRequest represents a request to update a record 59 + type PutRecordRequest struct { 60 + Repo string `json:"repo"` // DID of the repository 61 + Collection string `json:"collection"` // NSID of the collection 62 + RKey string `json:"rkey"` // Record key 63 + Validate bool `json:"validate"` // Whether to validate against lexicon 64 + Record json.RawMessage `json:"record"` // The record data 65 + } 66 + 67 + // PutRecordResponse represents the response after updating a record 68 + type PutRecordResponse struct { 69 + URI string `json:"uri"` // AT-URI of the updated record 70 + CID string `json:"cid"` // CID of the record 71 + } 72 + 73 + // DeleteRecordRequest represents a request to delete a record 74 + type DeleteRecordRequest struct { 75 + Repo string `json:"repo"` // DID of the repository 76 + Collection string `json:"collection"` // NSID of the collection 77 + RKey string `json:"rkey"` // Record key 78 + } 79 + 80 + // ListRecordsRequest represents a request to list records 81 + type ListRecordsRequest struct { 82 + Repo string `json:"repo"` // DID of the repository 83 + Collection string `json:"collection"` // NSID of the collection 84 + Limit int `json:"limit,omitempty"` 85 + Cursor string `json:"cursor,omitempty"` 86 + } 87 + 88 + // ListRecordsResponse represents the response when listing records 89 + type ListRecordsResponse struct { 90 + Cursor string `json:"cursor,omitempty"` 91 + Records []RecordOutput `json:"records"` 92 + } 93 + 94 + // RecordOutput represents a record in list responses 95 + type RecordOutput struct { 96 + URI string `json:"uri"` 97 + CID string `json:"cid"` 98 + Value json.RawMessage `json:"value"` 99 + } 100 + 101 + // Handler methods 102 + 103 + // CreateRecord handles POST /xrpc/com.atproto.repo.createRecord 104 + func (h *RepositoryHandler) CreateRecord(w http.ResponseWriter, r *http.Request) { 105 + var req CreateRecordRequest 106 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 107 + writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err)) 108 + return 109 + } 110 + 111 + // Validate required fields 112 + if req.Repo == "" || req.Collection == "" || len(req.Record) == 0 { 113 + writeError(w, http.StatusBadRequest, "missing required fields") 114 + return 115 + } 116 + 117 + // Create a generic record structure for CBOR encoding 118 + // In a real implementation, you would unmarshal to the specific lexicon type 119 + recordData := &GenericRecord{ 120 + Data: req.Record, 121 + } 122 + 123 + input := repository.CreateRecordInput{ 124 + DID: req.Repo, 125 + Collection: req.Collection, 126 + RecordKey: req.RKey, 127 + Record: recordData, 128 + Validate: req.Validate, 129 + } 130 + 131 + record, err := h.service.CreateRecord(input) 132 + if err != nil { 133 + writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create record: %v", err)) 134 + return 135 + } 136 + 137 + resp := CreateRecordResponse{ 138 + URI: record.URI, 139 + CID: record.CID.String(), 140 + } 141 + 142 + writeJSON(w, http.StatusOK, resp) 143 + } 144 + 145 + // GetRecord handles GET /xrpc/com.atproto.repo.getRecord 146 + func (h *RepositoryHandler) GetRecord(w http.ResponseWriter, r *http.Request) { 147 + // Parse query parameters 148 + repo := r.URL.Query().Get("repo") 149 + collection := r.URL.Query().Get("collection") 150 + rkey := r.URL.Query().Get("rkey") 151 + 152 + if repo == "" || collection == "" || rkey == "" { 153 + writeError(w, http.StatusBadRequest, "missing required parameters") 154 + return 155 + } 156 + 157 + input := repository.GetRecordInput{ 158 + DID: repo, 159 + Collection: collection, 160 + RecordKey: rkey, 161 + } 162 + 163 + record, err := h.service.GetRecord(input) 164 + if err != nil { 165 + if strings.Contains(err.Error(), "not found") { 166 + writeError(w, http.StatusNotFound, "record not found") 167 + return 168 + } 169 + writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get record: %v", err)) 170 + return 171 + } 172 + 173 + resp := GetRecordResponse{ 174 + URI: record.URI, 175 + CID: record.CID.String(), 176 + Value: json.RawMessage(record.Value), 177 + } 178 + 179 + writeJSON(w, http.StatusOK, resp) 180 + } 181 + 182 + // PutRecord handles POST /xrpc/com.atproto.repo.putRecord 183 + func (h *RepositoryHandler) PutRecord(w http.ResponseWriter, r *http.Request) { 184 + var req PutRecordRequest 185 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 186 + writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err)) 187 + return 188 + } 189 + 190 + // Validate required fields 191 + if req.Repo == "" || req.Collection == "" || req.RKey == "" || len(req.Record) == 0 { 192 + writeError(w, http.StatusBadRequest, "missing required fields") 193 + return 194 + } 195 + 196 + // Create a generic record structure for CBOR encoding 197 + recordData := &GenericRecord{ 198 + Data: req.Record, 199 + } 200 + 201 + input := repository.UpdateRecordInput{ 202 + DID: req.Repo, 203 + Collection: req.Collection, 204 + RecordKey: req.RKey, 205 + Record: recordData, 206 + Validate: req.Validate, 207 + } 208 + 209 + record, err := h.service.UpdateRecord(input) 210 + if err != nil { 211 + if strings.Contains(err.Error(), "not found") { 212 + writeError(w, http.StatusNotFound, "record not found") 213 + return 214 + } 215 + writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to update record: %v", err)) 216 + return 217 + } 218 + 219 + resp := PutRecordResponse{ 220 + URI: record.URI, 221 + CID: record.CID.String(), 222 + } 223 + 224 + writeJSON(w, http.StatusOK, resp) 225 + } 226 + 227 + // DeleteRecord handles POST /xrpc/com.atproto.repo.deleteRecord 228 + func (h *RepositoryHandler) DeleteRecord(w http.ResponseWriter, r *http.Request) { 229 + var req DeleteRecordRequest 230 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 231 + writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err)) 232 + return 233 + } 234 + 235 + // Validate required fields 236 + if req.Repo == "" || req.Collection == "" || req.RKey == "" { 237 + writeError(w, http.StatusBadRequest, "missing required fields") 238 + return 239 + } 240 + 241 + input := repository.DeleteRecordInput{ 242 + DID: req.Repo, 243 + Collection: req.Collection, 244 + RecordKey: req.RKey, 245 + } 246 + 247 + err := h.service.DeleteRecord(input) 248 + if err != nil { 249 + if strings.Contains(err.Error(), "not found") { 250 + writeError(w, http.StatusNotFound, "record not found") 251 + return 252 + } 253 + writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to delete record: %v", err)) 254 + return 255 + } 256 + 257 + w.WriteHeader(http.StatusOK) 258 + w.Write([]byte("{}")) 259 + } 260 + 261 + // ListRecords handles GET /xrpc/com.atproto.repo.listRecords 262 + func (h *RepositoryHandler) ListRecords(w http.ResponseWriter, r *http.Request) { 263 + // Parse query parameters 264 + repo := r.URL.Query().Get("repo") 265 + collection := r.URL.Query().Get("collection") 266 + limit := 50 // Default limit 267 + cursor := r.URL.Query().Get("cursor") 268 + 269 + if repo == "" || collection == "" { 270 + writeError(w, http.StatusBadRequest, "missing required parameters") 271 + return 272 + } 273 + 274 + // Parse limit if provided 275 + if limitStr := r.URL.Query().Get("limit"); limitStr != "" { 276 + fmt.Sscanf(limitStr, "%d", &limit) 277 + if limit > 100 { 278 + limit = 100 // Max limit 279 + } 280 + } 281 + 282 + records, nextCursor, err := h.service.ListRecords(repo, collection, limit, cursor) 283 + if err != nil { 284 + writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to list records: %v", err)) 285 + return 286 + } 287 + 288 + // Convert to output format 289 + recordOutputs := make([]RecordOutput, len(records)) 290 + for i, record := range records { 291 + recordOutputs[i] = RecordOutput{ 292 + URI: record.URI, 293 + CID: record.CID.String(), 294 + Value: json.RawMessage(record.Value), 295 + } 296 + } 297 + 298 + resp := ListRecordsResponse{ 299 + Cursor: nextCursor, 300 + Records: recordOutputs, 301 + } 302 + 303 + writeJSON(w, http.StatusOK, resp) 304 + } 305 + 306 + // GetRepo handles GET /xrpc/com.atproto.sync.getRepo 307 + func (h *RepositoryHandler) GetRepo(w http.ResponseWriter, r *http.Request) { 308 + // Parse query parameters 309 + did := r.URL.Query().Get("did") 310 + if did == "" { 311 + writeError(w, http.StatusBadRequest, "missing did parameter") 312 + return 313 + } 314 + 315 + // Export repository as CAR file 316 + carData, err := h.service.ExportRepository(did) 317 + if err != nil { 318 + if strings.Contains(err.Error(), "not found") { 319 + writeError(w, http.StatusNotFound, "repository not found") 320 + return 321 + } 322 + writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to export repository: %v", err)) 323 + return 324 + } 325 + 326 + // Set appropriate headers for CAR file 327 + w.Header().Set("Content-Type", "application/vnd.ipld.car") 328 + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(carData))) 329 + w.WriteHeader(http.StatusOK) 330 + w.Write(carData) 331 + } 332 + 333 + // Additional repository management endpoints 334 + 335 + // CreateRepository handles POST /xrpc/com.atproto.repo.createRepo 336 + func (h *RepositoryHandler) CreateRepository(w http.ResponseWriter, r *http.Request) { 337 + var req struct { 338 + DID string `json:"did"` 339 + } 340 + if err := json.NewDecoder(r.Body).Decode(&req); err != nil { 341 + writeError(w, http.StatusBadRequest, fmt.Sprintf("invalid request: %v", err)) 342 + return 343 + } 344 + 345 + if req.DID == "" { 346 + writeError(w, http.StatusBadRequest, "missing did") 347 + return 348 + } 349 + 350 + repo, err := h.service.CreateRepository(req.DID) 351 + if err != nil { 352 + if strings.Contains(err.Error(), "already exists") { 353 + writeError(w, http.StatusConflict, "repository already exists") 354 + return 355 + } 356 + writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to create repository: %v", err)) 357 + return 358 + } 359 + 360 + resp := struct { 361 + DID string `json:"did"` 362 + HeadCID string `json:"head"` 363 + }{ 364 + DID: repo.DID, 365 + HeadCID: repo.HeadCID.String(), 366 + } 367 + 368 + writeJSON(w, http.StatusOK, resp) 369 + } 370 + 371 + // GetCommit handles GET /xrpc/com.atproto.sync.getCommit 372 + func (h *RepositoryHandler) GetCommit(w http.ResponseWriter, r *http.Request) { 373 + // Parse query parameters 374 + did := r.URL.Query().Get("did") 375 + commitCIDStr := r.URL.Query().Get("cid") 376 + 377 + if did == "" || commitCIDStr == "" { 378 + writeError(w, http.StatusBadRequest, "missing required parameters") 379 + return 380 + } 381 + 382 + // Parse CID 383 + commitCID, err := cid.Parse(commitCIDStr) 384 + if err != nil { 385 + writeError(w, http.StatusBadRequest, "invalid cid") 386 + return 387 + } 388 + 389 + commit, err := h.service.GetCommit(did, commitCID) 390 + if err != nil { 391 + if strings.Contains(err.Error(), "not found") { 392 + writeError(w, http.StatusNotFound, "commit not found") 393 + return 394 + } 395 + writeError(w, http.StatusInternalServerError, fmt.Sprintf("failed to get commit: %v", err)) 396 + return 397 + } 398 + 399 + resp := struct { 400 + CID string `json:"cid"` 401 + DID string `json:"did"` 402 + Version int `json:"version"` 403 + PrevCID *string `json:"prev,omitempty"` 404 + DataCID string `json:"data"` 405 + Revision string `json:"rev"` 406 + Signature string `json:"sig"` 407 + CreatedAt string `json:"createdAt"` 408 + }{ 409 + CID: commit.CID.String(), 410 + DID: commit.DID, 411 + Version: commit.Version, 412 + DataCID: commit.DataCID.String(), 413 + Revision: commit.Revision, 414 + Signature: fmt.Sprintf("%x", commit.Signature), 415 + CreatedAt: commit.CreatedAt.Format("2006-01-02T15:04:05Z"), 416 + } 417 + 418 + if commit.PrevCID != nil { 419 + prev := commit.PrevCID.String() 420 + resp.PrevCID = &prev 421 + } 422 + 423 + writeJSON(w, http.StatusOK, resp) 424 + } 425 + 426 + // Helper functions 427 + 428 + func writeJSON(w http.ResponseWriter, status int, data interface{}) { 429 + w.Header().Set("Content-Type", "application/json") 430 + w.WriteHeader(status) 431 + json.NewEncoder(w).Encode(data) 432 + } 433 + 434 + func writeError(w http.ResponseWriter, status int, message string) { 435 + w.Header().Set("Content-Type", "application/json") 436 + w.WriteHeader(status) 437 + json.NewEncoder(w).Encode(map[string]interface{}{ 438 + "error": http.StatusText(status), 439 + "message": message, 440 + }) 441 + } 442 + 443 + // GenericRecord is a temporary structure for CBOR encoding 444 + // In a real implementation, you would have specific types for each lexicon 445 + type GenericRecord struct { 446 + Data json.RawMessage 447 + } 448 + 449 + // MarshalCBOR implements the CBORMarshaler interface 450 + func (g *GenericRecord) MarshalCBOR(w io.Writer) error { 451 + // Parse JSON data into a generic map for proper CBOR encoding 452 + var data map[string]interface{} 453 + if err := json.Unmarshal(g.Data, &data); err != nil { 454 + return fmt.Errorf("failed to unmarshal JSON data: %w", err) 455 + } 456 + 457 + // Use IPFS CBOR encoding to properly encode the data 458 + cborData, err := cbornode.DumpObject(data) 459 + if err != nil { 460 + return fmt.Errorf("failed to marshal as CBOR: %w", err) 461 + } 462 + 463 + _, err = w.Write(cborData) 464 + if err != nil { 465 + return fmt.Errorf("failed to write CBOR data: %w", err) 466 + } 467 + 468 + return nil 469 + }
+191
internal/api/handlers/repository_handler_test.go
··· 1 + package handlers 2 + 3 + import ( 4 + "bytes" 5 + "encoding/json" 6 + "net/http" 7 + "net/http/httptest" 8 + "testing" 9 + 10 + "Coves/internal/core/repository" 11 + "github.com/ipfs/go-cid" 12 + ) 13 + 14 + // MockRepositoryService is a mock implementation for testing 15 + type MockRepositoryService struct { 16 + repositories map[string]*repository.Repository 17 + records map[string]*repository.Record 18 + } 19 + 20 + func NewMockRepositoryService() *MockRepositoryService { 21 + return &MockRepositoryService{ 22 + repositories: make(map[string]*repository.Repository), 23 + records: make(map[string]*repository.Record), 24 + } 25 + } 26 + 27 + func (m *MockRepositoryService) CreateRepository(did string) (*repository.Repository, error) { 28 + repo := &repository.Repository{ 29 + DID: did, 30 + HeadCID: cid.Undef, 31 + } 32 + m.repositories[did] = repo 33 + return repo, nil 34 + } 35 + 36 + func (m *MockRepositoryService) GetRepository(did string) (*repository.Repository, error) { 37 + repo, exists := m.repositories[did] 38 + if !exists { 39 + return nil, nil 40 + } 41 + return repo, nil 42 + } 43 + 44 + func (m *MockRepositoryService) DeleteRepository(did string) error { 45 + delete(m.repositories, did) 46 + return nil 47 + } 48 + 49 + func (m *MockRepositoryService) CreateRecord(input repository.CreateRecordInput) (*repository.Record, error) { 50 + uri := "at://" + input.DID + "/" + input.Collection + "/" + input.RecordKey 51 + record := &repository.Record{ 52 + URI: uri, 53 + CID: cid.Undef, 54 + Collection: input.Collection, 55 + RecordKey: input.RecordKey, 56 + Value: []byte(`{"test": "data"}`), 57 + } 58 + m.records[uri] = record 59 + return record, nil 60 + } 61 + 62 + func (m *MockRepositoryService) GetRecord(input repository.GetRecordInput) (*repository.Record, error) { 63 + uri := "at://" + input.DID + "/" + input.Collection + "/" + input.RecordKey 64 + record, exists := m.records[uri] 65 + if !exists { 66 + return nil, nil 67 + } 68 + return record, nil 69 + } 70 + 71 + func (m *MockRepositoryService) UpdateRecord(input repository.UpdateRecordInput) (*repository.Record, error) { 72 + uri := "at://" + input.DID + "/" + input.Collection + "/" + input.RecordKey 73 + record := &repository.Record{ 74 + URI: uri, 75 + CID: cid.Undef, 76 + Collection: input.Collection, 77 + RecordKey: input.RecordKey, 78 + Value: []byte(`{"test": "updated"}`), 79 + } 80 + m.records[uri] = record 81 + return record, nil 82 + } 83 + 84 + func (m *MockRepositoryService) DeleteRecord(input repository.DeleteRecordInput) error { 85 + uri := "at://" + input.DID + "/" + input.Collection + "/" + input.RecordKey 86 + delete(m.records, uri) 87 + return nil 88 + } 89 + 90 + func (m *MockRepositoryService) ListRecords(did string, collection string, limit int, cursor string) ([]*repository.Record, string, error) { 91 + var records []*repository.Record 92 + for _, record := range m.records { 93 + if record.Collection == collection { 94 + records = append(records, record) 95 + } 96 + } 97 + return records, "", nil 98 + } 99 + 100 + func (m *MockRepositoryService) GetCommit(did string, cid cid.Cid) (*repository.Commit, error) { 101 + return nil, nil 102 + } 103 + 104 + func (m *MockRepositoryService) ListCommits(did string, limit int, cursor string) ([]*repository.Commit, string, error) { 105 + return []*repository.Commit{}, "", nil 106 + } 107 + 108 + func (m *MockRepositoryService) ExportRepository(did string) ([]byte, error) { 109 + return []byte("mock-car-data"), nil 110 + } 111 + 112 + func (m *MockRepositoryService) ImportRepository(did string, carData []byte) error { 113 + return nil 114 + } 115 + 116 + func TestCreateRecordHandler(t *testing.T) { 117 + mockService := NewMockRepositoryService() 118 + handler := NewRepositoryHandler(mockService) 119 + 120 + // Create test request 121 + reqData := CreateRecordRequest{ 122 + Repo: "did:plc:test123", 123 + Collection: "app.bsky.feed.post", 124 + RKey: "testkey", 125 + Record: json.RawMessage(`{"text": "Hello, world!"}`), 126 + } 127 + 128 + reqBody, err := json.Marshal(reqData) 129 + if err != nil { 130 + t.Fatalf("Failed to marshal request: %v", err) 131 + } 132 + 133 + req := httptest.NewRequest("POST", "/xrpc/com.atproto.repo.createRecord", bytes.NewReader(reqBody)) 134 + req.Header.Set("Content-Type", "application/json") 135 + w := httptest.NewRecorder() 136 + 137 + // Call handler 138 + handler.CreateRecord(w, req) 139 + 140 + // Check response 141 + if w.Code != http.StatusOK { 142 + t.Errorf("Expected status 200, got %d", w.Code) 143 + } 144 + 145 + var resp CreateRecordResponse 146 + if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { 147 + t.Fatalf("Failed to decode response: %v", err) 148 + } 149 + 150 + expectedURI := "at://did:plc:test123/app.bsky.feed.post/testkey" 151 + if resp.URI != expectedURI { 152 + t.Errorf("Expected URI %s, got %s", expectedURI, resp.URI) 153 + } 154 + } 155 + 156 + func TestGetRecordHandler(t *testing.T) { 157 + mockService := NewMockRepositoryService() 158 + handler := NewRepositoryHandler(mockService) 159 + 160 + // Create a test record first 161 + uri := "at://did:plc:test123/app.bsky.feed.post/testkey" 162 + testRecord := &repository.Record{ 163 + URI: uri, 164 + CID: cid.Undef, 165 + Collection: "app.bsky.feed.post", 166 + RecordKey: "testkey", 167 + Value: []byte(`{"text": "Hello, world!"}`), 168 + } 169 + mockService.records[uri] = testRecord 170 + 171 + // Create test request 172 + req := httptest.NewRequest("GET", "/xrpc/com.atproto.repo.getRecord?repo=did:plc:test123&collection=app.bsky.feed.post&rkey=testkey", nil) 173 + w := httptest.NewRecorder() 174 + 175 + // Call handler 176 + handler.GetRecord(w, req) 177 + 178 + // Check response 179 + if w.Code != http.StatusOK { 180 + t.Errorf("Expected status 200, got %d", w.Code) 181 + } 182 + 183 + var resp GetRecordResponse 184 + if err := json.NewDecoder(w.Body).Decode(&resp); err != nil { 185 + t.Fatalf("Failed to decode response: %v", err) 186 + } 187 + 188 + if resp.URI != uri { 189 + t.Errorf("Expected URI %s, got %s", uri, resp.URI) 190 + } 191 + }
+33
internal/api/routes/repository.go
··· 1 + package routes 2 + 3 + import ( 4 + "Coves/internal/api/handlers" 5 + "Coves/internal/core/repository" 6 + "github.com/go-chi/chi/v5" 7 + ) 8 + 9 + // RepositoryRoutes returns repository-related routes 10 + func RepositoryRoutes(service repository.RepositoryService) chi.Router { 11 + handler := handlers.NewRepositoryHandler(service) 12 + 13 + r := chi.NewRouter() 14 + 15 + // AT Protocol XRPC endpoints for repository operations 16 + r.Route("/xrpc", func(r chi.Router) { 17 + // Record operations 18 + r.Post("/com.atproto.repo.createRecord", handler.CreateRecord) 19 + r.Get("/com.atproto.repo.getRecord", handler.GetRecord) 20 + r.Post("/com.atproto.repo.putRecord", handler.PutRecord) 21 + r.Post("/com.atproto.repo.deleteRecord", handler.DeleteRecord) 22 + r.Get("/com.atproto.repo.listRecords", handler.ListRecords) 23 + 24 + // Repository operations 25 + r.Post("/com.atproto.repo.createRepo", handler.CreateRepository) 26 + 27 + // Sync operations 28 + r.Get("/com.atproto.sync.getRepo", handler.GetRepo) 29 + r.Get("/com.atproto.sync.getCommit", handler.GetCommit) 30 + }) 31 + 32 + return r 33 + }
+20
internal/api/routes/user.go
··· 1 + package routes 2 + 3 + import ( 4 + "Coves/internal/core/users" 5 + "github.com/go-chi/chi/v5" 6 + "net/http" 7 + ) 8 + 9 + // UserRoutes returns user-related routes 10 + func UserRoutes(service users.UserService) chi.Router { 11 + r := chi.NewRouter() 12 + 13 + // TODO: Implement user handlers 14 + r.Get("/", func(w http.ResponseWriter, r *http.Request) { 15 + w.WriteHeader(http.StatusOK) 16 + w.Write([]byte("User routes not yet implemented")) 17 + }) 18 + 19 + return r 20 + }
+104
internal/atproto/carstore/README.md
··· 1 + # CarStore Package 2 + 3 + This package provides integration with Indigo's carstore for managing ATProto repository CAR files in the Coves platform. 4 + 5 + ## Overview 6 + 7 + The carstore package wraps Indigo's carstore implementation to provide: 8 + - Filesystem-based storage of CAR (Content Addressable aRchive) files 9 + - PostgreSQL metadata tracking via GORM 10 + - DID to UID mapping for user repositories 11 + - Automatic garbage collection and compaction 12 + 13 + ## Architecture 14 + 15 + ``` 16 + [Repository Service] 17 + 18 + [RepoStore] ← Provides DID-based interface 19 + 20 + [CarStore] ← Wraps Indigo's carstore 21 + 22 + [Indigo CarStore] ← Actual implementation 23 + 24 + [PostgreSQL + Filesystem] 25 + ``` 26 + 27 + ## Components 28 + 29 + ### CarStore (`carstore.go`) 30 + Wraps Indigo's carstore implementation, providing methods for: 31 + - `ImportSlice`: Import CAR data for a user 32 + - `ReadUserCar`: Export user's repository as CAR 33 + - `GetUserRepoHead`: Get latest repository state 34 + - `CompactUserShards`: Run garbage collection 35 + - `WipeUserData`: Delete all user data 36 + 37 + ### UserMapping (`user_mapping.go`) 38 + Maps DIDs (Decentralized Identifiers) to numeric UIDs required by Indigo's carstore: 39 + - DIDs are strings like `did:plc:abc123xyz` 40 + - UIDs are numeric identifiers (models.Uid) 41 + - Maintains bidirectional mapping in PostgreSQL 42 + 43 + ### RepoStore (`repo_store.go`) 44 + Combines CarStore with UserMapping to provide DID-based operations: 45 + - `ImportRepo`: Import repository for a DID 46 + - `ReadRepo`: Export repository for a DID 47 + - `GetRepoHead`: Get latest state for a DID 48 + - `CompactRepo`: Run garbage collection for a DID 49 + - `DeleteRepo`: Remove all data for a DID 50 + 51 + ## Data Flow 52 + 53 + ### Creating a New Repository 54 + 1. Service calls `RepoStore.ImportRepo(did, carData)` 55 + 2. RepoStore maps DID to UID via UserMapping 56 + 3. CarStore imports the CAR slice 57 + 4. Indigo's carstore: 58 + - Stores CAR data as file on disk 59 + - Records metadata in PostgreSQL 60 + 61 + ### Reading a Repository 62 + 1. Service calls `RepoStore.ReadRepo(did)` 63 + 2. RepoStore maps DID to UID 64 + 3. CarStore reads user's CAR data 65 + 4. Returns complete CAR file 66 + 67 + ## Database Schema 68 + 69 + ### user_maps table 70 + ```sql 71 + CREATE TABLE user_maps ( 72 + uid SERIAL PRIMARY KEY, 73 + did VARCHAR UNIQUE NOT NULL, 74 + created_at BIGINT, 75 + updated_at BIGINT 76 + ); 77 + ``` 78 + 79 + ### Indigo's tables (auto-created) 80 + - `car_shards`: Metadata about CAR file shards 81 + - `block_refs`: Block reference tracking 82 + 83 + ## Storage 84 + 85 + CAR files are stored on the filesystem at the path specified during initialization (e.g., `./data/carstore/`). The storage is organized by Indigo's carstore implementation, typically with sharding for performance. 86 + 87 + ## Configuration 88 + 89 + Initialize the carstore with: 90 + ```go 91 + carDirs := []string{"./data/carstore"} 92 + repoStore, err := carstore.NewRepoStore(gormDB, carDirs) 93 + ``` 94 + 95 + ## Future Enhancements 96 + 97 + Current implementation supports repository-level operations. Record-level CRUD operations would require: 98 + 1. Reading the CAR file 99 + 2. Parsing into a repository structure 100 + 3. Modifying records 101 + 4. Re-serializing as CAR 102 + 5. Writing back to carstore 103 + 104 + This is planned for future XRPC implementation.
+72
internal/atproto/carstore/carstore.go
··· 1 + package carstore 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "io" 7 + 8 + "github.com/bluesky-social/indigo/carstore" 9 + "github.com/bluesky-social/indigo/models" 10 + "github.com/ipfs/go-cid" 11 + "gorm.io/gorm" 12 + ) 13 + 14 + // CarStore wraps Indigo's carstore for managing ATProto repository CAR files 15 + type CarStore struct { 16 + cs carstore.CarStore 17 + } 18 + 19 + // NewCarStore creates a new CarStore instance using Indigo's implementation 20 + func NewCarStore(db *gorm.DB, carDirs []string) (*CarStore, error) { 21 + // Initialize Indigo's carstore 22 + cs, err := carstore.NewCarStore(db, carDirs) 23 + if err != nil { 24 + return nil, fmt.Errorf("failed to create carstore: %w", err) 25 + } 26 + 27 + return &CarStore{ 28 + cs: cs, 29 + }, nil 30 + } 31 + 32 + // ImportSlice imports a CAR file slice for a user 33 + func (c *CarStore) ImportSlice(ctx context.Context, uid models.Uid, since *string, carData []byte) (cid.Cid, error) { 34 + rootCid, _, err := c.cs.ImportSlice(ctx, uid, since, carData) 35 + return rootCid, err 36 + } 37 + 38 + // ReadUserCar reads a user's repository CAR file 39 + func (c *CarStore) ReadUserCar(ctx context.Context, uid models.Uid, sinceRev string, incremental bool, w io.Writer) error { 40 + return c.cs.ReadUserCar(ctx, uid, sinceRev, incremental, w) 41 + } 42 + 43 + // GetUserRepoHead gets the latest repository head CID for a user 44 + func (c *CarStore) GetUserRepoHead(ctx context.Context, uid models.Uid) (cid.Cid, error) { 45 + return c.cs.GetUserRepoHead(ctx, uid) 46 + } 47 + 48 + // CompactUserShards performs garbage collection and compaction for a user's data 49 + func (c *CarStore) CompactUserShards(ctx context.Context, uid models.Uid, aggressive bool) error { 50 + _, err := c.cs.CompactUserShards(ctx, uid, aggressive) 51 + return err 52 + } 53 + 54 + // WipeUserData removes all data for a user 55 + func (c *CarStore) WipeUserData(ctx context.Context, uid models.Uid) error { 56 + return c.cs.WipeUserData(ctx, uid) 57 + } 58 + 59 + // NewDeltaSession creates a new session for writing deltas 60 + func (c *CarStore) NewDeltaSession(ctx context.Context, uid models.Uid, since *string) (*carstore.DeltaSession, error) { 61 + return c.cs.NewDeltaSession(ctx, uid, since) 62 + } 63 + 64 + // ReadOnlySession creates a read-only session for reading user data 65 + func (c *CarStore) ReadOnlySession(uid models.Uid) (*carstore.DeltaSession, error) { 66 + return c.cs.ReadOnlySession(uid) 67 + } 68 + 69 + // Stat returns statistics about the carstore 70 + func (c *CarStore) Stat(ctx context.Context, uid models.Uid) ([]carstore.UserStat, error) { 71 + return c.cs.Stat(ctx, uid) 72 + }
+122
internal/atproto/carstore/repo_store.go
··· 1 + package carstore 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "fmt" 7 + "io" 8 + 9 + "github.com/bluesky-social/indigo/models" 10 + "github.com/ipfs/go-cid" 11 + "gorm.io/gorm" 12 + ) 13 + 14 + // RepoStore combines CarStore with UserMapping to provide DID-based repository storage 15 + type RepoStore struct { 16 + cs *CarStore 17 + mapping *UserMapping 18 + } 19 + 20 + // NewRepoStore creates a new RepoStore instance 21 + func NewRepoStore(db *gorm.DB, carDirs []string) (*RepoStore, error) { 22 + // Create carstore 23 + cs, err := NewCarStore(db, carDirs) 24 + if err != nil { 25 + return nil, fmt.Errorf("failed to create carstore: %w", err) 26 + } 27 + 28 + // Create user mapping 29 + mapping, err := NewUserMapping(db) 30 + if err != nil { 31 + return nil, fmt.Errorf("failed to create user mapping: %w", err) 32 + } 33 + 34 + return &RepoStore{ 35 + cs: cs, 36 + mapping: mapping, 37 + }, nil 38 + } 39 + 40 + // ImportRepo imports a repository CAR file for a DID 41 + func (rs *RepoStore) ImportRepo(ctx context.Context, did string, carData io.Reader) (cid.Cid, error) { 42 + uid, err := rs.mapping.GetOrCreateUID(ctx, did) 43 + if err != nil { 44 + return cid.Undef, fmt.Errorf("failed to get UID for DID %s: %w", did, err) 45 + } 46 + 47 + // Read all data from the reader 48 + data, err := io.ReadAll(carData) 49 + if err != nil { 50 + return cid.Undef, fmt.Errorf("failed to read CAR data: %w", err) 51 + } 52 + 53 + return rs.cs.ImportSlice(ctx, uid, nil, data) 54 + } 55 + 56 + // ReadRepo reads a repository CAR file for a DID 57 + func (rs *RepoStore) ReadRepo(ctx context.Context, did string, sinceRev string) ([]byte, error) { 58 + uid, err := rs.mapping.GetUID(did) 59 + if err != nil { 60 + return nil, fmt.Errorf("failed to get UID for DID %s: %w", did, err) 61 + } 62 + 63 + var buf bytes.Buffer 64 + err = rs.cs.ReadUserCar(ctx, uid, sinceRev, false, &buf) 65 + if err != nil { 66 + return nil, fmt.Errorf("failed to read repo for DID %s: %w", did, err) 67 + } 68 + 69 + return buf.Bytes(), nil 70 + } 71 + 72 + // GetRepoHead gets the latest repository head CID for a DID 73 + func (rs *RepoStore) GetRepoHead(ctx context.Context, did string) (cid.Cid, error) { 74 + uid, err := rs.mapping.GetUID(did) 75 + if err != nil { 76 + return cid.Undef, fmt.Errorf("failed to get UID for DID %s: %w", did, err) 77 + } 78 + 79 + return rs.cs.GetUserRepoHead(ctx, uid) 80 + } 81 + 82 + // CompactRepo performs garbage collection for a DID's repository 83 + func (rs *RepoStore) CompactRepo(ctx context.Context, did string) error { 84 + uid, err := rs.mapping.GetUID(did) 85 + if err != nil { 86 + return fmt.Errorf("failed to get UID for DID %s: %w", did, err) 87 + } 88 + 89 + return rs.cs.CompactUserShards(ctx, uid, false) 90 + } 91 + 92 + // DeleteRepo removes all data for a DID's repository 93 + func (rs *RepoStore) DeleteRepo(ctx context.Context, did string) error { 94 + uid, err := rs.mapping.GetUID(did) 95 + if err != nil { 96 + return fmt.Errorf("failed to get UID for DID %s: %w", did, err) 97 + } 98 + 99 + return rs.cs.WipeUserData(ctx, uid) 100 + } 101 + 102 + // HasRepo checks if a repository exists for a DID 103 + func (rs *RepoStore) HasRepo(ctx context.Context, did string) (bool, error) { 104 + uid, err := rs.mapping.GetUID(did) 105 + if err != nil { 106 + // If no UID mapping exists, repo doesn't exist 107 + return false, nil 108 + } 109 + 110 + // Try to get the repo head 111 + head, err := rs.cs.GetUserRepoHead(ctx, uid) 112 + if err != nil { 113 + return false, nil 114 + } 115 + 116 + return head.Defined(), nil 117 + } 118 + 119 + // GetOrCreateUID gets or creates a UID for a DID 120 + func (rs *RepoStore) GetOrCreateUID(ctx context.Context, did string) (models.Uid, error) { 121 + return rs.mapping.GetOrCreateUID(ctx, did) 122 + }
+127
internal/atproto/carstore/user_mapping.go
··· 1 + package carstore 2 + 3 + import ( 4 + "context" 5 + "fmt" 6 + "sync" 7 + 8 + "github.com/bluesky-social/indigo/models" 9 + "gorm.io/gorm" 10 + ) 11 + 12 + // UserMapping manages the mapping between DIDs and numeric UIDs required by Indigo's carstore 13 + type UserMapping struct { 14 + db *gorm.DB 15 + mu sync.RWMutex 16 + didToUID map[string]models.Uid 17 + uidToDID map[models.Uid]string 18 + nextUID models.Uid 19 + } 20 + 21 + // UserMap represents the database model for DID to UID mapping 22 + type UserMap struct { 23 + UID models.Uid `gorm:"primaryKey;autoIncrement"` 24 + DID string `gorm:"uniqueIndex;not null"` 25 + CreatedAt int64 26 + UpdatedAt int64 27 + } 28 + 29 + // NewUserMapping creates a new UserMapping instance 30 + func NewUserMapping(db *gorm.DB) (*UserMapping, error) { 31 + // Auto-migrate the user mapping table 32 + if err := db.AutoMigrate(&UserMap{}); err != nil { 33 + return nil, fmt.Errorf("failed to migrate user mapping table: %w", err) 34 + } 35 + 36 + um := &UserMapping{ 37 + db: db, 38 + didToUID: make(map[string]models.Uid), 39 + uidToDID: make(map[models.Uid]string), 40 + nextUID: 1, 41 + } 42 + 43 + // Load existing mappings 44 + if err := um.loadMappings(); err != nil { 45 + return nil, fmt.Errorf("failed to load user mappings: %w", err) 46 + } 47 + 48 + return um, nil 49 + } 50 + 51 + // loadMappings loads all existing DID to UID mappings from the database 52 + func (um *UserMapping) loadMappings() error { 53 + var mappings []UserMap 54 + if err := um.db.Find(&mappings).Error; err != nil { 55 + return err 56 + } 57 + 58 + um.mu.Lock() 59 + defer um.mu.Unlock() 60 + 61 + for _, m := range mappings { 62 + um.didToUID[m.DID] = m.UID 63 + um.uidToDID[m.UID] = m.DID 64 + if m.UID >= um.nextUID { 65 + um.nextUID = m.UID + 1 66 + } 67 + } 68 + 69 + return nil 70 + } 71 + 72 + // GetOrCreateUID gets or creates a UID for a given DID 73 + func (um *UserMapping) GetOrCreateUID(ctx context.Context, did string) (models.Uid, error) { 74 + um.mu.RLock() 75 + if uid, exists := um.didToUID[did]; exists { 76 + um.mu.RUnlock() 77 + return uid, nil 78 + } 79 + um.mu.RUnlock() 80 + 81 + // Need to create a new mapping 82 + um.mu.Lock() 83 + defer um.mu.Unlock() 84 + 85 + // Double-check in case another goroutine created it 86 + if uid, exists := um.didToUID[did]; exists { 87 + return uid, nil 88 + } 89 + 90 + // Create new mapping 91 + userMap := &UserMap{ 92 + DID: did, 93 + } 94 + 95 + if err := um.db.Create(userMap).Error; err != nil { 96 + return 0, fmt.Errorf("failed to create user mapping: %w", err) 97 + } 98 + 99 + um.didToUID[did] = userMap.UID 100 + um.uidToDID[userMap.UID] = did 101 + 102 + return userMap.UID, nil 103 + } 104 + 105 + // GetUID returns the UID for a DID, or an error if not found 106 + func (um *UserMapping) GetUID(did string) (models.Uid, error) { 107 + um.mu.RLock() 108 + defer um.mu.RUnlock() 109 + 110 + uid, exists := um.didToUID[did] 111 + if !exists { 112 + return 0, fmt.Errorf("UID not found for DID: %s", did) 113 + } 114 + return uid, nil 115 + } 116 + 117 + // GetDID returns the DID for a UID, or an error if not found 118 + func (um *UserMapping) GetDID(uid models.Uid) (string, error) { 119 + um.mu.RLock() 120 + defer um.mu.RUnlock() 121 + 122 + did, exists := um.uidToDID[uid] 123 + if !exists { 124 + return "", fmt.Errorf("DID not found for UID: %d", uid) 125 + } 126 + return did, nil 127 + }
+201
internal/atproto/repo/wrapper.go
··· 1 + package repo 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "fmt" 7 + 8 + "github.com/bluesky-social/indigo/mst" 9 + "github.com/bluesky-social/indigo/repo" 10 + "github.com/ipfs/go-cid" 11 + blockstore "github.com/ipfs/go-ipfs-blockstore" 12 + cbornode "github.com/ipfs/go-ipld-cbor" 13 + cbg "github.com/whyrusleeping/cbor-gen" 14 + ) 15 + 16 + // Wrapper provides a thin wrapper around Indigo's repo package 17 + type Wrapper struct { 18 + repo *repo.Repo 19 + blockstore blockstore.Blockstore 20 + } 21 + 22 + // NewWrapper creates a new wrapper for a repository with the provided blockstore 23 + func NewWrapper(did string, signingKey interface{}, bs blockstore.Blockstore) (*Wrapper, error) { 24 + // Create new repository with the provided blockstore 25 + r := repo.NewRepo(context.Background(), did, bs) 26 + 27 + return &Wrapper{ 28 + repo: r, 29 + blockstore: bs, 30 + }, nil 31 + } 32 + 33 + // OpenWrapper opens an existing repository from CAR data with the provided blockstore 34 + func OpenWrapper(carData []byte, signingKey interface{}, bs blockstore.Blockstore) (*Wrapper, error) { 35 + r, err := repo.ReadRepoFromCar(context.Background(), bytes.NewReader(carData)) 36 + if err != nil { 37 + return nil, fmt.Errorf("failed to read repo from CAR: %w", err) 38 + } 39 + 40 + return &Wrapper{ 41 + repo: r, 42 + blockstore: bs, 43 + }, nil 44 + } 45 + 46 + // CreateRecord adds a new record to the repository 47 + func (w *Wrapper) CreateRecord(collection string, recordKey string, record cbg.CBORMarshaler) (cid.Cid, string, error) { 48 + // The repo.CreateRecord generates its own key, so we'll use that 49 + recordCID, rkey, err := w.repo.CreateRecord(context.Background(), collection, record) 50 + if err != nil { 51 + return cid.Undef, "", fmt.Errorf("failed to create record: %w", err) 52 + } 53 + 54 + // If a specific key was requested, we'd need to use PutRecord instead 55 + if recordKey != "" { 56 + // Use PutRecord for specific keys 57 + path := fmt.Sprintf("%s/%s", collection, recordKey) 58 + recordCID, err = w.repo.PutRecord(context.Background(), path, record) 59 + if err != nil { 60 + return cid.Undef, "", fmt.Errorf("failed to put record with key: %w", err) 61 + } 62 + return recordCID, recordKey, nil 63 + } 64 + 65 + return recordCID, rkey, nil 66 + } 67 + 68 + // GetRecord retrieves a record from the repository 69 + func (w *Wrapper) GetRecord(collection string, recordKey string) (cid.Cid, []byte, error) { 70 + path := fmt.Sprintf("%s/%s", collection, recordKey) 71 + 72 + recordCID, rec, err := w.repo.GetRecord(context.Background(), path) 73 + if err != nil { 74 + return cid.Undef, nil, fmt.Errorf("failed to get record: %w", err) 75 + } 76 + 77 + // Encode record to CBOR 78 + buf := new(bytes.Buffer) 79 + if err := rec.(cbg.CBORMarshaler).MarshalCBOR(buf); err != nil { 80 + return cid.Undef, nil, fmt.Errorf("failed to encode record: %w", err) 81 + } 82 + 83 + return recordCID, buf.Bytes(), nil 84 + } 85 + 86 + // UpdateRecord updates an existing record in the repository 87 + func (w *Wrapper) UpdateRecord(collection string, recordKey string, record cbg.CBORMarshaler) (cid.Cid, error) { 88 + path := fmt.Sprintf("%s/%s", collection, recordKey) 89 + 90 + // Check if record exists 91 + _, _, err := w.repo.GetRecord(context.Background(), path) 92 + if err != nil { 93 + return cid.Undef, fmt.Errorf("record not found: %w", err) 94 + } 95 + 96 + // Update the record 97 + recordCID, err := w.repo.UpdateRecord(context.Background(), path, record) 98 + if err != nil { 99 + return cid.Undef, fmt.Errorf("failed to update record: %w", err) 100 + } 101 + 102 + return recordCID, nil 103 + } 104 + 105 + // DeleteRecord removes a record from the repository 106 + func (w *Wrapper) DeleteRecord(collection string, recordKey string) error { 107 + path := fmt.Sprintf("%s/%s", collection, recordKey) 108 + 109 + if err := w.repo.DeleteRecord(context.Background(), path); err != nil { 110 + return fmt.Errorf("failed to delete record: %w", err) 111 + } 112 + 113 + return nil 114 + } 115 + 116 + // ListRecords returns all records in a collection 117 + func (w *Wrapper) ListRecords(collection string) ([]RecordInfo, error) { 118 + var records []RecordInfo 119 + 120 + err := w.repo.ForEach(context.Background(), collection, func(k string, v cid.Cid) error { 121 + // Skip if not in the requested collection 122 + if len(k) <= len(collection)+1 || k[:len(collection)] != collection || k[len(collection)] != '/' { 123 + return nil 124 + } 125 + 126 + recordKey := k[len(collection)+1:] 127 + records = append(records, RecordInfo{ 128 + Collection: collection, 129 + RecordKey: recordKey, 130 + CID: v, 131 + }) 132 + 133 + return nil 134 + }) 135 + 136 + if err != nil { 137 + return nil, fmt.Errorf("failed to list records: %w", err) 138 + } 139 + 140 + return records, nil 141 + } 142 + 143 + // Commit creates a new signed commit 144 + func (w *Wrapper) Commit(did string, signingKey interface{}) (*repo.SignedCommit, error) { 145 + // The commit function expects a signing function with context 146 + signingFunc := func(ctx context.Context, did string, data []byte) ([]byte, error) { 147 + // TODO: Implement proper signing based on signingKey type 148 + return []byte("mock-signature"), nil 149 + } 150 + 151 + _, _, err := w.repo.Commit(context.Background(), signingFunc) 152 + if err != nil { 153 + return nil, fmt.Errorf("failed to commit: %w", err) 154 + } 155 + 156 + // Return the signed commit from the repo 157 + sc := w.repo.SignedCommit() 158 + 159 + return &sc, nil 160 + } 161 + 162 + // GetHeadCID returns the CID of the current repository head 163 + func (w *Wrapper) GetHeadCID() (cid.Cid, error) { 164 + // TODO: Implement this properly 165 + // The repo package doesn't expose a direct way to get the head CID 166 + return cid.Undef, fmt.Errorf("not implemented") 167 + } 168 + 169 + // Export exports the repository as a CAR file 170 + func (w *Wrapper) Export() ([]byte, error) { 171 + // TODO: Implement proper CAR export using Indigo's carstore functionality 172 + // For now, return a placeholder 173 + return nil, fmt.Errorf("CAR export not yet implemented") 174 + } 175 + 176 + // GetMST returns the underlying Merkle Search Tree 177 + func (w *Wrapper) GetMST() (*mst.MerkleSearchTree, error) { 178 + // TODO: Implement MST access 179 + return nil, fmt.Errorf("not implemented") 180 + } 181 + 182 + // RecordInfo contains information about a record 183 + type RecordInfo struct { 184 + Collection string 185 + RecordKey string 186 + CID cid.Cid 187 + } 188 + 189 + // DecodeRecord decodes CBOR data into a record structure 190 + func DecodeRecord(data []byte, v interface{}) error { 191 + return cbornode.DecodeInto(data, v) 192 + } 193 + 194 + // EncodeRecord encodes a record structure into CBOR data 195 + func EncodeRecord(v cbg.CBORMarshaler) ([]byte, error) { 196 + buf := new(bytes.Buffer) 197 + if err := v.MarshalCBOR(buf); err != nil { 198 + return nil, err 199 + } 200 + return buf.Bytes(), nil 201 + }
+123
internal/core/repository/repository.go
··· 1 + package repository 2 + 3 + import ( 4 + "time" 5 + 6 + "github.com/ipfs/go-cid" 7 + ) 8 + 9 + // Repository represents an AT Protocol data repository 10 + type Repository struct { 11 + DID string // Decentralized identifier of the repository owner 12 + HeadCID cid.Cid // CID of the latest commit 13 + Revision string // Current revision identifier 14 + RecordCount int // Number of records in the repository 15 + StorageSize int64 // Total storage size in bytes 16 + CreatedAt time.Time 17 + UpdatedAt time.Time 18 + } 19 + 20 + // Commit represents a signed repository commit 21 + type Commit struct { 22 + CID cid.Cid // Content identifier of this commit 23 + DID string // DID of the committer 24 + Version int // Repository version 25 + PrevCID *cid.Cid // CID of the previous commit (nil for first commit) 26 + DataCID cid.Cid // CID of the MST root 27 + Revision string // Revision identifier 28 + Signature []byte // Cryptographic signature 29 + SigningKeyID string // Key ID used for signing 30 + CreatedAt time.Time 31 + } 32 + 33 + // Record represents a record in the repository 34 + type Record struct { 35 + URI string // AT-URI of the record (e.g., at://did:plc:123/app.bsky.feed.post/abc) 36 + CID cid.Cid // Content identifier 37 + Collection string // Collection name (e.g., app.bsky.feed.post) 38 + RecordKey string // Record key within collection 39 + Value []byte // The actual record data (typically CBOR) 40 + CreatedAt time.Time 41 + UpdatedAt time.Time 42 + } 43 + 44 + 45 + // CreateRecordInput represents input for creating a record 46 + type CreateRecordInput struct { 47 + DID string 48 + Collection string 49 + RecordKey string // Optional - will be generated if not provided 50 + Record interface{} 51 + Validate bool // Whether to validate against lexicon 52 + } 53 + 54 + // UpdateRecordInput represents input for updating a record 55 + type UpdateRecordInput struct { 56 + DID string 57 + Collection string 58 + RecordKey string 59 + Record interface{} 60 + Validate bool 61 + } 62 + 63 + // GetRecordInput represents input for retrieving a record 64 + type GetRecordInput struct { 65 + DID string 66 + Collection string 67 + RecordKey string 68 + } 69 + 70 + // DeleteRecordInput represents input for deleting a record 71 + type DeleteRecordInput struct { 72 + DID string 73 + Collection string 74 + RecordKey string 75 + } 76 + 77 + // RepositoryService defines the business logic for repository operations 78 + type RepositoryService interface { 79 + // Repository operations 80 + CreateRepository(did string) (*Repository, error) 81 + GetRepository(did string) (*Repository, error) 82 + DeleteRepository(did string) error 83 + 84 + // Record operations 85 + CreateRecord(input CreateRecordInput) (*Record, error) 86 + GetRecord(input GetRecordInput) (*Record, error) 87 + UpdateRecord(input UpdateRecordInput) (*Record, error) 88 + DeleteRecord(input DeleteRecordInput) error 89 + 90 + // Collection operations 91 + ListRecords(did string, collection string, limit int, cursor string) ([]*Record, string, error) 92 + 93 + // Commit operations 94 + GetCommit(did string, cid cid.Cid) (*Commit, error) 95 + ListCommits(did string, limit int, cursor string) ([]*Commit, string, error) 96 + 97 + // Export operations 98 + ExportRepository(did string) ([]byte, error) // Returns CAR file 99 + ImportRepository(did string, carData []byte) error 100 + } 101 + 102 + // RepositoryRepository defines the data access interface for repositories 103 + type RepositoryRepository interface { 104 + // Repository operations 105 + Create(repo *Repository) error 106 + GetByDID(did string) (*Repository, error) 107 + Update(repo *Repository) error 108 + Delete(did string) error 109 + 110 + // Commit operations 111 + CreateCommit(commit *Commit) error 112 + GetCommit(did string, cid cid.Cid) (*Commit, error) 113 + GetLatestCommit(did string) (*Commit, error) 114 + ListCommits(did string, limit int, offset int) ([]*Commit, error) 115 + 116 + // Record operations 117 + CreateRecord(record *Record) error 118 + GetRecord(did string, collection string, recordKey string) (*Record, error) 119 + UpdateRecord(record *Record) error 120 + DeleteRecord(did string, collection string, recordKey string) error 121 + ListRecords(did string, collection string, limit int, offset int) ([]*Record, error) 122 + 123 + }
+250
internal/core/repository/service.go
··· 1 + package repository 2 + 3 + import ( 4 + "bytes" 5 + "context" 6 + "fmt" 7 + "strings" 8 + "time" 9 + 10 + "Coves/internal/atproto/carstore" 11 + "github.com/ipfs/go-cid" 12 + "github.com/multiformats/go-multihash" 13 + ) 14 + 15 + // Service implements the RepositoryService interface using Indigo's carstore 16 + type Service struct { 17 + repo RepositoryRepository 18 + repoStore *carstore.RepoStore 19 + signingKeys map[string]interface{} // DID -> signing key 20 + } 21 + 22 + // NewService creates a new repository service using carstore 23 + func NewService(repo RepositoryRepository, repoStore *carstore.RepoStore) *Service { 24 + return &Service{ 25 + repo: repo, 26 + repoStore: repoStore, 27 + signingKeys: make(map[string]interface{}), 28 + } 29 + } 30 + 31 + // SetSigningKey sets the signing key for a DID 32 + func (s *Service) SetSigningKey(did string, signingKey interface{}) { 33 + s.signingKeys[did] = signingKey 34 + } 35 + 36 + // CreateRepository creates a new repository 37 + func (s *Service) CreateRepository(did string) (*Repository, error) { 38 + // Check if repository already exists 39 + existing, err := s.repo.GetByDID(did) 40 + if err != nil { 41 + return nil, fmt.Errorf("failed to check existing repository: %w", err) 42 + } 43 + if existing != nil { 44 + return nil, fmt.Errorf("repository already exists for DID: %s", did) 45 + } 46 + 47 + // For now, just create the user mapping without importing CAR data 48 + // The actual repository data will be created when records are added 49 + ctx := context.Background() 50 + 51 + // Ensure user mapping exists 52 + _, err = s.repoStore.GetOrCreateUID(ctx, did) 53 + if err != nil { 54 + return nil, fmt.Errorf("failed to create user mapping: %w", err) 55 + } 56 + 57 + 58 + // Create a placeholder CID for the empty repository 59 + emptyData := []byte("empty") 60 + mh, _ := multihash.Sum(emptyData, multihash.SHA2_256, -1) 61 + placeholderCID := cid.NewCidV1(cid.Raw, mh) 62 + 63 + // Create repository record 64 + repository := &Repository{ 65 + DID: did, 66 + HeadCID: placeholderCID, 67 + Revision: "rev-0", 68 + RecordCount: 0, 69 + StorageSize: 0, 70 + CreatedAt: time.Now(), 71 + UpdatedAt: time.Now(), 72 + } 73 + 74 + // Save to database 75 + if err := s.repo.Create(repository); err != nil { 76 + return nil, fmt.Errorf("failed to save repository: %w", err) 77 + } 78 + 79 + return repository, nil 80 + } 81 + 82 + // GetRepository retrieves a repository by DID 83 + func (s *Service) GetRepository(did string) (*Repository, error) { 84 + repo, err := s.repo.GetByDID(did) 85 + if err != nil { 86 + return nil, fmt.Errorf("failed to get repository: %w", err) 87 + } 88 + if repo == nil { 89 + return nil, fmt.Errorf("repository not found for DID: %s", did) 90 + } 91 + 92 + // Update head CID from carstore 93 + headCID, err := s.repoStore.GetRepoHead(context.Background(), did) 94 + if err == nil && headCID.Defined() { 95 + repo.HeadCID = headCID 96 + } 97 + 98 + return repo, nil 99 + } 100 + 101 + // DeleteRepository deletes a repository 102 + func (s *Service) DeleteRepository(did string) error { 103 + // Delete from carstore 104 + if err := s.repoStore.DeleteRepo(context.Background(), did); err != nil { 105 + return fmt.Errorf("failed to delete repo from carstore: %w", err) 106 + } 107 + 108 + // Delete from database 109 + if err := s.repo.Delete(did); err != nil { 110 + return fmt.Errorf("failed to delete repository: %w", err) 111 + } 112 + 113 + return nil 114 + } 115 + 116 + // ExportRepository exports a repository as a CAR file 117 + func (s *Service) ExportRepository(did string) ([]byte, error) { 118 + // First check if repository exists in our database 119 + repo, err := s.repo.GetByDID(did) 120 + if err != nil { 121 + return nil, fmt.Errorf("failed to get repository: %w", err) 122 + } 123 + if repo == nil { 124 + return nil, fmt.Errorf("repository not found for DID: %s", did) 125 + } 126 + 127 + // Try to read from carstore 128 + carData, err := s.repoStore.ReadRepo(context.Background(), did, "") 129 + if err != nil { 130 + // If no data in carstore yet, return empty CAR 131 + // This happens when a repo is created but no records added yet 132 + // Check for the specific error pattern from Indigo's carstore 133 + errMsg := err.Error() 134 + if strings.Contains(errMsg, "no data found for user") || 135 + strings.Contains(errMsg, "user not found") { 136 + return []byte{}, nil 137 + } 138 + return nil, fmt.Errorf("failed to export repository: %w", err) 139 + } 140 + 141 + return carData, nil 142 + } 143 + 144 + // ImportRepository imports a repository from a CAR file 145 + func (s *Service) ImportRepository(did string, carData []byte) error { 146 + ctx := context.Background() 147 + 148 + // If empty CAR data, just create user mapping 149 + if len(carData) == 0 { 150 + _, err := s.repoStore.GetOrCreateUID(ctx, did) 151 + if err != nil { 152 + return fmt.Errorf("failed to create user mapping: %w", err) 153 + } 154 + 155 + // Create placeholder CID 156 + emptyData := []byte("empty") 157 + mh, _ := multihash.Sum(emptyData, multihash.SHA2_256, -1) 158 + headCID := cid.NewCidV1(cid.Raw, mh) 159 + 160 + // Create repository record 161 + repo := &Repository{ 162 + DID: did, 163 + HeadCID: headCID, 164 + Revision: "imported-empty", 165 + RecordCount: 0, 166 + StorageSize: 0, 167 + CreatedAt: time.Now(), 168 + UpdatedAt: time.Now(), 169 + } 170 + if err := s.repo.Create(repo); err != nil { 171 + return fmt.Errorf("failed to create repository: %w", err) 172 + } 173 + return nil 174 + } 175 + 176 + // Import non-empty CAR into carstore 177 + headCID, err := s.repoStore.ImportRepo(ctx, did, bytes.NewReader(carData)) 178 + if err != nil { 179 + return fmt.Errorf("failed to import repository: %w", err) 180 + } 181 + 182 + // Create or update repository record 183 + repo, err := s.repo.GetByDID(did) 184 + if err != nil { 185 + return fmt.Errorf("failed to get repository: %w", err) 186 + } 187 + 188 + if repo == nil { 189 + // Create new repository 190 + repo = &Repository{ 191 + DID: did, 192 + HeadCID: headCID, 193 + Revision: "imported", 194 + RecordCount: 0, // TODO: Count records in CAR 195 + StorageSize: int64(len(carData)), 196 + CreatedAt: time.Now(), 197 + UpdatedAt: time.Now(), 198 + } 199 + if err := s.repo.Create(repo); err != nil { 200 + return fmt.Errorf("failed to create repository: %w", err) 201 + } 202 + } else { 203 + // Update existing repository 204 + repo.HeadCID = headCID 205 + repo.UpdatedAt = time.Now() 206 + if err := s.repo.Update(repo); err != nil { 207 + return fmt.Errorf("failed to update repository: %w", err) 208 + } 209 + } 210 + 211 + return nil 212 + } 213 + 214 + // CompactRepository runs garbage collection on a repository 215 + func (s *Service) CompactRepository(did string) error { 216 + return s.repoStore.CompactRepo(context.Background(), did) 217 + } 218 + 219 + // Note: Record-level operations would require more complex implementation 220 + // to work with the carstore. For now, these are placeholder implementations 221 + // that would need to be expanded to properly handle record CRUD operations 222 + // by reading the CAR, modifying the repo structure, and writing back. 223 + 224 + func (s *Service) CreateRecord(input CreateRecordInput) (*Record, error) { 225 + return nil, fmt.Errorf("record operations not yet implemented for carstore") 226 + } 227 + 228 + func (s *Service) GetRecord(input GetRecordInput) (*Record, error) { 229 + return nil, fmt.Errorf("record operations not yet implemented for carstore") 230 + } 231 + 232 + func (s *Service) UpdateRecord(input UpdateRecordInput) (*Record, error) { 233 + return nil, fmt.Errorf("record operations not yet implemented for carstore") 234 + } 235 + 236 + func (s *Service) DeleteRecord(input DeleteRecordInput) error { 237 + return fmt.Errorf("record operations not yet implemented for carstore") 238 + } 239 + 240 + func (s *Service) ListRecords(did string, collection string, limit int, cursor string) ([]*Record, string, error) { 241 + return nil, "", fmt.Errorf("record operations not yet implemented for carstore") 242 + } 243 + 244 + func (s *Service) GetCommit(did string, commitCID cid.Cid) (*Commit, error) { 245 + return nil, fmt.Errorf("commit operations not yet implemented for carstore") 246 + } 247 + 248 + func (s *Service) ListCommits(did string, limit int, cursor string) ([]*Commit, string, error) { 249 + return nil, "", fmt.Errorf("commit operations not yet implemented for carstore") 250 + }
+505
internal/core/repository/service_test.go
··· 1 + package repository_test 2 + 3 + import ( 4 + "context" 5 + "database/sql" 6 + "fmt" 7 + "os" 8 + "testing" 9 + 10 + "Coves/internal/atproto/carstore" 11 + "Coves/internal/core/repository" 12 + "Coves/internal/db/postgres" 13 + 14 + "github.com/ipfs/go-cid" 15 + _ "github.com/lib/pq" 16 + "github.com/pressly/goose/v3" 17 + postgresDriver "gorm.io/driver/postgres" 18 + "gorm.io/gorm" 19 + ) 20 + 21 + // Mock signing key for testing 22 + type mockSigningKey struct{} 23 + 24 + // Test database connection 25 + func setupTestDB(t *testing.T) (*sql.DB, *gorm.DB, func()) { 26 + // Use test database URL from environment or default 27 + dbURL := os.Getenv("TEST_DATABASE_URL") 28 + if dbURL == "" { 29 + // Skip test if no database configured 30 + t.Skip("TEST_DATABASE_URL not set, skipping database tests") 31 + } 32 + 33 + // Connect with sql.DB for migrations 34 + sqlDB, err := sql.Open("postgres", dbURL) 35 + if err != nil { 36 + t.Fatalf("Failed to connect to test database: %v", err) 37 + } 38 + 39 + // Run migrations 40 + if err := goose.Up(sqlDB, "../../db/migrations"); err != nil { 41 + t.Fatalf("Failed to run migrations: %v", err) 42 + } 43 + 44 + // Connect with GORM using a fresh connection 45 + gormDB, err := gorm.Open(postgresDriver.Open(dbURL), &gorm.Config{ 46 + DisableForeignKeyConstraintWhenMigrating: true, 47 + PrepareStmt: false, 48 + }) 49 + if err != nil { 50 + t.Fatalf("Failed to create GORM connection: %v", err) 51 + } 52 + 53 + // Cleanup function 54 + cleanup := func() { 55 + // Clean up test data 56 + gormDB.Exec("DELETE FROM repositories") 57 + gormDB.Exec("DELETE FROM commits") 58 + gormDB.Exec("DELETE FROM records") 59 + gormDB.Exec("DELETE FROM user_maps") 60 + gormDB.Exec("DELETE FROM car_shards") 61 + sqlDB.Close() 62 + } 63 + 64 + return sqlDB, gormDB, cleanup 65 + } 66 + 67 + func TestRepositoryService_CreateRepository(t *testing.T) { 68 + sqlDB, gormDB, cleanup := setupTestDB(t) 69 + defer cleanup() 70 + 71 + // Create temporary directory for carstore 72 + tempDir, err := os.MkdirTemp("", "carstore_test") 73 + if err != nil { 74 + t.Fatalf("Failed to create temp dir: %v", err) 75 + } 76 + defer os.RemoveAll(tempDir) 77 + 78 + // Initialize carstore 79 + carDirs := []string{tempDir} 80 + repoStore, err := carstore.NewRepoStore(gormDB, carDirs) 81 + if err != nil { 82 + t.Fatalf("Failed to create repo store: %v", err) 83 + } 84 + 85 + // Initialize repository service 86 + repoRepo := postgres.NewRepositoryRepo(sqlDB) 87 + service := repository.NewService(repoRepo, repoStore) 88 + 89 + // Test DID 90 + testDID := "did:plc:testuser123" 91 + 92 + // Set signing key 93 + service.SetSigningKey(testDID, &mockSigningKey{}) 94 + 95 + // Create repository 96 + repo, err := service.CreateRepository(testDID) 97 + if err != nil { 98 + t.Fatalf("Failed to create repository: %v", err) 99 + } 100 + 101 + // Verify repository was created 102 + if repo.DID != testDID { 103 + t.Errorf("Expected DID %s, got %s", testDID, repo.DID) 104 + } 105 + if !repo.HeadCID.Defined() { 106 + t.Error("Expected HeadCID to be defined") 107 + } 108 + if repo.RecordCount != 0 { 109 + t.Errorf("Expected RecordCount 0, got %d", repo.RecordCount) 110 + } 111 + 112 + // Verify repository exists in database 113 + fetchedRepo, err := service.GetRepository(testDID) 114 + if err != nil { 115 + t.Fatalf("Failed to get repository: %v", err) 116 + } 117 + if fetchedRepo.DID != testDID { 118 + t.Errorf("Expected fetched DID %s, got %s", testDID, fetchedRepo.DID) 119 + } 120 + 121 + // Test duplicate creation should fail 122 + _, err = service.CreateRepository(testDID) 123 + if err == nil { 124 + t.Error("Expected error creating duplicate repository") 125 + } 126 + } 127 + 128 + func TestRepositoryService_ImportExport(t *testing.T) { 129 + sqlDB, gormDB, cleanup := setupTestDB(t) 130 + defer cleanup() 131 + 132 + // Create temporary directory for carstore 133 + tempDir, err := os.MkdirTemp("", "carstore_test") 134 + if err != nil { 135 + t.Fatalf("Failed to create temp dir: %v", err) 136 + } 137 + defer os.RemoveAll(tempDir) 138 + 139 + // Log the temp directory for debugging 140 + t.Logf("Using carstore directory: %s", tempDir) 141 + 142 + // Initialize carstore 143 + carDirs := []string{tempDir} 144 + repoStore, err := carstore.NewRepoStore(gormDB, carDirs) 145 + if err != nil { 146 + t.Fatalf("Failed to create repo store: %v", err) 147 + } 148 + 149 + // Initialize repository service 150 + repoRepo := postgres.NewRepositoryRepo(sqlDB) 151 + service := repository.NewService(repoRepo, repoStore) 152 + 153 + // Create first repository 154 + did1 := "did:plc:user1" 155 + service.SetSigningKey(did1, &mockSigningKey{}) 156 + repo1, err := service.CreateRepository(did1) 157 + if err != nil { 158 + t.Fatalf("Failed to create repository 1: %v", err) 159 + } 160 + t.Logf("Created repository with HeadCID: %s", repo1.HeadCID) 161 + 162 + // Check what's in the database 163 + var userMapCount int 164 + gormDB.Raw("SELECT COUNT(*) FROM user_maps").Scan(&userMapCount) 165 + t.Logf("User maps count: %d", userMapCount) 166 + 167 + var carShardCount int 168 + gormDB.Raw("SELECT COUNT(*) FROM car_shards").Scan(&carShardCount) 169 + t.Logf("Car shards count: %d", carShardCount) 170 + 171 + // Check block_refs too 172 + var blockRefCount int 173 + gormDB.Raw("SELECT COUNT(*) FROM block_refs").Scan(&blockRefCount) 174 + t.Logf("Block refs count: %d", blockRefCount) 175 + 176 + // Export repository 177 + carData, err := service.ExportRepository(did1) 178 + if err != nil { 179 + t.Fatalf("Failed to export repository: %v", err) 180 + } 181 + // For now, empty repositories return empty CAR data 182 + t.Logf("Exported CAR data size: %d bytes", len(carData)) 183 + 184 + // Import to new DID 185 + did2 := "did:plc:user2" 186 + err = service.ImportRepository(did2, carData) 187 + if err != nil { 188 + t.Fatalf("Failed to import repository: %v", err) 189 + } 190 + 191 + // Verify imported repository 192 + repo2, err := service.GetRepository(did2) 193 + if err != nil { 194 + t.Fatalf("Failed to get imported repository: %v", err) 195 + } 196 + if repo2.DID != did2 { 197 + t.Errorf("Expected DID %s, got %s", did2, repo2.DID) 198 + } 199 + // Note: HeadCID might differ due to new import 200 + } 201 + 202 + func TestRepositoryService_DeleteRepository(t *testing.T) { 203 + sqlDB, gormDB, cleanup := setupTestDB(t) 204 + defer cleanup() 205 + 206 + // Create temporary directory for carstore 207 + tempDir, err := os.MkdirTemp("", "carstore_test") 208 + if err != nil { 209 + t.Fatalf("Failed to create temp dir: %v", err) 210 + } 211 + defer os.RemoveAll(tempDir) 212 + 213 + // Initialize carstore 214 + carDirs := []string{tempDir} 215 + repoStore, err := carstore.NewRepoStore(gormDB, carDirs) 216 + if err != nil { 217 + t.Fatalf("Failed to create repo store: %v", err) 218 + } 219 + 220 + // Initialize repository service 221 + repoRepo := postgres.NewRepositoryRepo(sqlDB) 222 + service := repository.NewService(repoRepo, repoStore) 223 + 224 + // Create repository 225 + testDID := "did:plc:deletetest" 226 + service.SetSigningKey(testDID, &mockSigningKey{}) 227 + _, err = service.CreateRepository(testDID) 228 + if err != nil { 229 + t.Fatalf("Failed to create repository: %v", err) 230 + } 231 + 232 + // Delete repository 233 + err = service.DeleteRepository(testDID) 234 + if err != nil { 235 + t.Fatalf("Failed to delete repository: %v", err) 236 + } 237 + 238 + // Verify repository is deleted 239 + _, err = service.GetRepository(testDID) 240 + if err == nil { 241 + t.Error("Expected error getting deleted repository") 242 + } 243 + } 244 + 245 + func TestRepositoryService_CompactRepository(t *testing.T) { 246 + sqlDB, gormDB, cleanup := setupTestDB(t) 247 + defer cleanup() 248 + 249 + // Create temporary directory for carstore 250 + tempDir, err := os.MkdirTemp("", "carstore_test") 251 + if err != nil { 252 + t.Fatalf("Failed to create temp dir: %v", err) 253 + } 254 + defer os.RemoveAll(tempDir) 255 + 256 + // Initialize carstore 257 + carDirs := []string{tempDir} 258 + repoStore, err := carstore.NewRepoStore(gormDB, carDirs) 259 + if err != nil { 260 + t.Fatalf("Failed to create repo store: %v", err) 261 + } 262 + 263 + // Initialize repository service 264 + repoRepo := postgres.NewRepositoryRepo(sqlDB) 265 + service := repository.NewService(repoRepo, repoStore) 266 + 267 + // Create repository 268 + testDID := "did:plc:compacttest" 269 + service.SetSigningKey(testDID, &mockSigningKey{}) 270 + _, err = service.CreateRepository(testDID) 271 + if err != nil { 272 + t.Fatalf("Failed to create repository: %v", err) 273 + } 274 + 275 + // Run compaction (should not error even with minimal data) 276 + err = service.CompactRepository(testDID) 277 + if err != nil { 278 + t.Errorf("Failed to compact repository: %v", err) 279 + } 280 + } 281 + 282 + // Test UserMapping functionality 283 + func TestUserMapping(t *testing.T) { 284 + _, gormDB, cleanup := setupTestDB(t) 285 + defer cleanup() 286 + 287 + // Create user mapping 288 + mapping, err := carstore.NewUserMapping(gormDB) 289 + if err != nil { 290 + t.Fatalf("Failed to create user mapping: %v", err) 291 + } 292 + 293 + // Test creating new mapping 294 + did1 := "did:plc:mapping1" 295 + uid1, err := mapping.GetOrCreateUID(context.Background(), did1) 296 + if err != nil { 297 + t.Fatalf("Failed to create UID for %s: %v", did1, err) 298 + } 299 + if uid1 == 0 { 300 + t.Error("Expected non-zero UID") 301 + } 302 + 303 + // Test getting existing mapping 304 + uid1Again, err := mapping.GetOrCreateUID(context.Background(), did1) 305 + if err != nil { 306 + t.Fatalf("Failed to get UID for %s: %v", did1, err) 307 + } 308 + if uid1 != uid1Again { 309 + t.Errorf("Expected same UID, got %d and %d", uid1, uid1Again) 310 + } 311 + 312 + // Test reverse lookup 313 + didLookup, err := mapping.GetDID(uid1) 314 + if err != nil { 315 + t.Fatalf("Failed to get DID for UID %d: %v", uid1, err) 316 + } 317 + if didLookup != did1 { 318 + t.Errorf("Expected DID %s, got %s", did1, didLookup) 319 + } 320 + 321 + // Test second user gets different UID 322 + did2 := "did:plc:mapping2" 323 + uid2, err := mapping.GetOrCreateUID(context.Background(), did2) 324 + if err != nil { 325 + t.Fatalf("Failed to create UID for %s: %v", did2, err) 326 + } 327 + if uid2 == uid1 { 328 + t.Error("Expected different UIDs for different DIDs") 329 + } 330 + } 331 + 332 + // Test with mock repository and carstore 333 + func TestRepositoryService_MockedComponents(t *testing.T) { 334 + // Use the existing mock repository from the old test file 335 + _ = NewMockRepositoryRepository() 336 + 337 + // For unit testing without real carstore, we would need to mock RepoStore 338 + // For now, this demonstrates the structure 339 + t.Skip("Mocked carstore tests would require creating mock RepoStore interface") 340 + } 341 + 342 + // Benchmark repository creation 343 + func BenchmarkRepositoryCreation(b *testing.B) { 344 + sqlDB, gormDB, cleanup := setupTestDB(&testing.T{}) 345 + defer cleanup() 346 + 347 + tempDir, _ := os.MkdirTemp("", "carstore_bench") 348 + defer os.RemoveAll(tempDir) 349 + 350 + carDirs := []string{tempDir} 351 + repoStore, _ := carstore.NewRepoStore(gormDB, carDirs) 352 + repoRepo := postgres.NewRepositoryRepo(sqlDB) 353 + service := repository.NewService(repoRepo, repoStore) 354 + 355 + b.ResetTimer() 356 + for i := 0; i < b.N; i++ { 357 + did := fmt.Sprintf("did:plc:bench%d", i) 358 + service.SetSigningKey(did, &mockSigningKey{}) 359 + _, _ = service.CreateRepository(did) 360 + } 361 + } 362 + 363 + // MockRepositoryRepository is a mock implementation of repository.RepositoryRepository 364 + type MockRepositoryRepository struct { 365 + repositories map[string]*repository.Repository 366 + commits map[string][]*repository.Commit 367 + records map[string]*repository.Record 368 + } 369 + 370 + func NewMockRepositoryRepository() *MockRepositoryRepository { 371 + return &MockRepositoryRepository{ 372 + repositories: make(map[string]*repository.Repository), 373 + commits: make(map[string][]*repository.Commit), 374 + records: make(map[string]*repository.Record), 375 + } 376 + } 377 + 378 + // Repository operations 379 + func (m *MockRepositoryRepository) Create(repo *repository.Repository) error { 380 + m.repositories[repo.DID] = repo 381 + return nil 382 + } 383 + 384 + func (m *MockRepositoryRepository) GetByDID(did string) (*repository.Repository, error) { 385 + repo, exists := m.repositories[did] 386 + if !exists { 387 + return nil, nil 388 + } 389 + return repo, nil 390 + } 391 + 392 + func (m *MockRepositoryRepository) Update(repo *repository.Repository) error { 393 + if _, exists := m.repositories[repo.DID]; !exists { 394 + return nil 395 + } 396 + m.repositories[repo.DID] = repo 397 + return nil 398 + } 399 + 400 + func (m *MockRepositoryRepository) Delete(did string) error { 401 + delete(m.repositories, did) 402 + return nil 403 + } 404 + 405 + // Commit operations 406 + func (m *MockRepositoryRepository) CreateCommit(commit *repository.Commit) error { 407 + m.commits[commit.DID] = append(m.commits[commit.DID], commit) 408 + return nil 409 + } 410 + 411 + func (m *MockRepositoryRepository) GetCommit(did string, commitCID cid.Cid) (*repository.Commit, error) { 412 + commits, exists := m.commits[did] 413 + if !exists { 414 + return nil, nil 415 + } 416 + 417 + for _, c := range commits { 418 + if c.CID.Equals(commitCID) { 419 + return c, nil 420 + } 421 + } 422 + return nil, nil 423 + } 424 + 425 + func (m *MockRepositoryRepository) GetLatestCommit(did string) (*repository.Commit, error) { 426 + commits, exists := m.commits[did] 427 + if !exists || len(commits) == 0 { 428 + return nil, nil 429 + } 430 + return commits[len(commits)-1], nil 431 + } 432 + 433 + func (m *MockRepositoryRepository) ListCommits(did string, limit int, offset int) ([]*repository.Commit, error) { 434 + commits, exists := m.commits[did] 435 + if !exists { 436 + return []*repository.Commit{}, nil 437 + } 438 + 439 + start := offset 440 + if start >= len(commits) { 441 + return []*repository.Commit{}, nil 442 + } 443 + 444 + end := start + limit 445 + if end > len(commits) { 446 + end = len(commits) 447 + } 448 + 449 + return commits[start:end], nil 450 + } 451 + 452 + // Record operations 453 + func (m *MockRepositoryRepository) CreateRecord(record *repository.Record) error { 454 + key := record.URI 455 + m.records[key] = record 456 + return nil 457 + } 458 + 459 + func (m *MockRepositoryRepository) GetRecord(did string, collection string, recordKey string) (*repository.Record, error) { 460 + uri := "at://" + did + "/" + collection + "/" + recordKey 461 + record, exists := m.records[uri] 462 + if !exists { 463 + return nil, nil 464 + } 465 + return record, nil 466 + } 467 + 468 + func (m *MockRepositoryRepository) UpdateRecord(record *repository.Record) error { 469 + key := record.URI 470 + if _, exists := m.records[key]; !exists { 471 + return nil 472 + } 473 + m.records[key] = record 474 + return nil 475 + } 476 + 477 + func (m *MockRepositoryRepository) DeleteRecord(did string, collection string, recordKey string) error { 478 + uri := "at://" + did + "/" + collection + "/" + recordKey 479 + delete(m.records, uri) 480 + return nil 481 + } 482 + 483 + func (m *MockRepositoryRepository) ListRecords(did string, collection string, limit int, offset int) ([]*repository.Record, error) { 484 + var records []*repository.Record 485 + prefix := "at://" + did + "/" + collection + "/" 486 + 487 + for uri, record := range m.records { 488 + if len(uri) > len(prefix) && uri[:len(prefix)] == prefix { 489 + records = append(records, record) 490 + } 491 + } 492 + 493 + // Simple pagination 494 + start := offset 495 + if start >= len(records) { 496 + return []*repository.Record{}, nil 497 + } 498 + 499 + end := start + limit 500 + if end > len(records) { 501 + end = len(records) 502 + } 503 + 504 + return records[start:end], nil 505 + }
+88
internal/db/migrations/002_create_repository_tables.sql
··· 1 + -- +goose Up 2 + -- +goose StatementBegin 3 + 4 + -- Repositories table stores metadata about each user's repository 5 + CREATE TABLE repositories ( 6 + did VARCHAR(256) PRIMARY KEY, 7 + head_cid VARCHAR(256) NOT NULL, 8 + revision VARCHAR(64) NOT NULL, 9 + record_count INTEGER NOT NULL DEFAULT 0, 10 + storage_size BIGINT NOT NULL DEFAULT 0, 11 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), 12 + updated_at TIMESTAMP NOT NULL DEFAULT NOW() 13 + ); 14 + 15 + CREATE INDEX idx_repositories_updated_at ON repositories(updated_at); 16 + 17 + -- Commits table stores the commit history 18 + CREATE TABLE commits ( 19 + cid VARCHAR(256) PRIMARY KEY, 20 + did VARCHAR(256) NOT NULL, 21 + version INTEGER NOT NULL, 22 + prev_cid VARCHAR(256), 23 + data_cid VARCHAR(256) NOT NULL, 24 + revision VARCHAR(64) NOT NULL, 25 + signature BYTEA NOT NULL, 26 + signing_key_id VARCHAR(256) NOT NULL, 27 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), 28 + FOREIGN KEY (did) REFERENCES repositories(did) ON DELETE CASCADE 29 + ); 30 + 31 + CREATE INDEX idx_commits_did ON commits(did); 32 + CREATE INDEX idx_commits_created_at ON commits(created_at); 33 + 34 + -- Records table stores record metadata (actual data is in MST) 35 + CREATE TABLE records ( 36 + id SERIAL PRIMARY KEY, 37 + did VARCHAR(256) NOT NULL, 38 + uri VARCHAR(512) NOT NULL, 39 + cid VARCHAR(256) NOT NULL, 40 + collection VARCHAR(256) NOT NULL, 41 + record_key VARCHAR(256) NOT NULL, 42 + value BYTEA NOT NULL, -- CBOR-encoded record data 43 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), 44 + updated_at TIMESTAMP NOT NULL DEFAULT NOW(), 45 + UNIQUE(did, collection, record_key), 46 + FOREIGN KEY (did) REFERENCES repositories(did) ON DELETE CASCADE 47 + ); 48 + 49 + CREATE INDEX idx_records_did_collection ON records(did, collection); 50 + CREATE INDEX idx_records_uri ON records(uri); 51 + CREATE INDEX idx_records_updated_at ON records(updated_at); 52 + 53 + -- Blobs table stores binary large objects 54 + CREATE TABLE blobs ( 55 + cid VARCHAR(256) PRIMARY KEY, 56 + mime_type VARCHAR(256) NOT NULL, 57 + size BIGINT NOT NULL, 58 + ref_count INTEGER NOT NULL DEFAULT 0, 59 + data BYTEA NOT NULL, 60 + created_at TIMESTAMP NOT NULL DEFAULT NOW() 61 + ); 62 + 63 + CREATE INDEX idx_blobs_ref_count ON blobs(ref_count); 64 + CREATE INDEX idx_blobs_created_at ON blobs(created_at); 65 + 66 + -- Blob references table tracks which records reference which blobs 67 + CREATE TABLE blob_refs ( 68 + id SERIAL PRIMARY KEY, 69 + record_id INTEGER NOT NULL, 70 + blob_cid VARCHAR(256) NOT NULL, 71 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), 72 + FOREIGN KEY (record_id) REFERENCES records(id) ON DELETE CASCADE, 73 + FOREIGN KEY (blob_cid) REFERENCES blobs(cid) ON DELETE RESTRICT, 74 + UNIQUE(record_id, blob_cid) 75 + ); 76 + 77 + CREATE INDEX idx_blob_refs_blob_cid ON blob_refs(blob_cid); 78 + 79 + -- +goose StatementEnd 80 + 81 + -- +goose Down 82 + -- +goose StatementBegin 83 + DROP TABLE IF EXISTS blob_refs; 84 + DROP TABLE IF EXISTS blobs; 85 + DROP TABLE IF EXISTS records; 86 + DROP TABLE IF EXISTS commits; 87 + DROP TABLE IF EXISTS repositories; 88 + -- +goose StatementEnd
+60
internal/db/migrations/003_update_for_carstore.sql
··· 1 + -- +goose Up 2 + -- +goose StatementBegin 3 + 4 + -- Remove the value column from records table since blocks are now stored in filesystem 5 + ALTER TABLE records DROP COLUMN IF EXISTS value; 6 + 7 + -- Drop blob-related tables since FileCarStore handles block storage 8 + DROP TABLE IF EXISTS blob_refs; 9 + DROP TABLE IF EXISTS blobs; 10 + 11 + -- Create block_refs table for garbage collection tracking 12 + CREATE TABLE block_refs ( 13 + cid VARCHAR(256) NOT NULL, 14 + did VARCHAR(256) NOT NULL, 15 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), 16 + PRIMARY KEY (cid, did), 17 + FOREIGN KEY (did) REFERENCES repositories(did) ON DELETE CASCADE 18 + ); 19 + 20 + CREATE INDEX idx_block_refs_did ON block_refs(did); 21 + CREATE INDEX idx_block_refs_created_at ON block_refs(created_at); 22 + 23 + -- +goose StatementEnd 24 + 25 + -- +goose Down 26 + -- +goose StatementBegin 27 + 28 + -- Recreate the original schema for rollback 29 + DROP TABLE IF EXISTS block_refs; 30 + 31 + -- Add back the value column to records table 32 + ALTER TABLE records ADD COLUMN value BYTEA; 33 + 34 + -- Recreate blobs table 35 + CREATE TABLE blobs ( 36 + cid VARCHAR(256) PRIMARY KEY, 37 + mime_type VARCHAR(256) NOT NULL, 38 + size BIGINT NOT NULL, 39 + ref_count INTEGER NOT NULL DEFAULT 0, 40 + data BYTEA NOT NULL, 41 + created_at TIMESTAMP NOT NULL DEFAULT NOW() 42 + ); 43 + 44 + CREATE INDEX idx_blobs_ref_count ON blobs(ref_count); 45 + CREATE INDEX idx_blobs_created_at ON blobs(created_at); 46 + 47 + -- Recreate blob_refs table 48 + CREATE TABLE blob_refs ( 49 + id SERIAL PRIMARY KEY, 50 + record_id INTEGER NOT NULL, 51 + blob_cid VARCHAR(256) NOT NULL, 52 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), 53 + FOREIGN KEY (record_id) REFERENCES records(id) ON DELETE CASCADE, 54 + FOREIGN KEY (blob_cid) REFERENCES blobs(cid) ON DELETE RESTRICT, 55 + UNIQUE(record_id, blob_cid) 56 + ); 57 + 58 + CREATE INDEX idx_blob_refs_blob_cid ON blob_refs(blob_cid); 59 + 60 + -- +goose StatementEnd
+20
internal/db/migrations/004_remove_block_refs_for_indigo.sql
··· 1 + -- +goose Up 2 + -- +goose StatementBegin 3 + -- Drop our block_refs table since Indigo's carstore will create its own 4 + DROP TABLE IF EXISTS block_refs; 5 + -- +goose StatementEnd 6 + 7 + -- +goose Down 8 + -- +goose StatementBegin 9 + -- Recreate block_refs table 10 + CREATE TABLE block_refs ( 11 + cid VARCHAR(256) NOT NULL, 12 + did VARCHAR(256) NOT NULL, 13 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), 14 + PRIMARY KEY (cid, did), 15 + FOREIGN KEY (did) REFERENCES repositories(did) ON DELETE CASCADE 16 + ); 17 + 18 + CREATE INDEX idx_block_refs_did ON block_refs(did); 19 + CREATE INDEX idx_block_refs_created_at ON block_refs(created_at); 20 + -- +goose StatementEnd
+465
internal/db/postgres/repository_repo.go
··· 1 + package postgres 2 + 3 + import ( 4 + "database/sql" 5 + "fmt" 6 + "time" 7 + 8 + "Coves/internal/core/repository" 9 + "github.com/ipfs/go-cid" 10 + "github.com/lib/pq" 11 + ) 12 + 13 + // RepositoryRepo implements repository.RepositoryRepository using PostgreSQL 14 + type RepositoryRepo struct { 15 + db *sql.DB 16 + } 17 + 18 + // NewRepositoryRepo creates a new PostgreSQL repository implementation 19 + func NewRepositoryRepo(db *sql.DB) *RepositoryRepo { 20 + return &RepositoryRepo{db: db} 21 + } 22 + 23 + // Repository operations 24 + 25 + func (r *RepositoryRepo) Create(repo *repository.Repository) error { 26 + query := ` 27 + INSERT INTO repositories (did, head_cid, revision, record_count, storage_size, created_at, updated_at) 28 + VALUES ($1, $2, $3, $4, $5, $6, $7)` 29 + 30 + _, err := r.db.Exec(query, 31 + repo.DID, 32 + repo.HeadCID.String(), 33 + repo.Revision, 34 + repo.RecordCount, 35 + repo.StorageSize, 36 + repo.CreatedAt, 37 + repo.UpdatedAt, 38 + ) 39 + if err != nil { 40 + return fmt.Errorf("failed to create repository: %w", err) 41 + } 42 + 43 + return nil 44 + } 45 + 46 + func (r *RepositoryRepo) GetByDID(did string) (*repository.Repository, error) { 47 + query := ` 48 + SELECT did, head_cid, revision, record_count, storage_size, created_at, updated_at 49 + FROM repositories 50 + WHERE did = $1` 51 + 52 + var repo repository.Repository 53 + var headCIDStr string 54 + 55 + err := r.db.QueryRow(query, did).Scan( 56 + &repo.DID, 57 + &headCIDStr, 58 + &repo.Revision, 59 + &repo.RecordCount, 60 + &repo.StorageSize, 61 + &repo.CreatedAt, 62 + &repo.UpdatedAt, 63 + ) 64 + if err == sql.ErrNoRows { 65 + return nil, nil 66 + } 67 + if err != nil { 68 + return nil, fmt.Errorf("failed to get repository: %w", err) 69 + } 70 + 71 + repo.HeadCID, err = cid.Parse(headCIDStr) 72 + if err != nil { 73 + return nil, fmt.Errorf("failed to parse head CID: %w", err) 74 + } 75 + 76 + return &repo, nil 77 + } 78 + 79 + func (r *RepositoryRepo) Update(repo *repository.Repository) error { 80 + query := ` 81 + UPDATE repositories 82 + SET head_cid = $2, revision = $3, record_count = $4, storage_size = $5, updated_at = $6 83 + WHERE did = $1` 84 + 85 + result, err := r.db.Exec(query, 86 + repo.DID, 87 + repo.HeadCID.String(), 88 + repo.Revision, 89 + repo.RecordCount, 90 + repo.StorageSize, 91 + time.Now(), 92 + ) 93 + if err != nil { 94 + return fmt.Errorf("failed to update repository: %w", err) 95 + } 96 + 97 + rowsAffected, err := result.RowsAffected() 98 + if err != nil { 99 + return fmt.Errorf("failed to get rows affected: %w", err) 100 + } 101 + if rowsAffected == 0 { 102 + return fmt.Errorf("repository not found: %s", repo.DID) 103 + } 104 + 105 + return nil 106 + } 107 + 108 + func (r *RepositoryRepo) Delete(did string) error { 109 + query := `DELETE FROM repositories WHERE did = $1` 110 + 111 + result, err := r.db.Exec(query, did) 112 + if err != nil { 113 + return fmt.Errorf("failed to delete repository: %w", err) 114 + } 115 + 116 + rowsAffected, err := result.RowsAffected() 117 + if err != nil { 118 + return fmt.Errorf("failed to get rows affected: %w", err) 119 + } 120 + if rowsAffected == 0 { 121 + return fmt.Errorf("repository not found: %s", did) 122 + } 123 + 124 + return nil 125 + } 126 + 127 + // Commit operations 128 + 129 + func (r *RepositoryRepo) CreateCommit(commit *repository.Commit) error { 130 + query := ` 131 + INSERT INTO commits (cid, did, version, prev_cid, data_cid, revision, signature, signing_key_id, created_at) 132 + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)` 133 + 134 + var prevCID *string 135 + if commit.PrevCID != nil { 136 + s := commit.PrevCID.String() 137 + prevCID = &s 138 + } 139 + 140 + _, err := r.db.Exec(query, 141 + commit.CID.String(), 142 + commit.DID, 143 + commit.Version, 144 + prevCID, 145 + commit.DataCID.String(), 146 + commit.Revision, 147 + commit.Signature, 148 + commit.SigningKeyID, 149 + commit.CreatedAt, 150 + ) 151 + if err != nil { 152 + return fmt.Errorf("failed to create commit: %w", err) 153 + } 154 + 155 + return nil 156 + } 157 + 158 + func (r *RepositoryRepo) GetCommit(did string, commitCID cid.Cid) (*repository.Commit, error) { 159 + query := ` 160 + SELECT cid, did, version, prev_cid, data_cid, revision, signature, signing_key_id, created_at 161 + FROM commits 162 + WHERE did = $1 AND cid = $2` 163 + 164 + var commit repository.Commit 165 + var cidStr, dataCIDStr string 166 + var prevCIDStr sql.NullString 167 + 168 + err := r.db.QueryRow(query, did, commitCID.String()).Scan( 169 + &cidStr, 170 + &commit.DID, 171 + &commit.Version, 172 + &prevCIDStr, 173 + &dataCIDStr, 174 + &commit.Revision, 175 + &commit.Signature, 176 + &commit.SigningKeyID, 177 + &commit.CreatedAt, 178 + ) 179 + if err == sql.ErrNoRows { 180 + return nil, nil 181 + } 182 + if err != nil { 183 + return nil, fmt.Errorf("failed to get commit: %w", err) 184 + } 185 + 186 + commit.CID, err = cid.Parse(cidStr) 187 + if err != nil { 188 + return nil, fmt.Errorf("failed to parse commit CID: %w", err) 189 + } 190 + 191 + commit.DataCID, err = cid.Parse(dataCIDStr) 192 + if err != nil { 193 + return nil, fmt.Errorf("failed to parse data CID: %w", err) 194 + } 195 + 196 + if prevCIDStr.Valid { 197 + prevCID, err := cid.Parse(prevCIDStr.String) 198 + if err != nil { 199 + return nil, fmt.Errorf("failed to parse prev CID: %w", err) 200 + } 201 + commit.PrevCID = &prevCID 202 + } 203 + 204 + return &commit, nil 205 + } 206 + 207 + func (r *RepositoryRepo) GetLatestCommit(did string) (*repository.Commit, error) { 208 + query := ` 209 + SELECT cid, did, version, prev_cid, data_cid, revision, signature, signing_key_id, created_at 210 + FROM commits 211 + WHERE did = $1 212 + ORDER BY created_at DESC 213 + LIMIT 1` 214 + 215 + var commit repository.Commit 216 + var cidStr, dataCIDStr string 217 + var prevCIDStr sql.NullString 218 + 219 + err := r.db.QueryRow(query, did).Scan( 220 + &cidStr, 221 + &commit.DID, 222 + &commit.Version, 223 + &prevCIDStr, 224 + &dataCIDStr, 225 + &commit.Revision, 226 + &commit.Signature, 227 + &commit.SigningKeyID, 228 + &commit.CreatedAt, 229 + ) 230 + if err == sql.ErrNoRows { 231 + return nil, nil 232 + } 233 + if err != nil { 234 + return nil, fmt.Errorf("failed to get latest commit: %w", err) 235 + } 236 + 237 + commit.CID, err = cid.Parse(cidStr) 238 + if err != nil { 239 + return nil, fmt.Errorf("failed to parse commit CID: %w", err) 240 + } 241 + 242 + commit.DataCID, err = cid.Parse(dataCIDStr) 243 + if err != nil { 244 + return nil, fmt.Errorf("failed to parse data CID: %w", err) 245 + } 246 + 247 + if prevCIDStr.Valid { 248 + prevCID, err := cid.Parse(prevCIDStr.String) 249 + if err != nil { 250 + return nil, fmt.Errorf("failed to parse prev CID: %w", err) 251 + } 252 + commit.PrevCID = &prevCID 253 + } 254 + 255 + return &commit, nil 256 + } 257 + 258 + func (r *RepositoryRepo) ListCommits(did string, limit int, offset int) ([]*repository.Commit, error) { 259 + query := ` 260 + SELECT cid, did, version, prev_cid, data_cid, revision, signature, signing_key_id, created_at 261 + FROM commits 262 + WHERE did = $1 263 + ORDER BY created_at DESC 264 + LIMIT $2 OFFSET $3` 265 + 266 + rows, err := r.db.Query(query, did, limit, offset) 267 + if err != nil { 268 + return nil, fmt.Errorf("failed to list commits: %w", err) 269 + } 270 + defer rows.Close() 271 + 272 + var commits []*repository.Commit 273 + for rows.Next() { 274 + var commit repository.Commit 275 + var cidStr, dataCIDStr string 276 + var prevCIDStr sql.NullString 277 + 278 + err := rows.Scan( 279 + &cidStr, 280 + &commit.DID, 281 + &commit.Version, 282 + &prevCIDStr, 283 + &dataCIDStr, 284 + &commit.Revision, 285 + &commit.Signature, 286 + &commit.SigningKeyID, 287 + &commit.CreatedAt, 288 + ) 289 + if err != nil { 290 + return nil, fmt.Errorf("failed to scan commit: %w", err) 291 + } 292 + 293 + commit.CID, err = cid.Parse(cidStr) 294 + if err != nil { 295 + return nil, fmt.Errorf("failed to parse commit CID: %w", err) 296 + } 297 + 298 + commit.DataCID, err = cid.Parse(dataCIDStr) 299 + if err != nil { 300 + return nil, fmt.Errorf("failed to parse data CID: %w", err) 301 + } 302 + 303 + if prevCIDStr.Valid { 304 + prevCID, err := cid.Parse(prevCIDStr.String) 305 + if err != nil { 306 + return nil, fmt.Errorf("failed to parse prev CID: %w", err) 307 + } 308 + commit.PrevCID = &prevCID 309 + } 310 + 311 + commits = append(commits, &commit) 312 + } 313 + 314 + return commits, nil 315 + } 316 + 317 + // Record operations 318 + 319 + func (r *RepositoryRepo) CreateRecord(record *repository.Record) error { 320 + query := ` 321 + INSERT INTO records (did, uri, cid, collection, record_key, created_at, updated_at) 322 + VALUES ($1, $2, $3, $4, $5, $6, $7)` 323 + 324 + _, err := r.db.Exec(query, 325 + record.URI[:len("at://")+len(record.URI[len("at://"):])-len(record.Collection)-len(record.RecordKey)-2], // Extract DID from URI 326 + record.URI, 327 + record.CID.String(), 328 + record.Collection, 329 + record.RecordKey, 330 + record.CreatedAt, 331 + record.UpdatedAt, 332 + ) 333 + if err != nil { 334 + if pqErr, ok := err.(*pq.Error); ok && pqErr.Code == "23505" { // unique_violation 335 + return fmt.Errorf("record already exists: %s", record.URI) 336 + } 337 + return fmt.Errorf("failed to create record: %w", err) 338 + } 339 + 340 + return nil 341 + } 342 + 343 + func (r *RepositoryRepo) GetRecord(did string, collection string, recordKey string) (*repository.Record, error) { 344 + query := ` 345 + SELECT uri, cid, collection, record_key, created_at, updated_at 346 + FROM records 347 + WHERE did = $1 AND collection = $2 AND record_key = $3` 348 + 349 + var record repository.Record 350 + var cidStr string 351 + 352 + err := r.db.QueryRow(query, did, collection, recordKey).Scan( 353 + &record.URI, 354 + &cidStr, 355 + &record.Collection, 356 + &record.RecordKey, 357 + &record.CreatedAt, 358 + &record.UpdatedAt, 359 + ) 360 + if err == sql.ErrNoRows { 361 + return nil, nil 362 + } 363 + if err != nil { 364 + return nil, fmt.Errorf("failed to get record: %w", err) 365 + } 366 + 367 + record.CID, err = cid.Parse(cidStr) 368 + if err != nil { 369 + return nil, fmt.Errorf("failed to parse record CID: %w", err) 370 + } 371 + 372 + return &record, nil 373 + } 374 + 375 + func (r *RepositoryRepo) UpdateRecord(record *repository.Record) error { 376 + did := record.URI[:len("at://")+len(record.URI[len("at://"):])-len(record.Collection)-len(record.RecordKey)-2] 377 + 378 + query := ` 379 + UPDATE records 380 + SET cid = $4, updated_at = $5 381 + WHERE did = $1 AND collection = $2 AND record_key = $3` 382 + 383 + result, err := r.db.Exec(query, 384 + did, 385 + record.Collection, 386 + record.RecordKey, 387 + record.CID.String(), 388 + time.Now(), 389 + ) 390 + if err != nil { 391 + return fmt.Errorf("failed to update record: %w", err) 392 + } 393 + 394 + rowsAffected, err := result.RowsAffected() 395 + if err != nil { 396 + return fmt.Errorf("failed to get rows affected: %w", err) 397 + } 398 + if rowsAffected == 0 { 399 + return fmt.Errorf("record not found: %s", record.URI) 400 + } 401 + 402 + return nil 403 + } 404 + 405 + func (r *RepositoryRepo) DeleteRecord(did string, collection string, recordKey string) error { 406 + query := `DELETE FROM records WHERE did = $1 AND collection = $2 AND record_key = $3` 407 + 408 + result, err := r.db.Exec(query, did, collection, recordKey) 409 + if err != nil { 410 + return fmt.Errorf("failed to delete record: %w", err) 411 + } 412 + 413 + rowsAffected, err := result.RowsAffected() 414 + if err != nil { 415 + return fmt.Errorf("failed to get rows affected: %w", err) 416 + } 417 + if rowsAffected == 0 { 418 + return fmt.Errorf("record not found") 419 + } 420 + 421 + return nil 422 + } 423 + 424 + func (r *RepositoryRepo) ListRecords(did string, collection string, limit int, offset int) ([]*repository.Record, error) { 425 + query := ` 426 + SELECT uri, cid, collection, record_key, created_at, updated_at 427 + FROM records 428 + WHERE did = $1 AND collection = $2 429 + ORDER BY created_at DESC 430 + LIMIT $3 OFFSET $4` 431 + 432 + rows, err := r.db.Query(query, did, collection, limit, offset) 433 + if err != nil { 434 + return nil, fmt.Errorf("failed to list records: %w", err) 435 + } 436 + defer rows.Close() 437 + 438 + var records []*repository.Record 439 + for rows.Next() { 440 + var record repository.Record 441 + var cidStr string 442 + 443 + err := rows.Scan( 444 + &record.URI, 445 + &cidStr, 446 + &record.Collection, 447 + &record.RecordKey, 448 + &record.CreatedAt, 449 + &record.UpdatedAt, 450 + ) 451 + if err != nil { 452 + return nil, fmt.Errorf("failed to scan record: %w", err) 453 + } 454 + 455 + record.CID, err = cid.Parse(cidStr) 456 + if err != nil { 457 + return nil, fmt.Errorf("failed to parse record CID: %w", err) 458 + } 459 + 460 + records = append(records, &record) 461 + } 462 + 463 + return records, nil 464 + } 465 +
+84
internal/db/test_db_compose/README.md
··· 1 + # Test Database Setup 2 + 3 + This directory contains the Docker Compose configuration for the Coves test database. 4 + 5 + ## Overview 6 + 7 + The test database is a PostgreSQL instance specifically for running automated tests. It's completely isolated from development and production databases. 8 + 9 + ### Configuration 10 + 11 + - **Port**: 5434 (different from dev: 5433, prod: 5432) 12 + - **Database**: coves_test 13 + - **User**: test_user 14 + - **Password**: test_password 15 + - **Data Volume**: ~/Code/Coves/test_db_data 16 + 17 + ## Usage 18 + 19 + ### Starting the Test Database 20 + 21 + ```bash 22 + cd internal/db/test_db_compose 23 + ./start-test-db.sh 24 + ``` 25 + 26 + This will: 27 + 1. Start the PostgreSQL container 28 + 2. Wait for it to be ready 29 + 3. Display the connection string 30 + 31 + ### Running Tests 32 + 33 + Once the database is running, you can run tests with: 34 + 35 + ```bash 36 + TEST_DATABASE_URL=postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable go test -v ./... 37 + ``` 38 + 39 + Or set the environment variable: 40 + 41 + ```bash 42 + export TEST_DATABASE_URL=postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable 43 + go test -v ./... 44 + ``` 45 + 46 + ### Stopping the Test Database 47 + 48 + ```bash 49 + ./stop-test-db.sh 50 + ``` 51 + 52 + ### Resetting Test Data 53 + 54 + To completely reset the test database (removes all data): 55 + 56 + ```bash 57 + ./reset-test-db.sh 58 + ``` 59 + 60 + ## Test Isolation 61 + 62 + The test database is isolated from other environments: 63 + 64 + | Environment | Port | Database Name | User | 65 + |------------|------|--------------|------| 66 + | Test | 5434 | coves_test | test_user | 67 + | Development | 5433 | coves_dev | dev_user | 68 + | Production | 5432 | coves | (varies) | 69 + 70 + ## What Gets Tested 71 + 72 + When tests run against this database, they will: 73 + 74 + 1. Run all migrations from `internal/db/migrations/` 75 + 2. Create Indigo carstore tables (via GORM auto-migration) 76 + 3. Test the full integration including: 77 + - Repository CRUD operations 78 + - CAR file metadata storage 79 + - User DID to UID mapping 80 + - Carstore operations 81 + 82 + ## CI/CD Integration 83 + 84 + For CI/CD pipelines, you can use the same Docker Compose setup or connect to a dedicated test database instance.
+19
internal/db/test_db_compose/docker-compose.yml
··· 1 + # Test Database Docker Compose Configuration 2 + # This database is specifically for running tests and is isolated from dev/prod 3 + services: 4 + postgres_test: 5 + image: postgres:15 6 + container_name: coves_test_db 7 + network_mode: host 8 + environment: 9 + POSTGRES_DB: coves_test 10 + POSTGRES_USER: test_user 11 + POSTGRES_PASSWORD: test_password 12 + PGPORT: 5434 # Different port from dev (5433) and prod (5432) 13 + volumes: 14 + - ~/Code/Coves/test_db_data:/var/lib/postgresql/data 15 + healthcheck: 16 + test: ["CMD-SHELL", "pg_isready -U test_user -d coves_test -p 5434"] 17 + interval: 5s 18 + timeout: 5s 19 + retries: 5
+15
internal/db/test_db_compose/reset-test-db.sh
··· 1 + #!/bin/bash 2 + # Reset the test database by removing all data 3 + 4 + echo "WARNING: This will delete all test database data!" 5 + echo "Press Ctrl+C to cancel, or Enter to continue..." 6 + read 7 + 8 + echo "Stopping test database..." 9 + docker-compose -f docker-compose.yml down 10 + 11 + echo "Removing test data volume..." 12 + rm -rf ~/Code/Coves/test_db_data 13 + 14 + echo "Starting fresh test database..." 15 + ./start-test-db.sh
+25
internal/db/test_db_compose/start-test-db.sh
··· 1 + #!/bin/bash 2 + # Start the test database 3 + 4 + echo "Starting Coves test database on port 5434..." 5 + docker-compose -f docker-compose.yml up -d 6 + 7 + # Wait for database to be ready 8 + echo "Waiting for database to be ready..." 9 + for i in {1..30}; do 10 + if docker-compose -f docker-compose.yml exec -T postgres_test pg_isready -U test_user -d coves_test -p 5434 &>/dev/null; then 11 + echo "Test database is ready!" 12 + echo "" 13 + echo "Connection string:" 14 + echo "TEST_DATABASE_URL=postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" 15 + echo "" 16 + echo "To run tests:" 17 + echo "TEST_DATABASE_URL=postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable go test -v ./..." 18 + exit 0 19 + fi 20 + echo -n "." 21 + sleep 1 22 + done 23 + 24 + echo "Failed to start test database" 25 + exit 1
+7
internal/db/test_db_compose/stop-test-db.sh
··· 1 + #!/bin/bash 2 + # Stop the test database 3 + 4 + echo "Stopping Coves test database..." 5 + docker-compose -f docker-compose.yml down 6 + 7 + echo "Test database stopped."
+50
run-tests.sh
··· 1 + #!/bin/bash 2 + # Helper script to run tests with the test database 3 + 4 + # Colors for output 5 + GREEN='\033[0;32m' 6 + RED='\033[0;31m' 7 + NC='\033[0m' # No Color 8 + 9 + echo "🧪 Coves Test Runner" 10 + echo "===================" 11 + echo "" 12 + 13 + # Check if test database is running 14 + if ! nc -z localhost 5434 2>/dev/null; then 15 + echo -e "${RED}❌ Test database is not running${NC}" 16 + echo "" 17 + echo "Starting test database..." 18 + cd internal/db/test_db_compose && ./start-test-db.sh 19 + cd ../../.. 20 + echo "" 21 + fi 22 + 23 + # Load test environment 24 + if [ -f .env.test ]; then 25 + export $(cat .env.test | grep -v '^#' | xargs) 26 + fi 27 + 28 + # Run tests 29 + echo "Running tests..." 30 + echo "" 31 + 32 + if [ $# -eq 0 ]; then 33 + # No arguments, run all tests 34 + go test -v ./... 35 + else 36 + # Pass arguments to go test 37 + go test -v "$@" 38 + fi 39 + 40 + TEST_RESULT=$? 41 + 42 + if [ $TEST_RESULT -eq 0 ]; then 43 + echo "" 44 + echo -e "${GREEN}✅ All tests passed!${NC}" 45 + else 46 + echo "" 47 + echo -e "${RED}❌ Some tests failed${NC}" 48 + fi 49 + 50 + exit $TEST_RESULT
server

This is a binary file and will not be displayed.

+1 -1
tests/integration/integration_test.go
··· 21 21 func setupTestDB(t *testing.T) *sql.DB { 22 22 dbURL := os.Getenv("TEST_DATABASE_URL") 23 23 if dbURL == "" { 24 - dbURL = "postgres://dev_user:dev_password@localhost:5433/coves_dev?sslmode=disable" 24 + dbURL = "postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" 25 25 } 26 26 27 27 db, err := sql.Open("postgres", dbURL)
+103
tests/integration/repository_test.go
··· 1 + package integration_test 2 + 3 + import ( 4 + "os" 5 + "testing" 6 + 7 + "Coves/internal/atproto/carstore" 8 + "Coves/internal/core/repository" 9 + "Coves/internal/db/postgres" 10 + "database/sql" 11 + _ "github.com/lib/pq" 12 + "github.com/pressly/goose/v3" 13 + postgresDriver "gorm.io/driver/postgres" 14 + "gorm.io/gorm" 15 + ) 16 + 17 + func TestRepositoryIntegration(t *testing.T) { 18 + // Skip if not running integration tests 19 + if testing.Short() { 20 + t.Skip("Skipping integration test") 21 + } 22 + 23 + // Use test database URL from environment or default 24 + dbURL := os.Getenv("TEST_DATABASE_URL") 25 + if dbURL == "" { 26 + dbURL = "postgres://test_user:test_password@localhost:5434/coves_test?sslmode=disable" 27 + } 28 + 29 + // Connect to test database with sql.DB for migrations 30 + sqlDB, err := sql.Open("postgres", dbURL) 31 + if err != nil { 32 + t.Fatalf("Failed to connect to test database: %v", err) 33 + } 34 + defer sqlDB.Close() 35 + 36 + // Run migrations 37 + if err := goose.Up(sqlDB, "../../internal/db/migrations"); err != nil { 38 + t.Fatalf("Failed to run migrations: %v", err) 39 + } 40 + 41 + // Connect with GORM for carstore 42 + gormDB, err := gorm.Open(postgresDriver.Open(dbURL), &gorm.Config{ 43 + DisableForeignKeyConstraintWhenMigrating: true, 44 + PrepareStmt: false, 45 + }) 46 + if err != nil { 47 + t.Fatalf("Failed to create GORM connection: %v", err) 48 + } 49 + 50 + // Create temporary directory for carstore 51 + tempDir, err := os.MkdirTemp("", "carstore_integration_test") 52 + if err != nil { 53 + t.Fatalf("Failed to create temp dir: %v", err) 54 + } 55 + defer os.RemoveAll(tempDir) 56 + 57 + // Initialize carstore 58 + carDirs := []string{tempDir} 59 + repoStore, err := carstore.NewRepoStore(gormDB, carDirs) 60 + if err != nil { 61 + t.Fatalf("Failed to create repo store: %v", err) 62 + } 63 + 64 + // Create repository repo 65 + repoRepo := postgres.NewRepositoryRepo(sqlDB) 66 + 67 + // Create service with both repo and repoStore 68 + service := repository.NewService(repoRepo, repoStore) 69 + 70 + // Test creating a repository 71 + did := "did:plc:testuser123" 72 + service.SetSigningKey(did, "mock-signing-key") 73 + 74 + repo, err := service.CreateRepository(did) 75 + if err != nil { 76 + t.Fatalf("Failed to create repository: %v", err) 77 + } 78 + 79 + if repo.DID != did { 80 + t.Errorf("Expected DID %s, got %s", did, repo.DID) 81 + } 82 + 83 + // Test getting the repository 84 + fetchedRepo, err := service.GetRepository(did) 85 + if err != nil { 86 + t.Fatalf("Failed to get repository: %v", err) 87 + } 88 + 89 + if fetchedRepo.DID != did { 90 + t.Errorf("Expected DID %s, got %s", did, fetchedRepo.DID) 91 + } 92 + 93 + // Clean up 94 + err = service.DeleteRepository(did) 95 + if err != nil { 96 + t.Fatalf("Failed to delete repository: %v", err) 97 + } 98 + 99 + // Clean up test data 100 + gormDB.Exec("DELETE FROM repositories") 101 + gormDB.Exec("DELETE FROM user_maps") 102 + gormDB.Exec("DELETE FROM car_shards") 103 + }