applesauce-sqlite 4.0.0 → 4.4.0
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/dist/better-sqlite3/event-database.d.ts +3 -1
- package/dist/better-sqlite3/event-database.js +9 -2
- package/dist/better-sqlite3/methods.d.ts +2 -2
- package/dist/better-sqlite3/methods.js +81 -50
- package/dist/bun/event-database.d.ts +3 -1
- package/dist/bun/event-database.js +9 -2
- package/dist/bun/methods.d.ts +2 -2
- package/dist/bun/methods.js +69 -48
- package/dist/helpers/search.d.ts +3 -3
- package/dist/helpers/sql.d.ts +5 -0
- package/dist/helpers/sql.js +60 -4
- package/dist/helpers/statements.d.ts +11 -0
- package/dist/helpers/statements.js +7 -2
- package/dist/libsql/event-database.d.ts +6 -2
- package/dist/libsql/event-database.js +15 -2
- package/dist/libsql/methods.d.ts +2 -2
- package/dist/libsql/methods.js +77 -36
- package/dist/native/event-database.d.ts +3 -1
- package/dist/native/event-database.js +9 -2
- package/dist/native/methods.d.ts +2 -2
- package/dist/native/methods.js +79 -52
- package/dist/turso/event-database.d.ts +50 -0
- package/dist/turso/event-database.js +114 -0
- package/dist/turso/index.d.ts +2 -0
- package/dist/turso/index.js +2 -0
- package/dist/turso/methods.d.ts +25 -0
- package/dist/turso/methods.js +145 -0
- package/dist/turso-wasm/event-database.d.ts +36 -0
- package/dist/turso-wasm/event-database.js +90 -0
- package/dist/turso-wasm/index.d.ts +2 -0
- package/dist/turso-wasm/index.js +2 -0
- package/dist/turso-wasm/methods.d.ts +22 -0
- package/dist/turso-wasm/methods.js +101 -0
- package/package.json +26 -3
|
@@ -18,11 +18,13 @@ export declare class BetterSqlite3EventDatabase implements IEventDatabase {
|
|
|
18
18
|
add(event: NostrEvent): NostrEvent;
|
|
19
19
|
/** Delete an event by ID */
|
|
20
20
|
remove(id: string): boolean;
|
|
21
|
+
/** Remove multiple events that match the given filters */
|
|
22
|
+
removeByFilters(filters: FilterWithSearch | FilterWithSearch[]): number;
|
|
21
23
|
/** Checks if an event exists */
|
|
22
24
|
hasEvent(id: string): boolean;
|
|
23
25
|
/** Get an event by its ID */
|
|
24
26
|
getEvent(id: string): NostrEvent | undefined;
|
|
25
|
-
/** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
|
|
27
|
+
/** Get the latest replaceable event For replaceable events (10000-19999 and 30000-39999), returns the most recent event */
|
|
26
28
|
getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
|
|
27
29
|
/** Checks if a replaceable event exists */
|
|
28
30
|
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logger } from "applesauce-core";
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
|
-
import { createTables, deleteEvent, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "../better-sqlite3/methods.js";
|
|
3
|
+
import { createTables, deleteEvent, deleteEventsByFilters, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "../better-sqlite3/methods.js";
|
|
4
4
|
import { enhancedSearchContentFormatter } from "../helpers/search.js";
|
|
5
5
|
const log = logger.extend("BetterSqlite3EventDatabase");
|
|
6
6
|
export class BetterSqlite3EventDatabase {
|
|
@@ -37,6 +37,13 @@ export class BetterSqlite3EventDatabase {
|
|
|
37
37
|
return false;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
/** Remove multiple events that match the given filters */
|
|
41
|
+
removeByFilters(filters) {
|
|
42
|
+
// If search is disabled, remove the search field from the filters
|
|
43
|
+
if (this.search && (Array.isArray(filters) ? filters.some((f) => "search" in f) : "search" in filters))
|
|
44
|
+
throw new Error("Cannot delete with search");
|
|
45
|
+
return deleteEventsByFilters(this.db, filters);
|
|
46
|
+
}
|
|
40
47
|
/** Checks if an event exists */
|
|
41
48
|
hasEvent(id) {
|
|
42
49
|
return hasEvent(this.db, id);
|
|
@@ -45,7 +52,7 @@ export class BetterSqlite3EventDatabase {
|
|
|
45
52
|
getEvent(id) {
|
|
46
53
|
return getEvent(this.db, id);
|
|
47
54
|
}
|
|
48
|
-
/** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
|
|
55
|
+
/** Get the latest replaceable event For replaceable events (10000-19999 and 30000-39999), returns the most recent event */
|
|
49
56
|
getReplaceable(kind, pubkey, identifier = "") {
|
|
50
57
|
return getReplaceable(this.db, kind, pubkey, identifier);
|
|
51
58
|
}
|
|
@@ -9,8 +9,6 @@ export declare function insertSearchContent(db: Database, event: NostrEvent, con
|
|
|
9
9
|
export declare function deleteSearchContent(db: Database, eventId: string): void;
|
|
10
10
|
/** Inserts an event into the `events`, `event_tags`, and search tables of a database */
|
|
11
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
12
|
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
15
13
|
export declare function deleteEvent(db: Database, id: string): boolean;
|
|
16
14
|
/** Checks if an event exists */
|
|
@@ -29,3 +27,5 @@ export declare function getEventsByFilters(db: Database, filters: FilterWithSear
|
|
|
29
27
|
export declare function searchEvents(db: Database, search: string, options?: Filter): NostrEvent[];
|
|
30
28
|
/** Rebuild the FTS5 search index for all events */
|
|
31
29
|
export declare function rebuildSearchIndex(db: Database, contentFormatter: SearchContentFormatter): void;
|
|
30
|
+
/** Removes multiple events that match the given filters from the database */
|
|
31
|
+
export declare function deleteEventsByFilters(db: Database, filters: FilterWithSearch | FilterWithSearch[]): number;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getIndexableTags, getReplaceableIdentifier } from "applesauce-core/helpers";
|
|
2
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,
|
|
3
|
+
import { buildFiltersQuery, buildDeleteFiltersQuery, rowToEvent } from "../helpers/sql.js";
|
|
4
|
+
import { CREATE_EVENT_TAGS_TABLE_STATEMENT, CREATE_EVENTS_TABLE_STATEMENT, CREATE_INDEXES_STATEMENTS, DELETE_EVENT_STATEMENT, GET_ALL_EVENTS_STATEMENT, GET_EVENT_STATEMENT, GET_REPLACEABLE_HISTORY_STATEMENT, GET_REPLACEABLE_STATEMENT, HAS_EVENT_STATEMENT, HAS_REPLACEABLE_STATEMENT, INSERT_EVENT_STATEMENT_WITH_IGNORE, INSERT_EVENT_TAG_STATEMENT, } from "../helpers/statements.js";
|
|
5
5
|
/** Create and migrate the `events`, `event_tags`, and search tables */
|
|
6
6
|
export function createTables(db, search = true) {
|
|
7
7
|
// Create the events table
|
|
@@ -33,77 +33,85 @@ export function deleteSearchContent(db, eventId) {
|
|
|
33
33
|
export function insertEvent(db, event, contentFormatter) {
|
|
34
34
|
const identifier = getReplaceableIdentifier(event);
|
|
35
35
|
return db.transaction(() => {
|
|
36
|
-
//
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
const indexableTags = getIndexableTags(event);
|
|
54
|
-
if (indexableTags && indexableTags.size > 0) {
|
|
55
|
-
const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT.sql);
|
|
56
|
-
for (const tagString of indexableTags) {
|
|
57
|
-
// Parse the "tagName:tagValue" format
|
|
58
|
-
const [name, value] = tagString.split(":");
|
|
59
|
-
if (name && value)
|
|
60
|
-
insertStmt.run(event.id, name, value);
|
|
36
|
+
// Try to insert the main event with OR IGNORE
|
|
37
|
+
const result = db
|
|
38
|
+
.prepare(INSERT_EVENT_STATEMENT_WITH_IGNORE.sql)
|
|
39
|
+
.run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
|
|
40
|
+
// If no rows were changed, the event already existed
|
|
41
|
+
if (result.changes === 0)
|
|
42
|
+
return false; // Event already exists, skip tags/search processing
|
|
43
|
+
// Event was inserted, continue with tags and search content
|
|
44
|
+
const indexableTags = getIndexableTags(event);
|
|
45
|
+
if (indexableTags && indexableTags.size > 0) {
|
|
46
|
+
const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT.sql);
|
|
47
|
+
for (const tagString of indexableTags) {
|
|
48
|
+
// Parse the "tagName:tagValue" format
|
|
49
|
+
const [name, value] = tagString.split(":");
|
|
50
|
+
if (name && value)
|
|
51
|
+
insertStmt.run(event.id, name, value);
|
|
52
|
+
}
|
|
61
53
|
}
|
|
62
|
-
|
|
54
|
+
if (contentFormatter) {
|
|
55
|
+
try {
|
|
56
|
+
insertSearchContent(db, event, contentFormatter);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
})();
|
|
63
64
|
}
|
|
64
65
|
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
65
66
|
export function deleteEvent(db, id) {
|
|
66
67
|
return db.transaction(() => {
|
|
67
|
-
// Delete from
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
68
|
+
// Delete from search tables if they exist
|
|
69
|
+
try {
|
|
70
|
+
deleteSearchContent(db, id);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
74
|
+
}
|
|
75
|
+
// Delete from events table - this will CASCADE to event_tags automatically!
|
|
76
|
+
// The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
|
77
|
+
// ensures that all related event_tags records are deleted automatically
|
|
78
|
+
const result = db.prepare(DELETE_EVENT_STATEMENT.sql).run(id);
|
|
75
79
|
return result.changes > 0;
|
|
76
80
|
})();
|
|
77
81
|
}
|
|
78
82
|
/** Checks if an event exists */
|
|
79
83
|
export function hasEvent(db, id) {
|
|
80
|
-
const
|
|
81
|
-
|
|
84
|
+
const result = db
|
|
85
|
+
.prepare(HAS_EVENT_STATEMENT.sql)
|
|
86
|
+
.get(id);
|
|
82
87
|
if (!result)
|
|
83
88
|
return false;
|
|
84
89
|
return result.count > 0;
|
|
85
90
|
}
|
|
86
91
|
/** Gets a single event from a database */
|
|
87
92
|
export function getEvent(db, id) {
|
|
88
|
-
const
|
|
89
|
-
const row = stmt.get(id);
|
|
93
|
+
const row = db.prepare(GET_EVENT_STATEMENT.sql).get(id);
|
|
90
94
|
return row && rowToEvent(row);
|
|
91
95
|
}
|
|
92
96
|
/** Gets the latest replaceable event from a database */
|
|
93
97
|
export function getReplaceable(db, kind, pubkey, identifier) {
|
|
94
|
-
const
|
|
95
|
-
|
|
98
|
+
const row = db
|
|
99
|
+
.prepare(GET_REPLACEABLE_STATEMENT.sql)
|
|
100
|
+
.get(kind, pubkey, identifier);
|
|
96
101
|
return row && rowToEvent(row);
|
|
97
102
|
}
|
|
98
103
|
/** Gets the history of a replaceable event from a database */
|
|
99
104
|
export function getReplaceableHistory(db, kind, pubkey, identifier) {
|
|
100
|
-
|
|
101
|
-
|
|
105
|
+
return db
|
|
106
|
+
.prepare(GET_REPLACEABLE_HISTORY_STATEMENT.sql)
|
|
107
|
+
.all(kind, pubkey, identifier)
|
|
108
|
+
.map(rowToEvent);
|
|
102
109
|
}
|
|
103
110
|
/** Checks if a replaceable event exists in a database */
|
|
104
111
|
export function hasReplaceable(db, kind, pubkey, identifier = "") {
|
|
105
|
-
const
|
|
106
|
-
|
|
112
|
+
const result = db
|
|
113
|
+
.prepare(HAS_REPLACEABLE_STATEMENT.sql)
|
|
114
|
+
.get(kind, pubkey, identifier);
|
|
107
115
|
if (!result)
|
|
108
116
|
return false;
|
|
109
117
|
return result.count > 0;
|
|
@@ -113,8 +121,7 @@ export function getEventsByFilters(db, filters) {
|
|
|
113
121
|
const query = buildFiltersQuery(filters);
|
|
114
122
|
if (!query)
|
|
115
123
|
return [];
|
|
116
|
-
const
|
|
117
|
-
const rows = stmt.all(...query.params);
|
|
124
|
+
const rows = db.prepare(query.sql).all(...query.params);
|
|
118
125
|
// Convert rows to events and add to set
|
|
119
126
|
return rows.map(rowToEvent);
|
|
120
127
|
}
|
|
@@ -136,10 +143,34 @@ export function rebuildSearchIndex(db, contentFormatter) {
|
|
|
136
143
|
// Clear existing search data
|
|
137
144
|
db.exec(`DELETE FROM events_search;`);
|
|
138
145
|
// Rebuild from all events
|
|
139
|
-
const
|
|
140
|
-
|
|
146
|
+
const events = db
|
|
147
|
+
.prepare(GET_ALL_EVENTS_STATEMENT.sql)
|
|
148
|
+
.all()
|
|
149
|
+
.map(rowToEvent);
|
|
141
150
|
for (const event of events) {
|
|
142
151
|
insertSearchContent(db, event, contentFormatter);
|
|
143
152
|
}
|
|
144
153
|
})();
|
|
145
154
|
}
|
|
155
|
+
/** Removes multiple events that match the given filters from the database */
|
|
156
|
+
export function deleteEventsByFilters(db, filters) {
|
|
157
|
+
const whereClause = buildDeleteFiltersQuery(filters);
|
|
158
|
+
if (!whereClause)
|
|
159
|
+
return 0;
|
|
160
|
+
return db.transaction(() => {
|
|
161
|
+
// Delete from search tables if they exist
|
|
162
|
+
try {
|
|
163
|
+
const searchDeleteQuery = `DELETE FROM search_content WHERE event_id IN (SELECT id FROM events ${whereClause.sql})`;
|
|
164
|
+
db.prepare(searchDeleteQuery).run(...whereClause.params);
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
168
|
+
}
|
|
169
|
+
// Delete from events table - this will CASCADE to event_tags automatically!
|
|
170
|
+
// The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
|
171
|
+
// ensures that all related event_tags records are deleted automatically
|
|
172
|
+
const deleteEventsQuery = `DELETE FROM events ${whereClause.sql}`;
|
|
173
|
+
const result = db.prepare(deleteEventsQuery).run(...whereClause.params);
|
|
174
|
+
return result.changes;
|
|
175
|
+
})();
|
|
176
|
+
}
|
|
@@ -18,11 +18,13 @@ export declare class BunSqliteEventDatabase implements IEventDatabase {
|
|
|
18
18
|
add(event: NostrEvent): NostrEvent;
|
|
19
19
|
/** Delete an event by ID */
|
|
20
20
|
remove(id: string): boolean;
|
|
21
|
+
/** Remove multiple events that match the given filters */
|
|
22
|
+
removeByFilters(filters: FilterWithSearch | FilterWithSearch[]): number;
|
|
21
23
|
/** Checks if an event exists */
|
|
22
24
|
hasEvent(id: string): boolean;
|
|
23
25
|
/** Get an event by its ID */
|
|
24
26
|
getEvent(id: string): NostrEvent | undefined;
|
|
25
|
-
/** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
|
|
27
|
+
/** Get the latest replaceable event For replaceable events (10000-19999 and 30000-39999), returns the most recent event */
|
|
26
28
|
getReplaceable(kind: number, pubkey: string, identifier?: string): NostrEvent | undefined;
|
|
27
29
|
/** Checks if a replaceable event exists */
|
|
28
30
|
hasReplaceable(kind: number, pubkey: string, identifier?: string): boolean;
|
|
@@ -2,7 +2,7 @@ import { logger } from "applesauce-core";
|
|
|
2
2
|
// @ts-ignore - bun:sqlite is a built-in module in Bun
|
|
3
3
|
import { Database } from "bun:sqlite";
|
|
4
4
|
import { enhancedSearchContentFormatter } from "../helpers/search.js";
|
|
5
|
-
import { createTables, deleteEvent, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./methods.js";
|
|
5
|
+
import { createTables, deleteEvent, deleteEventsByFilters, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./methods.js";
|
|
6
6
|
const log = logger.extend("BunSqliteEventDatabase");
|
|
7
7
|
export class BunSqliteEventDatabase {
|
|
8
8
|
db;
|
|
@@ -38,6 +38,13 @@ export class BunSqliteEventDatabase {
|
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/** Remove multiple events that match the given filters */
|
|
42
|
+
removeByFilters(filters) {
|
|
43
|
+
// If search is disabled, remove the search field from the filters
|
|
44
|
+
if (this.search && (Array.isArray(filters) ? filters.some((f) => "search" in f) : "search" in filters))
|
|
45
|
+
throw new Error("Cannot delete with search");
|
|
46
|
+
return deleteEventsByFilters(this.db, filters);
|
|
47
|
+
}
|
|
41
48
|
/** Checks if an event exists */
|
|
42
49
|
hasEvent(id) {
|
|
43
50
|
return hasEvent(this.db, id);
|
|
@@ -46,7 +53,7 @@ export class BunSqliteEventDatabase {
|
|
|
46
53
|
getEvent(id) {
|
|
47
54
|
return getEvent(this.db, id);
|
|
48
55
|
}
|
|
49
|
-
/** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
|
|
56
|
+
/** Get the latest replaceable event For replaceable events (10000-19999 and 30000-39999), returns the most recent event */
|
|
50
57
|
getReplaceable(kind, pubkey, identifier = "") {
|
|
51
58
|
return getReplaceable(this.db, kind, pubkey, identifier);
|
|
52
59
|
}
|
package/dist/bun/methods.d.ts
CHANGED
|
@@ -9,8 +9,6 @@ export declare function insertSearchContent(db: Database, event: NostrEvent, con
|
|
|
9
9
|
export declare function deleteSearchContent(db: Database, eventId: string): void;
|
|
10
10
|
/** Inserts an event into the `events`, `event_tags`, and search tables of a database */
|
|
11
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
12
|
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
15
13
|
export declare function deleteEvent(db: Database, id: string): boolean;
|
|
16
14
|
/** Checks if an event exists */
|
|
@@ -29,3 +27,5 @@ export declare function getEventsByFilters(db: Database, filters: FilterWithSear
|
|
|
29
27
|
export declare function searchEvents(db: Database, search: string, options?: Filter): NostrEvent[];
|
|
30
28
|
/** Rebuild the FTS5 search index for all events */
|
|
31
29
|
export declare function rebuildSearchIndex(db: Database, contentFormatter: SearchContentFormatter): void;
|
|
30
|
+
/** Removes multiple events that match the given filters from the database */
|
|
31
|
+
export declare function deleteEventsByFilters(db: Database, filters: FilterWithSearch | FilterWithSearch[]): number;
|
package/dist/bun/methods.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getIndexableTags, getReplaceableIdentifier } from "applesauce-core/helpers";
|
|
2
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,
|
|
3
|
+
import { buildFiltersQuery, buildDeleteFiltersQuery, rowToEvent } from "../helpers/sql.js";
|
|
4
|
+
import { CREATE_EVENT_TAGS_TABLE_STATEMENT, CREATE_EVENTS_TABLE_STATEMENT, CREATE_INDEXES_STATEMENTS, DELETE_EVENT_STATEMENT, GET_ALL_EVENTS_STATEMENT, GET_EVENT_STATEMENT, GET_REPLACEABLE_HISTORY_STATEMENT, GET_REPLACEABLE_STATEMENT, HAS_EVENT_STATEMENT, HAS_REPLACEABLE_STATEMENT, INSERT_EVENT_STATEMENT_WITH_IGNORE, INSERT_EVENT_TAG_STATEMENT, } from "../helpers/statements.js";
|
|
5
5
|
/** Create and migrate the `events`, `event_tags`, and search tables */
|
|
6
6
|
export function createTables(db, search = true) {
|
|
7
7
|
// Create the events table
|
|
@@ -33,46 +33,50 @@ export function deleteSearchContent(db, eventId) {
|
|
|
33
33
|
export function insertEvent(db, event, contentFormatter) {
|
|
34
34
|
const identifier = getReplaceableIdentifier(event);
|
|
35
35
|
const transaction = db.transaction(() => {
|
|
36
|
-
//
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
36
|
+
// Try to insert the main event with OR IGNORE
|
|
37
|
+
const result = db
|
|
38
|
+
.query(INSERT_EVENT_STATEMENT_WITH_IGNORE.sql)
|
|
39
|
+
.run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
|
|
40
|
+
// If no rows were changed, the event already existed
|
|
41
|
+
if (result.changes === 0)
|
|
42
|
+
return false; // Event already exists, skip tags/search processing
|
|
43
|
+
// Event was inserted, continue with tags and search content
|
|
44
|
+
const indexableTags = getIndexableTags(event);
|
|
45
|
+
if (indexableTags && indexableTags.size > 0) {
|
|
46
|
+
const insertStmt = db.query(INSERT_EVENT_TAG_STATEMENT.sql);
|
|
47
|
+
for (const tagString of indexableTags) {
|
|
48
|
+
// Parse the "tagName:tagValue" format
|
|
49
|
+
const [name, value] = tagString.split(":");
|
|
50
|
+
if (name && value)
|
|
51
|
+
insertStmt.run(event.id, name, value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (contentFormatter) {
|
|
55
|
+
try {
|
|
56
|
+
insertSearchContent(db, event, contentFormatter);
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
45
63
|
});
|
|
46
64
|
return transaction();
|
|
47
65
|
}
|
|
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
66
|
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
66
67
|
export function deleteEvent(db, id) {
|
|
67
68
|
const transaction = db.transaction(() => {
|
|
68
|
-
// Delete from
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
69
|
+
// Delete from search tables if they exist
|
|
70
|
+
try {
|
|
71
|
+
deleteSearchContent(db, id);
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
75
|
+
}
|
|
76
|
+
// Delete from events table - this will CASCADE to event_tags automatically!
|
|
77
|
+
// The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
|
78
|
+
// ensures that all related event_tags records are deleted automatically
|
|
79
|
+
const result = db.query(DELETE_EVENT_STATEMENT.sql).run(id);
|
|
76
80
|
return result.changes > 0;
|
|
77
81
|
});
|
|
78
82
|
return transaction();
|
|
@@ -87,25 +91,21 @@ export function hasEvent(db, id) {
|
|
|
87
91
|
}
|
|
88
92
|
/** Gets a single event from a database */
|
|
89
93
|
export function getEvent(db, id) {
|
|
90
|
-
const
|
|
91
|
-
const row = stmt.get(id);
|
|
94
|
+
const row = db.query(GET_EVENT_STATEMENT.sql).get(id);
|
|
92
95
|
return row ? rowToEvent(row) : undefined;
|
|
93
96
|
}
|
|
94
97
|
/** Gets the latest replaceable event from a database */
|
|
95
98
|
export function getReplaceable(db, kind, pubkey, identifier) {
|
|
96
|
-
const
|
|
97
|
-
const row = stmt.get(kind, pubkey, identifier);
|
|
99
|
+
const row = db.query(GET_REPLACEABLE_STATEMENT.sql).get(kind, pubkey, identifier);
|
|
98
100
|
return row ? rowToEvent(row) : undefined;
|
|
99
101
|
}
|
|
100
102
|
/** Gets the history of a replaceable event from a database */
|
|
101
103
|
export function getReplaceableHistory(db, kind, pubkey, identifier) {
|
|
102
|
-
|
|
103
|
-
return stmt.all(kind, pubkey, identifier).map(rowToEvent);
|
|
104
|
+
return db.query(GET_REPLACEABLE_HISTORY_STATEMENT.sql).all(kind, pubkey, identifier).map(rowToEvent);
|
|
104
105
|
}
|
|
105
106
|
/** Checks if a replaceable event exists in a database */
|
|
106
107
|
export function hasReplaceable(db, kind, pubkey, identifier = "") {
|
|
107
|
-
const
|
|
108
|
-
const result = stmt.get(kind, pubkey, identifier);
|
|
108
|
+
const result = db.query(HAS_REPLACEABLE_STATEMENT.sql).get(kind, pubkey, identifier);
|
|
109
109
|
if (!result)
|
|
110
110
|
return false;
|
|
111
111
|
return result.count > 0;
|
|
@@ -115,8 +115,7 @@ export function getEventsByFilters(db, filters) {
|
|
|
115
115
|
const query = buildFiltersQuery(filters);
|
|
116
116
|
if (!query)
|
|
117
117
|
return [];
|
|
118
|
-
const
|
|
119
|
-
const rows = stmt.all(...query.params);
|
|
118
|
+
const rows = db.query(query.sql).all(...query.params);
|
|
120
119
|
// Map rows to events
|
|
121
120
|
return rows.map(rowToEvent);
|
|
122
121
|
}
|
|
@@ -138,11 +137,33 @@ export function rebuildSearchIndex(db, contentFormatter) {
|
|
|
138
137
|
// Clear existing search data
|
|
139
138
|
db.exec(`DELETE FROM events_search;`);
|
|
140
139
|
// Rebuild from all events
|
|
141
|
-
const
|
|
142
|
-
const events = stmt.all().map(rowToEvent);
|
|
140
|
+
const events = db.query(GET_ALL_EVENTS_STATEMENT.sql).all().map(rowToEvent);
|
|
143
141
|
for (const event of events) {
|
|
144
142
|
insertSearchContent(db, event, contentFormatter);
|
|
145
143
|
}
|
|
146
144
|
});
|
|
147
145
|
transaction();
|
|
148
146
|
}
|
|
147
|
+
/** Removes multiple events that match the given filters from the database */
|
|
148
|
+
export function deleteEventsByFilters(db, filters) {
|
|
149
|
+
const whereClause = buildDeleteFiltersQuery(filters);
|
|
150
|
+
if (!whereClause)
|
|
151
|
+
return 0;
|
|
152
|
+
const transaction = db.transaction(() => {
|
|
153
|
+
// Delete from search tables if they exist
|
|
154
|
+
try {
|
|
155
|
+
const searchDeleteQuery = `DELETE FROM search_content WHERE event_id IN (SELECT id FROM events ${whereClause.sql})`;
|
|
156
|
+
db.query(searchDeleteQuery).run(...whereClause.params);
|
|
157
|
+
}
|
|
158
|
+
catch (error) {
|
|
159
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
160
|
+
}
|
|
161
|
+
// Delete from events table - this will CASCADE to event_tags automatically!
|
|
162
|
+
// The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
|
163
|
+
// ensures that all related event_tags records are deleted automatically
|
|
164
|
+
const deleteEventsQuery = `DELETE FROM events ${whereClause.sql}`;
|
|
165
|
+
const result = db.query(deleteEventsQuery).run(...whereClause.params);
|
|
166
|
+
return result.changes;
|
|
167
|
+
});
|
|
168
|
+
return transaction();
|
|
169
|
+
}
|
package/dist/helpers/search.d.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { FilterWithAnd, NostrEvent } from "applesauce-core/helpers";
|
|
2
2
|
import type { Statement } from "./statements.js";
|
|
3
3
|
export declare const CREATE_SEARCH_TABLE_STATEMENT: Statement<[]>;
|
|
4
4
|
export declare const INSERT_SEARCH_CONTENT_STATEMENT: Statement<[string, string, number, string, number]>;
|
|
5
5
|
export declare const DELETE_SEARCH_CONTENT_STATEMENT: Statement<[string]>;
|
|
6
|
-
/** Filter with search field */
|
|
7
|
-
export type FilterWithSearch =
|
|
6
|
+
/** Filter with search field and NIP-ND AND operator support */
|
|
7
|
+
export type FilterWithSearch = FilterWithAnd & {
|
|
8
8
|
search?: string;
|
|
9
9
|
order?: "created_at" | "rank";
|
|
10
10
|
};
|
package/dist/helpers/sql.d.ts
CHANGED
|
@@ -13,3 +13,8 @@ export declare function buildFiltersQuery(filters: FilterWithSearch | FilterWith
|
|
|
13
13
|
sql: string;
|
|
14
14
|
params: any[];
|
|
15
15
|
} | null;
|
|
16
|
+
/** Builds a WHERE clause for events that match the given filters */
|
|
17
|
+
export declare function buildDeleteFiltersQuery(filters: FilterWithSearch | FilterWithSearch[]): {
|
|
18
|
+
sql: string;
|
|
19
|
+
params: any[];
|
|
20
|
+
} | null;
|
package/dist/helpers/sql.js
CHANGED
|
@@ -49,19 +49,49 @@ export function buildFilterConditions(filter) {
|
|
|
49
49
|
conditions.push(`events.created_at <= ?`);
|
|
50
50
|
params.push(filter.until);
|
|
51
51
|
}
|
|
52
|
-
// Handle tag filters (
|
|
52
|
+
// Handle AND tag filters (& prefix) first - NIP-ND
|
|
53
|
+
// AND takes precedence and requires ALL values to be present
|
|
54
|
+
for (const [key, values] of Object.entries(filter)) {
|
|
55
|
+
if (key.startsWith("&") && values && Array.isArray(values) && values.length > 0) {
|
|
56
|
+
const tagName = key.slice(1); // Remove the '&' prefix
|
|
57
|
+
// Use a single subquery with GROUP BY + HAVING COUNT for efficiency
|
|
58
|
+
// This is more efficient than multiple JOINs for the same tag
|
|
59
|
+
const placeholders = values.map(() => "?").join(", ");
|
|
60
|
+
conditions.push(`events.id IN (
|
|
61
|
+
SELECT event_id
|
|
62
|
+
FROM event_tags
|
|
63
|
+
WHERE tag_name = ? AND tag_value IN (${placeholders})
|
|
64
|
+
GROUP BY event_id
|
|
65
|
+
HAVING COUNT(DISTINCT tag_value) = ?
|
|
66
|
+
)`);
|
|
67
|
+
// Add parameters: tagName, all tag values, then count of values
|
|
68
|
+
params.push(tagName, ...values, values.length);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Handle OR tag filters (# prefix)
|
|
72
|
+
// Skip values that are in AND tags (NIP-ND rule)
|
|
53
73
|
for (const [key, values] of Object.entries(filter)) {
|
|
54
74
|
if (key.startsWith("#") && values && Array.isArray(values) && values.length > 0) {
|
|
55
75
|
const tagName = key.slice(1); // Remove the '#' prefix
|
|
76
|
+
// Check if there's a corresponding AND filter for this tag
|
|
77
|
+
const andKey = `&${tagName}`;
|
|
78
|
+
const andValues = filter[andKey];
|
|
79
|
+
// Filter out values that are in AND tags (NIP-ND rule)
|
|
80
|
+
const filteredValues = andValues
|
|
81
|
+
? values.filter((v) => !andValues.includes(v))
|
|
82
|
+
: values;
|
|
83
|
+
// Only apply OR filter if there are values left after filtering
|
|
84
|
+
if (filteredValues.length === 0)
|
|
85
|
+
continue;
|
|
56
86
|
// Use the event_tags table for efficient tag filtering
|
|
57
|
-
const placeholders =
|
|
87
|
+
const placeholders = filteredValues.map(() => "?").join(", ");
|
|
58
88
|
conditions.push(`events.id IN (
|
|
59
89
|
SELECT DISTINCT event_id
|
|
60
90
|
FROM event_tags
|
|
61
91
|
WHERE tag_name = ? AND tag_value IN (${placeholders})
|
|
62
92
|
)`);
|
|
63
|
-
// Add parameters: tagName first, then all the tag values
|
|
64
|
-
params.push(tagName, ...
|
|
93
|
+
// Add parameters: tagName first, then all the filtered tag values
|
|
94
|
+
params.push(tagName, ...filteredValues);
|
|
65
95
|
}
|
|
66
96
|
}
|
|
67
97
|
return { conditions, params, search };
|
|
@@ -120,3 +150,29 @@ export function buildFiltersQuery(filters) {
|
|
|
120
150
|
}
|
|
121
151
|
return { sql: query, params: allParams };
|
|
122
152
|
}
|
|
153
|
+
/** Builds a WHERE clause for events that match the given filters */
|
|
154
|
+
export function buildDeleteFiltersQuery(filters) {
|
|
155
|
+
const filterArray = Array.isArray(filters) ? filters : [filters];
|
|
156
|
+
if (filterArray.length === 0)
|
|
157
|
+
return null;
|
|
158
|
+
const filterQueries = [];
|
|
159
|
+
const allParams = [];
|
|
160
|
+
for (const filter of filterArray) {
|
|
161
|
+
const { conditions, params } = buildFilterConditions(filter);
|
|
162
|
+
if (conditions.length === 0) {
|
|
163
|
+
// If no conditions, this filter matches all events
|
|
164
|
+
filterQueries.push("1=1");
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
// AND logic within a single filter
|
|
168
|
+
filterQueries.push(`(${conditions.join(" AND ")})`);
|
|
169
|
+
}
|
|
170
|
+
allParams.push(...params);
|
|
171
|
+
}
|
|
172
|
+
// Combine all filter conditions with OR logic
|
|
173
|
+
const whereClause = filterQueries.length > 0 ? `WHERE ${filterQueries.join(" OR ")}` : "";
|
|
174
|
+
return {
|
|
175
|
+
sql: whereClause, // Just return the WHERE clause
|
|
176
|
+
params: allParams,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -28,6 +28,17 @@ export type EventRow = {
|
|
|
28
28
|
tags: string;
|
|
29
29
|
sig: string;
|
|
30
30
|
};
|
|
31
|
+
export declare const INSERT_EVENT_STATEMENT_WITH_IGNORE: Statement<[
|
|
32
|
+
string,
|
|
33
|
+
number,
|
|
34
|
+
string,
|
|
35
|
+
number,
|
|
36
|
+
string,
|
|
37
|
+
string,
|
|
38
|
+
string,
|
|
39
|
+
string
|
|
40
|
+
]>;
|
|
41
|
+
/** For implementations that don't support OR IGNORE (libsql, turso-wasm) */
|
|
31
42
|
export declare const INSERT_EVENT_STATEMENT: Statement<[string, number, string, number, string, string, string, string]>;
|
|
32
43
|
export declare const DELETE_EVENT_TAGS_STATEMENT: Statement<[string]>;
|
|
33
44
|
export declare const INSERT_EVENT_TAG_STATEMENT: Statement<[string, string, string]>;
|
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
// Event-related statements
|
|
2
|
+
export const INSERT_EVENT_STATEMENT_WITH_IGNORE = {
|
|
3
|
+
sql: `INSERT OR IGNORE INTO events (id, kind, pubkey, created_at, content, tags, sig, identifier)
|
|
4
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5
|
+
};
|
|
6
|
+
/** For implementations that don't support OR IGNORE (libsql, turso-wasm) */
|
|
2
7
|
export const INSERT_EVENT_STATEMENT = {
|
|
3
|
-
sql: `INSERT
|
|
8
|
+
sql: `INSERT INTO events (id, kind, pubkey, created_at, content, tags, sig, identifier)
|
|
4
9
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
5
10
|
};
|
|
6
11
|
export const DELETE_EVENT_TAGS_STATEMENT = {
|
|
7
12
|
sql: `DELETE FROM event_tags WHERE event_id = ?`,
|
|
8
13
|
};
|
|
9
14
|
export const INSERT_EVENT_TAG_STATEMENT = {
|
|
10
|
-
sql: `INSERT
|
|
15
|
+
sql: `INSERT INTO event_tags (event_id, tag_name, tag_value) VALUES (?, ?, ?)`,
|
|
11
16
|
};
|
|
12
17
|
export const DELETE_EVENT_STATEMENT = {
|
|
13
18
|
sql: `DELETE FROM events WHERE id = ?`,
|