applesauce-sqlite 0.0.0-next-20250918142212 → 0.0.0-next-20250923113611
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.
- package/package.json +7 -5
- package/dist/helpers/sqlite.d.ts +0 -66
- package/dist/helpers/sqlite.js +0 -367
- package/dist/sqlite-event-database.d.ts +0 -53
- package/dist/sqlite-event-database.js +0 -105
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "applesauce-sqlite",
|
|
3
|
-
"version": "0.0.0-next-
|
|
3
|
+
"version": "0.0.0-next-20250923113611",
|
|
4
4
|
"description": "sqlite event databases for applesauce",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -82,17 +82,18 @@
|
|
|
82
82
|
}
|
|
83
83
|
},
|
|
84
84
|
"dependencies": {
|
|
85
|
-
"applesauce-core": "0.0.0-next-
|
|
85
|
+
"applesauce-core": "0.0.0-next-20250923113611"
|
|
86
86
|
},
|
|
87
87
|
"optionalDependencies": {
|
|
88
|
-
"
|
|
89
|
-
"
|
|
88
|
+
"@libsql/client": "^0.15.15",
|
|
89
|
+
"better-sqlite3": "^12.2.0"
|
|
90
90
|
},
|
|
91
91
|
"devDependencies": {
|
|
92
92
|
"@hirez_io/observer-spy": "^2.2.0",
|
|
93
93
|
"@types/better-sqlite3": "^7.6.13",
|
|
94
94
|
"@types/ws": "^8.5.13",
|
|
95
|
-
"applesauce-signers": "0.0.0-next-
|
|
95
|
+
"applesauce-signers": "0.0.0-next-20250923113611",
|
|
96
|
+
"rimraf": "^6.0.1",
|
|
96
97
|
"typescript": "^5.7.3",
|
|
97
98
|
"vitest": "^3.2.4",
|
|
98
99
|
"vitest-websocket-mock": "^0.5.0",
|
|
@@ -103,6 +104,7 @@
|
|
|
103
104
|
"url": "lightning:nostrudel@geyser.fund"
|
|
104
105
|
},
|
|
105
106
|
"scripts": {
|
|
107
|
+
"prebuild": "rimraf dist",
|
|
106
108
|
"build": "tsc",
|
|
107
109
|
"watch:build": "tsc --watch > /dev/null",
|
|
108
110
|
"test": "vitest run --passWithNoTests",
|
package/dist/helpers/sqlite.d.ts
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { Filter, NostrEvent } from "applesauce-core/helpers";
|
|
2
|
-
import { Database } from "better-sqlite3";
|
|
3
|
-
export declare const CREATE_EVENTS_TABLE = "\nCREATE TABLE IF NOT EXISTS events (\n id TEXT PRIMARY KEY,\n kind INTEGER NOT NULL,\n pubkey TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n content TEXT NOT NULL,\n tags TEXT,\n sig TEXT NOT NULL,\n identifier TEXT NOT NULL DEFAULT ''\n);\n";
|
|
4
|
-
export declare const CREATE_EVENT_TAGS_TABLE = "\nCREATE TABLE IF NOT EXISTS event_tags (\n event_id TEXT NOT NULL,\n tag_name TEXT NOT NULL,\n tag_value TEXT NOT NULL,\n FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,\n PRIMARY KEY (event_id, tag_name, tag_value)\n);\n";
|
|
5
|
-
export declare const CREATE_SEARCH_TABLE = "\nCREATE VIRTUAL TABLE IF NOT EXISTS events_search USING fts5(\n event_id UNINDEXED,\n content,\n kind UNINDEXED,\n pubkey UNINDEXED,\n created_at UNINDEXED\n);\n";
|
|
6
|
-
export declare const CREATE_INDEXES: string[];
|
|
7
|
-
export type EventRow = {
|
|
8
|
-
id: string;
|
|
9
|
-
kind: number;
|
|
10
|
-
pubkey: string;
|
|
11
|
-
created_at: number;
|
|
12
|
-
content: string;
|
|
13
|
-
tags: string;
|
|
14
|
-
sig: string;
|
|
15
|
-
};
|
|
16
|
-
/** Filter with search field */
|
|
17
|
-
export type FilterWithSearch = Filter & {
|
|
18
|
-
search?: string;
|
|
19
|
-
order?: "created_at" | "rank";
|
|
20
|
-
};
|
|
21
|
-
/** Content formatter function type for search indexing */
|
|
22
|
-
export type SearchContentFormatter = (event: NostrEvent) => string;
|
|
23
|
-
/** Default search content formatter - returns the raw content */
|
|
24
|
-
export declare const defaultSearchContentFormatter: SearchContentFormatter;
|
|
25
|
-
/** Enhanced search content formatter that includes tags and special handling for kind 0 events */
|
|
26
|
-
export declare const enhancedSearchContentFormatter: SearchContentFormatter;
|
|
27
|
-
/** Create and migrate the `events`, `event_tags`, and search tables */
|
|
28
|
-
export declare function createTables(db: Database, search?: boolean): void;
|
|
29
|
-
/** Inserts search content for an event */
|
|
30
|
-
export declare function insertSearchContent(db: Database, event: NostrEvent, contentFormatter: SearchContentFormatter): void;
|
|
31
|
-
/** Removes search content for an event */
|
|
32
|
-
export declare function deleteSearchContent(db: Database, eventId: string): void;
|
|
33
|
-
/** Inserts an event into the `events`, `event_tags`, and search tables of a database */
|
|
34
|
-
export declare function insertEvent(db: Database, event: NostrEvent, contentFormatter?: SearchContentFormatter): boolean;
|
|
35
|
-
/** Insert indexable tags for an event into the event_tags table */
|
|
36
|
-
export declare function insertEventTags(db: Database, event: NostrEvent): void;
|
|
37
|
-
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
38
|
-
export declare function deleteEvent(db: Database, id: string): boolean;
|
|
39
|
-
/** Checks if an event exists */
|
|
40
|
-
export declare function hasEvent(db: Database, id: string): boolean;
|
|
41
|
-
/** Gets a single event from a database */
|
|
42
|
-
export declare function getEvent(db: Database, id: string): NostrEvent | undefined;
|
|
43
|
-
/** Gets the latest replaceable event from a database */
|
|
44
|
-
export declare function getReplaceable(db: Database, kind: number, pubkey: string, identifier: string): NostrEvent | undefined;
|
|
45
|
-
/** Gets the history of a replaceable event from a database */
|
|
46
|
-
export declare function getReplaceableHistory(db: Database, kind: number, pubkey: string, identifier: string): NostrEvent[];
|
|
47
|
-
/** Checks if a replaceable event exists in a database */
|
|
48
|
-
export declare function hasReplaceable(db: Database, kind: number, pubkey: string, identifier?: string): boolean;
|
|
49
|
-
/** Convert database row to NostrEvent */
|
|
50
|
-
export declare function rowToEvent(row: EventRow): NostrEvent;
|
|
51
|
-
/** Builds conditions for a single filter */
|
|
52
|
-
export declare function buildFilterConditions(filter: FilterWithSearch): {
|
|
53
|
-
conditions: string[];
|
|
54
|
-
params: any[];
|
|
55
|
-
search: boolean;
|
|
56
|
-
};
|
|
57
|
-
export declare function buildFiltersQuery(filters: FilterWithSearch | FilterWithSearch[]): {
|
|
58
|
-
sql: string;
|
|
59
|
-
params: any[];
|
|
60
|
-
} | null;
|
|
61
|
-
/** Get all events that match the filters (includes NIP-50 search support) */
|
|
62
|
-
export declare function getEventsByFilters(db: Database, filters: FilterWithSearch | FilterWithSearch[]): Set<NostrEvent>;
|
|
63
|
-
/** Search events using FTS5 full-text search (convenience wrapper around getEventsByFilters) */
|
|
64
|
-
export declare function searchEvents(db: Database, search: string, options?: Filter): NostrEvent[];
|
|
65
|
-
/** Rebuild the FTS5 search index for all events */
|
|
66
|
-
export declare function rebuildSearchIndex(db: Database, contentFormatter: SearchContentFormatter): void;
|
package/dist/helpers/sqlite.js
DELETED
|
@@ -1,367 +0,0 @@
|
|
|
1
|
-
import { logger } from "applesauce-core";
|
|
2
|
-
import { getIndexableTags, getReplaceableIdentifier } from "applesauce-core/helpers";
|
|
3
|
-
const log = logger.extend("sqlite:tables");
|
|
4
|
-
// SQL schema for Nostr events
|
|
5
|
-
export const CREATE_EVENTS_TABLE = `
|
|
6
|
-
CREATE TABLE IF NOT EXISTS events (
|
|
7
|
-
id TEXT PRIMARY KEY,
|
|
8
|
-
kind INTEGER NOT NULL,
|
|
9
|
-
pubkey TEXT NOT NULL,
|
|
10
|
-
created_at INTEGER NOT NULL,
|
|
11
|
-
content TEXT NOT NULL,
|
|
12
|
-
tags TEXT,
|
|
13
|
-
sig TEXT NOT NULL,
|
|
14
|
-
identifier TEXT NOT NULL DEFAULT ''
|
|
15
|
-
);
|
|
16
|
-
`;
|
|
17
|
-
// SQL schema for event tags (for efficient tag filtering)
|
|
18
|
-
export const CREATE_EVENT_TAGS_TABLE = `
|
|
19
|
-
CREATE TABLE IF NOT EXISTS event_tags (
|
|
20
|
-
event_id TEXT NOT NULL,
|
|
21
|
-
tag_name TEXT NOT NULL,
|
|
22
|
-
tag_value TEXT NOT NULL,
|
|
23
|
-
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
|
|
24
|
-
PRIMARY KEY (event_id, tag_name, tag_value)
|
|
25
|
-
);
|
|
26
|
-
`;
|
|
27
|
-
// SQL schema for FTS5 search table - stores formatted searchable content directly
|
|
28
|
-
export const CREATE_SEARCH_TABLE = `
|
|
29
|
-
CREATE VIRTUAL TABLE IF NOT EXISTS events_search USING fts5(
|
|
30
|
-
event_id UNINDEXED,
|
|
31
|
-
content,
|
|
32
|
-
kind UNINDEXED,
|
|
33
|
-
pubkey UNINDEXED,
|
|
34
|
-
created_at UNINDEXED
|
|
35
|
-
);
|
|
36
|
-
`;
|
|
37
|
-
export const CREATE_INDEXES = [
|
|
38
|
-
// Events table indexes
|
|
39
|
-
`CREATE INDEX IF NOT EXISTS kind_idx ON events(kind);`,
|
|
40
|
-
`CREATE INDEX IF NOT EXISTS pubkey_idx ON events(pubkey);`,
|
|
41
|
-
`CREATE INDEX IF NOT EXISTS created_at_idx ON events(created_at);`,
|
|
42
|
-
`CREATE INDEX IF NOT EXISTS identifier_idx ON events(identifier);`,
|
|
43
|
-
// Event tags table indexes for efficient tag filtering
|
|
44
|
-
`CREATE INDEX IF NOT EXISTS event_tags_event_id_idx ON event_tags(event_id);`,
|
|
45
|
-
`CREATE INDEX IF NOT EXISTS event_tags_name_value_idx ON event_tags(tag_name, tag_value);`,
|
|
46
|
-
];
|
|
47
|
-
/** Default search content formatter - returns the raw content */
|
|
48
|
-
export const defaultSearchContentFormatter = (event) => {
|
|
49
|
-
return event.content;
|
|
50
|
-
};
|
|
51
|
-
/** Enhanced search content formatter that includes tags and special handling for kind 0 events */
|
|
52
|
-
export const enhancedSearchContentFormatter = (event) => {
|
|
53
|
-
let searchableContent = event.content;
|
|
54
|
-
// Special handling for kind 0 (profile metadata) events
|
|
55
|
-
if (event.kind === 0) {
|
|
56
|
-
try {
|
|
57
|
-
const profile = JSON.parse(event.content);
|
|
58
|
-
const profileFields = [];
|
|
59
|
-
// Include common profile fields in search
|
|
60
|
-
if (profile.name)
|
|
61
|
-
profileFields.push(profile.name);
|
|
62
|
-
if (profile.display_name)
|
|
63
|
-
profileFields.push(profile.display_name);
|
|
64
|
-
if (profile.about)
|
|
65
|
-
profileFields.push(profile.about);
|
|
66
|
-
if (profile.nip05)
|
|
67
|
-
profileFields.push(profile.nip05);
|
|
68
|
-
if (profile.lud16)
|
|
69
|
-
profileFields.push(profile.lud16);
|
|
70
|
-
searchableContent = profileFields.join(" ");
|
|
71
|
-
}
|
|
72
|
-
catch (e) {
|
|
73
|
-
// If JSON parsing fails, use the raw content
|
|
74
|
-
searchableContent = event.content;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
// Include relevant tags in the searchable content
|
|
78
|
-
const relevantTags = ["t", "subject", "title", "summary", "d"]; // hashtags, subject, title, summary, identifier
|
|
79
|
-
const tagContent = [];
|
|
80
|
-
for (const tag of event.tags) {
|
|
81
|
-
if (tag.length >= 2 && relevantTags.includes(tag[0])) {
|
|
82
|
-
tagContent.push(tag[1]);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
// Combine content with tag content
|
|
86
|
-
if (tagContent.length > 0) {
|
|
87
|
-
searchableContent += " " + tagContent.join(" ");
|
|
88
|
-
}
|
|
89
|
-
return searchableContent;
|
|
90
|
-
};
|
|
91
|
-
/** Create and migrate the `events`, `event_tags`, and search tables */
|
|
92
|
-
export function createTables(db, search = true) {
|
|
93
|
-
// Create the events table
|
|
94
|
-
log("Creating events table");
|
|
95
|
-
db.exec(CREATE_EVENTS_TABLE);
|
|
96
|
-
// Create the event_tags table
|
|
97
|
-
log("Creating event_tags table");
|
|
98
|
-
db.exec(CREATE_EVENT_TAGS_TABLE);
|
|
99
|
-
// Create the FTS5 search table
|
|
100
|
-
if (search) {
|
|
101
|
-
log("Creating events_search FTS5 table");
|
|
102
|
-
db.exec(CREATE_SEARCH_TABLE);
|
|
103
|
-
}
|
|
104
|
-
// Create indexes
|
|
105
|
-
log("Creating indexes");
|
|
106
|
-
CREATE_INDEXES.forEach((indexSql) => {
|
|
107
|
-
db.exec(indexSql);
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
/** Inserts search content for an event */
|
|
111
|
-
export function insertSearchContent(db, event, contentFormatter) {
|
|
112
|
-
const searchableContent = contentFormatter(event);
|
|
113
|
-
// Insert/update directly into the FTS5 table
|
|
114
|
-
const stmt = db.prepare(`
|
|
115
|
-
INSERT OR REPLACE INTO events_search (event_id, content, kind, pubkey, created_at)
|
|
116
|
-
VALUES (?, ?, ?, ?, ?)
|
|
117
|
-
`);
|
|
118
|
-
stmt.run(event.id, searchableContent, event.kind, event.pubkey, event.created_at);
|
|
119
|
-
}
|
|
120
|
-
/** Removes search content for an event */
|
|
121
|
-
export function deleteSearchContent(db, eventId) {
|
|
122
|
-
const stmt = db.prepare(`DELETE FROM events_search WHERE event_id = ?`);
|
|
123
|
-
stmt.run(eventId);
|
|
124
|
-
}
|
|
125
|
-
/** Inserts an event into the `events`, `event_tags`, and search tables of a database */
|
|
126
|
-
export function insertEvent(db, event, contentFormatter) {
|
|
127
|
-
const identifier = getReplaceableIdentifier(event);
|
|
128
|
-
return db.transaction(() => {
|
|
129
|
-
// Insert/update the main event
|
|
130
|
-
const stmt = db.prepare(`
|
|
131
|
-
INSERT OR REPLACE INTO events (id, kind, pubkey, created_at, content, tags, sig, identifier)
|
|
132
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
133
|
-
`);
|
|
134
|
-
const result = stmt.run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
|
|
135
|
-
// Insert indexable tags into the event_tags table
|
|
136
|
-
insertEventTags(db, event);
|
|
137
|
-
// Insert searchable content into the search tables
|
|
138
|
-
if (contentFormatter)
|
|
139
|
-
insertSearchContent(db, event, contentFormatter);
|
|
140
|
-
return result.changes > 0;
|
|
141
|
-
})();
|
|
142
|
-
}
|
|
143
|
-
/** Insert indexable tags for an event into the event_tags table */
|
|
144
|
-
export function insertEventTags(db, event) {
|
|
145
|
-
// Clear existing tags for this event first
|
|
146
|
-
const deleteStmt = db.prepare(`DELETE FROM event_tags WHERE event_id = ?`);
|
|
147
|
-
deleteStmt.run(event.id);
|
|
148
|
-
// Get only the indexable tags using applesauce-core helper
|
|
149
|
-
const indexableTags = getIndexableTags(event);
|
|
150
|
-
if (indexableTags && indexableTags.size > 0) {
|
|
151
|
-
const insertStmt = db.prepare(`INSERT OR IGNORE INTO event_tags (event_id, tag_name, tag_value) VALUES (?, ?, ?)`);
|
|
152
|
-
for (const tagString of indexableTags) {
|
|
153
|
-
// Parse the "tagName:tagValue" format
|
|
154
|
-
const [name, value] = tagString.split(":");
|
|
155
|
-
if (name && value)
|
|
156
|
-
insertStmt.run(event.id, name, value);
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
161
|
-
export function deleteEvent(db, id) {
|
|
162
|
-
return db.transaction(() => {
|
|
163
|
-
// Delete from event_tags first (foreign key constraint)
|
|
164
|
-
const deleteTagsStmt = db.prepare(`DELETE FROM event_tags WHERE event_id = ?`);
|
|
165
|
-
deleteTagsStmt.run(id);
|
|
166
|
-
// Delete from search tables
|
|
167
|
-
deleteSearchContent(db, id);
|
|
168
|
-
// Delete from events table
|
|
169
|
-
const deleteEventStmt = db.prepare(`DELETE FROM events WHERE id = ?`);
|
|
170
|
-
const result = deleteEventStmt.run(id);
|
|
171
|
-
return result.changes > 0;
|
|
172
|
-
})();
|
|
173
|
-
}
|
|
174
|
-
/** Checks if an event exists */
|
|
175
|
-
export function hasEvent(db, id) {
|
|
176
|
-
const stmt = db.prepare(`SELECT COUNT(*) as count FROM events WHERE id = ?`);
|
|
177
|
-
const result = stmt.get(id);
|
|
178
|
-
if (!result)
|
|
179
|
-
return false;
|
|
180
|
-
return result.count > 0;
|
|
181
|
-
}
|
|
182
|
-
/** Gets a single event from a database */
|
|
183
|
-
export function getEvent(db, id) {
|
|
184
|
-
const stmt = db.prepare(`SELECT * FROM events WHERE id = ?`);
|
|
185
|
-
const row = stmt.get(id);
|
|
186
|
-
return row && rowToEvent(row);
|
|
187
|
-
}
|
|
188
|
-
/** Gets the latest replaceable event from a database */
|
|
189
|
-
export function getReplaceable(db, kind, pubkey, identifier) {
|
|
190
|
-
const stmt = db.prepare(`SELECT * FROM events WHERE kind = ? AND pubkey = ? AND identifier = ? ORDER BY created_at DESC LIMIT 1`);
|
|
191
|
-
const row = stmt.get(kind, pubkey, identifier);
|
|
192
|
-
return row && rowToEvent(row);
|
|
193
|
-
}
|
|
194
|
-
/** Gets the history of a replaceable event from a database */
|
|
195
|
-
export function getReplaceableHistory(db, kind, pubkey, identifier) {
|
|
196
|
-
const stmt = db.prepare(`SELECT * FROM events WHERE kind = ? AND pubkey = ? AND identifier = ? ORDER BY created_at DESC`);
|
|
197
|
-
return stmt.all(kind, pubkey, identifier).map(rowToEvent);
|
|
198
|
-
}
|
|
199
|
-
/** Checks if a replaceable event exists in a database */
|
|
200
|
-
export function hasReplaceable(db, kind, pubkey, identifier = "") {
|
|
201
|
-
const stmt = db.prepare(`SELECT COUNT(*) as count FROM events WHERE kind = ? AND pubkey = ? AND identifier = ?`);
|
|
202
|
-
const result = stmt.get(kind, pubkey, identifier);
|
|
203
|
-
if (!result)
|
|
204
|
-
return false;
|
|
205
|
-
return result.count > 0;
|
|
206
|
-
}
|
|
207
|
-
/** Convert database row to NostrEvent */
|
|
208
|
-
export function rowToEvent(row) {
|
|
209
|
-
return {
|
|
210
|
-
id: row.id,
|
|
211
|
-
kind: row.kind,
|
|
212
|
-
pubkey: row.pubkey,
|
|
213
|
-
created_at: row.created_at,
|
|
214
|
-
content: row.content,
|
|
215
|
-
tags: JSON.parse(row.tags || "[]"),
|
|
216
|
-
sig: row.sig,
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
/** Builds conditions for a single filter */
|
|
220
|
-
export function buildFilterConditions(filter) {
|
|
221
|
-
const conditions = [];
|
|
222
|
-
const params = [];
|
|
223
|
-
let search = false;
|
|
224
|
-
// Handle NIP-50 search filter
|
|
225
|
-
if (filter.search && filter.search.trim()) {
|
|
226
|
-
conditions.push(`events_search MATCH ?`);
|
|
227
|
-
params.push(filter.search.trim());
|
|
228
|
-
search = true;
|
|
229
|
-
}
|
|
230
|
-
// Handle IDs filter
|
|
231
|
-
if (filter.ids && filter.ids.length > 0) {
|
|
232
|
-
const placeholders = filter.ids.map(() => `?`).join(", ");
|
|
233
|
-
conditions.push(`events.id IN (${placeholders})`);
|
|
234
|
-
params.push(...filter.ids);
|
|
235
|
-
}
|
|
236
|
-
// Handle kinds filter
|
|
237
|
-
if (filter.kinds && filter.kinds.length > 0) {
|
|
238
|
-
const placeholders = filter.kinds.map(() => `?`).join(", ");
|
|
239
|
-
conditions.push(`events.kind IN (${placeholders})`);
|
|
240
|
-
params.push(...filter.kinds);
|
|
241
|
-
}
|
|
242
|
-
// Handle authors filter (pubkeys)
|
|
243
|
-
if (filter.authors && filter.authors.length > 0) {
|
|
244
|
-
const placeholders = filter.authors.map(() => `?`).join(", ");
|
|
245
|
-
conditions.push(`events.pubkey IN (${placeholders})`);
|
|
246
|
-
params.push(...filter.authors);
|
|
247
|
-
}
|
|
248
|
-
// Handle since filter (timestamp >= since)
|
|
249
|
-
if (filter.since !== undefined) {
|
|
250
|
-
conditions.push(`events.created_at >= ?`);
|
|
251
|
-
params.push(filter.since);
|
|
252
|
-
}
|
|
253
|
-
// Handle until filter (timestamp <= until)
|
|
254
|
-
if (filter.until !== undefined) {
|
|
255
|
-
conditions.push(`events.created_at <= ?`);
|
|
256
|
-
params.push(filter.until);
|
|
257
|
-
}
|
|
258
|
-
// Handle tag filters (e.g., #e, #p, #t, #d, etc.)
|
|
259
|
-
for (const [key, values] of Object.entries(filter)) {
|
|
260
|
-
if (key.startsWith("#") && values && Array.isArray(values) && values.length > 0) {
|
|
261
|
-
const tagName = key.slice(1); // Remove the '#' prefix
|
|
262
|
-
// Use the event_tags table for efficient tag filtering
|
|
263
|
-
const placeholders = values.map(() => "?").join(", ");
|
|
264
|
-
conditions.push(`events.id IN (
|
|
265
|
-
SELECT DISTINCT event_id
|
|
266
|
-
FROM event_tags
|
|
267
|
-
WHERE tag_name = ? AND tag_value IN (${placeholders})
|
|
268
|
-
)`);
|
|
269
|
-
// Add parameters: tagName first, then all the tag values
|
|
270
|
-
params.push(tagName, ...values);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
return { conditions, params, search };
|
|
274
|
-
}
|
|
275
|
-
export function buildFiltersQuery(filters) {
|
|
276
|
-
const filterArray = Array.isArray(filters) ? filters : [filters];
|
|
277
|
-
if (filterArray.length === 0)
|
|
278
|
-
return null;
|
|
279
|
-
// Build queries for each filter (OR logic between filters)
|
|
280
|
-
const filterQueries = [];
|
|
281
|
-
const allParams = [];
|
|
282
|
-
let globalLimit;
|
|
283
|
-
// Build the final query with proper ordering and limit
|
|
284
|
-
let fromClause = "events";
|
|
285
|
-
let orderBy = "events.created_at DESC, events.id ASC";
|
|
286
|
-
for (const filter of filterArray) {
|
|
287
|
-
const { conditions, params, search } = buildFilterConditions(filter);
|
|
288
|
-
if (search) {
|
|
289
|
-
// Override the from clause to join the events_search table
|
|
290
|
-
fromClause = "events INNER JOIN events_search ON events.id = events_search.event_id";
|
|
291
|
-
// Set the order by clause based on the filter order
|
|
292
|
-
switch (filter.order) {
|
|
293
|
-
case "created_at":
|
|
294
|
-
orderBy = "events.created_at DESC, events.id ASC";
|
|
295
|
-
break;
|
|
296
|
-
case "rank":
|
|
297
|
-
orderBy = "events_search.rank, events.created_at DESC";
|
|
298
|
-
break;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
if (conditions.length === 0) {
|
|
302
|
-
// If no conditions, this filter matches all events
|
|
303
|
-
filterQueries.push("1=1");
|
|
304
|
-
}
|
|
305
|
-
else {
|
|
306
|
-
// AND logic within a single filter
|
|
307
|
-
filterQueries.push(`(${conditions.join(" AND ")})`);
|
|
308
|
-
}
|
|
309
|
-
allParams.push(...params);
|
|
310
|
-
// Track the most restrictive limit across all filters
|
|
311
|
-
if (filter.limit !== undefined) {
|
|
312
|
-
globalLimit = globalLimit === undefined ? filter.limit : Math.min(globalLimit, filter.limit);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
// Combine all filter conditions with OR logic
|
|
316
|
-
const whereClause = filterQueries.length > 0 ? `WHERE ${filterQueries.join(" OR ")}` : "";
|
|
317
|
-
let query = `
|
|
318
|
-
SELECT DISTINCT events.* FROM ${fromClause}
|
|
319
|
-
${whereClause}
|
|
320
|
-
ORDER BY ${orderBy}
|
|
321
|
-
`;
|
|
322
|
-
// Apply global limit if specified
|
|
323
|
-
if (globalLimit !== undefined && globalLimit > 0) {
|
|
324
|
-
query += ` LIMIT ?`;
|
|
325
|
-
allParams.push(globalLimit);
|
|
326
|
-
}
|
|
327
|
-
return { sql: query, params: allParams };
|
|
328
|
-
}
|
|
329
|
-
/** Get all events that match the filters (includes NIP-50 search support) */
|
|
330
|
-
export function getEventsByFilters(db, filters) {
|
|
331
|
-
const query = buildFiltersQuery(filters);
|
|
332
|
-
if (!query)
|
|
333
|
-
return new Set();
|
|
334
|
-
const eventSet = new Set();
|
|
335
|
-
const stmt = db.prepare(query.sql);
|
|
336
|
-
const rows = stmt.all(...query.params);
|
|
337
|
-
// Convert rows to events and add to set
|
|
338
|
-
for (const row of rows)
|
|
339
|
-
eventSet.add(rowToEvent(row));
|
|
340
|
-
return eventSet;
|
|
341
|
-
}
|
|
342
|
-
/** Search events using FTS5 full-text search (convenience wrapper around getEventsByFilters) */
|
|
343
|
-
export function searchEvents(db, search, options) {
|
|
344
|
-
if (!search.trim())
|
|
345
|
-
return [];
|
|
346
|
-
// Build filter with search and other options
|
|
347
|
-
const filter = {
|
|
348
|
-
search: search.trim(),
|
|
349
|
-
...options,
|
|
350
|
-
};
|
|
351
|
-
// Use the main filter system which now supports search
|
|
352
|
-
const results = getEventsByFilters(db, filter);
|
|
353
|
-
return Array.from(results);
|
|
354
|
-
}
|
|
355
|
-
/** Rebuild the FTS5 search index for all events */
|
|
356
|
-
export function rebuildSearchIndex(db, contentFormatter) {
|
|
357
|
-
db.transaction(() => {
|
|
358
|
-
// Clear existing search data
|
|
359
|
-
db.exec(`DELETE FROM events_search;`);
|
|
360
|
-
// Rebuild from all events
|
|
361
|
-
const stmt = db.prepare(`SELECT * FROM events`);
|
|
362
|
-
const events = stmt.all().map(rowToEvent);
|
|
363
|
-
for (const event of events) {
|
|
364
|
-
insertSearchContent(db, event, contentFormatter);
|
|
365
|
-
}
|
|
366
|
-
})();
|
|
367
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
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 { defaultSearchContentFormatter, enhancedSearchContentFormatter, type SearchContentFormatter } from "./helpers/sqlite.js";
|
|
5
|
-
export { defaultSearchContentFormatter, enhancedSearchContentFormatter, type SearchContentFormatter };
|
|
6
|
-
/** Options for the SqliteEventDatabase */
|
|
7
|
-
export type SqliteEventDatabaseOptions = {
|
|
8
|
-
search?: boolean;
|
|
9
|
-
searchContentFormatter?: SearchContentFormatter;
|
|
10
|
-
};
|
|
11
|
-
export declare class SqliteEventDatabase implements IEventDatabase {
|
|
12
|
-
db: TDatabase;
|
|
13
|
-
/** If search is enabled */
|
|
14
|
-
private search;
|
|
15
|
-
/** The search content formatter */
|
|
16
|
-
private searchContentFormatter;
|
|
17
|
-
constructor(database?: string | TDatabase, options?: SqliteEventDatabaseOptions);
|
|
18
|
-
/** Store a Nostr event in the database */
|
|
19
|
-
add(event: NostrEvent): NostrEvent;
|
|
20
|
-
/** Delete an event by ID */
|
|
21
|
-
remove(id: string): boolean;
|
|
22
|
-
/** Checks if an event exists */
|
|
23
|
-
hasEvent(id: string): boolean;
|
|
24
|
-
/** Get an event by its ID */
|
|
25
|
-
getEvent(id: string): NostrEvent | undefined;
|
|
26
|
-
/** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
|
|
27
|
-
getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
|
|
28
|
-
/** Checks if a replaceable event exists */
|
|
29
|
-
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
|
|
30
|
-
/** Returns all the versions of a replaceable event */
|
|
31
|
-
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): NostrEvent[];
|
|
32
|
-
/** Get all events that match the filters (supports NIP-50 search field) */
|
|
33
|
-
getByFilters(filters: (Filter & {
|
|
34
|
-
search?: string;
|
|
35
|
-
}) | (Filter & {
|
|
36
|
-
search?: string;
|
|
37
|
-
})[]): Set<NostrEvent>;
|
|
38
|
-
/** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
|
|
39
|
-
getTimeline(filters: (Filter & {
|
|
40
|
-
search?: string;
|
|
41
|
-
}) | (Filter & {
|
|
42
|
-
search?: string;
|
|
43
|
-
})[]): NostrEvent[];
|
|
44
|
-
/** Set the search content formatter */
|
|
45
|
-
setSearchContentFormatter(formatter: SearchContentFormatter): void;
|
|
46
|
-
/** Get the current search content formatter */
|
|
47
|
-
getSearchContentFormatter(): SearchContentFormatter;
|
|
48
|
-
/** Rebuild the search index for all events */
|
|
49
|
-
rebuildSearchIndex(): void;
|
|
50
|
-
/** Close the database connection */
|
|
51
|
-
close(): void;
|
|
52
|
-
[Symbol.dispose](): void;
|
|
53
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
import { logger } from "applesauce-core";
|
|
2
|
-
import { insertEventIntoDescendingList } from "applesauce-core/helpers";
|
|
3
|
-
import Database from "better-sqlite3";
|
|
4
|
-
import { createTables, defaultSearchContentFormatter, deleteEvent, enhancedSearchContentFormatter, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./helpers/sqlite.js";
|
|
5
|
-
const log = logger.extend("SqliteEventDatabase");
|
|
6
|
-
// Export the search content formatters and types for external use
|
|
7
|
-
export { defaultSearchContentFormatter, enhancedSearchContentFormatter };
|
|
8
|
-
export class SqliteEventDatabase {
|
|
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 ?? true;
|
|
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
|
-
}
|