diff --git a/bots/discord/src/classes/Database.ts b/bots/discord/src/classes/Database.ts index 46db0bc..d0b7b54 100644 --- a/bots/discord/src/classes/Database.ts +++ b/bots/discord/src/classes/Database.ts @@ -1,72 +1,97 @@ -import { Database, type SQLQueryBindings, type Statement } from 'bun:sqlite' +import { Database } from 'bun:sqlite' -export class LabeledResponseDatabase { - readonly tableName = 'labeledResponses' - readonly tableStruct = ` - reply TEXT PRIMARY KEY NOT NULL, - channel TEXT NOT NULL, - guild TEXT NOT NULL, - referenceMessage TEXT NOT NULL, - label TEXT NOT NULL, - text TEXT NOT NULL, - correctedBy TEXT, - CHECK ( - typeof("text") = 'text' AND - length("text") > 0 AND - length("text") <= 280 - ) - ` +type BasicSQLBindings = string | number | null - #statements: { - save: Statement - edit: Statement - get: Statement - } +export class BasicDatabase> { + #db: Database + #table: string - constructor() { - // TODO: put in config - const db = new Database('responses.db', { + constructor(file: string, struct: string, tableName = 'data') { + const db = new Database(file, { create: true, readwrite: true, }) - db.run(`CREATE TABLE IF NOT EXISTS ${this.tableName} ( - ${this.tableStruct} - );`) + this.#db = db + this.#table = tableName - this.#statements = { - save: db.prepare( - `INSERT INTO ${this.tableName} VALUES ($reply, $channel, $guild, $reference, $label, $text, NULL);`, - ), - edit: db.prepare( - `UPDATE ${this.tableName} SET label = $label, correctedBy = $correctedBy WHERE reply = $reply`, - ), - get: db.prepare(`SELECT * FROM ${this.tableName} WHERE reply = $reply`), - } as const + db.run(`CREATE TABLE IF NOT EXISTS ${tableName} (${struct});`) + } + + run(statement: string) { + this.#db.run(statement) + } + + prepare(statement: string) { + return this.#db.prepare(statement) + } + + query(statement: string) { + return this.#db.query(statement) + } + + insert(...values: BasicSQLBindings[]) { + this.run(`INSERT INTO ${this.#table} VALUES (${values.map(this.#encodeValue).join(', ')});`) + } + + update(what: Partial, where: string) { + const set = Object.entries(what) + .map(([key, value]) => `${key} = ${this.#encodeValue(value)}`) + .join(', ') + + this.run(`UPDATE ${this.#table} SET ${set} WHERE ${where};`) + } + + delete(where: string) { + this.run(`DELETE FROM ${this.#table} WHERE ${where};`) + } + + select(columns: string[] | string, where: string) { + const realColumns = Array.isArray(columns) ? columns.join(', ') : columns + return this.query(`SELECT ${realColumns} FROM ${this.#table} WHERE ${where};`).get() + } + + #encodeValue(value: unknown) { + if (typeof value === 'string') return `'${value}'` + if (typeof value === 'number') return value + if (typeof value === 'boolean') return value ? 1 : 0 + if (value === null) return 'NULL' + return null + } +} + +export class LabeledResponseDatabase { + #db: BasicDatabase + + constructor() { + this.#db = new BasicDatabase( + 'responses.db', + `reply TEXT PRIMARY KEY NOT NULL, + channel TEXT NOT NULL, + guild TEXT NOT NULL, + referenceMessage TEXT KEY NOT NULL, + label TEXT NOT NULL, + text TEXT NOT NULL, + correctedBy TEXT, + CHECK ( + typeof("text") = 'text' AND + length("text") > 0 AND + length("text") <= 280 + )`, + ) } save({ reply, channel, guild, referenceMessage, label, text }: Omit) { const actualText = text.slice(0, 280) - this.#statements.save.run({ - $reply: reply, - $channel: channel, - $guild: guild, - $reference: referenceMessage, - $label: label, - $text: actualText, - }) + this.#db.insert(reply, channel, guild, referenceMessage, label, actualText, null) } get(reply: string) { - return this.#statements.get.get({ $reply: reply }) + return this.#db.select('*', `reply = ${reply}`) } edit(reply: string, { label, correctedBy }: Pick) { - this.#statements.edit.run({ - $reply: reply, - $label: label, - $correctedBy: correctedBy, - }) + this.#db.update({ label, correctedBy }, `reply = ${reply}`) } }