applesauce-sqlite 0.0.0-next-20250915145415

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -0
  3. package/dist/better-sqlite3/event-database.d.ts +52 -0
  4. package/dist/better-sqlite3/event-database.js +104 -0
  5. package/dist/better-sqlite3/index.d.ts +2 -0
  6. package/dist/better-sqlite3/index.js +2 -0
  7. package/dist/better-sqlite3/methods.d.ts +31 -0
  8. package/dist/better-sqlite3/methods.js +149 -0
  9. package/dist/bun/event-database.d.ts +52 -0
  10. package/dist/bun/event-database.js +105 -0
  11. package/dist/bun/index.d.ts +2 -0
  12. package/dist/bun/index.js +2 -0
  13. package/dist/bun/methods.d.ts +31 -0
  14. package/dist/bun/methods.js +152 -0
  15. package/dist/helpers/index.d.ts +3 -0
  16. package/dist/helpers/index.js +3 -0
  17. package/dist/helpers/search.d.ts +16 -0
  18. package/dist/helpers/search.js +60 -0
  19. package/dist/helpers/sql.d.ts +15 -0
  20. package/dist/helpers/sql.js +122 -0
  21. package/dist/helpers/sqlite.d.ts +66 -0
  22. package/dist/helpers/sqlite.js +367 -0
  23. package/dist/helpers/statements.d.ts +47 -0
  24. package/dist/helpers/statements.js +65 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.js +1 -0
  27. package/dist/libsql/event-database.d.ts +54 -0
  28. package/dist/libsql/event-database.js +106 -0
  29. package/dist/libsql/index.d.ts +2 -0
  30. package/dist/libsql/index.js +2 -0
  31. package/dist/libsql/methods.d.ts +31 -0
  32. package/dist/libsql/methods.js +249 -0
  33. package/dist/native/event-database.d.ts +52 -0
  34. package/dist/native/event-database.js +104 -0
  35. package/dist/native/index.d.ts +2 -0
  36. package/dist/native/index.js +2 -0
  37. package/dist/native/methods.d.ts +31 -0
  38. package/dist/native/methods.js +174 -0
  39. package/dist/relay.d.ts +1 -0
  40. package/dist/relay.js +166 -0
  41. package/dist/sqlite-event-database.d.ts +53 -0
  42. package/dist/sqlite-event-database.js +105 -0
  43. package/package.json +113 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 hzrd149
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # applesauce-sqlite
2
+
3
+ A SQLite3 event database implementation for AppleSauce, providing persistent storage for Nostr events. This package extends the core `applesauce-core` functionality by replacing the default in-memory event database with a persistent SQLite database.
4
+
5
+ ## Key Features
6
+
7
+ - **Persistent Storage**: Store Nostr events in a SQLite database that persists between application restarts
8
+ - **Hybrid Architecture**: Combines in-memory caching with SQLite persistence for optimal performance
9
+ - **Full Compatibility**: Drop-in replacement for the default in-memory event database
10
+ - **Efficient Querying**: Optimized SQLite queries for filtering and retrieving Nostr events
11
+ - **Built-in Relay**: Includes a complete Nostr relay implementation using the SQLite database
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ # For better-sqlite3
17
+ npm install applesauce-sqlite better-sqlite3
18
+
19
+ # For libsql
20
+ npm install applesauce-sqlite @libsql/client
21
+
22
+ # For bun / deno / native sqlite
23
+ bun add applesauce-sqlite
24
+ ```
25
+
26
+ ## Basic Usage
27
+
28
+ ### Using an event database with EventStore
29
+
30
+ ```js
31
+ import { EventStore } from "applesauce-core";
32
+ import { BetterSqlite3EventDatabase } from "applesauce-sqlite/better-sqlite3";
33
+
34
+ // Create a SQLite database (file-based or in-memory)
35
+ const database = new BetterSqlite3EventDatabase("./events.db"); // or ":memory:" for in-memory
36
+
37
+ // Create EventStore with SQLite backend
38
+ const eventStore = new EventStore(database);
39
+
40
+ // Use the event store as normal
41
+ eventStore.add(someNostrEvent);
42
+
43
+ // The events are now persisted to SQLite!
44
+ ```
45
+
46
+ ### With Models and Subscriptions
47
+
48
+ ```js
49
+ import { EventStore } from "applesauce-core";
50
+ import { ProfileModel, TimelineModel } from "applesauce-core/models";
51
+ import { BetterSqlite3EventDatabase } from "applesauce-better-sqlite3";
52
+ import { Relay } from "nostr-tools/relay";
53
+
54
+ // Create persistent event store
55
+ const database = new BetterSqlite3EventDatabase("./events.db");
56
+ const eventStore = new EventStore(database);
57
+
58
+ // Connect to a relay and store events
59
+ const relay = await Relay.connect("wss://relay.example.com");
60
+ const sub = relay.subscribe([{ kinds: [0, 1] }], {
61
+ onevent(event) {
62
+ eventStore.add(event); // Events are automatically persisted
63
+ },
64
+ });
65
+
66
+ // Use models as normal - they'll work with persisted data
67
+ const profile = eventStore.model(ProfileModel, "npub...");
68
+ profile.subscribe((parsed) => {
69
+ console.log("Profile loaded from database:", parsed);
70
+ });
71
+
72
+ // Timeline will include events from previous sessions
73
+ const timeline = eventStore.model(TimelineModel, { kinds: [1] });
74
+ timeline.subscribe((events) => {
75
+ console.log("Timeline with persisted events:", events.length);
76
+ });
77
+ ```
78
+
79
+ ## Advanced Usage
80
+
81
+ ### Custom Relay Implementation
82
+
83
+ ```js
84
+ import { EventStore } from "applesauce-core";
85
+ import { BetterSqlite3EventDatabase } from "applesauce-better-sqlite3";
86
+ import { WebSocketServer } from "ws";
87
+
88
+ // Create your own relay with custom logic
89
+ const database = new BetterSqlite3EventDatabase("./custom-relay.db");
90
+ const eventStore = new EventStore(database);
91
+
92
+ const wss = new WebSocketServer({ port: 8080 });
93
+
94
+ wss.on("connection", (ws) => {
95
+ ws.on("message", (data) => {
96
+ const message = JSON.parse(data.toString());
97
+
98
+ if (message[0] === "EVENT") {
99
+ const event = message[1];
100
+ const added = eventStore.add(event);
101
+
102
+ if (added) {
103
+ ws.send(JSON.stringify(["OK", event.id, true, ""]));
104
+ // Broadcast to other clients...
105
+ } else {
106
+ ws.send(JSON.stringify(["OK", event.id, false, "rejected"]));
107
+ }
108
+ }
109
+ });
110
+ });
111
+ ```
112
+
113
+ ## License
114
+
115
+ MIT
@@ -0,0 +1,52 @@
1
+ import { IEventDatabase } from "applesauce-core";
2
+ import { Filter, NostrEvent } from "applesauce-core/helpers";
3
+ import { type Database as TDatabase } from "better-sqlite3";
4
+ import { SearchContentFormatter } from "../helpers/search.js";
5
+ /** Options for the {@link BetterSqlite3EventDatabase} */
6
+ export type BetterSqlite3EventDatabaseOptions = {
7
+ search?: boolean;
8
+ searchContentFormatter?: SearchContentFormatter;
9
+ };
10
+ export declare class BetterSqlite3EventDatabase implements IEventDatabase {
11
+ db: TDatabase;
12
+ /** If search is enabled */
13
+ private search;
14
+ /** The search content formatter */
15
+ private searchContentFormatter;
16
+ constructor(database?: string | TDatabase, options?: BetterSqlite3EventDatabaseOptions);
17
+ /** Store a Nostr event in the database */
18
+ add(event: NostrEvent): NostrEvent;
19
+ /** Delete an event by ID */
20
+ remove(id: string): boolean;
21
+ /** Checks if an event exists */
22
+ hasEvent(id: string): boolean;
23
+ /** Get an event by its ID */
24
+ getEvent(id: string): NostrEvent | undefined;
25
+ /** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
26
+ getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
27
+ /** Checks if a replaceable event exists */
28
+ hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
29
+ /** Returns all the versions of a replaceable event */
30
+ getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[];
31
+ /** Get all events that match the filters (supports NIP-50 search field) */
32
+ getByFilters(filters: (Filter & {
33
+ search?: string;
34
+ }) | (Filter & {
35
+ search?: string;
36
+ })[]): Set<NostrEvent>;
37
+ /** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
38
+ getTimeline(filters: (Filter & {
39
+ search?: string;
40
+ }) | (Filter & {
41
+ search?: string;
42
+ })[]): NostrEvent[];
43
+ /** Set the search content formatter */
44
+ setSearchContentFormatter(formatter: SearchContentFormatter): void;
45
+ /** Get the current search content formatter */
46
+ getSearchContentFormatter(): SearchContentFormatter;
47
+ /** Rebuild the search index for all events */
48
+ rebuildSearchIndex(): void;
49
+ /** Close the database connection */
50
+ close(): void;
51
+ [Symbol.dispose](): void;
52
+ }
@@ -0,0 +1,104 @@
1
+ import { logger } from "applesauce-core";
2
+ import { insertEventIntoDescendingList } from "applesauce-core/helpers";
3
+ import Database from "better-sqlite3";
4
+ import { createTables, deleteEvent, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "../better-sqlite3/methods.js";
5
+ import { enhancedSearchContentFormatter } from "../helpers/search.js";
6
+ const log = logger.extend("BetterSqlite3EventDatabase");
7
+ export class BetterSqlite3EventDatabase {
8
+ db;
9
+ /** If search is enabled */
10
+ search;
11
+ /** The search content formatter */
12
+ searchContentFormatter;
13
+ constructor(database = ":memory:", options) {
14
+ this.db = typeof database === "string" ? new Database(database) : database;
15
+ this.search = options?.search ?? false;
16
+ this.searchContentFormatter = options?.searchContentFormatter ?? enhancedSearchContentFormatter;
17
+ // Setup the database tables and indexes
18
+ createTables(this.db, this.search);
19
+ }
20
+ /** Store a Nostr event in the database */
21
+ add(event) {
22
+ try {
23
+ insertEvent(this.db, event, this.search ? this.searchContentFormatter : undefined);
24
+ return event;
25
+ }
26
+ catch (error) {
27
+ log("Error inserting event:", error);
28
+ throw error;
29
+ }
30
+ }
31
+ /** Delete an event by ID */
32
+ remove(id) {
33
+ try {
34
+ // Remove event from database
35
+ return deleteEvent(this.db, id);
36
+ }
37
+ catch (error) {
38
+ return false;
39
+ }
40
+ }
41
+ /** Checks if an event exists */
42
+ hasEvent(id) {
43
+ return hasEvent(this.db, id);
44
+ }
45
+ /** Get an event by its ID */
46
+ getEvent(id) {
47
+ return getEvent(this.db, id);
48
+ }
49
+ /** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
50
+ getReplaceable(kind, pubkey, identifier = "") {
51
+ return getReplaceable(this.db, kind, pubkey, identifier);
52
+ }
53
+ /** Checks if a replaceable event exists */
54
+ hasReplaceable(kind, pubkey, identifier = "") {
55
+ return hasReplaceable(this.db, kind, pubkey, identifier);
56
+ }
57
+ /** Returns all the versions of a replaceable event */
58
+ getReplaceableHistory(kind, pubkey, identifier = "") {
59
+ return getReplaceableHistory(this.db, kind, pubkey, identifier);
60
+ }
61
+ /** Get all events that match the filters (supports NIP-50 search field) */
62
+ getByFilters(filters) {
63
+ try {
64
+ // If search is disabled, remove the search field from the filters
65
+ if (!this.search && (Array.isArray(filters) ? filters.some((f) => "search" in f) : "search" in filters))
66
+ throw new Error("Search is disabled");
67
+ return getEventsByFilters(this.db, filters);
68
+ }
69
+ catch (error) {
70
+ return new Set();
71
+ }
72
+ }
73
+ /** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
74
+ getTimeline(filters) {
75
+ const events = this.getByFilters(filters);
76
+ const timeline = [];
77
+ for (const event of events)
78
+ insertEventIntoDescendingList(timeline, event);
79
+ return timeline;
80
+ }
81
+ /** Set the search content formatter */
82
+ setSearchContentFormatter(formatter) {
83
+ this.searchContentFormatter = formatter;
84
+ }
85
+ /** Get the current search content formatter */
86
+ getSearchContentFormatter() {
87
+ return this.searchContentFormatter;
88
+ }
89
+ /** Rebuild the search index for all events */
90
+ rebuildSearchIndex() {
91
+ if (!this.search)
92
+ throw new Error("Search is disabled");
93
+ rebuildSearchIndex(this.db, this.searchContentFormatter);
94
+ log("Search index rebuilt successfully");
95
+ }
96
+ /** Close the database connection */
97
+ close() {
98
+ log("Closing database connection");
99
+ this.db.close();
100
+ }
101
+ [Symbol.dispose]() {
102
+ this.close();
103
+ }
104
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./event-database.js";
2
+ export * from "./methods.js";
@@ -0,0 +1,2 @@
1
+ export * from "./event-database.js";
2
+ export * from "./methods.js";
@@ -0,0 +1,31 @@
1
+ import { Filter, NostrEvent } from "applesauce-core/helpers";
2
+ import { Database } from "better-sqlite3";
3
+ import { FilterWithSearch, SearchContentFormatter } from "../helpers/search.js";
4
+ /** Create and migrate the `events`, `event_tags`, and search tables */
5
+ export declare function createTables(db: Database, search?: boolean): void;
6
+ /** Inserts search content for an event */
7
+ export declare function insertSearchContent(db: Database, event: NostrEvent, contentFormatter: SearchContentFormatter): void;
8
+ /** Removes search content for an event */
9
+ export declare function deleteSearchContent(db: Database, eventId: string): void;
10
+ /** Inserts an event into the `events`, `event_tags`, and search tables of a database */
11
+ export declare function insertEvent(db: Database, event: NostrEvent, contentFormatter?: SearchContentFormatter): boolean;
12
+ /** Insert indexable tags for an event into the event_tags table */
13
+ export declare function insertEventTags(db: Database, event: NostrEvent): void;
14
+ /** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
15
+ export declare function deleteEvent(db: Database, id: string): boolean;
16
+ /** Checks if an event exists */
17
+ export declare function hasEvent(db: Database, id: string): boolean;
18
+ /** Gets a single event from a database */
19
+ export declare function getEvent(db: Database, id: string): NostrEvent | undefined;
20
+ /** Gets the latest replaceable event from a database */
21
+ export declare function getReplaceable(db: Database, kind: number, pubkey: string, identifier: string): NostrEvent | undefined;
22
+ /** Gets the history of a replaceable event from a database */
23
+ export declare function getReplaceableHistory(db: Database, kind: number, pubkey: string, identifier: string): NostrEvent[];
24
+ /** Checks if a replaceable event exists in a database */
25
+ export declare function hasReplaceable(db: Database, kind: number, pubkey: string, identifier?: string): boolean;
26
+ /** Get all events that match the filters (includes NIP-50 search support) */
27
+ export declare function getEventsByFilters(db: Database, filters: FilterWithSearch | FilterWithSearch[]): Set<NostrEvent>;
28
+ /** Search events using FTS5 full-text search (convenience wrapper around getEventsByFilters) */
29
+ export declare function searchEvents(db: Database, search: string, options?: Filter): NostrEvent[];
30
+ /** Rebuild the FTS5 search index for all events */
31
+ export declare function rebuildSearchIndex(db: Database, contentFormatter: SearchContentFormatter): void;
@@ -0,0 +1,149 @@
1
+ import { getIndexableTags, getReplaceableIdentifier } from "applesauce-core/helpers";
2
+ import { CREATE_SEARCH_TABLE_STATEMENT, DELETE_SEARCH_CONTENT_STATEMENT, INSERT_SEARCH_CONTENT_STATEMENT, } from "../helpers/search.js";
3
+ import { buildFiltersQuery, rowToEvent } from "../helpers/sql.js";
4
+ import { CREATE_EVENT_TAGS_TABLE_STATEMENT, CREATE_EVENTS_TABLE_STATEMENT, CREATE_INDEXES_STATEMENTS, DELETE_EVENT_STATEMENT, DELETE_EVENT_TAGS_STATEMENT, GET_ALL_EVENTS_STATEMENT, GET_EVENT_STATEMENT, GET_REPLACEABLE_HISTORY_STATEMENT, GET_REPLACEABLE_STATEMENT, HAS_EVENT_STATEMENT, HAS_REPLACEABLE_STATEMENT, INSERT_EVENT_STATEMENT, INSERT_EVENT_TAG_STATEMENT, } from "../helpers/statements.js";
5
+ /** Create and migrate the `events`, `event_tags`, and search tables */
6
+ export function createTables(db, search = true) {
7
+ // Create the events table
8
+ db.exec(CREATE_EVENTS_TABLE_STATEMENT.sql);
9
+ // Create the event_tags table
10
+ db.exec(CREATE_EVENT_TAGS_TABLE_STATEMENT.sql);
11
+ // Create the FTS5 search table
12
+ if (search) {
13
+ db.exec(CREATE_SEARCH_TABLE_STATEMENT.sql);
14
+ }
15
+ // Create indexes
16
+ CREATE_INDEXES_STATEMENTS.forEach((indexStatement) => {
17
+ db.exec(indexStatement.sql);
18
+ });
19
+ }
20
+ /** Inserts search content for an event */
21
+ export function insertSearchContent(db, event, contentFormatter) {
22
+ const searchableContent = contentFormatter(event);
23
+ // Insert/update directly into the FTS5 table
24
+ const stmt = db.prepare(INSERT_SEARCH_CONTENT_STATEMENT.sql);
25
+ stmt.run(event.id, searchableContent, event.kind, event.pubkey, event.created_at);
26
+ }
27
+ /** Removes search content for an event */
28
+ export function deleteSearchContent(db, eventId) {
29
+ const stmt = db.prepare(DELETE_SEARCH_CONTENT_STATEMENT.sql);
30
+ stmt.run(eventId);
31
+ }
32
+ /** Inserts an event into the `events`, `event_tags`, and search tables of a database */
33
+ export function insertEvent(db, event, contentFormatter) {
34
+ const identifier = getReplaceableIdentifier(event);
35
+ return db.transaction(() => {
36
+ // Insert/update the main event
37
+ const stmt = db.prepare(INSERT_EVENT_STATEMENT.sql);
38
+ const result = stmt.run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
39
+ // Insert indexable tags into the event_tags table
40
+ insertEventTags(db, event);
41
+ // Insert searchable content into the search tables
42
+ if (contentFormatter)
43
+ insertSearchContent(db, event, contentFormatter);
44
+ return result.changes > 0;
45
+ })();
46
+ }
47
+ /** Insert indexable tags for an event into the event_tags table */
48
+ export function insertEventTags(db, event) {
49
+ // Clear existing tags for this event first
50
+ const deleteStmt = db.prepare(DELETE_EVENT_TAGS_STATEMENT.sql);
51
+ deleteStmt.run(event.id);
52
+ // Get only the indexable tags using applesauce-core helper
53
+ const indexableTags = getIndexableTags(event);
54
+ if (indexableTags && indexableTags.size > 0) {
55
+ const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT.sql);
56
+ for (const tagString of indexableTags) {
57
+ // Parse the "tagName:tagValue" format
58
+ const [name, value] = tagString.split(":");
59
+ if (name && value)
60
+ insertStmt.run(event.id, name, value);
61
+ }
62
+ }
63
+ }
64
+ /** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
65
+ export function deleteEvent(db, id) {
66
+ return db.transaction(() => {
67
+ // Delete from event_tags first (foreign key constraint)
68
+ const deleteTagsStmt = db.prepare(DELETE_EVENT_TAGS_STATEMENT.sql);
69
+ deleteTagsStmt.run(id);
70
+ // Delete from search tables
71
+ deleteSearchContent(db, id);
72
+ // Delete from events table
73
+ const deleteEventStmt = db.prepare(DELETE_EVENT_STATEMENT.sql);
74
+ const result = deleteEventStmt.run(id);
75
+ return result.changes > 0;
76
+ })();
77
+ }
78
+ /** Checks if an event exists */
79
+ export function hasEvent(db, id) {
80
+ const stmt = db.prepare(HAS_EVENT_STATEMENT.sql);
81
+ const result = stmt.get(id);
82
+ if (!result)
83
+ return false;
84
+ return result.count > 0;
85
+ }
86
+ /** Gets a single event from a database */
87
+ export function getEvent(db, id) {
88
+ const stmt = db.prepare(GET_EVENT_STATEMENT.sql);
89
+ const row = stmt.get(id);
90
+ return row && rowToEvent(row);
91
+ }
92
+ /** Gets the latest replaceable event from a database */
93
+ export function getReplaceable(db, kind, pubkey, identifier) {
94
+ const stmt = db.prepare(GET_REPLACEABLE_STATEMENT.sql);
95
+ const row = stmt.get(kind, pubkey, identifier);
96
+ return row && rowToEvent(row);
97
+ }
98
+ /** Gets the history of a replaceable event from a database */
99
+ export function getReplaceableHistory(db, kind, pubkey, identifier) {
100
+ const stmt = db.prepare(GET_REPLACEABLE_HISTORY_STATEMENT.sql);
101
+ return stmt.all(kind, pubkey, identifier).map(rowToEvent);
102
+ }
103
+ /** Checks if a replaceable event exists in a database */
104
+ export function hasReplaceable(db, kind, pubkey, identifier = "") {
105
+ const stmt = db.prepare(HAS_REPLACEABLE_STATEMENT.sql);
106
+ const result = stmt.get(kind, pubkey, identifier);
107
+ if (!result)
108
+ return false;
109
+ return result.count > 0;
110
+ }
111
+ /** Get all events that match the filters (includes NIP-50 search support) */
112
+ export function getEventsByFilters(db, filters) {
113
+ const query = buildFiltersQuery(filters);
114
+ if (!query)
115
+ return new Set();
116
+ const eventSet = new Set();
117
+ const stmt = db.prepare(query.sql);
118
+ const rows = stmt.all(...query.params);
119
+ // Convert rows to events and add to set
120
+ for (const row of rows)
121
+ eventSet.add(rowToEvent(row));
122
+ return eventSet;
123
+ }
124
+ /** Search events using FTS5 full-text search (convenience wrapper around getEventsByFilters) */
125
+ export function searchEvents(db, search, options) {
126
+ if (!search.trim())
127
+ return [];
128
+ // Build filter with search and other options
129
+ const filter = {
130
+ search: search.trim(),
131
+ ...options,
132
+ };
133
+ // Use the main filter system which now supports search
134
+ const results = getEventsByFilters(db, filter);
135
+ return Array.from(results);
136
+ }
137
+ /** Rebuild the FTS5 search index for all events */
138
+ export function rebuildSearchIndex(db, contentFormatter) {
139
+ db.transaction(() => {
140
+ // Clear existing search data
141
+ db.exec(`DELETE FROM events_search;`);
142
+ // Rebuild from all events
143
+ const stmt = db.prepare(GET_ALL_EVENTS_STATEMENT.sql);
144
+ const events = stmt.all().map(rowToEvent);
145
+ for (const event of events) {
146
+ insertSearchContent(db, event, contentFormatter);
147
+ }
148
+ })();
149
+ }
@@ -0,0 +1,52 @@
1
+ import { IEventDatabase } from "applesauce-core";
2
+ import { Filter, NostrEvent } from "applesauce-core/helpers";
3
+ import { Database } from "bun:sqlite";
4
+ import { SearchContentFormatter } from "../helpers/search.js";
5
+ /** Options for the {@link BunSqliteEventDatabase} */
6
+ export type BunSqliteEventDatabaseOptions = {
7
+ search?: boolean;
8
+ searchContentFormatter?: SearchContentFormatter;
9
+ };
10
+ export declare class BunSqliteEventDatabase implements IEventDatabase {
11
+ db: Database;
12
+ /** If search is enabled */
13
+ private search;
14
+ /** The search content formatter */
15
+ private searchContentFormatter;
16
+ constructor(database?: string | Database, options?: BunSqliteEventDatabaseOptions);
17
+ /** Store a Nostr event in the database */
18
+ add(event: NostrEvent): NostrEvent;
19
+ /** Delete an event by ID */
20
+ remove(id: string): boolean;
21
+ /** Checks if an event exists */
22
+ hasEvent(id: string): boolean;
23
+ /** Get an event by its ID */
24
+ getEvent(id: string): NostrEvent | undefined;
25
+ /** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
26
+ getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
27
+ /** Checks if a replaceable event exists */
28
+ hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
29
+ /** Returns all the versions of a replaceable event */
30
+ getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[];
31
+ /** Get all events that match the filters (supports NIP-50 search field) */
32
+ getByFilters(filters: (Filter & {
33
+ search?: string;
34
+ }) | (Filter & {
35
+ search?: string;
36
+ })[]): Set<NostrEvent>;
37
+ /** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
38
+ getTimeline(filters: (Filter & {
39
+ search?: string;
40
+ }) | (Filter & {
41
+ search?: string;
42
+ })[]): NostrEvent[];
43
+ /** Set the search content formatter */
44
+ setSearchContentFormatter(formatter: SearchContentFormatter): void;
45
+ /** Get the current search content formatter */
46
+ getSearchContentFormatter(): SearchContentFormatter;
47
+ /** Rebuild the search index for all events */
48
+ rebuildSearchIndex(): void;
49
+ /** Close the database connection */
50
+ close(): void;
51
+ [Symbol.dispose](): void;
52
+ }
@@ -0,0 +1,105 @@
1
+ import { logger } from "applesauce-core";
2
+ import { insertEventIntoDescendingList } from "applesauce-core/helpers";
3
+ // @ts-ignore - bun:sqlite is a built-in module in Bun
4
+ import { Database } from "bun:sqlite";
5
+ import { createTables, deleteEvent, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./methods.js";
6
+ import { enhancedSearchContentFormatter } from "../helpers/search.js";
7
+ const log = logger.extend("BunSqliteEventDatabase");
8
+ export class BunSqliteEventDatabase {
9
+ db;
10
+ /** If search is enabled */
11
+ search;
12
+ /** The search content formatter */
13
+ searchContentFormatter;
14
+ constructor(database = ":memory:", options) {
15
+ this.db = typeof database === "string" ? new Database(database) : database;
16
+ this.search = options?.search ?? false;
17
+ this.searchContentFormatter = options?.searchContentFormatter ?? enhancedSearchContentFormatter;
18
+ // Setup the database tables and indexes
19
+ createTables(this.db, this.search);
20
+ }
21
+ /** Store a Nostr event in the database */
22
+ add(event) {
23
+ try {
24
+ insertEvent(this.db, event, this.search ? this.searchContentFormatter : undefined);
25
+ return event;
26
+ }
27
+ catch (error) {
28
+ log("Error inserting event:", error);
29
+ throw error;
30
+ }
31
+ }
32
+ /** Delete an event by ID */
33
+ remove(id) {
34
+ try {
35
+ // Remove event from database
36
+ return deleteEvent(this.db, id);
37
+ }
38
+ catch (error) {
39
+ return false;
40
+ }
41
+ }
42
+ /** Checks if an event exists */
43
+ hasEvent(id) {
44
+ return hasEvent(this.db, id);
45
+ }
46
+ /** Get an event by its ID */
47
+ getEvent(id) {
48
+ return getEvent(this.db, id);
49
+ }
50
+ /** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
51
+ getReplaceable(kind, pubkey, identifier = "") {
52
+ return getReplaceable(this.db, kind, pubkey, identifier);
53
+ }
54
+ /** Checks if a replaceable event exists */
55
+ hasReplaceable(kind, pubkey, identifier = "") {
56
+ return hasReplaceable(this.db, kind, pubkey, identifier);
57
+ }
58
+ /** Returns all the versions of a replaceable event */
59
+ getReplaceableHistory(kind, pubkey, identifier = "") {
60
+ return getReplaceableHistory(this.db, kind, pubkey, identifier);
61
+ }
62
+ /** Get all events that match the filters (supports NIP-50 search field) */
63
+ getByFilters(filters) {
64
+ try {
65
+ // If search is disabled, remove the search field from the filters
66
+ if (!this.search && (Array.isArray(filters) ? filters.some((f) => "search" in f) : "search" in filters))
67
+ throw new Error("Search is disabled");
68
+ return getEventsByFilters(this.db, filters);
69
+ }
70
+ catch (error) {
71
+ return new Set();
72
+ }
73
+ }
74
+ /** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
75
+ getTimeline(filters) {
76
+ const events = this.getByFilters(filters);
77
+ const timeline = [];
78
+ for (const event of events)
79
+ insertEventIntoDescendingList(timeline, event);
80
+ return timeline;
81
+ }
82
+ /** Set the search content formatter */
83
+ setSearchContentFormatter(formatter) {
84
+ this.searchContentFormatter = formatter;
85
+ }
86
+ /** Get the current search content formatter */
87
+ getSearchContentFormatter() {
88
+ return this.searchContentFormatter;
89
+ }
90
+ /** Rebuild the search index for all events */
91
+ rebuildSearchIndex() {
92
+ if (!this.search)
93
+ throw new Error("Search is disabled");
94
+ rebuildSearchIndex(this.db, this.searchContentFormatter);
95
+ log("Search index rebuilt successfully");
96
+ }
97
+ /** Close the database connection */
98
+ close() {
99
+ log("Closing database connection");
100
+ this.db.close();
101
+ }
102
+ [Symbol.dispose]() {
103
+ this.close();
104
+ }
105
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./event-database.js";
2
+ export * from "./methods.js";
@@ -0,0 +1,2 @@
1
+ export * from "./event-database.js";
2
+ export * from "./methods.js";