Refactor db.ts to use kysely statements

This commit is contained in:
Alex Gleason 2023-08-07 00:50:12 -05:00
parent ecc9db86dd
commit 3cb5f91d3b
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7
8 changed files with 141 additions and 110 deletions

View File

@ -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);

View File

@ -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: {} });

View File

@ -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<URL>((value) => value instanceof URL)
path: ['resource', 'acct'],
});
function handleAcct(c: AppContext, resource: URL): Response {
async function handleAcct(c: AppContext, resource: URL): Promise<Response> {
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);

164
src/db.ts
View File

@ -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<User, 'pubkey' | 'username'>): 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<Tables>({
dialect: new DenoSqliteDialect({
database: sqlite,
}),
});
getFilter<K extends number = number>(_filter: Filter<K>) {
// TODO
}
}
const db = new DittoDB(
new Sqlite('data/db.sqlite3'),
);
export { db };
export { db, type EventRow, type TagRow, type UserRow };

39
src/db/events.ts Normal file
View File

@ -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<void> {
return db.transaction().execute(async (trx) => {
await trx.insertInto('events')
.values({
...event,
tags: JSON.stringify(event.tags),
})
.executeTakeFirst();
const tags = event.tags.reduce<Insertable<TagRow>[]>((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<K extends number = number>(_filter: Filter<K>) {
// TODO
}
export { getFilter, insertEvent };

27
src/db/users.ts Normal file
View File

@ -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<UserRow>) {
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<Insertable<UserRow>>) {
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 };

View File

@ -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';

View File

@ -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);
}