diff --git a/package-lock.json b/package-lock.json index 42cc629..6505674 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "game-logger", - "version": "1.0.0", + "version": "0.0.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "game-logger", - "version": "1.0.0", + "version": "0.0.2", "license": "ISC", "dependencies": { "@ts-rest/core": "^3.51.0", @@ -21,6 +21,7 @@ "zod": "^3.24.1" }, "devDependencies": { + "@anatine/zod-openapi": "^2.2.6", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fontsource/roboto": "^5.1.1", @@ -28,6 +29,7 @@ "@mui/material": "^6.3.1", "@mui/x-date-pickers": "^7.23.3", "@tanstack/react-query": "^4.0.0", + "@ts-rest/open-api": "^3.51.0", "@ts-rest/react-query": "^3.51.0", "@types/cors": "^2.8.17", "@types/express": "^4.0.0", @@ -35,13 +37,16 @@ "@types/node": "^22.10.5", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", + "@types/swagger-ui-express": "^4.1.7", "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.1.2", "date-fns": "^4.1.0", "material-react-table": "^3.1.0", + "openapi3-ts": "^4.4.0", "react": "^18.0.0", "react-dom": "^18.0.0", "react-router": "^7.1.1", + "swagger-ui-express": "^5.0.1", "ts-node-dev": "^2.0.0", "typescript": "^5.7.2", "vite": "^6.0.7" @@ -72,6 +77,20 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@anatine/zod-openapi": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@anatine/zod-openapi/-/zod-openapi-2.2.6.tgz", + "integrity": "sha512-Z5sr2Nq2xifEpPbPdUcvyl776LY652oR3VHMV++WFSmRrRL8RDP2XTkbuGn+vgfVNOD7UrndYwCWnxaiw7IZog==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-deepmerge": "^6.0.3" + }, + "peerDependencies": { + "openapi3-ts": "^4.1.2", + "zod": "^3.20.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", @@ -1666,6 +1685,14 @@ "win32" ] }, + "node_modules/@scarf/scarf": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", + "integrity": "sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0" + }, "node_modules/@tanstack/match-sorter-utils": { "version": "8.19.4", "resolved": "https://registry.npmjs.org/@tanstack/match-sorter-utils/-/match-sorter-utils-8.19.4.tgz", @@ -1820,6 +1847,55 @@ } } }, + "node_modules/@ts-rest/open-api": { + "version": "3.51.0", + "resolved": "https://registry.npmjs.org/@ts-rest/open-api/-/open-api-3.51.0.tgz", + "integrity": "sha512-fvpvRr6HIbAMNZR//QQQi75z5qTxMEBMRtmbaBXVi5e1WVVwOK7P6YBaGWTQp6DXSvsZVULX5VZXmsDd1Z1dew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@anatine/zod-openapi": "^1.12.0", + "openapi3-ts": "^2.0.2" + }, + "peerDependencies": { + "@ts-rest/core": "~3.51.0", + "zod": "^3.22.3" + } + }, + "node_modules/@ts-rest/open-api/node_modules/@anatine/zod-openapi": { + "version": "1.14.2", + "resolved": "https://registry.npmjs.org/@anatine/zod-openapi/-/zod-openapi-1.14.2.tgz", + "integrity": "sha512-q0qHfnuNYVKu0Swrnnvfj9971AEyW7c8v9jCOZGCl5ZbyGMNG4RPyJkRcMi/JC8CRfdOe0IDfNm1nNsi2avprg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ts-deepmerge": "^6.0.3" + }, + "peerDependencies": { + "openapi3-ts": "^2.0.0 || ^3.0.0", + "zod": "^3.20.0" + } + }, + "node_modules/@ts-rest/open-api/node_modules/openapi3-ts": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-2.0.2.tgz", + "integrity": "sha512-TxhYBMoqx9frXyOgnRHufjQfPXomTIHYKhSKJ6jHfj13kS8OEIhvmE8CTuQyKtjjWttAjX5DPxM1vmalEpo8Qw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^1.10.2" + } + }, + "node_modules/@ts-rest/open-api/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/@ts-rest/react-query": { "version": "3.51.0", "resolved": "https://registry.npmjs.org/@ts-rest/react-query/-/react-query-3.51.0.tgz", @@ -2112,6 +2188,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.7.tgz", + "integrity": "sha512-ovLM9dNincXkzH4YwyYpll75vhzPBlWx6La89wwvYH7mHjVpf0X0K/vR/aUM7SRxmr5tt9z7E5XJcjQ46q+S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@vitejs/plugin-react": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.4.tgz", @@ -3892,6 +3979,16 @@ "wrappy": "1" } }, + "node_modules/openapi3-ts": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/openapi3-ts/-/openapi3-ts-4.4.0.tgz", + "integrity": "sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==", + "dev": true, + "license": "MIT", + "dependencies": { + "yaml": "^2.5.0" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4714,6 +4811,32 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/swagger-ui-dist": { + "version": "5.18.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.18.2.tgz", + "integrity": "sha512-J+y4mCw/zXh1FOj5wGJvnAajq6XgHOyywsa9yITmwxIlJbMqITq3gYRZHaeqLVH/eV/HOPphE6NjF+nbSNC5Zw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@scarf/scarf": "=1.4.0" + } + }, + "node_modules/swagger-ui-express": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-5.0.1.tgz", + "integrity": "sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "swagger-ui-dist": ">=5.0.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4746,6 +4869,16 @@ "tree-kill": "cli.js" } }, + "node_modules/ts-deepmerge": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ts-deepmerge/-/ts-deepmerge-6.2.1.tgz", + "integrity": "sha512-8CYSLazCyj0DJDpPIxOFzJG46r93uh6EynYjuey+bxcLltBeqZL7DMfaE5ZPzZNFlav7wx+2TDa/mBl8gkTYzw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14.13.1" + } + }, "node_modules/ts-node": { "version": "10.9.2", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", @@ -5114,8 +5247,6 @@ "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", "dev": true, "license": "ISC", - "optional": true, - "peer": true, "bin": { "yaml": "bin.mjs" }, diff --git a/package.json b/package.json index 1cc52ce..be75d14 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "author": "", "license": "ISC", "devDependencies": { + "@anatine/zod-openapi": "^2.2.6", "@emotion/react": "^11.14.0", "@emotion/styled": "^11.14.0", "@fontsource/roboto": "^5.1.1", @@ -24,6 +25,7 @@ "@mui/material": "^6.3.1", "@mui/x-date-pickers": "^7.23.3", "@tanstack/react-query": "^4.0.0", + "@ts-rest/open-api": "^3.51.0", "@ts-rest/react-query": "^3.51.0", "@types/cors": "^2.8.17", "@types/express": "^4.0.0", @@ -31,13 +33,16 @@ "@types/node": "^22.10.5", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", + "@types/swagger-ui-express": "^4.1.7", "@vitejs/plugin-react": "^4.3.4", "concurrently": "^9.1.2", "date-fns": "^4.1.0", "material-react-table": "^3.1.0", + "openapi3-ts": "^4.4.0", "react": "^18.0.0", "react-dom": "^18.0.0", "react-router": "^7.1.1", + "swagger-ui-express": "^5.0.1", "ts-node-dev": "^2.0.0", "typescript": "^5.7.2", "vite": "^6.0.7" diff --git a/src/common/contract.ts b/src/common/contract.ts index 278eab2..a3d02b4 100644 --- a/src/common/contract.ts +++ b/src/common/contract.ts @@ -1,65 +1,227 @@ 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().optional().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(), + 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(), - save_id: z.string(), - game_name: z.string(), - version: z.string(), - start_time: z.date(), - end_time: z.date(), - num_events: z.number().positive(), + 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 GetSessionsRequestQuery = z.object({ - skip: z.coerce.number().optional(), - count: z.coerce.number().optional(), - id: z.string().optional(), +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(), - version: z.string(), - log_entries: z.object({ - message: z.string(), - timestamp: z.date(), - metadata: z.record(z.string(), z.string()) - }).array(), + 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({ - getSessions: { - method: "GET", - path: "/api/get-sessions", - responses: { - 200: GetSessionsResponseSchema, +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" }, - query: GetSessionsRequestQuery, - summary: "get all the sessions", }, - getSession: { - method: "GET", - path: "/api/get-session/:id", - responses: { - 200: GetSessionResponseSchema, - }, - summary: "get a session by id", + { + pathPrefix: "/api", } -}); +); diff --git a/src/server/db/post-log.ts b/src/server/db/post-log.ts new file mode 100644 index 0000000..c587f30 --- /dev/null +++ b/src/server/db/post-log.ts @@ -0,0 +1,58 @@ +import { SingleMetadata } from "../contract"; +import { HttpError } from "../http-error"; +import { db } from "./init"; + +export async function postLog( + gameName: string, + version: string, + saveGuid: string | undefined, + sessionGuid: string, + message: string, + metadata: SingleMetadata[] +) { + if ( + gameName === "" || + sessionGuid === "" || + message === "" || + version === "" + ) { + const errorMessage = `One of the necessary fields was missing: gameName=${gameName}, sessionGuid=${sessionGuid}, message=${message}, version=${version}`; + throw new HttpError(422, errorMessage); + } else { + await db.transaction().execute(async (trx) => { + // upsert the session info + await trx + .insertInto("session_info") + .ignore() + .values({ + id: sessionGuid, + game_name: gameName, + save_id: saveGuid || "", + version, + }) + .execute(); + // add the main log line + const insertResult = await trx + .insertInto("log_entries") + .values({ + session_info_id: sessionGuid, + message: message, + timestamp: new Date(), + }) + .executeTakeFirstOrThrow(); + // 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/http-error.ts b/src/server/http-error.ts new file mode 100644 index 0000000..e1859fe --- /dev/null +++ b/src/server/http-error.ts @@ -0,0 +1,13 @@ +export class HttpError extends Error { + private _code: number; + + constructor(code: number, msg: string) { + super(msg); + this._code = code; + Object.setPrototypeOf(this, HttpError); + } + + public get code() { + return this._code; + } +} \ No newline at end of file diff --git a/src/server/index.ts b/src/server/index.ts index b3105a7..63d1ec8 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,13 +1,17 @@ import "dotenv/config"; import express from "express"; -import cors from "cors"; import bodyParser from "body-parser"; -import { registerPostMessage } from "./post-message"; import { installRouter } from "./router"; import path from "path"; +import { installCors } from "./install-cors"; +import { installMeta } from "./install-meta"; +import { OpenAPIObject } from "openapi3-ts/oas31"; const app = express(); +// enable cors +installCors(app); + // serve che client files app.use(express.static("./dist/client")); @@ -15,11 +19,11 @@ app.use(express.static("./dist/client")); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); -// install the ts-rest router -installRouter(app); +// install the openapi/swagger/etc... metadata about the api +const openAPIObject = installMeta(app); -// register the POST in multipart to add messages -registerPostMessage(app); +// install the ts-rest router +installRouter(app, openAPIObject as unknown as OpenAPIObject /* required because of mixups in versioning betwee @ts-rest/open-api, @anatine/zop-openapi and openapi3-ts */); // handle every other GET route with index.html app.get("*", function (_req, res, next) { diff --git a/src/server/install-cors.ts b/src/server/install-cors.ts new file mode 100644 index 0000000..13d87e2 --- /dev/null +++ b/src/server/install-cors.ts @@ -0,0 +1,16 @@ +import cors, { CorsOptions } from "cors"; +import { Express } from "express"; + +export const postCorsOptions: CorsOptions = { + origin: (process.env.HTTP_POST_ALLOWED_ORIGINS || "*").split(","), + methods: ["POST", "GET"], +}; + +export function installCors(app: Express) { + console.log( + `Installing CORS; enabled for origins ${ + process.env.HTTP_POST_ALLOWED_ORIGINS + } and methods ${(postCorsOptions.methods as string[]).join(", ")}` + ); + app.use(cors(postCorsOptions)); +} diff --git a/src/server/install-meta.ts b/src/server/install-meta.ts new file mode 100644 index 0000000..00032b9 --- /dev/null +++ b/src/server/install-meta.ts @@ -0,0 +1,29 @@ +import { Express } from "express"; +import { generateOpenApi } from "@ts-rest/open-api"; +import { contract } from "../common/contract"; +import * as swaggerUi from "swagger-ui-express"; + +export function installMeta(app: Express) { + const openApiDocument = generateOpenApi( + contract, + { + info: { + title: "Game Logger API", + version: "0.1.0", + contact: { + email: "onefoxonewolf@gmail.com", + name: "owof games", + url: "https://owof.games", + }, + description: "API to write and read logs from a game", + }, + }, + { + setOperationId: true, + } + ); + + app.use("/swagger-ui", swaggerUi.serve, swaggerUi.setup(openApiDocument)); + + return openApiDocument; +} diff --git a/src/server/post-message.ts b/src/server/post-message.ts deleted file mode 100644 index be14eb8..0000000 --- a/src/server/post-message.ts +++ /dev/null @@ -1,70 +0,0 @@ -import type { Express } from "express"; -import multer from "multer"; -import cors, { type CorsOptions } from "cors"; -import { addEntry } from "./db"; - -const upload = multer(); - -const postCorsOptions: CorsOptions = { - origin: (process.env.HTTP_POST_ALLOWED_ORIGINS || "*").split(","), - methods: ["POST"], -}; - -export function registerPostMessage(app: Express) { - app.options("/", cors(postCorsOptions)); - app.post("/", cors(postCorsOptions), (req, res) => { - console.log("\nReceived logging message:"); - const metadata: [string, string][] = []; - let gameName: string = ""; - let sessionGuid: string = ""; - let message: string = ""; - let version: string = ""; - let saveGuid: string = ""; - for (const key of Object.keys(req.body)) { - const value = req.body[key]; - console.log(` ${key}: ${value}`); - switch (key) { - case "saveGuid": - saveGuid = value; - break; - case "gameName": - gameName = value; - break; - case "sessionGuid": - sessionGuid = value; - break; - case "message": - message = value; - break; - case "version": - version = value; - break; - default: - metadata.push([key, req.body[key]]); - break; - } - } - if ( - gameName === "" || - sessionGuid === "" || - message === "" || - version === "" - ) { - res.status(422); // unprocessable entity: used when the body has correct syntax but wrong semantics - const errorMessage = `One of the necessary fields was missing: gameName=${gameName}, sessionGuid=${sessionGuid}, message=${message}, version=${version}`; - console.error(errorMessage); - res.send(errorMessage); - } else { - addEntry(saveGuid, sessionGuid, gameName, version, message, metadata) - .then(() => { - res.status(204); // no content - res.send(""); - }) - .catch((err) => { - console.error(err); - res.status(500); // internal server error - res.send("Internal server error"); - }); - } - }); -} diff --git a/src/server/router.ts b/src/server/router.ts index e7c0a4a..d71ec46 100644 --- a/src/server/router.ts +++ b/src/server/router.ts @@ -3,10 +3,44 @@ import type { Express } from "express"; import { contract } from "../common/contract"; import { getSessions } from "./db/get-sessions"; import { getSession } from "./db/get-session"; +import { postLog } from "./db/post-log"; +import { HttpError } from "./http-error"; +import { OpenAPIObject } from "openapi3-ts/oas31"; -export function installRouter(app: Express) { +export function installRouter(app: Express, openAPIObject: OpenAPIObject) { const s = initServer(); const router = s.router(contract, { + postLog: async ({ params, body }) => { + try { + await postLog( + params.gameName, + params.version, + params.saveGuid, + params.sessionGuid, + body.message, + body.metadata + ); + return { + status: 204, + }; + } catch (e) { + if (e instanceof HttpError && e.code === 422) { + return { + status: 422, + body: { + message: e.message, + }, + }; + } else { + return { + status: 500, + body: { + message: String(e), + }, + }; + } + } + }, getSessions: async ({ query: { skip, count, id } }) => { const body = await getSessions(skip, count, id); return { @@ -21,6 +55,12 @@ export function installRouter(app: Express) { body, }; }, + getOpenApi: async () => { + return { + status: 200, + body: openAPIObject, + }; + }, }); createExpressEndpoints(contract, router, app);