1 package sqlite
2 3 import (
4 "errors"
5 "fmt"
6 "time"
7 8 "github.com/nbd-wtf/go-nostr"
9 )
10 11 type Option func(*Store) error
12 13 // WithAdditionalSchema allows to specify an additional database schema, like new tables,
14 // virtual tables, indexes and triggers.
15 func WithAdditionalSchema(schema string) Option {
16 return func(s *Store) error {
17 if _, err := s.DB.Exec(schema); err != nil {
18 return fmt.Errorf("failed to apply additional schema: %w", err)
19 }
20 return nil
21 }
22 }
23 24 // WithBusyTimeout sets the SQLite PRAGMA busy_timeout. This allows concurrent
25 // operations to retry when the database is locked, up to the specified duration.
26 // The duration is internally converted to milliseconds, as required by SQLite.
27 func WithBusyTimeout(d time.Duration) Option {
28 return func(s *Store) error {
29 // had to use string manipulation rather than the '?' syntax because
30 // apparently doesn't work with PRAGMA commands.
31 cmd := fmt.Sprintf("PRAGMA busy_timeout = %d", d.Milliseconds())
32 _, err := s.DB.Exec(cmd)
33 if err != nil {
34 return fmt.Errorf("failed to set busy timeout: %w", err)
35 }
36 return nil
37 }
38 }
39 40 // WithOptimisationEvery sets how many writes trigger a PRAGMA optimize operation.
41 func WithOptimisationEvery(n int) Option {
42 return func(s *Store) error {
43 if n < 0 {
44 return errors.New("number of write before PRAGMA optimize must be non-negative")
45 }
46 s.optimizeEvery = int32(n)
47 return nil
48 }
49 }
50 51 // WithFilterPolicy sets a custom [nastro.FilterPolicy] on the Store.
52 // It will be used to validate and modify filters before executing queries.
53 func WithFilterPolicy(p FilterPolicy) Option {
54 return func(s *Store) error {
55 s.filterPolicy = p
56 return nil
57 }
58 }
59 60 // WithEventPolicy sets a custom [nastro.EventPolicy] on the Store.
61 // It will be used to validate events before inserting them into the database.
62 func WithEventPolicy(p EventPolicy) Option {
63 return func(s *Store) error {
64 s.eventPolicy = p
65 return nil
66 }
67 }
68 69 // WithQueryBuilder allows to specify the query builder used by the store in [Store.Query].
70 func WithQueryBuilder(b QueryBuilder) Option {
71 return func(s *Store) error {
72 s.queryBuilder = b
73 return nil
74 }
75 }
76 77 // WithCountBuilder allows to specify the query builder used by the store in [Store.Count].
78 func WithCountBuilder(b QueryBuilder) Option {
79 return func(s *Store) error {
80 s.countBuilder = b
81 return nil
82 }
83 }
84 85 // EventPolicy validates a nostr event before it's written to the store.
86 type EventPolicy func(*nostr.Event) error
87 88 // FilterPolicy sanitizes a list of filters before building a query.
89 // It returns a potentially modified list and an error if the input is invalid.
90 type FilterPolicy func(...nostr.Filter) (nostr.Filters, error)
91 92 // DefaultEventPolicy returns an error if the event has too many tags, or if the
93 // content is too big.
94 func defaultEventPolicy(e *nostr.Event) error {
95 if len(e.Tags) > 100_000 {
96 return fmt.Errorf("event has too many tags: %d", len(e.Tags))
97 }
98 if len(e.Content) > 10_000_000 {
99 return fmt.Errorf("event too much content: %d", len(e.Content))
100 }
101 return nil
102 }
103 104 // DefaultFilterPolicy enforces 4 rules:
105 // 1. Filters must be less than 200.
106 // 2. Filters can't have the "search" field, as NIP-50 is not supported by default.
107 // 3. Filters with LimitZero set are ignored (i.e., removed).
108 // 4. Remaining filters must have a Limit > 0.
109 //
110 // It returns the cleaned list of filters or an error.
111 func defaultFilterPolicy(filters ...nostr.Filter) (nostr.Filters, error) {
112 if len(filters) > 200 {
113 return nil, fmt.Errorf("filters must be less than 200: %d", len(filters))
114 }
115 116 if containSearch(filters) {
117 return nil, errors.New("NIP-50 search is not supported")
118 }
119 120 result := make(nostr.Filters, 0, len(filters))
121 for _, f := range filters {
122 if !f.LimitZero {
123 if f.Limit < 1 {
124 return nil, errors.New("unspecified filter's limit")
125 }
126 result = append(result, f)
127 }
128 }
129 return result, nil
130 }
131 132 func containSearch(filters nostr.Filters) bool {
133 for _, f := range filters {
134 if f.Search != "" {
135 return true
136 }
137 }
138 return false
139 }
140