feat: embrace full REST API and OpenAPI+Swagger

This commit is contained in:
mattia
2025-01-06 16:36:53 +01:00
parent f9c61bae6f
commit 79e4a4c012
10 changed files with 507 additions and 119 deletions

View File

@@ -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<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().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<typeof GetSessionsResponseSchema>;
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<typeof GetSessionResponseSchema>;
//////
////// 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<OpenAPIObject>(),
},
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",
}
});
);