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
@@ -0,0 +1,31 @@
1
+ import { Filter, NostrEvent } from "applesauce-core/helpers";
2
+ import { Client, Transaction } from "@libsql/client";
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: Client, search?: boolean): Promise<void>;
6
+ /** Inserts search content for an event */
7
+ export declare function insertSearchContent(db: Client | Transaction, event: NostrEvent, contentFormatter: SearchContentFormatter): Promise<void>;
8
+ /** Removes search content for an event */
9
+ export declare function deleteSearchContent(db: Client | Transaction, eventId: string): Promise<void>;
10
+ /** Inserts an event into the `events`, `event_tags`, and search tables of a database */
11
+ export declare function insertEvent(db: Client, event: NostrEvent, contentFormatter?: SearchContentFormatter): Promise<boolean>;
12
+ /** Insert indexable tags for an event into the event_tags table */
13
+ export declare function insertEventTags(db: Client | Transaction, event: NostrEvent): Promise<void>;
14
+ /** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
15
+ export declare function deleteEvent(db: Client, id: string): Promise<boolean>;
16
+ /** Checks if an event exists */
17
+ export declare function hasEvent(db: Client, id: string): Promise<boolean>;
18
+ /** Gets a single event from a database */
19
+ export declare function getEvent(db: Client, id: string): Promise<NostrEvent | undefined>;
20
+ /** Gets the latest replaceable event from a database */
21
+ export declare function getReplaceable(db: Client, kind: number, pubkey: string, identifier: string): Promise<NostrEvent | undefined>;
22
+ /** Gets the history of a replaceable event from a database */
23
+ export declare function getReplaceableHistory(db: Client, kind: number, pubkey: string, identifier: string): Promise<NostrEvent[]>;
24
+ /** Checks if a replaceable event exists in a database */
25
+ export declare function hasReplaceable(db: Client, kind: number, pubkey: string, identifier?: string): Promise<boolean>;
26
+ /** Get all events that match the filters (includes NIP-50 search support) */
27
+ export declare function getEventsByFilters(db: Client, filters: FilterWithSearch | FilterWithSearch[]): Promise<Set<NostrEvent>>;
28
+ /** Search events using FTS5 full-text search (convenience wrapper around getEventsByFilters) */
29
+ export declare function searchEvents(db: Client, search: string, options?: Filter): Promise<NostrEvent[]>;
30
+ /** Rebuild the FTS5 search index for all events */
31
+ export declare function rebuildSearchIndex(db: Client, contentFormatter: SearchContentFormatter): Promise<void>;
@@ -0,0 +1,249 @@
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 async function createTables(db, search = true) {
7
+ // Create the events table
8
+ await db.execute(CREATE_EVENTS_TABLE_STATEMENT.sql);
9
+ // Create the event_tags table
10
+ await db.execute(CREATE_EVENT_TAGS_TABLE_STATEMENT.sql);
11
+ // Create the FTS5 search table
12
+ if (search) {
13
+ await db.execute(CREATE_SEARCH_TABLE_STATEMENT.sql);
14
+ }
15
+ // Create indexes
16
+ for (const indexStatement of CREATE_INDEXES_STATEMENTS) {
17
+ await db.execute(indexStatement.sql);
18
+ }
19
+ }
20
+ /** Inserts search content for an event */
21
+ export async function insertSearchContent(db, event, contentFormatter) {
22
+ const searchableContent = contentFormatter(event);
23
+ // Insert/update directly into the FTS5 table
24
+ await db.execute({
25
+ sql: INSERT_SEARCH_CONTENT_STATEMENT.sql,
26
+ args: [event.id, searchableContent, event.kind, event.pubkey, event.created_at],
27
+ });
28
+ }
29
+ /** Removes search content for an event */
30
+ export async function deleteSearchContent(db, eventId) {
31
+ await db.execute({
32
+ sql: DELETE_SEARCH_CONTENT_STATEMENT.sql,
33
+ args: [eventId],
34
+ });
35
+ }
36
+ /** Inserts an event into the `events`, `event_tags`, and search tables of a database */
37
+ export async function insertEvent(db, event, contentFormatter) {
38
+ const identifier = getReplaceableIdentifier(event);
39
+ const transaction = await db.transaction();
40
+ try {
41
+ // Insert/update the main event
42
+ const result = await transaction.execute({
43
+ sql: INSERT_EVENT_STATEMENT.sql,
44
+ args: [
45
+ event.id,
46
+ event.kind,
47
+ event.pubkey,
48
+ event.created_at,
49
+ event.content,
50
+ JSON.stringify(event.tags),
51
+ event.sig,
52
+ identifier,
53
+ ],
54
+ });
55
+ // Insert indexable tags into the event_tags table
56
+ await insertEventTags(transaction, event);
57
+ // Insert searchable content into the search tables
58
+ if (contentFormatter)
59
+ await insertSearchContent(transaction, event, contentFormatter);
60
+ await transaction.commit();
61
+ return result.rowsAffected > 0;
62
+ }
63
+ catch (error) {
64
+ await transaction.rollback();
65
+ throw error;
66
+ }
67
+ }
68
+ /** Insert indexable tags for an event into the event_tags table */
69
+ export async function insertEventTags(db, event) {
70
+ // Clear existing tags for this event first
71
+ await db.execute({
72
+ sql: DELETE_EVENT_TAGS_STATEMENT.sql,
73
+ args: [event.id],
74
+ });
75
+ // Get only the indexable tags using applesauce-core helper
76
+ const indexableTags = getIndexableTags(event);
77
+ if (indexableTags && indexableTags.size > 0) {
78
+ for (const tagString of indexableTags) {
79
+ // Parse the "tagName:tagValue" format
80
+ const [name, value] = tagString.split(":");
81
+ if (name && value) {
82
+ await db.execute({
83
+ sql: INSERT_EVENT_TAG_STATEMENT.sql,
84
+ args: [event.id, name, value],
85
+ });
86
+ }
87
+ }
88
+ }
89
+ }
90
+ /** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
91
+ export async function deleteEvent(db, id) {
92
+ const transaction = await db.transaction();
93
+ try {
94
+ // Delete from event_tags first (foreign key constraint)
95
+ await transaction.execute({
96
+ sql: DELETE_EVENT_TAGS_STATEMENT.sql,
97
+ args: [id],
98
+ });
99
+ // Delete from search tables
100
+ await deleteSearchContent(transaction, id);
101
+ // Delete from events table
102
+ const result = await transaction.execute({
103
+ sql: DELETE_EVENT_STATEMENT.sql,
104
+ args: [id],
105
+ });
106
+ await transaction.commit();
107
+ return result.rowsAffected > 0;
108
+ }
109
+ catch (error) {
110
+ await transaction.rollback();
111
+ throw error;
112
+ }
113
+ }
114
+ /** Checks if an event exists */
115
+ export async function hasEvent(db, id) {
116
+ const result = await db.execute({
117
+ sql: HAS_EVENT_STATEMENT.sql,
118
+ args: [id],
119
+ });
120
+ if (!result.rows[0])
121
+ return false;
122
+ return result.rows[0][0] > 0;
123
+ }
124
+ /** Gets a single event from a database */
125
+ export async function getEvent(db, id) {
126
+ const result = await db.execute({
127
+ sql: GET_EVENT_STATEMENT.sql,
128
+ args: [id],
129
+ });
130
+ const row = result.rows[0];
131
+ return (row &&
132
+ rowToEvent({
133
+ id: row[0],
134
+ kind: row[1],
135
+ pubkey: row[2],
136
+ created_at: row[3],
137
+ content: row[4],
138
+ tags: row[5],
139
+ sig: row[6],
140
+ }));
141
+ }
142
+ /** Gets the latest replaceable event from a database */
143
+ export async function getReplaceable(db, kind, pubkey, identifier) {
144
+ const result = await db.execute({
145
+ sql: GET_REPLACEABLE_STATEMENT.sql,
146
+ args: [kind, pubkey, identifier],
147
+ });
148
+ const row = result.rows[0];
149
+ return (row &&
150
+ rowToEvent({
151
+ id: row[0],
152
+ kind: row[1],
153
+ pubkey: row[2],
154
+ created_at: row[3],
155
+ content: row[4],
156
+ tags: row[5],
157
+ sig: row[6],
158
+ }));
159
+ }
160
+ /** Gets the history of a replaceable event from a database */
161
+ export async function getReplaceableHistory(db, kind, pubkey, identifier) {
162
+ const result = await db.execute({
163
+ sql: GET_REPLACEABLE_HISTORY_STATEMENT.sql,
164
+ args: [kind, pubkey, identifier],
165
+ });
166
+ return result.rows.map((row) => rowToEvent({
167
+ id: row[0],
168
+ kind: row[1],
169
+ pubkey: row[2],
170
+ created_at: row[3],
171
+ content: row[4],
172
+ tags: row[5],
173
+ sig: row[6],
174
+ }));
175
+ }
176
+ /** Checks if a replaceable event exists in a database */
177
+ export async function hasReplaceable(db, kind, pubkey, identifier = "") {
178
+ const result = await db.execute({
179
+ sql: HAS_REPLACEABLE_STATEMENT.sql,
180
+ args: [kind, pubkey, identifier],
181
+ });
182
+ if (!result.rows[0])
183
+ return false;
184
+ return result.rows[0][0] > 0;
185
+ }
186
+ /** Get all events that match the filters (includes NIP-50 search support) */
187
+ export async function getEventsByFilters(db, filters) {
188
+ const query = buildFiltersQuery(filters);
189
+ if (!query)
190
+ return new Set();
191
+ const eventSet = new Set();
192
+ const result = await db.execute({
193
+ sql: query.sql,
194
+ args: query.params,
195
+ });
196
+ // Convert rows to events and add to set
197
+ for (const row of result.rows) {
198
+ eventSet.add(rowToEvent({
199
+ id: row[0],
200
+ kind: row[1],
201
+ pubkey: row[2],
202
+ created_at: row[3],
203
+ content: row[4],
204
+ tags: row[5],
205
+ sig: row[6],
206
+ }));
207
+ }
208
+ return eventSet;
209
+ }
210
+ /** Search events using FTS5 full-text search (convenience wrapper around getEventsByFilters) */
211
+ export async function searchEvents(db, search, options) {
212
+ if (!search.trim())
213
+ return [];
214
+ // Build filter with search and other options
215
+ const filter = {
216
+ search: search.trim(),
217
+ ...options,
218
+ };
219
+ // Use the main filter system which now supports search
220
+ const results = await getEventsByFilters(db, filter);
221
+ return Array.from(results);
222
+ }
223
+ /** Rebuild the FTS5 search index for all events */
224
+ export async function rebuildSearchIndex(db, contentFormatter) {
225
+ const transaction = await db.transaction();
226
+ try {
227
+ // Clear existing search data
228
+ await transaction.execute(`DELETE FROM events_search;`);
229
+ // Rebuild from all events
230
+ const result = await transaction.execute(GET_ALL_EVENTS_STATEMENT.sql);
231
+ const events = result.rows.map((row) => rowToEvent({
232
+ id: row[0],
233
+ kind: row[1],
234
+ pubkey: row[2],
235
+ created_at: row[3],
236
+ content: row[4],
237
+ tags: row[5],
238
+ sig: row[6],
239
+ }));
240
+ for (const event of events) {
241
+ await insertSearchContent(transaction, event, contentFormatter);
242
+ }
243
+ await transaction.commit();
244
+ }
245
+ catch (error) {
246
+ await transaction.rollback();
247
+ throw error;
248
+ }
249
+ }
@@ -0,0 +1,52 @@
1
+ import { IEventDatabase } from "applesauce-core";
2
+ import { Filter, NostrEvent } from "applesauce-core/helpers";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import { SearchContentFormatter } from "../helpers/search.js";
5
+ /** Options for the {@link NativeSqliteEventDatabase} */
6
+ export type NativeSqliteEventDatabaseOptions = {
7
+ search?: boolean;
8
+ searchContentFormatter?: SearchContentFormatter;
9
+ };
10
+ export declare class NativeSqliteEventDatabase implements IEventDatabase {
11
+ db: DatabaseSync;
12
+ /** If search is enabled */
13
+ private search;
14
+ /** The search content formatter */
15
+ private searchContentFormatter;
16
+ constructor(database?: string | DatabaseSync, options?: NativeSqliteEventDatabaseOptions);
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 { DatabaseSync } from "node:sqlite";
4
+ import { enhancedSearchContentFormatter } from "../helpers/search.js";
5
+ import { createTables, deleteEvent, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./methods.js";
6
+ const log = logger.extend("NativeSqliteEventDatabase");
7
+ export class NativeSqliteEventDatabase {
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 DatabaseSync(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 { DatabaseSync } from "node:sqlite";
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: DatabaseSync, search?: boolean): void;
6
+ /** Inserts search content for an event */
7
+ export declare function insertSearchContent(db: DatabaseSync, event: NostrEvent, contentFormatter: SearchContentFormatter): void;
8
+ /** Removes search content for an event */
9
+ export declare function deleteSearchContent(db: DatabaseSync, eventId: string): void;
10
+ /** Inserts an event into the `events`, `event_tags`, and search tables of a database */
11
+ export declare function insertEvent(db: DatabaseSync, event: NostrEvent, contentFormatter?: SearchContentFormatter): boolean;
12
+ /** Insert indexable tags for an event into the event_tags table */
13
+ export declare function insertEventTags(db: DatabaseSync, 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: DatabaseSync, id: string): boolean;
16
+ /** Checks if an event exists */
17
+ export declare function hasEvent(db: DatabaseSync, id: string): boolean;
18
+ /** Gets a single event from a database */
19
+ export declare function getEvent(db: DatabaseSync, id: string): NostrEvent | undefined;
20
+ /** Gets the latest replaceable event from a database */
21
+ export declare function getReplaceable(db: DatabaseSync, 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: DatabaseSync, kind: number, pubkey: string, identifier: string): NostrEvent[];
24
+ /** Checks if a replaceable event exists in a database */
25
+ export declare function hasReplaceable(db: DatabaseSync, 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: DatabaseSync, filters: FilterWithSearch | FilterWithSearch[]): Set<NostrEvent>;
28
+ /** Search events using FTS5 full-text search (convenience wrapper around getEventsByFilters) */
29
+ export declare function searchEvents(db: DatabaseSync, search: string, options?: Filter): NostrEvent[];
30
+ /** Rebuild the FTS5 search index for all events */
31
+ export declare function rebuildSearchIndex(db: DatabaseSync, contentFormatter: SearchContentFormatter): void;
@@ -0,0 +1,174 @@
1
+ import { logger } from "applesauce-core";
2
+ import { getIndexableTags, getReplaceableIdentifier } from "applesauce-core/helpers";
3
+ import { CREATE_SEARCH_TABLE_STATEMENT, DELETE_SEARCH_CONTENT_STATEMENT, INSERT_SEARCH_CONTENT_STATEMENT, } from "../helpers/search.js";
4
+ import { buildFiltersQuery, rowToEvent } from "../helpers/sql.js";
5
+ 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";
6
+ const log = logger.extend("sqlite:tables");
7
+ /** Create and migrate the `events`, `event_tags`, and search tables */
8
+ export function createTables(db, search = true) {
9
+ // Create the events table
10
+ log("Creating events table");
11
+ db.exec(CREATE_EVENTS_TABLE_STATEMENT.sql);
12
+ // Create the event_tags table
13
+ log("Creating event_tags table");
14
+ db.exec(CREATE_EVENT_TAGS_TABLE_STATEMENT.sql);
15
+ // Create the FTS5 search table
16
+ if (search) {
17
+ log("Creating events_search FTS5 table");
18
+ db.exec(CREATE_SEARCH_TABLE_STATEMENT.sql);
19
+ }
20
+ // Create indexes
21
+ log("Creating indexes");
22
+ CREATE_INDEXES_STATEMENTS.forEach((indexStatement) => {
23
+ db.exec(indexStatement.sql);
24
+ });
25
+ }
26
+ /** Inserts search content for an event */
27
+ export function insertSearchContent(db, event, contentFormatter) {
28
+ const searchableContent = contentFormatter(event);
29
+ // Insert/update directly into the FTS5 table
30
+ const stmt = db.prepare(INSERT_SEARCH_CONTENT_STATEMENT.sql);
31
+ stmt.run(event.id, searchableContent, event.kind, event.pubkey, event.created_at);
32
+ }
33
+ /** Removes search content for an event */
34
+ export function deleteSearchContent(db, eventId) {
35
+ const stmt = db.prepare(DELETE_SEARCH_CONTENT_STATEMENT.sql);
36
+ stmt.run(eventId);
37
+ }
38
+ /** Inserts an event into the `events`, `event_tags`, and search tables of a database */
39
+ export function insertEvent(db, event, contentFormatter) {
40
+ const identifier = getReplaceableIdentifier(event);
41
+ // Node.js sqlite doesn't have a transaction method like better-sqlite3, so we use BEGIN/COMMIT
42
+ db.exec("BEGIN");
43
+ try {
44
+ // Insert/update the main event
45
+ const stmt = db.prepare(INSERT_EVENT_STATEMENT.sql);
46
+ const result = stmt.run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
47
+ // Insert indexable tags into the event_tags table
48
+ insertEventTags(db, event);
49
+ // Insert searchable content into the search tables
50
+ if (contentFormatter)
51
+ insertSearchContent(db, event, contentFormatter);
52
+ db.exec("COMMIT");
53
+ return result.changes > 0;
54
+ }
55
+ catch (error) {
56
+ db.exec("ROLLBACK");
57
+ throw error;
58
+ }
59
+ }
60
+ /** Insert indexable tags for an event into the event_tags table */
61
+ export function insertEventTags(db, event) {
62
+ // Clear existing tags for this event first
63
+ const deleteStmt = db.prepare(DELETE_EVENT_TAGS_STATEMENT.sql);
64
+ deleteStmt.run(event.id);
65
+ // Get only the indexable tags using applesauce-core helper
66
+ const indexableTags = getIndexableTags(event);
67
+ if (indexableTags && indexableTags.size > 0) {
68
+ const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT.sql);
69
+ for (const tagString of indexableTags) {
70
+ // Parse the "tagName:tagValue" format
71
+ const [name, value] = tagString.split(":");
72
+ if (name && value)
73
+ insertStmt.run(event.id, name, value);
74
+ }
75
+ }
76
+ }
77
+ /** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
78
+ export function deleteEvent(db, id) {
79
+ db.exec("BEGIN");
80
+ try {
81
+ // Delete from event_tags first (foreign key constraint)
82
+ const deleteTagsStmt = db.prepare(DELETE_EVENT_TAGS_STATEMENT.sql);
83
+ deleteTagsStmt.run(id);
84
+ // Delete from search tables
85
+ deleteSearchContent(db, id);
86
+ // Delete from events table
87
+ const deleteEventStmt = db.prepare(DELETE_EVENT_STATEMENT.sql);
88
+ const result = deleteEventStmt.run(id);
89
+ db.exec("COMMIT");
90
+ return result.changes > 0;
91
+ }
92
+ catch (error) {
93
+ db.exec("ROLLBACK");
94
+ throw error;
95
+ }
96
+ }
97
+ /** Checks if an event exists */
98
+ export function hasEvent(db, id) {
99
+ const stmt = db.prepare(HAS_EVENT_STATEMENT.sql);
100
+ const result = stmt.get(id);
101
+ if (!result)
102
+ return false;
103
+ return result.count > 0;
104
+ }
105
+ /** Gets a single event from a database */
106
+ export function getEvent(db, id) {
107
+ const stmt = db.prepare(GET_EVENT_STATEMENT.sql);
108
+ const row = stmt.get(id);
109
+ return row && rowToEvent(row);
110
+ }
111
+ /** Gets the latest replaceable event from a database */
112
+ export function getReplaceable(db, kind, pubkey, identifier) {
113
+ const stmt = db.prepare(GET_REPLACEABLE_STATEMENT.sql);
114
+ const row = stmt.get(kind, pubkey, identifier);
115
+ return row && rowToEvent(row);
116
+ }
117
+ /** Gets the history of a replaceable event from a database */
118
+ export function getReplaceableHistory(db, kind, pubkey, identifier) {
119
+ const stmt = db.prepare(GET_REPLACEABLE_HISTORY_STATEMENT.sql);
120
+ return stmt.all(kind, pubkey, identifier).map(rowToEvent);
121
+ }
122
+ /** Checks if a replaceable event exists in a database */
123
+ export function hasReplaceable(db, kind, pubkey, identifier = "") {
124
+ const stmt = db.prepare(HAS_REPLACEABLE_STATEMENT.sql);
125
+ const result = stmt.get(kind, pubkey, identifier);
126
+ if (!result)
127
+ return false;
128
+ return result.count > 0;
129
+ }
130
+ /** Get all events that match the filters (includes NIP-50 search support) */
131
+ export function getEventsByFilters(db, filters) {
132
+ const query = buildFiltersQuery(filters);
133
+ if (!query)
134
+ return new Set();
135
+ const eventSet = new Set();
136
+ const stmt = db.prepare(query.sql);
137
+ const rows = stmt.all(...query.params);
138
+ // Convert rows to events and add to set
139
+ for (const row of rows)
140
+ eventSet.add(rowToEvent(row));
141
+ return eventSet;
142
+ }
143
+ /** Search events using FTS5 full-text search (convenience wrapper around getEventsByFilters) */
144
+ export function searchEvents(db, search, options) {
145
+ if (!search.trim())
146
+ return [];
147
+ // Build filter with search and other options
148
+ const filter = {
149
+ search: search.trim(),
150
+ ...options,
151
+ };
152
+ // Use the main filter system which now supports search
153
+ const results = getEventsByFilters(db, filter);
154
+ return Array.from(results);
155
+ }
156
+ /** Rebuild the FTS5 search index for all events */
157
+ export function rebuildSearchIndex(db, contentFormatter) {
158
+ db.exec("BEGIN");
159
+ try {
160
+ // Clear existing search data
161
+ db.exec(`DELETE FROM events_search;`);
162
+ // Rebuild from all events
163
+ const stmt = db.prepare(GET_ALL_EVENTS_STATEMENT.sql);
164
+ const events = stmt.all().map(rowToEvent);
165
+ for (const event of events) {
166
+ insertSearchContent(db, event, contentFormatter);
167
+ }
168
+ db.exec("COMMIT");
169
+ }
170
+ catch (error) {
171
+ db.exec("ROLLBACK");
172
+ throw error;
173
+ }
174
+ }
@@ -0,0 +1 @@
1
+ export {};