diff --git a/package-lock.json b/package-lock.json index 7c4aabb..71b3783 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,6 +34,7 @@ "@ts-rest/react-query": "^3.51.0", "@types/cors": "^2.8.17", "@types/express": "^4.0.0", + "@types/lodash-es": "^4.17.12", "@types/multer": "^1.4.12", "@types/node": "^22.10.5", "@types/react": "^18.0.0", @@ -43,6 +44,7 @@ "concurrently": "^9.1.2", "date-fns": "^4.1.0", "jotai": "^2.11.0", + "lodash-es": "^4.17.21", "material-react-table": "^3.1.0", "openapi3-ts": "^4.4.0", "react": "^18.0.0", @@ -1796,6 +1798,14 @@ "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==", + "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", @@ -1948,6 +1958,21 @@ "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==", + "dev": true + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -2590,6 +2615,15 @@ "node": ">=10" } }, + "node_modules/cosmiconfig/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, + "engines": { + "node": ">= 6" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -3345,6 +3379,12 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "dev": true + }, "node_modules/long": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", @@ -3662,17 +3702,6 @@ "yaml": "^2.5.0" } }, - "node_modules/openapi3-ts/node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4819,11 +4848,14 @@ "dev": true }, "node_modules/yaml": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "bin": { + "yaml": "bin.mjs" + }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/yargs": { diff --git a/package.json b/package.json index 5f3d637..a5dc030 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@ts-rest/react-query": "^3.51.0", "@types/cors": "^2.8.17", "@types/express": "^4.0.0", + "@types/lodash-es": "^4.17.12", "@types/multer": "^1.4.12", "@types/node": "^22.10.5", "@types/react": "^18.0.0", @@ -36,6 +37,7 @@ "concurrently": "^9.1.2", "date-fns": "^4.1.0", "jotai": "^2.11.0", + "lodash-es": "^4.17.21", "material-react-table": "^3.1.0", "openapi3-ts": "^4.4.0", "react": "^18.0.0", diff --git a/src/common/contract/index.ts b/src/common/contract/index.ts index b0f45c6..0882924 100644 --- a/src/common/contract/index.ts +++ b/src/common/contract/index.ts @@ -15,6 +15,7 @@ const { GetSessionResponseSchema, GetSessionRequestPathSchema } = const { GetSessionsRequestQuerySchema, GetSessionsResponseSchema } = exportGetSessionsSchema(z); const { + LogEntrySchema, PostLogRequestBodySchema, PostLogRequestParamsSchema, SingleMetadataSchema, @@ -112,3 +113,4 @@ export const contract = c.router( export type SingleMetadata = z.infer; export type GetSessionsResponse = z.infer; export type GetSessionResponse = z.infer; +export type LogEntry = z.infer; \ No newline at end of file diff --git a/src/common/contract/post-log.ts b/src/common/contract/post-log.ts index 53337fd..4d12457 100644 --- a/src/common/contract/post-log.ts +++ b/src/common/contract/post-log.ts @@ -38,7 +38,7 @@ export function exportPostLogSchema(z: Z) { }), }); - const PostLogRequestBodySchema = z.object({ + const LogEntrySchema = z.object({ message: z.string().openapi({ title: "Message", description: "A human readable message that describes the log line", @@ -55,7 +55,10 @@ export function exportPostLogSchema(z: Z) { }), }); + const PostLogRequestBodySchema = LogEntrySchema.or(LogEntrySchema.array()); + return { + LogEntrySchema, SingleMetadataSchema, PostLogRequestParamsSchema, PostLogRequestBodySchema, diff --git a/src/server/db/migrations/2025-01-07T10-45-43.020Z.ts b/src/server/db/migrations/2025-01-07T10-45-43.020Z.ts index 94b4b89..a4e8715 100644 --- a/src/server/db/migrations/2025-01-07T10-45-43.020Z.ts +++ b/src/server/db/migrations/2025-01-07T10-45-43.020Z.ts @@ -12,12 +12,12 @@ export async function up(db: Kysely): Promise { await db.schema .createTable("log_entries") .addColumn("id", "int4", (col) => col.primaryKey().autoIncrement()) - .addColumn("session_info_id", "char(36)", (col) => - col - .notNull() - .references("session_info.id") - .onDelete("cascade") - .onUpdate("cascade") + .addColumn("session_info_id", "char(36)", (col) => col.notNull()) + .addForeignKeyConstraint( + "log_entries_session_info_id", + ["session_info_id"], + "session_info", + ["id"] ) .addColumn("message", "varchar(255)", (col) => col.notNull()) .addColumn("timestamp", "datetime", (col) => col.notNull()) @@ -39,12 +39,12 @@ export async function up(db: Kysely): Promise { await db.schema .createTable("log_metadata") .addColumn("id", "int4", (col) => col.primaryKey().autoIncrement()) - .addColumn("log_entry_id", "int4", (col) => - col - .notNull() - .references("log_entries.id") - .onDelete("cascade") - .onUpdate("cascade") + .addColumn("log_entry_id", "int4", (col) => col.notNull()) + .addForeignKeyConstraint( + "log_metadata_log_entry_id", + ["log_entry_id"], + "log_entries", + ["id"] ) .addColumn("key", "varchar(50)", (col) => col.notNull()) .addColumn("value", "varchar(255)", (col) => col.notNull()) diff --git a/src/server/db/migrations/2025-01-07T15-46-56.493Z.ts b/src/server/db/migrations/2025-01-07T15-46-56.493Z.ts new file mode 100644 index 0000000..642ae8d --- /dev/null +++ b/src/server/db/migrations/2025-01-07T15-46-56.493Z.ts @@ -0,0 +1,34 @@ +import { Kysely } from "kysely"; + +export async function up(db: Kysely): Promise { + await db.schema + .alterTable("log_metadata") + .dropConstraint("log_metadata_log_entry_id") + .execute(); + await db.schema + .dropIndex("log_metadata_log_entry_id") + .on("log_metadata") + .execute(); + await db.schema + .alterTable("log_metadata") + .renameTo("_log_metadata_backup") + .execute(); + + await db.schema + .alterTable("log_entries") + .addColumn("metadata", "json") + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema + .alterTable("_log_metadata_backup") + .renameTo("log_metadata") + .execute(); + + await db.schema + .createIndex("log_metadata_log_entry_id") + .on("log_metadata") + .column("log_entry_id") + .execute(); +} diff --git a/src/server/db/migrations/index.ts b/src/server/db/migrations/index.ts index f7ce95e..29c59b4 100644 --- a/src/server/db/migrations/index.ts +++ b/src/server/db/migrations/index.ts @@ -1 +1,2 @@ export * as migration20250107T104543020Z from "./2025-01-07T10-45-43.020Z"; +export * as migration20250107T154656493Z from "./2025-01-07T15-46-56.493Z"; diff --git a/src/server/db/post-log.ts b/src/server/db/post-log.ts index b7083ff..8d84400 100644 --- a/src/server/db/post-log.ts +++ b/src/server/db/post-log.ts @@ -1,23 +1,37 @@ -import { SingleMetadata } from "../contract"; +import { LogEntry, SingleMetadata } from "../contract"; import { HttpError } from "../http-error"; import { db } from "./init"; +import { zip } from "lodash-es"; + +declare global { + interface BigInt { + /** Convert to BigInt to string form in JSON.stringify */ + toJSON: () => string; + } +} +BigInt.prototype.toJSON = function () { + return this.toString(); +}; export async function postLog( gameName: string, version: string, saveGuid: string, sessionGuid: string, - message: string, - category: string | undefined, - metadata: SingleMetadata[] + entries: LogEntry[] + // message: string, + // category: string | undefined, + // metadata: SingleMetadata[] ) { if ( gameName === "" || sessionGuid === "" || - message === "" || + entries.some((entry) => entry.message === "") || version === "" ) { - const errorMessage = `One of the necessary fields was missing: gameName=${gameName}, sessionGuid=${sessionGuid}, message=${message}, version=${version}`; + const errorMessage = `One of the necessary fields was missing: gameName=${gameName}, sessionGuid=${sessionGuid}, messages=${entries + .map((e) => e.message) + .join(", ")}, version=${version}`; throw new HttpError(422, errorMessage); } else { await db.transaction().execute(async (trx) => { @@ -35,26 +49,36 @@ export async function postLog( // add the main log line const insertResult = await trx .insertInto("log_entries") - .values({ - session_info_id: sessionGuid, - message: message, - timestamp: new Date(), - category, - }) - .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(); + .values( + entries.map((entry) => ({ + session_info_id: sessionGuid, + message: entry.message, + timestamp: new Date(), + category: entry.category, + })) + ) + .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/router.ts b/src/server/router.ts index 4cdaa46..b3d8109 100644 --- a/src/server/router.ts +++ b/src/server/router.ts @@ -11,15 +11,15 @@ export function installRouter(app: Express, openAPIObject: OpenAPIObject) { const s = initServer(); const router = s.router(contract, { postLog: async ({ params, body }) => { + const entries = 'length' in body ? body : [body]; + try { await postLog( params.gameName, params.version, params.saveGuid, params.sessionGuid, - body.message, - body.category, - body.metadata + entries ); return { status: 204,