import { type RollableTables, RollTable, RollTableOrder, ValueAccess } from './rolltable.js'; interface DbResponse { tableId: RollTable, text: string, } export enum UpdateResult { Updated = 0, NewConflict = 1, OldGlobal = 2, NoOldText = 3, } export enum DeleteResult { Deleted = 0, OldGlobal = 2, NoOldText = 3, } function buildRollableTables(responses: DbResponse[]): RollableTables { const out: {[key in RollTable]?: string[]} = {} for (const table of RollTableOrder) { out[table] = [] } for (const { tableId, text } of responses) { out[tableId]?.push(text) } return out as RollableTables } export class DbAccess { private readonly getResponsesInServerQuery: D1PreparedStatement private readonly getResponsesInDMQuery: D1PreparedStatement; private readonly putResponseQuery: D1PreparedStatement; private readonly checkResponseAlreadyExistsQuery: D1PreparedStatement; private readonly getResponsesGlobal: D1PreparedStatement; private readonly updateResponseQuery: D1PreparedStatement; private readonly deleteResponseQuery: D1PreparedStatement; constructor(db: D1Database) { this.getResponsesGlobal = db.prepare( `SELECT DISTINCT tableId, text FROM responses WHERE access = ${ValueAccess.Global}`) this.getResponsesInServerQuery = db.prepare( `SELECT DISTINCT tableId, text FROM responses WHERE access = ${ValueAccess.Global} OR (access = ${ValueAccess.Server} AND serverSnowflake = ?);`) this.getResponsesInDMQuery = db.prepare( `SELECT DISTINCT tableId, text FROM responses WHERE access = ${ValueAccess.Global} OR (access = ${ValueAccess.CreatorDM} AND userSnowflake = ?);`) this.putResponseQuery = db.prepare( `INSERT OR IGNORE INTO responses (id, tableId, text, timestamp, userSnowflake, serverSnowflake, access) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING timestamp, access;`) this.updateResponseQuery = db.prepare( `UPDATE responses SET text = ?3, timestamp = ?4, userSnowflake = ?5, serverSnowflake = ?6 WHERE tableId = ?1 AND text = ?2 AND ((?7 = ${ValueAccess.CreatorDM} AND access = ${ValueAccess.CreatorDM} AND userSnowflake = ?5) OR (?7 = ${ValueAccess.Server} AND access = ${ValueAccess.Server} AND serverSnowflake = ?6)) RETURNING timestamp, access;`) this.deleteResponseQuery = db.prepare( `DELETE FROM responses WHERE tableId = ?1 AND text = ?2 AND ((?5 = ${ValueAccess.CreatorDM} AND access = ${ValueAccess.CreatorDM} AND userSnowflake = ?3) OR (?5 = ${ValueAccess.Server} AND access = ${ValueAccess.Server} AND serverSnowflake = ?4)) RETURNING timestamp, access;`) this.checkResponseAlreadyExistsQuery = db.prepare( `SELECT timestamp, access FROM responses WHERE tableId = ?1 AND text = ?2 AND (access = ${ValueAccess.Global} OR (?3 = ${ValueAccess.CreatorDM} AND access = ${ValueAccess.CreatorDM} AND userSnowflake = ?4) OR (?3 = ${ValueAccess.Server} AND access = ${ValueAccess.Server} AND serverSnowflake = ?4));`) } async getGlobalResponses(): Promise { const {results} = await this.getResponsesGlobal.all() return buildRollableTables(results) } async getResponsesInServer(inServerSnowflake: string): Promise { const statement = this.getResponsesInServerQuery.bind(inServerSnowflake) const {results} = await statement.all() return buildRollableTables(results) } async getResponsesInDMWith(withUserSnowflake: string): Promise { const statement = this.getResponsesInDMQuery.bind(withUserSnowflake) const {results} = await statement.all() return buildRollableTables(results) } async putResponse(requestTimestamp: number, table: RollTable, text: string, fromUserSnowflake: string, inServerSnowflake: string|null, access?: ValueAccess): Promise<{ timestamp: number, access: ValueAccess, inserted: boolean }> { const effectiveAccess = access ?? (inServerSnowflake ? ValueAccess.Server : ValueAccess.CreatorDM) const relevantSnowflake = access === ValueAccess.Server ? inServerSnowflake : access === ValueAccess.CreatorDM ? fromUserSnowflake : null const existingResponseStatement = this.checkResponseAlreadyExistsQuery.bind(table, text, effectiveAccess, relevantSnowflake) const existingResponse = await existingResponseStatement.first<{timestamp: number, access: ValueAccess}>() if (existingResponse) { return { timestamp: existingResponse.timestamp, access: existingResponse.access, inserted: false, } } const statement = this.putResponseQuery.bind( requestTimestamp, table, text, requestTimestamp, fromUserSnowflake, inServerSnowflake, effectiveAccess) const result = await statement.first<{timestamp: number, access: ValueAccess}>() if (!result) { throw Error("no response from insert") } return { timestamp: result.timestamp, access: result.access, inserted: true } } async updateResponse(timestamp: number, table: RollTable, oldText: string, newText: string, userId: string, guildId: string | null, access?: ValueAccess): Promise<{result: UpdateResult.NoOldText|UpdateResult.Updated} | {result: UpdateResult.NewConflict, timestamp: number, access: ValueAccess} | {result: UpdateResult.OldGlobal, timestamp: number, access: ValueAccess.Global}> { const effectiveAccess = access ?? (guildId ? ValueAccess.Server : ValueAccess.CreatorDM) const relevantSnowflake = access === ValueAccess.Server ? guildId : access === ValueAccess.CreatorDM ? userId : null const existingOldResponseStatement = this.checkResponseAlreadyExistsQuery.bind(table, oldText, effectiveAccess, relevantSnowflake) const existingOldResponse = await existingOldResponseStatement.first<{timestamp: number, access: ValueAccess}>() if (!existingOldResponse) { return { result: UpdateResult.NoOldText } } else if (existingOldResponse.access === ValueAccess.Global) { return { timestamp: existingOldResponse.timestamp, access: existingOldResponse.access, result: UpdateResult.OldGlobal, } } const existingNewResponseStatement = this.checkResponseAlreadyExistsQuery.bind(table, newText, effectiveAccess, relevantSnowflake) const existingNewResponse = await existingNewResponseStatement.first<{timestamp: number, access: ValueAccess}>() if (existingNewResponse) { return { result: UpdateResult.NewConflict, timestamp: existingNewResponse.timestamp, access: existingNewResponse.access, } } const statement = this.updateResponseQuery.bind( table, oldText, newText, timestamp, userId, guildId, effectiveAccess) await statement.run() return {result: UpdateResult.Updated} } async deleteResponse(table: RollTable, text: string, userId: string, guildId: string | null, access?: ValueAccess): Promise< {result: DeleteResult.Deleted, timestamp: number, access: ValueAccess} | {result: DeleteResult.NoOldText} | {result: DeleteResult.OldGlobal, timestamp: number, access: ValueAccess.Global} > { const effectiveAccess = access ?? (guildId ? ValueAccess.Server : ValueAccess.CreatorDM) const relevantSnowflake = access === ValueAccess.Server ? guildId : access === ValueAccess.CreatorDM ? userId : null const existingOldResponseStatement = this.checkResponseAlreadyExistsQuery.bind(table, text, effectiveAccess, relevantSnowflake) const existingOldResponse = await existingOldResponseStatement.first<{timestamp: number, access: ValueAccess}>() if (!existingOldResponse) { return { result: DeleteResult.NoOldText } } else if (existingOldResponse.access === ValueAccess.Global) { return { timestamp: existingOldResponse.timestamp, access: existingOldResponse.access, result: DeleteResult.OldGlobal, } } const statement = this.deleteResponseQuery.bind( table, text, userId, guildId, effectiveAccess) const deleted = await statement.first<{timestamp: number, access: ValueAccess}>() if (!deleted) { throw Error("no response from delete") } return {result: DeleteResult.Deleted, timestamp: deleted.timestamp, access: deleted.access} } }