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
|
@@ -14,17 +14,21 @@ export declare class LibsqlEventDatabase implements IAsyncEventDatabase {
|
|
|
14
14
|
/** The search content formatter */
|
|
15
15
|
private searchContentFormatter;
|
|
16
16
|
constructor(database: string | Client, options?: LibsqlEventDatabaseOptions);
|
|
17
|
+
/** Create a TursoWasmEventDatabase from a database and initialize it */
|
|
18
|
+
static fromClient(database: Client, options?: LibsqlEventDatabaseOptions): Promise<LibsqlEventDatabase>;
|
|
17
19
|
/** Initialize the database by creating tables and indexes */
|
|
18
|
-
initialize(): Promise<
|
|
20
|
+
initialize(): Promise<this>;
|
|
19
21
|
/** Store a Nostr event in the database */
|
|
20
22
|
add(event: NostrEvent): Promise<NostrEvent>;
|
|
21
23
|
/** Delete an event by ID */
|
|
22
24
|
remove(id: string): Promise<boolean>;
|
|
25
|
+
/** Remove multiple events that match the given filters */
|
|
26
|
+
removeByFilters(filters: FilterWithSearch | FilterWithSearch[]): Promise<number>;
|
|
23
27
|
/** Checks if an event exists */
|
|
24
28
|
hasEvent(id: string): Promise<boolean>;
|
|
25
29
|
/** Get an event by its ID */
|
|
26
30
|
getEvent(id: string): Promise<NostrEvent | undefined>;
|
|
27
|
-
/** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
|
|
31
|
+
/** Get the latest replaceable event For replaceable events (10000-19999 and 30000-39999), returns the most recent event */
|
|
28
32
|
getReplaceable(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent | undefined>;
|
|
29
33
|
/** Checks if a replaceable event exists */
|
|
30
34
|
hasReplaceable(kind: number, pubkey: string, identifier?: string): Promise<boolean>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createClient } from "@libsql/client";
|
|
2
2
|
import { logger } from "applesauce-core";
|
|
3
3
|
import { enhancedSearchContentFormatter } from "../helpers/search.js";
|
|
4
|
-
import { createTables, deleteEvent, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./methods.js";
|
|
4
|
+
import { createTables, deleteEvent, deleteEventsByFilters, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./methods.js";
|
|
5
5
|
const log = logger.extend("LibsqlEventDatabase");
|
|
6
6
|
export class LibsqlEventDatabase {
|
|
7
7
|
db;
|
|
@@ -14,9 +14,15 @@ export class LibsqlEventDatabase {
|
|
|
14
14
|
this.search = options?.search ?? false;
|
|
15
15
|
this.searchContentFormatter = options?.searchContentFormatter ?? enhancedSearchContentFormatter;
|
|
16
16
|
}
|
|
17
|
+
/** Create a TursoWasmEventDatabase from a database and initialize it */
|
|
18
|
+
static async fromClient(database, options) {
|
|
19
|
+
const eventDatabase = new LibsqlEventDatabase(database, options);
|
|
20
|
+
return await eventDatabase.initialize();
|
|
21
|
+
}
|
|
17
22
|
/** Initialize the database by creating tables and indexes */
|
|
18
23
|
async initialize() {
|
|
19
24
|
await createTables(this.db, this.search);
|
|
25
|
+
return this;
|
|
20
26
|
}
|
|
21
27
|
/** Store a Nostr event in the database */
|
|
22
28
|
async add(event) {
|
|
@@ -39,6 +45,13 @@ export class LibsqlEventDatabase {
|
|
|
39
45
|
return false;
|
|
40
46
|
}
|
|
41
47
|
}
|
|
48
|
+
/** Remove multiple events that match the given filters */
|
|
49
|
+
async removeByFilters(filters) {
|
|
50
|
+
// If search is disabled, remove the search field from the filters
|
|
51
|
+
if (this.search && (Array.isArray(filters) ? filters.some((f) => "search" in f) : "search" in filters))
|
|
52
|
+
throw new Error("Cannot delete with search");
|
|
53
|
+
return await deleteEventsByFilters(this.db, filters);
|
|
54
|
+
}
|
|
42
55
|
/** Checks if an event exists */
|
|
43
56
|
async hasEvent(id) {
|
|
44
57
|
return await hasEvent(this.db, id);
|
|
@@ -47,7 +60,7 @@ export class LibsqlEventDatabase {
|
|
|
47
60
|
async getEvent(id) {
|
|
48
61
|
return await getEvent(this.db, id);
|
|
49
62
|
}
|
|
50
|
-
/** Get the latest replaceable event For replaceable events (10000-19999), returns the most recent event */
|
|
63
|
+
/** Get the latest replaceable event For replaceable events (10000-19999 and 30000-39999), returns the most recent event */
|
|
51
64
|
async getReplaceable(kind, pubkey, identifier = "") {
|
|
52
65
|
return await getReplaceable(this.db, kind, pubkey, identifier);
|
|
53
66
|
}
|
package/dist/libsql/methods.d.ts
CHANGED
|
@@ -9,8 +9,6 @@ export declare function insertSearchContent(db: Client | Transaction, event: Nos
|
|
|
9
9
|
export declare function deleteSearchContent(db: Client | Transaction, eventId: string): Promise<void>;
|
|
10
10
|
/** Inserts an event into the `events`, `event_tags`, and search tables of a database */
|
|
11
11
|
export declare function insertEvent(db: Client, event: NostrEvent, contentFormatter?: SearchContentFormatter): Promise<boolean>;
|
|
12
|
-
/** Insert indexable tags for an event into the event_tags table */
|
|
13
|
-
export declare function insertEventTags(db: Client | Transaction, event: NostrEvent): Promise<void>;
|
|
14
12
|
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
15
13
|
export declare function deleteEvent(db: Client, id: string): Promise<boolean>;
|
|
16
14
|
/** Checks if an event exists */
|
|
@@ -29,3 +27,5 @@ export declare function getEventsByFilters(db: Client, filters: FilterWithSearch
|
|
|
29
27
|
export declare function searchEvents(db: Client, search: string, options?: Filter): Promise<NostrEvent[]>;
|
|
30
28
|
/** Rebuild the FTS5 search index for all events */
|
|
31
29
|
export declare function rebuildSearchIndex(db: Client, contentFormatter: SearchContentFormatter): Promise<void>;
|
|
30
|
+
/** Removes multiple events that match the given filters from the database */
|
|
31
|
+
export declare function deleteEventsByFilters(db: Client, filters: FilterWithSearch | FilterWithSearch[]): Promise<number>;
|
package/dist/libsql/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, INSERT_EVENT_TAG_STATEMENT, } from "../helpers/statements.js";
|
|
5
5
|
/** Create and migrate the `events`, `event_tags`, and search tables */
|
|
6
6
|
export async function createTables(db, search = true) {
|
|
7
7
|
// Create the events table
|
|
@@ -38,7 +38,16 @@ export async function insertEvent(db, event, contentFormatter) {
|
|
|
38
38
|
const identifier = getReplaceableIdentifier(event);
|
|
39
39
|
const transaction = await db.transaction();
|
|
40
40
|
try {
|
|
41
|
-
//
|
|
41
|
+
// Check if event already exists
|
|
42
|
+
const existsResult = await transaction.execute({
|
|
43
|
+
sql: HAS_EVENT_STATEMENT.sql,
|
|
44
|
+
args: [event.id],
|
|
45
|
+
});
|
|
46
|
+
if (existsResult.rows[0] && existsResult.rows[0][0] > 0) {
|
|
47
|
+
await transaction.rollback();
|
|
48
|
+
return false; // Event already exists, skip insertion
|
|
49
|
+
}
|
|
50
|
+
// Insert the event
|
|
42
51
|
const result = await transaction.execute({
|
|
43
52
|
sql: INSERT_EVENT_STATEMENT.sql,
|
|
44
53
|
args: [
|
|
@@ -53,10 +62,28 @@ export async function insertEvent(db, event, contentFormatter) {
|
|
|
53
62
|
],
|
|
54
63
|
});
|
|
55
64
|
// Insert indexable tags into the event_tags table
|
|
56
|
-
|
|
65
|
+
const indexableTags = getIndexableTags(event);
|
|
66
|
+
if (indexableTags && indexableTags.size > 0) {
|
|
67
|
+
for (const tagString of indexableTags) {
|
|
68
|
+
// Parse the "tagName:tagValue" format
|
|
69
|
+
const [name, value] = tagString.split(":");
|
|
70
|
+
if (name && value) {
|
|
71
|
+
await db.execute({
|
|
72
|
+
sql: INSERT_EVENT_TAG_STATEMENT.sql,
|
|
73
|
+
args: [event.id, name, value],
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
57
78
|
// Insert searchable content into the search tables
|
|
58
|
-
if (contentFormatter)
|
|
59
|
-
|
|
79
|
+
if (contentFormatter) {
|
|
80
|
+
try {
|
|
81
|
+
await insertSearchContent(transaction, event, contentFormatter);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
60
87
|
await transaction.commit();
|
|
61
88
|
return result.rowsAffected > 0;
|
|
62
89
|
}
|
|
@@ -65,40 +92,20 @@ export async function insertEvent(db, event, contentFormatter) {
|
|
|
65
92
|
throw error;
|
|
66
93
|
}
|
|
67
94
|
}
|
|
68
|
-
/** Insert indexable tags for an event into the event_tags table */
|
|
69
|
-
export async function insertEventTags(db, event) {
|
|
70
|
-
// Clear existing tags for this event first
|
|
71
|
-
await db.execute({
|
|
72
|
-
sql: DELETE_EVENT_TAGS_STATEMENT.sql,
|
|
73
|
-
args: [event.id],
|
|
74
|
-
});
|
|
75
|
-
// Get only the indexable tags using applesauce-core helper
|
|
76
|
-
const indexableTags = getIndexableTags(event);
|
|
77
|
-
if (indexableTags && indexableTags.size > 0) {
|
|
78
|
-
for (const tagString of indexableTags) {
|
|
79
|
-
// Parse the "tagName:tagValue" format
|
|
80
|
-
const [name, value] = tagString.split(":");
|
|
81
|
-
if (name && value) {
|
|
82
|
-
await db.execute({
|
|
83
|
-
sql: INSERT_EVENT_TAG_STATEMENT.sql,
|
|
84
|
-
args: [event.id, name, value],
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
95
|
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
91
96
|
export async function deleteEvent(db, id) {
|
|
92
97
|
const transaction = await db.transaction();
|
|
93
98
|
try {
|
|
94
|
-
// Delete from
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Delete from events table
|
|
99
|
+
// Delete from search tables if they exist
|
|
100
|
+
try {
|
|
101
|
+
await deleteSearchContent(transaction, id);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
105
|
+
}
|
|
106
|
+
// Delete from events table - this will CASCADE to event_tags automatically!
|
|
107
|
+
// The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
|
108
|
+
// ensures that all related event_tags records are deleted automatically
|
|
102
109
|
const result = await transaction.execute({
|
|
103
110
|
sql: DELETE_EVENT_STATEMENT.sql,
|
|
104
111
|
args: [id],
|
|
@@ -246,3 +253,37 @@ export async function rebuildSearchIndex(db, contentFormatter) {
|
|
|
246
253
|
throw error;
|
|
247
254
|
}
|
|
248
255
|
}
|
|
256
|
+
/** Removes multiple events that match the given filters from the database */
|
|
257
|
+
export async function deleteEventsByFilters(db, filters) {
|
|
258
|
+
const whereClause = buildDeleteFiltersQuery(filters);
|
|
259
|
+
if (!whereClause)
|
|
260
|
+
return 0;
|
|
261
|
+
const transaction = await db.transaction();
|
|
262
|
+
try {
|
|
263
|
+
// Delete from search tables if they exist
|
|
264
|
+
try {
|
|
265
|
+
const searchDeleteQuery = `DELETE FROM search_content WHERE event_id IN (SELECT id FROM events ${whereClause.sql})`;
|
|
266
|
+
await transaction.execute({
|
|
267
|
+
sql: searchDeleteQuery,
|
|
268
|
+
args: whereClause.params,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
273
|
+
}
|
|
274
|
+
// Delete from events table - this will CASCADE to event_tags automatically!
|
|
275
|
+
// The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
|
276
|
+
// ensures that all related event_tags records are deleted automatically
|
|
277
|
+
const deleteEventsQuery = `DELETE FROM events ${whereClause.sql}`;
|
|
278
|
+
const result = await transaction.execute({
|
|
279
|
+
sql: deleteEventsQuery,
|
|
280
|
+
args: whereClause.params,
|
|
281
|
+
});
|
|
282
|
+
await transaction.commit();
|
|
283
|
+
return result.rowsAffected;
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
await transaction.rollback();
|
|
287
|
+
throw error;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -18,11 +18,13 @@ export declare class NativeSqliteEventDatabase 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,7 +1,7 @@
|
|
|
1
1
|
import { logger } from "applesauce-core";
|
|
2
2
|
import { DatabaseSync } from "node:sqlite";
|
|
3
3
|
import { enhancedSearchContentFormatter } from "../helpers/search.js";
|
|
4
|
-
import { createTables, deleteEvent, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./methods.js";
|
|
4
|
+
import { createTables, deleteEvent, deleteEventsByFilters, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, rebuildSearchIndex, } from "./methods.js";
|
|
5
5
|
const log = logger.extend("NativeSqliteEventDatabase");
|
|
6
6
|
export class NativeSqliteEventDatabase {
|
|
7
7
|
db;
|
|
@@ -37,6 +37,13 @@ export class NativeSqliteEventDatabase {
|
|
|
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 NativeSqliteEventDatabase {
|
|
|
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
|
}
|
package/dist/native/methods.d.ts
CHANGED
|
@@ -9,8 +9,6 @@ export declare function insertSearchContent(db: DatabaseSync, event: NostrEvent,
|
|
|
9
9
|
export declare function deleteSearchContent(db: DatabaseSync, 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: DatabaseSync, event: NostrEvent, contentFormatter?: SearchContentFormatter): boolean;
|
|
12
|
-
/** Insert indexable tags for an event into the event_tags table */
|
|
13
|
-
export declare function insertEventTags(db: DatabaseSync, event: NostrEvent): void;
|
|
14
12
|
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
15
13
|
export declare function deleteEvent(db: DatabaseSync, id: string): boolean;
|
|
16
14
|
/** Checks if an event exists */
|
|
@@ -29,3 +27,5 @@ export declare function getEventsByFilters(db: DatabaseSync, filters: FilterWith
|
|
|
29
27
|
export declare function searchEvents(db: DatabaseSync, search: string, options?: Filter): NostrEvent[];
|
|
30
28
|
/** Rebuild the FTS5 search index for all events */
|
|
31
29
|
export declare function rebuildSearchIndex(db: DatabaseSync, contentFormatter: SearchContentFormatter): void;
|
|
30
|
+
/** Removes multiple events that match the given filters from the database */
|
|
31
|
+
export declare function deleteEventsByFilters(db: DatabaseSync, filters: FilterWithSearch | FilterWithSearch[]): number;
|
package/dist/native/methods.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { logger } from "applesauce-core";
|
|
2
2
|
import { getIndexableTags, getReplaceableIdentifier } from "applesauce-core/helpers";
|
|
3
3
|
import { CREATE_SEARCH_TABLE_STATEMENT, DELETE_SEARCH_CONTENT_STATEMENT, INSERT_SEARCH_CONTENT_STATEMENT, } from "../helpers/search.js";
|
|
4
|
-
import { buildFiltersQuery, rowToEvent } from "../helpers/sql.js";
|
|
5
|
-
import { CREATE_EVENT_TAGS_TABLE_STATEMENT, CREATE_EVENTS_TABLE_STATEMENT, CREATE_INDEXES_STATEMENTS, DELETE_EVENT_STATEMENT,
|
|
4
|
+
import { buildFiltersQuery, buildDeleteFiltersQuery, rowToEvent } from "../helpers/sql.js";
|
|
5
|
+
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";
|
|
6
6
|
const log = logger.extend("sqlite:tables");
|
|
7
7
|
/** Create and migrate the `events`, `event_tags`, and search tables */
|
|
8
8
|
export function createTables(db, search = true) {
|
|
@@ -27,13 +27,11 @@ export function createTables(db, search = true) {
|
|
|
27
27
|
export function insertSearchContent(db, event, contentFormatter) {
|
|
28
28
|
const searchableContent = contentFormatter(event);
|
|
29
29
|
// Insert/update directly into the FTS5 table
|
|
30
|
-
|
|
31
|
-
stmt.run(event.id, searchableContent, event.kind, event.pubkey, event.created_at);
|
|
30
|
+
db.prepare(INSERT_SEARCH_CONTENT_STATEMENT.sql).run(event.id, searchableContent, event.kind, event.pubkey, event.created_at);
|
|
32
31
|
}
|
|
33
32
|
/** Removes search content for an event */
|
|
34
33
|
export function deleteSearchContent(db, eventId) {
|
|
35
|
-
|
|
36
|
-
stmt.run(eventId);
|
|
34
|
+
db.prepare(DELETE_SEARCH_CONTENT_STATEMENT.sql).run(eventId);
|
|
37
35
|
}
|
|
38
36
|
/** Inserts an event into the `events`, `event_tags`, and search tables of a database */
|
|
39
37
|
export function insertEvent(db, event, contentFormatter) {
|
|
@@ -41,49 +39,57 @@ export function insertEvent(db, event, contentFormatter) {
|
|
|
41
39
|
// Node.js sqlite doesn't have a transaction method like better-sqlite3, so we use BEGIN/COMMIT
|
|
42
40
|
db.exec("BEGIN");
|
|
43
41
|
try {
|
|
44
|
-
//
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
42
|
+
// Try to insert the main event with OR IGNORE
|
|
43
|
+
const result = db
|
|
44
|
+
.prepare(INSERT_EVENT_STATEMENT_WITH_IGNORE.sql)
|
|
45
|
+
.run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
|
|
46
|
+
// If no rows were changed, the event already existed
|
|
47
|
+
if (result.changes === 0) {
|
|
48
|
+
db.exec("COMMIT");
|
|
49
|
+
return false; // Event already exists, skip tags/search processing
|
|
50
|
+
}
|
|
51
|
+
// Event was inserted, continue with tags and search content
|
|
52
|
+
// Get only the indexable tags using applesauce-core helper
|
|
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);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (contentFormatter) {
|
|
64
|
+
try {
|
|
65
|
+
insertSearchContent(db, event, contentFormatter);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
69
|
+
}
|
|
70
|
+
}
|
|
52
71
|
db.exec("COMMIT");
|
|
53
|
-
return
|
|
72
|
+
return true;
|
|
54
73
|
}
|
|
55
74
|
catch (error) {
|
|
56
75
|
db.exec("ROLLBACK");
|
|
57
76
|
throw error;
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
|
-
/** Insert indexable tags for an event into the event_tags table */
|
|
61
|
-
export function insertEventTags(db, event) {
|
|
62
|
-
// Clear existing tags for this event first
|
|
63
|
-
const deleteStmt = db.prepare(DELETE_EVENT_TAGS_STATEMENT.sql);
|
|
64
|
-
deleteStmt.run(event.id);
|
|
65
|
-
// Get only the indexable tags using applesauce-core helper
|
|
66
|
-
const indexableTags = getIndexableTags(event);
|
|
67
|
-
if (indexableTags && indexableTags.size > 0) {
|
|
68
|
-
const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT.sql);
|
|
69
|
-
for (const tagString of indexableTags) {
|
|
70
|
-
// Parse the "tagName:tagValue" format
|
|
71
|
-
const [name, value] = tagString.split(":");
|
|
72
|
-
if (name && value)
|
|
73
|
-
insertStmt.run(event.id, name, value);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
79
|
/** Removes an event by id from the `events`, `event_tags`, and search tables of a database */
|
|
78
80
|
export function deleteEvent(db, id) {
|
|
79
|
-
|
|
81
|
+
// Delete from search tables first (outside transaction to avoid rollback issues)
|
|
80
82
|
try {
|
|
81
|
-
// Delete from event_tags first (foreign key constraint)
|
|
82
|
-
const deleteTagsStmt = db.prepare(DELETE_EVENT_TAGS_STATEMENT.sql);
|
|
83
|
-
deleteTagsStmt.run(id);
|
|
84
|
-
// Delete from search tables
|
|
85
83
|
deleteSearchContent(db, id);
|
|
86
|
-
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
87
|
+
}
|
|
88
|
+
db.exec("BEGIN");
|
|
89
|
+
try {
|
|
90
|
+
// Delete from events table - this will CASCADE to event_tags automatically!
|
|
91
|
+
// The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
|
92
|
+
// ensures that all related event_tags records are deleted automatically
|
|
87
93
|
const deleteEventStmt = db.prepare(DELETE_EVENT_STATEMENT.sql);
|
|
88
94
|
const result = deleteEventStmt.run(id);
|
|
89
95
|
db.exec("COMMIT");
|
|
@@ -96,33 +102,28 @@ export function deleteEvent(db, id) {
|
|
|
96
102
|
}
|
|
97
103
|
/** Checks if an event exists */
|
|
98
104
|
export function hasEvent(db, id) {
|
|
99
|
-
const
|
|
100
|
-
const result = stmt.get(id);
|
|
105
|
+
const result = db.prepare(HAS_EVENT_STATEMENT.sql).get(id);
|
|
101
106
|
if (!result)
|
|
102
107
|
return false;
|
|
103
108
|
return result.count > 0;
|
|
104
109
|
}
|
|
105
110
|
/** Gets a single event from a database */
|
|
106
111
|
export function getEvent(db, id) {
|
|
107
|
-
const
|
|
108
|
-
const row = stmt.get(id);
|
|
112
|
+
const row = db.prepare(GET_EVENT_STATEMENT.sql).get(id);
|
|
109
113
|
return row && rowToEvent(row);
|
|
110
114
|
}
|
|
111
115
|
/** Gets the latest replaceable event from a database */
|
|
112
116
|
export function getReplaceable(db, kind, pubkey, identifier) {
|
|
113
|
-
const
|
|
114
|
-
const row = stmt.get(kind, pubkey, identifier);
|
|
117
|
+
const row = db.prepare(GET_REPLACEABLE_STATEMENT.sql).get(kind, pubkey, identifier);
|
|
115
118
|
return row && rowToEvent(row);
|
|
116
119
|
}
|
|
117
120
|
/** Gets the history of a replaceable event from a database */
|
|
118
121
|
export function getReplaceableHistory(db, kind, pubkey, identifier) {
|
|
119
|
-
|
|
120
|
-
return stmt.all(kind, pubkey, identifier).map(rowToEvent);
|
|
122
|
+
return db.prepare(GET_REPLACEABLE_HISTORY_STATEMENT.sql).all(kind, pubkey, identifier).map(rowToEvent);
|
|
121
123
|
}
|
|
122
124
|
/** Checks if a replaceable event exists in a database */
|
|
123
125
|
export function hasReplaceable(db, kind, pubkey, identifier = "") {
|
|
124
|
-
const
|
|
125
|
-
const result = stmt.get(kind, pubkey, identifier);
|
|
126
|
+
const result = db.prepare(HAS_REPLACEABLE_STATEMENT.sql).get(kind, pubkey, identifier);
|
|
126
127
|
if (!result)
|
|
127
128
|
return false;
|
|
128
129
|
return result.count > 0;
|
|
@@ -132,8 +133,7 @@ export function getEventsByFilters(db, filters) {
|
|
|
132
133
|
const query = buildFiltersQuery(filters);
|
|
133
134
|
if (!query)
|
|
134
135
|
return [];
|
|
135
|
-
const
|
|
136
|
-
const rows = stmt.all(...query.params);
|
|
136
|
+
const rows = db.prepare(query.sql).all(...query.params);
|
|
137
137
|
// Convert rows to events and add to set
|
|
138
138
|
return rows.map(rowToEvent);
|
|
139
139
|
}
|
|
@@ -156,8 +156,7 @@ export function rebuildSearchIndex(db, contentFormatter) {
|
|
|
156
156
|
// Clear existing search data
|
|
157
157
|
db.exec(`DELETE FROM events_search;`);
|
|
158
158
|
// Rebuild from all events
|
|
159
|
-
const
|
|
160
|
-
const events = stmt.all().map(rowToEvent);
|
|
159
|
+
const events = db.prepare(GET_ALL_EVENTS_STATEMENT.sql).all().map(rowToEvent);
|
|
161
160
|
for (const event of events) {
|
|
162
161
|
insertSearchContent(db, event, contentFormatter);
|
|
163
162
|
}
|
|
@@ -168,3 +167,31 @@ export function rebuildSearchIndex(db, contentFormatter) {
|
|
|
168
167
|
throw error;
|
|
169
168
|
}
|
|
170
169
|
}
|
|
170
|
+
/** Removes multiple events that match the given filters from the database */
|
|
171
|
+
export function deleteEventsByFilters(db, filters) {
|
|
172
|
+
const whereClause = buildDeleteFiltersQuery(filters);
|
|
173
|
+
if (!whereClause)
|
|
174
|
+
return 0;
|
|
175
|
+
db.exec("BEGIN");
|
|
176
|
+
try {
|
|
177
|
+
// Delete from search tables first (no foreign key, so do manually)
|
|
178
|
+
try {
|
|
179
|
+
const searchDeleteQuery = `DELETE FROM search_content WHERE event_id IN (SELECT id FROM events ${whereClause.sql})`;
|
|
180
|
+
db.prepare(searchDeleteQuery).run(...whereClause.params);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
// Search table might not exist if search is disabled, ignore the error
|
|
184
|
+
}
|
|
185
|
+
// Delete from events table - this will CASCADE to event_tags automatically!
|
|
186
|
+
// The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
|
|
187
|
+
// ensures that all related event_tags records are deleted automatically
|
|
188
|
+
const deleteEventsQuery = `DELETE FROM events ${whereClause.sql}`;
|
|
189
|
+
const result = db.prepare(deleteEventsQuery).run(...whereClause.params);
|
|
190
|
+
db.exec("COMMIT");
|
|
191
|
+
return Number(result.changes);
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
db.exec("ROLLBACK");
|
|
195
|
+
throw error;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Database } from "@tursodatabase/database";
|
|
2
|
+
import { IAsyncEventDatabase } from "applesauce-core";
|
|
3
|
+
import { NostrEvent } from "applesauce-core/helpers";
|
|
4
|
+
import { FilterWithSearch, SearchContentFormatter } from "../helpers/search.js";
|
|
5
|
+
/** Options for the {@link TursoEventDatabase} */
|
|
6
|
+
export type TursoEventDatabaseOptions = {
|
|
7
|
+
search?: boolean;
|
|
8
|
+
searchContentFormatter?: SearchContentFormatter;
|
|
9
|
+
};
|
|
10
|
+
export declare class TursoEventDatabase implements IAsyncEventDatabase {
|
|
11
|
+
db: Database;
|
|
12
|
+
/** If search is enabled */
|
|
13
|
+
private search;
|
|
14
|
+
/** The search content formatter */
|
|
15
|
+
private searchContentFormatter;
|
|
16
|
+
constructor(database: Database, options?: TursoEventDatabaseOptions);
|
|
17
|
+
/** Create a TursoEventDatabase from a database and initialize it */
|
|
18
|
+
static fromDatabase(database: string | Database, options?: TursoEventDatabaseOptions): Promise<TursoEventDatabase>;
|
|
19
|
+
/** Initialize the database by creating tables and indexes */
|
|
20
|
+
initialize(): Promise<this>;
|
|
21
|
+
/** Store a Nostr event in the database */
|
|
22
|
+
add(event: NostrEvent): Promise<NostrEvent>;
|
|
23
|
+
/** Delete an event by ID */
|
|
24
|
+
remove(id: string): Promise<boolean>;
|
|
25
|
+
/** Remove multiple events that match the given filters */
|
|
26
|
+
removeByFilters(filters: FilterWithSearch | FilterWithSearch[]): Promise<number>;
|
|
27
|
+
/** Checks if an event exists */
|
|
28
|
+
hasEvent(id: string): Promise<boolean>;
|
|
29
|
+
/** Get an event by its ID */
|
|
30
|
+
getEvent(id: string): Promise<NostrEvent | undefined>;
|
|
31
|
+
/** Get the latest replaceable event For replaceable events (10000-19999 and 30000-39999), returns the most recent event */
|
|
32
|
+
getReplaceable(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent | undefined>;
|
|
33
|
+
/** Checks if a replaceable event exists */
|
|
34
|
+
hasReplaceable(kind: number, pubkey: string, identifier?: string): Promise<boolean>;
|
|
35
|
+
/** Returns all the versions of a replaceable event */
|
|
36
|
+
getReplaceableHistory(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent[] | undefined>;
|
|
37
|
+
/** Get all events that match the filters (supports NIP-50 search field) */
|
|
38
|
+
getByFilters(filters: FilterWithSearch | FilterWithSearch[]): Promise<NostrEvent[]>;
|
|
39
|
+
/** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
|
|
40
|
+
getTimeline(filters: FilterWithSearch | FilterWithSearch[]): Promise<NostrEvent[]>;
|
|
41
|
+
/** Set the search content formatter */
|
|
42
|
+
setSearchContentFormatter(formatter: SearchContentFormatter): void;
|
|
43
|
+
/** Get the current search content formatter */
|
|
44
|
+
getSearchContentFormatter(): SearchContentFormatter;
|
|
45
|
+
/** Rebuild the search index for all events */
|
|
46
|
+
rebuildSearchIndex(): Promise<void>;
|
|
47
|
+
/** Close the database connection */
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
[Symbol.dispose](): void;
|
|
50
|
+
}
|