From 3cb5f91d3b073a4c42b92c1e0d623e0f39bcff2f Mon Sep 17 00:00:00 2001 From: Alex Gleason Date: Mon, 7 Aug 2023 00:50:12 -0500 Subject: [PATCH] Refactor db.ts to use kysely statements --- src/controllers/activitypub/actor.ts | 4 +- src/controllers/well-known/nostr.ts | 6 +- src/controllers/well-known/webfinger.ts | 5 +- src/db.ts | 164 +++++++++--------------- src/db/events.ts | 39 ++++++ src/db/users.ts | 27 ++++ src/deps.ts | 2 +- src/loopback.ts | 4 +- 8 files changed, 141 insertions(+), 110 deletions(-) create mode 100644 src/db/events.ts create mode 100644 src/db/users.ts diff --git a/src/controllers/activitypub/actor.ts b/src/controllers/activitypub/actor.ts index 0051c95..d3547ac 100644 --- a/src/controllers/activitypub/actor.ts +++ b/src/controllers/activitypub/actor.ts @@ -1,5 +1,5 @@ import { getAuthor } from '@/client.ts'; -import { db } from '@/db.ts'; +import { findUser } from '@/db/users.ts'; import { toActor } from '@/transformers/nostr-to-activitypub.ts'; import { activityJson } from '@/utils.ts'; @@ -8,7 +8,7 @@ import type { AppContext, AppController } from '@/app.ts'; const actorController: AppController = async (c) => { const username = c.req.param('username'); - const user = db.getUserByUsername(username); + const user = await findUser({ username }); if (!user) return notFound(c); const event = await getAuthor(user.pubkey); diff --git a/src/controllers/well-known/nostr.ts b/src/controllers/well-known/nostr.ts index 1e6a6d2..0d646fc 100644 --- a/src/controllers/well-known/nostr.ts +++ b/src/controllers/well-known/nostr.ts @@ -1,5 +1,5 @@ import { Conf } from '@/config.ts'; -import { db } from '@/db.ts'; +import { findUser } from '@/db/users.ts'; import { z } from '@/deps.ts'; import type { AppController } from '@/app.ts'; @@ -10,9 +10,9 @@ const nameSchema = z.string().min(1).regex(/^\w+$/); * Serves NIP-05's nostr.json. * https://github.com/nostr-protocol/nips/blob/master/05.md */ -const nostrController: AppController = (c) => { +const nostrController: AppController = async (c) => { const name = nameSchema.safeParse(c.req.query('name')); - const user = name.success ? db.getUserByUsername(name.data) : null; + const user = name.success ? await findUser({ username: name.data }) : null; if (!user) return c.json({ names: {}, relays: {} }); diff --git a/src/controllers/well-known/webfinger.ts b/src/controllers/well-known/webfinger.ts index 0f65ef6..531acbc 100644 --- a/src/controllers/well-known/webfinger.ts +++ b/src/controllers/well-known/webfinger.ts @@ -4,6 +4,7 @@ import { nip19, z } from '@/deps.ts'; import type { AppContext, AppController } from '@/app.ts'; import type { Webfinger } from '@/schemas/webfinger.ts'; +import { findUser } from '@/db/users.ts'; const webfingerQuerySchema = z.object({ resource: z.string().url(), @@ -36,14 +37,14 @@ const acctSchema = z.custom((value) => value instanceof URL) path: ['resource', 'acct'], }); -function handleAcct(c: AppContext, resource: URL): Response { +async function handleAcct(c: AppContext, resource: URL): Promise { const result = acctSchema.safeParse(resource); if (!result.success) { return c.json({ error: 'Invalid acct URI', schema: result.error }, 400); } const [username, host] = result.data; - const user = db.getUserByUsername(username); + const user = await findUser({ username }); if (!user) { return c.json({ error: 'Not found' }, 404); diff --git a/src/db.ts b/src/db.ts index 511612c..240b871 100644 --- a/src/db.ts +++ b/src/db.ts @@ -1,114 +1,78 @@ -import { type Filter, Sqlite } from '@/deps.ts'; -import { SignedEvent } from '@/event.ts'; +import { DenoSqliteDialect, Kysely, Sqlite } from '@/deps.ts'; -interface User { +interface Tables { + events: EventRow; + tags: TagRow; + users: UserRow; +} + +interface EventRow { + id: string; + kind: number; + pubkey: string; + content: string; + created_at: number; + tags: string; + sig: string; +} + +interface TagRow { + tag: string; + value_1: string | null; + value_2: string | null; + value_3: string | null; + event_id: string; +} + +interface UserRow { pubkey: string; username: string; inserted_at: Date; } -class DittoDB { - #db: Sqlite; +const sqlite = new Sqlite('data/db.sqlite3'); - constructor(db: Sqlite) { - this.#db = db; +// TODO: move this into a proper migration +sqlite.execute(` + CREATE TABLE IF NOT EXISTS events ( + id TEXT PRIMARY KEY, + kind INTEGER NOT NULL, + pubkey TEXT NOT NULL, + content TEXT NOT NULL, + created_at INTEGER NOT NULL, + tags TEXT NOT NULL, + sig TEXT NOT NULL + ); - this.#db.execute(` - CREATE TABLE IF NOT EXISTS events ( - id TEXT PRIMARY KEY, - kind INTEGER NOT NULL, - pubkey TEXT NOT NULL, - content TEXT NOT NULL, - created_at INTEGER NOT NULL, - tags TEXT NOT NULL, - sig TEXT NOT NULL - ); - - CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind); - CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey); - - CREATE TABLE IF NOT EXISTS tags ( - tag TEXT NOT NULL, - value_1 TEXT, - value_2 TEXT, - value_3 TEXT, - event_id TEXT NOT NULL, - FOREIGN KEY(event_id) REFERENCES events(id) ON DELETE CASCADE - ); - - CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag); - CREATE INDEX IF NOT EXISTS idx_tags_value_1 ON tags(value_1); - CREATE INDEX IF NOT EXISTS idx_tags_event_id ON tags(event_id); - - CREATE TABLE IF NOT EXISTS users ( - pubkey TEXT PRIMARY KEY, - username TEXT NOT NULL, - inserted_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL - ); + CREATE INDEX IF NOT EXISTS idx_events_kind ON events(kind); + CREATE INDEX IF NOT EXISTS idx_events_pubkey ON events(pubkey); - CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username); - `); - } + CREATE TABLE IF NOT EXISTS tags ( + tag TEXT NOT NULL, + value_1 TEXT, + value_2 TEXT, + value_3 TEXT, + event_id TEXT NOT NULL, + FOREIGN KEY(event_id) REFERENCES events(id) ON DELETE CASCADE + ); - insertUser(user: Pick): void { - this.#db.query( - 'INSERT INTO users(pubkey, username) VALUES (?, ?)', - [user.pubkey, user.username], - ); - } + CREATE INDEX IF NOT EXISTS idx_tags_tag ON tags(tag); + CREATE INDEX IF NOT EXISTS idx_tags_value_1 ON tags(value_1); + CREATE INDEX IF NOT EXISTS idx_tags_event_id ON tags(event_id); - getUserByUsername(username: string): User | null { - const result = this.#db.query<[string, string, Date]>( - 'SELECT pubkey, username, inserted_at FROM users WHERE username = ?', - [username], - )[0]; - if (!result) return null; - return { - pubkey: result[0], - username: result[1], - inserted_at: result[2], - }; - } + CREATE TABLE IF NOT EXISTS users ( + pubkey TEXT PRIMARY KEY, + username TEXT NOT NULL, + inserted_at DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL + ); - insertEvent(event: SignedEvent): void { - this.#db.transaction(() => { - this.#db.query( - ` - INSERT INTO events(id, kind, pubkey, content, created_at, tags, sig) - VALUES (?, ?, ?, ?, ?, ?, ?) - `, - [ - event.id, - event.kind, - event.pubkey, - event.content, - event.created_at, - JSON.stringify(event.tags), - event.sig, - ], - ); + CREATE UNIQUE INDEX IF NOT EXISTS idx_users_username ON users(username); +`); - for (const [tag, value1, value2, value3] of event.tags) { - if (['p', 'e', 'q', 'd', 't', 'proxy'].includes(tag)) { - this.#db.query( - ` - INSERT INTO tags(event_id, tag, value_1, value_2, value_3) - VALUES (?, ?, ?, ?, ?) - `, - [event.id, tag, value1 || null, value2 || null, value3 || null], - ); - } - } - }); - } +const db = new Kysely({ + dialect: new DenoSqliteDialect({ + database: sqlite, + }), +}); - getFilter(_filter: Filter) { - // TODO - } -} - -const db = new DittoDB( - new Sqlite('data/db.sqlite3'), -); - -export { db }; +export { db, type EventRow, type TagRow, type UserRow }; diff --git a/src/db/events.ts b/src/db/events.ts new file mode 100644 index 0000000..e9fdf02 --- /dev/null +++ b/src/db/events.ts @@ -0,0 +1,39 @@ +import { type Filter, type Insertable } from '@/deps.ts'; +import { type SignedEvent } from '@/event.ts'; + +import { db, type TagRow } from '../db.ts'; + +function insertEvent(event: SignedEvent): Promise { + return db.transaction().execute(async (trx) => { + await trx.insertInto('events') + .values({ + ...event, + tags: JSON.stringify(event.tags), + }) + .executeTakeFirst(); + + const tags = event.tags.reduce[]>((results, tag) => { + if (['p', 'e', 'q', 'd', 't', 'proxy'].includes(tag[0])) { + results.push({ + event_id: event.id, + tag: tag[0], + value_1: tag[1] || null, + value_2: tag[2] || null, + value_3: tag[3] || null, + }); + } + + return results; + }, []); + + await trx.insertInto('tags') + .values(tags) + .execute(); + }); +} + +function getFilter(_filter: Filter) { + // TODO +} + +export { getFilter, insertEvent }; diff --git a/src/db/users.ts b/src/db/users.ts new file mode 100644 index 0000000..632eca7 --- /dev/null +++ b/src/db/users.ts @@ -0,0 +1,27 @@ +import { type Insertable } from '@/deps.ts'; + +import { db, type UserRow } from '../db.ts'; + +/** Adds a user to the database. */ +function insertUser(user: Insertable) { + return db.insertInto('users').values(user).execute(); +} + +/** + * Finds a single user based on one or more properties. + * + * ```ts + * await findUser({ username: 'alex' }); + * ``` + */ +function findUser(user: Partial>) { + let query = db.selectFrom('users').selectAll(); + + for (const [key, value] of Object.entries(user)) { + query = query.where(key as keyof UserRow, '=', value); + } + + return query.executeTakeFirst(); +} + +export { findUser, insertUser }; diff --git a/src/deps.ts b/src/deps.ts index f5a311d..d83e674 100644 --- a/src/deps.ts +++ b/src/deps.ts @@ -50,5 +50,5 @@ export * as secp from 'npm:@noble/secp256k1@^2.0.0'; export { LRUCache } from 'npm:lru-cache@^10.0.0'; export { DB as Sqlite } from 'https://deno.land/x/sqlite@v3.7.3/mod.ts'; export * as dotenv from 'https://deno.land/std@0.197.0/dotenv/mod.ts'; -export { Kysely } from 'npm:kysely@^0.25.0'; +export { type Insertable, Kysely, type NullableInsertKeys } from 'npm:kysely@^0.25.0'; export { DenoSqliteDialect } from 'https://gitlab.com/soapbox-pub/kysely-deno-sqlite/-/raw/76748303a45fac64a889cd2b9265c6c9b8ef2e8b/mod.ts'; diff --git a/src/loopback.ts b/src/loopback.ts index af69d91..1562913 100644 --- a/src/loopback.ts +++ b/src/loopback.ts @@ -1,5 +1,5 @@ import { Conf } from '@/config.ts'; -import { db } from '@/db.ts'; +import { insertEvent } from '@/db/events.ts'; import { RelayPool } from '@/deps.ts'; import { trends } from '@/trends.ts'; import { nostrDate, nostrNow } from '@/utils.ts'; @@ -22,7 +22,7 @@ relay.subscribe( /** Handle events through the loopback pipeline. */ function handleEvent(event: SignedEvent): void { console.info('loopback event:', event.id); - db.insertEvent(event); + insertEvent(event); trackHashtags(event); }