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.
- package/LICENSE +21 -0
- package/README.md +115 -0
- package/dist/better-sqlite3/event-database.d.ts +52 -0
- package/dist/better-sqlite3/event-database.js +104 -0
- package/dist/better-sqlite3/index.d.ts +2 -0
- package/dist/better-sqlite3/index.js +2 -0
- package/dist/better-sqlite3/methods.d.ts +31 -0
- package/dist/better-sqlite3/methods.js +149 -0
- package/dist/bun/event-database.d.ts +52 -0
- package/dist/bun/event-database.js +105 -0
- package/dist/bun/index.d.ts +2 -0
- package/dist/bun/index.js +2 -0
- package/dist/bun/methods.d.ts +31 -0
- package/dist/bun/methods.js +152 -0
- package/dist/helpers/index.d.ts +3 -0
- package/dist/helpers/index.js +3 -0
- package/dist/helpers/search.d.ts +16 -0
- package/dist/helpers/search.js +60 -0
- package/dist/helpers/sql.d.ts +15 -0
- package/dist/helpers/sql.js +122 -0
- package/dist/helpers/sqlite.d.ts +66 -0
- package/dist/helpers/sqlite.js +367 -0
- package/dist/helpers/statements.d.ts +47 -0
- package/dist/helpers/statements.js +65 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/libsql/event-database.d.ts +54 -0
- package/dist/libsql/event-database.js +106 -0
- package/dist/libsql/index.d.ts +2 -0
- package/dist/libsql/index.js +2 -0
- package/dist/libsql/methods.d.ts +31 -0
- package/dist/libsql/methods.js +249 -0
- package/dist/native/event-database.d.ts +52 -0
- package/dist/native/event-database.js +104 -0
- package/dist/native/index.d.ts +2 -0
- package/dist/native/index.js +2 -0
- package/dist/native/methods.d.ts +31 -0
- package/dist/native/methods.js +174 -0
- package/dist/relay.d.ts +1 -0
- package/dist/relay.js +166 -0
- package/dist/sqlite-event-database.d.ts +53 -0
- package/dist/sqlite-event-database.js +105 -0
- package/package.json +113 -0
|
@@ -0,0 +1,367 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic SQL Statement type that defines a statement with its expected parameters
|
|
3
|
+
* This allows database implementations to import these and infer the parameter types
|
|
4
|
+
*/
|
|
5
|
+
export type Statement<TParams extends readonly unknown[] = any[], TResult = any> = {
|
|
6
|
+
/** The SQL query string */
|
|
7
|
+
sql: string;
|
|
8
|
+
/** Type information for parameters (not used at runtime, just for type inference) */
|
|
9
|
+
_params?: TParams;
|
|
10
|
+
/** Type information for result (not used at runtime, just for type inference) */
|
|
11
|
+
_result?: TResult;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Helper type to extract parameter types from a Statement
|
|
15
|
+
*/
|
|
16
|
+
export type StatementParams<T> = T extends Statement<infer P, any> ? P : never;
|
|
17
|
+
/**
|
|
18
|
+
* Helper type to extract result type from a Statement
|
|
19
|
+
*/
|
|
20
|
+
export type StatementResult<T> = T extends Statement<any, infer R> ? R : never;
|
|
21
|
+
/** Types for the `events` table */
|
|
22
|
+
export type EventRow = {
|
|
23
|
+
id: string;
|
|
24
|
+
kind: number;
|
|
25
|
+
pubkey: string;
|
|
26
|
+
created_at: number;
|
|
27
|
+
content: string;
|
|
28
|
+
tags: string;
|
|
29
|
+
sig: string;
|
|
30
|
+
};
|
|
31
|
+
export declare const INSERT_EVENT_STATEMENT: Statement<[string, number, string, number, string, string, string, string]>;
|
|
32
|
+
export declare const DELETE_EVENT_TAGS_STATEMENT: Statement<[string]>;
|
|
33
|
+
export declare const INSERT_EVENT_TAG_STATEMENT: Statement<[string, string, string]>;
|
|
34
|
+
export declare const DELETE_EVENT_STATEMENT: Statement<[string]>;
|
|
35
|
+
export declare const HAS_EVENT_STATEMENT: Statement<[string], {
|
|
36
|
+
count: number;
|
|
37
|
+
}>;
|
|
38
|
+
export declare const GET_EVENT_STATEMENT: Statement<[string], EventRow>;
|
|
39
|
+
export declare const GET_REPLACEABLE_STATEMENT: Statement<[number, string, string], EventRow>;
|
|
40
|
+
export declare const GET_REPLACEABLE_HISTORY_STATEMENT: Statement<[number, string, string], EventRow>;
|
|
41
|
+
export declare const HAS_REPLACEABLE_STATEMENT: Statement<[number, string, string], {
|
|
42
|
+
count: number;
|
|
43
|
+
}>;
|
|
44
|
+
export declare const GET_ALL_EVENTS_STATEMENT: Statement<[], EventRow>;
|
|
45
|
+
export declare const CREATE_EVENTS_TABLE_STATEMENT: Statement<[]>;
|
|
46
|
+
export declare const CREATE_EVENT_TAGS_TABLE_STATEMENT: Statement<[]>;
|
|
47
|
+
export declare const CREATE_INDEXES_STATEMENTS: Statement<[]>[];
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Event-related statements
|
|
2
|
+
export const INSERT_EVENT_STATEMENT = {
|
|
3
|
+
sql: `INSERT OR REPLACE INTO events (id, kind, pubkey, created_at, content, tags, sig, identifier)
|
|
4
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5
|
+
};
|
|
6
|
+
export const DELETE_EVENT_TAGS_STATEMENT = {
|
|
7
|
+
sql: `DELETE FROM event_tags WHERE event_id = ?`,
|
|
8
|
+
};
|
|
9
|
+
export const INSERT_EVENT_TAG_STATEMENT = {
|
|
10
|
+
sql: `INSERT OR IGNORE INTO event_tags (event_id, tag_name, tag_value) VALUES (?, ?, ?)`,
|
|
11
|
+
};
|
|
12
|
+
export const DELETE_EVENT_STATEMENT = {
|
|
13
|
+
sql: `DELETE FROM events WHERE id = ?`,
|
|
14
|
+
};
|
|
15
|
+
export const HAS_EVENT_STATEMENT = {
|
|
16
|
+
sql: `SELECT COUNT(*) as count FROM events WHERE id = ?`,
|
|
17
|
+
};
|
|
18
|
+
export const GET_EVENT_STATEMENT = {
|
|
19
|
+
sql: `SELECT * FROM events WHERE id = ?`,
|
|
20
|
+
};
|
|
21
|
+
export const GET_REPLACEABLE_STATEMENT = {
|
|
22
|
+
sql: `SELECT * FROM events WHERE kind = ? AND pubkey = ? AND identifier = ? ORDER BY created_at DESC LIMIT 1`,
|
|
23
|
+
};
|
|
24
|
+
export const GET_REPLACEABLE_HISTORY_STATEMENT = {
|
|
25
|
+
sql: `SELECT * FROM events WHERE kind = ? AND pubkey = ? AND identifier = ? ORDER BY created_at DESC`,
|
|
26
|
+
};
|
|
27
|
+
export const HAS_REPLACEABLE_STATEMENT = {
|
|
28
|
+
sql: `SELECT COUNT(*) as count FROM events WHERE kind = ? AND pubkey = ? AND identifier = ?`,
|
|
29
|
+
};
|
|
30
|
+
export const GET_ALL_EVENTS_STATEMENT = {
|
|
31
|
+
sql: `SELECT * FROM events`,
|
|
32
|
+
};
|
|
33
|
+
// SQL schema setup statements
|
|
34
|
+
export const CREATE_EVENTS_TABLE_STATEMENT = {
|
|
35
|
+
sql: `CREATE TABLE IF NOT EXISTS events (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
kind INTEGER NOT NULL,
|
|
38
|
+
pubkey TEXT NOT NULL,
|
|
39
|
+
created_at INTEGER NOT NULL,
|
|
40
|
+
content TEXT NOT NULL,
|
|
41
|
+
tags TEXT,
|
|
42
|
+
sig TEXT NOT NULL,
|
|
43
|
+
identifier TEXT NOT NULL DEFAULT ''
|
|
44
|
+
)`,
|
|
45
|
+
};
|
|
46
|
+
export const CREATE_EVENT_TAGS_TABLE_STATEMENT = {
|
|
47
|
+
sql: `CREATE TABLE IF NOT EXISTS event_tags (
|
|
48
|
+
event_id TEXT NOT NULL,
|
|
49
|
+
tag_name TEXT NOT NULL,
|
|
50
|
+
tag_value TEXT NOT NULL,
|
|
51
|
+
FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE,
|
|
52
|
+
PRIMARY KEY (event_id, tag_name, tag_value)
|
|
53
|
+
)`,
|
|
54
|
+
};
|
|
55
|
+
// Index creation statements
|
|
56
|
+
export const CREATE_INDEXES_STATEMENTS = [
|
|
57
|
+
// Events table indexes
|
|
58
|
+
{ sql: `CREATE INDEX IF NOT EXISTS kind_idx ON events(kind)` },
|
|
59
|
+
{ sql: `CREATE INDEX IF NOT EXISTS pubkey_idx ON events(pubkey)` },
|
|
60
|
+
{ sql: `CREATE INDEX IF NOT EXISTS created_at_idx ON events(created_at)` },
|
|
61
|
+
{ sql: `CREATE INDEX IF NOT EXISTS identifier_idx ON events(identifier)` },
|
|
62
|
+
// Event tags table indexes for efficient tag filtering
|
|
63
|
+
{ sql: `CREATE INDEX IF NOT EXISTS event_tags_event_id_idx ON event_tags(event_id)` },
|
|
64
|
+
{ sql: `CREATE INDEX IF NOT EXISTS event_tags_name_value_idx ON event_tags(tag_name, tag_value)` },
|
|
65
|
+
];
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./helpers/index.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./helpers/index.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { IAsyncEventDatabase } from "applesauce-core";
|
|
2
|
+
import { Filter, NostrEvent } from "applesauce-core/helpers";
|
|
3
|
+
import { Client } from "@libsql/client";
|
|
4
|
+
import { SearchContentFormatter } from "../helpers/search.js";
|
|
5
|
+
/** Options for the {@link LibsqlEventDatabase} */
|
|
6
|
+
export type LibsqlEventDatabaseOptions = {
|
|
7
|
+
search?: boolean;
|
|
8
|
+
searchContentFormatter?: SearchContentFormatter;
|
|
9
|
+
};
|
|
10
|
+
export declare class LibsqlEventDatabase implements IAsyncEventDatabase {
|
|
11
|
+
db: Client;
|
|
12
|
+
/** If search is enabled */
|
|
13
|
+
private search;
|
|
14
|
+
/** The search content formatter */
|
|
15
|
+
private searchContentFormatter;
|
|
16
|
+
constructor(database: string | Client, options?: LibsqlEventDatabaseOptions);
|
|
17
|
+
/** Initialize the database by creating tables and indexes */
|
|
18
|
+
initialize(): Promise<void>;
|
|
19
|
+
/** Store a Nostr event in the database */
|
|
20
|
+
add(event: NostrEvent): Promise<NostrEvent>;
|
|
21
|
+
/** Delete an event by ID */
|
|
22
|
+
remove(id: string): Promise<boolean>;
|
|
23
|
+
/** Checks if an event exists */
|
|
24
|
+
hasEvent(id: string): Promise<boolean>;
|
|
25
|
+
/** Get an event by its ID */
|
|
26
|
+
getEvent(id: string): Promise<NostrEvent | undefined>;
|
|
27
|
+
/** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
|
|
28
|
+
getReplaceable(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent | undefined>;
|
|
29
|
+
/** Checks if a replaceable event exists */
|
|
30
|
+
hasReplaceable(kind: number, pubkey: string, identifier?: string): Promise<boolean>;
|
|
31
|
+
/** Returns all the versions of a replaceable event */
|
|
32
|
+
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent[] | undefined>;
|
|
33
|
+
/** Get all events that match the filters (supports NIP-50 search field) */
|
|
34
|
+
getByFilters(filters: (Filter & {
|
|
35
|
+
search?: string;
|
|
36
|
+
}) | (Filter & {
|
|
37
|
+
search?: string;
|
|
38
|
+
})[]): Promise<Set<NostrEvent>>;
|
|
39
|
+
/** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
|
|
40
|
+
getTimeline(filters: (Filter & {
|
|
41
|
+
search?: string;
|
|
42
|
+
}) | (Filter & {
|
|
43
|
+
search?: string;
|
|
44
|
+
})[]): Promise<NostrEvent[]>;
|
|
45
|
+
/** Set the search content formatter */
|
|
46
|
+
setSearchContentFormatter(formatter: SearchContentFormatter): void;
|
|
47
|
+
/** Get the current search content formatter */
|
|
48
|
+
getSearchContentFormatter(): SearchContentFormatter;
|
|
49
|
+
/** Rebuild the search index for all events */
|
|
50
|
+
rebuildSearchIndex(): Promise<void>;
|
|
51
|
+
/** Close the database connection */
|
|
52
|
+
close(): void;
|
|
53
|
+
[Symbol.dispose](): void;
|
|
54
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { logger } from "applesauce-core";
|
|
2
|
+
import { insertEventIntoDescendingList } from "applesauce-core/helpers";
|
|
3
|
+
import { createClient } from "@libsql/client";
|
|
4
|
+
import { createTables, deleteEvent, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./methods.js";
|
|
5
|
+
import { enhancedSearchContentFormatter } from "../helpers/search.js";
|
|
6
|
+
const log = logger.extend("LibsqlEventDatabase");
|
|
7
|
+
export class LibsqlEventDatabase {
|
|
8
|
+
db;
|
|
9
|
+
/** If search is enabled */
|
|
10
|
+
search;
|
|
11
|
+
/** The search content formatter */
|
|
12
|
+
searchContentFormatter;
|
|
13
|
+
constructor(database, options) {
|
|
14
|
+
this.db = typeof database === "string" ? createClient({ url: database }) : database;
|
|
15
|
+
this.search = options?.search ?? false;
|
|
16
|
+
this.searchContentFormatter = options?.searchContentFormatter ?? enhancedSearchContentFormatter;
|
|
17
|
+
}
|
|
18
|
+
/** Initialize the database by creating tables and indexes */
|
|
19
|
+
async initialize() {
|
|
20
|
+
await createTables(this.db, this.search);
|
|
21
|
+
}
|
|
22
|
+
/** Store a Nostr event in the database */
|
|
23
|
+
async add(event) {
|
|
24
|
+
try {
|
|
25
|
+
await insertEvent(this.db, event, this.search ? this.searchContentFormatter : undefined);
|
|
26
|
+
return event;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
log("Error inserting event:", error);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Delete an event by ID */
|
|
34
|
+
async remove(id) {
|
|
35
|
+
try {
|
|
36
|
+
// Remove event from database
|
|
37
|
+
return await deleteEvent(this.db, id);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/** Checks if an event exists */
|
|
44
|
+
async hasEvent(id) {
|
|
45
|
+
return await hasEvent(this.db, id);
|
|
46
|
+
}
|
|
47
|
+
/** Get an event by its ID */
|
|
48
|
+
async getEvent(id) {
|
|
49
|
+
return await getEvent(this.db, id);
|
|
50
|
+
}
|
|
51
|
+
/** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
|
|
52
|
+
async getReplaceable(kind, pubkey, identifier = "") {
|
|
53
|
+
return await getReplaceable(this.db, kind, pubkey, identifier);
|
|
54
|
+
}
|
|
55
|
+
/** Checks if a replaceable event exists */
|
|
56
|
+
async hasReplaceable(kind, pubkey, identifier = "") {
|
|
57
|
+
return await hasReplaceable(this.db, kind, pubkey, identifier);
|
|
58
|
+
}
|
|
59
|
+
/** Returns all the versions of a replaceable event */
|
|
60
|
+
async getReplaceableHistory(kind, pubkey, identifier = "") {
|
|
61
|
+
return await getReplaceableHistory(this.db, kind, pubkey, identifier);
|
|
62
|
+
}
|
|
63
|
+
/** Get all events that match the filters (supports NIP-50 search field) */
|
|
64
|
+
async getByFilters(filters) {
|
|
65
|
+
try {
|
|
66
|
+
// If search is disabled, remove the search field from the filters
|
|
67
|
+
if (!this.search && (Array.isArray(filters) ? filters.some((f) => "search" in f) : "search" in filters))
|
|
68
|
+
throw new Error("Search is disabled");
|
|
69
|
+
return await getEventsByFilters(this.db, filters);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
return new Set();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
|
|
76
|
+
async getTimeline(filters) {
|
|
77
|
+
const events = await this.getByFilters(filters);
|
|
78
|
+
const timeline = [];
|
|
79
|
+
for (const event of events)
|
|
80
|
+
insertEventIntoDescendingList(timeline, event);
|
|
81
|
+
return timeline;
|
|
82
|
+
}
|
|
83
|
+
/** Set the search content formatter */
|
|
84
|
+
setSearchContentFormatter(formatter) {
|
|
85
|
+
this.searchContentFormatter = formatter;
|
|
86
|
+
}
|
|
87
|
+
/** Get the current search content formatter */
|
|
88
|
+
getSearchContentFormatter() {
|
|
89
|
+
return this.searchContentFormatter;
|
|
90
|
+
}
|
|
91
|
+
/** Rebuild the search index for all events */
|
|
92
|
+
async rebuildSearchIndex() {
|
|
93
|
+
if (!this.search)
|
|
94
|
+
throw new Error("Search is disabled");
|
|
95
|
+
await rebuildSearchIndex(this.db, this.searchContentFormatter);
|
|
96
|
+
log("Search index rebuilt successfully");
|
|
97
|
+
}
|
|
98
|
+
/** Close the database connection */
|
|
99
|
+
close() {
|
|
100
|
+
log("Closing database connection");
|
|
101
|
+
this.db.close();
|
|
102
|
+
}
|
|
103
|
+
[Symbol.dispose]() {
|
|
104
|
+
this.close();
|
|
105
|
+
}
|
|
106
|
+
}
|