Refactor db.ts to use kysely statements
This commit is contained in:
parent
ecc9db86dd
commit
3cb5f91d3b
|
@ -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);
|
||||
|
|
|
@ -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: {} });
|
||||
|
||||
|
|
|
@ -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
164
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<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 };
|
||||
|
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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';
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue