applesauce-sqlite 0.0.0-next-20250930093922 → 0.0.0-next-20251117143754

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.
@@ -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<void>;
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
  }
@@ -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>;
@@ -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, 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";
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
- // Insert/update the main event
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
- await insertEventTags(transaction, event);
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
- await insertSearchContent(transaction, event, contentFormatter);
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 event_tags first (foreign key constraint)
95
- await transaction.execute({
96
- sql: DELETE_EVENT_TAGS_STATEMENT.sql,
97
- args: [id],
98
- });
99
- // Delete from search tables
100
- await deleteSearchContent(transaction, id);
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
  }
@@ -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;
@@ -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, 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";
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
- const stmt = db.prepare(INSERT_SEARCH_CONTENT_STATEMENT.sql);
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
- const stmt = db.prepare(DELETE_SEARCH_CONTENT_STATEMENT.sql);
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
- // Insert/update the main event
45
- const stmt = db.prepare(INSERT_EVENT_STATEMENT.sql);
46
- const result = stmt.run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
47
- // Insert indexable tags into the event_tags table
48
- insertEventTags(db, event);
49
- // Insert searchable content into the search tables
50
- if (contentFormatter)
51
- insertSearchContent(db, event, contentFormatter);
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 result.changes > 0;
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
- db.exec("BEGIN");
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
- // Delete from events table
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 stmt = db.prepare(HAS_EVENT_STATEMENT.sql);
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 stmt = db.prepare(GET_EVENT_STATEMENT.sql);
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 stmt = db.prepare(GET_REPLACEABLE_STATEMENT.sql);
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
- const stmt = db.prepare(GET_REPLACEABLE_HISTORY_STATEMENT.sql);
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 stmt = db.prepare(HAS_REPLACEABLE_STATEMENT.sql);
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 stmt = db.prepare(query.sql);
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 stmt = db.prepare(GET_ALL_EVENTS_STATEMENT.sql);
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
+ }