Bsky short link service (#4542)

* bskylink: scaffold service w/ initial config and schema

* bskylink: implement link creation and redirects

* bskylink: tidy

* bskylink: tests

* bskylink: tidy, add error handler

* bskylink: add dockerfile

* bskylink: add build

* bskylink: fix some express plumbing

* bskyweb: proxy fallthrough routes to link service redirects

* bskyweb: build w/ link proxy

* Add AASA to bskylink (#4588)

---------

Co-authored-by: Hailey <me@haileyok.com>
This commit is contained in:
devin ivy 2024-06-21 12:41:06 -04:00 committed by GitHub
parent ba21fddd78
commit 55812b0394
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
29 changed files with 2118 additions and 1 deletions

174
bskylink/src/db/index.ts Normal file
View file

@ -0,0 +1,174 @@
import assert from 'assert'
import {
Kysely,
KyselyPlugin,
Migrator,
PluginTransformQueryArgs,
PluginTransformResultArgs,
PostgresDialect,
QueryResult,
RootOperationNode,
UnknownRow,
} from 'kysely'
import {default as Pg} from 'pg'
import {dbLogger as log} from '../logger.js'
import {default as migrations} from './migrations/index.js'
import {DbMigrationProvider} from './migrations/provider.js'
import {DbSchema} from './schema.js'
export class Database {
migrator: Migrator
destroyed = false
constructor(public db: Kysely<DbSchema>, public cfg: PgConfig) {
this.migrator = new Migrator({
db,
migrationTableSchema: cfg.schema,
provider: new DbMigrationProvider(migrations),
})
}
static postgres(opts: PgOptions): Database {
const {schema, url, txLockNonce} = opts
const pool =
opts.pool ??
new Pg.Pool({
connectionString: url,
max: opts.poolSize,
maxUses: opts.poolMaxUses,
idleTimeoutMillis: opts.poolIdleTimeoutMs,
})
// Select count(*) and other pg bigints as js integer
Pg.types.setTypeParser(Pg.types.builtins.INT8, n => parseInt(n, 10))
// Setup schema usage, primarily for test parallelism (each test suite runs in its own pg schema)
if (schema && !/^[a-z_]+$/i.test(schema)) {
throw new Error(`Postgres schema must only contain [A-Za-z_]: ${schema}`)
}
pool.on('error', onPoolError)
const db = new Kysely<DbSchema>({
dialect: new PostgresDialect({pool}),
})
return new Database(db, {
pool,
schema,
url,
txLockNonce,
})
}
async transaction<T>(fn: (db: Database) => Promise<T>): Promise<T> {
const leakyTxPlugin = new LeakyTxPlugin()
return this.db
.withPlugin(leakyTxPlugin)
.transaction()
.execute(txn => {
const dbTxn = new Database(txn, this.cfg)
return fn(dbTxn)
.catch(async err => {
leakyTxPlugin.endTx()
// ensure that all in-flight queries are flushed & the connection is open
await dbTxn.db.getExecutor().provideConnection(async () => {})
throw err
})
.finally(() => leakyTxPlugin.endTx())
})
}
get schema(): string | undefined {
return this.cfg.schema
}
get isTransaction() {
return this.db.isTransaction
}
assertTransaction() {
assert(this.isTransaction, 'Transaction required')
}
assertNotTransaction() {
assert(!this.isTransaction, 'Cannot be in a transaction')
}
async close(): Promise<void> {
if (this.destroyed) return
await this.db.destroy()
this.destroyed = true
}
async migrateToOrThrow(migration: string) {
if (this.schema) {
await this.db.schema.createSchema(this.schema).ifNotExists().execute()
}
const {error, results} = await this.migrator.migrateTo(migration)
if (error) {
throw error
}
if (!results) {
throw new Error('An unknown failure occurred while migrating')
}
return results
}
async migrateToLatestOrThrow() {
if (this.schema) {
await this.db.schema.createSchema(this.schema).ifNotExists().execute()
}
const {error, results} = await this.migrator.migrateToLatest()
if (error) {
throw error
}
if (!results) {
throw new Error('An unknown failure occurred while migrating')
}
return results
}
}
export default Database
export type PgConfig = {
pool: Pg.Pool
url: string
schema?: string
txLockNonce?: string
}
type PgOptions = {
url: string
pool?: Pg.Pool
schema?: string
poolSize?: number
poolMaxUses?: number
poolIdleTimeoutMs?: number
txLockNonce?: string
}
class LeakyTxPlugin implements KyselyPlugin {
private txOver = false
endTx() {
this.txOver = true
}
transformQuery(args: PluginTransformQueryArgs): RootOperationNode {
if (this.txOver) {
throw new Error('tx already failed')
}
return args.node
}
async transformResult(
args: PluginTransformResultArgs,
): Promise<QueryResult<UnknownRow>> {
return args.result
}
}
const onPoolError = (err: Error) => log.error({err}, 'db pool error')

View file

@ -0,0 +1,15 @@
import {Kysely} from 'kysely'
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable('link')
.addColumn('id', 'varchar', col => col.primaryKey())
.addColumn('type', 'smallint', col => col.notNull()) // integer enum: 1->starterpack
.addColumn('path', 'varchar', col => col.notNull())
.addUniqueConstraint('link_path_unique', ['path'])
.execute()
}
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable('link').execute()
}

View file

@ -0,0 +1,5 @@
import * as init from './001-init.js'
export default {
'001': init,
}

View file

@ -0,0 +1,8 @@
import {Migration, MigrationProvider} from 'kysely'
export class DbMigrationProvider implements MigrationProvider {
constructor(private migrations: Record<string, Migration>) {}
async getMigrations(): Promise<Record<string, Migration>> {
return this.migrations
}
}

17
bskylink/src/db/schema.ts Normal file
View file

@ -0,0 +1,17 @@
import {Selectable} from 'kysely'
export type DbSchema = {
link: Link
}
export interface Link {
id: string
type: LinkType
path: string
}
export enum LinkType {
StarterPack = 1,
}
export type LinkEntry = Selectable<Link>