feat: use JSON column + bulk update support.
This commit is contained in:
@@ -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({
|
||||
|
||||
@@ -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] }),
|
||||
{}
|
||||
),
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -13,22 +13,11 @@ export async function getSession(id: string): Promise<GetSessionResponse> {
|
||||
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<GetSessionResponse> {
|
||||
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] }),
|
||||
{}
|
||||
),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
MigrationProvider,
|
||||
Migrator,
|
||||
MysqlDialect,
|
||||
ParseJSONResultsPlugin,
|
||||
} from "kysely";
|
||||
import { env } from "process";
|
||||
|
||||
@@ -42,6 +43,8 @@ export const db = new Kysely<Database>({
|
||||
});
|
||||
}
|
||||
},
|
||||
// MariaDB's json columns are actually just LONGTEXT columns
|
||||
plugins: [new ParseJSONResultsPlugin()],
|
||||
});
|
||||
|
||||
import * as migrations from "./migrations";
|
||||
|
||||
12
src/server/db/kysely-mariadb-json.ts
Normal file
12
src/server/db/kysely-mariadb-json.ts
Normal file
@@ -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<T>(value: T): RawBuilder<T> {
|
||||
const json = JSON.stringify(value);
|
||||
return sql`${json}`;
|
||||
}
|
||||
@@ -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();
|
||||
// }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<SessionInfoTable>;
|
||||
export type NewSessionInfoTable = Insertable<SessionInfoTable>;
|
||||
|
||||
/*
|
||||
* LOG ENTRIES
|
||||
*/
|
||||
|
||||
export interface LogEntriesTable
|
||||
{
|
||||
id: Generated<number>;
|
||||
@@ -13,6 +35,7 @@ export interface LogEntriesTable
|
||||
message: string;
|
||||
timestamp: Date;
|
||||
category?: string;
|
||||
metadata: [string, string][];
|
||||
}
|
||||
|
||||
export type LogEntry = Selectable<LogEntriesTable>;
|
||||
@@ -27,15 +50,4 @@ export interface LogMetadataTable
|
||||
}
|
||||
|
||||
export type LogMetadata = Selectable<LogMetadataTable>;
|
||||
export type NewLogMetadata = Insertable<LogMetadataTable>;
|
||||
|
||||
export interface SessionInfoTable
|
||||
{
|
||||
id: string;
|
||||
save_id: string;
|
||||
game_name: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
export type SessionInfo = Selectable<SessionInfoTable>;
|
||||
export type NewSessionInfoTable = Insertable<SessionInfoTable>;
|
||||
export type NewLogMetadata = Insertable<LogMetadataTable>;
|
||||
Reference in New Issue
Block a user