import { type FinalGeneratedContents, type FinalGeneratedState, type GeneratedContents, type GeneratedState, type InProgressGeneratedContents, type InProgressGeneratedState, RolledValues, RollSelections, type RollTable, type RollTableAuthor, RollTableDatabase, type RollTableDetailsNoResults, type RollTableResult, type RollTableResultFull } from '../../common/rolltable.js'; import { type PreparedQueries, type QueryOutput, TypedDBWrapper } from './querytypes.js'; import { DatabaseQueries, } from './queries.js'; import { recordError } from '../discord/embed.js'; 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 }; } function processGeneratedRow(result: QueryOutput[number]): RollTableResult & { selected: boolean } { if (result.tableId === null) { return { full: false, table: { full: false, emoji: result.tableEmoji, title: result.tableTitle, header: result.tableHeader, ordinal: result.tableOrdinal }, text: result.resultText, selected: false }; } else if (result.mappingId === null) { return { full: false, table: { full: 'details', emoji: result.tableEmoji, header: result.tableHeader, id: result.tableId, identifier: result.tableIdentifier, name: result.tableName, ordinal: result.tableOrdinal, title: result.tableTitle }, text: result.resultText, selected: result.selected }; } else { return { full: true, table: { full: 'details', emoji: result.tableEmoji, header: result.tableHeader, id: result.tableId, identifier: result.tableIdentifier, name: result.tableName, ordinal: result.tableOrdinal, title: result.tableTitle }, author: result.authorId && result.authorName && result.authorRelation ? { id: result.authorId, name: result.authorName, url: result.authorUrl, relation: result.authorRelation } : null, set: { id: result.setId, name: result.setName, description: result.setDescription, global: !!(result.setGlobal) }, mappingId: result.mappingId, textId: result.resultId, text: result.resultText, updated: new Date(result.updated), selected: result.selected }; } } function processGeneration(results: QueryOutput, final: true): FinalGeneratedState function processGeneration(results: QueryOutput, final: false): InProgressGeneratedState function processGeneration(results: QueryOutput, final: boolean): GeneratedState function processGeneration(results: QueryOutput, final: boolean): GeneratedState { const rolled = new Map(); const selected = new Set(); for (const rawResult of results) { const processed = processGeneratedRow(rawResult); rolled.set(processed.table, processed); if (!final && processed.selected) { selected.add(processed.table); } } return final ? { final, rolled } : { final, rolled, selected }; } 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' }; } private async runGenerateFromDiscord(reroll: true, setId: string|null, contents?: InProgressGeneratedContents | null, finalize?: false): Promise private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: GeneratedContents, finalize: false): Promise private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: GeneratedContents, finalize: true): Promise private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: InProgressGeneratedContents): Promise private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: FinalGeneratedContents): Promise private async runGenerateFromDiscord(reroll: false, setId: string|null, contents: GeneratedContents, finalize?: boolean): Promise private async runGenerateFromDiscord(reroll: boolean, setId: string|null, contents?: GeneratedContents | null, finalize?: boolean): Promise { const results = await this.db.run(this.queries.generateFromDiscord({ reroll, setSnowflake: setId, original: contents ? Array.from(contents.rolled) : null, selection: contents && !contents.final ? Array.from(contents.selected) : null })) return processGeneration(results, finalize ?? contents?.final ?? 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.runGenerateFromDiscord(false, setId, contents); } async generateFromDiscordSet(setId: string): Promise { return this.runGenerateFromDiscord(true, setId); } async rerollFromDiscordSet(setId: string, existing: InProgressGeneratedContents): Promise { return this.runGenerateFromDiscord(true, setId, existing); } async reopenFromDiscordSet(setId: string, existing: FinalGeneratedContents): Promise { return this.runGenerateFromDiscord(false, setId, existing, false); } async finalizeFromDiscordSet(setId: string, existing: InProgressGeneratedContents): Promise { return this.runGenerateFromDiscord(false, setId, existing, true); } 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 getWebPageDataForDiscordSet(reroll: true, setSnowflake: string|null, oldResults?: InProgressGeneratedContents | null, finalize?: false): Promise private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults?: null): Promise private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: GeneratedContents, finalize: false): Promise private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: GeneratedContents, finalize: true): Promise private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: InProgressGeneratedContents): Promise private async getWebPageDataForDiscordSet(reroll: false, setSnowflake: string|null, oldResults: FinalGeneratedContents): Promise private async getWebPageDataForDiscordSet(reroll: boolean, setSnowflake: string|null, oldResults?: GeneratedContents|null, finalize?: boolean): Promise private async getWebPageDataForDiscordSet(reroll: boolean, setSnowflake: string|null, oldResults?: GeneratedContents|null, finalize?: boolean): Promise { const { tables, mappings, results, sets, authors } = await this.db.run(this.queries.getFullDatabaseForDiscordSet({ reroll, setSnowflake, original: oldResults ? Array.from(oldResults.rolled) : null, selection: oldResults && !oldResults.final ? Array.from(oldResults.selected) : null, })) const db = new RollTableDatabase({ tables, authors, sets, results: mappings.map(v => ({...v, updated: new Date(v.updated)}))}) if (!results) { return db } const rolled = new RolledValues() for (const result of results) { switch (result.type) { case 'mapping': const mapping = db.mappings.get(result.mappingId) if (mapping) { rolled.add(mapping) } else { recordError({ error: Error(`no mapping with ID ${result.mappingId}`), context: 'getting web page data for discord set', }) } break case 'unknownText': const table = db.tables.get(result.tableId) if (table) { rolled.add({ full: false, text: result.text, table }) } else { recordError({ error: Error(`no table with ID ${result.tableId}`), context: `assembling unknown text for discord set` }) } break case 'unknownTable': rolled.add({ full: false, table: { full: false, ordinal: result.ordinal, header: result.header, emoji: result.emoji, title: result.title }, text: result.text }) break } } if (finalize === true || (finalize === null && oldResults?.final)) { return { final: true, db, rolled } } const selected = new RollSelections() for (const table of tables) { if (table.selected) { selected.add(db.tables.get(table.id)!) } } return { final: false, db, rolled, selected } } async getGeneratorPageForDiscordSet(setSnowflake: string|null, oldResults?: InProgressGeneratedContents|null): Promise { return this.getWebPageDataForDiscordSet(true, setSnowflake, oldResults, false) } }