atproto pastebin service: https://plonk.li
at plonkin 4.5 kB view raw
1import SqliteDb from "better-sqlite3"; 2import { randomBytes } from "crypto"; 3import e from "express"; 4 5import { 6 Kysely, 7 Migrator, 8 SqliteDialect, 9 Migration, 10 MigrationProvider, 11} from "kysely"; 12import { isNumber } from "util"; 13 14export type DatabaseSchema = { 15 paste: Paste; 16 comment: Comment; 17 auth_state: AuthState; 18 auth_session: AuthSession; 19}; 20 21export type Paste = { 22 uri: string; 23 authorDid: string; 24 shortUrl: string; 25 code: string; 26 lang: string; 27 title: string; 28 createdAt: string; 29 indexedAt: string; 30}; 31 32export type AuthSession = { 33 key: string; 34 session: AuthSessionJson; 35}; 36 37export type AuthState = { 38 key: string; 39 state: AuthStateJson; 40}; 41 42export type Comment = { 43 uri: string; 44 authorDid: string; 45 body: string; 46 createdAt: string; 47 indexedAt: string; 48 pasteUri: string; 49 pasteCid: string; 50}; 51 52type AuthSessionJson = string; 53type AuthStateJson = string; 54 55export type Schema = { 56 paste: Paste; 57 auth_session: AuthSession; 58 auth_state: AuthState; 59}; 60 61const migrations: Record<string, Migration> = {}; 62 63const migrationProvider: MigrationProvider = { 64 async getMigrations() { 65 return migrations; 66 }, 67}; 68 69migrations["001"] = { 70 async up(db: Kysely<unknown>) { 71 await db.schema 72 .createTable("paste") 73 .addColumn("uri", "varchar", (col) => col.primaryKey()) 74 .addColumn("shortUrl", "varchar", (col) => col.notNull().unique()) 75 .addColumn("authorDid", "varchar", (col) => col.notNull()) 76 .addColumn("code", "varchar", (col) => col.notNull()) 77 .addColumn("lang", "varchar") 78 .addColumn("title", "varchar", (col) => col.notNull()) 79 .addColumn("createdAt", "varchar", (col) => col.notNull()) 80 .addColumn("indexedAt", "varchar", (col) => col.notNull()) 81 .execute(); 82 83 await db.schema 84 .createTable("auth_session") 85 .addColumn("key", "varchar", (col) => col.primaryKey()) 86 .addColumn("session", "varchar", (col) => col.notNull()) 87 .execute(); 88 89 await db.schema 90 .createTable("auth_state") 91 .addColumn("key", "varchar", (col) => col.primaryKey()) 92 .addColumn("state", "varchar", (col) => col.notNull()) 93 .execute(); 94 }, 95 async down(db: Kysely<unknown>) { 96 await db.schema.dropTable("auth_state").execute(); 97 await db.schema.dropTable("auth_session").execute(); 98 await db.schema.dropTable("paste").execute(); 99 }, 100}; 101 102migrations["002"] = { 103 async up(db: Kysely<unknown>) { 104 await db.schema 105 .createTable("comment") 106 .addColumn("uri", "varchar", (col) => col.primaryKey()) 107 .addColumn("authorDid", "varchar", (col) => col.notNull()) 108 .addColumn("body", "varchar", (col) => col.notNull()) 109 .addColumn("createdAt", "varchar", (col) => col.notNull()) 110 .addColumn("indexedAt", "varchar", (col) => col.notNull()) 111 .addColumn("pasteUri", "varchar", (col) => col.notNull()) 112 .addColumn("pasteCid", "varchar", (col) => col.notNull()) 113 .execute(); 114 }, 115 async down(db: Kysely<unknown>) { 116 await db.schema.dropTable("comments").execute(); 117 }, 118}; 119 120function generateShortString(length: number): string { 121 return randomBytes(length).toString("base64url").substring(0, length); 122} 123 124export async function newShortUrl( 125 db: Database, 126 length: number = 2, 127): Promise<string> { 128 let shortUrl: string; 129 130 while (true) { 131 shortUrl = generateShortString(length); 132 133 try { 134 let exists = await db 135 .selectFrom("paste") 136 .selectAll() 137 .where("shortUrl", "=", shortUrl) 138 .limit(1) 139 .executeTakeFirst(); 140 if (!exists) { 141 break; // Break the loop if insertion is successful 142 } 143 } catch (error) { 144 // If we run out of options at the current length, increase the length 145 if (await hasExhaustedShortUrls(db, length)) { 146 length++; 147 } 148 throw error; 149 } 150 } 151 152 return shortUrl; 153} 154 155// Check if all short URLs of the current length have been exhausted 156async function hasExhaustedShortUrls( 157 db: Database, 158 length: number, 159): Promise<boolean> { 160 const totalPossible = Math.pow(64, length); 161 const count = await db 162 .selectFrom("paste") 163 .select((e) => e.fn.count("uri").as("count")) 164 .executeTakeFirst(); 165 if (isNumber(count)) { 166 return count >= totalPossible; 167 } else { 168 return true; 169 } 170} 171 172export const createDb = (location: string): Database => { 173 return new Kysely<DatabaseSchema>({ 174 dialect: new SqliteDialect({ 175 database: new SqliteDb(location), 176 }), 177 }); 178}; 179 180export const migrateToLatest = async (db: Database) => { 181 const migrator = new Migrator({ db, provider: migrationProvider }); 182 const { error } = await migrator.migrateToLatest(); 183 if (error) throw error; 184}; 185 186export type Database = Kysely<DatabaseSchema>;