feat: category export and API cleanup.

This commit is contained in:
mattia
2025-01-06 20:45:51 +01:00
parent 50ecb39cce
commit 99859177b3
16 changed files with 566 additions and 248 deletions

View File

@@ -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<typeof SingleMetadataSchema>;
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<typeof GetSessionsResponseSchema>;
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<typeof GetSessionResponseSchema>;
////// 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<OpenAPIObject>(),
},
summary: "get the OpenAPI document for this API"
},
},
{
pathPrefix: "/api",
}
);

View File

@@ -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 };
}

View File

@@ -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 };
}

View File

@@ -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 };
}

View File

@@ -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 };
}

View File

@@ -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<OpenAPIObject>(),
},
summary: "get the OpenAPI document for this API",
},
},
{
pathPrefix: "/api",
}
);
///// TYPES
export type SingleMetadata = z.infer<typeof SingleMetadataSchema>;
export type GetSessionsResponse = z.infer<typeof GetSessionsResponseSchema>;
export type GetSessionResponse = z.infer<typeof GetSessionResponseSchema>;

View File

@@ -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,
};
}

View File

@@ -0,0 +1,3 @@
import { z } from "zod";
export type Z = typeof z;