diff --git a/src/common/contract/get-sessions.ts b/src/common/contract/get-sessions.ts index 44b20c7..7505d9a 100644 --- a/src/common/contract/get-sessions.ts +++ b/src/common/contract/get-sessions.ts @@ -6,6 +6,7 @@ export function exportGetSessionsSchema(z: Z) { title: "Total logs", description: "The total number of logs satisfying the filters, regardless of pagination", + example: 14, }), sessions: z .object({ diff --git a/src/server/db/get-entries.ts b/src/server/db/get-entries.ts index e7f5216..c03f75e 100644 --- a/src/server/db/get-entries.ts +++ b/src/server/db/get-entries.ts @@ -5,7 +5,7 @@ export function getEntries(sessionId: string, category?: string) { let query = trx .selectFrom("log_entries") .where("session_info_id", "=", sessionId) - .select(["id", "message", "timestamp", "category"]) + .select(["id", "message", "timestamp", "category", "metadata"]) .orderBy("timestamp", "asc"); if (category) { query = query.where("category", "=", category); @@ -13,24 +13,14 @@ export function getEntries(sessionId: string, category?: string) { const logEntries = await query.execute(); - // get all the metadata for the log entries - const metadata = await trx - .selectFrom("log_metadata") - .where( - "log_entry_id", - "in", - logEntries.map((entry) => entry.id) - ) - .select(["log_entry_id", "key", "value"]) - .execute(); - return logEntries.map((entry) => ({ message: entry.message, timestamp: entry.timestamp, category: entry.category, - metadata: metadata - .filter((m) => m.log_entry_id === entry.id) - .reduce((acc, m) => ({ ...acc, [m.key]: m.value }), {}), + metadata: entry.metadata.reduce( + (acc, m) => ({ ...acc, [m[0]]: m[1] }), + {} + ), })); }); } diff --git a/src/server/db/get-session.ts b/src/server/db/get-session.ts index b4e4dea..e06f715 100644 --- a/src/server/db/get-session.ts +++ b/src/server/db/get-session.ts @@ -13,22 +13,11 @@ export async function getSession(id: string): Promise { const logEntries = await db .selectFrom("log_entries") .where("session_info_id", "=", id) - .select(["id", "message", "timestamp", "category"]) + .select(["id", "message", "timestamp", "category", "metadata"]) .orderBy("timestamp", "asc") .execute(); - // get all the metadata for the log entries - const metadata = await db - .selectFrom("log_metadata") - .where( - "log_entry_id", - "in", - logEntries.map((entry) => entry.id) - ) - .select(["log_entry_id", "key", "value"]) - .execute(); - - // buidl the object accordingly + // build the object accordingly return { game_name: session.game_name, version: session.version, @@ -36,9 +25,10 @@ export async function getSession(id: string): Promise { message: entry.message, timestamp: entry.timestamp, category: entry.category, - metadata: metadata - .filter((m) => m.log_entry_id === entry.id) - .reduce((acc, m) => ({ ...acc, [m.key]: m.value }), {}), + metadata: entry.metadata.reduce( + (acc, m) => ({ ...acc, [m[0]]: m[1] }), + {} + ), })), }; } diff --git a/src/server/db/init.ts b/src/server/db/init.ts index ab05d43..8320f41 100644 --- a/src/server/db/init.ts +++ b/src/server/db/init.ts @@ -6,6 +6,7 @@ import { MigrationProvider, Migrator, MysqlDialect, + ParseJSONResultsPlugin, } from "kysely"; import { env } from "process"; @@ -42,6 +43,8 @@ export const db = new Kysely({ }); } }, + // MariaDB's json columns are actually just LONGTEXT columns + plugins: [new ParseJSONResultsPlugin()], }); import * as migrations from "./migrations"; diff --git a/src/server/db/kysely-mariadb-json.ts b/src/server/db/kysely-mariadb-json.ts new file mode 100644 index 0000000..ecf8761 --- /dev/null +++ b/src/server/db/kysely-mariadb-json.ts @@ -0,0 +1,12 @@ +import { RawBuilder, sql } from "kysely"; + +/** + * Expression builder to wrap values that must be inserted in a JSON MariaDB column. + * MariaDB simply treat JSON as a LONGTEXT with a CHECK constraint to ensure it's a valid JSON. This means it's enough to escape a string. + * @param value The value to transform to JSON. + * @returns A builder to produce the JSON value. + */ +export function json(value: T): RawBuilder { + const json = JSON.stringify(value); + return sql`${json}`; +} diff --git a/src/server/db/post-log.ts b/src/server/db/post-log.ts index 8d84400..ed0d11e 100644 --- a/src/server/db/post-log.ts +++ b/src/server/db/post-log.ts @@ -2,6 +2,7 @@ import { LogEntry, SingleMetadata } from "../contract"; import { HttpError } from "../http-error"; import { db } from "./init"; import { zip } from "lodash-es"; +import { json } from "./kysely-mariadb-json"; declare global { interface BigInt { @@ -46,8 +47,8 @@ export async function postLog( version, }) .execute(); - // add the main log line - const insertResult = await trx + // add the main log lines with their metadata + await trx .insertInto("log_entries") .values( entries.map((entry) => ({ @@ -55,30 +56,11 @@ export async function postLog( message: entry.message, timestamp: new Date(), category: entry.category, + metadata: json(entry.metadata.map((x) => [x.key, x.value])), })) ) .returning("id") .execute(); - console.log("insertResulttt:", JSON.stringify(insertResult, null, 2)); - for (const r of insertResult) { - console.log("insertResult", r.id); - } - // console.log("insertResult.insertId", (insertResult as any).insertId); - // console.log("keys:", Object.keys(insertResult)); - throw new Error("aaa"); - // add all the metadata - // if (metadata.length > 0) { - // await trx - // .insertInto("log_metadata") - // .values( - // metadata.map(({ key, value }) => ({ - // log_entry_id: Number(insertResult.insertId), - // key, - // value, - // })) - // ) - // .execute(); - // } }); } } diff --git a/src/server/db/types.ts b/src/server/db/types.ts index 82ea228..b2df391 100644 --- a/src/server/db/types.ts +++ b/src/server/db/types.ts @@ -1,11 +1,33 @@ import { Generated, Insertable, Selectable } from "kysely"; +/* + * DATABASE + */ + export interface Database { log_entries: LogEntriesTable; - log_metadata: LogMetadataTable; session_info: SessionInfoTable; } +/* + * SESSION INFO + */ + +export interface SessionInfoTable +{ + id: string; + game_name: string; + version: string; + save_id: string; +} + +export type SessionInfo = Selectable; +export type NewSessionInfoTable = Insertable; + +/* + * LOG ENTRIES + */ + export interface LogEntriesTable { id: Generated; @@ -13,6 +35,7 @@ export interface LogEntriesTable message: string; timestamp: Date; category?: string; + metadata: [string, string][]; } export type LogEntry = Selectable; @@ -27,15 +50,4 @@ export interface LogMetadataTable } export type LogMetadata = Selectable; -export type NewLogMetadata = Insertable; - -export interface SessionInfoTable -{ - id: string; - save_id: string; - game_name: string; - version: string; -} - -export type SessionInfo = Selectable; -export type NewSessionInfoTable = Insertable; \ No newline at end of file +export type NewLogMetadata = Insertable; \ No newline at end of file