applesauce-sqlite 4.0.0 → 4.1.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.
@@ -0,0 +1,145 @@
1
+ import { getIndexableTags, getReplaceableIdentifier } from "applesauce-core/helpers";
2
+ import { buildDeleteFiltersQuery, buildFiltersQuery, rowToEvent } from "../helpers/sql.js";
3
+ import { CREATE_SEARCH_TABLE_STATEMENT, DELETE_SEARCH_CONTENT_STATEMENT, INSERT_SEARCH_CONTENT_STATEMENT, } from "../helpers/search.js";
4
+ import { CREATE_EVENT_TAGS_TABLE_STATEMENT, CREATE_EVENTS_TABLE_STATEMENT, CREATE_INDEXES_STATEMENTS, DELETE_EVENT_STATEMENT, GET_EVENT_STATEMENT, GET_REPLACEABLE_HISTORY_STATEMENT, GET_REPLACEABLE_STATEMENT, HAS_EVENT_STATEMENT, HAS_REPLACEABLE_STATEMENT, INSERT_EVENT_STATEMENT, INSERT_EVENT_TAG_STATEMENT, } from "../helpers/statements.js";
5
+ /** Create and migrate the `events`, `event_tags`, and search tables */
6
+ export async function createTables(db, search = false) {
7
+ // Create the events table
8
+ await db.exec(CREATE_EVENTS_TABLE_STATEMENT.sql);
9
+ // Create the event_tags table
10
+ await db.exec(CREATE_EVENT_TAGS_TABLE_STATEMENT.sql);
11
+ // Create search table if search is enabled
12
+ if (search) {
13
+ await db.exec(CREATE_SEARCH_TABLE_STATEMENT.sql);
14
+ }
15
+ // Create indexes
16
+ for (const indexStatement of CREATE_INDEXES_STATEMENTS) {
17
+ await db.exec(indexStatement.sql);
18
+ }
19
+ }
20
+ /** Inserts an event into the `events`, `event_tags` */
21
+ export async function insertEvent(db, event, searchContentFormatter) {
22
+ const identifier = getReplaceableIdentifier(event);
23
+ return await db.transaction(async () => {
24
+ // Try to insert the main event with OR IGNORE
25
+ const result = await db
26
+ .prepare(INSERT_EVENT_STATEMENT.sql)
27
+ .run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
28
+ // If no rows were changed, the event already existed
29
+ if (result.changes === 0)
30
+ return false; // Event already exists, skip tags processing
31
+ // Event was inserted, continue with tags
32
+ const indexableTags = getIndexableTags(event);
33
+ if (indexableTags && indexableTags.size > 0) {
34
+ const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT.sql);
35
+ for (const tagString of indexableTags) {
36
+ // Parse the "tagName:tagValue" format
37
+ const [name, value] = tagString.split(":");
38
+ if (name && value)
39
+ await insertStmt.run(event.id, name, value);
40
+ }
41
+ }
42
+ // Insert search content if search is enabled
43
+ if (searchContentFormatter) {
44
+ try {
45
+ const searchContent = searchContentFormatter(event);
46
+ await db
47
+ .prepare(INSERT_SEARCH_CONTENT_STATEMENT.sql)
48
+ .run(event.id, searchContent, event.kind, event.pubkey, event.created_at);
49
+ }
50
+ catch (error) {
51
+ // Search table might not exist if search is disabled, ignore the error
52
+ }
53
+ }
54
+ return result.changes > 0;
55
+ })();
56
+ }
57
+ /** Removes an event by id from the `events`, `event_tags` */
58
+ export async function deleteEvent(db, id) {
59
+ return await db.transaction(async () => {
60
+ // Delete from search table first if it exists
61
+ try {
62
+ await db.prepare(DELETE_SEARCH_CONTENT_STATEMENT.sql).run(id);
63
+ }
64
+ catch (error) {
65
+ // Search table might not exist if search is disabled, ignore the error
66
+ }
67
+ // Delete from events table - this will CASCADE to event_tags automatically!
68
+ // The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
69
+ // ensures that all related event_tags records are deleted automatically
70
+ const result = await db.prepare(DELETE_EVENT_STATEMENT.sql).run(id);
71
+ return result.changes > 0;
72
+ })();
73
+ }
74
+ /** Checks if an event exists */
75
+ export async function hasEvent(db, id) {
76
+ const result = await db.prepare(HAS_EVENT_STATEMENT.sql).get(id);
77
+ if (!result)
78
+ return false;
79
+ return result.count > 0;
80
+ }
81
+ /** Gets a single event from a database */
82
+ export async function getEvent(db, id) {
83
+ const row = await db.prepare(GET_EVENT_STATEMENT.sql).get(id);
84
+ return row && rowToEvent(row);
85
+ }
86
+ /** Gets the latest replaceable event from a database */
87
+ export async function getReplaceable(db, kind, pubkey, identifier) {
88
+ const row = await db.prepare(GET_REPLACEABLE_STATEMENT.sql).get(kind, pubkey, identifier);
89
+ return row && rowToEvent(row);
90
+ }
91
+ /** Gets the history of a replaceable event from a database */
92
+ export async function getReplaceableHistory(db, kind, pubkey, identifier) {
93
+ const rows = await db.prepare(GET_REPLACEABLE_HISTORY_STATEMENT.sql).all(kind, pubkey, identifier);
94
+ return rows.map(rowToEvent);
95
+ }
96
+ /** Checks if a replaceable event exists in a database */
97
+ export async function hasReplaceable(db, kind, pubkey, identifier = "") {
98
+ const result = await db.prepare(HAS_REPLACEABLE_STATEMENT.sql).get(kind, pubkey, identifier);
99
+ if (!result)
100
+ return false;
101
+ return result.count > 0;
102
+ }
103
+ /** Get all events that match the filters */
104
+ export async function getEventsByFilters(db, filters) {
105
+ const query = buildFiltersQuery(filters);
106
+ if (!query)
107
+ return [];
108
+ const rows = await db.prepare(query.sql).all(...query.params);
109
+ // Convert rows to events and add to set
110
+ return rows.map(rowToEvent);
111
+ }
112
+ /** Removes multiple events that match the given filters from the database */
113
+ export async function deleteEventsByFilters(db, filters) {
114
+ const whereClause = buildDeleteFiltersQuery(filters);
115
+ if (!whereClause)
116
+ return 0;
117
+ return await db.transaction(async () => {
118
+ // Delete from events table - this will CASCADE to event_tags automatically!
119
+ // The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
120
+ // ensures that all related event_tags records are deleted automatically
121
+ const deleteEventsQuery = `DELETE FROM events ${whereClause.sql}`;
122
+ const result = await db.prepare(deleteEventsQuery).run(...whereClause.params);
123
+ return result.changes;
124
+ })();
125
+ }
126
+ /** Rebuild the search index for all events */
127
+ export async function rebuildSearchIndex(db, searchContentFormatter) {
128
+ try {
129
+ // Clear the search table
130
+ await db.exec("DELETE FROM events_search");
131
+ // Get all events and rebuild the search index
132
+ const events = await db.prepare("SELECT * FROM events").all();
133
+ for (const eventRow of events) {
134
+ const event = rowToEvent(eventRow);
135
+ const searchContent = searchContentFormatter(event);
136
+ await db
137
+ .prepare(INSERT_SEARCH_CONTENT_STATEMENT.sql)
138
+ .run(event.id, searchContent, event.kind, event.pubkey, event.created_at);
139
+ }
140
+ }
141
+ catch (error) {
142
+ // Search table might not exist if search is disabled, throw a more descriptive error
143
+ throw new Error("Search table does not exist. Make sure search is enabled when creating the database.");
144
+ }
145
+ }
@@ -0,0 +1,36 @@
1
+ import { Database } from "@tursodatabase/database-wasm";
2
+ import { IAsyncEventDatabase } from "applesauce-core";
3
+ import { Filter, NostrEvent } from "applesauce-core/helpers";
4
+ /** Options for the {@link TursoWasmEventDatabase} */
5
+ export type TursoWasmEventDatabaseOptions = {};
6
+ export declare class TursoWasmEventDatabase implements IAsyncEventDatabase {
7
+ db: Database;
8
+ constructor(database: Database, _options?: TursoWasmEventDatabaseOptions);
9
+ /** Create a TursoWasmEventDatabase from a database and initialize it */
10
+ static fromDatabase(database: Database, options?: TursoWasmEventDatabaseOptions): Promise<TursoWasmEventDatabase>;
11
+ /** Initialize the database by creating tables and indexes */
12
+ initialize(): Promise<this>;
13
+ /** Store a Nostr event in the database */
14
+ add(event: NostrEvent): Promise<NostrEvent>;
15
+ /** Delete an event by ID */
16
+ remove(id: string): Promise<boolean>;
17
+ /** Remove multiple events that match the given filters */
18
+ removeByFilters(filters: Filter | Filter[]): Promise<number>;
19
+ /** Checks if an event exists */
20
+ hasEvent(id: string): Promise<boolean>;
21
+ /** Get an event by its ID */
22
+ getEvent(id: string): Promise<NostrEvent | undefined>;
23
+ /** Get the latest replaceable event For replaceable events (10000-19999 and 30000-39999), returns the most recent event */
24
+ getReplaceable(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent | undefined>;
25
+ /** Checks if a replaceable event exists */
26
+ hasReplaceable(kind: number, pubkey: string, identifier?: string): Promise<boolean>;
27
+ /** Returns all the versions of a replaceable event */
28
+ getReplaceableHistory(kind: number, pubkey: string, identifier?: string): Promise<NostrEvent[] | undefined>;
29
+ /** Get all events that match the filters (supports NIP-50 search field) */
30
+ getByFilters(filters: Filter | Filter[]): Promise<NostrEvent[]>;
31
+ /** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
32
+ getTimeline(filters: Filter | Filter[]): Promise<NostrEvent[]>;
33
+ /** Close the database connection */
34
+ close(): Promise<void>;
35
+ [Symbol.dispose](): void;
36
+ }
@@ -0,0 +1,90 @@
1
+ import { logger } from "applesauce-core";
2
+ import { createTables, deleteEvent, deleteEventsByFilters, getEvent, getEventsByFilters, getReplaceable, getReplaceableHistory, hasEvent, hasReplaceable, insertEvent, } from "./methods.js";
3
+ const log = logger.extend("TursoWasmEventDatabase");
4
+ export class TursoWasmEventDatabase {
5
+ db;
6
+ constructor(database, _options) {
7
+ this.db = database;
8
+ }
9
+ /** Create a TursoWasmEventDatabase from a database and initialize it */
10
+ static async fromDatabase(database, options) {
11
+ const eventDatabase = new TursoWasmEventDatabase(database, options);
12
+ return await eventDatabase.initialize();
13
+ }
14
+ /** Initialize the database by creating tables and indexes */
15
+ async initialize() {
16
+ await createTables(this.db);
17
+ return this;
18
+ }
19
+ /** Store a Nostr event in the database */
20
+ async add(event) {
21
+ try {
22
+ await insertEvent(this.db, event);
23
+ return event;
24
+ }
25
+ catch (error) {
26
+ log("Error inserting event:", error);
27
+ throw error;
28
+ }
29
+ }
30
+ /** Delete an event by ID */
31
+ async remove(id) {
32
+ try {
33
+ // Remove event from database
34
+ return await deleteEvent(this.db, id);
35
+ }
36
+ catch (error) {
37
+ return false;
38
+ }
39
+ }
40
+ /** Remove multiple events that match the given filters */
41
+ async removeByFilters(filters) {
42
+ return await deleteEventsByFilters(this.db, filters);
43
+ }
44
+ /** Checks if an event exists */
45
+ async hasEvent(id) {
46
+ return await hasEvent(this.db, id);
47
+ }
48
+ /** Get an event by its ID */
49
+ async getEvent(id) {
50
+ return await getEvent(this.db, id);
51
+ }
52
+ /** Get the latest replaceable event For replaceable events (10000-19999 and 30000-39999), returns the most recent event */
53
+ async getReplaceable(kind, pubkey, identifier = "") {
54
+ return await getReplaceable(this.db, kind, pubkey, identifier);
55
+ }
56
+ /** Checks if a replaceable event exists */
57
+ async hasReplaceable(kind, pubkey, identifier = "") {
58
+ return await hasReplaceable(this.db, kind, pubkey, identifier);
59
+ }
60
+ /** Returns all the versions of a replaceable event */
61
+ async getReplaceableHistory(kind, pubkey, identifier = "") {
62
+ return await getReplaceableHistory(this.db, kind, pubkey, identifier);
63
+ }
64
+ /** Get all events that match the filters (supports NIP-50 search field) */
65
+ async getByFilters(filters) {
66
+ // If search is disabled, remove the search field from the filters
67
+ if (Array.isArray(filters) ? filters.some((f) => "search" in f) : "search" in filters)
68
+ throw new Error("Search is not supported");
69
+ return await getEventsByFilters(this.db, filters);
70
+ }
71
+ /** Get a timeline of events that match the filters (returns array in chronological order, supports NIP-50 search) */
72
+ async getTimeline(filters) {
73
+ if (Array.isArray(filters) ? filters.some((f) => "search" in f) : "search" in filters)
74
+ throw new Error("Search is not supported");
75
+ // No need to sort since query defaults to created_at descending order
76
+ return await this.getByFilters(filters);
77
+ }
78
+ /** Close the database connection */
79
+ async close() {
80
+ log("Closing database connection");
81
+ await this.db.close();
82
+ }
83
+ [Symbol.dispose]() {
84
+ // Note: dispose is synchronous, but close is async
85
+ // This is a limitation of the dispose pattern
86
+ this.close().catch((error) => {
87
+ log("Error closing database in dispose:", error);
88
+ });
89
+ }
90
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./event-database.js";
2
+ export * from "./methods.js";
@@ -0,0 +1,2 @@
1
+ export * from "./event-database.js";
2
+ export * from "./methods.js";
@@ -0,0 +1,22 @@
1
+ import { Database } from "@tursodatabase/database-wasm";
2
+ import { Filter, NostrEvent } from "applesauce-core/helpers";
3
+ /** Create and migrate the `events`, `event_tags`, and search tables */
4
+ export declare function createTables(db: Database): Promise<void>;
5
+ /** Inserts an event into the `events`, `event_tags` */
6
+ export declare function insertEvent(db: Database, event: NostrEvent): Promise<boolean>;
7
+ /** Removes an event by id from the `events`, `event_tags` */
8
+ export declare function deleteEvent(db: Database, id: string): Promise<boolean>;
9
+ /** Checks if an event exists */
10
+ export declare function hasEvent(db: Database, id: string): Promise<boolean>;
11
+ /** Gets a single event from a database */
12
+ export declare function getEvent(db: Database, id: string): Promise<NostrEvent | undefined>;
13
+ /** Gets the latest replaceable event from a database */
14
+ export declare function getReplaceable(db: Database, kind: number, pubkey: string, identifier: string): Promise<NostrEvent | undefined>;
15
+ /** Gets the history of a replaceable event from a database */
16
+ export declare function getReplaceableHistory(db: Database, kind: number, pubkey: string, identifier: string): Promise<NostrEvent[]>;
17
+ /** Checks if a replaceable event exists in a database */
18
+ export declare function hasReplaceable(db: Database, kind: number, pubkey: string, identifier?: string): Promise<boolean>;
19
+ /** Get all events that match the filters */
20
+ export declare function getEventsByFilters(db: Database, filters: Filter | Filter[]): Promise<NostrEvent[]>;
21
+ /** Removes multiple events that match the given filters from the database */
22
+ export declare function deleteEventsByFilters(db: Database, filters: Filter | Filter[]): Promise<number>;
@@ -0,0 +1,101 @@
1
+ import { getIndexableTags, getReplaceableIdentifier } from "applesauce-core/helpers";
2
+ import { buildDeleteFiltersQuery, buildFiltersQuery, rowToEvent } from "../helpers/sql.js";
3
+ import { CREATE_EVENT_TAGS_TABLE_STATEMENT, CREATE_EVENTS_TABLE_STATEMENT, CREATE_INDEXES_STATEMENTS, DELETE_EVENT_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
+ /** Create and migrate the `events`, `event_tags`, and search tables */
5
+ export async function createTables(db) {
6
+ // Create the events table
7
+ await db.exec(CREATE_EVENTS_TABLE_STATEMENT.sql);
8
+ // Create the event_tags table
9
+ await db.exec(CREATE_EVENT_TAGS_TABLE_STATEMENT.sql);
10
+ // Create indexes
11
+ for (const indexStatement of CREATE_INDEXES_STATEMENTS) {
12
+ await db.exec(indexStatement.sql);
13
+ }
14
+ }
15
+ /** Inserts an event into the `events`, `event_tags` */
16
+ export async function insertEvent(db, event) {
17
+ const identifier = getReplaceableIdentifier(event);
18
+ return await db.transaction(async () => {
19
+ // Try to insert the main event with OR IGNORE
20
+ const result = await db
21
+ .prepare(INSERT_EVENT_STATEMENT.sql)
22
+ .run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
23
+ // If no rows were changed, the event already existed
24
+ if (result.changes === 0)
25
+ return false; // Event already exists, skip tags processing
26
+ // Event was inserted, continue with tags
27
+ const indexableTags = getIndexableTags(event);
28
+ if (indexableTags && indexableTags.size > 0) {
29
+ const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT.sql);
30
+ for (const tagString of indexableTags) {
31
+ // Parse the "tagName:tagValue" format
32
+ const [name, value] = tagString.split(":");
33
+ if (name && value)
34
+ await insertStmt.run(event.id, name, value);
35
+ }
36
+ }
37
+ return result.changes > 0;
38
+ })();
39
+ }
40
+ /** Removes an event by id from the `events`, `event_tags` */
41
+ export async function deleteEvent(db, id) {
42
+ return await db.transaction(async () => {
43
+ // Delete from events table - this will CASCADE to event_tags automatically!
44
+ // The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
45
+ // ensures that all related event_tags records are deleted automatically
46
+ const result = await db.prepare(DELETE_EVENT_STATEMENT.sql).run(id);
47
+ return result.changes > 0;
48
+ })();
49
+ }
50
+ /** Checks if an event exists */
51
+ export async function hasEvent(db, id) {
52
+ const result = await db.prepare(HAS_EVENT_STATEMENT.sql).get(id);
53
+ if (!result)
54
+ return false;
55
+ return result.count > 0;
56
+ }
57
+ /** Gets a single event from a database */
58
+ export async function getEvent(db, id) {
59
+ const row = await db.prepare(GET_EVENT_STATEMENT.sql).get(id);
60
+ return row && rowToEvent(row);
61
+ }
62
+ /** Gets the latest replaceable event from a database */
63
+ export async function getReplaceable(db, kind, pubkey, identifier) {
64
+ const row = await db.prepare(GET_REPLACEABLE_STATEMENT.sql).get(kind, pubkey, identifier);
65
+ return row && rowToEvent(row);
66
+ }
67
+ /** Gets the history of a replaceable event from a database */
68
+ export async function getReplaceableHistory(db, kind, pubkey, identifier) {
69
+ const rows = await db.prepare(GET_REPLACEABLE_HISTORY_STATEMENT.sql).all(kind, pubkey, identifier);
70
+ return rows.map(rowToEvent);
71
+ }
72
+ /** Checks if a replaceable event exists in a database */
73
+ export async function hasReplaceable(db, kind, pubkey, identifier = "") {
74
+ const result = await db.prepare(HAS_REPLACEABLE_STATEMENT.sql).get(kind, pubkey, identifier);
75
+ if (!result)
76
+ return false;
77
+ return result.count > 0;
78
+ }
79
+ /** Get all events that match the filters */
80
+ export async function getEventsByFilters(db, filters) {
81
+ const query = buildFiltersQuery(filters);
82
+ if (!query)
83
+ return [];
84
+ const rows = await db.prepare(query.sql).all(...query.params);
85
+ // Convert rows to events and add to set
86
+ return rows.map(rowToEvent);
87
+ }
88
+ /** Removes multiple events that match the given filters from the database */
89
+ export async function deleteEventsByFilters(db, filters) {
90
+ const whereClause = buildDeleteFiltersQuery(filters);
91
+ if (!whereClause)
92
+ return 0;
93
+ return await db.transaction(async () => {
94
+ // Delete from events table - this will CASCADE to event_tags automatically!
95
+ // The foreign key constraint: FOREIGN KEY (event_id) REFERENCES events(id) ON DELETE CASCADE
96
+ // ensures that all related event_tags records are deleted automatically
97
+ const deleteEventsQuery = `DELETE FROM events ${whereClause.sql}`;
98
+ const result = await db.prepare(deleteEventsQuery).run(...whereClause.params);
99
+ return result.changes;
100
+ })();
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-sqlite",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "sqlite event databases for applesauce",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -79,20 +79,43 @@
79
79
  "import": "./dist/libsql/*.js",
80
80
  "require": "./dist/libsql/*.js",
81
81
  "types": "./dist/libsql/*.d.ts"
82
+ },
83
+ "./turso-wasm": {
84
+ "import": "./dist/turso-wasm/index.js",
85
+ "require": "./dist/turso-wasm/index.js",
86
+ "types": "./dist/turso-wasm/index.d.ts"
87
+ },
88
+ "./turso-wasm/*": {
89
+ "import": "./dist/turso-wasm/*.js",
90
+ "require": "./dist/turso-wasm/*.js",
91
+ "types": "./dist/turso-wasm/*.d.ts"
92
+ },
93
+ "./turso": {
94
+ "import": "./dist/turso/index.js",
95
+ "require": "./dist/turso/index.js",
96
+ "types": "./dist/turso/index.d.ts"
97
+ },
98
+ "./turso/*": {
99
+ "import": "./dist/turso/*.js",
100
+ "require": "./dist/turso/*.js",
101
+ "types": "./dist/turso/*.d.ts"
82
102
  }
83
103
  },
84
104
  "dependencies": {
85
- "applesauce-core": "^4.0.0"
105
+ "applesauce-core": "^4.1.0"
86
106
  },
87
107
  "optionalDependencies": {
88
108
  "@libsql/client": "^0.15.15",
109
+ "@tursodatabase/database": "^0.2.2",
110
+ "@tursodatabase/database-wasm": "^0.2.2",
89
111
  "better-sqlite3": "^12.2.0"
90
112
  },
91
113
  "devDependencies": {
92
114
  "@hirez_io/observer-spy": "^2.2.0",
93
115
  "@types/better-sqlite3": "^7.6.13",
94
116
  "@types/ws": "^8.5.13",
95
- "applesauce-signers": "^4.0.0",
117
+ "applesauce-signers": "^4.1.0",
118
+ "nostr-tools": "^2.17.0",
96
119
  "rimraf": "^6.0.1",
97
120
  "typescript": "^5.7.3",
98
121
  "vitest": "^3.2.4",