import { type FinalGeneratedContents, type FinalGeneratedState, type GeneratedContents, type GeneratedState, type InProgressGeneratedContents, type InProgressGeneratedState, RolledValues, rollOn, rollResultToString, RollSelections, type RollTable, type RollTableAuthor, RollTableDatabase, type RollTableDetailsNoResults, type RollTableResultFull, } from '../../common/rolltable'; import { type PreparedQueries, type QueryOutput, TypedDBWrapper } from './querytypes'; import { DatabaseQueries } from './queries'; function processOperationResult(result: QueryOutput<(typeof DatabaseQueries)['getResultMappingsForDiscordSet']>[number] | undefined): (RollTableResultFull & { status: 'updated' | 'existing' }) | undefined { if (!result) { return result; } return { full: true, mappingId: result.mappingId, textId: result.resultId, text: result.resultText, table: { full: 'details', id: result.tableId, identifier: result.tableIdentifier, name: result.tableName, title: result.tableTitle, emoji: result.tableEmoji, header: result.tableHeader, ordinal: result.tableOrdinal }, author: (result.authorId === null || result.authorName === null || result.authorRelation === null) ? null : { id: result.authorId, name: result.authorName, url: result.authorUrl, relation: result.authorRelation }, set: { id: result.setId, name: result.setName, description: result.setDescription, global: !!(result.setGlobal) }, updated: new Date(result.updated), status: result.status }; } export class Database { private readonly db: TypedDBWrapper; private readonly queries: PreparedQueries; constructor(db: D1Database) { this.db = new TypedDBWrapper(db); this.queries = this.db.prepareAll(DatabaseQueries); } async autocompleteTable(tableSoFar: string) { return this.db.run(this.queries.autocompleteTable({ tableIdentifierSubstring: tableSoFar })); } async autocompleteText(setSnowflake: string, tableIdentifier: string, partialText: string, includeGlobal: boolean) { return this.db.run(this.queries.autocompleteTextForDiscordSet({ setSnowflake: setSnowflake, tableIdentifierSubstring: tableIdentifier, pattern: partialText, includeGlobal })); } async addResponseFromDiscord(timestamp: number, table: string | number, text: string, userId: string, username: string, setId: string) { const [, , , , results] = await this.db.batch( this.queries.addResultForAddMapping({ tableIdentifier: table, text }), this.queries.addDiscordAuthorForAddMapping({ userSnowflake: userId, username }), this.queries.addDiscordSetForAddMapping({ setSnowflake: setId, userSnowflake: userId }), this.queries.addDiscordResultMapping({ timestamp, tableIdentifier: table, resultText: text, userSnowflake: userId, setSnowflake: setId }), this.queries.getResultMappingsForDiscordSet({ timestamp, tableIdentifier: table, text, setSnowflake: setId, includeGlobal: false }) ); const result = processOperationResult(results[0]); if (!result) { throw Error('failed adding the new response'); } return { ...result, status: result.status === 'updated' ? 'added' : 'existed' }; } async editResponseFromDiscord(timestamp: number, table: number | string, oldText: string, newText: string, userId: string, username: string, setId: string): Promise<{ status: 'nonexistent' } | { status: 'noneditable', old: RollTableResultFull } | { status: 'conflict' | 'updated', old: RollTableResultFull, new: RollTableResultFull, }> { const [oldResults, , , , newResults] = await this.db.batch( this.queries.getResultMappingsForDiscordSet({ timestamp, tableIdentifier: table, text: oldText, setSnowflake: setId, includeGlobal: true }), this.queries.addResultForEditMapping({ tableIdentifier: table, oldText, newText, setSnowflake: setId }), this.queries.addDiscordAuthorForEditMapping({ userSnowflake: userId, username, tableIdentifier: table, oldText, newText, setSnowflake: setId }), this.queries.editMappingForDiscord({ timestamp, tableIdentifier: table, oldText, newText, userSnowflake: userId, setSnowflake: setId }), this.queries.getResultMappingsForDiscordSet({ timestamp, tableIdentifier: table, text: newText, setSnowflake: setId, includeGlobal: false }) ); const oldResult = processOperationResult(oldResults[0]); if (!oldResult) { return { status: 'nonexistent' }; } if (oldResult.set?.global) { return { status: 'noneditable', old: oldResult }; } const newResult = processOperationResult(newResults[0]); if (!newResult) { throw Error('failed to update response'); } return { status: newResult.status === 'updated' ? 'updated' : 'conflict', old: oldResult, new: newResult }; } async deleteResponseFromDiscord(table: number | string, text: string, setId: string): Promise<{ status: 'nonexistent' } | { status: 'noneditable' | 'deleted', old: RollTableResultFull }> { const [oldResults, deleted] = await this.db.batch( this.queries.getResultMappingsForDiscordSet({ timestamp: null, tableIdentifier: table, text, setSnowflake: setId, includeGlobal: true }), this.queries.deleteDiscordResultMapping({ tableIdentifier: table, text, setSnowflake: setId }) ); const oldResult = processOperationResult(oldResults[0]); if (!oldResult) { return { status: 'nonexistent' }; } if (!deleted) { return { status: 'noneditable', old: oldResult }; } return { status: 'deleted', old: oldResult }; } async getResponseFromDiscord(table: number | string, text: string, setId: string): Promise<{ status: 'nonexistent' } | ({ status: 'existent', } & RollTableResultFull)> { const results = await this.db.run(this.queries.getResultMappingsForDiscordSet({ timestamp: null, tableIdentifier: table, text, setSnowflake: setId, includeGlobal: true })); const result = processOperationResult(results[0]); if (!result) { return { status: 'nonexistent' }; } return { ...result, status: 'existent' }; } async getDiscordAuthor(id: string): Promise { return await this.db.run(this.queries.getDiscordAuthor({ userSnowflake: id })); } async setDiscordAuthor(id: string, username: string, name: string | null, url: string | null): Promise { const [, result] = await this.db.batch( this.queries.setDiscordAuthor({ userSnowflake: id, username: username, name: name, url: url }), this.queries.getDiscordAuthor({ userSnowflake: id }) ); return result; } private async getGeneratorDataForDiscordSet(reroll: true, setSnowflake: string | null, oldResults?: InProgressGeneratedContents | null, finalize?: false): Promise private async getGeneratorDataForDiscordSet(reroll: false, setSnowflake: string | null, oldResults?: null): Promise private async getGeneratorDataForDiscordSet(reroll: false, setSnowflake: string | null, oldResults: GeneratedContents, finalize: false): Promise private async getGeneratorDataForDiscordSet(reroll: false, setSnowflake: string | null, oldResults: GeneratedContents, finalize: true): Promise private async getGeneratorDataForDiscordSet(reroll: false, setSnowflake: string | null, oldResults: InProgressGeneratedContents): Promise private async getGeneratorDataForDiscordSet(reroll: false, setSnowflake: string | null, oldResults: FinalGeneratedContents): Promise private async getGeneratorDataForDiscordSet(reroll: boolean, setSnowflake: string | null, oldResults: GeneratedContents, finalize?: boolean): Promise private async getGeneratorDataForDiscordSet(reroll: boolean, setSnowflake: string | null, oldResults?: GeneratedContents | null, finalize?: boolean): Promise { const oldHeaders = oldResults && oldResults.rolled.size > 0 ? Array.from(oldResults.rolled.keys()) : []; const [tables, oldKeys, oldSelection, { mappings, sets, authors }] = await this.db.batch( this.queries.getTables({}), this.queries.getTableIdsByIdentifierOrHeader({ identifiersOrHeaders: oldHeaders }), this.queries.getTableIdsByIdentifierOrHeader({ identifiersOrHeaders: oldResults && !oldResults.final && oldResults.selected.size > 0 ? Array.from(oldResults.selected) : [] }), this.queries.getFullDatabaseForDiscordSet({ setSnowflake })); const db = new RollTableDatabase({ tables: tables.map(v => ({ ...v, full: 'details' })), authors, sets, results: mappings.map(v => ({ ...v, updated: new Date(v.updated) })) }); if (!oldResults && !reroll) { return db; } const selected = new RollSelections(oldSelection.flatMap(v => { if (v === null) { return []; } const table = db.tables.get(v); if (!table) { return []; } return [table]; })); const rolled = new RolledValues(); const rollKeys = oldResults ? oldKeys : tables.map(t => t.id); for (let index = 0; index < rollKeys.length; index += 1) { const tableId = rollKeys[index]; const lookupTable = tableId !== null ? db.tables.get(tableId) : null; const oldHeader = oldHeaders[index]; const [oldEmoji, oldTitle] = oldHeader ? oldHeader.split(' ', 2) : ['', '']; const table: RollTable = lookupTable ?? { full: false, header: oldHeader, emoji: oldEmoji, title: oldTitle, ordinal: index }; const text = oldResults?.rolled.get(oldHeader); if (reroll && table.full && (!text || selected.has(table))) { const result = rollOn(table) rolled.add(result); } else if (text) { const lookupResult = text && table.full === 'results' ? table.resultsByText.get(text) : null; const result = lookupResult ?? { full: false, text: text, table } rolled.add(result); } } return (finalize ?? oldResults?.final) ? { final: true, db, rolled } : { final: false, db, rolled, selected }; } async getGeneratorPageForDiscordSet(setSnowflake: string | null, oldResults?: InProgressGeneratedContents | null): Promise { return this.getGeneratorDataForDiscordSet(true, setSnowflake, oldResults, false); } async expandFromDiscordSet(setId: string, contents: FinalGeneratedContents): Promise async expandFromDiscordSet(setId: string, contents: InProgressGeneratedContents): Promise async expandFromDiscordSet(setId: string, contents: GeneratedContents): Promise async expandFromDiscordSet(setId: string, contents: GeneratedContents): Promise { return this.getGeneratorDataForDiscordSet(false, setId, contents); } async generateFromDiscordSet(setId: string): Promise { return this.getGeneratorDataForDiscordSet(true, setId); } async rerollFromDiscordSet(setId: string, existing: InProgressGeneratedContents): Promise { return this.getGeneratorDataForDiscordSet(true, setId, existing); } async reopenFromDiscordSet(setId: string, existing: FinalGeneratedContents): Promise { return this.getGeneratorDataForDiscordSet(false, setId, existing, false); } async finalizeFromDiscordSet(setId: string, existing: InProgressGeneratedContents): Promise { return this.getGeneratorDataForDiscordSet(false, setId, existing, true); } }