options.go raw

   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