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,31 @@
|
|
|
1
|
+
import { Filter, NostrEvent } from "applesauce-core/helpers";
|
|
2
|
+
import { Database } from "bun: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: Database, search?: boolean): void;
|
|
6
|
+
/** Inserts search content for an event */
|
|
7
|
+
export declare function insertSearchContent(db: Database, event: NostrEvent, contentFormatter: SearchContentFormatter): void;
|
|
8
|
+
/** Removes search content for an event */
|
|
9
|
+
export declare function deleteSearchContent(db: Database, eventId: string): void;
|
|
10
|
+
/** Inserts an event into the `events`, `event_tags`, and search tables of a database */
|
|
11
|
+
export declare function insertEvent(db: Database, event: NostrEvent, contentFormatter?: SearchContentFormatter): boolean;
|
|
12
|
+
/** Insert indexable tags for an event into the event_tags table */
|
|
13
|
+
export declare function insertEventTags(db: Database, event: NostrEvent): void;
|
|
14
|
+
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
15
|
+
export declare function deleteEvent(db: Database, id: string): boolean;
|
|
16
|
+
/** Checks if an event exists */
|
|
17
|
+
export declare function hasEvent(db: Database, id: string): boolean;
|
|
18
|
+
/** Gets a single event from a database */
|
|
19
|
+
export declare function getEvent(db: Database, id: string): NostrEvent | undefined;
|
|
20
|
+
/** Gets the latest replaceable event from a database */
|
|
21
|
+
export declare function getReplaceable(db: Database, kind: number, pubkey: string, identifier: string): NostrEvent | undefined;
|
|
22
|
+
/** Gets the history of a replaceable event from a database */
|
|
23
|
+
export declare function getReplaceableHistory(db: Database, kind: number, pubkey: string, identifier: string): NostrEvent[];
|
|
24
|
+
/** Checks if a replaceable event exists in a database */
|
|
25
|
+
export declare function hasReplaceable(db: Database, kind: number, pubkey: string, identifier?: string): boolean;
|
|
26
|
+
/** Get all events that match the filters (includes NIP-50 search support) */
|
|
27
|
+
export declare function getEventsByFilters(db: Database, filters: FilterWithSearch | FilterWithSearch[]): Set<NostrEvent>;
|
|
28
|
+
/** Search events using FTS5 full-text search (convenience wrapper around getEventsByFilters) */
|
|
29
|
+
export declare function searchEvents(db: Database, search: string, options?: Filter): NostrEvent[];
|
|
30
|
+
/** Rebuild the FTS5 search index for all events */
|
|
31
|
+
export declare function rebuildSearchIndex(db: Database, contentFormatter: SearchContentFormatter): void;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { getIndexableTags, getReplaceableIdentifier } from "applesauce-core/helpers";
|
|
2
|
+
import { CREATE_SEARCH_TABLE_STATEMENT, DELETE_SEARCH_CONTENT_STATEMENT, INSERT_SEARCH_CONTENT_STATEMENT, } from "../helpers/search.js";
|
|
3
|
+
import { buildFiltersQuery, rowToEvent } from "../helpers/sql.js";
|
|
4
|
+
import { CREATE_EVENT_TAGS_TABLE_STATEMENT, CREATE_EVENTS_TABLE_STATEMENT, CREATE_INDEXES_STATEMENTS, DELETE_EVENT_STATEMENT, DELETE_EVENT_TAGS_STATEMENT, GET_ALL_EVENTS_STATEMENT, GET_EVENT_STATEMENT, GET_REPLACEABLE_HISTORY_STATEMENT, GET_REPLACEABLE_STATEMENT, HAS_EVENT_STATEMENT, HAS_REPLACEABLE_STATEMENT, INSERT_EVENT_STATEMENT, INSERT_EVENT_TAG_STATEMENT, } from "../helpers/statements.js";
|
|
5
|
+
/** Create and migrate the `events`, `event_tags`, and search tables */
|
|
6
|
+
export function createTables(db, search = true) {
|
|
7
|
+
// Create the events table
|
|
8
|
+
db.exec(CREATE_EVENTS_TABLE_STATEMENT.sql);
|
|
9
|
+
// Create the event_tags table
|
|
10
|
+
db.exec(CREATE_EVENT_TAGS_TABLE_STATEMENT.sql);
|
|
11
|
+
// Create the FTS5 search table
|
|
12
|
+
if (search) {
|
|
13
|
+
db.exec(CREATE_SEARCH_TABLE_STATEMENT.sql);
|
|
14
|
+
}
|
|
15
|
+
// Create indexes
|
|
16
|
+
CREATE_INDEXES_STATEMENTS.forEach((indexStatement) => {
|
|
17
|
+
db.exec(indexStatement.sql);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
/** Inserts search content for an event */
|
|
21
|
+
export function insertSearchContent(db, event, contentFormatter) {
|
|
22
|
+
const searchableContent = contentFormatter(event);
|
|
23
|
+
// Insert/update directly into the FTS5 table
|
|
24
|
+
const stmt = db.query(INSERT_SEARCH_CONTENT_STATEMENT.sql);
|
|
25
|
+
stmt.run(event.id, searchableContent, event.kind, event.pubkey, event.created_at);
|
|
26
|
+
}
|
|
27
|
+
/** Removes search content for an event */
|
|
28
|
+
export function deleteSearchContent(db, eventId) {
|
|
29
|
+
const stmt = db.query(DELETE_SEARCH_CONTENT_STATEMENT.sql);
|
|
30
|
+
stmt.run(eventId);
|
|
31
|
+
}
|
|
32
|
+
/** Inserts an event into the `events`, `event_tags`, and search tables of a database */
|
|
33
|
+
export function insertEvent(db, event, contentFormatter) {
|
|
34
|
+
const identifier = getReplaceableIdentifier(event);
|
|
35
|
+
const transaction = db.transaction(() => {
|
|
36
|
+
// Insert/update the main event
|
|
37
|
+
const stmt = db.query(INSERT_EVENT_STATEMENT.sql);
|
|
38
|
+
const result = stmt.run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
|
|
39
|
+
// Insert indexable tags into the event_tags table
|
|
40
|
+
insertEventTags(db, event);
|
|
41
|
+
// Insert searchable content into the search tables
|
|
42
|
+
if (contentFormatter)
|
|
43
|
+
insertSearchContent(db, event, contentFormatter);
|
|
44
|
+
return result.changes > 0;
|
|
45
|
+
});
|
|
46
|
+
return transaction();
|
|
47
|
+
}
|
|
48
|
+
/** Insert indexable tags for an event into the event_tags table */
|
|
49
|
+
export function insertEventTags(db, event) {
|
|
50
|
+
// Clear existing tags for this event first
|
|
51
|
+
const deleteStmt = db.query(DELETE_EVENT_TAGS_STATEMENT.sql);
|
|
52
|
+
deleteStmt.run(event.id);
|
|
53
|
+
// Get only the indexable tags using applesauce-core helper
|
|
54
|
+
const indexableTags = getIndexableTags(event);
|
|
55
|
+
if (indexableTags && indexableTags.size > 0) {
|
|
56
|
+
const insertStmt = db.query(INSERT_EVENT_TAG_STATEMENT.sql);
|
|
57
|
+
for (const tagString of indexableTags) {
|
|
58
|
+
// Parse the "tagName:tagValue" format
|
|
59
|
+
const [name, value] = tagString.split(":");
|
|
60
|
+
if (name && value)
|
|
61
|
+
insertStmt.run(event.id, name, value);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
66
|
+
export function deleteEvent(db, id) {
|
|
67
|
+
const transaction = db.transaction(() => {
|
|
68
|
+
// Delete from event_tags first (foreign key constraint)
|
|
69
|
+
const deleteTagsStmt = db.query(DELETE_EVENT_TAGS_STATEMENT.sql);
|
|
70
|
+
deleteTagsStmt.run(id);
|
|
71
|
+
// Delete from search tables
|
|
72
|
+
deleteSearchContent(db, id);
|
|
73
|
+
// Delete from events table
|
|
74
|
+
const deleteEventStmt = db.query(DELETE_EVENT_STATEMENT.sql);
|
|
75
|
+
const result = deleteEventStmt.run(id);
|
|
76
|
+
return result.changes > 0;
|
|
77
|
+
});
|
|
78
|
+
return transaction();
|
|
79
|
+
}
|
|
80
|
+
/** Checks if an event exists */
|
|
81
|
+
export function hasEvent(db, id) {
|
|
82
|
+
const stmt = db.query(HAS_EVENT_STATEMENT.sql);
|
|
83
|
+
const result = stmt.get(id);
|
|
84
|
+
if (!result)
|
|
85
|
+
return false;
|
|
86
|
+
return result.count > 0;
|
|
87
|
+
}
|
|
88
|
+
/** Gets a single event from a database */
|
|
89
|
+
export function getEvent(db, id) {
|
|
90
|
+
const stmt = db.query(GET_EVENT_STATEMENT.sql);
|
|
91
|
+
const row = stmt.get(id);
|
|
92
|
+
return row ? rowToEvent(row) : undefined;
|
|
93
|
+
}
|
|
94
|
+
/** Gets the latest replaceable event from a database */
|
|
95
|
+
export function getReplaceable(db, kind, pubkey, identifier) {
|
|
96
|
+
const stmt = db.query(GET_REPLACEABLE_STATEMENT.sql);
|
|
97
|
+
const row = stmt.get(kind, pubkey, identifier);
|
|
98
|
+
return row ? rowToEvent(row) : undefined;
|
|
99
|
+
}
|
|
100
|
+
/** Gets the history of a replaceable event from a database */
|
|
101
|
+
export function getReplaceableHistory(db, kind, pubkey, identifier) {
|
|
102
|
+
const stmt = db.query(GET_REPLACEABLE_HISTORY_STATEMENT.sql);
|
|
103
|
+
return stmt.all(kind, pubkey, identifier).map(rowToEvent);
|
|
104
|
+
}
|
|
105
|
+
/** Checks if a replaceable event exists in a database */
|
|
106
|
+
export function hasReplaceable(db, kind, pubkey, identifier = "") {
|
|
107
|
+
const stmt = db.query(HAS_REPLACEABLE_STATEMENT.sql);
|
|
108
|
+
const result = stmt.get(kind, pubkey, identifier);
|
|
109
|
+
if (!result)
|
|
110
|
+
return false;
|
|
111
|
+
return result.count > 0;
|
|
112
|
+
}
|
|
113
|
+
/** Get all events that match the filters (includes NIP-50 search support) */
|
|
114
|
+
export function getEventsByFilters(db, filters) {
|
|
115
|
+
const query = buildFiltersQuery(filters);
|
|
116
|
+
if (!query)
|
|
117
|
+
return new Set();
|
|
118
|
+
const eventSet = new Set();
|
|
119
|
+
const stmt = db.query(query.sql);
|
|
120
|
+
const rows = stmt.all(...query.params);
|
|
121
|
+
// Convert rows to events and add to set
|
|
122
|
+
for (const row of rows)
|
|
123
|
+
eventSet.add(rowToEvent(row));
|
|
124
|
+
return eventSet;
|
|
125
|
+
}
|
|
126
|
+
/** Search events using FTS5 full-text search (convenience wrapper around getEventsByFilters) */
|
|
127
|
+
export function searchEvents(db, search, options) {
|
|
128
|
+
if (!search.trim())
|
|
129
|
+
return [];
|
|
130
|
+
// Build filter with search and other options
|
|
131
|
+
const filter = {
|
|
132
|
+
search: search.trim(),
|
|
133
|
+
...options,
|
|
134
|
+
};
|
|
135
|
+
// Use the main filter system which now supports search
|
|
136
|
+
const results = getEventsByFilters(db, filter);
|
|
137
|
+
return Array.from(results);
|
|
138
|
+
}
|
|
139
|
+
/** Rebuild the FTS5 search index for all events */
|
|
140
|
+
export function rebuildSearchIndex(db, contentFormatter) {
|
|
141
|
+
const transaction = db.transaction(() => {
|
|
142
|
+
// Clear existing search data
|
|
143
|
+
db.exec(`DELETE FROM events_search;`);
|
|
144
|
+
// Rebuild from all events
|
|
145
|
+
const stmt = db.query(GET_ALL_EVENTS_STATEMENT.sql);
|
|
146
|
+
const events = stmt.all().map(rowToEvent);
|
|
147
|
+
for (const event of events) {
|
|
148
|
+
insertSearchContent(db, event, contentFormatter);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
transaction();
|
|
152
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Filter, NostrEvent } from "applesauce-core/helpers";
|
|
2
|
+
import type { Statement } from "./statements.js";
|
|
3
|
+
export declare const CREATE_SEARCH_TABLE_STATEMENT: Statement<[]>;
|
|
4
|
+
export declare const INSERT_SEARCH_CONTENT_STATEMENT: Statement<[string, string, number, string, number]>;
|
|
5
|
+
export declare const DELETE_SEARCH_CONTENT_STATEMENT: Statement<[string]>;
|
|
6
|
+
/** Filter with search field */
|
|
7
|
+
export type FilterWithSearch = Filter & {
|
|
8
|
+
search?: string;
|
|
9
|
+
order?: "created_at" | "rank";
|
|
10
|
+
};
|
|
11
|
+
/** Content formatter function type for search indexing */
|
|
12
|
+
export type SearchContentFormatter = (event: NostrEvent) => string;
|
|
13
|
+
/** Default search content formatter - returns the raw content */
|
|
14
|
+
export declare const defaultSearchContentFormatter: SearchContentFormatter;
|
|
15
|
+
/** Enhanced search content formatter that includes tags and special handling for kind 0 events */
|
|
16
|
+
export declare const enhancedSearchContentFormatter: SearchContentFormatter;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// SQL schema for FTS5 search table - stores formatted searchable content directly
|
|
2
|
+
export const CREATE_SEARCH_TABLE_STATEMENT = {
|
|
3
|
+
sql: `CREATE VIRTUAL TABLE IF NOT EXISTS events_search USING fts5(
|
|
4
|
+
event_id UNINDEXED,
|
|
5
|
+
content,
|
|
6
|
+
kind UNINDEXED,
|
|
7
|
+
pubkey UNINDEXED,
|
|
8
|
+
created_at UNINDEXED
|
|
9
|
+
)`,
|
|
10
|
+
};
|
|
11
|
+
export const INSERT_SEARCH_CONTENT_STATEMENT = {
|
|
12
|
+
sql: `INSERT OR REPLACE INTO events_search (event_id, content, kind, pubkey, created_at)
|
|
13
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
14
|
+
};
|
|
15
|
+
export const DELETE_SEARCH_CONTENT_STATEMENT = {
|
|
16
|
+
sql: `DELETE FROM events_search WHERE event_id = ?`,
|
|
17
|
+
};
|
|
18
|
+
/** Default search content formatter - returns the raw content */
|
|
19
|
+
export const defaultSearchContentFormatter = (event) => {
|
|
20
|
+
return event.content;
|
|
21
|
+
};
|
|
22
|
+
/** Enhanced search content formatter that includes tags and special handling for kind 0 events */
|
|
23
|
+
export const enhancedSearchContentFormatter = (event) => {
|
|
24
|
+
let searchableContent = event.content;
|
|
25
|
+
// Special handling for kind 0 (profile metadata) events
|
|
26
|
+
if (event.kind === 0) {
|
|
27
|
+
try {
|
|
28
|
+
const profile = JSON.parse(event.content);
|
|
29
|
+
const profileFields = [];
|
|
30
|
+
// Include common profile fields in search
|
|
31
|
+
if (profile.name)
|
|
32
|
+
profileFields.push(profile.name);
|
|
33
|
+
if (profile.display_name)
|
|
34
|
+
profileFields.push(profile.display_name);
|
|
35
|
+
if (profile.about)
|
|
36
|
+
profileFields.push(profile.about);
|
|
37
|
+
if (profile.nip05)
|
|
38
|
+
profileFields.push(profile.nip05);
|
|
39
|
+
if (profile.lud16)
|
|
40
|
+
profileFields.push(profile.lud16);
|
|
41
|
+
searchableContent = profileFields.join(" ");
|
|
42
|
+
}
|
|
43
|
+
catch (e) {
|
|
44
|
+
// If JSON parsing fails, use the raw content
|
|
45
|
+
searchableContent = event.content;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Include relevant tags in the searchable content
|
|
49
|
+
const relevantTags = ["t", "subject", "title", "summary", "description", "d"];
|
|
50
|
+
const tagContent = [];
|
|
51
|
+
for (const tag of event.tags) {
|
|
52
|
+
if (tag.length >= 2 && relevantTags.includes(tag[0]))
|
|
53
|
+
tagContent.push(tag[1]);
|
|
54
|
+
}
|
|
55
|
+
// Combine content with tag content
|
|
56
|
+
if (tagContent.length > 0) {
|
|
57
|
+
searchableContent += " " + tagContent.join(" ");
|
|
58
|
+
}
|
|
59
|
+
return searchableContent;
|
|
60
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NostrEvent } from "applesauce-core/helpers";
|
|
2
|
+
import { FilterWithSearch } from "./search.js";
|
|
3
|
+
import { EventRow } from "./statements.js";
|
|
4
|
+
/** Convert database row to NostrEvent */
|
|
5
|
+
export declare function rowToEvent(row: EventRow): NostrEvent;
|
|
6
|
+
/** Builds conditions for a single filter */
|
|
7
|
+
export declare function buildFilterConditions(filter: FilterWithSearch): {
|
|
8
|
+
conditions: string[];
|
|
9
|
+
params: any[];
|
|
10
|
+
search: boolean;
|
|
11
|
+
};
|
|
12
|
+
export declare function buildFiltersQuery(filters: FilterWithSearch | FilterWithSearch[]): {
|
|
13
|
+
sql: string;
|
|
14
|
+
params: any[];
|
|
15
|
+
} | null;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/** Convert database row to NostrEvent */
|
|
2
|
+
export function rowToEvent(row) {
|
|
3
|
+
return {
|
|
4
|
+
id: row.id,
|
|
5
|
+
kind: row.kind,
|
|
6
|
+
pubkey: row.pubkey,
|
|
7
|
+
created_at: row.created_at,
|
|
8
|
+
content: row.content,
|
|
9
|
+
tags: JSON.parse(row.tags || "[]"),
|
|
10
|
+
sig: row.sig,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/** Builds conditions for a single filter */
|
|
14
|
+
export function buildFilterConditions(filter) {
|
|
15
|
+
const conditions = [];
|
|
16
|
+
const params = [];
|
|
17
|
+
let search = false;
|
|
18
|
+
// Handle NIP-50 search filter
|
|
19
|
+
if (filter.search && filter.search.trim()) {
|
|
20
|
+
conditions.push(`events_search MATCH ?`);
|
|
21
|
+
params.push(filter.search.trim());
|
|
22
|
+
search = true;
|
|
23
|
+
}
|
|
24
|
+
// Handle IDs filter
|
|
25
|
+
if (filter.ids && filter.ids.length > 0) {
|
|
26
|
+
const placeholders = filter.ids.map(() => `?`).join(", ");
|
|
27
|
+
conditions.push(`events.id IN (${placeholders})`);
|
|
28
|
+
params.push(...filter.ids);
|
|
29
|
+
}
|
|
30
|
+
// Handle kinds filter
|
|
31
|
+
if (filter.kinds && filter.kinds.length > 0) {
|
|
32
|
+
const placeholders = filter.kinds.map(() => `?`).join(", ");
|
|
33
|
+
conditions.push(`events.kind IN (${placeholders})`);
|
|
34
|
+
params.push(...filter.kinds);
|
|
35
|
+
}
|
|
36
|
+
// Handle authors filter (pubkeys)
|
|
37
|
+
if (filter.authors && filter.authors.length > 0) {
|
|
38
|
+
const placeholders = filter.authors.map(() => `?`).join(", ");
|
|
39
|
+
conditions.push(`events.pubkey IN (${placeholders})`);
|
|
40
|
+
params.push(...filter.authors);
|
|
41
|
+
}
|
|
42
|
+
// Handle since filter (timestamp >= since)
|
|
43
|
+
if (filter.since !== undefined) {
|
|
44
|
+
conditions.push(`events.created_at >= ?`);
|
|
45
|
+
params.push(filter.since);
|
|
46
|
+
}
|
|
47
|
+
// Handle until filter (timestamp <= until)
|
|
48
|
+
if (filter.until !== undefined) {
|
|
49
|
+
conditions.push(`events.created_at <= ?`);
|
|
50
|
+
params.push(filter.until);
|
|
51
|
+
}
|
|
52
|
+
// Handle tag filters (e.g., #e, #p, #t, #d, etc.)
|
|
53
|
+
for (const [key, values] of Object.entries(filter)) {
|
|
54
|
+
if (key.startsWith("#") && values && Array.isArray(values) && values.length > 0) {
|
|
55
|
+
const tagName = key.slice(1); // Remove the '#' prefix
|
|
56
|
+
// Use the event_tags table for efficient tag filtering
|
|
57
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
58
|
+
conditions.push(`events.id IN (
|
|
59
|
+
SELECT DISTINCT event_id
|
|
60
|
+
FROM event_tags
|
|
61
|
+
WHERE tag_name = ? AND tag_value IN (${placeholders})
|
|
62
|
+
)`);
|
|
63
|
+
// Add parameters: tagName first, then all the tag values
|
|
64
|
+
params.push(tagName, ...values);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { conditions, params, search };
|
|
68
|
+
}
|
|
69
|
+
export function buildFiltersQuery(filters) {
|
|
70
|
+
const filterArray = Array.isArray(filters) ? filters : [filters];
|
|
71
|
+
if (filterArray.length === 0)
|
|
72
|
+
return null;
|
|
73
|
+
// Build queries for each filter (OR logic between filters)
|
|
74
|
+
const filterQueries = [];
|
|
75
|
+
const allParams = [];
|
|
76
|
+
let globalLimit;
|
|
77
|
+
// Build the final query with proper ordering and limit
|
|
78
|
+
let fromClause = "events";
|
|
79
|
+
let orderBy = "events.created_at DESC, events.id ASC";
|
|
80
|
+
for (const filter of filterArray) {
|
|
81
|
+
const { conditions, params, search } = buildFilterConditions(filter);
|
|
82
|
+
if (search) {
|
|
83
|
+
// Override the from clause to join the events_search table
|
|
84
|
+
fromClause = "events INNER JOIN events_search ON events.id = events_search.event_id";
|
|
85
|
+
// Set the order by clause based on the filter order
|
|
86
|
+
switch (filter.order) {
|
|
87
|
+
case "created_at":
|
|
88
|
+
orderBy = "events.created_at DESC, events.id ASC";
|
|
89
|
+
break;
|
|
90
|
+
case "rank":
|
|
91
|
+
orderBy = "events_search.rank, events.created_at DESC";
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (conditions.length === 0) {
|
|
96
|
+
// If no conditions, this filter matches all events
|
|
97
|
+
filterQueries.push("1=1");
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
// AND logic within a single filter
|
|
101
|
+
filterQueries.push(`(${conditions.join(" AND ")})`);
|
|
102
|
+
}
|
|
103
|
+
allParams.push(...params);
|
|
104
|
+
// Track the most restrictive limit across all filters
|
|
105
|
+
if (filter.limit !== undefined) {
|
|
106
|
+
globalLimit = globalLimit === undefined ? filter.limit : Math.min(globalLimit, filter.limit);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Combine all filter conditions with OR logic
|
|
110
|
+
const whereClause = filterQueries.length > 0 ? `WHERE ${filterQueries.join(" OR ")}` : "";
|
|
111
|
+
let query = `
|
|
112
|
+
SELECT DISTINCT events.* FROM ${fromClause}
|
|
113
|
+
${whereClause}
|
|
114
|
+
ORDER BY ${orderBy}
|
|
115
|
+
`;
|
|
116
|
+
// Apply global limit if specified
|
|
117
|
+
if (globalLimit !== undefined && globalLimit > 0) {
|
|
118
|
+
query += ` LIMIT ?`;
|
|
119
|
+
allParams.push(globalLimit);
|
|
120
|
+
}
|
|
121
|
+
return { sql: query, params: allParams };
|
|
122
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
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;
|