applesauce-sqlite 4.4.0 → 5.2.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/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # applesauce-sqlite
2
2
 
3
- A SQLite3 event database implementation for AppleSauce, providing persistent storage for Nostr events. This package extends the core `applesauce-core` functionality by replacing the default in-memory event database with a persistent SQLite database.
3
+ A SQLite3 event database implementation for Applesauce, providing persistent storage for Nostr events. This package extends the core `applesauce-core` functionality by replacing the default in-memory event database with a persistent SQLite database.
4
4
 
5
5
  ## Key Features
6
6
 
@@ -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
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";
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_WITH_IGNORE, } 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
@@ -43,7 +43,7 @@ export function insertEvent(db, event, contentFormatter) {
43
43
  // Event was inserted, continue with tags and search content
44
44
  const indexableTags = getIndexableTags(event);
45
45
  if (indexableTags && indexableTags.size > 0) {
46
- const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT.sql);
46
+ const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT_WITH_IGNORE.sql);
47
47
  for (const tagString of indexableTags) {
48
48
  // Parse the "tagName:tagValue" format
49
49
  const [name, value] = tagString.split(":");
@@ -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
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";
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_WITH_IGNORE, } 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
@@ -43,7 +43,7 @@ export function insertEvent(db, event, contentFormatter) {
43
43
  // Event was inserted, continue with tags and search content
44
44
  const indexableTags = getIndexableTags(event);
45
45
  if (indexableTags && indexableTags.size > 0) {
46
- const insertStmt = db.query(INSERT_EVENT_TAG_STATEMENT.sql);
46
+ const insertStmt = db.query(INSERT_EVENT_TAG_STATEMENT_WITH_IGNORE.sql);
47
47
  for (const tagString of indexableTags) {
48
48
  // Parse the "tagName:tagValue" format
49
49
  const [name, value] = tagString.split(":");
@@ -1,10 +1,10 @@
1
- import { FilterWithAnd, NostrEvent } from "applesauce-core/helpers";
1
+ import { Filter, 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 and NIP-ND AND operator support */
7
- export type FilterWithSearch = FilterWithAnd & {
6
+ /** Filter with search field and NIP-91 AND operator support */
7
+ export type FilterWithSearch = Filter & {
8
8
  search?: string;
9
9
  order?: "created_at" | "rank";
10
10
  };
@@ -49,7 +49,7 @@ export function buildFilterConditions(filter) {
49
49
  conditions.push(`events.created_at <= ?`);
50
50
  params.push(filter.until);
51
51
  }
52
- // Handle AND tag filters (& prefix) first - NIP-ND
52
+ // Handle AND tag filters (& prefix) first - NIP-91
53
53
  // AND takes precedence and requires ALL values to be present
54
54
  for (const [key, values] of Object.entries(filter)) {
55
55
  if (key.startsWith("&") && values && Array.isArray(values) && values.length > 0) {
@@ -69,14 +69,14 @@ export function buildFilterConditions(filter) {
69
69
  }
70
70
  }
71
71
  // Handle OR tag filters (# prefix)
72
- // Skip values that are in AND tags (NIP-ND rule)
72
+ // Skip values that are in AND tags (NIP-91 rule)
73
73
  for (const [key, values] of Object.entries(filter)) {
74
74
  if (key.startsWith("#") && values && Array.isArray(values) && values.length > 0) {
75
75
  const tagName = key.slice(1); // Remove the '#' prefix
76
76
  // Check if there's a corresponding AND filter for this tag
77
77
  const andKey = `&${tagName}`;
78
78
  const andValues = filter[andKey];
79
- // Filter out values that are in AND tags (NIP-ND rule)
79
+ // Filter out values that are in AND tags (NIP-91 rule)
80
80
  const filteredValues = andValues
81
81
  ? values.filter((v) => !andValues.includes(v))
82
82
  : values;
@@ -38,10 +38,13 @@ export declare const INSERT_EVENT_STATEMENT_WITH_IGNORE: Statement<[
38
38
  string,
39
39
  string
40
40
  ]>;
41
- /** For implementations that don't support OR IGNORE (libsql, turso-wasm) */
41
+ /** For implementations that don't support INSERT OR IGNORE (Turso embedded backends) */
42
42
  export declare const INSERT_EVENT_STATEMENT: Statement<[string, number, string, number, string, string, string, string]>;
43
43
  export declare const DELETE_EVENT_TAGS_STATEMENT: Statement<[string]>;
44
+ /** For implementations that don't support INSERT OR IGNORE (Turso embedded backends) */
44
45
  export declare const INSERT_EVENT_TAG_STATEMENT: Statement<[string, string, string]>;
46
+ /** For sqlite implementations that support idempotent tag inserts with INSERT OR IGNORE */
47
+ export declare const INSERT_EVENT_TAG_STATEMENT_WITH_IGNORE: Statement<[string, string, string]>;
45
48
  export declare const DELETE_EVENT_STATEMENT: Statement<[string]>;
46
49
  export declare const HAS_EVENT_STATEMENT: Statement<[string], {
47
50
  count: number;
@@ -3,7 +3,7 @@ export const INSERT_EVENT_STATEMENT_WITH_IGNORE = {
3
3
  sql: `INSERT OR IGNORE INTO events (id, kind, pubkey, created_at, content, tags, sig, identifier)
4
4
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
5
5
  };
6
- /** For implementations that don't support OR IGNORE (libsql, turso-wasm) */
6
+ /** For implementations that don't support INSERT OR IGNORE (Turso embedded backends) */
7
7
  export const INSERT_EVENT_STATEMENT = {
8
8
  sql: `INSERT INTO events (id, kind, pubkey, created_at, content, tags, sig, identifier)
9
9
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
@@ -11,9 +11,14 @@ export const INSERT_EVENT_STATEMENT = {
11
11
  export const DELETE_EVENT_TAGS_STATEMENT = {
12
12
  sql: `DELETE FROM event_tags WHERE event_id = ?`,
13
13
  };
14
+ /** For implementations that don't support INSERT OR IGNORE (Turso embedded backends) */
14
15
  export const INSERT_EVENT_TAG_STATEMENT = {
15
16
  sql: `INSERT INTO event_tags (event_id, tag_name, tag_value) VALUES (?, ?, ?)`,
16
17
  };
18
+ /** For sqlite implementations that support idempotent tag inserts with INSERT OR IGNORE */
19
+ export const INSERT_EVENT_TAG_STATEMENT_WITH_IGNORE = {
20
+ sql: `INSERT OR IGNORE INTO event_tags (event_id, tag_name, tag_value) VALUES (?, ?, ?)`,
21
+ };
17
22
  export const DELETE_EVENT_STATEMENT = {
18
23
  sql: `DELETE FROM events WHERE id = ?`,
19
24
  };
@@ -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
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";
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_WITH_IGNORE, } 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
@@ -68,8 +68,8 @@ export async function insertEvent(db, event, contentFormatter) {
68
68
  // Parse the "tagName:tagValue" format
69
69
  const [name, value] = tagString.split(":");
70
70
  if (name && value) {
71
- await db.execute({
72
- sql: INSERT_EVENT_TAG_STATEMENT.sql,
71
+ await transaction.execute({
72
+ sql: INSERT_EVENT_TAG_STATEMENT_WITH_IGNORE.sql,
73
73
  args: [event.id, name, value],
74
74
  });
75
75
  }
@@ -2,7 +2,7 @@ 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
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";
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_WITH_IGNORE, } 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) {
@@ -52,7 +52,7 @@ export function insertEvent(db, event, contentFormatter) {
52
52
  // Get only the indexable tags using applesauce-core helper
53
53
  const indexableTags = getIndexableTags(event);
54
54
  if (indexableTags && indexableTags.size > 0) {
55
- const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT.sql);
55
+ const insertStmt = db.prepare(INSERT_EVENT_TAG_STATEMENT_WITH_IGNORE.sql);
56
56
  for (const tagString of indexableTags) {
57
57
  // Parse the "tagName:tagValue" format
58
58
  const [name, value] = tagString.split(":");
package/dist/relay.js CHANGED
@@ -5,7 +5,7 @@ import { WebSocket, WebSocketServer } from "ws";
5
5
  import { BetterSqlite3EventDatabase } from "./better-sqlite3/event-database.js";
6
6
  // Create the event store with SQLite backend
7
7
  const database = new BetterSqlite3EventDatabase(process.env.DATABASE_PATH || ":memory:");
8
- const eventStore = new EventStore(database);
8
+ const eventStore = new EventStore({ database });
9
9
  // Set validation method for event store
10
10
  eventStore.verifyEvent = verifyEvent;
11
11
  const subscriptions = new Map();
@@ -21,7 +21,11 @@ export async function createTables(db, search = false) {
21
21
  export async function insertEvent(db, event, searchContentFormatter) {
22
22
  const identifier = getReplaceableIdentifier(event);
23
23
  return await db.transaction(async () => {
24
- // Try to insert the main event with OR IGNORE
24
+ // Turso embedded does not support INSERT OR IGNORE, so check for duplicates first
25
+ const existing = await db.prepare(HAS_EVENT_STATEMENT.sql).get(event.id);
26
+ if (existing && existing.count > 0)
27
+ return false;
28
+ // Insert the main event
25
29
  const result = await db
26
30
  .prepare(INSERT_EVENT_STATEMENT.sql)
27
31
  .run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
@@ -16,7 +16,11 @@ export async function createTables(db) {
16
16
  export async function insertEvent(db, event) {
17
17
  const identifier = getReplaceableIdentifier(event);
18
18
  return await db.transaction(async () => {
19
- // Try to insert the main event with OR IGNORE
19
+ // Turso embedded does not support INSERT OR IGNORE, so check for duplicates first
20
+ const existing = await db.prepare(HAS_EVENT_STATEMENT.sql).get(event.id);
21
+ if (existing && existing.count > 0)
22
+ return false;
23
+ // Insert the main event
20
24
  const result = await db
21
25
  .prepare(INSERT_EVENT_STATEMENT.sql)
22
26
  .run(event.id, event.kind, event.pubkey, event.created_at, event.content, JSON.stringify(event.tags), event.sig, identifier);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "applesauce-sqlite",
3
- "version": "4.4.0",
3
+ "version": "5.2.0",
4
4
  "description": "sqlite event databases for applesauce",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -102,7 +102,7 @@
102
102
  }
103
103
  },
104
104
  "dependencies": {
105
- "applesauce-core": "^4.4.0"
105
+ "applesauce-core": "^5.2.0"
106
106
  },
107
107
  "optionalDependencies": {
108
108
  "@libsql/client": "^0.15.15",
@@ -113,12 +113,11 @@
113
113
  "devDependencies": {
114
114
  "@hirez_io/observer-spy": "^2.2.0",
115
115
  "@types/better-sqlite3": "^7.6.13",
116
- "@types/ws": "^8.5.13",
117
- "applesauce-signers": "^4.1.0",
118
- "nostr-tools": "^2.17.0",
119
- "rimraf": "^6.0.1",
120
- "typescript": "^5.7.3",
121
- "vitest": "^3.2.4",
116
+ "@types/ws": "^8.18.1",
117
+ "applesauce-signers": "^5.2.0",
118
+ "rimraf": "^6.1.2",
119
+ "typescript": "^5.9.3",
120
+ "vitest": "^4.0.15",
122
121
  "vitest-websocket-mock": "^0.5.0",
123
122
  "ws": "^8.18.3"
124
123
  },