import { FastifyPluginCallback } from "fastify"; import { Static, Type } from "@sinclair/typebox"; import { connection } from "./db"; // NOTE: see https://www.npmjs.com/package/fastify-plugin for TS plugin definition const GameParamsSchema = Type.Object({ game_id: Type.String({ format: "uuid" }), }); type GameParamsType = Static<typeof GameParamsSchema>; const ClueParamsSchema = Type.Object({ game_id: Type.String({ format: "uuid" }), clue_id: Type.String({ format: "uuid" }), }); type ClueParamsType = Static<typeof ClueParamsSchema>; const ClueSchema = Type.Object({ id: Type.Readonly(Type.String({ format: "uuid" })), game_id: Type.Readonly(Type.String({ format: "uuid" })), word_id: Type.Readonly(Type.String({ format: "uuid" })), began_at: Type.Optional(Type.String({ format: "date-time" })), ended_at: Type.Optional(Type.String({ format: "date-time" })), }); const CluePostSchema = Type.Pick(ClueSchema, ["game_id", "word_id"]); const CluePatchSchema = Type.Pick(ClueSchema, ["began_at", "ended_at"]); type ClueType = Static<typeof ClueSchema>; const ShotSchema = Type.Object({ id: Type.Readonly(Type.String({ format: "uuid" })), game_id: Type.Readonly(Type.String({ format: "uuid" })), clue_id: Type.Optional(Type.Readonly(Type.String({ format: "uuid" }))), began_at: Type.String({ format: "date-time" }), ended_at: Type.String({ format: "date-time" }), typed: Type.String(), final: Type.String(), }); const ShotPostSchema = Type.Omit(ShotSchema, ["id"]); type ShotType = Static<typeof ShotSchema>; const GameSchema = Type.Object({ id: Type.Readonly(Type.String({ format: "uuid" })), began_at: Type.Optional(Type.String({ format: "date-time" })), ended_at: Type.Optional(Type.String({ format: "date-time" })), }); type GameType = Static<typeof GameSchema>; const GamePatchSchema = Type.Omit(GameSchema, ["id"]); const WordSchema = Type.Object({ id: Type.String({ format: "uuid" }), image: Type.String(), ocr_confidence: Type.Number({ minimum: 0, maximum: 1 }), ocr_transcript: Type.String(), }); type WordType = Static<typeof WordSchema>; const apiPlugin: FastifyPluginCallback = (fastify, options, next) => { fastify.route<{ Reply: WordType; }>({ method: "GET", url: "/word", schema: { response: { 200: WordSchema, 404: {}, // TODO: JSend error }, }, handler: async (request, reply) => { // TODO: scope this to game to avoid collisions const word = await connection<WordType>("words") .whereBetween("ocr_confidence", [0.4, 0.8]) // i.e. needs improvement but it's not trash .whereRaw(`"ocr_transcript" ~ '^[[:alpha:]]+$'`) // i.e. no numbers nor symbols .orderByRaw("RANDOM()") .first(); if (word === undefined) { reply.code(404).send(); } else { reply.code(200).send(word); } }, }); fastify.route<{ Params: GameParamsType; Reply: GameType; }>({ method: "GET", url: "/games/:game_id", schema: { params: GameParamsSchema, response: { 200: GameSchema, 404: {}, // TODO: JSend error }, }, handler: async (request, reply) => { const game = await connection<GameType>("games") .where("id", request.params.game_id) .first(); if (game === undefined) { reply.code(404).send(); } else { reply.code(200).send(game); } }, }); fastify.route<{ Reply: GameType; }>({ method: "POST", url: "/games", schema: { response: { 200: GameSchema, }, }, handler: async (request, reply) => { const games = await connection .table("games") .insert({}) .returning<GameType[]>("*"); reply.code(200).send(games[0]); }, }); fastify.route<{ Params: GameParamsType; Body: Static<typeof GamePatchSchema>; Reply: GameType; }>({ method: "PATCH", url: "/games/:game_id", schema: { params: GameParamsSchema, body: GamePatchSchema, response: { 200: GameSchema, }, }, handler: async (request, reply) => { const game = await connection<GameType>("games") .where("id", request.params.game_id) .first(); if (game === undefined) { reply.code(404).send(); } else { const games = await connection<GameType>("games") .update(request.body) .returning("*"); reply.code(200).send(games[0]); } }, }); fastify.route<{ Params: Static<typeof GameParamsSchema>; Body: Static<typeof CluePostSchema>; Reply: ClueType; }>({ method: "POST", url: "/games/:game_id/clues", schema: { params: GameParamsSchema, body: CluePostSchema, response: { 200: ClueSchema, }, }, handler: async (request, reply) => {}, }); fastify.route<{ Params: Static<typeof ClueParamsSchema>; Body: Static<typeof CluePatchSchema>; Reply: ClueType; }>({ method: "PATCH", url: "/games/:game_id/clues/:clue_id", schema: { params: ClueParamsSchema, body: CluePatchSchema, response: { 200: ClueSchema, }, }, handler: async (request, reply) => {}, }); fastify.route<{ Params: Static<typeof GameParamsSchema>; Body: Static<typeof ShotPostSchema>; Reply: ShotType; }>({ method: "POST", url: "/games/:game_id/shots", schema: { params: GameParamsSchema, body: ShotPostSchema, response: { 200: ShotSchema, }, }, handler: async (request, reply) => {}, }); next(); }; export default apiPlugin;