- commit
- 9c8d241
- parent
- d247cbd
- author
- Eric Bower
- date
- 2025-12-17 22:03:10 -0500 EST
refactor(db): use sqlx
+1,
-0
1@@ -16,3 +16,4 @@ __debug_bin
2 .aider*
3 ssh
4 web
5+.beads/
+2,
-1
1@@ -7,12 +7,13 @@ import (
2 "os"
3 "time"
4
5+ "github.com/jmoiron/sqlx"
6 "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/pico/pkg/db/postgres"
8 "github.com/picosh/pico/pkg/shared"
9 )
10
11-func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
12+func findPosts(dbpool *sqlx.DB) ([]*db.Post, error) {
13 var posts []*db.Post
14 rs, err := dbpool.Query(`SELECT
15 id, user_id, filename, title, text, description,
+5,
-4
1@@ -7,12 +7,13 @@ import (
2 "log/slog"
3 "os"
4
5+ "github.com/jmoiron/sqlx"
6 "github.com/picosh/pico/pkg/db"
7 "github.com/picosh/pico/pkg/db/postgres"
8 "github.com/picosh/pico/pkg/shared"
9 )
10
11-func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
12+func findPosts(dbpool *sqlx.DB) ([]*db.Post, error) {
13 var posts []*db.Post
14 rs, err := dbpool.Query(`SELECT
15 posts.id, user_id, filename, title, text, description,
16@@ -147,7 +148,7 @@ func main() {
17 panic(err)
18 }
19
20- proseKeys, err := proseDb.FindKeysForUser(proseUser)
21+ proseKeys, err := proseDb.FindKeysByUser(proseUser)
22 if err != nil {
23 panic(err)
24 }
25@@ -165,7 +166,7 @@ func main() {
26 updateIDs := []*ConflictData{}
27 logger.Info("Finding conflicts")
28 for _, listUser := range listUsers {
29- listKeys, err := listsDb.FindKeysForUser(listUser)
30+ listKeys, err := listsDb.FindKeysByUser(listUser)
31 if err != nil {
32 panic(err)
33 }
34@@ -180,7 +181,7 @@ func main() {
35 continue
36 } else {
37 proseUser := userMap[listUser.Name]
38- proseKeys, err := proseDb.FindKeysForUser(proseUser)
39+ proseKeys, err := proseDb.FindKeysByUser(proseUser)
40 if err != nil {
41 panic(err)
42 }
1@@ -1,16 +1,16 @@
2 package main
3
4 import (
5- "database/sql"
6 "log/slog"
7 "os"
8
9+ "github.com/jmoiron/sqlx"
10 "github.com/picosh/pico/pkg/db"
11 "github.com/picosh/pico/pkg/db/postgres"
12 "github.com/picosh/pico/pkg/shared"
13 )
14
15-func findPosts(dbpool *sql.DB) ([]*db.Post, error) {
16+func findPosts(dbpool *sqlx.DB) ([]*db.Post, error) {
17 var posts []*db.Post
18 rs, err := dbpool.Query(`SELECT
19 posts.id, user_id, filename, title, text, description,
20@@ -69,7 +69,7 @@ func main() {
21 continue
22 }
23 if len(parsed.Tags) > 0 {
24- err := picoDb.ReplaceTagsForPost(parsed.Tags, post.ID)
25+ err := picoDb.ReplaceTagsByPost(parsed.Tags, post.ID)
26 panic(err)
27 }
28 }
M
go.mod
+48,
-12
1@@ -1,8 +1,6 @@
2 module github.com/picosh/pico
3
4-go 1.24
5-
6-toolchain go1.24.0
7+go 1.24.0
8
9 // replace github.com/picosh/tunkit => ../tunkit
10
11@@ -55,6 +53,8 @@ require (
12 github.com/prometheus/client_golang v1.22.0
13 github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
14 github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d
15+ github.com/testcontainers/testcontainers-go v0.40.0
16+ github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0
17 github.com/x-way/crawlerdetect v0.2.28
18 github.com/yuin/goldmark v1.7.8
19 github.com/yuin/goldmark-highlighting/v2 v2.0.0-20230729083705-37449abec8cc
20@@ -62,7 +62,7 @@ require (
21 go.abhg.dev/goldmark/anchor v0.2.0
22 go.abhg.dev/goldmark/hashtag v0.3.1
23 go.abhg.dev/goldmark/toc v0.11.0
24- golang.org/x/crypto v0.38.0
25+ golang.org/x/crypto v0.43.0
26 google.golang.org/protobuf v1.36.6
27 gopkg.in/yaml.v2 v2.4.0
28 modernc.org/sqlite v1.36.2
29@@ -71,9 +71,10 @@ require (
30 require (
31 cel.dev/expr v0.22.1 // indirect
32 codeberg.org/emersion/go-scfg v0.1.0 // indirect
33- dario.cat/mergo v1.0.1 // indirect
34+ dario.cat/mergo v1.0.2 // indirect
35 filippo.io/edwards25519 v1.1.0 // indirect
36 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 // indirect
37+ github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
38 github.com/KimMachineGun/automemlimit v0.7.1 // indirect
39 github.com/Masterminds/goutils v1.1.1 // indirect
40 github.com/Masterminds/semver/v3 v3.3.1 // indirect
41@@ -98,13 +99,19 @@ require (
42 github.com/caddyserver/certmagic v0.23.0 // indirect
43 github.com/caddyserver/zerossl v0.1.3 // indirect
44 github.com/ccoveille/go-safecast v1.6.1 // indirect
45+ github.com/cenkalti/backoff/v4 v4.3.0 // indirect
46 github.com/cespare/xxhash v1.1.0 // indirect
47 github.com/cespare/xxhash/v2 v2.3.0 // indirect
48 github.com/chzyer/readline v1.5.1 // indirect
49 github.com/cloudflare/circl v1.6.0 // indirect
50+ github.com/containerd/errdefs v1.0.0 // indirect
51+ github.com/containerd/errdefs/pkg v0.3.0 // indirect
52+ github.com/containerd/log v0.1.0 // indirect
53+ github.com/containerd/platforms v0.2.1 // indirect
54 github.com/coreos/go-oidc/v3 v3.13.0 // indirect
55 github.com/coreos/go-semver v0.3.1 // indirect
56 github.com/coreos/go-systemd/v22 v22.5.0 // indirect
57+ github.com/cpuguy83/dockercfg v0.3.2 // indirect
58 github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
59 github.com/darkweak/go-esi v0.0.6 // indirect
60 github.com/darkweak/storages/badger v0.0.14 // indirect
61@@ -115,6 +122,7 @@ require (
62 github.com/darkweak/storages/otter v0.0.14 // indirect
63 github.com/darkweak/storages/redis v0.0.14 // indirect
64 github.com/darkweak/storages/simplefs v0.0.14 // indirect
65+ github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
66 github.com/delthas/go-libnp v0.1.0 // indirect
67 github.com/delthas/go-localeinfo v0.1.0 // indirect
68 github.com/dgraph-io/badger v1.6.2 // indirect
69@@ -124,7 +132,11 @@ require (
70 github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da // indirect
71 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
72 github.com/disintegration/imaging v1.6.2 // indirect
73+ github.com/distribution/reference v0.6.0 // indirect
74 github.com/dlclark/regexp2 v1.11.5 // indirect
75+ github.com/docker/docker v28.5.1+incompatible // indirect
76+ github.com/docker/go-connections v0.6.0 // indirect
77+ github.com/docker/go-units v0.5.0 // indirect
78 github.com/dolthub/maphash v0.1.0 // indirect
79 github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d // indirect
80 github.com/dsoprea/go-exif/v2 v2.0.0-20230826092837-6579e82b732d // indirect
81@@ -134,6 +146,8 @@ require (
82 github.com/dsoprea/go-png-image-structure v0.0.0-20210512210324-29b889a6093d // indirect
83 github.com/dsoprea/go-utility v0.0.0-20221003172846-a3e1774ef349 // indirect
84 github.com/dustin/go-humanize v1.0.1 // indirect
85+ github.com/ebitengine/purego v0.8.4 // indirect
86+ github.com/felixge/httpsnoop v1.0.4 // indirect
87 github.com/forPelevin/gomoji v1.3.0 // indirect
88 github.com/francoispqt/gojay v1.2.13 // indirect
89 github.com/gammazero/deque v1.0.0 // indirect
90@@ -143,6 +157,8 @@ require (
91 github.com/go-ini/ini v1.67.0 // indirect
92 github.com/go-jose/go-jose/v3 v3.0.4 // indirect
93 github.com/go-jose/go-jose/v4 v4.0.5 // indirect
94+ github.com/go-logr/logr v1.4.2 // indirect
95+ github.com/go-logr/stdr v1.2.2 // indirect
96 github.com/go-ole/go-ole v1.3.0 // indirect
97 github.com/go-redis/redis/v8 v8.11.5 // indirect
98 github.com/go-sql-driver/mysql v1.9.1 // indirect
99@@ -186,6 +202,7 @@ require (
100 github.com/kr/text v0.2.0 // indirect
101 github.com/libdns/libdns v1.0.0-beta.1 // indirect
102 github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
103+ github.com/magiconair/properties v1.8.10 // indirect
104 github.com/manifoldco/promptui v0.9.0 // indirect
105 github.com/maruel/natural v1.1.1 // indirect
106 github.com/mattn/go-colorable v0.1.14 // indirect
107@@ -204,8 +221,16 @@ require (
108 github.com/mitchellh/reflectwalk v1.0.2 // indirect
109 github.com/mmcdole/goxpp v1.1.1 // indirect
110 github.com/mmcloughlin/md4 v0.1.2 // indirect
111+ github.com/moby/docker-image-spec v1.3.1 // indirect
112+ github.com/moby/go-archive v0.1.0 // indirect
113+ github.com/moby/patternmatcher v0.6.0 // indirect
114+ github.com/moby/sys/sequential v0.6.0 // indirect
115+ github.com/moby/sys/user v0.4.0 // indirect
116+ github.com/moby/sys/userns v0.1.0 // indirect
117+ github.com/moby/term v0.5.0 // indirect
118 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
119 github.com/modern-go/reflect2 v1.0.2 // indirect
120+ github.com/morikuni/aec v1.0.0 // indirect
121 github.com/mschoch/smat v0.2.0 // indirect
122 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
123 github.com/nats-io/nats.go v1.40.1 // indirect
124@@ -215,10 +240,13 @@ require (
125 github.com/neurosnap/go-jpeg-image-structure v0.0.0-20221010133817-70b1c1ff679e // indirect
126 github.com/nutsdb/nutsdb v1.0.4 // indirect
127 github.com/onsi/ginkgo/v2 v2.23.3 // indirect
128+ github.com/opencontainers/go-digest v1.0.0 // indirect
129+ github.com/opencontainers/image-spec v1.1.1 // indirect
130 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 // indirect
131 github.com/philhofer/fwd v1.1.3-0.20240916144458-20a13a1f6b7c // indirect
132 github.com/pierrec/lz4/v4 v4.1.22 // indirect
133 github.com/pkg/errors v0.9.1 // indirect
134+ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
135 github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
136 github.com/pquerna/cachecontrol v0.2.0 // indirect
137 github.com/prometheus/client_model v0.6.2 // indirect
138@@ -238,9 +266,11 @@ require (
139 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
140 github.com/secure-io/sio-go v0.3.1 // indirect
141 github.com/shirou/gopsutil/v3 v3.24.5 // indirect
142+ github.com/shirou/gopsutil/v4 v4.25.6 // indirect
143 github.com/shoenig/go-m1cpu v0.1.6 // indirect
144 github.com/shopspring/decimal v1.4.0 // indirect
145 github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
146+ github.com/sirupsen/logrus v1.9.3 // indirect
147 github.com/slackhq/nebula v1.9.5 // indirect
148 github.com/smallstep/certificates v0.28.3 // indirect
149 github.com/smallstep/cli-utils v0.12.1 // indirect
150@@ -254,6 +284,7 @@ require (
151 github.com/spf13/cobra v1.9.1 // indirect
152 github.com/spf13/pflag v1.0.6 // indirect
153 github.com/stoewer/go-strcase v1.3.0 // indirect
154+ github.com/stretchr/testify v1.11.1 // indirect
155 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 // indirect
156 github.com/tidwall/btree v1.7.0 // indirect
157 github.com/tidwall/gjson v1.17.0 // indirect
158@@ -276,6 +307,11 @@ require (
159 go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
160 go.etcd.io/etcd/client/v3 v3.5.21 // indirect
161 go.opencensus.io v0.24.0 // indirect
162+ go.opentelemetry.io/auto/sdk v1.1.0 // indirect
163+ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 // indirect
164+ go.opentelemetry.io/otel v1.35.0 // indirect
165+ go.opentelemetry.io/otel/metric v1.35.0 // indirect
166+ go.opentelemetry.io/otel/trace v1.35.0 // indirect
167 go.step.sm/crypto v0.60.0 // indirect
168 go.uber.org/automaxprocs v1.6.0 // indirect
169 go.uber.org/mock v0.5.0 // indirect
170@@ -285,15 +321,15 @@ require (
171 golang.org/x/crypto/x509roots/fallback v0.0.0-20250317152234-d0a798f77473 // indirect
172 golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
173 golang.org/x/image v0.25.0 // indirect
174- golang.org/x/mod v0.24.0 // indirect
175- golang.org/x/net v0.38.0 // indirect
176+ golang.org/x/mod v0.28.0 // indirect
177+ golang.org/x/net v0.45.0 // indirect
178 golang.org/x/oauth2 v0.28.0 // indirect
179- golang.org/x/sync v0.14.0 // indirect
180- golang.org/x/sys v0.33.0 // indirect
181- golang.org/x/term v0.32.0 // indirect
182- golang.org/x/text v0.25.0 // indirect
183+ golang.org/x/sync v0.17.0 // indirect
184+ golang.org/x/sys v0.37.0 // indirect
185+ golang.org/x/term v0.36.0 // indirect
186+ golang.org/x/text v0.30.0 // indirect
187 golang.org/x/time v0.11.0 // indirect
188- golang.org/x/tools v0.31.0 // indirect
189+ golang.org/x/tools v0.37.0 // indirect
190 google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 // indirect
191 google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
192 google.golang.org/grpc v1.71.0 // indirect
M
go.sum
+101,
-26
1@@ -20,8 +20,8 @@ cloud.google.com/go/longrunning v0.6.6 h1:XJNDo5MUfMM05xK3ewpbSdmt7R2Zw+aQEMbdQR
2 cloud.google.com/go/longrunning v0.6.6/go.mod h1:hyeGJUrPHcx0u2Uu1UFSoYZLn4lkMrccJig0t4FI7yw=
3 codeberg.org/emersion/go-scfg v0.1.0 h1:6dnGU0ZI4gX+O5rMjwhoaySItzHG710eXL5TIQKl+uM=
4 codeberg.org/emersion/go-scfg v0.1.0/go.mod h1:0nooW1ufBB4SlJEdTtiVN9Or+bnNM1icOkQ6Tbrq6O0=
5-dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
6-dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
7+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
8+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
9 dmitri.shuralyov.com/app/changes v0.0.0-20180602232624-0a106ad413e3/go.mod h1:Yl+fi1br7+Rr3LqpNJf1/uxUdtRUV+Tnj0o93V2B9MU=
10 dmitri.shuralyov.com/html/belt v0.0.0-20180602232347-f7d459c86be0/go.mod h1:JLBrvjyP0v+ecvNYvCpyZgu5/xkfAUhi6wJj28eUfSU=
11 dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1:a1inKt/atXimZ4Mv927x+r7UpyzRUf4emIoiiSC2TN4=
12@@ -33,8 +33,12 @@ git.sr.ht/~delthas/senpai v0.4.0 h1:G1cycL0ARqYkqOErjEx/7Cneqykq3POfCsMYnpkWhUY=
13 git.sr.ht/~delthas/senpai v0.4.0/go.mod h1:mzdu4o3wANA6cYzRnrz3w+uGPHA2z3j02JDrr/M3Myc=
14 git.sr.ht/~rockorager/vaxis v0.14.1-0.20250527151737-5530f9f4bcf6 h1:lmSD6ksbKNMLdhT8R2k5FQPz8ObjslX8NkCM6z1kifI=
15 git.sr.ht/~rockorager/vaxis v0.14.1-0.20250527151737-5530f9f4bcf6/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
16+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk=
17+github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8=
18 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
19 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
20+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
21+github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
22 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
23 github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
24 github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
25@@ -146,6 +150,8 @@ github.com/caddyserver/zerossl v0.1.3 h1:onS+pxp3M8HnHpN5MMbOMyNjmTheJyWRaZYwn+Y
26 github.com/caddyserver/zerossl v0.1.3/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
27 github.com/ccoveille/go-safecast v1.6.1 h1:Nb9WMDR8PqhnKCVs2sCB+OqhohwO5qaXtCviZkIff5Q=
28 github.com/ccoveille/go-safecast v1.6.1/go.mod h1:QqwNjxQ7DAqY0C721OIO9InMk9zCwcsO7tnRuHytad8=
29+github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
30+github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
31 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
32 github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
33 github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
34@@ -170,6 +176,14 @@ github.com/cloudflare/circl v1.6.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZ
35 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
36 github.com/containerd/console v1.0.4 h1:F2g4+oChYvBTsASRTz8NP6iIAi97J3TtSAsLbIFn4ro=
37 github.com/containerd/console v1.0.4/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk=
38+github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
39+github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
40+github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
41+github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
42+github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
43+github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
44+github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A=
45+github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw=
46 github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
47 github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
48 github.com/coreos/go-oidc/v3 v3.13.0 h1:M66zd0pcc5VxvBNM4pB331Wrsanby+QomQYjN8HamW8=
49@@ -180,11 +194,15 @@ github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03V
50 github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
51 github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
52 github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
53+github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA=
54+github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc=
55 github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
56 github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
57 github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0=
58 github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
59 github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
60+github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY=
61+github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4=
62 github.com/darkweak/go-esi v0.0.6 h1:eVHCJfqrZwOHPfRK7JTlSYG9F8lfpX/d4lz/41RQkd8=
63 github.com/darkweak/go-esi v0.0.6/go.mod h1:IJSayeQZDUh5R5ayyDC3wUEBykti12aUa0eUxZZeodk=
64 github.com/darkweak/souin v1.7.7 h1:pIj/cknEsV/+xgq74VOGE1+0yy0dNnt5I6HGRczJlKk=
65@@ -235,10 +253,18 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r
66 github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
67 github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1ei82L+c=
68 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=
69+github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
70+github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
71 github.com/dlclark/regexp2 v1.4.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
72 github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
73 github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ=
74 github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
75+github.com/docker/docker v28.5.1+incompatible h1:Bm8DchhSD2J6PsFzxC35TZo4TLGR2PdW/E69rU45NhM=
76+github.com/docker/docker v28.5.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
77+github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
78+github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
79+github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
80+github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
81 github.com/dolthub/maphash v0.1.0 h1:bsQ7JsF4FkkWyrP3oCnFJgrCUAFbFf3kOl4L/QxPDyQ=
82 github.com/dolthub/maphash v0.1.0/go.mod h1:gkg4Ch4CdCDu5h6PMriVLawB7koZ+5ijb9puGMV50a4=
83 github.com/dsoprea/go-exif v0.0.0-20230826092837-6579e82b732d h1:ygcRCGNKuEiA98k7X35hknEN8RIRUF1jrz7k1rZCvsk=
84@@ -268,6 +294,8 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uA
85 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
86 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
87 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
88+github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
89+github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
90 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
91 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
92 github.com/emersion/go-smtp v0.23.0 h1:ZiriTOTK7sKep7jbWqgB5kPsiBp5wnE5auEMnwRMnGc=
93@@ -317,6 +345,7 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb
94 github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
95 github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
96 github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
97+github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
98 github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
99 github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
100 github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
101@@ -437,6 +466,9 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
102 github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
103 github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
104 github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
105+github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
106+github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg=
107+github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ=
108 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
109 github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
110 github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
111@@ -536,6 +568,8 @@ github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr32
112 github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
113 github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
114 github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
115+github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
116+github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
117 github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
118 github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
119 github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
120@@ -561,6 +595,8 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
121 github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
122 github.com/maypok86/otter v1.2.4 h1:HhW1Pq6VdJkmWwcZZq19BlEQkHtI8xgsQzBVXJU0nfc=
123 github.com/maypok86/otter v1.2.4/go.mod h1:mKLfoI7v1HOmQMwFgX4QkRk23mX6ge3RDvjdHOWG4R4=
124+github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
125+github.com/mdelapenya/tlscert v0.2.0/go.mod h1:O4njj3ELLnJjGdkN7M/vIVCpZ+Cf0L6muqOG4tLSl8o=
126 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI=
127 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
128 github.com/mholt/acmez/v3 v3.1.2 h1:auob8J/0FhmdClQicvJvuDavgd5ezwLBfKuYmynhYzc=
129@@ -596,6 +632,22 @@ github.com/mmcdole/goxpp v1.1.1 h1:RGIX+D6iQRIunGHrKqnA2+700XMCnNv0bAOOv5MUhx8=
130 github.com/mmcdole/goxpp v1.1.1/go.mod h1:v+25+lT2ViuQ7mVxcncQ8ch1URund48oH+jhjiwEgS8=
131 github.com/mmcloughlin/md4 v0.1.2 h1:kGYl+iNbxhyz4u76ka9a+0TXP9KWt/LmnM0QhZwhcBo=
132 github.com/mmcloughlin/md4 v0.1.2/go.mod h1:AAxFX59fddW0IguqNzWlf1lazh1+rXeIt/Bj49cqDTQ=
133+github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
134+github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
135+github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ=
136+github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo=
137+github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk=
138+github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc=
139+github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
140+github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
141+github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
142+github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
143+github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs=
144+github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs=
145+github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g=
146+github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
147+github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
148+github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
149 github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
150 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
151 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
152@@ -603,6 +655,8 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN
153 github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
154 github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
155 github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
156+github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
157+github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
158 github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
159 github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
160 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
161@@ -642,6 +696,10 @@ github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAl
162 github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs=
163 github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
164 github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
165+github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
166+github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
167+github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
168+github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M=
169 github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
170 github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
171 github.com/pascaldekloe/goe v0.1.0 h1:cBOtyMzM9HTpWjXfbbunk26uA6nG3a8n06Wieeh0MwY=
172@@ -746,6 +804,8 @@ github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZ
173 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
174 github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
175 github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
176+github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
177+github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
178 github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
179 github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
180 github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
181@@ -829,6 +889,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
182 github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
183 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
184 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
185+github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
186 github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
187 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
188 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
189@@ -841,11 +902,15 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
190 github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
191 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
192 github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
193-github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
194-github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
195+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
196+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
197 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53 h1:uxMgm0C+EjytfAqyfBG55ZONKQ7mvd7x4YYCWsf8QHQ=
198 github.com/tailscale/tscert v0.0.0-20240608151842-d3f834017e53/go.mod h1:kNGUQ3VESx3VZwRwA9MSCUegIl6+saPL8Noq82ozCaU=
199 github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
200+github.com/testcontainers/testcontainers-go v0.40.0 h1:pSdJYLOVgLE8YdUY2FHQ1Fxu+aMnb6JfVz1mxk7OeMU=
201+github.com/testcontainers/testcontainers-go v0.40.0/go.mod h1:FSXV5KQtX2HAMlm7U3APNyLkkap35zNLxukw9oBi/MY=
202+github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0 h1:s2bIayFXlbDFexo96y+htn7FzuhpXLYJNnIuglNKqOk=
203+github.com/testcontainers/testcontainers-go/modules/postgres v0.40.0/go.mod h1:h+u/2KoREGTnTl9UwrQ/g+XhasAT8E6dClclAADeXoQ=
204 github.com/tidwall/btree v1.1.0/go.mod h1:TzIRzen6yHbibdSfK6t8QimqbUnoxUSrZfeW7Uob0q4=
205 github.com/tidwall/btree v1.7.0 h1:L1fkJH/AuEh5zBnnBbmTwQ5Lt+bRJ5A8EWecslvo9iI=
206 github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
207@@ -928,16 +993,22 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.5
208 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.59.0/go.mod h1:ijPqXp5P6IRRByFVVg9DY8P5HkxkHE5ARIa+86aXPf4=
209 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0 h1:CV7UdSGJt/Ao6Gp4CXckLxVRRsRgDHoI8XjbL3PDl8s=
210 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.59.0/go.mod h1:FRmFuRJfag1IZ2dPkHnEoSFVgTVPUd2qf5Vi69hLb8I=
211-go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY=
212-go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI=
213-go.opentelemetry.io/otel/metric v1.34.0 h1:+eTR3U0MyfWjRDhmFMxe2SsW64QrZ84AOhvqS7Y+PoQ=
214-go.opentelemetry.io/otel/metric v1.34.0/go.mod h1:CEDrp0fy2D0MvkXE+dPV7cMi8tWZwX3dmaIhwPOaqHE=
215+go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
216+go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
217+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60=
218+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM=
219+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0 h1:BEj3SPM81McUZHYjRS5pEgNgnmzGJ5tRpU5krWnV8Bs=
220+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.34.0/go.mod h1:9cKLGBDzI/F3NoHLQGm4ZrYdIHsvGt6ej6hUowxY0J4=
221+go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
222+go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
223 go.opentelemetry.io/otel/sdk v1.34.0 h1:95zS4k/2GOy069d321O8jWgYsW3MzVV+KuSPKp7Wr1A=
224 go.opentelemetry.io/otel/sdk v1.34.0/go.mod h1:0e/pNiaMAqaykJGKbi+tSjWfNNHMTxoC9qANsCzbyxU=
225 go.opentelemetry.io/otel/sdk/metric v1.34.0 h1:5CeK9ujjbFVL5c1PhLuStg1wxA7vQv7ce1EK0Gyvahk=
226 go.opentelemetry.io/otel/sdk/metric v1.34.0/go.mod h1:jQ/r8Ze28zRKoNRdkjCZxfs6YvBTG1+YIqyFVFYec5w=
227-go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k=
228-go.opentelemetry.io/otel/trace v1.34.0/go.mod h1:Svm7lSjQD7kG7KJ/MUHPVXSDGz2OX4h0M2jHBhmSfRE=
229+go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
230+go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
231+go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4=
232+go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4=
233 go.step.sm/crypto v0.60.0 h1:UgSw8DFG5xUOGB3GUID17UA32G4j1iNQ4qoMhBmsVFw=
234 go.step.sm/crypto v0.60.0/go.mod h1:Ep83Lv818L4gV0vhFTdPWRKnL6/5fRMpi8SaoP5ArSw=
235 go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
236@@ -970,8 +1041,8 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
237 golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
238 golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
239 golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
240-golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
241-golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
242+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
243+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
244 golang.org/x/crypto/x509roots/fallback v0.0.0-20250317152234-d0a798f77473 h1:XxwA7BHJuoBTkv1tIM7d/8rDnl9i9M0M5anwcJWZtbA=
245 golang.org/x/crypto/x509roots/fallback v0.0.0-20250317152234-d0a798f77473/go.mod h1:lxN5T34bK4Z/i6cMaU7frUU57VkDXFD4Kamfl/cp9oU=
246 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
247@@ -992,8 +1063,8 @@ golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
248 golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
249 golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
250 golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
251-golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
252-golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
253+golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
254+golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
255 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
256 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
257 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
258@@ -1030,8 +1101,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
259 golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
260 golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
261 golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
262-golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
263-golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
264+golang.org/x/net v0.45.0 h1:RLBg5JKixCy82FtLJpeNlVM0nrSqpCRYzVU1n8kj0tM=
265+golang.org/x/net v0.45.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
266 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
267 golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
268 golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
269@@ -1056,8 +1127,8 @@ golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
270 golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
271 golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
272 golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
273-golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
274-golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
275+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
276+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
277 golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
278 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
279 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
280@@ -1097,10 +1168,12 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
281 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
282 golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
283 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
284+golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
285 golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
286 golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
287 golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
288 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
289+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
290 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
291 golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
292 golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
293@@ -1115,8 +1188,8 @@ golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
294 golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
295 golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
296 golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
297-golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
298-golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
299+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
300+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
301 golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
302 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
303 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
304@@ -1128,8 +1201,8 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
305 golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
306 golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
307 golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
308-golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
309-golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
310+golang.org/x/term v0.36.0 h1:zMPR+aF8gfksFprF/Nc/rd1wRS1EI6nDBGyWAvDzx2Q=
311+golang.org/x/term v0.36.0/go.mod h1:Qu394IJq6V6dCBRgwqshf3mPF85AqzYEzofzRdZkWss=
312 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
313 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
314 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
315@@ -1144,8 +1217,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
316 golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
317 golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
318 golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
319-golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
320-golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
321+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
322+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
323 golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
324 golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
325 golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
326@@ -1167,8 +1240,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
327 golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
328 golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
329 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
330-golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
331-golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
332+golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
333+golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
334 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
335 golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
336 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
337@@ -1243,6 +1316,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
338 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
339 gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
340 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
341+gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
342+gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
343 grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
344 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
345 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+6,
-6
1@@ -85,7 +85,7 @@ func introspectHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
2 token := r.FormValue("token")
3 apiConfig.Cfg.Logger.Info("introspect token", "token", token)
4
5- user, err := apiConfig.Dbpool.FindUserForToken(token)
6+ user, err := apiConfig.Dbpool.FindUserByToken(token)
7 if err != nil {
8 apiConfig.Cfg.Logger.Error(err.Error())
9 http.Error(w, err.Error(), http.StatusUnauthorized)
10@@ -207,7 +207,7 @@ func tokenHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
11 "grantType", grantType,
12 )
13
14- _, err := apiConfig.Dbpool.FindUserForToken(token)
15+ _, err := apiConfig.Dbpool.FindUserByToken(token)
16 if err != nil {
17 apiConfig.Cfg.Logger.Error(err.Error())
18 http.Error(w, err.Error(), http.StatusUnauthorized)
19@@ -269,7 +269,7 @@ func keyHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
20 return
21 }
22
23- user, err := apiConfig.Dbpool.FindUserForKey(data.Username, authed.Pubkey)
24+ user, err := apiConfig.Dbpool.FindUserByKey(data.Username, authed.Pubkey)
25 if err != nil {
26 log.Error("find user for key", "err", err)
27 w.WriteHeader(http.StatusUnauthorized)
28@@ -337,7 +337,7 @@ func userHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
29 return
30 }
31
32- keys, err := apiConfig.Dbpool.FindKeysForUser(user)
33+ keys, err := apiConfig.Dbpool.FindKeysByUser(user)
34 if err != nil {
35 apiConfig.Cfg.Logger.Error(err.Error())
36 http.Error(w, err.Error(), http.StatusNotFound)
37@@ -357,7 +357,7 @@ func userHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
38 func rssHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
39 return func(w http.ResponseWriter, r *http.Request) {
40 apiToken := r.PathValue("token")
41- user, err := apiConfig.Dbpool.FindUserForToken(apiToken)
42+ user, err := apiConfig.Dbpool.FindUserByToken(apiToken)
43 if err != nil {
44 apiConfig.Cfg.Logger.Error(
45 "could not find user for token",
46@@ -545,7 +545,7 @@ func paymentWebhookHandler(apiConfig *shared.ApiConfig) http.HandlerFunc {
47
48 func AddPlusFeedForUser(dbpool db.DB, userID, email string) error {
49 // check if they already have a post grepping for the auth rss url
50- posts, err := dbpool.FindPostsForUser(&db.Pager{Num: 1000, Page: 0}, userID, "feeds")
51+ posts, err := dbpool.FindPostsByUser(&db.Pager{Num: 1000, Page: 0}, userID, "feeds")
52 if err != nil {
53 return err
54 }
+6,
-6
1@@ -53,7 +53,7 @@ func TestPaymentWebhook(t *testing.T) {
2
3 testResponse(t, responseRecorder, 200, "text/plain")
4
5- posts, err := apiConfig.Dbpool.FindPostsForUser(&db.Pager{Num: 1000, Page: 0}, testUserID, "feeds")
6+ posts, err := apiConfig.Dbpool.FindPostsByUser(&db.Pager{Num: 1000, Page: 0}, testUserID, "feeds")
7 if err != nil {
8 t.Error("could not find posts for user")
9 }
10@@ -224,22 +224,22 @@ func (a *AuthDb) FindUserByName(username string) (*db.User, error) {
11 return &db.User{ID: testUserID, Name: username}, nil
12 }
13
14-func (a *AuthDb) FindUserForKey(username string, pubkey string) (*db.User, error) {
15+func (a *AuthDb) FindUserByKey(username string, pubkey string) (*db.User, error) {
16 return &db.User{ID: testUserID, Name: username}, nil
17 }
18
19-func (a *AuthDb) FindUserForToken(token string) (*db.User, error) {
20+func (a *AuthDb) FindUserByToken(token string) (*db.User, error) {
21 if token != "123" {
22 return nil, fmt.Errorf("invalid token")
23 }
24 return &db.User{ID: testUserID, Name: testUsername}, nil
25 }
26
27-func (a *AuthDb) HasFeatureForUser(userID string, feature string) bool {
28+func (a *AuthDb) HasFeatureByUser(userID string, feature string) bool {
29 return true
30 }
31
32-func (a *AuthDb) FindKeysForUser(user *db.User) ([]*db.PublicKey, error) {
33+func (a *AuthDb) FindKeysByUser(user *db.User) ([]*db.PublicKey, error) {
34 return []*db.PublicKey{{ID: "1", UserID: user.ID, Name: "my-key", Key: "nice-pubkey", CreatedAt: &time.Time{}}}, nil
35 }
36
37@@ -254,7 +254,7 @@ func (a *AuthDb) InsertPost(post *db.Post) (*db.Post, error) {
38 return post, nil
39 }
40
41-func (a *AuthDb) FindPostsForUser(pager *db.Pager, userID, space string) (*db.Paginate[*db.Post], error) {
42+func (a *AuthDb) FindPostsByUser(pager *db.Pager, userID, space string) (*db.Paginate[*db.Post], error) {
43 return &db.Paginate[*db.Post]{
44 Data: a.Posts,
45 }, nil
+1,
-1
1@@ -64,7 +64,7 @@ func Middleware(dbpool db.DB, cfg *shared.ConfigSite) pssh.SSHServerMiddleware {
2 )
3 return writer.Flush()
4 case "ls":
5- posts, err := dbpool.FindPostsForUser(&db.Pager{Page: 0, Num: 1000}, user.ID, "feeds")
6+ posts, err := dbpool.FindPostsByUser(&db.Pager{Page: 0, Num: 1000}, user.ID, "feeds")
7 if err != nil {
8 _, _ = fmt.Fprintln(sesh.Stderr(), err)
9 return err
+1,
-1
1@@ -332,7 +332,7 @@ Also, we have centralized logs in our pico.sh TUI that will display realtime fee
2 func (f *Fetcher) RunUser(user *db.User, now time.Time) error {
3 logger := shared.LoggerWithUser(f.cfg.Logger, user)
4 logger.Info("run user")
5- posts, err := f.db.FindPostsForUser(&db.Pager{Num: 100}, user.ID, "feeds")
6+ posts, err := f.db.FindPostsByUser(&db.Pager{Num: 100}, user.ID, "feeds")
7 if err != nil {
8 return err
9 }
+1,
-1
1@@ -87,7 +87,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
2 }
3 logger = shared.LoggerWithUser(blogger, user)
4
5- pager, err := dbpool.FindPostsForUser(&db.Pager{Num: 1000, Page: 0}, user.ID, cfg.Space)
6+ pager, err := dbpool.FindPostsByUser(&db.Pager{Num: 1000, Page: 0}, user.ID, cfg.Space)
7 if err != nil {
8 logger.Error("could not find posts for user", "err", err.Error())
9 http.Error(w, "could not fetch posts for blog", http.StatusInternalServerError)
+1,
-1
1@@ -53,7 +53,7 @@ func (me *PgsPsqlDB) FindUserByPubkey(key string) (*db.User, error) {
2 if len(pk) == 0 {
3 return nil, fmt.Errorf("pubkey not found in our database: [%s]", key)
4 }
5- // When we run PublicKeyForKey and there are multiple public keys returned from the database
6+ // When we run PublicKeyByKey and there are multiple public keys returned from the database
7 // that should mean that we don't have the correct username for this public key.
8 // When that happens we need to reject the authentication and ask the user to provide the correct
9 // username when using ssh. So instead of `ssh <domain>` it should be `ssh user@<domain>`
+1,
-1
1@@ -24,7 +24,7 @@ func getUser(s *pssh.SSHServerConnSession, dbpool db.DB) (*db.User, error) {
2
3 key := utils.KeyForKeyText(s.PublicKey())
4
5- user, err := dbpool.FindUserForKey(s.User(), key)
6+ user, err := dbpool.FindUserByKey(s.User(), key)
7 if err != nil {
8 return nil, err
9 }
+4,
-4
1@@ -32,7 +32,7 @@ func NewUploadHandler(dbpool db.DB, cfg *shared.ConfigSite) *UploadHandler {
2 }
3
4 func (h *UploadHandler) getAuthorizedKeyFile(user *db.User) (*sendutils.VirtualFile, string, error) {
5- keys, err := h.DBPool.FindKeysForUser(user)
6+ keys, err := h.DBPool.FindKeysByUser(user)
7 text := ""
8 var modTime time.Time
9 for _, pk := range keys {
10@@ -137,7 +137,7 @@ func (h *UploadHandler) Validate(s *pssh.SSHServerConnSession) error {
11 return fmt.Errorf("key not found")
12 }
13
14- user, err := h.DBPool.FindUserForKey(s.User(), key)
15+ user, err := h.DBPool.FindUserByKey(s.User(), key)
16 if err != nil {
17 return err
18 }
19@@ -215,7 +215,7 @@ func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger,
20 logger.Info("processing new authorized_keys")
21 dbpool := h.DBPool
22
23- curKeysStr, err := dbpool.FindKeysForUser(user)
24+ curKeysStr, err := dbpool.FindKeysByUser(user)
25 if err != nil {
26 return err
27 }
28@@ -247,7 +247,7 @@ func (h *UploadHandler) ProcessAuthorizedKeys(text []byte, logger *slog.Logger,
29 _, _ = fmt.Fprintf(s.Stderr(), "adding pubkey (%s)\n", key)
30 logger.Info("adding pubkey", "pubkey", key)
31
32- err = dbpool.InsertPublicKey(user.ID, key, pk.Comment, nil)
33+ err = dbpool.InsertPublicKey(user.ID, key, pk.Comment)
34 if err != nil {
35 _, _ = fmt.Fprintf(s.Stderr(), "error: could not insert pubkey: %s (%s)\n", err.Error(), key)
36 logger.Error("could not insert pubkey", "err", err.Error())
+1,
-1
1@@ -173,7 +173,7 @@ func Middleware(handler *CliHandler) pssh.SSHServerMiddleware {
2 isAdmin := false
3 impersonate := false
4 if user != nil {
5- isAdmin = handler.DBPool.HasFeatureForUser(user.ID, "admin")
6+ isAdmin = handler.DBPool.HasFeatureByUser(user.ID, "admin")
7 if isAdmin && strings.HasPrefix(sesh.User(), "admin__") {
8 impersonate = true
9 }
+4,
-4
1@@ -171,7 +171,7 @@ func blogHandler(w http.ResponseWriter, r *http.Request) {
2 var posts []*db.Post
3 var p *db.Paginate[*db.Post]
4 if tag == "" {
5- p, err = dbpool.FindPostsForUser(pager, user.ID, cfg.Space)
6+ p, err = dbpool.FindPostsByUser(pager, user.ID, cfg.Space)
7 } else {
8 p, err = dbpool.FindUserPostsByTag(pager, tag, user.ID, cfg.Space)
9 }
10@@ -548,7 +548,7 @@ func readHandler(w http.ResponseWriter, r *http.Request) {
11 var pager *db.Paginate[*db.Post]
12 var err error
13 if tag == "" {
14- pager, err = dbpool.FindPostsForFeed(&db.Pager{Num: 30, Page: page}, cfg.Space)
15+ pager, err = dbpool.FindPostsByFeed(&db.Pager{Num: 30, Page: page}, cfg.Space)
16 } else {
17 pager, err = dbpool.FindPostsByTag(&db.Pager{Num: 30, Page: page}, tag, cfg.Space)
18 }
19@@ -639,7 +639,7 @@ func rssBlogHandler(w http.ResponseWriter, r *http.Request) {
20 var posts []*db.Post
21 var p *db.Paginate[*db.Post]
22 if tag == "" {
23- p, err = dbpool.FindPostsForUser(pager, user.ID, cfg.Space)
24+ p, err = dbpool.FindPostsByUser(pager, user.ID, cfg.Space)
25 } else {
26 p, err = dbpool.FindUserPostsByTag(pager, tag, user.ID, cfg.Space)
27 }
28@@ -762,7 +762,7 @@ func rssHandler(w http.ResponseWriter, r *http.Request) {
29 logger := shared.GetLogger(r)
30 cfg := shared.GetCfg(r)
31
32- pager, err := dbpool.FindPostsForFeed(&db.Pager{Num: 25, Page: 0}, cfg.Space)
33+ pager, err := dbpool.FindPostsByFeed(&db.Pager{Num: 25, Page: 0}, cfg.Space)
34 if err != nil {
35 logger.Error("find all posts", "err", err.Error())
36 http.Error(w, err.Error(), http.StatusInternalServerError)
+94,
-94
1@@ -127,29 +127,29 @@ func (p *FeedItemData) Scan(value any) error {
2 }
3
4 type Post struct {
5- ID string `json:"id"`
6- UserID string `json:"user_id"`
7- Filename string `json:"filename"`
8- Slug string `json:"slug"`
9- Title string `json:"title"`
10- Text string `json:"text"`
11- Description string `json:"description"`
12- CreatedAt *time.Time `json:"created_at"`
13- PublishAt *time.Time `json:"publish_at"`
14- Username string `json:"username"`
15- UpdatedAt *time.Time `json:"updated_at"`
16- ExpiresAt *time.Time `json:"expires_at"`
17- Hidden bool `json:"hidden"`
18- Views int `json:"views"`
19- Space string `json:"space"`
20- Shasum string `json:"shasum"`
21- FileSize int `json:"file_size"`
22- MimeType string `json:"mime_type"`
23- Data PostData `json:"data"`
24- Tags []string `json:"tags"`
25+ ID string `json:"id" db:"id"`
26+ UserID string `json:"user_id" db:"user_id"`
27+ Filename string `json:"filename" db:"filename"`
28+ Slug string `json:"slug" db:"slug"`
29+ Title string `json:"title" db:"title"`
30+ Text string `json:"text" db:"text"`
31+ Description string `json:"description" db:"description"`
32+ CreatedAt *time.Time `json:"created_at" db:"created_at"`
33+ PublishAt *time.Time `json:"publish_at" db:"publish_at"`
34+ Username string `json:"username" db:"name"`
35+ UpdatedAt *time.Time `json:"updated_at" db:"updated_at"`
36+ ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
37+ Hidden bool `json:"hidden" db:"hidden"`
38+ Views int `json:"views" db:"views"`
39+ Space string `json:"space" db:"cur_space"`
40+ Shasum string `json:"shasum" db:"shasum"`
41+ FileSize int `json:"file_size" db:"file_size"`
42+ MimeType string `json:"mime_type" db:"mime_type"`
43+ Data PostData `json:"data" db:"data"`
44+ Tags []string `json:"tags" db:"-"`
45
46 // computed
47- IsVirtual bool
48+ IsVirtual bool `db:"-"`
49 }
50
51 type Paginate[T any] struct {
52@@ -158,13 +158,13 @@ type Paginate[T any] struct {
53 }
54
55 type VisitInterval struct {
56- Interval *time.Time `json:"interval"`
57- Visitors int `json:"visitors"`
58+ Interval *time.Time `json:"interval" db:"interval"`
59+ Visitors int `json:"visitors" db:"visitors"`
60 }
61
62 type VisitUrl struct {
63- Url string `json:"url"`
64- Count int `json:"count"`
65+ Url string `json:"url" db:"url"`
66+ Count int `json:"count" db:"count"`
67 }
68
69 type SummaryOpts struct {
70@@ -184,34 +184,46 @@ type SummaryVisits struct {
71 }
72
73 type PostAnalytics struct {
74- ID string
75- PostID string
76- Views int
77- UpdateAt *time.Time
78+ ID string `json:"id" db:"id"`
79+ PostID string `json:"post_id" db:"post_id"`
80+ Views int `json:"views" db:"views"`
81+ UpdateAt *time.Time `json:"updated_at" db:"updated_at"`
82 }
83
84 type AnalyticsVisits struct {
85- ID string `json:"id"`
86- UserID string `json:"user_id"`
87- ProjectID string `json:"project_id"`
88- PostID string `json:"post_id"`
89- Namespace string `json:"namespace"`
90- Host string `json:"host"`
91- Path string `json:"path"`
92- IpAddress string `json:"ip_address"`
93- UserAgent string `json:"user_agent"`
94- Referer string `json:"referer"`
95- Status int `json:"status"`
96- ContentType string `json:"content_type"`
97+ ID string `json:"id" db:"id"`
98+ UserID string `json:"user_id" db:"user_id"`
99+ ProjectID string `json:"project_id" db:"project_id"`
100+ PostID string `json:"post_id" db:"post_id"`
101+ Namespace string `json:"namespace" db:"namespace"`
102+ Host string `json:"host" db:"host"`
103+ Path string `json:"path" db:"path"`
104+ IpAddress string `json:"ip_address" db:"ip_address"`
105+ UserAgent string `json:"user_agent" db:"user_agent"`
106+ Referer string `json:"referer" db:"referer"`
107+ Status int `json:"status" db:"status"`
108+ ContentType string `json:"content_type" db:"content_type"`
109+}
110+
111+type AccessLogData struct{}
112+
113+func (p *AccessLogData) Scan(value any) error {
114+ b, err := tcast(value)
115+ if err != nil {
116+ return err
117+ }
118+
119+ return json.Unmarshal(b, &p)
120 }
121
122 type AccessLog struct {
123- ID string `json:"id"`
124- UserID string `json:"user_id"`
125- Service string `json:"service"`
126- Pubkey string `json:"pubkey"`
127- Identity string `json:"identity"`
128- CreatedAt *time.Time `json:"created_at"`
129+ ID string `json:"id" db:"id"`
130+ UserID string `json:"user_id" db:"user_id"`
131+ Service string `json:"service" db:"service"`
132+ Pubkey string `json:"pubkey" db:"pubkey"`
133+ Identity string `json:"identity" db:"identity"`
134+ Data AccessLogData `json:"data" db:"data"`
135+ CreatedAt *time.Time `json:"created_at" db:"created_at"`
136 }
137
138 type Pager struct {
139@@ -220,19 +232,20 @@ type Pager struct {
140 }
141
142 type FeedItem struct {
143- ID string
144- PostID string
145- GUID string
146- Data FeedItemData
147- CreatedAt *time.Time
148+ ID string `json:"id" db:"id"`
149+ PostID string `json:"post_id" db:"post_id"`
150+ GUID string `json:"guid" db:"guid"`
151+ Data FeedItemData `json:"data" db:"data"`
152+ CreatedAt *time.Time `json:"created_at" db:"created_at"`
153 }
154
155 type Token struct {
156- ID string `json:"id"`
157- UserID string `json:"user_id"`
158- Name string `json:"name"`
159- CreatedAt *time.Time `json:"created_at"`
160- ExpiresAt *time.Time `json:"expires_at"`
161+ ID string `json:"id" db:"id"`
162+ UserID string `json:"user_id" db:"user_id"`
163+ Name string `json:"name" db:"name"`
164+ Token string `json:"token" db:"token"`
165+ CreatedAt *time.Time `json:"created_at" db:"created_at"`
166+ ExpiresAt *time.Time `json:"expires_at" db:"expires_at"`
167 }
168
169 type FeatureFlag struct {
170@@ -352,17 +365,17 @@ type UserServiceStats struct {
171 }
172
173 type TunsEventLog struct {
174- ID string `json:"id"`
175- ServerID string `json:"server_id"`
176- Time *time.Time `json:"time"`
177- User string `json:"user"`
178- UserId string `json:"user_id"`
179- RemoteAddr string `json:"remote_addr"`
180- EventType string `json:"event_type"`
181- TunnelID string `json:"tunnel_id"`
182- TunnelType string `json:"tunnel_type"`
183- ConnectionType string `json:"connection_type"`
184- CreatedAt *time.Time `json:"created_at"`
185+ ID string `json:"id" db:"id"`
186+ ServerID string `json:"server_id" db:"server_id"`
187+ Time *time.Time `json:"time" db:"time"`
188+ User string `json:"user" db:"user"`
189+ UserId string `json:"user_id" db:"user_id"`
190+ RemoteAddr string `json:"remote_addr" db:"remote_addr"`
191+ EventType string `json:"event_type" db:"event_type"`
192+ TunnelID string `json:"tunnel_id" db:"tunnel_id"`
193+ TunnelType string `json:"tunnel_type" db:"tunnel_type"`
194+ ConnectionType string `json:"connection_type" db:"connection_type"`
195+ CreatedAt *time.Time `json:"created_at" db:"created_at"`
196 }
197
198 var NameValidator = regexp.MustCompile("^[a-zA-Z0-9]{1,50}$")
199@@ -389,54 +402,41 @@ var DenyList = []string{
200
201 type DB interface {
202 RegisterUser(name, pubkey, comment string) (*User, error)
203- RemoveUsers(userIDs []string) error
204 UpdatePublicKey(pubkeyID, name string) (*PublicKey, error)
205- InsertPublicKey(userID, pubkey, name string, tx *sql.Tx) error
206- FindPublicKeyForKey(pubkey string) (*PublicKey, error)
207- FindPublicKey(pubkeyID string) (*PublicKey, error)
208- FindKeysForUser(user *User) ([]*PublicKey, error)
209+ InsertPublicKey(userID, pubkey, name string) error
210+ FindKeysByUser(user *User) ([]*PublicKey, error)
211 RemoveKeys(pubkeyIDs []string) error
212
213 FindUsers() ([]*User, error)
214 FindUserByName(name string) (*User, error)
215- FindUserForNameAndKey(name string, pubkey string) (*User, error)
216- FindUserForKey(name string, pubkey string) (*User, error)
217+ FindUserByKey(name string, pubkey string) (*User, error)
218 FindUserByPubkey(pubkey string) (*User, error)
219 FindUser(userID string) (*User, error)
220- ValidateName(name string) (bool, error)
221- SetUserName(userID string, name string) error
222
223- FindUserForToken(token string) (*User, error)
224- FindTokensForUser(userID string) ([]*Token, error)
225+ FindUserByToken(token string) (*User, error)
226+ FindTokensByUser(userID string) ([]*Token, error)
227 InsertToken(userID, name string) (string, error)
228 UpsertToken(userID, name string) (string, error)
229- FindTokenByName(userID, name string) (string, error)
230 RemoveToken(tokenID string) error
231
232 FindPosts() ([]*Post, error)
233 FindPost(postID string) (*Post, error)
234- FindPostsForUser(pager *Pager, userID string, space string) (*Paginate[*Post], error)
235- FindAllPostsForUser(userID string, space string) ([]*Post, error)
236+ FindPostsByUser(pager *Pager, userID string, space string) (*Paginate[*Post], error)
237+ FindAllPostsByUser(userID string, space string) ([]*Post, error)
238 FindUsersWithPost(space string) ([]*User, error)
239- FindPostsBeforeDate(date *time.Time, space string) ([]*Post, error)
240 FindExpiredPosts(space string) ([]*Post, error)
241- FindUpdatedPostsForUser(userID string, space string) ([]*Post, error)
242 FindPostWithFilename(filename string, userID string, space string) (*Post, error)
243 FindPostWithSlug(slug string, userID string, space string) (*Post, error)
244- FindPostsForFeed(pager *Pager, space string) (*Paginate[*Post], error)
245- FindAllUpdatedPosts(pager *Pager, space string) (*Paginate[*Post], error)
246+ FindPostsByFeed(pager *Pager, space string) (*Paginate[*Post], error)
247 InsertPost(post *Post) (*Post, error)
248 UpdatePost(post *Post) (*Post, error)
249 RemovePosts(postIDs []string) error
250
251- ReplaceTagsForPost(tags []string, postID string) error
252+ ReplaceTagsByPost(tags []string, postID string) error
253 FindUserPostsByTag(pager *Pager, tag, userID, space string) (*Paginate[*Post], error)
254 FindPostsByTag(pager *Pager, tag, space string) (*Paginate[*Post], error)
255 FindPopularTags(space string) ([]string, error)
256- FindTagsForPost(postID string) ([]string, error)
257- FindTagsForUser(userID string, space string) ([]string, error)
258-
259- ReplaceAliasesForPost(aliases []string, postID string) error
260+ ReplaceAliasesByPost(aliases []string, postID string) error
261
262 InsertVisit(view *AnalyticsVisits) error
263 VisitSummary(opts *SummaryOpts) (*SummaryVisits, error)
264@@ -445,9 +445,9 @@ type DB interface {
265
266 AddPicoPlusUser(username, email, paymentType, txId string) error
267 FindFeature(userID string, feature string) (*FeatureFlag, error)
268- FindFeaturesForUser(userID string) ([]*FeatureFlag, error)
269- HasFeatureForUser(userID string, feature string) bool
270- FindTotalSizeForUser(userID string) (int, error)
271+ FindFeaturesByUser(userID string) ([]*FeatureFlag, error)
272+ HasFeatureByUser(userID string, feature string) bool
273+
274 InsertFeature(userID, name string, expiresAt time.Time) (*FeatureFlag, error)
275 RemoveFeature(userID, names string) error
276
+299,
-813
1@@ -1,7 +1,6 @@
2 package postgres
3
4 import (
5- "context"
6 "database/sql"
7 "errors"
8 "fmt"
9@@ -12,6 +11,7 @@ import (
10
11 "slices"
12
13+ "github.com/jmoiron/sqlx"
14 _ "github.com/lib/pq"
15 "github.com/picosh/pico/pkg/db"
16 "github.com/picosh/utils"
17@@ -23,275 +23,16 @@ var SelectPost = `
18 posts.id, user_id, app_users.name, filename, slug, title, text, description,
19 posts.created_at, publish_at, posts.updated_at, hidden, file_size, mime_type, shasum, data, expires_at, views`
20
21-var (
22- sqlSelectPosts = fmt.Sprintf(`
23- SELECT %s
24- FROM posts
25- LEFT JOIN app_users ON app_users.id = posts.user_id`, SelectPost)
26-
27- sqlSelectPostsBeforeDate = fmt.Sprintf(`
28- SELECT %s
29- FROM posts
30- LEFT JOIN app_users ON app_users.id = posts.user_id
31- WHERE publish_at::date <= $1 AND cur_space = $2`, SelectPost)
32-
33- sqlSelectPostWithFilename = fmt.Sprintf(`
34- SELECT %s, STRING_AGG(coalesce(post_tags.name, ''), ',') tags
35- FROM posts
36- LEFT JOIN app_users ON app_users.id = posts.user_id
37- LEFT JOIN post_tags ON post_tags.post_id = posts.id
38- WHERE filename = $1 AND user_id = $2 AND cur_space = $3
39- GROUP BY %s`, SelectPost, SelectPost)
40-
41- sqlSelectPostWithSlug = fmt.Sprintf(`
42- SELECT %s, STRING_AGG(coalesce(post_tags.name, ''), ',') tags
43- FROM posts
44- LEFT JOIN app_users ON app_users.id = posts.user_id
45- LEFT JOIN post_tags ON post_tags.post_id = posts.id
46- WHERE slug = $1 AND user_id = $2 AND cur_space = $3
47- GROUP BY %s`, SelectPost, SelectPost)
48-
49- sqlSelectPost = fmt.Sprintf(`
50- SELECT %s
51- FROM posts
52- LEFT JOIN app_users ON app_users.id = posts.user_id
53- WHERE posts.id = $1`, SelectPost)
54-
55- sqlSelectUpdatedPostsForUser = fmt.Sprintf(`
56- SELECT %s
57- FROM posts
58- LEFT JOIN app_users ON app_users.id = posts.user_id
59- WHERE user_id = $1 AND publish_at::date <= CURRENT_DATE AND cur_space = $2
60- ORDER BY posts.updated_at DESC`, SelectPost)
61-
62- sqlSelectExpiredPosts = fmt.Sprintf(`
63- SELECT %s
64- FROM posts
65- LEFT JOIN app_users ON app_users.id = posts.user_id
66- WHERE
67- cur_space = $1 AND
68- expires_at <= now();
69- `, SelectPost)
70-
71- sqlSelectPostsForUser = fmt.Sprintf(`
72- SELECT %s, STRING_AGG(coalesce(post_tags.name, ''), ',') tags
73- FROM posts
74- LEFT JOIN app_users ON app_users.id = posts.user_id
75- LEFT JOIN post_tags ON post_tags.post_id = posts.id
76- WHERE
77- hidden = FALSE AND
78- user_id = $1 AND
79- publish_at::date <= CURRENT_DATE AND
80- cur_space = $2
81- GROUP BY %s
82- ORDER BY publish_at DESC, slug DESC
83- LIMIT $3 OFFSET $4`, SelectPost, SelectPost)
84-
85- sqlSelectAllPostsForUser = fmt.Sprintf(`
86- SELECT %s
87- FROM posts
88- LEFT JOIN app_users ON app_users.id = posts.user_id
89- WHERE
90- user_id = $1 AND
91- cur_space = $2
92- ORDER BY publish_at DESC`, SelectPost)
93-
94- sqlSelectPostsByTag = `
95- SELECT
96- posts.id,
97- user_id,
98- filename,
99- slug,
100- title,
101- text,
102- description,
103- publish_at,
104- app_users.name as username,
105- posts.updated_at,
106- posts.mime_type
107- FROM posts
108- LEFT JOIN app_users ON app_users.id = posts.user_id
109- LEFT JOIN post_tags ON post_tags.post_id = posts.id
110- WHERE
111- post_tags.name = $3 AND
112- publish_at::date <= CURRENT_DATE AND
113- cur_space = $4
114- ORDER BY publish_at DESC
115- LIMIT $1 OFFSET $2`
116-
117- sqlSelectUserPostsByTag = fmt.Sprintf(`
118- SELECT %s
119- FROM posts
120- LEFT JOIN app_users ON app_users.id = posts.user_id
121- LEFT JOIN post_tags ON post_tags.post_id = posts.id
122- WHERE
123- hidden = FALSE AND
124- user_id = $1 AND
125- (post_tags.name = $2 OR hidden = true) AND
126- publish_at::date <= CURRENT_DATE AND
127- cur_space = $3
128- ORDER BY publish_at DESC
129- LIMIT $4 OFFSET $5`, SelectPost)
130-)
131-
132-const (
133- sqlSelectPublicKey = `SELECT id, user_id, name, public_key, created_at FROM public_keys WHERE public_key = $1`
134- sqlSelectPublicKeys = `SELECT id, user_id, name, public_key, created_at FROM public_keys WHERE user_id = $1 ORDER BY created_at ASC`
135- sqlSelectUser = `SELECT id, name, created_at FROM app_users WHERE id = $1`
136- sqlSelectUserForName = `SELECT id, name, created_at FROM app_users WHERE name = $1`
137- sqlSelectUserForNameAndKey = `SELECT app_users.id, app_users.name, app_users.created_at, public_keys.id as pk_id, public_keys.public_key, public_keys.created_at as pk_created_at FROM app_users LEFT JOIN public_keys ON public_keys.user_id = app_users.id WHERE app_users.name = $1 AND public_keys.public_key = $2`
138- sqlSelectUsers = `SELECT id, name, created_at FROM app_users ORDER BY name ASC`
139-
140- sqlSelectUserForToken = `
141- SELECT app_users.id, app_users.name, app_users.created_at
142- FROM app_users
143- LEFT JOIN tokens ON tokens.user_id = app_users.id
144- WHERE tokens.token = $1 AND tokens.expires_at > NOW()`
145- sqlInsertToken = `INSERT INTO tokens (user_id, name) VALUES($1, $2) RETURNING token;`
146- sqlRemoveToken = `DELETE FROM tokens WHERE id = $1`
147- sqlSelectTokensForUser = `SELECT id, user_id, name, created_at, expires_at FROM tokens WHERE user_id = $1`
148- sqlSelectTokenByNameForUser = `SELECT token FROM tokens WHERE user_id = $1 AND name = $2`
149-
150- sqlSelectFeatureForUser = `SELECT id, user_id, payment_history_id, name, data, created_at, expires_at FROM feature_flags WHERE user_id = $1 AND name = $2 ORDER BY expires_at DESC LIMIT 1`
151- sqlSelectSizeForUser = `SELECT COALESCE(sum(file_size), 0) FROM posts WHERE user_id = $1`
152-
153- sqlSelectPostIdByAliasSlug = `SELECT post_id FROM post_aliases WHERE slug = $1`
154- sqlSelectTagPostCount = `
155- SELECT count(posts.id)
156- FROM posts
157- LEFT JOIN post_tags ON post_tags.post_id = posts.id
158- WHERE hidden = FALSE AND cur_space=$1 and post_tags.name = $2`
159- sqlSelectPostCount = `SELECT count(id) FROM posts WHERE hidden = FALSE AND cur_space=$1`
160- sqlSelectAllUpdatedPosts = `
161- SELECT
162- posts.id,
163- user_id,
164- filename,
165- slug,
166- title,
167- text,
168- description,
169- publish_at,
170- app_users.name as username,
171- posts.updated_at,
172- posts.mime_type
173- FROM posts
174- LEFT JOIN app_users ON app_users.id = posts.user_id
175- WHERE hidden = FALSE AND publish_at::date <= CURRENT_DATE AND cur_space = $3
176- ORDER BY updated_at DESC
177- LIMIT $1 OFFSET $2`
178- // add some users to deny list since they are robogenerating a bunch of posts
179- // per day and are creating a lot of noise.
180- sqlSelectPostsByRank = `
181- SELECT *
182- FROM (
183- SELECT DISTINCT ON (posts.user_id)
184- posts.id,
185- posts.user_id,
186- posts.filename,
187- posts.slug,
188- posts.title,
189- posts.text,
190- posts.description,
191- posts.publish_at,
192- app_users.name AS username,
193- posts.updated_at,
194- posts.mime_type
195- FROM posts
196- LEFT JOIN app_users ON app_users.id = posts.user_id
197- WHERE
198- hidden = FALSE
199- AND publish_at::date <= CURRENT_DATE
200- AND cur_space = $3
201- ORDER BY posts.user_id, publish_at DESC
202- ) AS latest_posts
203- ORDER BY publish_at DESC
204- LIMIT $1 OFFSET $2`
205-
206- sqlSelectPopularTags = `
207- SELECT name, count(post_id) as "tally"
208- FROM post_tags
209- LEFT JOIN posts ON posts.id = post_id
210- WHERE posts.cur_space = $1
211- GROUP BY name
212- ORDER BY tally DESC
213- LIMIT 5`
214- sqlSelectTagsForUser = `
215- SELECT name
216- FROM post_tags
217- LEFT JOIN posts ON posts.id = post_id
218- WHERE posts.user_id = $1 AND posts.cur_space = $2
219- GROUP BY name`
220- sqlSelectTagsForPost = `SELECT name FROM post_tags WHERE post_id=$1`
221- sqlSelectFeedItemsByPost = `SELECT id, post_id, guid, data, created_at FROM feed_items WHERE post_id=$1`
222-
223- sqlInsertPost = `
224- INSERT INTO posts
225- (user_id, filename, slug, title, text, description, publish_at, hidden, cur_space,
226- file_size, mime_type, shasum, data, expires_at, updated_at)
227- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
228- RETURNING id`
229- sqlInsertUser = `INSERT INTO app_users (name) VALUES($1) returning id`
230- sqlInsertTag = `INSERT INTO post_tags (post_id, name) VALUES($1, $2) RETURNING id;`
231- sqlInsertAliases = `INSERT INTO post_aliases (post_id, slug) VALUES($1, $2) RETURNING id;`
232- sqlInsertFeedItems = `INSERT INTO feed_items (post_id, guid, data) VALUES ($1, $2, $3) RETURNING id;`
233-
234- sqlUpdatePost = `
235- UPDATE posts
236- SET slug = $1, title = $2, text = $3, description = $4, updated_at = $5, publish_at = $6,
237- file_size = $7, shasum = $8, data = $9, hidden = $11, expires_at = $12
238- WHERE id = $10`
239- sqlUpdateUserName = `UPDATE app_users SET name = $1 WHERE id = $2`
240-
241- sqlRemoveAliasesByPost = `DELETE FROM post_aliases WHERE post_id = $1`
242- sqlRemoveTagsByPost = `DELETE FROM post_tags WHERE post_id = $1`
243- sqlRemovePosts = `DELETE FROM posts WHERE id = ANY($1::uuid[])`
244- sqlRemoveKeys = `DELETE FROM public_keys WHERE id = ANY($1::uuid[])`
245- sqlRemoveUsers = `DELETE FROM app_users WHERE id = ANY($1::uuid[])`
246-
247- sqlInsertProject = `INSERT INTO projects (user_id, name, project_dir) VALUES ($1, $2, $3) RETURNING id;`
248- sqlUpdateProject = `UPDATE projects SET updated_at = $3 WHERE user_id = $1 AND name = $2;`
249- sqlFindProjectByName = `SELECT id, user_id, name, project_dir, acl, blocked, created_at, updated_at FROM projects WHERE user_id = $1 AND name = $2;`
250-)
251-
252 type PsqlDB struct {
253 Logger *slog.Logger
254- Db *sql.DB
255+ Db *sqlx.DB
256 }
257
258 type RowScanner interface {
259 Scan(dest ...any) error
260 }
261
262-func CreatePostFromRow(r RowScanner) (*db.Post, error) {
263- post := &db.Post{}
264- err := r.Scan(
265- &post.ID,
266- &post.UserID,
267- &post.Username,
268- &post.Filename,
269- &post.Slug,
270- &post.Title,
271- &post.Text,
272- &post.Description,
273- &post.CreatedAt,
274- &post.PublishAt,
275- &post.UpdatedAt,
276- &post.Hidden,
277- &post.FileSize,
278- &post.MimeType,
279- &post.Shasum,
280- &post.Data,
281- &post.ExpiresAt,
282- &post.Views,
283- )
284- if err != nil {
285- return nil, err
286- }
287- return post, nil
288-}
289-
290-func CreatePostWithTagsFromRow(r RowScanner) (*db.Post, error) {
291+func CreatePostWithTagsByRow(r RowScanner) (*db.Post, error) {
292 post := &db.Post{}
293 tagStr := ""
294 err := r.Scan(
295@@ -338,7 +79,7 @@ func NewDB(databaseUrl string, logger *slog.Logger) *PsqlDB {
296 }
297 d.Logger.Info("Connecting to postgres", "databaseUrl", databaseUrl)
298
299- db, err := sql.Open("postgres", databaseUrl)
300+ db, err := sqlx.Connect("postgres", databaseUrl)
301 if err != nil {
302 d.Logger.Error(err.Error())
303 }
304@@ -348,35 +89,26 @@ func NewDB(databaseUrl string, logger *slog.Logger) *PsqlDB {
305
306 func (me *PsqlDB) RegisterUser(username, pubkey, comment string) (*db.User, error) {
307 lowerName := strings.ToLower(username)
308- valid, err := me.ValidateName(lowerName)
309+ valid, err := me.validateName(lowerName)
310 if !valid {
311 return nil, err
312 }
313
314- ctx := context.Background()
315- tx, err := me.Db.BeginTx(ctx, nil)
316- if err != nil {
317- return nil, err
318- }
319- defer func() {
320- err = tx.Rollback()
321- }()
322-
323- stmt, err := tx.Prepare(sqlInsertUser)
324+ tx, err := me.Db.Beginx()
325 if err != nil {
326 return nil, err
327 }
328 defer func() {
329- _ = stmt.Close()
330+ _ = tx.Rollback()
331 }()
332
333 var id string
334- err = stmt.QueryRow(lowerName).Scan(&id)
335+ err = tx.QueryRow(`INSERT INTO app_users (name) VALUES($1) returning id`, lowerName).Scan(&id)
336 if err != nil {
337 return nil, err
338 }
339
340- err = me.InsertPublicKey(id, pubkey, comment, tx)
341+ err = me.insertPublicKeyWithTx(id, pubkey, comment, tx)
342 if err != nil {
343 return nil, err
344 }
345@@ -386,36 +118,31 @@ func (me *PsqlDB) RegisterUser(username, pubkey, comment string) (*db.User, erro
346 return nil, err
347 }
348
349- return me.FindUserForKey(username, pubkey)
350+ return me.FindUserByKey(username, pubkey)
351 }
352
353-func (me *PsqlDB) RemoveUsers(userIDs []string) error {
354- param := "{" + strings.Join(userIDs, ",") + "}"
355- _, err := me.Db.Exec(sqlRemoveUsers, param)
356+func (me *PsqlDB) insertPublicKeyWithTx(userID, key, name string, tx *sqlx.Tx) error {
357+ pk, _ := me.findPublicKeyByKey(key)
358+ if pk != nil {
359+ return db.ErrPublicKeyTaken
360+ }
361+ query := `INSERT INTO public_keys (user_id, public_key, name) VALUES ($1, $2, $3)`
362+ _, err := tx.Exec(query, userID, key, name)
363 return err
364 }
365
366-func (me *PsqlDB) InsertPublicKey(userID, key, name string, tx *sql.Tx) error {
367- pk, _ := me.FindPublicKeyForKey(key)
368+func (me *PsqlDB) InsertPublicKey(userID, key, name string) error {
369+ pk, _ := me.findPublicKeyByKey(key)
370 if pk != nil {
371 return db.ErrPublicKeyTaken
372 }
373 query := `INSERT INTO public_keys (user_id, public_key, name) VALUES ($1, $2, $3)`
374- var err error
375- if tx != nil {
376- _, err = tx.Exec(query, userID, key, name)
377- } else {
378- _, err = me.Db.Exec(query, userID, key, name)
379- }
380- if err != nil {
381- return err
382- }
383-
384- return nil
385+ _, err := me.Db.Exec(query, userID, key, name)
386+ return err
387 }
388
389 func (me *PsqlDB) UpdatePublicKey(pubkeyID, name string) (*db.PublicKey, error) {
390- pk, err := me.FindPublicKey(pubkeyID)
391+ pk, err := me.findPublicKey(pubkeyID)
392 if err != nil {
393 return nil, err
394 }
395@@ -426,19 +153,20 @@ func (me *PsqlDB) UpdatePublicKey(pubkeyID, name string) (*db.PublicKey, error)
396 return nil, err
397 }
398
399- pk, err = me.FindPublicKey(pubkeyID)
400+ pk, err = me.findPublicKey(pubkeyID)
401 if err != nil {
402 return nil, err
403 }
404 return pk, nil
405 }
406
407-func (me *PsqlDB) FindPublicKeyForKey(key string) (*db.PublicKey, error) {
408+func (me *PsqlDB) findPublicKeyByKey(key string) (*db.PublicKey, error) {
409 var keys []*db.PublicKey
410- rs, err := me.Db.Query(sqlSelectPublicKey, key)
411+ rs, err := me.Db.Queryx(`SELECT id, user_id, name, public_key, created_at FROM public_keys WHERE public_key = $1`, key)
412 if err != nil {
413 return nil, err
414 }
415+ defer func() { _ = rs.Close() }()
416
417 for rs.Next() {
418 pk := &db.PublicKey{}
419@@ -458,7 +186,7 @@ func (me *PsqlDB) FindPublicKeyForKey(key string) (*db.PublicKey, error) {
420 return nil, fmt.Errorf("pubkey not found in our database: [%s]", key)
421 }
422
423- // When we run PublicKeyForKey and there are multiple public keys returned from the database
424+ // When we run PublicKeyByKey and there are multiple public keys returned from the database
425 // that should mean that we don't have the correct username for this public key.
426 // When that happens we need to reject the authentication and ask the user to provide the correct
427 // username when using ssh. So instead of `ssh <domain>` it should be `ssh user@<domain>`
428@@ -469,86 +197,33 @@ func (me *PsqlDB) FindPublicKeyForKey(key string) (*db.PublicKey, error) {
429 return keys[0], nil
430 }
431
432-func (me *PsqlDB) FindPublicKey(pubkeyID string) (*db.PublicKey, error) {
433- var keys []*db.PublicKey
434- rs, err := me.Db.Query(`SELECT id, user_id, name, public_key, created_at FROM public_keys WHERE id = $1`, pubkeyID)
435+func (me *PsqlDB) findPublicKey(pubkeyID string) (*db.PublicKey, error) {
436+ pk := &db.PublicKey{}
437+ err := me.Db.Get(pk, `SELECT * FROM public_keys WHERE id = $1`, pubkeyID)
438 if err != nil {
439 return nil, err
440 }
441-
442- for rs.Next() {
443- pk := &db.PublicKey{}
444- err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt)
445- if err != nil {
446- return nil, err
447- }
448-
449- keys = append(keys, pk)
450- }
451-
452- if rs.Err() != nil {
453- return nil, rs.Err()
454- }
455-
456- if len(keys) == 0 {
457- return nil, errors.New("no public keys found for key provided")
458- }
459-
460- return keys[0], nil
461+ return pk, nil
462 }
463
464-func (me *PsqlDB) FindKeysForUser(user *db.User) ([]*db.PublicKey, error) {
465+func (me *PsqlDB) FindKeysByUser(user *db.User) ([]*db.PublicKey, error) {
466 var keys []*db.PublicKey
467- rs, err := me.Db.Query(sqlSelectPublicKeys, user.ID)
468+ err := me.Db.Select(&keys, `SELECT * FROM public_keys WHERE user_id = $1 ORDER BY created_at ASC`, user.ID)
469 if err != nil {
470- return keys, err
471- }
472- for rs.Next() {
473- pk := &db.PublicKey{}
474- err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.Key, &pk.CreatedAt)
475- if err != nil {
476- return keys, err
477- }
478-
479- keys = append(keys, pk)
480- }
481- if rs.Err() != nil {
482- return keys, rs.Err()
483+ return nil, err
484 }
485 return keys, nil
486 }
487
488 func (me *PsqlDB) RemoveKeys(keyIDs []string) error {
489 param := "{" + strings.Join(keyIDs, ",") + "}"
490- _, err := me.Db.Exec(sqlRemoveKeys, param)
491+ _, err := me.Db.Exec(`DELETE FROM public_keys WHERE id = ANY($1::uuid[])`, param)
492 return err
493 }
494
495-func (me *PsqlDB) FindPostsBeforeDate(date *time.Time, space string) ([]*db.Post, error) {
496- // now := time.Now()
497- // expired := now.AddDate(0, 0, -3)
498- var posts []*db.Post
499- rs, err := me.Db.Query(sqlSelectPostsBeforeDate, date, space)
500- if err != nil {
501- return posts, err
502- }
503- for rs.Next() {
504- post, err := CreatePostFromRow(rs)
505- if err != nil {
506- return nil, err
507- }
508-
509- posts = append(posts, post)
510- }
511- if rs.Err() != nil {
512- return posts, rs.Err()
513- }
514- return posts, nil
515-}
516-
517 func (me *PsqlDB) FindUsersWithPost(space string) ([]*db.User, error) {
518 var users []*db.User
519- rs, err := me.Db.Query(
520+ rs, err := me.Db.Queryx(
521 `SELECT u.id, u.name, u.created_at
522 FROM app_users u
523 INNER JOIN posts ON u.id=posts.user_id
524@@ -559,6 +234,7 @@ func (me *PsqlDB) FindUsersWithPost(space string) ([]*db.User, error) {
525 if err != nil {
526 return users, err
527 }
528+ defer func() { _ = rs.Close() }()
529 for rs.Next() {
530 var name sql.NullString
531 user := &db.User{}
532@@ -580,9 +256,9 @@ func (me *PsqlDB) FindUsersWithPost(space string) ([]*db.User, error) {
533 return users, nil
534 }
535
536-func (me *PsqlDB) FindUserForKey(username string, key string) (*db.User, error) {
537+func (me *PsqlDB) FindUserByKey(username string, key string) (*db.User, error) {
538 me.Logger.Info("attempting to find user with only public key", "key", key)
539- pk, err := me.FindPublicKeyForKey(key)
540+ pk, err := me.findPublicKeyByKey(key)
541 if err == nil {
542 me.Logger.Info("found pubkey, looking for user", "key", key, "userId", pk.UserID)
543 user, err := me.FindUser(pk.UserID)
544@@ -595,7 +271,7 @@ func (me *PsqlDB) FindUserForKey(username string, key string) (*db.User, error)
545
546 if errors.Is(err, &db.ErrMultiplePublicKeys{}) {
547 me.Logger.Info("detected multiple users with same public key", "user", username)
548- user, err := me.FindUserForNameAndKey(username, key)
549+ user, err := me.findUserForNameAndKey(username, key)
550 if err != nil {
551 me.Logger.Info("could not find user by username and public key", "user", username, "key", key)
552 // this is a little hacky but if we cannot find a user by name and public key
553@@ -611,7 +287,7 @@ func (me *PsqlDB) FindUserForKey(username string, key string) (*db.User, error)
554
555 func (me *PsqlDB) FindUserByPubkey(key string) (*db.User, error) {
556 me.Logger.Info("attempting to find user with only public key", "key", key)
557- pk, err := me.FindPublicKeyForKey(key)
558+ pk, err := me.findPublicKeyByKey(key)
559 if err != nil {
560 return nil, err
561 }
562@@ -627,19 +303,14 @@ func (me *PsqlDB) FindUserByPubkey(key string) (*db.User, error) {
563
564 func (me *PsqlDB) FindUser(userID string) (*db.User, error) {
565 user := &db.User{}
566- var un sql.NullString
567- r := me.Db.QueryRow(sqlSelectUser, userID)
568- err := r.Scan(&user.ID, &un, &user.CreatedAt)
569+ err := me.Db.Get(user, `SELECT id, COALESCE(name, '') as name, created_at FROM app_users WHERE id = $1`, userID)
570 if err != nil {
571 return nil, err
572 }
573- if un.Valid {
574- user.Name = un.String
575- }
576 return user, nil
577 }
578
579-func (me *PsqlDB) ValidateName(name string) (bool, error) {
580+func (me *PsqlDB) validateName(name string) (bool, error) {
581 lower := strings.ToLower(name)
582 if slices.Contains(db.DenyList, lower) {
583 return false, fmt.Errorf("%s is on deny list: %w", lower, db.ErrNameDenied)
584@@ -657,19 +328,18 @@ func (me *PsqlDB) ValidateName(name string) (bool, error) {
585
586 func (me *PsqlDB) FindUserByName(name string) (*db.User, error) {
587 user := &db.User{}
588- r := me.Db.QueryRow(sqlSelectUserForName, strings.ToLower(name))
589- err := r.Scan(&user.ID, &user.Name, &user.CreatedAt)
590+ err := me.Db.Get(user, `SELECT * FROM app_users WHERE name = $1`, strings.ToLower(name))
591 if err != nil {
592 return nil, err
593 }
594 return user, nil
595 }
596
597-func (me *PsqlDB) FindUserForNameAndKey(name string, key string) (*db.User, error) {
598+func (me *PsqlDB) findUserForNameAndKey(name string, key string) (*db.User, error) {
599 user := &db.User{}
600 pk := &db.PublicKey{}
601
602- r := me.Db.QueryRow(sqlSelectUserForNameAndKey, strings.ToLower(name), key)
603+ r := me.Db.QueryRow(`SELECT app_users.id, app_users.name, app_users.created_at, public_keys.id as pk_id, public_keys.public_key, public_keys.created_at as pk_created_at FROM app_users LEFT JOIN public_keys ON public_keys.user_id = app_users.id WHERE app_users.name = $1 AND public_keys.public_key = $2`, strings.ToLower(name), key)
604 err := r.Scan(&user.ID, &user.Name, &user.CreatedAt, &pk.ID, &pk.Key, &pk.CreatedAt)
605 if err != nil {
606 return nil, err
607@@ -679,32 +349,29 @@ func (me *PsqlDB) FindUserForNameAndKey(name string, key string) (*db.User, erro
608 return user, nil
609 }
610
611-func (me *PsqlDB) FindUserForToken(token string) (*db.User, error) {
612+func (me *PsqlDB) FindUserByToken(token string) (*db.User, error) {
613 user := &db.User{}
614-
615- r := me.Db.QueryRow(sqlSelectUserForToken, token)
616- err := r.Scan(&user.ID, &user.Name, &user.CreatedAt)
617+ err := me.Db.Get(user, `
618+ SELECT app_users.id, app_users.name, app_users.created_at
619+ FROM app_users
620+ LEFT JOIN tokens ON tokens.user_id = app_users.id
621+ WHERE tokens.token = $1 AND tokens.expires_at > NOW()`, token)
622 if err != nil {
623 return nil, err
624 }
625-
626 return user, nil
627 }
628
629-func (me *PsqlDB) SetUserName(userID string, name string) error {
630- lowerName := strings.ToLower(name)
631- valid, err := me.ValidateName(lowerName)
632- if !valid {
633- return err
634- }
635-
636- _, err = me.Db.Exec(sqlUpdateUserName, lowerName, userID)
637- return err
638-}
639-
640 func (me *PsqlDB) FindPostWithFilename(filename string, persona_id string, space string) (*db.Post, error) {
641- r := me.Db.QueryRow(sqlSelectPostWithFilename, filename, persona_id, space)
642- post, err := CreatePostWithTagsFromRow(r)
643+ query := fmt.Sprintf(`
644+ SELECT %s, STRING_AGG(coalesce(post_tags.name, ''), ',') tags
645+ FROM posts
646+ LEFT JOIN app_users ON app_users.id = posts.user_id
647+ LEFT JOIN post_tags ON post_tags.post_id = posts.id
648+ WHERE filename = $1 AND user_id = $2 AND cur_space = $3
649+ GROUP BY %s`, SelectPost, SelectPost)
650+ r := me.Db.QueryRow(query, filename, persona_id, space)
651+ post, err := CreatePostWithTagsByRow(r)
652 if err != nil {
653 return nil, err
654 }
655@@ -713,11 +380,18 @@ func (me *PsqlDB) FindPostWithFilename(filename string, persona_id string, space
656 }
657
658 func (me *PsqlDB) FindPostWithSlug(slug string, user_id string, space string) (*db.Post, error) {
659- r := me.Db.QueryRow(sqlSelectPostWithSlug, slug, user_id, space)
660- post, err := CreatePostWithTagsFromRow(r)
661+ query := fmt.Sprintf(`
662+ SELECT %s, STRING_AGG(coalesce(post_tags.name, ''), ',') tags
663+ FROM posts
664+ LEFT JOIN app_users ON app_users.id = posts.user_id
665+ LEFT JOIN post_tags ON post_tags.post_id = posts.id
666+ WHERE slug = $1 AND user_id = $2 AND cur_space = $3
667+ GROUP BY %s`, SelectPost, SelectPost)
668+ r := me.Db.QueryRow(query, slug, user_id, space)
669+ post, err := CreatePostWithTagsByRow(r)
670 if err != nil {
671 // attempt to find post inside post_aliases
672- alias := me.Db.QueryRow(sqlSelectPostIdByAliasSlug, slug)
673+ alias := me.Db.QueryRow(`SELECT post_id FROM post_aliases WHERE slug = $1`, slug)
674 postID := ""
675 err := alias.Scan(&postID)
676 if err != nil {
677@@ -731,16 +405,20 @@ func (me *PsqlDB) FindPostWithSlug(slug string, user_id string, space string) (*
678 }
679
680 func (me *PsqlDB) FindPost(postID string) (*db.Post, error) {
681- r := me.Db.QueryRow(sqlSelectPost, postID)
682- post, err := CreatePostFromRow(r)
683+ post := &db.Post{}
684+ query := fmt.Sprintf(`
685+ SELECT %s
686+ FROM posts
687+ LEFT JOIN app_users ON app_users.id = posts.user_id
688+ WHERE posts.id = $1`, SelectPost)
689+ err := me.Db.Get(post, query, postID)
690 if err != nil {
691 return nil, err
692 }
693-
694 return post, nil
695 }
696
697-func (me *PsqlDB) postPager(rs *sql.Rows, pageNum int, space string, tag string) (*db.Paginate[*db.Post], error) {
698+func (me *PsqlDB) postPager(rs *sqlx.Rows, pageNum int, space string, tag string) (*db.Paginate[*db.Post], error) {
699 var posts []*db.Post
700 for rs.Next() {
701 post := &db.Post{}
702@@ -770,9 +448,13 @@ func (me *PsqlDB) postPager(rs *sql.Rows, pageNum int, space string, tag string)
703 var count int
704 var err error
705 if tag == "" {
706- err = me.Db.QueryRow(sqlSelectPostCount, space).Scan(&count)
707+ err = me.Db.QueryRow(`SELECT count(id) FROM posts WHERE hidden = FALSE AND cur_space=$1`, space).Scan(&count)
708 } else {
709- err = me.Db.QueryRow(sqlSelectTagPostCount, space, tag).Scan(&count)
710+ err = me.Db.QueryRow(`
711+ SELECT count(posts.id)
712+ FROM posts
713+ LEFT JOIN post_tags ON post_tags.post_id = posts.id
714+ WHERE hidden = FALSE AND cur_space=$1 and post_tags.name = $2`, space, tag).Scan(&count)
715 }
716 if err != nil {
717 return nil, err
718@@ -786,26 +468,50 @@ func (me *PsqlDB) postPager(rs *sql.Rows, pageNum int, space string, tag string)
719 return pager, nil
720 }
721
722-func (me *PsqlDB) FindPostsForFeed(page *db.Pager, space string) (*db.Paginate[*db.Post], error) {
723- rs, err := me.Db.Query(sqlSelectPostsByRank, page.Num, page.Num*page.Page, space)
724- if err != nil {
725- return nil, err
726- }
727- return me.postPager(rs, page.Num, space, "")
728-}
729-
730-func (me *PsqlDB) FindAllUpdatedPosts(page *db.Pager, space string) (*db.Paginate[*db.Post], error) {
731- rs, err := me.Db.Query(sqlSelectAllUpdatedPosts, page.Num, page.Num*page.Page, space)
732+func (me *PsqlDB) FindPostsByFeed(page *db.Pager, space string) (*db.Paginate[*db.Post], error) {
733+ query := `
734+ SELECT *
735+ FROM (
736+ SELECT DISTINCT ON (posts.user_id)
737+ posts.id,
738+ posts.user_id,
739+ posts.filename,
740+ posts.slug,
741+ posts.title,
742+ posts.text,
743+ posts.description,
744+ posts.publish_at,
745+ app_users.name AS username,
746+ posts.updated_at,
747+ posts.mime_type
748+ FROM posts
749+ LEFT JOIN app_users ON app_users.id = posts.user_id
750+ WHERE
751+ hidden = FALSE
752+ AND publish_at::date <= CURRENT_DATE
753+ AND cur_space = $3
754+ ORDER BY posts.user_id, publish_at DESC
755+ ) AS latest_posts
756+ ORDER BY publish_at DESC
757+ LIMIT $1 OFFSET $2`
758+ rs, err := me.Db.Queryx(query, page.Num, page.Num*page.Page, space)
759 if err != nil {
760 return nil, err
761 }
762+ defer func() { _ = rs.Close() }()
763 return me.postPager(rs, page.Num, space, "")
764 }
765
766 func (me *PsqlDB) InsertPost(post *db.Post) (*db.Post, error) {
767 var id string
768+ query := `
769+ INSERT INTO posts
770+ (user_id, filename, slug, title, text, description, publish_at, hidden, cur_space,
771+ file_size, mime_type, shasum, data, expires_at, updated_at)
772+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
773+ RETURNING id`
774 err := me.Db.QueryRow(
775- sqlInsertPost,
776+ query,
777 post.UserID,
778 post.Filename,
779 post.Slug,
780@@ -830,8 +536,13 @@ func (me *PsqlDB) InsertPost(post *db.Post) (*db.Post, error) {
781 }
782
783 func (me *PsqlDB) UpdatePost(post *db.Post) (*db.Post, error) {
784+ query := `
785+ UPDATE posts
786+ SET slug = $1, title = $2, text = $3, description = $4, updated_at = $5, publish_at = $6,
787+ file_size = $7, shasum = $8, data = $9, hidden = $11, expires_at = $12
788+ WHERE id = $10`
789 _, err := me.Db.Exec(
790- sqlUpdatePost,
791+ query,
792 post.Slug,
793 post.Title,
794 post.Text,
795@@ -854,14 +565,27 @@ func (me *PsqlDB) UpdatePost(post *db.Post) (*db.Post, error) {
796
797 func (me *PsqlDB) RemovePosts(postIDs []string) error {
798 param := "{" + strings.Join(postIDs, ",") + "}"
799- _, err := me.Db.Exec(sqlRemovePosts, param)
800+ _, err := me.Db.Exec(`DELETE FROM posts WHERE id = ANY($1::uuid[])`, param)
801 return err
802 }
803
804-func (me *PsqlDB) FindPostsForUser(page *db.Pager, userID string, space string) (*db.Paginate[*db.Post], error) {
805+func (me *PsqlDB) FindPostsByUser(page *db.Pager, userID string, space string) (*db.Paginate[*db.Post], error) {
806 var posts []*db.Post
807- rs, err := me.Db.Query(
808- sqlSelectPostsForUser,
809+ query := fmt.Sprintf(`
810+ SELECT %s, STRING_AGG(coalesce(post_tags.name, ''), ',') tags
811+ FROM posts
812+ LEFT JOIN app_users ON app_users.id = posts.user_id
813+ LEFT JOIN post_tags ON post_tags.post_id = posts.id
814+ WHERE
815+ hidden = FALSE AND
816+ user_id = $1 AND
817+ publish_at::date <= CURRENT_DATE AND
818+ cur_space = $2
819+ GROUP BY %s
820+ ORDER BY publish_at DESC, slug DESC
821+ LIMIT $3 OFFSET $4`, SelectPost, SelectPost)
822+ rs, err := me.Db.Queryx(
823+ query,
824 userID,
825 space,
826 page.Num,
827@@ -870,8 +594,9 @@ func (me *PsqlDB) FindPostsForUser(page *db.Pager, userID string, space string)
828 if err != nil {
829 return nil, err
830 }
831+ defer func() { _ = rs.Close() }()
832 for rs.Next() {
833- post, err := CreatePostWithTagsFromRow(rs)
834+ post, err := CreatePostWithTagsByRow(rs)
835 if err != nil {
836 return nil, err
837 }
838@@ -884,7 +609,7 @@ func (me *PsqlDB) FindPostsForUser(page *db.Pager, userID string, space string)
839 }
840
841 var count int
842- err = me.Db.QueryRow(sqlSelectPostCount, space).Scan(&count)
843+ err = me.Db.QueryRow(`SELECT count(id) FROM posts WHERE hidden = FALSE AND cur_space=$1`, space).Scan(&count)
844 if err != nil {
845 return nil, err
846 }
847@@ -896,82 +621,49 @@ func (me *PsqlDB) FindPostsForUser(page *db.Pager, userID string, space string)
848 return pager, nil
849 }
850
851-func (me *PsqlDB) FindAllPostsForUser(userID string, space string) ([]*db.Post, error) {
852- var posts []*db.Post
853- rs, err := me.Db.Query(sqlSelectAllPostsForUser, userID, space)
854- if err != nil {
855- return posts, err
856- }
857- for rs.Next() {
858- post, err := CreatePostFromRow(rs)
859- if err != nil {
860- return nil, err
861- }
862-
863- posts = append(posts, post)
864- }
865- if rs.Err() != nil {
866- return posts, rs.Err()
867- }
868- return posts, nil
869-}
870-
871-func (me *PsqlDB) FindPosts() ([]*db.Post, error) {
872+func (me *PsqlDB) FindAllPostsByUser(userID string, space string) ([]*db.Post, error) {
873 var posts []*db.Post
874- rs, err := me.Db.Query(sqlSelectPosts)
875- if err != nil {
876- return posts, err
877- }
878- for rs.Next() {
879- post, err := CreatePostFromRow(rs)
880- if err != nil {
881- return nil, err
882- }
883-
884- posts = append(posts, post)
885- }
886- if rs.Err() != nil {
887- return posts, rs.Err()
888+ query := fmt.Sprintf(`
889+ SELECT %s
890+ FROM posts
891+ LEFT JOIN app_users ON app_users.id = posts.user_id
892+ WHERE
893+ user_id = $1 AND
894+ cur_space = $2
895+ ORDER BY publish_at DESC`, SelectPost)
896+ err := me.Db.Select(&posts, query, userID, space)
897+ if err != nil {
898+ return nil, err
899 }
900 return posts, nil
901 }
902
903-func (me *PsqlDB) FindExpiredPosts(space string) ([]*db.Post, error) {
904+func (me *PsqlDB) FindPosts() ([]*db.Post, error) {
905 var posts []*db.Post
906- rs, err := me.Db.Query(sqlSelectExpiredPosts, space)
907+ query := fmt.Sprintf(`
908+ SELECT %s
909+ FROM posts
910+ LEFT JOIN app_users ON app_users.id = posts.user_id`, SelectPost)
911+ err := me.Db.Select(&posts, query)
912 if err != nil {
913- return posts, err
914- }
915- for rs.Next() {
916- post, err := CreatePostFromRow(rs)
917- if err != nil {
918- return nil, err
919- }
920-
921- posts = append(posts, post)
922- }
923- if rs.Err() != nil {
924- return posts, rs.Err()
925+ return nil, err
926 }
927 return posts, nil
928 }
929
930-func (me *PsqlDB) FindUpdatedPostsForUser(userID string, space string) ([]*db.Post, error) {
931+func (me *PsqlDB) FindExpiredPosts(space string) ([]*db.Post, error) {
932 var posts []*db.Post
933- rs, err := me.Db.Query(sqlSelectUpdatedPostsForUser, userID, space)
934+ query := fmt.Sprintf(`
935+ SELECT %s
936+ FROM posts
937+ LEFT JOIN app_users ON app_users.id = posts.user_id
938+ WHERE
939+ cur_space = $1 AND
940+ expires_at <= now();
941+ `, SelectPost)
942+ err := me.Db.Select(&posts, query, space)
943 if err != nil {
944- return posts, err
945- }
946- for rs.Next() {
947- post, err := CreatePostFromRow(rs)
948- if err != nil {
949- return nil, err
950- }
951-
952- posts = append(posts, post)
953- }
954- if rs.Err() != nil {
955- return posts, rs.Err()
956+ return nil, err
957 }
958 return posts, nil
959 }
960@@ -1033,10 +725,11 @@ func (me *PsqlDB) visitUnique(opts *db.SummaryOpts) ([]*db.VisitInterval, error)
961 GROUP BY interval_start`, opts.Interval, where)
962
963 intervals := []*db.VisitInterval{}
964- rs, err := me.Db.Query(uniqueVisitors, opts.Origin, with, opts.UserID)
965+ rs, err := me.Db.Queryx(uniqueVisitors, opts.Origin, with, opts.UserID)
966 if err != nil {
967 return nil, err
968 }
969+ defer func() { _ = rs.Close() }()
970
971 for rs.Next() {
972 interval := &db.VisitInterval{}
973@@ -1068,10 +761,11 @@ func (me *PsqlDB) visitReferer(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
974 LIMIT 10`, where)
975
976 intervals := []*db.VisitUrl{}
977- rs, err := me.Db.Query(topUrls, opts.Origin, with, opts.UserID)
978+ rs, err := me.Db.Queryx(topUrls, opts.Origin, with, opts.UserID)
979 if err != nil {
980 return nil, err
981 }
982+ defer func() { _ = rs.Close() }()
983
984 for rs.Next() {
985 interval := &db.VisitUrl{}
986@@ -1103,10 +797,11 @@ func (me *PsqlDB) visitUrl(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
987 LIMIT 10`, where)
988
989 intervals := []*db.VisitUrl{}
990- rs, err := me.Db.Query(topUrls, opts.Origin, with, opts.UserID)
991+ rs, err := me.Db.Queryx(topUrls, opts.Origin, with, opts.UserID)
992 if err != nil {
993 return nil, err
994 }
995+ defer func() { _ = rs.Close() }()
996
997 for rs.Next() {
998 interval := &db.VisitUrl{}
999@@ -1142,10 +837,11 @@ func (me *PsqlDB) VisitUrlNotFound(opts *db.SummaryOpts) ([]*db.VisitUrl, error)
1000 LIMIT %d`, where, limit)
1001
1002 intervals := []*db.VisitUrl{}
1003- rs, err := me.Db.Query(topUrls, opts.Origin, with, opts.UserID)
1004+ rs, err := me.Db.Queryx(topUrls, opts.Origin, with, opts.UserID)
1005 if err != nil {
1006 return nil, err
1007 }
1008+ defer func() { _ = rs.Close() }()
1009
1010 for rs.Next() {
1011 interval := &db.VisitUrl{}
1012@@ -1175,10 +871,11 @@ func (me *PsqlDB) visitHost(opts *db.SummaryOpts) ([]*db.VisitUrl, error) {
1013 ORDER BY host_count DESC`
1014
1015 intervals := []*db.VisitUrl{}
1016- rs, err := me.Db.Query(topUrls, opts.UserID)
1017+ rs, err := me.Db.Queryx(topUrls, opts.UserID)
1018 if err != nil {
1019 return nil, err
1020 }
1021+ defer func() { _ = rs.Close() }()
1022
1023 for rs.Next() {
1024 interval := &db.VisitUrl{}
1025@@ -1233,41 +930,23 @@ func (me *PsqlDB) FindVisitSiteList(opts *db.SummaryOpts) ([]*db.VisitUrl, error
1026
1027 func (me *PsqlDB) FindUsers() ([]*db.User, error) {
1028 var users []*db.User
1029- rs, err := me.Db.Query(sqlSelectUsers)
1030+ err := me.Db.Select(&users, `SELECT id, COALESCE(name, '') as name, created_at FROM app_users ORDER BY name ASC`)
1031 if err != nil {
1032- return users, err
1033- }
1034- for rs.Next() {
1035- var name sql.NullString
1036- user := &db.User{}
1037- err := rs.Scan(
1038- &user.ID,
1039- &name,
1040- &user.CreatedAt,
1041- )
1042- if err != nil {
1043- return users, err
1044- }
1045- user.Name = name.String
1046-
1047- users = append(users, user)
1048- }
1049- if rs.Err() != nil {
1050- return users, rs.Err()
1051+ return nil, err
1052 }
1053 return users, nil
1054 }
1055
1056-func (me *PsqlDB) removeTagsForPost(tx *sql.Tx, postID string) error {
1057- _, err := tx.Exec(sqlRemoveTagsByPost, postID)
1058+func (me *PsqlDB) removeTagsByPost(tx *sqlx.Tx, postID string) error {
1059+ _, err := tx.Exec(`DELETE FROM post_tags WHERE post_id = $1`, postID)
1060 return err
1061 }
1062
1063-func (me *PsqlDB) insertTagsForPost(tx *sql.Tx, tags []string, postID string) ([]string, error) {
1064+func (me *PsqlDB) insertTagsByPost(tx *sqlx.Tx, tags []string, postID string) ([]string, error) {
1065 ids := make([]string, 0)
1066 for _, tag := range tags {
1067 id := ""
1068- err := tx.QueryRow(sqlInsertTag, postID, tag).Scan(&id)
1069+ err := tx.QueryRow(`INSERT INTO post_tags (post_id, name) VALUES($1, $2) RETURNING id;`, postID, tag).Scan(&id)
1070 if err != nil {
1071 return nil, err
1072 }
1073@@ -1277,22 +956,21 @@ func (me *PsqlDB) insertTagsForPost(tx *sql.Tx, tags []string, postID string) ([
1074 return ids, nil
1075 }
1076
1077-func (me *PsqlDB) ReplaceTagsForPost(tags []string, postID string) error {
1078- ctx := context.Background()
1079- tx, err := me.Db.BeginTx(ctx, nil)
1080+func (me *PsqlDB) ReplaceTagsByPost(tags []string, postID string) error {
1081+ tx, err := me.Db.Beginx()
1082 if err != nil {
1083 return err
1084 }
1085 defer func() {
1086- err = tx.Rollback()
1087+ _ = tx.Rollback()
1088 }()
1089
1090- err = me.removeTagsForPost(tx, postID)
1091+ err = me.removeTagsByPost(tx, postID)
1092 if err != nil {
1093 return err
1094 }
1095
1096- _, err = me.insertTagsForPost(tx, tags, postID)
1097+ _, err = me.insertTagsByPost(tx, tags, postID)
1098 if err != nil {
1099 return err
1100 }
1101@@ -1301,12 +979,12 @@ func (me *PsqlDB) ReplaceTagsForPost(tags []string, postID string) error {
1102 return err
1103 }
1104
1105-func (me *PsqlDB) removeAliasesForPost(tx *sql.Tx, postID string) error {
1106- _, err := tx.Exec(sqlRemoveAliasesByPost, postID)
1107+func (me *PsqlDB) removeAliasesByPost(tx *sqlx.Tx, postID string) error {
1108+ _, err := tx.Exec(`DELETE FROM post_aliases WHERE post_id = $1`, postID)
1109 return err
1110 }
1111
1112-func (me *PsqlDB) insertAliasesForPost(tx *sql.Tx, aliases []string, postID string) ([]string, error) {
1113+func (me *PsqlDB) insertAliasesByPost(tx *sqlx.Tx, aliases []string, postID string) ([]string, error) {
1114 // hardcoded
1115 denyList := []string{
1116 "rss",
1117@@ -1337,7 +1015,7 @@ func (me *PsqlDB) insertAliasesForPost(tx *sql.Tx, aliases []string, postID stri
1118 continue
1119 }
1120 id := ""
1121- err := tx.QueryRow(sqlInsertAliases, postID, alias).Scan(&id)
1122+ err := tx.QueryRow(`INSERT INTO post_aliases (post_id, slug) VALUES($1, $2) RETURNING id;`, postID, alias).Scan(&id)
1123 if err != nil {
1124 return nil, err
1125 }
1126@@ -1347,22 +1025,21 @@ func (me *PsqlDB) insertAliasesForPost(tx *sql.Tx, aliases []string, postID stri
1127 return ids, nil
1128 }
1129
1130-func (me *PsqlDB) ReplaceAliasesForPost(aliases []string, postID string) error {
1131- ctx := context.Background()
1132- tx, err := me.Db.BeginTx(ctx, nil)
1133+func (me *PsqlDB) ReplaceAliasesByPost(aliases []string, postID string) error {
1134+ tx, err := me.Db.Beginx()
1135 if err != nil {
1136 return err
1137 }
1138 defer func() {
1139- err = tx.Rollback()
1140+ _ = tx.Rollback()
1141 }()
1142
1143- err = me.removeAliasesForPost(tx, postID)
1144+ err = me.removeAliasesByPost(tx, postID)
1145 if err != nil {
1146 return err
1147 }
1148
1149- _, err = me.insertAliasesForPost(tx, aliases, postID)
1150+ _, err = me.insertAliasesByPost(tx, aliases, postID)
1151 if err != nil {
1152 return err
1153 }
1154@@ -1373,8 +1050,22 @@ func (me *PsqlDB) ReplaceAliasesForPost(aliases []string, postID string) error {
1155
1156 func (me *PsqlDB) FindUserPostsByTag(page *db.Pager, tag, userID, space string) (*db.Paginate[*db.Post], error) {
1157 var posts []*db.Post
1158- rs, err := me.Db.Query(
1159- sqlSelectUserPostsByTag,
1160+ query := fmt.Sprintf(`
1161+ SELECT %s
1162+ FROM posts
1163+ LEFT JOIN app_users ON app_users.id = posts.user_id
1164+ LEFT JOIN post_tags ON post_tags.post_id = posts.id
1165+ WHERE
1166+ hidden = FALSE AND
1167+ user_id = $1 AND
1168+ (post_tags.name = $2 OR hidden = true) AND
1169+ publish_at::date <= CURRENT_DATE AND
1170+ cur_space = $3
1171+ ORDER BY publish_at DESC
1172+ LIMIT $4 OFFSET $5`, SelectPost)
1173+ err := me.Db.Select(
1174+ &posts,
1175+ query,
1176 userID,
1177 tag,
1178 space,
1179@@ -1384,21 +1075,9 @@ func (me *PsqlDB) FindUserPostsByTag(page *db.Pager, tag, userID, space string)
1180 if err != nil {
1181 return nil, err
1182 }
1183- for rs.Next() {
1184- post, err := CreatePostFromRow(rs)
1185- if err != nil {
1186- return nil, err
1187- }
1188-
1189- posts = append(posts, post)
1190- }
1191-
1192- if rs.Err() != nil {
1193- return nil, rs.Err()
1194- }
1195
1196 var count int
1197- err = me.Db.QueryRow(sqlSelectPostCount, space).Scan(&count)
1198+ err = me.Db.QueryRow(`SELECT count(id) FROM posts WHERE hidden = FALSE AND cur_space=$1`, space).Scan(&count)
1199 if err != nil {
1200 return nil, err
1201 }
1202@@ -1411,8 +1090,30 @@ func (me *PsqlDB) FindUserPostsByTag(page *db.Pager, tag, userID, space string)
1203 }
1204
1205 func (me *PsqlDB) FindPostsByTag(pager *db.Pager, tag, space string) (*db.Paginate[*db.Post], error) {
1206- rs, err := me.Db.Query(
1207- sqlSelectPostsByTag,
1208+ query := `
1209+ SELECT
1210+ posts.id,
1211+ user_id,
1212+ filename,
1213+ slug,
1214+ title,
1215+ text,
1216+ description,
1217+ publish_at,
1218+ app_users.name as username,
1219+ posts.updated_at,
1220+ posts.mime_type
1221+ FROM posts
1222+ LEFT JOIN app_users ON app_users.id = posts.user_id
1223+ LEFT JOIN post_tags ON post_tags.post_id = posts.id
1224+ WHERE
1225+ post_tags.name = $3 AND
1226+ publish_at::date <= CURRENT_DATE AND
1227+ cur_space = $4
1228+ ORDER BY publish_at DESC
1229+ LIMIT $1 OFFSET $2`
1230+ rs, err := me.Db.Queryx(
1231+ query,
1232 pager.Num,
1233 pager.Num*pager.Page,
1234 tag,
1235@@ -1421,16 +1122,26 @@ func (me *PsqlDB) FindPostsByTag(pager *db.Pager, tag, space string) (*db.Pagina
1236 if err != nil {
1237 return nil, err
1238 }
1239+ defer func() { _ = rs.Close() }()
1240
1241 return me.postPager(rs, pager.Num, space, tag)
1242 }
1243
1244 func (me *PsqlDB) FindPopularTags(space string) ([]string, error) {
1245 tags := make([]string, 0)
1246- rs, err := me.Db.Query(sqlSelectPopularTags, space)
1247+ query := `
1248+ SELECT name, count(post_id) as "tally"
1249+ FROM post_tags
1250+ LEFT JOIN posts ON posts.id = post_id
1251+ WHERE posts.cur_space = $1
1252+ GROUP BY name
1253+ ORDER BY tally DESC
1254+ LIMIT 5`
1255+ rs, err := me.Db.Queryx(query, space)
1256 if err != nil {
1257 return tags, err
1258 }
1259+ defer func() { _ = rs.Close() }()
1260 for rs.Next() {
1261 name := ""
1262 tally := 0
1263@@ -1447,112 +1158,30 @@ func (me *PsqlDB) FindPopularTags(space string) ([]string, error) {
1264 return tags, nil
1265 }
1266
1267-func (me *PsqlDB) FindTagsForUser(userID string, space string) ([]string, error) {
1268- tags := []string{}
1269- rs, err := me.Db.Query(sqlSelectTagsForUser, userID, space)
1270- if err != nil {
1271- return tags, err
1272- }
1273- for rs.Next() {
1274- name := ""
1275- err := rs.Scan(&name)
1276- if err != nil {
1277- return tags, err
1278- }
1279-
1280- tags = append(tags, name)
1281- }
1282- if rs.Err() != nil {
1283- return tags, rs.Err()
1284- }
1285- return tags, nil
1286-}
1287-
1288-func (me *PsqlDB) FindTagsForPost(postID string) ([]string, error) {
1289- tags := make([]string, 0)
1290- rs, err := me.Db.Query(sqlSelectTagsForPost, postID)
1291- if err != nil {
1292- return tags, err
1293- }
1294-
1295- for rs.Next() {
1296- name := ""
1297- err := rs.Scan(&name)
1298- if err != nil {
1299- return tags, err
1300- }
1301-
1302- tags = append(tags, name)
1303- }
1304-
1305- if rs.Err() != nil {
1306- return tags, rs.Err()
1307- }
1308-
1309- return tags, nil
1310-}
1311-
1312 func (me *PsqlDB) FindFeature(userID string, feature string) (*db.FeatureFlag, error) {
1313 ff := &db.FeatureFlag{}
1314- // payment history is allowed to be null
1315- // https://devtidbits.com/2020/08/03/go-sql-error-converting-null-to-string-is-unsupported/
1316- var paymentHistoryID sql.NullString
1317- err := me.Db.QueryRow(sqlSelectFeatureForUser, userID, feature).Scan(
1318- &ff.ID,
1319- &ff.UserID,
1320- &paymentHistoryID,
1321- &ff.Name,
1322- &ff.Data,
1323- &ff.CreatedAt,
1324- &ff.ExpiresAt,
1325- )
1326+ err := me.Db.Get(ff, `SELECT * FROM feature_flags WHERE user_id = $1 AND name = $2 ORDER BY expires_at DESC LIMIT 1`, userID, feature)
1327 if err != nil {
1328 return nil, err
1329 }
1330-
1331- ff.PaymentHistoryID = paymentHistoryID
1332-
1333 return ff, nil
1334 }
1335
1336-func (me *PsqlDB) FindFeaturesForUser(userID string) ([]*db.FeatureFlag, error) {
1337+func (me *PsqlDB) FindFeaturesByUser(userID string) ([]*db.FeatureFlag, error) {
1338 var features []*db.FeatureFlag
1339 // https://stackoverflow.com/a/16920077
1340- query := `SELECT DISTINCT ON (name)
1341- id, user_id, payment_history_id, name, data, created_at, expires_at
1342+ query := `SELECT DISTINCT ON (name) *
1343 FROM feature_flags
1344 WHERE user_id=$1
1345 ORDER BY name, expires_at DESC;`
1346- rs, err := me.Db.Query(query, userID)
1347+ err := me.Db.Select(&features, query, userID)
1348 if err != nil {
1349- return features, err
1350- }
1351- for rs.Next() {
1352- var paymentHistoryID sql.NullString
1353- ff := &db.FeatureFlag{}
1354- err := rs.Scan(
1355- &ff.ID,
1356- &ff.UserID,
1357- &paymentHistoryID,
1358- &ff.Name,
1359- &ff.Data,
1360- &ff.CreatedAt,
1361- &ff.ExpiresAt,
1362- )
1363- if err != nil {
1364- return features, err
1365- }
1366- ff.PaymentHistoryID = paymentHistoryID
1367-
1368- features = append(features, ff)
1369- }
1370- if rs.Err() != nil {
1371- return features, rs.Err()
1372+ return nil, err
1373 }
1374 return features, nil
1375 }
1376
1377-func (me *PsqlDB) HasFeatureForUser(userID string, feature string) bool {
1378+func (me *PsqlDB) HasFeatureByUser(userID string, feature string) bool {
1379 ff, err := me.FindFeature(userID, feature)
1380 if err != nil {
1381 return false
1382@@ -1560,28 +1189,18 @@ func (me *PsqlDB) HasFeatureForUser(userID string, feature string) bool {
1383 return ff.IsValid()
1384 }
1385
1386-func (me *PsqlDB) FindTotalSizeForUser(userID string) (int, error) {
1387- var fileSize int
1388- err := me.Db.QueryRow(sqlSelectSizeForUser, userID).Scan(&fileSize)
1389- if err != nil {
1390- return 0, err
1391- }
1392- return fileSize, nil
1393-}
1394-
1395 func (me *PsqlDB) InsertFeedItems(postID string, items []*db.FeedItem) error {
1396- ctx := context.Background()
1397- tx, err := me.Db.BeginTx(ctx, nil)
1398+ tx, err := me.Db.Beginx()
1399 if err != nil {
1400 return err
1401 }
1402 defer func() {
1403- err = tx.Rollback()
1404+ _ = tx.Rollback()
1405 }()
1406
1407 for _, item := range items {
1408 _, err := tx.Exec(
1409- sqlInsertFeedItems,
1410+ `INSERT INTO feed_items (post_id, guid, data) VALUES ($1, $2, $3) RETURNING id;`,
1411 item.PostID,
1412 item.GUID,
1413 item.Data,
1414@@ -1599,33 +1218,11 @@ func (me *PsqlDB) InsertFeedItems(postID string, items []*db.FeedItem) error {
1415 }
1416
1417 func (me *PsqlDB) FindFeedItemsByPostID(postID string) ([]*db.FeedItem, error) {
1418- // sqlSelectFeedItemsByPost
1419- items := make([]*db.FeedItem, 0)
1420- rs, err := me.Db.Query(sqlSelectFeedItemsByPost, postID)
1421+ var items []*db.FeedItem
1422+ err := me.Db.Select(&items, `SELECT * FROM feed_items WHERE post_id=$1`, postID)
1423 if err != nil {
1424- return items, err
1425- }
1426-
1427- for rs.Next() {
1428- item := &db.FeedItem{}
1429- err := rs.Scan(
1430- &item.ID,
1431- &item.PostID,
1432- &item.GUID,
1433- &item.Data,
1434- &item.CreatedAt,
1435- )
1436- if err != nil {
1437- return items, err
1438- }
1439-
1440- items = append(items, item)
1441- }
1442-
1443- if rs.Err() != nil {
1444- return items, rs.Err()
1445+ return nil, err
1446 }
1447-
1448 return items, nil
1449 }
1450
1451@@ -1635,7 +1232,7 @@ func (me *PsqlDB) InsertProject(userID, name, projectDir string) (string, error)
1452 }
1453
1454 var id string
1455- err := me.Db.QueryRow(sqlInsertProject, userID, name, projectDir).Scan(&id)
1456+ err := me.Db.QueryRow(`INSERT INTO projects (user_id, name, project_dir) VALUES ($1, $2, $3) RETURNING id;`, userID, name, projectDir).Scan(&id)
1457 if err != nil {
1458 return "", err
1459 }
1460@@ -1643,33 +1240,22 @@ func (me *PsqlDB) InsertProject(userID, name, projectDir string) (string, error)
1461 }
1462
1463 func (me *PsqlDB) UpdateProject(userID, name string) error {
1464- _, err := me.Db.Exec(sqlUpdateProject, userID, name, time.Now())
1465+ _, err := me.Db.Exec(`UPDATE projects SET updated_at = $3 WHERE user_id = $1 AND name = $2;`, userID, name, time.Now())
1466 return err
1467 }
1468
1469 func (me *PsqlDB) FindProjectByName(userID, name string) (*db.Project, error) {
1470 project := &db.Project{}
1471- r := me.Db.QueryRow(sqlFindProjectByName, userID, name)
1472- err := r.Scan(
1473- &project.ID,
1474- &project.UserID,
1475- &project.Name,
1476- &project.ProjectDir,
1477- &project.Acl,
1478- &project.Blocked,
1479- &project.CreatedAt,
1480- &project.UpdatedAt,
1481- )
1482+ err := me.Db.Get(project, `SELECT * FROM projects WHERE user_id = $1 AND name = $2;`, userID, name)
1483 if err != nil {
1484 return nil, err
1485 }
1486-
1487 return project, nil
1488 }
1489
1490 func (me *PsqlDB) InsertToken(userID, name string) (string, error) {
1491 var token string
1492- err := me.Db.QueryRow(sqlInsertToken, userID, name).Scan(&token)
1493+ err := me.Db.QueryRow(`INSERT INTO tokens (user_id, name) VALUES($1, $2) RETURNING token;`, userID, name).Scan(&token)
1494 if err != nil {
1495 return "", err
1496 }
1497@@ -1677,7 +1263,7 @@ func (me *PsqlDB) InsertToken(userID, name string) (string, error) {
1498 }
1499
1500 func (me *PsqlDB) UpsertToken(userID, name string) (string, error) {
1501- token, _ := me.FindTokenByName(userID, name)
1502+ token, _ := me.findTokenByName(userID, name)
1503 if token != "" {
1504 return token, nil
1505 }
1506@@ -1686,9 +1272,9 @@ func (me *PsqlDB) UpsertToken(userID, name string) (string, error) {
1507 return token, err
1508 }
1509
1510-func (me *PsqlDB) FindTokenByName(userID, name string) (string, error) {
1511+func (me *PsqlDB) findTokenByName(userID, name string) (string, error) {
1512 var token string
1513- err := me.Db.QueryRow(sqlSelectTokenByNameForUser, userID, name).Scan(&token)
1514+ err := me.Db.QueryRow(`SELECT token FROM tokens WHERE user_id = $1 AND name = $2`, userID, name).Scan(&token)
1515 if err != nil {
1516 return "", err
1517 }
1518@@ -1696,29 +1282,17 @@ func (me *PsqlDB) FindTokenByName(userID, name string) (string, error) {
1519 }
1520
1521 func (me *PsqlDB) RemoveToken(tokenID string) error {
1522- _, err := me.Db.Exec(sqlRemoveToken, tokenID)
1523+ _, err := me.Db.Exec(`DELETE FROM tokens WHERE id = $1`, tokenID)
1524 return err
1525 }
1526
1527-func (me *PsqlDB) FindTokensForUser(userID string) ([]*db.Token, error) {
1528- var keys []*db.Token
1529- rs, err := me.Db.Query(sqlSelectTokensForUser, userID)
1530+func (me *PsqlDB) FindTokensByUser(userID string) ([]*db.Token, error) {
1531+ var tokens []*db.Token
1532+ err := me.Db.Select(&tokens, `SELECT * FROM tokens WHERE user_id = $1`, userID)
1533 if err != nil {
1534- return keys, err
1535- }
1536- for rs.Next() {
1537- pk := &db.Token{}
1538- err := rs.Scan(&pk.ID, &pk.UserID, &pk.Name, &pk.CreatedAt, &pk.ExpiresAt)
1539- if err != nil {
1540- return keys, err
1541- }
1542-
1543- keys = append(keys, pk)
1544- }
1545- if rs.Err() != nil {
1546- return keys, rs.Err()
1547+ return nil, err
1548 }
1549- return keys, nil
1550+ return tokens, nil
1551 }
1552
1553 func (me *PsqlDB) InsertFeature(userID, name string, expiresAt time.Time) (*db.FeatureFlag, error) {
1554@@ -1763,13 +1337,12 @@ func (me *PsqlDB) AddPicoPlusUser(username, email, paymentType, txId string) err
1555 return err
1556 }
1557
1558- ctx := context.Background()
1559- tx, err := me.Db.BeginTx(ctx, nil)
1560+ tx, err := me.Db.Beginx()
1561 if err != nil {
1562 return err
1563 }
1564 defer func() {
1565- err = tx.Rollback()
1566+ _ = tx.Rollback()
1567 }()
1568
1569 var paymentHistoryId sql.NullString
1570@@ -1849,69 +1422,32 @@ func (me *PsqlDB) InsertTunsEventLog(log *db.TunsEventLog) error {
1571 }
1572
1573 func (me *PsqlDB) FindTunsEventLogsByAddr(userID, addr string) ([]*db.TunsEventLog, error) {
1574- logs := []*db.TunsEventLog{}
1575- rs, err := me.Db.Query(
1576- `SELECT id, user_id, server_id, remote_addr, event_type, tunnel_type, connection_type, tunnel_id, created_at
1577- FROM tuns_event_logs WHERE user_id=$1 AND tunnel_id=$2 ORDER BY created_at DESC`, userID, addr)
1578+ var logs []*db.TunsEventLog
1579+ err := me.Db.Select(&logs,
1580+ `SELECT * FROM tuns_event_logs WHERE user_id=$1 AND tunnel_id=$2 ORDER BY created_at DESC`, userID, addr)
1581 if err != nil {
1582 return nil, err
1583 }
1584-
1585- for rs.Next() {
1586- log := db.TunsEventLog{}
1587- err := rs.Scan(
1588- &log.ID, &log.UserId, &log.ServerID, &log.RemoteAddr,
1589- &log.EventType, &log.TunnelType, &log.ConnectionType,
1590- &log.TunnelID, &log.CreatedAt,
1591- )
1592- if err != nil {
1593- return nil, err
1594- }
1595- logs = append(logs, &log)
1596- }
1597-
1598- if rs.Err() != nil {
1599- return nil, rs.Err()
1600- }
1601-
1602 return logs, nil
1603 }
1604
1605 func (me *PsqlDB) FindTunsEventLogs(userID string) ([]*db.TunsEventLog, error) {
1606- logs := []*db.TunsEventLog{}
1607- rs, err := me.Db.Query(
1608- `SELECT id, user_id, server_id, remote_addr, event_type, tunnel_type, connection_type, tunnel_id, created_at
1609- FROM tuns_event_logs WHERE user_id=$1 ORDER BY created_at DESC`, userID)
1610+ var logs []*db.TunsEventLog
1611+ err := me.Db.Select(&logs,
1612+ `SELECT * FROM tuns_event_logs WHERE user_id=$1 ORDER BY created_at DESC`, userID)
1613 if err != nil {
1614 return nil, err
1615 }
1616-
1617- for rs.Next() {
1618- log := db.TunsEventLog{}
1619- err := rs.Scan(
1620- &log.ID, &log.UserId, &log.ServerID, &log.RemoteAddr,
1621- &log.EventType, &log.TunnelType, &log.ConnectionType,
1622- &log.TunnelID, &log.CreatedAt,
1623- )
1624- if err != nil {
1625- return nil, err
1626- }
1627- logs = append(logs, &log)
1628- }
1629-
1630- if rs.Err() != nil {
1631- return nil, rs.Err()
1632- }
1633-
1634 return logs, nil
1635 }
1636
1637 func (me *PsqlDB) FindUserStats(userID string) (*db.UserStats, error) {
1638 stats := db.UserStats{}
1639- rs, err := me.Db.Query(`SELECT cur_space, count(id), min(created_at), max(created_at), max(updated_at) FROM posts WHERE user_id=$1 GROUP BY cur_space`, userID)
1640+ rs, err := me.Db.Queryx(`SELECT cur_space, count(id), min(created_at), max(created_at), max(updated_at) FROM posts WHERE user_id=$1 GROUP BY cur_space`, userID)
1641 if err != nil {
1642 return nil, err
1643 }
1644+ defer func() { _ = rs.Close() }()
1645
1646 for rs.Next() {
1647 stat := db.UserServiceStats{}
1648@@ -1942,79 +1478,29 @@ func (me *PsqlDB) FindUserStats(userID string) (*db.UserStats, error) {
1649 }
1650
1651 func (me *PsqlDB) FindAccessLogs(userID string, fromDate *time.Time) ([]*db.AccessLog, error) {
1652- logs := []*db.AccessLog{}
1653- rs, err := me.Db.Query(
1654- `SELECT id, user_id, service, pubkey, identity, created_at FROM access_logs WHERE user_id=$1 AND created_at >= $2 ORDER BY created_at DESC`, userID, fromDate)
1655+ var logs []*db.AccessLog
1656+ err := me.Db.Select(&logs, `SELECT * FROM access_logs WHERE user_id=$1 AND created_at >= $2 ORDER BY created_at DESC`, userID, fromDate)
1657 if err != nil {
1658 return nil, err
1659 }
1660-
1661- for rs.Next() {
1662- log := db.AccessLog{}
1663- err := rs.Scan(
1664- &log.ID, &log.UserID, &log.Service, &log.Pubkey, &log.Identity, &log.CreatedAt,
1665- )
1666- if err != nil {
1667- return nil, err
1668- }
1669- logs = append(logs, &log)
1670- }
1671-
1672- if rs.Err() != nil {
1673- return nil, rs.Err()
1674- }
1675-
1676 return logs, nil
1677 }
1678
1679 func (me *PsqlDB) FindAccessLogsByPubkey(pubkey string, fromDate *time.Time) ([]*db.AccessLog, error) {
1680- logs := []*db.AccessLog{}
1681- rs, err := me.Db.Query(
1682- `SELECT id, user_id, service, pubkey, identity, created_at FROM access_logs WHERE pubkey=$1 AND created_at >= $2 ORDER BY created_at DESC`, pubkey, fromDate)
1683+ var logs []*db.AccessLog
1684+ err := me.Db.Select(&logs, `SELECT * FROM access_logs WHERE pubkey=$1 AND created_at >= $2 ORDER BY created_at DESC`, pubkey, fromDate)
1685 if err != nil {
1686 return nil, err
1687 }
1688-
1689- for rs.Next() {
1690- log := db.AccessLog{}
1691- err := rs.Scan(
1692- &log.ID, &log.UserID, &log.Service, &log.Pubkey, &log.Identity, &log.CreatedAt,
1693- )
1694- if err != nil {
1695- return nil, err
1696- }
1697- logs = append(logs, &log)
1698- }
1699-
1700- if rs.Err() != nil {
1701- return nil, rs.Err()
1702- }
1703-
1704 return logs, nil
1705 }
1706
1707 func (me *PsqlDB) FindPubkeysInAccessLogs(userID string) ([]string, error) {
1708- pubkeys := []string{}
1709- rs, err := me.Db.Query(
1710- `SELECT DISTINCT(pubkey) FROM access_logs WHERE user_id=$1`, userID,
1711- )
1712+ var pubkeys []string
1713+ err := me.Db.Select(&pubkeys, `SELECT DISTINCT(pubkey) FROM access_logs WHERE user_id=$1`, userID)
1714 if err != nil {
1715 return nil, err
1716 }
1717-
1718- for rs.Next() {
1719- pubkey := ""
1720- err := rs.Scan(&pubkey)
1721- if err != nil {
1722- return nil, err
1723- }
1724- pubkeys = append(pubkeys, pubkey)
1725- }
1726-
1727- if rs.Err() != nil {
1728- return nil, rs.Err()
1729- }
1730-
1731 return pubkeys, nil
1732 }
1733
+1472,
-0
1@@ -0,0 +1,1472 @@
2+package postgres
3+
4+import (
5+ "context"
6+ "fmt"
7+ "log/slog"
8+ "os"
9+ "os/exec"
10+ "path/filepath"
11+ "runtime"
12+ "sort"
13+ "testing"
14+ "time"
15+
16+ "github.com/jmoiron/sqlx"
17+ _ "github.com/lib/pq"
18+ "github.com/picosh/pico/pkg/db"
19+ "github.com/testcontainers/testcontainers-go"
20+ "github.com/testcontainers/testcontainers-go/modules/postgres"
21+ "github.com/testcontainers/testcontainers-go/wait"
22+)
23+
24+var testDB *PsqlDB
25+var testLogger *slog.Logger
26+var skipTests bool
27+
28+func setupContainerRuntime() bool {
29+ // Check if DATABASE_URL is set for external postgres (CI/CD or manual testing)
30+ if os.Getenv("TEST_DATABASE_URL") != "" {
31+ return true
32+ }
33+
34+ // Try podman first
35+ if cmd := exec.Command("podman", "info"); cmd.Run() == nil {
36+ // For podman, we need to ensure the socket is running
37+ // User should run: systemctl --user start podman.socket
38+ _ = os.Setenv("TESTCONTAINERS_RYUK_DISABLED", "true")
39+
40+ // Check if socket exists and is accessible
41+ xdgRuntime := os.Getenv("XDG_RUNTIME_DIR")
42+ if xdgRuntime != "" {
43+ socketPath := xdgRuntime + "/podman/podman.sock"
44+ if _, err := os.Stat(socketPath); err == nil {
45+ _ = os.Setenv("DOCKER_HOST", "unix://"+socketPath)
46+ return true
47+ }
48+ }
49+ // Socket not available, need to start it
50+ fmt.Println("Podman detected but socket not running. Run: systemctl --user start podman.socket")
51+ return false
52+ }
53+
54+ // Try docker
55+ if cmd := exec.Command("docker", "info"); cmd.Run() == nil {
56+ return true
57+ }
58+
59+ return false
60+}
61+
62+func TestMain(m *testing.M) {
63+ ctx := context.Background()
64+ testLogger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelError}))
65+
66+ // Check for external database URL first (for CI/CD or manual testing)
67+ if dbURL := os.Getenv("TEST_DATABASE_URL"); dbURL != "" {
68+ testDB = NewDB(dbURL, testLogger)
69+ if err := setupTestSchema(testDB.Db); err != nil {
70+ panic(fmt.Sprintf("failed to setup schema: %s", err))
71+ }
72+ code := m.Run()
73+ _ = testDB.Close()
74+ os.Exit(code)
75+ }
76+
77+ if !setupContainerRuntime() {
78+ fmt.Println("Container runtime not available, skipping integration tests")
79+ fmt.Println("To run tests, either:")
80+ fmt.Println(" - Set TEST_DATABASE_URL to a postgres connection string")
81+ fmt.Println(" - Start podman socket: systemctl --user start podman.socket")
82+ fmt.Println(" - Start docker daemon")
83+ skipTests = true
84+ os.Exit(0)
85+ }
86+
87+ pgContainer, err := postgres.Run(ctx,
88+ "postgres:14",
89+ postgres.WithDatabase("pico_test"),
90+ postgres.WithUsername("postgres"),
91+ postgres.WithPassword("postgres"),
92+ testcontainers.WithWaitStrategy(
93+ wait.ForLog("database system is ready to accept connections").
94+ WithOccurrence(2).
95+ WithStartupTimeout(30*time.Second)),
96+ )
97+ if err != nil {
98+ fmt.Printf("Failed to start postgres container (Docker may not be running): %s\n", err)
99+ skipTests = true
100+ os.Exit(0)
101+ }
102+
103+ connStr, err := pgContainer.ConnectionString(ctx, "sslmode=disable")
104+ if err != nil {
105+ panic(fmt.Sprintf("failed to get connection string: %s", err))
106+ }
107+
108+ testDB = NewDB(connStr, testLogger)
109+
110+ if err := setupTestSchema(testDB.Db); err != nil {
111+ panic(fmt.Sprintf("failed to setup schema: %s", err))
112+ }
113+
114+ code := m.Run()
115+
116+ _ = testDB.Close()
117+ _ = pgContainer.Terminate(ctx)
118+
119+ os.Exit(code)
120+}
121+
122+func getProjectRoot() string {
123+ _, filename, _, ok := runtime.Caller(0)
124+ if !ok {
125+ panic("failed to get current file path")
126+ }
127+ // storage_test.go is in pkg/db/postgres/, so go up 4 levels to get project root
128+ return filepath.Join(filepath.Dir(filename), "..", "..", "..")
129+}
130+
131+func setupTestSchema(db *sqlx.DB) error {
132+ projectRoot := getProjectRoot()
133+ migrationsDir := filepath.Join(projectRoot, "sql", "migrations")
134+
135+ // Read all migration files
136+ entries, err := os.ReadDir(migrationsDir)
137+ if err != nil {
138+ return fmt.Errorf("failed to read migrations directory: %w", err)
139+ }
140+
141+ // Sort by filename (they're date-prefixed, so alphabetical order is correct)
142+ var migrationFiles []string
143+ for _, entry := range entries {
144+ if !entry.IsDir() && filepath.Ext(entry.Name()) == ".sql" {
145+ migrationFiles = append(migrationFiles, entry.Name())
146+ }
147+ }
148+ sort.Strings(migrationFiles)
149+
150+ // Execute each migration in order
151+ for _, filename := range migrationFiles {
152+ migrationPath := filepath.Join(migrationsDir, filename)
153+ content, err := os.ReadFile(migrationPath)
154+ if err != nil {
155+ return fmt.Errorf("failed to read migration %s: %w", filename, err)
156+ }
157+
158+ _, err = db.Exec(string(content))
159+ if err != nil {
160+ return fmt.Errorf("failed to execute migration %s: %w", filename, err)
161+ }
162+ }
163+
164+ return nil
165+}
166+
167+func cleanupTestData(t *testing.T) {
168+ t.Helper()
169+ tables := []string{
170+ "access_logs", "tuns_event_logs", "analytics_visits",
171+ "feed_items", "post_aliases", "post_tags", "posts",
172+ "projects", "feature_flags", "payment_history", "tokens",
173+ "public_keys", "app_users",
174+ }
175+ for _, table := range tables {
176+ _, err := testDB.Db.Exec(fmt.Sprintf("DELETE FROM %s", table))
177+ if err != nil {
178+ t.Fatalf("failed to clean up %s: %v", table, err)
179+ }
180+ }
181+}
182+
183+func mustInsertPost(t *testing.T, post *db.Post) *db.Post {
184+ t.Helper()
185+ now := time.Now()
186+ if post.UpdatedAt == nil {
187+ post.UpdatedAt = &now
188+ }
189+ if post.PublishAt == nil {
190+ post.PublishAt = &now
191+ }
192+ created, err := testDB.InsertPost(post)
193+ if err != nil {
194+ t.Fatalf("InsertPost failed: %v", err)
195+ }
196+ return created
197+}
198+
199+// ============ User Management Tests ============
200+
201+func TestRegisterUser_Success(t *testing.T) {
202+ cleanupTestData(t)
203+
204+ user, err := testDB.RegisterUser("testuser", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI testkey", "test comment")
205+ if err != nil {
206+ t.Fatalf("RegisterUser failed: %v", err)
207+ }
208+ if user == nil {
209+ t.Fatal("expected user, got nil")
210+ }
211+ if user.Name != "testuser" {
212+ t.Errorf("expected name 'testuser', got '%s'", user.Name)
213+ }
214+ if user.PublicKey == nil {
215+ t.Fatal("expected public key, got nil")
216+ }
217+}
218+
219+func TestRegisterUser_DuplicateName(t *testing.T) {
220+ cleanupTestData(t)
221+
222+ _, err := testDB.RegisterUser("duplicateuser", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI key1", "comment1")
223+ if err != nil {
224+ t.Fatalf("first RegisterUser failed: %v", err)
225+ }
226+
227+ _, err = testDB.RegisterUser("duplicateuser", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI key2", "comment2")
228+ if err == nil {
229+ t.Error("expected error for duplicate name, got nil")
230+ }
231+}
232+
233+func TestRegisterUser_DeniedName(t *testing.T) {
234+ cleanupTestData(t)
235+
236+ _, err := testDB.RegisterUser("admin", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI adminkey", "comment")
237+ if err == nil {
238+ t.Error("expected error for denied name 'admin', got nil")
239+ }
240+}
241+
242+func TestRegisterUser_InvalidName(t *testing.T) {
243+ cleanupTestData(t)
244+
245+ _, err := testDB.RegisterUser("user@invalid", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI invalidkey", "comment")
246+ if err == nil {
247+ t.Error("expected error for invalid name, got nil")
248+ }
249+}
250+
251+func TestFindUser(t *testing.T) {
252+ cleanupTestData(t)
253+
254+ created, err := testDB.RegisterUser("findme", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI findmekey", "comment")
255+ if err != nil {
256+ t.Fatalf("RegisterUser failed: %v", err)
257+ }
258+
259+ found, err := testDB.FindUser(created.ID)
260+ if err != nil {
261+ t.Fatalf("FindUser failed: %v", err)
262+ }
263+ if found.Name != "findme" {
264+ t.Errorf("expected name 'findme', got '%s'", found.Name)
265+ }
266+}
267+
268+func TestFindUserByName(t *testing.T) {
269+ cleanupTestData(t)
270+
271+ _, err := testDB.RegisterUser("nameduser", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI namedkey", "comment")
272+ if err != nil {
273+ t.Fatalf("RegisterUser failed: %v", err)
274+ }
275+
276+ found, err := testDB.FindUserByName("nameduser")
277+ if err != nil {
278+ t.Fatalf("FindUserByName failed: %v", err)
279+ }
280+ if found.Name != "nameduser" {
281+ t.Errorf("expected name 'nameduser', got '%s'", found.Name)
282+ }
283+}
284+
285+func TestFindUserByPubkey(t *testing.T) {
286+ cleanupTestData(t)
287+
288+ pubkey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI pubkeyuser"
289+ _, err := testDB.RegisterUser("pubkeyuser", pubkey, "comment")
290+ if err != nil {
291+ t.Fatalf("RegisterUser failed: %v", err)
292+ }
293+
294+ found, err := testDB.FindUserByPubkey(pubkey)
295+ if err != nil {
296+ t.Fatalf("FindUserByPubkey failed: %v", err)
297+ }
298+ if found.Name != "pubkeyuser" {
299+ t.Errorf("expected name 'pubkeyuser', got '%s'", found.Name)
300+ }
301+}
302+
303+func TestFindUserByKey(t *testing.T) {
304+ cleanupTestData(t)
305+
306+ pubkey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI forkeyfind"
307+ _, err := testDB.RegisterUser("forkeyfind", pubkey, "comment")
308+ if err != nil {
309+ t.Fatalf("RegisterUser failed: %v", err)
310+ }
311+
312+ found, err := testDB.FindUserByKey("forkeyfind", pubkey)
313+ if err != nil {
314+ t.Fatalf("FindUserByKey failed: %v", err)
315+ }
316+ if found.Name != "forkeyfind" {
317+ t.Errorf("expected name 'forkeyfind', got '%s'", found.Name)
318+ }
319+}
320+
321+func TestFindUsers(t *testing.T) {
322+ cleanupTestData(t)
323+
324+ _, _ = testDB.RegisterUser("user1", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI user1key", "comment")
325+ _, _ = testDB.RegisterUser("user2", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI user2key", "comment")
326+
327+ users, err := testDB.FindUsers()
328+ if err != nil {
329+ t.Fatalf("FindUsers failed: %v", err)
330+ }
331+ if len(users) != 2 {
332+ t.Errorf("expected 2 users, got %d", len(users))
333+ }
334+}
335+
336+// ============ Public Key Management Tests ============
337+
338+func TestInsertPublicKey_Success(t *testing.T) {
339+ cleanupTestData(t)
340+
341+ user, err := testDB.RegisterUser("keyowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI keyowner1", "comment")
342+ if err != nil {
343+ t.Fatalf("RegisterUser failed: %v", err)
344+ }
345+
346+ err = testDB.InsertPublicKey(user.ID, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI secondkey", "second key")
347+ if err != nil {
348+ t.Fatalf("InsertPublicKey failed: %v", err)
349+ }
350+
351+ keys, err := testDB.FindKeysByUser(user)
352+ if err != nil {
353+ t.Fatalf("FindKeysByUser failed: %v", err)
354+ }
355+ if len(keys) != 2 {
356+ t.Errorf("expected 2 keys, got %d", len(keys))
357+ }
358+}
359+
360+func TestInsertPublicKey_Duplicate(t *testing.T) {
361+ cleanupTestData(t)
362+
363+ user, _ := testDB.RegisterUser("dupkeyowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI dupkey", "comment")
364+
365+ err := testDB.InsertPublicKey(user.ID, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI dupkey", "same key")
366+ if err == nil {
367+ t.Error("expected error for duplicate key, got nil")
368+ }
369+}
370+
371+func TestUpdatePublicKey(t *testing.T) {
372+ cleanupTestData(t)
373+
374+ user, _ := testDB.RegisterUser("updatekeyowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI updatekeyowner", "original")
375+
376+ updated, err := testDB.UpdatePublicKey(user.PublicKey.ID, "new-name")
377+ if err != nil {
378+ t.Fatalf("UpdatePublicKey failed: %v", err)
379+ }
380+ if updated.Name != "new-name" {
381+ t.Errorf("expected name 'new-name', got '%s'", updated.Name)
382+ }
383+}
384+
385+func TestFindKeysByUser(t *testing.T) {
386+ cleanupTestData(t)
387+
388+ user, _ := testDB.RegisterUser("multikeyowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI multikeyowner1", "key1")
389+ _ = testDB.InsertPublicKey(user.ID, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI multikeyowner2", "key2")
390+
391+ keys, err := testDB.FindKeysByUser(user)
392+ if err != nil {
393+ t.Fatalf("FindKeysByUser failed: %v", err)
394+ }
395+ if len(keys) != 2 {
396+ t.Errorf("expected 2 keys, got %d", len(keys))
397+ }
398+}
399+
400+func TestRemoveKeys(t *testing.T) {
401+ cleanupTestData(t)
402+
403+ user, _ := testDB.RegisterUser("removekeyowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI removekeyowner", "key1")
404+ _ = testDB.InsertPublicKey(user.ID, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI removekeyowner2", "key2")
405+
406+ keys, _ := testDB.FindKeysByUser(user)
407+ if len(keys) != 2 {
408+ t.Fatalf("expected 2 keys before removal, got %d", len(keys))
409+ }
410+
411+ err := testDB.RemoveKeys([]string{keys[1].ID})
412+ if err != nil {
413+ t.Fatalf("RemoveKeys failed: %v", err)
414+ }
415+
416+ keys, _ = testDB.FindKeysByUser(user)
417+ if len(keys) != 1 {
418+ t.Errorf("expected 1 key after removal, got %d", len(keys))
419+ }
420+}
421+
422+// ============ Token Management Tests ============
423+
424+func TestInsertToken(t *testing.T) {
425+ cleanupTestData(t)
426+
427+ user, _ := testDB.RegisterUser("tokenowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI tokenowner", "comment")
428+
429+ token, err := testDB.InsertToken(user.ID, "my-token")
430+ if err != nil {
431+ t.Fatalf("InsertToken failed: %v", err)
432+ }
433+ if token == "" {
434+ t.Error("expected token string, got empty")
435+ }
436+}
437+
438+func TestUpsertToken(t *testing.T) {
439+ cleanupTestData(t)
440+
441+ user, _ := testDB.RegisterUser("upserttokenowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI upserttokenowner", "comment")
442+
443+ token1, err := testDB.UpsertToken(user.ID, "upsert-token")
444+ if err != nil {
445+ t.Fatalf("first UpsertToken failed: %v", err)
446+ }
447+
448+ token2, err := testDB.UpsertToken(user.ID, "upsert-token")
449+ if err != nil {
450+ t.Fatalf("second UpsertToken failed: %v", err)
451+ }
452+
453+ if token1 != token2 {
454+ t.Errorf("expected same token, got different: %s vs %s", token1, token2)
455+ }
456+}
457+
458+func TestFindTokensByUser(t *testing.T) {
459+ cleanupTestData(t)
460+
461+ user, _ := testDB.RegisterUser("tokensowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI tokensowner", "comment")
462+ _, _ = testDB.InsertToken(user.ID, "token1")
463+ _, _ = testDB.InsertToken(user.ID, "token2")
464+
465+ tokens, err := testDB.FindTokensByUser(user.ID)
466+ if err != nil {
467+ t.Fatalf("FindTokensByUser failed: %v", err)
468+ }
469+ if len(tokens) != 2 {
470+ t.Errorf("expected 2 tokens, got %d", len(tokens))
471+ }
472+}
473+
474+func TestFindUserByToken_Valid(t *testing.T) {
475+ cleanupTestData(t)
476+
477+ user, _ := testDB.RegisterUser("validtokenowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI validtokenowner", "comment")
478+ token, _ := testDB.InsertToken(user.ID, "valid-token")
479+
480+ found, err := testDB.FindUserByToken(token)
481+ if err != nil {
482+ t.Fatalf("FindUserByToken failed: %v", err)
483+ }
484+ if found.Name != "validtokenowner" {
485+ t.Errorf("expected name 'validtokenowner', got '%s'", found.Name)
486+ }
487+}
488+
489+func TestFindUserByToken_Expired(t *testing.T) {
490+ cleanupTestData(t)
491+
492+ user, _ := testDB.RegisterUser("expiredtokenowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI expiredtokenowner", "comment")
493+ token, _ := testDB.InsertToken(user.ID, "expired-token")
494+
495+ _, err := testDB.Db.Exec("UPDATE tokens SET expires_at = NOW() - INTERVAL '1 day' WHERE token = $1", token)
496+ if err != nil {
497+ t.Fatalf("failed to expire token: %v", err)
498+ }
499+
500+ _, err = testDB.FindUserByToken(token)
501+ if err == nil {
502+ t.Error("expected error for expired token, got nil")
503+ }
504+}
505+
506+func TestRemoveToken(t *testing.T) {
507+ cleanupTestData(t)
508+
509+ user, _ := testDB.RegisterUser("removetokenowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI removetokenowner", "comment")
510+ _, _ = testDB.InsertToken(user.ID, "remove-token")
511+
512+ tokens, _ := testDB.FindTokensByUser(user.ID)
513+ if len(tokens) != 1 {
514+ t.Fatalf("expected 1 token, got %d", len(tokens))
515+ }
516+
517+ err := testDB.RemoveToken(tokens[0].ID)
518+ if err != nil {
519+ t.Fatalf("RemoveToken failed: %v", err)
520+ }
521+
522+ tokens, _ = testDB.FindTokensByUser(user.ID)
523+ if len(tokens) != 0 {
524+ t.Errorf("expected 0 tokens after removal, got %d", len(tokens))
525+ }
526+}
527+
528+// ============ Post CRUD Tests ============
529+
530+func TestInsertPost(t *testing.T) {
531+ cleanupTestData(t)
532+
533+ user, _ := testDB.RegisterUser("postowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI postowner", "comment")
534+
535+ now := time.Now()
536+ post := &db.Post{
537+ UserID: user.ID,
538+ Filename: "test.md",
539+ Slug: "test-post",
540+ Title: "Test Post",
541+ Text: "Post content",
542+ Description: "A test post",
543+ PublishAt: &now,
544+ UpdatedAt: &now,
545+ Space: "prose",
546+ MimeType: "text/markdown",
547+ }
548+
549+ created, err := testDB.InsertPost(post)
550+ if err != nil {
551+ t.Fatalf("InsertPost failed: %v", err)
552+ }
553+ if created.ID == "" {
554+ t.Error("expected post ID, got empty")
555+ }
556+ if created.Title != "Test Post" {
557+ t.Errorf("expected title 'Test Post', got '%s'", created.Title)
558+ }
559+}
560+
561+func TestUpdatePost(t *testing.T) {
562+ cleanupTestData(t)
563+
564+ user, _ := testDB.RegisterUser("updatepostowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI updatepostowner", "comment")
565+
566+ now := time.Now()
567+ post := mustInsertPost(t, &db.Post{
568+ UserID: user.ID,
569+ Filename: "update.md",
570+ Slug: "update-post",
571+ Title: "Original Title",
572+ Text: "Original content",
573+ Space: "prose",
574+ })
575+
576+ post.Title = "Updated Title"
577+ post.Text = "Updated content"
578+ post.UpdatedAt = &now
579+
580+ updated, err := testDB.UpdatePost(post)
581+ if err != nil {
582+ t.Fatalf("UpdatePost failed: %v", err)
583+ }
584+ if updated.Title != "Updated Title" {
585+ t.Errorf("expected title 'Updated Title', got '%s'", updated.Title)
586+ }
587+}
588+
589+func TestRemovePosts(t *testing.T) {
590+ cleanupTestData(t)
591+
592+ user, _ := testDB.RegisterUser("removepostowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI removepostowner", "comment")
593+
594+ post := mustInsertPost(t, &db.Post{
595+ UserID: user.ID,
596+ Filename: "remove.md",
597+ Slug: "remove-post",
598+ Title: "To Remove",
599+ Space: "prose",
600+ })
601+
602+ err := testDB.RemovePosts([]string{post.ID})
603+ if err != nil {
604+ t.Fatalf("RemovePosts failed: %v", err)
605+ }
606+
607+ _, err = testDB.FindPost(post.ID)
608+ if err == nil {
609+ t.Error("expected error finding removed post, got nil")
610+ }
611+}
612+
613+func TestFindPost(t *testing.T) {
614+ cleanupTestData(t)
615+
616+ user, _ := testDB.RegisterUser("findpostowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI findpostowner", "comment")
617+
618+ created := mustInsertPost(t, &db.Post{
619+ UserID: user.ID,
620+ Filename: "find.md",
621+ Slug: "find-post",
622+ Title: "Find Me",
623+ Space: "prose",
624+ })
625+
626+ found, err := testDB.FindPost(created.ID)
627+ if err != nil {
628+ t.Fatalf("FindPost failed: %v", err)
629+ }
630+ if found.Title != "Find Me" {
631+ t.Errorf("expected title 'Find Me', got '%s'", found.Title)
632+ }
633+}
634+
635+func TestFindPostWithFilename(t *testing.T) {
636+ cleanupTestData(t)
637+
638+ user, _ := testDB.RegisterUser("filenamepostowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI filenamepostowner", "comment")
639+
640+ _ = mustInsertPost(t, &db.Post{
641+ UserID: user.ID,
642+ Filename: "byfilename.md",
643+ Slug: "byfilename-post",
644+ Title: "By Filename",
645+ Space: "prose",
646+ })
647+
648+ found, err := testDB.FindPostWithFilename("byfilename.md", user.ID, "prose")
649+ if err != nil {
650+ t.Fatalf("FindPostWithFilename failed: %v", err)
651+ }
652+ if found.Title != "By Filename" {
653+ t.Errorf("expected title 'By Filename', got '%s'", found.Title)
654+ }
655+}
656+
657+func TestFindPostWithSlug(t *testing.T) {
658+ cleanupTestData(t)
659+
660+ user, _ := testDB.RegisterUser("slugpostowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI slugpostowner", "comment")
661+
662+ _ = mustInsertPost(t, &db.Post{
663+ UserID: user.ID,
664+ Filename: "byslug.md",
665+ Slug: "byslug-post",
666+ Title: "By Slug",
667+ Space: "prose",
668+ })
669+
670+ found, err := testDB.FindPostWithSlug("byslug-post", user.ID, "prose")
671+ if err != nil {
672+ t.Fatalf("FindPostWithSlug failed: %v", err)
673+ }
674+ if found.Title != "By Slug" {
675+ t.Errorf("expected title 'By Slug', got '%s'", found.Title)
676+ }
677+}
678+
679+func TestFindPosts(t *testing.T) {
680+ cleanupTestData(t)
681+
682+ user, _ := testDB.RegisterUser("postsowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI postsowner", "comment")
683+
684+ _ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "post1.md", Slug: "post1", Title: "Post 1", Space: "prose"})
685+ _ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "post2.md", Slug: "post2", Title: "Post 2", Space: "prose"})
686+
687+ posts, err := testDB.FindPosts()
688+ if err != nil {
689+ t.Fatalf("FindPosts failed: %v", err)
690+ }
691+ if len(posts) != 2 {
692+ t.Errorf("expected 2 posts, got %d", len(posts))
693+ }
694+}
695+
696+func TestFindPostsByUser(t *testing.T) {
697+ cleanupTestData(t)
698+
699+ user, _ := testDB.RegisterUser("userpostsowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI userpostsowner", "comment")
700+ now := time.Now()
701+
702+ _ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "userpost1.md", Slug: "userpost1", Title: "User Post 1", Space: "prose", PublishAt: &now})
703+ _ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "userpost2.md", Slug: "userpost2", Title: "User Post 2", Space: "prose", PublishAt: &now})
704+
705+ pager := &db.Pager{Num: 10, Page: 0}
706+ result, err := testDB.FindPostsByUser(pager, user.ID, "prose")
707+ if err != nil {
708+ t.Fatalf("FindPostsByUser failed: %v", err)
709+ }
710+ if len(result.Data) != 2 {
711+ t.Errorf("expected 2 posts, got %d", len(result.Data))
712+ }
713+}
714+
715+func TestFindAllPostsByUser(t *testing.T) {
716+ cleanupTestData(t)
717+
718+ user, _ := testDB.RegisterUser("allpostsowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI allpostsowner", "comment")
719+
720+ _ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "allpost1.md", Slug: "allpost1", Title: "All Post 1", Space: "prose"})
721+ _ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "allpost2.md", Slug: "allpost2", Title: "All Post 2", Space: "prose"})
722+
723+ posts, err := testDB.FindAllPostsByUser(user.ID, "prose")
724+ if err != nil {
725+ t.Fatalf("FindAllPostsByUser failed: %v", err)
726+ }
727+ if len(posts) != 2 {
728+ t.Errorf("expected 2 posts, got %d", len(posts))
729+ }
730+}
731+
732+func TestFindUsersWithPost(t *testing.T) {
733+ cleanupTestData(t)
734+
735+ user, _ := testDB.RegisterUser("feedspostowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI feedspostowner", "comment")
736+
737+ _ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "feed.txt", Slug: "feed", Title: "Feed", Space: "feeds"})
738+
739+ users, err := testDB.FindUsersWithPost("feeds")
740+ if err != nil {
741+ t.Fatalf("FindUsersWithPost failed: %v", err)
742+ }
743+ if len(users) != 1 {
744+ t.Errorf("expected 1 user, got %d", len(users))
745+ }
746+}
747+
748+func TestFindExpiredPosts(t *testing.T) {
749+ cleanupTestData(t)
750+
751+ user, _ := testDB.RegisterUser("expiredpostowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI expiredpostowner", "comment")
752+
753+ expired := time.Now().Add(-24 * time.Hour)
754+ _ = mustInsertPost(t, &db.Post{
755+ UserID: user.ID,
756+ Filename: "expired.txt",
757+ Slug: "expired",
758+ Title: "Expired",
759+ Space: "pastes",
760+ ExpiresAt: &expired,
761+ })
762+
763+ posts, err := testDB.FindExpiredPosts("pastes")
764+ if err != nil {
765+ t.Fatalf("FindExpiredPosts failed: %v", err)
766+ }
767+ if len(posts) != 1 {
768+ t.Errorf("expected 1 expired post, got %d", len(posts))
769+ }
770+}
771+
772+func TestFindPostsByFeed(t *testing.T) {
773+ cleanupTestData(t)
774+
775+ user, _ := testDB.RegisterUser("feedowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI feedowner", "comment")
776+
777+ now := time.Now()
778+ _ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "feedpost.md", Slug: "feedpost", Title: "Feed Post", Space: "prose", PublishAt: &now})
779+
780+ pager := &db.Pager{Num: 10, Page: 0}
781+ result, err := testDB.FindPostsByFeed(pager, "prose")
782+ if err != nil {
783+ t.Fatalf("FindPostsByFeed failed: %v", err)
784+ }
785+ if len(result.Data) < 1 {
786+ t.Errorf("expected at least 1 post in feed, got %d", len(result.Data))
787+ }
788+}
789+
790+// ============ Tags Tests ============
791+
792+func TestReplaceTagsByPost(t *testing.T) {
793+ cleanupTestData(t)
794+
795+ user, _ := testDB.RegisterUser("tagspostowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI tagspostowner", "comment")
796+ post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "tagged.md", Slug: "tagged", Title: "Tagged", Space: "prose"})
797+
798+ err := testDB.ReplaceTagsByPost([]string{"tag1", "tag2"}, post.ID)
799+ if err != nil {
800+ t.Fatalf("ReplaceTagsByPost failed: %v", err)
801+ }
802+
803+ found, _ := testDB.FindPostWithFilename("tagged.md", user.ID, "prose")
804+ if len(found.Tags) != 2 {
805+ t.Errorf("expected 2 tags, got %d", len(found.Tags))
806+ }
807+}
808+
809+func TestFindUserPostsByTag(t *testing.T) {
810+ cleanupTestData(t)
811+
812+ user, _ := testDB.RegisterUser("usertagowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI usertagowner", "comment")
813+ now := time.Now()
814+ post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "usertag.md", Slug: "usertag", Title: "User Tag", Space: "prose", PublishAt: &now})
815+ _ = testDB.ReplaceTagsByPost([]string{"mytag"}, post.ID)
816+
817+ pager := &db.Pager{Num: 10, Page: 0}
818+ result, err := testDB.FindUserPostsByTag(pager, "mytag", user.ID, "prose")
819+ if err != nil {
820+ t.Fatalf("FindUserPostsByTag failed: %v", err)
821+ }
822+ if len(result.Data) != 1 {
823+ t.Errorf("expected 1 post with tag, got %d", len(result.Data))
824+ }
825+}
826+
827+func TestFindPostsByTag(t *testing.T) {
828+ cleanupTestData(t)
829+
830+ user, _ := testDB.RegisterUser("tagsearchowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI tagsearchowner", "comment")
831+ now := time.Now()
832+ post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "tagsearch.md", Slug: "tagsearch", Title: "Tag Search", Space: "prose", PublishAt: &now})
833+ _ = testDB.ReplaceTagsByPost([]string{"searchtag"}, post.ID)
834+
835+ pager := &db.Pager{Num: 10, Page: 0}
836+ result, err := testDB.FindPostsByTag(pager, "searchtag", "prose")
837+ if err != nil {
838+ t.Fatalf("FindPostsByTag failed: %v", err)
839+ }
840+ if len(result.Data) != 1 {
841+ t.Errorf("expected 1 post with tag, got %d", len(result.Data))
842+ }
843+}
844+
845+func TestFindPopularTags(t *testing.T) {
846+ cleanupTestData(t)
847+
848+ user, _ := testDB.RegisterUser("populartagowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI populartagowner", "comment")
849+ post1 := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "pop1.md", Slug: "pop1", Title: "Pop 1", Space: "prose"})
850+ post2 := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "pop2.md", Slug: "pop2", Title: "Pop 2", Space: "prose"})
851+ _ = testDB.ReplaceTagsByPost([]string{"popular"}, post1.ID)
852+ _ = testDB.ReplaceTagsByPost([]string{"popular"}, post2.ID)
853+
854+ tags, err := testDB.FindPopularTags("prose")
855+ if err != nil {
856+ t.Fatalf("FindPopularTags failed: %v", err)
857+ }
858+ if len(tags) < 1 {
859+ t.Errorf("expected at least 1 popular tag, got %d", len(tags))
860+ }
861+}
862+
863+// ============ Aliases Tests ============
864+
865+func TestReplaceAliasesByPost(t *testing.T) {
866+ cleanupTestData(t)
867+
868+ user, _ := testDB.RegisterUser("aliasowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI aliasowner", "comment")
869+ post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "aliased.md", Slug: "aliased", Title: "Aliased", Space: "prose"})
870+
871+ err := testDB.ReplaceAliasesByPost([]string{"alias1", "alias2"}, post.ID)
872+ if err != nil {
873+ t.Fatalf("ReplaceAliasesByPost failed: %v", err)
874+ }
875+}
876+
877+func TestFindPostWithSlug_Alias(t *testing.T) {
878+ cleanupTestData(t)
879+
880+ user, _ := testDB.RegisterUser("aliassearchowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI aliassearchowner", "comment")
881+ post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "original.md", Slug: "original", Title: "Original", Space: "prose"})
882+ _ = testDB.ReplaceAliasesByPost([]string{"my-alias"}, post.ID)
883+
884+ found, err := testDB.FindPostWithSlug("my-alias", user.ID, "prose")
885+ if err != nil {
886+ t.Fatalf("FindPostWithSlug for alias failed: %v", err)
887+ }
888+ if found.Title != "Original" {
889+ t.Errorf("expected title 'Original', got '%s'", found.Title)
890+ }
891+}
892+
893+// ============ Analytics Tests ============
894+
895+func TestInsertVisit(t *testing.T) {
896+ cleanupTestData(t)
897+
898+ user, _ := testDB.RegisterUser("visitowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI visitowner", "comment")
899+
900+ visit := &db.AnalyticsVisits{
901+ UserID: user.ID,
902+ Host: "example.com",
903+ Path: "/test",
904+ IpAddress: "192.168.1.1",
905+ UserAgent: "TestAgent/1.0",
906+ Referer: "https://referrer.com",
907+ Status: 200,
908+ }
909+
910+ err := testDB.InsertVisit(visit)
911+ if err != nil {
912+ t.Fatalf("InsertVisit failed: %v", err)
913+ }
914+}
915+
916+func TestVisitSummary(t *testing.T) {
917+ cleanupTestData(t)
918+
919+ user, _ := testDB.RegisterUser("summaryowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI summaryowner", "comment")
920+
921+ visit := &db.AnalyticsVisits{
922+ UserID: user.ID,
923+ Host: "summary.com",
924+ Path: "/page",
925+ IpAddress: "192.168.1.2",
926+ Status: 200,
927+ }
928+ _ = testDB.InsertVisit(visit)
929+
930+ opts := &db.SummaryOpts{
931+ Interval: "day",
932+ Origin: time.Now().Add(-24 * time.Hour),
933+ Host: "summary.com",
934+ UserID: user.ID,
935+ }
936+
937+ summary, err := testDB.VisitSummary(opts)
938+ if err != nil {
939+ t.Fatalf("VisitSummary failed: %v", err)
940+ }
941+ if summary == nil {
942+ t.Error("expected summary, got nil")
943+ }
944+}
945+
946+func TestFindVisitSiteList(t *testing.T) {
947+ cleanupTestData(t)
948+
949+ user, _ := testDB.RegisterUser("sitelistowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI sitelistowner", "comment")
950+
951+ _ = testDB.InsertVisit(&db.AnalyticsVisits{
952+ UserID: user.ID,
953+ Host: "site1.com",
954+ Path: "/",
955+ IpAddress: "192.168.1.3",
956+ Status: 200,
957+ })
958+
959+ opts := &db.SummaryOpts{UserID: user.ID}
960+ sites, err := testDB.FindVisitSiteList(opts)
961+ if err != nil {
962+ t.Fatalf("FindVisitSiteList failed: %v", err)
963+ }
964+ if len(sites) < 1 {
965+ t.Errorf("expected at least 1 site, got %d", len(sites))
966+ }
967+}
968+
969+func TestVisitUrlNotFound(t *testing.T) {
970+ cleanupTestData(t)
971+
972+ user, _ := testDB.RegisterUser("notfoundowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI notfoundowner", "comment")
973+
974+ _ = testDB.InsertVisit(&db.AnalyticsVisits{
975+ UserID: user.ID,
976+ Host: "notfound.com",
977+ Path: "/missing",
978+ IpAddress: "192.168.1.4",
979+ Status: 404,
980+ })
981+
982+ opts := &db.SummaryOpts{
983+ Origin: time.Now().Add(-24 * time.Hour),
984+ Host: "notfound.com",
985+ UserID: user.ID,
986+ }
987+ notFound, err := testDB.VisitUrlNotFound(opts)
988+ if err != nil {
989+ t.Fatalf("VisitUrlNotFound failed: %v", err)
990+ }
991+ if len(notFound) < 1 {
992+ t.Errorf("expected at least 1 not found URL, got %d", len(notFound))
993+ }
994+}
995+
996+// ============ Features Tests ============
997+
998+func TestInsertFeature(t *testing.T) {
999+ cleanupTestData(t)
1000+
1001+ user, _ := testDB.RegisterUser("featureowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI featureowner", "comment")
1002+
1003+ expiresAt := time.Now().Add(365 * 24 * time.Hour)
1004+ feature, err := testDB.InsertFeature(user.ID, "plus", expiresAt)
1005+ if err != nil {
1006+ t.Fatalf("InsertFeature failed: %v", err)
1007+ }
1008+ if feature.Name != "plus" {
1009+ t.Errorf("expected feature name 'plus', got '%s'", feature.Name)
1010+ }
1011+}
1012+
1013+func TestFindFeature(t *testing.T) {
1014+ cleanupTestData(t)
1015+
1016+ user, _ := testDB.RegisterUser("findfeatureowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI findfeatureowner", "comment")
1017+ expiresAt := time.Now().Add(365 * 24 * time.Hour)
1018+ _, _ = testDB.InsertFeature(user.ID, "plus", expiresAt)
1019+
1020+ feature, err := testDB.FindFeature(user.ID, "plus")
1021+ if err != nil {
1022+ t.Fatalf("FindFeature failed: %v", err)
1023+ }
1024+ if feature.Name != "plus" {
1025+ t.Errorf("expected feature name 'plus', got '%s'", feature.Name)
1026+ }
1027+}
1028+
1029+func TestFindFeaturesByUser(t *testing.T) {
1030+ cleanupTestData(t)
1031+
1032+ user, _ := testDB.RegisterUser("featuresowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI featuresowner", "comment")
1033+ expiresAt := time.Now().Add(365 * 24 * time.Hour)
1034+ _, _ = testDB.InsertFeature(user.ID, "plus", expiresAt)
1035+ _, _ = testDB.InsertFeature(user.ID, "pro", expiresAt)
1036+
1037+ features, err := testDB.FindFeaturesByUser(user.ID)
1038+ if err != nil {
1039+ t.Fatalf("FindFeaturesByUser failed: %v", err)
1040+ }
1041+ if len(features) != 2 {
1042+ t.Errorf("expected 2 features, got %d", len(features))
1043+ }
1044+}
1045+
1046+func TestHasFeatureByUser(t *testing.T) {
1047+ cleanupTestData(t)
1048+
1049+ user, _ := testDB.RegisterUser("hasfeatureowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI hasfeatureowner", "comment")
1050+ expiresAt := time.Now().Add(365 * 24 * time.Hour)
1051+ _, _ = testDB.InsertFeature(user.ID, "plus", expiresAt)
1052+
1053+ has := testDB.HasFeatureByUser(user.ID, "plus")
1054+ if !has {
1055+ t.Error("expected HasFeatureByUser to return true")
1056+ }
1057+
1058+ hasNot := testDB.HasFeatureByUser(user.ID, "nonexistent")
1059+ if hasNot {
1060+ t.Error("expected HasFeatureByUser to return false for nonexistent feature")
1061+ }
1062+}
1063+
1064+func TestRemoveFeature(t *testing.T) {
1065+ cleanupTestData(t)
1066+
1067+ user, _ := testDB.RegisterUser("removefeatureowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI removefeatureowner", "comment")
1068+ expiresAt := time.Now().Add(365 * 24 * time.Hour)
1069+ _, _ = testDB.InsertFeature(user.ID, "plus", expiresAt)
1070+
1071+ err := testDB.RemoveFeature(user.ID, "plus")
1072+ if err != nil {
1073+ t.Fatalf("RemoveFeature failed: %v", err)
1074+ }
1075+
1076+ _, err = testDB.FindFeature(user.ID, "plus")
1077+ if err == nil {
1078+ t.Error("expected error finding removed feature, got nil")
1079+ }
1080+}
1081+
1082+func TestAddPicoPlusUser(t *testing.T) {
1083+ cleanupTestData(t)
1084+
1085+ _, _ = testDB.RegisterUser("picoplusowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI picoplusowner", "comment")
1086+
1087+ err := testDB.AddPicoPlusUser("picoplusowner", "test@example.com", "stripe", "tx123")
1088+ if err != nil {
1089+ t.Fatalf("AddPicoPlusUser failed: %v", err)
1090+ }
1091+}
1092+
1093+// ============ Feed Items Tests ============
1094+
1095+func TestInsertFeedItems(t *testing.T) {
1096+ cleanupTestData(t)
1097+
1098+ user, _ := testDB.RegisterUser("feeditemsowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI feeditemsowner", "comment")
1099+ post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "feed.txt", Slug: "feed", Title: "Feed", Space: "feeds"})
1100+
1101+ items := []*db.FeedItem{
1102+ {PostID: post.ID, GUID: "guid-1", Data: db.FeedItemData{Title: "Item 1", Link: "http://example.com/1"}},
1103+ {PostID: post.ID, GUID: "guid-2", Data: db.FeedItemData{Title: "Item 2", Link: "http://example.com/2"}},
1104+ }
1105+
1106+ err := testDB.InsertFeedItems(post.ID, items)
1107+ if err != nil {
1108+ t.Fatalf("InsertFeedItems failed: %v", err)
1109+ }
1110+}
1111+
1112+func TestFindFeedItemsByPostID(t *testing.T) {
1113+ cleanupTestData(t)
1114+
1115+ user, _ := testDB.RegisterUser("findfeeditemsowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI findfeeditemsowner", "comment")
1116+ post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "findfeed.txt", Slug: "findfeed", Title: "Find Feed", Space: "feeds"})
1117+
1118+ items := []*db.FeedItem{
1119+ {PostID: post.ID, GUID: "find-guid-1", Data: db.FeedItemData{Title: "Find Item 1"}},
1120+ }
1121+ _ = testDB.InsertFeedItems(post.ID, items)
1122+
1123+ found, err := testDB.FindFeedItemsByPostID(post.ID)
1124+ if err != nil {
1125+ t.Fatalf("FindFeedItemsByPostID failed: %v", err)
1126+ }
1127+ if len(found) != 1 {
1128+ t.Errorf("expected 1 feed item, got %d", len(found))
1129+ }
1130+}
1131+
1132+// ============ Projects Tests ============
1133+
1134+func TestUpsertProject(t *testing.T) {
1135+ cleanupTestData(t)
1136+
1137+ user, _ := testDB.RegisterUser("projectowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI projectowner", "comment")
1138+
1139+ project, err := testDB.UpsertProject(user.ID, "my-project", "my-project")
1140+ if err != nil {
1141+ t.Fatalf("UpsertProject failed: %v", err)
1142+ }
1143+ if project.Name != "my-project" {
1144+ t.Errorf("expected project name 'my-project', got '%s'", project.Name)
1145+ }
1146+
1147+ project2, err := testDB.UpsertProject(user.ID, "my-project", "my-project")
1148+ if err != nil {
1149+ t.Fatalf("UpsertProject (update) failed: %v", err)
1150+ }
1151+ if project2.ID != project.ID {
1152+ t.Error("expected same project ID on upsert")
1153+ }
1154+}
1155+
1156+func TestFindProjectByName(t *testing.T) {
1157+ cleanupTestData(t)
1158+
1159+ user, _ := testDB.RegisterUser("findprojectowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI findprojectowner", "comment")
1160+ _, _ = testDB.UpsertProject(user.ID, "findme-project", "findme-project")
1161+
1162+ project, err := testDB.FindProjectByName(user.ID, "findme-project")
1163+ if err != nil {
1164+ t.Fatalf("FindProjectByName failed: %v", err)
1165+ }
1166+ if project.Name != "findme-project" {
1167+ t.Errorf("expected project name 'findme-project', got '%s'", project.Name)
1168+ }
1169+}
1170+
1171+// ============ User Stats Tests ============
1172+
1173+func TestFindUserStats(t *testing.T) {
1174+ cleanupTestData(t)
1175+
1176+ user, _ := testDB.RegisterUser("statsowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI statsowner", "comment")
1177+ _ = mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "stat.md", Slug: "stat", Title: "Stat", Space: "prose"})
1178+ _, _ = testDB.UpsertProject(user.ID, "stat-project", "stat-project")
1179+
1180+ stats, err := testDB.FindUserStats(user.ID)
1181+ if err != nil {
1182+ t.Fatalf("FindUserStats failed: %v", err)
1183+ }
1184+ if stats.Prose.Num != 1 {
1185+ t.Errorf("expected 1 prose post, got %d", stats.Prose.Num)
1186+ }
1187+ if stats.Pages.Num != 1 {
1188+ t.Errorf("expected 1 project, got %d", stats.Pages.Num)
1189+ }
1190+}
1191+
1192+// ============ Tuns Event Logs Tests ============
1193+
1194+func TestInsertTunsEventLog(t *testing.T) {
1195+ cleanupTestData(t)
1196+
1197+ user, _ := testDB.RegisterUser("tunsowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI tunsowner", "comment")
1198+
1199+ log := &db.TunsEventLog{
1200+ UserId: user.ID,
1201+ ServerID: "server-1",
1202+ RemoteAddr: "192.168.1.1:1234",
1203+ EventType: "connect",
1204+ TunnelType: "http",
1205+ ConnectionType: "tcp",
1206+ TunnelID: "tunnel-123",
1207+ }
1208+
1209+ err := testDB.InsertTunsEventLog(log)
1210+ if err != nil {
1211+ t.Fatalf("InsertTunsEventLog failed: %v", err)
1212+ }
1213+}
1214+
1215+func TestFindTunsEventLogs(t *testing.T) {
1216+ cleanupTestData(t)
1217+
1218+ user, _ := testDB.RegisterUser("findtunsowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI findtunsowner", "comment")
1219+ _ = testDB.InsertTunsEventLog(&db.TunsEventLog{
1220+ UserId: user.ID,
1221+ ServerID: "server-1",
1222+ RemoteAddr: "192.168.1.1:1234",
1223+ EventType: "connect",
1224+ TunnelType: "http",
1225+ ConnectionType: "tcp",
1226+ TunnelID: "tunnel-456",
1227+ })
1228+
1229+ logs, err := testDB.FindTunsEventLogs(user.ID)
1230+ if err != nil {
1231+ t.Fatalf("FindTunsEventLogs failed: %v", err)
1232+ }
1233+ if len(logs) != 1 {
1234+ t.Errorf("expected 1 log, got %d", len(logs))
1235+ }
1236+}
1237+
1238+func TestFindTunsEventLogsByAddr(t *testing.T) {
1239+ cleanupTestData(t)
1240+
1241+ user, _ := testDB.RegisterUser("tunsaddrowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI tunsaddrowner", "comment")
1242+ _ = testDB.InsertTunsEventLog(&db.TunsEventLog{
1243+ UserId: user.ID,
1244+ ServerID: "server-1",
1245+ RemoteAddr: "192.168.1.1:1234",
1246+ EventType: "connect",
1247+ TunnelType: "http",
1248+ ConnectionType: "tcp",
1249+ TunnelID: "tunnel-789",
1250+ })
1251+
1252+ logs, err := testDB.FindTunsEventLogsByAddr(user.ID, "tunnel-789")
1253+ if err != nil {
1254+ t.Fatalf("FindTunsEventLogsByAddr failed: %v", err)
1255+ }
1256+ if len(logs) != 1 {
1257+ t.Errorf("expected 1 log, got %d", len(logs))
1258+ }
1259+}
1260+
1261+// ============ Access Logs Tests ============
1262+
1263+func TestInsertAccessLog(t *testing.T) {
1264+ cleanupTestData(t)
1265+
1266+ user, _ := testDB.RegisterUser("accesslogowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI accesslogowner", "comment")
1267+
1268+ log := &db.AccessLog{
1269+ UserID: user.ID,
1270+ Service: "pgs",
1271+ Pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI accesslogowner",
1272+ Identity: "accesslogowner",
1273+ }
1274+
1275+ err := testDB.InsertAccessLog(log)
1276+ if err != nil {
1277+ t.Fatalf("InsertAccessLog failed: %v", err)
1278+ }
1279+}
1280+
1281+func TestFindAccessLogs(t *testing.T) {
1282+ cleanupTestData(t)
1283+
1284+ user, _ := testDB.RegisterUser("findaccesslogowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI findaccesslogowner", "comment")
1285+ _ = testDB.InsertAccessLog(&db.AccessLog{
1286+ UserID: user.ID,
1287+ Service: "pgs",
1288+ Pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI findaccesslogowner",
1289+ Identity: "findaccesslogowner",
1290+ })
1291+
1292+ fromDate := time.Now().Add(-24 * time.Hour)
1293+ logs, err := testDB.FindAccessLogs(user.ID, &fromDate)
1294+ if err != nil {
1295+ t.Fatalf("FindAccessLogs failed: %v", err)
1296+ }
1297+ if len(logs) != 1 {
1298+ t.Errorf("expected 1 log, got %d", len(logs))
1299+ }
1300+}
1301+
1302+func TestFindPubkeysInAccessLogs(t *testing.T) {
1303+ cleanupTestData(t)
1304+
1305+ user, _ := testDB.RegisterUser("pubkeylogowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI pubkeylogowner", "comment")
1306+ _ = testDB.InsertAccessLog(&db.AccessLog{
1307+ UserID: user.ID,
1308+ Service: "pgs",
1309+ Pubkey: "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI pubkeylogowner",
1310+ Identity: "pubkeylogowner",
1311+ })
1312+
1313+ pubkeys, err := testDB.FindPubkeysInAccessLogs(user.ID)
1314+ if err != nil {
1315+ t.Fatalf("FindPubkeysInAccessLogs failed: %v", err)
1316+ }
1317+ if len(pubkeys) != 1 {
1318+ t.Errorf("expected 1 pubkey, got %d", len(pubkeys))
1319+ }
1320+}
1321+
1322+func TestFindAccessLogsByPubkey(t *testing.T) {
1323+ cleanupTestData(t)
1324+
1325+ user, _ := testDB.RegisterUser("accessbypubkeyowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI accessbypubkeyowner", "comment")
1326+ pubkey := "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI accessbypubkeyowner"
1327+ _ = testDB.InsertAccessLog(&db.AccessLog{
1328+ UserID: user.ID,
1329+ Service: "pgs",
1330+ Pubkey: pubkey,
1331+ Identity: "accessbypubkeyowner",
1332+ })
1333+
1334+ fromDate := time.Now().Add(-24 * time.Hour)
1335+ logs, err := testDB.FindAccessLogsByPubkey(pubkey, &fromDate)
1336+ if err != nil {
1337+ t.Fatalf("FindAccessLogsByPubkey failed: %v", err)
1338+ }
1339+ if len(logs) != 1 {
1340+ t.Errorf("expected 1 log, got %d", len(logs))
1341+ }
1342+}
1343+
1344+// ============ JSONB Roundtrip Tests ============
1345+
1346+func TestPostData_JSONBRoundtrip(t *testing.T) {
1347+ cleanupTestData(t)
1348+
1349+ user, _ := testDB.RegisterUser("postdataowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI postdataowner", "comment")
1350+
1351+ now := time.Now().Truncate(time.Second)
1352+ postData := db.PostData{
1353+ ImgPath: "/images/test.png",
1354+ LastDigest: &now,
1355+ Attempts: 5,
1356+ }
1357+
1358+ post := mustInsertPost(t, &db.Post{
1359+ UserID: user.ID,
1360+ Filename: "jsonb.md",
1361+ Slug: "jsonb",
1362+ Title: "JSONB Test",
1363+ Space: "prose",
1364+ Data: postData,
1365+ })
1366+
1367+ found, err := testDB.FindPost(post.ID)
1368+ if err != nil {
1369+ t.Fatalf("FindPost failed: %v", err)
1370+ }
1371+ if found.Data.ImgPath != "/images/test.png" {
1372+ t.Errorf("expected ImgPath '/images/test.png', got '%s'", found.Data.ImgPath)
1373+ }
1374+ if found.Data.Attempts != 5 {
1375+ t.Errorf("expected Attempts 5, got %d", found.Data.Attempts)
1376+ }
1377+}
1378+
1379+func TestProjectAcl_JSONBRoundtrip(t *testing.T) {
1380+ cleanupTestData(t)
1381+
1382+ user, _ := testDB.RegisterUser("aclowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI aclowner", "comment")
1383+
1384+ project, err := testDB.UpsertProject(user.ID, "acl-project", "acl-project")
1385+ if err != nil {
1386+ t.Fatalf("UpsertProject failed: %v", err)
1387+ }
1388+
1389+ found, err := testDB.FindProjectByName(user.ID, "acl-project")
1390+ if err != nil {
1391+ t.Fatalf("FindProjectByName failed: %v", err)
1392+ }
1393+ if found.Acl.Type != "public" {
1394+ t.Errorf("expected Acl.Type 'public', got '%s'", found.Acl.Type)
1395+ }
1396+ _ = project
1397+}
1398+
1399+func TestFeedItemData_JSONBRoundtrip(t *testing.T) {
1400+ cleanupTestData(t)
1401+
1402+ user, _ := testDB.RegisterUser("feedjsonbowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI feedjsonbowner", "comment")
1403+ post := mustInsertPost(t, &db.Post{UserID: user.ID, Filename: "feedjsonb.txt", Slug: "feedjsonb", Title: "Feed JSONB", Space: "feeds"})
1404+
1405+ now := time.Now().Truncate(time.Second)
1406+ items := []*db.FeedItem{
1407+ {
1408+ PostID: post.ID,
1409+ GUID: "jsonb-guid",
1410+ Data: db.FeedItemData{
1411+ Title: "JSONB Item",
1412+ Description: "Description",
1413+ Content: "Content",
1414+ Link: "http://example.com",
1415+ PublishedAt: &now,
1416+ },
1417+ },
1418+ }
1419+ _ = testDB.InsertFeedItems(post.ID, items)
1420+
1421+ found, err := testDB.FindFeedItemsByPostID(post.ID)
1422+ if err != nil {
1423+ t.Fatalf("FindFeedItemsByPostID failed: %v", err)
1424+ }
1425+ if len(found) != 1 {
1426+ t.Fatalf("expected 1 item, got %d", len(found))
1427+ }
1428+ if found[0].Data.Title != "JSONB Item" {
1429+ t.Errorf("expected title 'JSONB Item', got '%s'", found[0].Data.Title)
1430+ }
1431+}
1432+
1433+func TestFeatureFlagData_JSONBRoundtrip(t *testing.T) {
1434+ cleanupTestData(t)
1435+
1436+ user, _ := testDB.RegisterUser("featurejsonbowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI featurejsonbowner", "comment")
1437+
1438+ err := testDB.AddPicoPlusUser("featurejsonbowner", "test@example.com", "stripe", "tx456")
1439+ if err != nil {
1440+ t.Fatalf("AddPicoPlusUser failed: %v", err)
1441+ }
1442+
1443+ feature, err := testDB.FindFeature(user.ID, "plus")
1444+ if err != nil {
1445+ t.Fatalf("FindFeature failed: %v", err)
1446+ }
1447+ if feature.Data.StorageMax != 10000000000 {
1448+ t.Errorf("expected StorageMax 10000000000, got %d", feature.Data.StorageMax)
1449+ }
1450+ if feature.Data.FileMax != 50000000 {
1451+ t.Errorf("expected FileMax 50000000, got %d", feature.Data.FileMax)
1452+ }
1453+}
1454+
1455+func TestPaymentHistoryData_JSONBRoundtrip(t *testing.T) {
1456+ cleanupTestData(t)
1457+
1458+ user, _ := testDB.RegisterUser("paymentjsonbowner", "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI paymentjsonbowner", "comment")
1459+
1460+ err := testDB.AddPicoPlusUser("paymentjsonbowner", "payment@example.com", "stripe", "tx789")
1461+ if err != nil {
1462+ t.Fatalf("AddPicoPlusUser failed: %v", err)
1463+ }
1464+
1465+ var txId string
1466+ err = testDB.Db.QueryRow("SELECT data->>'tx_id' FROM payment_history WHERE user_id = $1", user.ID).Scan(&txId)
1467+ if err != nil {
1468+ t.Fatalf("failed to query payment history: %v", err)
1469+ }
1470+ if txId != "tx789" {
1471+ t.Errorf("expected tx_id 'tx789', got '%s'", txId)
1472+ }
1473+}
+13,
-74
1@@ -1,7 +1,6 @@
2 package stub
3
4 import (
5- "database/sql"
6 "fmt"
7 "log/slog"
8 "time"
9@@ -29,27 +28,15 @@ func (me *StubDB) RegisterUser(username, pubkey, comment string) (*db.User, erro
10 return nil, errNotImpl
11 }
12
13-func (me *StubDB) RemoveUsers(userIDs []string) error {
14- return errNotImpl
15-}
16-
17-func (me *StubDB) InsertPublicKey(userID, key, name string, tx *sql.Tx) error {
18- return errNotImpl
19-}
20-
21 func (me *StubDB) UpdatePublicKey(pubkeyID, name string) (*db.PublicKey, error) {
22 return nil, errNotImpl
23 }
24
25-func (me *StubDB) FindPublicKeyForKey(key string) (*db.PublicKey, error) {
26- return nil, errNotImpl
27-}
28-
29-func (me *StubDB) FindPublicKey(pubkeyID string) (*db.PublicKey, error) {
30- return nil, errNotImpl
31+func (me *StubDB) InsertPublicKey(userID, key, name string) error {
32+ return errNotImpl
33 }
34
35-func (me *StubDB) FindKeysForUser(user *db.User) ([]*db.PublicKey, error) {
36+func (me *StubDB) FindKeysByUser(user *db.User) ([]*db.PublicKey, error) {
37 return []*db.PublicKey{}, errNotImpl
38 }
39
40@@ -57,11 +44,7 @@ func (me *StubDB) RemoveKeys(keyIDs []string) error {
41 return errNotImpl
42 }
43
44-func (me *StubDB) FindPostsBeforeDate(date *time.Time, space string) ([]*db.Post, error) {
45- return []*db.Post{}, errNotImpl
46-}
47-
48-func (me *StubDB) FindUserForKey(username string, key string) (*db.User, error) {
49+func (me *StubDB) FindUserByKey(username string, key string) (*db.User, error) {
50 return nil, errNotImpl
51 }
52
53@@ -73,26 +56,14 @@ func (me *StubDB) FindUser(userID string) (*db.User, error) {
54 return nil, errNotImpl
55 }
56
57-func (me *StubDB) ValidateName(name string) (bool, error) {
58- return false, errNotImpl
59-}
60-
61 func (me *StubDB) FindUserByName(name string) (*db.User, error) {
62 return nil, errNotImpl
63 }
64
65-func (me *StubDB) FindUserForNameAndKey(name string, key string) (*db.User, error) {
66+func (me *StubDB) FindUserByToken(token string) (*db.User, error) {
67 return nil, errNotImpl
68 }
69
70-func (me *StubDB) FindUserForToken(token string) (*db.User, error) {
71- return nil, errNotImpl
72-}
73-
74-func (me *StubDB) SetUserName(userID string, name string) error {
75- return errNotImpl
76-}
77-
78 func (me *StubDB) FindPostWithFilename(filename string, persona_id string, space string) (*db.Post, error) {
79 return nil, errNotImpl
80 }
81@@ -105,11 +76,7 @@ func (me *StubDB) FindPost(postID string) (*db.Post, error) {
82 return nil, errNotImpl
83 }
84
85-func (me *StubDB) FindPostsForFeed(page *db.Pager, space string) (*db.Paginate[*db.Post], error) {
86- return &db.Paginate[*db.Post]{}, errNotImpl
87-}
88-
89-func (me *StubDB) FindAllUpdatedPosts(page *db.Pager, space string) (*db.Paginate[*db.Post], error) {
90+func (me *StubDB) FindPostsByFeed(page *db.Pager, space string) (*db.Paginate[*db.Post], error) {
91 return &db.Paginate[*db.Post]{}, errNotImpl
92 }
93
94@@ -125,11 +92,11 @@ func (me *StubDB) RemovePosts(postIDs []string) error {
95 return errNotImpl
96 }
97
98-func (me *StubDB) FindPostsForUser(page *db.Pager, userID string, space string) (*db.Paginate[*db.Post], error) {
99+func (me *StubDB) FindPostsByUser(page *db.Pager, userID string, space string) (*db.Paginate[*db.Post], error) {
100 return &db.Paginate[*db.Post]{}, errNotImpl
101 }
102
103-func (me *StubDB) FindAllPostsForUser(userID string, space string) ([]*db.Post, error) {
104+func (me *StubDB) FindAllPostsByUser(userID string, space string) ([]*db.Post, error) {
105 return []*db.Post{}, errNotImpl
106 }
107
108@@ -141,10 +108,6 @@ func (me *StubDB) FindExpiredPosts(space string) ([]*db.Post, error) {
109 return []*db.Post{}, errNotImpl
110 }
111
112-func (me *StubDB) FindUpdatedPostsForUser(userID string, space string) ([]*db.Post, error) {
113- return []*db.Post{}, errNotImpl
114-}
115-
116 func (me *StubDB) Close() error {
117 return errNotImpl
118 }
119@@ -165,11 +128,11 @@ func (me *StubDB) FindUsers() ([]*db.User, error) {
120 return []*db.User{}, errNotImpl
121 }
122
123-func (me *StubDB) ReplaceTagsForPost(tags []string, postID string) error {
124+func (me *StubDB) ReplaceTagsByPost(tags []string, postID string) error {
125 return errNotImpl
126 }
127
128-func (me *StubDB) ReplaceAliasesForPost(aliases []string, postID string) error {
129+func (me *StubDB) ReplaceAliasesByPost(aliases []string, postID string) error {
130 return errNotImpl
131 }
132
133@@ -185,26 +148,18 @@ func (me *StubDB) FindPopularTags(space string) ([]string, error) {
134 return []string{}, errNotImpl
135 }
136
137-func (me *StubDB) FindTagsForPost(postID string) ([]string, error) {
138- return []string{}, errNotImpl
139-}
140-
141 func (me *StubDB) FindFeature(userID string, feature string) (*db.FeatureFlag, error) {
142 return nil, errNotImpl
143 }
144
145-func (me *StubDB) FindFeaturesForUser(userID string) ([]*db.FeatureFlag, error) {
146+func (me *StubDB) FindFeaturesByUser(userID string) ([]*db.FeatureFlag, error) {
147 return []*db.FeatureFlag{}, errNotImpl
148 }
149
150-func (me *StubDB) HasFeatureForUser(userID string, feature string) bool {
151+func (me *StubDB) HasFeatureByUser(userID string, feature string) bool {
152 return false
153 }
154
155-func (me *StubDB) FindTotalSizeForUser(userID string) (int, error) {
156- return 0, errNotImpl
157-}
158-
159 func (me *StubDB) InsertFeedItems(postID string, items []*db.FeedItem) error {
160 return errNotImpl
161 }
162@@ -217,14 +172,6 @@ func (me *StubDB) UpsertProject(userID, name, projectDir string) (*db.Project, e
163 return nil, errNotImpl
164 }
165
166-func (me *StubDB) InsertProject(userID, name, projectDir string) (string, error) {
167- return "", errNotImpl
168-}
169-
170-func (me *StubDB) UpdateProject(userID, name string) error {
171- return errNotImpl
172-}
173-
174 func (me *StubDB) FindProjectByName(userID, name string) (*db.Project, error) {
175 return &db.Project{}, errNotImpl
176 }
177@@ -237,15 +184,11 @@ func (me *StubDB) UpsertToken(userID, name string) (string, error) {
178 return "", errNotImpl
179 }
180
181-func (me *StubDB) FindTokenByName(userID, name string) (string, error) {
182- return "", errNotImpl
183-}
184-
185 func (me *StubDB) RemoveToken(tokenID string) error {
186 return errNotImpl
187 }
188
189-func (me *StubDB) FindTokensForUser(userID string) ([]*db.Token, error) {
190+func (me *StubDB) FindTokensByUser(userID string) ([]*db.Token, error) {
191 return []*db.Token{}, errNotImpl
192 }
193
194@@ -261,10 +204,6 @@ func (me *StubDB) AddPicoPlusUser(username, email, paymentType, txId string) err
195 return errNotImpl
196 }
197
198-func (me *StubDB) FindTagsForUser(userID string, tag string) ([]string, error) {
199- return []string{}, errNotImpl
200-}
201-
202 func (me *StubDB) FindUserStats(userID string) (*db.UserStats, error) {
203 return nil, errNotImpl
204 }
+4,
-4
1@@ -197,7 +197,7 @@ func (h *ScpUploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.
2 "aliases",
3 strings.Join(metadata.Aliases, ","),
4 )
5- err = h.DBPool.ReplaceAliasesForPost(metadata.Aliases, post.ID)
6+ err = h.DBPool.ReplaceAliasesByPost(metadata.Aliases, post.ID)
7 if err != nil {
8 logger.Error("post could not replace aliases", "err", err.Error())
9 return "", fmt.Errorf("error for %s: %v", filename, err)
10@@ -209,7 +209,7 @@ func (h *ScpUploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.
11 "found post tags, replacing with old tags",
12 "tags", strings.Join(metadata.Tags, ","),
13 )
14- err = h.DBPool.ReplaceTagsForPost(metadata.Tags, post.ID)
15+ err = h.DBPool.ReplaceTagsByPost(metadata.Tags, post.ID)
16 if err != nil {
17 logger.Error("post could not replace tags", "err", err.Error())
18 return "", fmt.Errorf("error for %s: %v", filename, err)
19@@ -249,7 +249,7 @@ func (h *ScpUploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.
20 "found post tags, replacing with old tags",
21 "tags", strings.Join(metadata.Tags, ","),
22 )
23- err = h.DBPool.ReplaceTagsForPost(metadata.Tags, post.ID)
24+ err = h.DBPool.ReplaceTagsByPost(metadata.Tags, post.ID)
25 if err != nil {
26 logger.Error("post could not replace tags", "err", err.Error())
27 return "", fmt.Errorf("error for %s: %v", filename, err)
28@@ -259,7 +259,7 @@ func (h *ScpUploadHandler) Write(s *pssh.SSHServerConnSession, entry *sendutils.
29 "found post aliases, replacing with old aliases",
30 "aliases", strings.Join(metadata.Aliases, ","),
31 )
32- err = h.DBPool.ReplaceAliasesForPost(metadata.Aliases, post.ID)
33+ err = h.DBPool.ReplaceAliasesByPost(metadata.Aliases, post.ID)
34 if err != nil {
35 logger.Error("post could not replace aliases", "err", err.Error())
36 return "", fmt.Errorf("error for %s: %v", filename, err)
+1,
-1
1@@ -151,7 +151,7 @@ func BaseList(s *pssh.SSHServerConnSession, fpath string, isDir bool, recursive
2 })
3
4 for _, space := range spaces {
5- curPosts, e := dbpool.FindAllPostsForUser(user.ID, space)
6+ curPosts, e := dbpool.FindAllPostsByUser(user.ID, space)
7 if e != nil {
8 err = e
9 break
1@@ -155,7 +155,7 @@ func CleanHost(raw string) (string, error) {
2 var ErrAnalyticsDisabled = errors.New("owner does not have site analytics enabled")
3
4 func AnalyticsVisitFromVisit(visit *db.AnalyticsVisits, dbpool db.DB, secret string) error {
5- if !dbpool.HasFeatureForUser(visit.UserID, "analytics") {
6+ if !dbpool.HasFeatureByUser(visit.UserID, "analytics") {
7 return ErrAnalyticsDisabled
8 }
9
10@@ -206,7 +206,7 @@ func ipFromRequest(r *http.Request) string {
11 }
12
13 func AnalyticsVisitFromRequest(r *http.Request, dbpool db.DB, userID string) (*db.AnalyticsVisits, error) {
14- if !dbpool.HasFeatureForUser(userID, "analytics") {
15+ if !dbpool.HasFeatureByUser(userID, "analytics") {
16 return nil, ErrAnalyticsDisabled
17 }
18
1@@ -77,15 +77,15 @@ type ApiConfig struct {
2 }
3
4 func (hc *ApiConfig) HasPrivilegedAccess(apiToken string) bool {
5- user, err := hc.Dbpool.FindUserForToken(apiToken)
6+ user, err := hc.Dbpool.FindUserByToken(apiToken)
7 if err != nil {
8 return false
9 }
10- return hc.Dbpool.HasFeatureForUser(user.ID, "auth")
11+ return hc.Dbpool.HasFeatureByUser(user.ID, "auth")
12 }
13
14 func (hc *ApiConfig) HasPlusOrSpace(user *db.User, space string) bool {
15- return hc.Dbpool.HasFeatureForUser(user.ID, "plus") || hc.Dbpool.HasFeatureForUser(user.ID, space)
16+ return hc.Dbpool.HasFeatureByUser(user.ID, "plus") || hc.Dbpool.HasFeatureByUser(user.ID, space)
17 }
18
19 func (hc *ApiConfig) CreateCtx(prevCtx context.Context, subdomain string) context.Context {
+1,
-1
1@@ -393,7 +393,7 @@ func (m *AnalyticsPage) fetchSiteStats(site string, interval string) {
2 }
3
4 func (m *AnalyticsPage) fetchFeatures() error {
5- features, err := m.shared.Dbpool.FindFeaturesForUser(m.shared.User.ID)
6+ features, err := m.shared.Dbpool.FindFeaturesByUser(m.shared.User.ID)
7 m.features = features
8 return err
9 }
+1,
-1
1@@ -127,7 +127,7 @@ func (m *FeaturesList) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
2 }
3
4 func (m *FeaturesList) fetchFeatures() error {
5- features, err := m.shared.Dbpool.FindFeaturesForUser(m.shared.User.ID)
6+ features, err := m.shared.Dbpool.FindFeaturesByUser(m.shared.User.ID)
7 m.features = features
8 return err
9 }
+2,
-2
1@@ -43,7 +43,7 @@ func (m *PubkeysPage) Footer() []Shortcut {
2 }
3
4 func (m *PubkeysPage) fetchKeys() error {
5- keys, err := m.shared.Dbpool.FindKeysForUser(m.shared.User)
6+ keys, err := m.shared.Dbpool.FindKeysByUser(m.shared.User)
7 if err != nil {
8 return err
9
10@@ -256,7 +256,7 @@ func (m *AddKeyPage) addPubkey(pubkey string) error {
11 key := utils.KeyForKeyText(pk)
12
13 return m.shared.Dbpool.InsertPublicKey(
14- m.shared.User.ID, key, comment, nil,
15+ m.shared.User.ID, key, comment,
16 )
17 }
18
+1,
-1
1@@ -41,7 +41,7 @@ func (m *TokensPage) Footer() []Shortcut {
2 }
3
4 func (m *TokensPage) fetchTokens() error {
5- tokens, err := m.shared.Dbpool.FindTokensForUser(m.shared.User.ID)
6+ tokens, err := m.shared.Dbpool.FindTokensByUser(m.shared.User.ID)
7 if err != nil {
8 return err
9
+2,
-2
1@@ -289,7 +289,7 @@ func FindUser(shrd *SharedModel) (*db.User, error) {
2
3 key := utils.KeyForKeyText(shrd.Session.PublicKey())
4
5- user, err := shrd.Dbpool.FindUserForKey(usr, key)
6+ user, err := shrd.Dbpool.FindUserByKey(usr, key)
7 if err != nil {
8 logger.Error("no user found for public key", "err", err.Error())
9 // we only want to throw an error for specific cases
10@@ -304,7 +304,7 @@ func FindUser(shrd *SharedModel) (*db.User, error) {
11
12 // impersonation
13 if strings.HasPrefix(usr, adminPrefix) {
14- hasFeature := shrd.Dbpool.HasFeatureForUser(user.ID, "admin")
15+ hasFeature := shrd.Dbpool.HasFeatureByUser(user.ID, "admin")
16 if hasFeature {
17 impersonate := strings.TrimPrefix(usr, adminPrefix)
18 user, err = shrd.Dbpool.FindUserByName(impersonate)