Add prefix to event store
This commit is contained in:
@@ -6,6 +6,13 @@ This is a multi-tenant relay based on [Khatru](https://gitworkshop.dev/fiatjaf.c
|
||||
|
||||
A single zooid instance can run any number of "virtual" relays. The `config` directory can contain any number of configuration files, each of which represents a single virtual relay.
|
||||
|
||||
## Environment
|
||||
|
||||
Zooid supports a few environment variables, which configure shared resources, like the web server or sqlite database file.
|
||||
|
||||
- `PORT` - the port the server will listen on for all requests. Defaults to `3334`.
|
||||
- `DATABASE_PATH` - the location of the database path. Defaults to `./data.db`
|
||||
|
||||
## Configuration
|
||||
|
||||
Configuration files are written using [toml](https://toml.io). The name of the configuration file should be the hostname the relay serves, for example `relay.example.com`. Config files contain the following sections:
|
||||
@@ -56,13 +63,6 @@ Defines roles that can be assigned to different users and attendant privileges.
|
||||
|
||||
A special `[roles.member]` heading may be used to configure policies for all relay users (that is, pubkeys assigned to other roles, or who have redeemed an invite code).
|
||||
|
||||
### `[data]`
|
||||
|
||||
Contains information related to data persistence.
|
||||
|
||||
- `events` - the location of the sqlite database file used to store events. Defaults to `./data/{my-relay}/events`.
|
||||
- `media` - the location of the sqlite database file used to store file metadata. Defaults to `./data/{my-relay}/media`.
|
||||
|
||||
### Example
|
||||
|
||||
The below config file might be saved as `./config/my-relay.example.com` in order to route requests from `wss://my-relay.example.com` to this virtual relay.
|
||||
|
||||
@@ -5,6 +5,8 @@ go 1.24.1
|
||||
require (
|
||||
fiatjaf.com/nostr v0.0.0-20250924142401-59bd3c29fffd
|
||||
github.com/BurntSushi/toml v1.5.0
|
||||
github.com/Masterminds/squirrel v1.5.4
|
||||
github.com/gosimple/slug v1.15.0
|
||||
github.com/mattn/go-sqlite3 v1.14.32
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
@@ -12,8 +14,6 @@ require (
|
||||
|
||||
require (
|
||||
github.com/ImVexed/fasturl v0.0.0-20230304231329-4e41488060f3 // indirect
|
||||
github.com/Masterminds/squirrel v1.5.4 // indirect
|
||||
github.com/PowerDNS/lmdb-go v1.9.3 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect
|
||||
@@ -23,6 +23,7 @@ require (
|
||||
github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/fasthttp/websocket v1.5.12 // indirect
|
||||
github.com/gosimple/unidecode v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
|
||||
@@ -31,16 +31,16 @@ github.com/fasthttp/websocket v1.5.12/go.mod h1:I+liyL7/4moHojiOgUOIKEWm9EIxHqxZ
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo=
|
||||
github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ=
|
||||
github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o=
|
||||
github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
@@ -62,8 +62,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1 h1:GJYJZwO6IdxN/IKbneznS6yPkVC+c3zyY/j19c++5Fg=
|
||||
github.com/puzpuzpuz/xsync/v3 v3.5.1/go.mod h1:VjzYrABPabuM4KyBh1Ftq6u8nhwY5tBPKP9jpmh0nnA=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
|
||||
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
|
||||
github.com/savsgio/gotils v0.0.0-20240704082632-aef3928b8a38 h1:D0vL7YNisV2yqE55+q0lFuGse6U8lxlg7fYTctlT5Gc=
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
func (s *SqliteBackend) DeleteEvent(id nostr.ID) error {
|
||||
_, err := squirrel.Delete("events").Where(squirrel.Eq{"id": id.Hex()}).RunWith(s.db).Exec()
|
||||
_, err := squirrel.Delete(s.tmpl("{{.Prefix}}events")).Where(squirrel.Eq{"id": id.Hex()}).RunWith(s.db).Exec()
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
+38
-22
@@ -1,8 +1,10 @@
|
||||
package sqlite
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"text/template"
|
||||
|
||||
"fiatjaf.com/nostr/eventstore"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
@@ -13,6 +15,7 @@ var _ eventstore.Store = (*SqliteBackend)(nil)
|
||||
type SqliteBackend struct {
|
||||
db *sql.DB
|
||||
Path string
|
||||
Prefix string
|
||||
FTSAvailable bool
|
||||
}
|
||||
|
||||
@@ -41,10 +44,20 @@ func (s *SqliteBackend) Init() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SqliteBackend) tmpl(t string) string {
|
||||
var buf bytes.Buffer
|
||||
err := template.Must(template.New("schema").Parse(t)).Execute(&buf, s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (s *SqliteBackend) createSchema() error {
|
||||
// Create basic schema first
|
||||
basicSchema := `
|
||||
CREATE TABLE IF NOT EXISTS events (
|
||||
basicSchema := s.tmpl(`
|
||||
CREATE TABLE IF NOT EXISTS {{.Prefix}}events (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at INTEGER NOT NULL,
|
||||
kind INTEGER NOT NULL,
|
||||
@@ -54,23 +67,23 @@ func (s *SqliteBackend) createSchema() error {
|
||||
sig TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_events_created_at ON events(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_kind_pubkey ON events(kind, pubkey);
|
||||
CREATE INDEX IF NOT EXISTS idx_events_kind_pubkey_created_at ON events(kind, pubkey, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_created_at ON {{.Prefix}}events(created_at);
|
||||
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_kind ON {{.Prefix}}events(kind);
|
||||
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_pubkey ON {{.Prefix}}events(pubkey);
|
||||
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_kind_pubkey ON {{.Prefix}}events(kind, pubkey);
|
||||
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_events_kind_pubkey_created_at ON {{.Prefix}}events(kind, pubkey, created_at DESC);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS event_tags (
|
||||
CREATE TABLE IF NOT EXISTS {{.Prefix}}event_tags (
|
||||
event_id TEXT NOT NULL,
|
||||
key TEXT NOT NULL,
|
||||
value TEXT NOT NULL,
|
||||
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
||||
FOREIGN KEY (event_id) REFERENCES {{.Prefix}}events(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_event_tags_event_id ON event_tags(event_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_tags_key ON event_tags(key);
|
||||
CREATE INDEX IF NOT EXISTS idx_event_tags_key_value ON event_tags(key, value);
|
||||
`
|
||||
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_event_tags_event_id ON {{.Prefix}}event_tags(event_id);
|
||||
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_event_tags_key ON {{.Prefix}}event_tags(key);
|
||||
CREATE INDEX IF NOT EXISTS {{.Prefix}}idx_event_tags_key_value ON {{.Prefix}}event_tags(key, value);
|
||||
`)
|
||||
|
||||
if _, err := s.db.Exec(basicSchema); err != nil {
|
||||
return fmt.Errorf("failed to create schema: %w", err)
|
||||
@@ -78,23 +91,26 @@ func (s *SqliteBackend) createSchema() error {
|
||||
|
||||
// Try to create FTS5 schema - if it fails, continue without it
|
||||
ftsSchema := `
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS events_fts USING fts5(
|
||||
CREATE VIRTUAL TABLE IF NOT EXISTS {{.Prefix}}events_fts USING fts5(
|
||||
content,
|
||||
content='events',
|
||||
content='{{.Prefix}}events',
|
||||
content_rowid='rowid'
|
||||
);
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS events_ai AFTER INSERT ON events BEGIN
|
||||
INSERT INTO events_fts(rowid, content) VALUES (new.rowid, new.content);
|
||||
CREATE TRIGGER IF NOT EXISTS {{.Prefix}}events_ai AFTER INSERT ON {{.Prefix}}events BEGIN
|
||||
INSERT INTO {{.Prefix}}events_fts(rowid, content) VALUES (new.rowid, new.content);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS events_ad AFTER DELETE ON events BEGIN
|
||||
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', old.rowid, old.content);
|
||||
CREATE TRIGGER IF NOT EXISTS {{.Prefix}}events_ad AFTER DELETE ON {{.Prefix}}events BEGIN
|
||||
INSERT INTO {{.Prefix}}events_fts({{.Prefix}}events_fts, rowid, content)
|
||||
VALUES('delete', old.rowid, old.content);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER IF NOT EXISTS events_au AFTER UPDATE ON events BEGIN
|
||||
INSERT INTO events_fts(events_fts, rowid, content) VALUES('delete', old.rowid, old.content);
|
||||
INSERT INTO events_fts(rowid, content) VALUES (new.rowid, new.content);
|
||||
CREATE TRIGGER IF NOT EXISTS {{.Prefix}}events_au AFTER UPDATE ON {{.Prefix}}events BEGIN
|
||||
INSERT INTO {{.Prefix}}events_fts({{.Prefix}}events_fts, rowid, content)
|
||||
VALUES('delete', old.rowid, old.content);
|
||||
INSERT INTO {{.Prefix}}events_fts(rowid, content)
|
||||
VALUES (new.rowid, new.content);
|
||||
END;
|
||||
`
|
||||
|
||||
|
||||
+3
-3
@@ -76,12 +76,12 @@ func (s *SqliteBackend) QueryEvents(filter nostr.Filter, maxLimit int) iter.Seq[
|
||||
|
||||
func (s *SqliteBackend) buildSelectQuery(filter nostr.Filter, limit int) squirrel.SelectBuilder {
|
||||
qb := squirrel.Select("id", "created_at", "kind", "pubkey", "content", "tags", "sig").
|
||||
From("events").
|
||||
From(s.tmpl("{{.Prefix}}events")).
|
||||
OrderBy("created_at DESC")
|
||||
|
||||
// Handle search with FTS (if available)
|
||||
if filter.Search != "" && s.FTSAvailable {
|
||||
qb = qb.Join("events_fts ON events.rowid = events_fts.rowid").
|
||||
qb = qb.Join(s.tmpl("{{.Prefix}}events_fts ON {{.Prefix}}events.rowid = {{.Prefix}}events_fts.rowid")).
|
||||
Where(squirrel.Eq{"events_fts": filter.Search})
|
||||
} else if filter.Search != "" {
|
||||
// Fallback to LIKE search if FTS not available
|
||||
@@ -128,7 +128,7 @@ func (s *SqliteBackend) buildSelectQuery(filter nostr.Filter, limit int) squirre
|
||||
}
|
||||
|
||||
subQuery := squirrel.Select("event_id").
|
||||
From("event_tags").
|
||||
From(s.tmpl("{{.Prefix}}event_tags")).
|
||||
Where(squirrel.Eq{"key": tagKey}).
|
||||
Where(squirrel.Eq{"value": tagValueInterfaces})
|
||||
|
||||
|
||||
+3
-3
@@ -13,7 +13,7 @@ import (
|
||||
func (s *SqliteBackend) SaveEvent(evt nostr.Event) error {
|
||||
// Check if event already exists
|
||||
var existingID string
|
||||
qb := squirrel.Select("id").From("events").Where(squirrel.Eq{"id": evt.ID.Hex()})
|
||||
qb := squirrel.Select("id").From(s.tmpl("{{.Prefix}}events")).Where(squirrel.Eq{"id": evt.ID.Hex()})
|
||||
err := qb.RunWith(s.db).QueryRow().Scan(&existingID)
|
||||
if err == nil {
|
||||
// Event already exists
|
||||
@@ -27,7 +27,7 @@ func (s *SqliteBackend) SaveEvent(evt nostr.Event) error {
|
||||
}
|
||||
|
||||
// Insert the event
|
||||
insertQb := squirrel.Insert("events").
|
||||
insertQb := squirrel.Insert(s.tmpl("{{.Prefix}}events")).
|
||||
Columns("id", "created_at", "kind", "pubkey", "content", "tags", "sig").
|
||||
Values(
|
||||
evt.ID.Hex(),
|
||||
@@ -48,7 +48,7 @@ func (s *SqliteBackend) SaveEvent(evt nostr.Event) error {
|
||||
// Insert single-letter tags into event_tags table
|
||||
for _, tag := range evt.Tags {
|
||||
if len(tag) >= 2 && len(tag[0]) == 1 {
|
||||
tagQb := squirrel.Insert("event_tags").
|
||||
tagQb := squirrel.Insert(s.tmpl("{{.Prefix}}event_tags")).
|
||||
Columns("event_id", "key", "value").
|
||||
Values(evt.ID.Hex(), tag[0], tag[1])
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ func TestSqliteFlow(t *testing.T) {
|
||||
os.RemoveAll("/tmp/sqlitetest.db")
|
||||
|
||||
sb := &SqliteBackend{
|
||||
Path: "/tmp/sqlitetest.db",
|
||||
Path: "/tmp/sqlitetest.db",
|
||||
Prefix: "prefix",
|
||||
}
|
||||
err := sb.Init()
|
||||
assert.NoError(t, err)
|
||||
|
||||
+6
-2
@@ -11,6 +11,7 @@ import (
|
||||
"fiatjaf.com/nostr"
|
||||
"fiatjaf.com/nostr/eventstore"
|
||||
"fiatjaf.com/nostr/khatru"
|
||||
"github.com/gosimple/slug"
|
||||
"zooid/sqlite"
|
||||
)
|
||||
|
||||
@@ -40,8 +41,11 @@ func MakeInstance(hostname string) (*Instance, error) {
|
||||
instance := &Instance{
|
||||
Host: hostname,
|
||||
Config: config,
|
||||
Events: &sqlite.SqliteBackend{Path: config.Data.Events},
|
||||
Relay: khatru.NewRelay(),
|
||||
Events: &sqlite.SqliteBackend{
|
||||
Path: Env("DATABASE_PATH", "./data.db"),
|
||||
Prefix: slug.Make(hostname) + "__",
|
||||
},
|
||||
Relay: khatru.NewRelay(),
|
||||
}
|
||||
|
||||
instance.Relay.Info.Name = config.Self.Name
|
||||
|
||||
Reference in New Issue
Block a user