+12
.env.test.example
+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
+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
+3
CLAUDE.md
+70
TESTING_SUMMARY.md
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+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
+7
internal/db/test_db_compose/stop-test-db.sh
+50
run-tests.sh
+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
server
This is a binary file and will not be displayed.
+1
-1
tests/integration/integration_test.go
+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
+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
+
}