Antonio Mika
·
2026-10-11
1package postgres
2
3import (
4 "database/sql"
5 "fmt"
6 "time"
7
8 "github.com/picosh/pico/pkg/db"
9)
10
11func (me *PsqlDB) FindFeature(userID string, feature string) (*db.FeatureFlag, error) {
12 ff := &db.FeatureFlag{}
13 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)
14 if err != nil {
15 return nil, err
16 }
17 return ff, nil
18}
19
20func (me *PsqlDB) FindFeaturesByUser(userID string) ([]*db.FeatureFlag, error) {
21 var features []*db.FeatureFlag
22 // https://stackoverflow.com/a/16920077
23 query := `SELECT DISTINCT ON (name) *
24 FROM feature_flags
25 WHERE user_id=$1
26 ORDER BY name, expires_at DESC;`
27 err := me.Db.Select(&features, query, userID)
28 if err != nil {
29 return nil, err
30 }
31 return features, nil
32}
33
34func (me *PsqlDB) InsertFeature(userID, name string, expiresAt time.Time) (*db.FeatureFlag, error) {
35 var featureID string
36 err := me.Db.QueryRow(
37 `INSERT INTO feature_flags (user_id, name, expires_at) VALUES ($1, $2, $3) RETURNING id;`,
38 userID,
39 name,
40 expiresAt,
41 ).Scan(&featureID)
42 if err != nil {
43 return nil, err
44 }
45
46 feature, err := me.FindFeature(userID, name)
47 if err != nil {
48 return nil, err
49 }
50
51 return feature, nil
52}
53
54func (me *PsqlDB) RemoveFeature(userID string, name string) error {
55 _, err := me.Db.Exec(`DELETE FROM feature_flags WHERE user_id = $1 AND name = $2`, userID, name)
56 return err
57}
58
59func (me *PsqlDB) createFeatureExpiresAt(userID, name string) time.Time {
60 ff, _ := me.FindFeature(userID, name)
61 // if the feature flag has already expired we don't want to add a year to it since that will
62 // not grant the user a full year
63 if ff == nil || !ff.IsValid() {
64 t := time.Now()
65 return t.AddDate(1, 0, 0)
66 }
67 return ff.ExpiresAt.AddDate(1, 0, 0)
68}
69
70func (me *PsqlDB) AddPicoPlusUser(username, email, paymentType, txId string) error {
71 user, err := me.FindUserByName(username)
72 if err != nil {
73 return err
74 }
75
76 tx, err := me.Db.Beginx()
77 if err != nil {
78 return err
79 }
80 defer func() {
81 _ = tx.Rollback()
82 }()
83
84 var paymentHistoryId sql.NullString
85 if paymentType != "" {
86 data := db.PaymentHistoryData{
87 Notes: "",
88 TxID: txId,
89 }
90
91 err := tx.QueryRow(
92 `INSERT INTO payment_history (user_id, payment_type, amount, data) VALUES ($1, $2, 24 * 1000000, $3) RETURNING id;`,
93 user.ID,
94 paymentType,
95 data,
96 ).Scan(&paymentHistoryId)
97 if err != nil {
98 return err
99 }
100 }
101
102 plus := me.createFeatureExpiresAt(user.ID, "plus")
103 plusQuery := fmt.Sprintf(`INSERT INTO feature_flags (user_id, name, data, expires_at, payment_history_id)
104 VALUES ($1, 'plus', '{"storage_max":5000000000, "file_max":100000000, "email": "%s"}'::jsonb, $2, $3);`, email)
105 _, err = tx.Exec(plusQuery, user.ID, plus, paymentHistoryId)
106 if err != nil {
107 return err
108 }
109
110 return tx.Commit()
111}
112
113func (me *PsqlDB) AddFeatureUser(username, name string) error {
114 user, err := me.FindUserByName(username)
115 if err != nil {
116 return err
117 }
118
119 expiresAt := me.createFeatureExpiresAt(user.ID, name)
120 _, err = me.InsertFeature(user.ID, name, expiresAt)
121 return err
122}