From 824166b717dae549f637d626c1f23336f9244990 Mon Sep 17 00:00:00 2001 From: Mattia Belletti Date: Mon, 7 Jul 2025 11:43:37 +0200 Subject: [PATCH] chore: added log-elements schema --- src/common/contract/get-v1-schema.ts | 217 +++++++++++++++++++++++++++ src/common/contract/index.ts | 22 ++- src/server/router.ts | 1 + 3 files changed, 239 insertions(+), 1 deletion(-) create mode 100644 src/common/contract/get-v1-schema.ts diff --git a/src/common/contract/get-v1-schema.ts b/src/common/contract/get-v1-schema.ts new file mode 100644 index 0000000..e57c85a --- /dev/null +++ b/src/common/contract/get-v1-schema.ts @@ -0,0 +1,217 @@ +import { Z } from "./types"; + +export function exportGetV1Schema(z: Z) { + const PostLogElementsMetadataNoOpenApiSchema = z + .record(z.string(), z.string().optional()) + .optional(); + /// common elements + + const PostLogElementsMetadataSchema = + PostLogElementsMetadataNoOpenApiSchema.openapi({ + title: "Metadata", + description: + "Optional metadata to provide extra information about this object", + }); + + /// session start + + const PostLogElementsUserSchema = z.object({ + platform: z.string().openapi({ + title: "Platform", + description: "Name of the platform of this user", + example: "Steam", + }), + id: z.string().openapi({ + title: "Id", + description: "Unique identifier for this user on the given platform", + example: "RedGlow", + }), + }); + + const PostLogElementsMachineSchema = z.object({ + id: z.string().openapi({ + title: "Id", + description: + "Unique identifier for this machine; it can be produced by hashing together a number of unique characteristics of the machine in a privacy-sensitive way", + example: "8662d7bdb037dc82a49d7579d9309397", + }), + metadata: PostLogElementsMetadataSchema, + }); + + const PostLogElementsGameSchema = z.object({ + author: z.string().openapi({ + title: "Author", + description: "The author of this game", + example: "owof games", + }), + name: z.string().openapi({ + title: "Name", + description: "Name of the game", + example: "The Good Dog Show", + }), + version: z.string().openapi({ + title: "Version", + description: "The version of the game", + example: "1.2.1.d352cd643ee8028826273c16af0963a4a73295b5", + }), + }); + + const PostLogElementsSessionStartSchema = z.object({ + type: z.literal("session-start"), + id: z.string().openapi({ + title: "Id", + description: "Unique id for this session", + example: "d3f53283-0e84-449c-81a0-9031b2037bfb", + }), + user: PostLogElementsUserSchema.openapi({ + title: "User", + description: "User that has started this session", + }), + machine: PostLogElementsMachineSchema.openapi({ + title: "Machine", + description: "Information about the machine this session started from", + }), + game: PostLogElementsGameSchema, + startTime: z.string().datetime().openapi({ + title: "Start timestamp", + description: "Timestamp of the start of this session", + }), + metadata: PostLogElementsMetadataSchema, + }); + + /// session end + + const PostLogElementsSessionEndSchema = z.object({ + type: z.literal("session-end"), + id: z.string().openapi({ + title: "Id", + description: + "Unique id for this session (must match the id of a start session)", + example: "d3f53283-0e84-449c-81a0-9031b2037bfb", + }), + endTime: z.string().datetime().openapi({ + title: "End timestamp", + description: + "Timestamp of the end of this session (must be greater than the start time)", + }), + }); + + /// activity start + + const PostLogElementsActivityStartSchema = z.object({ + type: z.literal("activity-start"), + id: z.string().openapi({ + title: "Id", + description: "Unique id for this activity", + example: "3fcfc0be-a430-4425-b033-daa2c8340d13", + }), + sessionId: z.string().openapi({ + title: "Id", + description: + "Unique id for the session this activity belongs to (must be the same id of a previous session-start)", + example: "d3f53283-0e84-449c-81a0-9031b2037bfb", + }), + parentId: z.string().optional().openapi({ + title: "Parent Id", + description: + "Unique id of the parent activity (an activity that contains this activity); optional", + example: "c2c62510-3b96-4be8-bad1-3acca2e559fe", + }), + saveId: z.string().optional().openapi({ + title: "Save Id", + description: + "Id of the save file that is currently being used; must be unique for the user of the session this activity belongs to; optional", + example: "1442", + }), + startTime: z.string().datetime().openapi({ + title: "Start timestamp", + description: "Timestamp of the start of this activity", + }), + metadata: PostLogElementsMetadataSchema, + }); + + /// activity end + + const PostLogElementsActivityEndSchema = z.object({ + type: z.literal("activity-end"), + id: z.string().openapi({ + title: "Id", + description: + "Unique id for this activity (must match the id of a previous start-activity)", + example: "3fcfc0be-a430-4425-b033-daa2c8340d13", + }), + saveId: z.string().optional().openapi({ + title: "Save Id", + description: + "Id of the save file that is currently being used; must be unique for the user of the session this activity belongs to; if not null, overwrites the value of the matching start-activity; optional", + example: "1442", + }), + endTime: z.string().datetime().openapi({ + title: "End timestamp", + description: + "Timestamp of the end of this activity (must be greather than the startTime of the matching start-activity)", + }), + }); + + /// log + + const PostLogElementsLogSchema = z.object({ + type: z.literal("log"), + activityId: z.string().optional().openapi({ + title: "Activity Id", + description: + "Id of the activity this log belongs to; can be null if it does not belong to an activity", + example: "3fcfc0be-a430-4425-b033-daa2c8340d13", + }), + sessionId: z.string().optional().openapi({ + title: "Session id", + description: + "Id of the session this log belongs to; it's compulsory if activityId is missing, optional otherwise; if both are present, they must be consistent", + example: "d3f53283-0e84-449c-81a0-9031b2037bfb", + }), + level: z.number().openapi({ + title: "Level", + description: + "A log level; the log levels are in range 0-599 (from least significant to most significant); they are logically mapped in blocks of 100 values at a time on the names trace, debug, information, warning, error, critical", + example: "400", + }), + category: z.string().openapi({ + title: "Category", + description: + "A category for the log, used to filter them in the interface, either by full match or by prefix", + example: "UI.SaveGameView", + }), + timestamp: z.string().datetime().openapi({ + title: "Timestamp", + description: "The ISO date and time at which this log was emitted", + }), + message: z.string().openapi({ + title: "Log message", + description: "The message of this log", + }), + genericMetadata: PostLogElementsMetadataNoOpenApiSchema.openapi({ + title: "Metadata", + description: + "Optional metadata to provide extra information about this object (e.g.: file name, line, function name)", + }), + specificMetadata: PostLogElementsMetadataNoOpenApiSchema.openapi({ + title: "Metadata", + description: + "Optional metadata to provide the information used to interpolate the message line (e.g.: button name, character, item)", + }), + }); + + /// union and array + + const PostLogElementsSchema = z + .discriminatedUnion("type", [ + PostLogElementsSessionStartSchema, + PostLogElementsSessionEndSchema, + PostLogElementsActivityStartSchema, + PostLogElementsActivityEndSchema, + PostLogElementsLogSchema, + ]) + .array(); + + return { PostLogElementsSchema }; +} diff --git a/src/common/contract/index.ts b/src/common/contract/index.ts index 0882924..a2a447e 100644 --- a/src/common/contract/index.ts +++ b/src/common/contract/index.ts @@ -7,6 +7,7 @@ import { exportGetSessionsSchema } from "./get-sessions"; import { exportPostLogSchema } from "./post-log"; import { exportGetEntriesSchema } from "./get-entries"; import { exportGetCategoriesSchema } from "./get-categories"; +import { exportGetV1Schema } from "./get-v1-schema"; extendZodWithOpenApi(z); @@ -27,6 +28,7 @@ const { } = exportGetEntriesSchema(z); const { GetCategoriesRequestQuerySchema, GetCategoriesResponseSchema } = exportGetCategoriesSchema(z); +const { PostLogElementsSchema } = exportGetV1Schema(z); const ErrorSchema = z.object({ code: z.number().optional().openapi({ @@ -43,6 +45,23 @@ const ErrorSchema = z.object({ const c = initContract(); +const v1Contract = c.router( + { + postLogElements: { + method: "POST", + path: "/logElements", + body: PostLogElementsSchema, + responses: { + 204: c.noBody(), + 500: ErrorSchema, + }, + }, + }, + { + pathPrefix: "/v1", + } +); + export const contract = c.router( { postLog: { @@ -102,6 +121,7 @@ export const contract = c.router( }, summary: "get the OpenAPI document for this API", }, + v1: v1Contract, }, { pathPrefix: "/api", @@ -113,4 +133,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 +export type LogEntry = z.infer; diff --git a/src/server/router.ts b/src/server/router.ts index b3d8109..d9eb51f 100644 --- a/src/server/router.ts +++ b/src/server/router.ts @@ -76,6 +76,7 @@ export function installRouter(app: Express, openAPIObject: OpenAPIObject) { body: openAPIObject, }; }, + postLogElements() { }); createExpressEndpoints(contract, router, app);