UNPKG

@atproto/ozone

Version:

Backend service for moderating the Bluesky network.

228 lines (201 loc) 5.66 kB
import { Selectable } from 'kysely' import { Database } from '../db' import { TimeIdKeyset, paginate } from '../db/pagination' import { SetDetail } from '../db/schema/ozone_set' import { SetView } from '../lexicon/types/tools/ozone/set/defs' export type SetServiceCreator = (db: Database) => SetService export class SetService { constructor(public db: Database) {} static creator() { return (db: Database) => new SetService(db) } buildQueryForSetWithSize() { return this.db.db.selectFrom('set_detail as s').select([ 's.id', 's.name', 's.description', 's.createdAt', 's.updatedAt', (eb) => eb .selectFrom('set_value') .select((e) => e.fn.count<number>('setId').as('count')) .whereRef('setId', '=', 's.id') .as('setSize'), ]) } async query({ limit, cursor, namePrefix, sortBy, sortDirection, }: { limit: number cursor?: string namePrefix?: string sortBy: 'name' | 'createdAt' | 'updatedAt' sortDirection: 'asc' | 'desc' }): Promise<{ sets: Selectable<SetDetail & { setSize: number }>[] cursor?: string }> { let qb = this.buildQueryForSetWithSize().limit(limit) if (namePrefix) { qb = qb.where('s.name', 'like', `${namePrefix}%`) } if (cursor) { if (sortBy === 'name') { qb = qb.where('s.name', sortDirection === 'asc' ? '>' : '<', cursor) } else { qb = qb.where( `s.${sortBy}`, sortDirection === 'asc' ? '>' : '<', new Date(cursor), ) } } qb = qb.orderBy(`s.${sortBy}`, sortDirection) const sets = await qb.execute() const lastItem = sets.at(-1) return { sets, cursor: lastItem ? sortBy === 'name' ? lastItem?.name : lastItem?.[sortBy].toISOString() : undefined, } } async getByName(name: string): Promise<Selectable<SetDetail> | undefined> { const query = this.db.db .selectFrom('set_detail') .selectAll() .where('name', '=', name) return await query.executeTakeFirst() } async getByNameWithSize( name: string, ): Promise<Selectable<SetDetail & { setSize: number }> | undefined> { return await this.buildQueryForSetWithSize() .where('s.name', '=', name) .executeTakeFirst() } async getSetWithValues({ name, limit, cursor, }: { name: string limit: number cursor?: string }): Promise< | { set: Selectable<SetDetail & { setSize: number }> values: string[] cursor?: string } | undefined > { const set = await this.getByNameWithSize(name) if (!set) return undefined const { ref } = this.db.db.dynamic const qb = this.db.db .selectFrom('set_value') .selectAll() .where('setId', '=', set.id) const keyset = new TimeIdKeyset(ref(`createdAt`), ref('id')) const paginatedBuilder = paginate(qb, { limit, cursor, keyset, direction: 'asc', }) const result = await paginatedBuilder.execute() return { set, values: result.map((v) => v.value), cursor: keyset.packFromResult(result), } } async upsert({ name, description, }: Pick<SetDetail, 'name' | 'description'>): Promise<void> { await this.db.db .insertInto('set_detail') .values({ name, description, updatedAt: new Date(), }) .onConflict((oc) => { // if description is provided as a string, even an empty one, update it // otherwise, just update the updatedAt timestamp return oc.column('name').doUpdateSet( typeof description === 'string' ? { description, updatedAt: new Date(), } : { updatedAt: new Date() }, ) }) .execute() } async addValues(setId: number, values: string[]): Promise<void> { await this.db.transaction(async (txn) => { const now = new Date() const query = txn.db .insertInto('set_value') .values( values.map((value) => ({ setId, value, createdAt: now, })), ) .onConflict((oc) => oc.columns(['setId', 'value']).doNothing()) await query.execute() // Update the set's updatedAt timestamp await txn.db .updateTable('set_detail') .set({ updatedAt: now }) .where('id', '=', setId) .execute() }) } async removeValues(setId: number, values: string[]): Promise<void> { if (values.length < 1) { return } await this.db.transaction(async (txn) => { const query = txn.db .deleteFrom('set_value') .where('setId', '=', setId) .where('value', 'in', values) await query.execute() // Update the set's updatedAt timestamp await txn.db .updateTable('set_detail') .set({ updatedAt: new Date() }) .where('id', '=', setId) .execute() }) } async removeSet(setId: number): Promise<void> { await this.db.transaction(async (txn) => { await txn.db.deleteFrom('set_value').where('setId', '=', setId).execute() await txn.db.deleteFrom('set_detail').where('id', '=', setId).execute() }) } view(set: Selectable<SetDetail> & { setSize: number }): SetView { return { name: set.name, description: set.description || undefined, setSize: set.setSize, createdAt: set.createdAt.toISOString(), updatedAt: set.updatedAt.toISOString(), } } }