diff --git a/src/client/SessionDetails.tsx b/src/client/SessionDetails.tsx index adfa90c..f6854a3 100644 --- a/src/client/SessionDetails.tsx +++ b/src/client/SessionDetails.tsx @@ -1,22 +1,39 @@ import { useParams } from "react-router"; import { client } from "./client"; import { + Autocomplete, + Button, Card, + CardActions, CardContent, + CircularProgress, + Collapse, + IconButton, List, ListItem, ListItemText, Stack, + TextField, Typography, } from "@mui/material"; -import { useMemo } from "react"; import { + Fragment, + useCallback, + useMemo, + useState, +} from "react"; +import { + createMRTColumnHelper, MaterialReactTable, - MRT_ColumnDef, useMaterialReactTable, } from "material-react-table"; import { GetSessionResponse } from "./contract"; import { format } from "date-fns"; +import FileDownloadIcon from "@mui/icons-material/FileDownload"; +import { useQueryClient } from "@tanstack/react-query"; + +type LogEntry = GetSessionResponse["log_entries"][0]; +const columnHelper = createMRTColumnHelper(); // <--- pass your TData type as a generic to createMRTColumnHelper (if using TS) export function SessionDetails() { const { sessionId } = useParams<{ sessionId: string }>(); @@ -28,42 +45,49 @@ export function SessionDetails() { params: { id: sessionId }, }); - const columns = useMemo< - MRT_ColumnDef[] - >( + const columns = useMemo( () => [ - { - accessorKey: "message", + columnHelper.accessor("category", { + header: "Category", + Cell: ({ cell }) => {cell.getValue()}, + }), + columnHelper.accessor("message", { header: "Message", - }, - { - accessorKey: "timestamp", + Cell: ({ cell }) => {cell.getValue()}, + }), + columnHelper.accessor("timestamp", { header: "Timestamp", - Cell: (x) => {format(x.row.original.timestamp, "Pp")}, - }, - { - accessorKey: "metadata", + Cell: ({ cell }) => ( + {format(cell.getValue(), "Pp")} + ), + }), + columnHelper.accessor("metadata", { header: "Metadata", - Cell: (x) => { - const metadata = x.row.original.metadata; + Cell: ({ cell }) => { + const metadata = cell.getValue(); return ( {Object.keys(metadata) .sort() .map((key) => ( - {key}: {metadata[key]} + + + {key}: {metadata[key]} + + ))} ); }, - }, + }), ], [] ); const table = useMaterialReactTable({ + layoutMode: "grid", columns, data: getSessionQuery.status === "success" @@ -71,6 +95,62 @@ export function SessionDetails() { : [], }); + const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); + const toggleExportDialog = useCallback( + () => setIsExportDialogOpen((x) => !x), + [] + ); + + const getCategoriesQuery = client.getCategories.useQuery( + ["get-categories", sessionId], + { + query: { + sessionId, + }, + }, + { + enabled: isExportDialogOpen, + } + ); + + const [selectedCategory, setSelectedCategory] = useState(""); + const handleSelectedCategoryChange = useCallback( + (_: unknown, newValue: string | null) => { + setSelectedCategory(newValue || ""); + }, + [] + ); + + const queryClient = useQueryClient(); + const onExport = useCallback(() => { + client.getEntries + .fetchQuery( + queryClient, + ["get-entries", sessionId, selectedCategory], + { + params: { sessionId }, + query: { category: selectedCategory }, + }, + { + meta: { causeDownload: true }, + } + ) + .then((data) => { + const json = JSON.stringify(data.body, null, 2); + const blob = new Blob([json], { type: "application/json" }); + const url = URL.createObjectURL(blob); + + const a = document.createElement("a"); + a.setAttribute("href", url); + a.setAttribute( + "download", + `entries-${sessionId}-${selectedCategory}.json` + ); + a.click(); + }) + .catch(console.error); + }, [queryClient, sessionId, selectedCategory]); + return getSessionQuery.status === "error" ? (

Error

) : ( @@ -93,6 +173,52 @@ export function SessionDetails() { : getSessionQuery.data.body.version} + + + + + + + + + + Export + + ( + + {getCategoriesQuery.isLoading ? ( + + ) : null} + {params.InputProps.endAdornment} + + ), + }, + }} + /> + )} + /> + + + + {getSessionQuery.status === "success" && ( diff --git a/src/client/Sessions.tsx b/src/client/Sessions.tsx index ddcbd6e..b437be2 100644 --- a/src/client/Sessions.tsx +++ b/src/client/Sessions.tsx @@ -7,7 +7,7 @@ import { type MRT_ColumnDef, type MRT_PaginationState, } from "material-react-table"; -import { GetSessionsResponse } from "../common/contract"; +import { GetSessionsResponse } from "./contract"; import { format, formatDuration, interval, intervalToDuration } from "date-fns"; import { useMemo, useState } from "react"; import { useNavigate } from "react-router"; diff --git a/src/common/contract.ts b/src/common/contract.ts deleted file mode 100644 index 462e527..0000000 --- a/src/common/contract.ts +++ /dev/null @@ -1,227 +0,0 @@ -import { initContract } from "@ts-rest/core"; -import { z } from "zod"; -import { extendZodWithOpenApi } from "@anatine/zod-openapi"; -import { OpenAPIObject } from "openapi3-ts/oas31"; - -////// INITIALIZATION - -extendZodWithOpenApi(z); - -const c = initContract(); - -const ErrorSchema = z.object({ - code: z.number().optional().openapi({ - title: "Error code", - description: "Optional code to uniquely identify this error", - }), - message: z.string().openapi({ - title: "Error message", - description: "Human readable message to describe the error", - }), -}); - -////// POST LOG - -const SingleMetadataSchema = z.object({ - key: z.string().openapi({ - title: "Key", - description: "Name of this metadata property", - example: "attack-power", - }), - value: z.string().openapi({ - title: "Value", - description: "Value of the metadata property", - example: "923", - }), -}); - -export type SingleMetadata = z.infer; - -const PostLogRequestParamsSchema = z.object({ - gameName: z.string().openapi({ - title: "Game name", - description: "Name of the game that generated this log", - example: "Asteroid", - }), - version: z.string().openapi({ - title: "Version", - description: "Version of the game that generated this log", - example: "1.7.0", - }), - saveGuid: z.string().uuid().openapi({ - title: "Save GUID", - description: - "A unique identifier for a whole game, which can be part of a multi-session game", - example: "11111111-2222-3333-4444-555555555555", - }), - sessionGuid: z.string().uuid().openapi({ - title: "Session GUID", - description: "A unique identifier for the single play session", - example: "11111111-2222-3333-4444-555555555555", - }), -}); - -const PostLogRequestBodySchema = z.object({ - message: z.string().openapi({ - title: "Message", - description: "A human readable message that describes the log line", - example: "Clicked on the telephone", - }), - metadata: SingleMetadataSchema.array().openapi({ - title: "Metadata", - description: "Extra data saved with the log message", - }), -}); - -////// GET SESSIONS - -const GetSessionsResponseSchema = z.object({ - total: z.number().positive().openapi({ - title: "Total logs", - description: - "The total number of logs satisfying the filters, regardless of pagination", - }), - sessions: z - .object({ - id: z.string().openapi({ - title: "Session GUID", - description: "A unique identifier for the single play session", - example: "11111111-2222-3333-4444-555555555555", - }), - save_id: z.string().openapi({ - title: "Save GUID", - description: - "A unique identifier for a whole game, which can be part of a multi-session game", - example: "11111111-2222-3333-4444-555555555555", - }), - game_name: z.string().openapi({ - title: "Game name", - description: "Name of the game that generated this log", - example: "Asteroid", - }), - version: z.string().openapi({ - title: "Version", - description: "Version of the game that generated this log", - example: "1.7.0", - }), - start_time: z.date().openapi({ - title: "Start Time", - description: "Time of the first log event for this session", - example: "2025-01-06T14:46:38.000Z", - }), - end_time: z.date().openapi({ - title: "Start Time", - description: "Time of the last log event for this session", - example: "2025-01-06T14:46:38.000Z", - }), - num_events: z.number().positive().openapi({ - title: "# Events", - description: "Number of events in this session", - example: "48", - }), - }) - .array(), -}); - -export type GetSessionsResponse = z.infer; - -const GetSessionsRequestQuerySchema = z.object({ - skip: z.coerce.number().optional().openapi({ - title: "Skip", - default: - "How many log events to skip from the beginning of this session (used for pagination)", - example: "30", - }), - count: z.coerce.number().optional().openapi({ - title: "Count", - default: "How many log events to return at most (used for pagination)", - example: "10", - }), - id: z.string().optional().openapi({ - title: "Session GUID", - description: "A filter for the session GUID", - example: "11111111-2222-3333-4444-555555555555", - }), -}); - -////// GET SESSION - -const GetSessionResponseSchema = z.object({ - game_name: z.string().openapi({ - title: "Game name", - description: "Name of the game that generated this log", - example: "Asteroid", - }), - version: z.string().openapi({ - title: "Version", - description: "Version of the game that generated this log", - example: "1.7.0", - }), - log_entries: z - .object({ - message: z.string().openapi({ - title: "Message", - description: "A human readable message that describes the log line", - example: "Clicked on the telephone", - }), - timestamp: z.date().openapi({ - title: "Timestamp", - description: "Date/time when the event occoured", - example: "2025-01-06T14:46:38.000Z", - }), - metadata: z.record(z.string(), z.string()).openapi({ - title: "Metadata", - description: "Extra data saved with the log message", - }), - }) - .array(), -}); - -export type GetSessionResponse = z.infer; - -////// CONTRACT - -export const contract = c.router( - { - postLog: { - method: "POST", - path: "/logs/:gameName/:version/:saveGuid/:sessionGuid", - body: PostLogRequestBodySchema, - pathParams: PostLogRequestParamsSchema, - responses: { - 204: c.noBody(), - 422: ErrorSchema, - 500: ErrorSchema, - }, - summary: "post a new log", - }, - getSessions: { - method: "GET", - path: "/get-sessions", - responses: { - 200: GetSessionsResponseSchema, - }, - query: GetSessionsRequestQuerySchema, - summary: "get all the sessions", - }, - getSession: { - method: "GET", - path: "/get-session/:id", - responses: { - 200: GetSessionResponseSchema, - }, - summary: "get a session by id", - }, - getOpenApi: { - method: "GET", - path: "/get-openapi", - responses: { - 200: c.type(), - }, - summary: "get the OpenAPI document for this API" - }, - }, - { - pathPrefix: "/api", - } -); diff --git a/src/common/contract/get-categories.ts b/src/common/contract/get-categories.ts new file mode 100644 index 0000000..51fba2d --- /dev/null +++ b/src/common/contract/get-categories.ts @@ -0,0 +1,20 @@ +import { Z } from "./types"; + +export function exportGetCategoriesSchema(z: Z) { + const GetCategoriesRequestQuerySchema = z.object({ + sessionId: z.string().uuid().openapi({ + title: "Session GUID", + description: + "The GUID of the session where the log entries are retrieved from", + example: "11111111-2222-3333-4444-555555555555", + }), + }); + + const GetCategoriesResponseSchema = z.string().array().openapi({ + title: "Categories", + description: "The categories present for this session", + example: "UI.save-load", + }); + + return { GetCategoriesRequestQuerySchema, GetCategoriesResponseSchema }; +} diff --git a/src/common/contract/get-entries.ts b/src/common/contract/get-entries.ts new file mode 100644 index 0000000..c9b4a42 --- /dev/null +++ b/src/common/contract/get-entries.ts @@ -0,0 +1,27 @@ +import { exportGetSessionSchema } from "./get-session"; +import { Z } from "./types"; + +export function exportGetEntriesSchema(z: Z) { + const { LogEntrySchema } = exportGetSessionSchema(z); + + const GetEntriesRequestPathSchema = z.object({ + sessionId: z.string().uuid().openapi({ + title: "Session GUID", + description: + "The GUID of the session where the log entries are retrieved from", + example: "11111111-2222-3333-4444-555555555555", + }), + }); + + const GetEntriesRequestQuerySchema = z.object({ + category: z.string().optional().openapi({ + title: "Category", + description: "Optional category filter", + example: "UI.save-load", + }), + }); + + const GetEntriesResponseSchema = LogEntrySchema.array(); + + return { GetEntriesRequestPathSchema, GetEntriesRequestQuerySchema, GetEntriesResponseSchema }; +} diff --git a/src/common/contract/get-session.ts b/src/common/contract/get-session.ts new file mode 100644 index 0000000..02b4461 --- /dev/null +++ b/src/common/contract/get-session.ts @@ -0,0 +1,51 @@ +import { Z } from "./types"; + +export function exportGetSessionSchema(z: Z) { + const LogEntrySchema = z + .object({ + message: z.string().openapi({ + title: "Message", + description: "A human readable message that describes the log line", + example: "Clicked on the telephone", + }), + timestamp: z.date().openapi({ + title: "Timestamp", + description: "Date/time when the event occoured", + example: "2025-01-06T14:46:38.000Z", + }), + metadata: z.record(z.string(), z.string()).openapi({ + title: "Metadata", + description: "Extra data saved with the log message", + }), + category: z.string().optional().openapi({ + title: "Category", + description: + "An optional category to identify the family of the message", + example: "UI.save-load.loading", + }), + }); + + const GetSessionResponseSchema = z.object({ + game_name: z.string().openapi({ + title: "Game name", + description: "Name of the game that generated this log", + example: "Asteroid", + }), + version: z.string().openapi({ + title: "Version", + description: "Version of the game that generated this log", + example: "1.7.0", + }), + log_entries: LogEntrySchema.array(), + }); + + const GetSessionRequestPathSchema = z.object({ + id: z.string().uuid().openapi({ + title: "Session GUID", + description: "GUID of the session to retrieve", + example: "11111111-2222-3333-4444-555555555555", + }), + }); + + return { GetSessionResponseSchema, GetSessionRequestPathSchema, LogEntrySchema }; +} diff --git a/src/common/contract/get-sessions.ts b/src/common/contract/get-sessions.ts new file mode 100644 index 0000000..44b20c7 --- /dev/null +++ b/src/common/contract/get-sessions.ts @@ -0,0 +1,72 @@ +import { Z } from "./types"; + +export function exportGetSessionsSchema(z: Z) { + const GetSessionsResponseSchema = z.object({ + total: z.number().positive().openapi({ + title: "Total logs", + description: + "The total number of logs satisfying the filters, regardless of pagination", + }), + sessions: z + .object({ + id: z.string().openapi({ + title: "Session GUID", + description: "A unique identifier for the single play session", + example: "11111111-2222-3333-4444-555555555555", + }), + save_id: z.string().openapi({ + title: "Save GUID", + description: + "A unique identifier for a whole game, which can be part of a multi-session game", + example: "11111111-2222-3333-4444-555555555555", + }), + game_name: z.string().openapi({ + title: "Game name", + description: "Name of the game that generated this log", + example: "Asteroid", + }), + version: z.string().openapi({ + title: "Version", + description: "Version of the game that generated this log", + example: "1.7.0", + }), + start_time: z.date().openapi({ + title: "Start Time", + description: "Time of the first log event for this session", + example: "2025-01-06T14:46:38.000Z", + }), + end_time: z.date().openapi({ + title: "Start Time", + description: "Time of the last log event for this session", + example: "2025-01-06T14:46:38.000Z", + }), + num_events: z.number().positive().openapi({ + title: "# Events", + description: "Number of events in this session", + example: "48", + }), + }) + .array(), + }); + + const GetSessionsRequestQuerySchema = z.object({ + skip: z.coerce.number().optional().openapi({ + title: "Skip", + default: + "How many log events to skip from the beginning of this session (used for pagination)", + example: "30", + }), + count: z.coerce.number().optional().openapi({ + title: "Count", + default: "How many log events to return at most (used for pagination)", + example: "10", + }), + id: z.string().optional().openapi({ + title: "Session GUID", + description: "A filter for the session GUID", + example: "11111111-2222-3333-4444-555555555555", + }), + }); + + return { GetSessionsResponseSchema, GetSessionsRequestQuerySchema }; +} diff --git a/src/common/contract/index.ts b/src/common/contract/index.ts new file mode 100644 index 0000000..b0f45c6 --- /dev/null +++ b/src/common/contract/index.ts @@ -0,0 +1,114 @@ +import { initContract } from "@ts-rest/core"; +import { z } from "zod"; +import { extendZodWithOpenApi } from "@anatine/zod-openapi"; +import { OpenAPIObject } from "openapi3-ts/oas31"; +import { exportGetSessionSchema } from "./get-session"; +import { exportGetSessionsSchema } from "./get-sessions"; +import { exportPostLogSchema } from "./post-log"; +import { exportGetEntriesSchema } from "./get-entries"; +import { exportGetCategoriesSchema } from "./get-categories"; + +extendZodWithOpenApi(z); + +const { GetSessionResponseSchema, GetSessionRequestPathSchema } = + exportGetSessionSchema(z); +const { GetSessionsRequestQuerySchema, GetSessionsResponseSchema } = + exportGetSessionsSchema(z); +const { + PostLogRequestBodySchema, + PostLogRequestParamsSchema, + SingleMetadataSchema, +} = exportPostLogSchema(z); +const { + GetEntriesRequestPathSchema, + GetEntriesResponseSchema, + GetEntriesRequestQuerySchema, +} = exportGetEntriesSchema(z); +const { GetCategoriesRequestQuerySchema, GetCategoriesResponseSchema } = + exportGetCategoriesSchema(z); + +const ErrorSchema = z.object({ + code: z.number().optional().openapi({ + title: "Error code", + description: "Optional code to uniquely identify this error", + }), + message: z.string().openapi({ + title: "Error message", + description: "Human readable message to describe the error", + }), +}); + +////// CONTRACT + +const c = initContract(); + +export const contract = c.router( + { + postLog: { + method: "POST", + path: "/logs/:gameName/:version/:saveGuid/:sessionGuid", + body: PostLogRequestBodySchema, + pathParams: PostLogRequestParamsSchema, + responses: { + 204: c.noBody(), + 422: ErrorSchema, + 500: ErrorSchema, + }, + summary: "post a new log", + }, + getSessions: { + method: "GET", + path: "/get-sessions", + responses: { + 200: GetSessionsResponseSchema, + }, + query: GetSessionsRequestQuerySchema, + summary: "get all the sessions", + }, + getSession: { + method: "GET", + path: "/get-session/:id", + pathParams: GetSessionRequestPathSchema, + responses: { + 200: GetSessionResponseSchema, + }, + summary: "get a session by id", + }, + getEntries: { + method: "GET", + path: "/sessions/:sessionId/entries", + pathParams: GetEntriesRequestPathSchema, + query: GetEntriesRequestQuerySchema, + responses: { + 200: GetEntriesResponseSchema, + }, + summary: "get the entries of a session", + }, + getCategories: { + method: "GET", + path: "/categories", + query: GetCategoriesRequestQuerySchema, + responses: { + 200: GetCategoriesResponseSchema, + }, + summary: "Get the log categories present for this session", + }, + getOpenApi: { + method: "GET", + path: "/get-openapi", + responses: { + 200: c.type(), + }, + summary: "get the OpenAPI document for this API", + }, + }, + { + pathPrefix: "/api", + } +); + +///// TYPES + +export type SingleMetadata = z.infer; +export type GetSessionsResponse = z.infer; +export type GetSessionResponse = z.infer; diff --git a/src/common/contract/post-log.ts b/src/common/contract/post-log.ts new file mode 100644 index 0000000..53337fd --- /dev/null +++ b/src/common/contract/post-log.ts @@ -0,0 +1,63 @@ +import { Z } from "./types"; + +export function exportPostLogSchema(z: Z) { + const SingleMetadataSchema = z.object({ + key: z.string().openapi({ + title: "Key", + description: "Name of this metadata property", + example: "attack-power", + }), + value: z.string().openapi({ + title: "Value", + description: "Value of the metadata property", + example: "923", + }), + }); + + const PostLogRequestParamsSchema = z.object({ + gameName: z.string().openapi({ + title: "Game name", + description: "Name of the game that generated this log", + example: "Asteroid", + }), + version: z.string().openapi({ + title: "Version", + description: "Version of the game that generated this log", + example: "1.7.0", + }), + saveGuid: z.string().uuid().openapi({ + title: "Save GUID", + description: + "A unique identifier for a whole game, which can be part of a multi-session game", + example: "11111111-2222-3333-4444-555555555555", + }), + sessionGuid: z.string().uuid().openapi({ + title: "Session GUID", + description: "A unique identifier for the single play session", + example: "11111111-2222-3333-4444-555555555555", + }), + }); + + const PostLogRequestBodySchema = z.object({ + message: z.string().openapi({ + title: "Message", + description: "A human readable message that describes the log line", + example: "Clicked on the telephone", + }), + metadata: SingleMetadataSchema.array().openapi({ + title: "Metadata", + description: "Extra data saved with the log message", + }), + category: z.string().optional().openapi({ + title: "Category", + description: "An optional category to identify the family of the message", + example: "UI.save-load.loading", + }), + }); + + return { + SingleMetadataSchema, + PostLogRequestParamsSchema, + PostLogRequestBodySchema, + }; +} diff --git a/src/common/contract/types.ts b/src/common/contract/types.ts new file mode 100644 index 0000000..aacb42d --- /dev/null +++ b/src/common/contract/types.ts @@ -0,0 +1,3 @@ +import { z } from "zod"; + +export type Z = typeof z; \ No newline at end of file diff --git a/src/server/db/get-categories.ts b/src/server/db/get-categories.ts new file mode 100644 index 0000000..49e69fc --- /dev/null +++ b/src/server/db/get-categories.ts @@ -0,0 +1,12 @@ +import { db } from "./init"; + +export async function getCategories(sessionId: string) { + const result = await db + .selectFrom("log_entries") + .select("category") + .where("log_entries.session_info_id", "=", sessionId) + .where("category", "is not", null) + .distinct() + .execute(); + return result.map((entry) => entry.category!); +} diff --git a/src/server/db/get-entries.ts b/src/server/db/get-entries.ts new file mode 100644 index 0000000..e7f5216 --- /dev/null +++ b/src/server/db/get-entries.ts @@ -0,0 +1,36 @@ +import { db } from "./init"; + +export function getEntries(sessionId: string, category?: string) { + return db.transaction().execute(async (trx) => { + let query = trx + .selectFrom("log_entries") + .where("session_info_id", "=", sessionId) + .select(["id", "message", "timestamp", "category"]) + .orderBy("timestamp", "asc"); + if (category) { + query = query.where("category", "=", category); + } + + 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 }), {}), + })); + }); +} diff --git a/src/server/db/get-session.ts b/src/server/db/get-session.ts index 9879500..b4e4dea 100644 --- a/src/server/db/get-session.ts +++ b/src/server/db/get-session.ts @@ -13,7 +13,7 @@ export async function getSession(id: string): Promise { const logEntries = await db .selectFrom("log_entries") .where("session_info_id", "=", id) - .select(["id", "message", "timestamp"]) + .select(["id", "message", "timestamp", "category"]) .orderBy("timestamp", "asc") .execute(); @@ -35,6 +35,7 @@ export async function getSession(id: string): Promise { log_entries: 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 }), {}), diff --git a/src/server/db/post-log.ts b/src/server/db/post-log.ts index 4306ac0..b7083ff 100644 --- a/src/server/db/post-log.ts +++ b/src/server/db/post-log.ts @@ -8,6 +8,7 @@ export async function postLog( saveGuid: string, sessionGuid: string, message: string, + category: string | undefined, metadata: SingleMetadata[] ) { if ( @@ -38,6 +39,7 @@ export async function postLog( session_info_id: sessionGuid, message: message, timestamp: new Date(), + category, }) .executeTakeFirstOrThrow(); // add all the metadata diff --git a/src/server/db/types.ts b/src/server/db/types.ts index 59cb69c..82ea228 100644 --- a/src/server/db/types.ts +++ b/src/server/db/types.ts @@ -12,6 +12,7 @@ export interface LogEntriesTable session_info_id: string; message: string; timestamp: Date; + category?: string; } export type LogEntry = Selectable; diff --git a/src/server/router.ts b/src/server/router.ts index 06a586a..4cdaa46 100644 --- a/src/server/router.ts +++ b/src/server/router.ts @@ -1,9 +1,11 @@ import { createExpressEndpoints, initServer } from "@ts-rest/express"; -import type { Express } from "express"; +import { type Express } from "express"; import { contract } from "../common/contract"; import { HttpError } from "./http-error"; import { OpenAPIObject } from "openapi3-ts/oas31"; import { getSession, getSessions, postLog } from "./db"; +import { getEntries } from "./db/get-entries"; +import { getCategories } from "./db/get-categories"; export function installRouter(app: Express, openAPIObject: OpenAPIObject) { const s = initServer(); @@ -16,6 +18,7 @@ export function installRouter(app: Express, openAPIObject: OpenAPIObject) { params.saveGuid, params.sessionGuid, body.message, + body.category, body.metadata ); return { @@ -53,6 +56,20 @@ export function installRouter(app: Express, openAPIObject: OpenAPIObject) { body, }; }, + getEntries: async ({ params: { sessionId }, query: { category } }) => { + const body = await getEntries(sessionId, category); + return { + status: 200, + body, + }; + }, + getCategories: async ({ query: { sessionId } }) => { + const body = await getCategories(sessionId); + return { + status: 200, + body, + }; + }, getOpenApi: async () => { return { status: 200,