From a8d36db20bfc9f77c82165a338a5193d3cddcb34 Mon Sep 17 00:00:00 2001 From: pinks Date: Sun, 8 Oct 2023 23:23:54 +0200 Subject: [PATCH] update rest api --- api/jobsRoute.ts | 14 ++++---- api/mod.ts | 4 +-- api/paramsRoute.ts | 28 +++++++--------- api/{api.ts => serveApi.ts} | 6 ++-- api/sessionsRoute.ts | 51 ++++++++++++++++------------- api/usersRoute.ts | 64 ++++++++++++++++++------------------- deno.json | 7 ++-- ui/App.tsx | 4 +-- ui/AppHeader.tsx | 10 +++--- ui/QueuePage.tsx | 4 +-- ui/SettingsPage.tsx | 17 +++++----- ui/apiClient.tsx | 17 ++++++---- 12 files changed, 116 insertions(+), 110 deletions(-) rename api/{api.ts => serveApi.ts} (69%) diff --git a/api/jobsRoute.ts b/api/jobsRoute.ts index 6e49425..cc85982 100644 --- a/api/jobsRoute.ts +++ b/api/jobsRoute.ts @@ -1,13 +1,15 @@ -import { Endpoint, Route } from "t_rest/server"; +import { createEndpoint, createMethodFilter } from "t_rest/server"; import { generationQueue } from "../app/generationQueue.ts"; -export const jobsRoute = { - GET: new Endpoint( +export const jobsRoute = createMethodFilter({ + GET: createEndpoint( { query: null, body: null }, async () => ({ status: 200, - type: "application/json", - body: await generationQueue.getAllJobs(), + body: { + type: "application/json", + data: await generationQueue.getAllJobs(), + }, }), ), -} satisfies Route; +}); diff --git a/api/mod.ts b/api/mod.ts index 5e506c7..66e4120 100644 --- a/api/mod.ts +++ b/api/mod.ts @@ -1,11 +1,11 @@ import { route } from "reroute"; import { serveSpa } from "serve_spa"; -import { api } from "./api.ts"; +import { serveApi } from "./serveApi.ts"; export async function serveUi() { const server = Deno.serve({ port: 5999 }, (request) => route(request, { - "/api/*": (request) => api.serve(request), + "/api/*": (request) => serveApi(request), "/*": (request) => serveSpa(request, { fsRoot: new URL("../ui/", import.meta.url).pathname, diff --git a/api/paramsRoute.ts b/api/paramsRoute.ts index 0b13c53..69d2561 100644 --- a/api/paramsRoute.ts +++ b/api/paramsRoute.ts @@ -1,26 +1,22 @@ import { deepMerge } from "std/collections/deep_merge.ts"; import { getLogger } from "std/log/mod.ts"; -import { Endpoint, Route } from "t_rest/server"; +import { createEndpoint, createMethodFilter } from "t_rest/server"; import { configSchema, getConfig, setConfig } from "../app/config.ts"; import { bot } from "../bot/mod.ts"; import { sessions } from "./sessionsRoute.ts"; export const logger = () => getLogger(); -export const paramsRoute = { - GET: new Endpoint( +export const paramsRoute = createMethodFilter({ + GET: createEndpoint( { query: null, body: null }, async () => { const config = await getConfig(); - return { - status: 200, - type: "application/json", - body: config?.defaultParams, - }; + return { status: 200, body: { type: "application/json", data: config.defaultParams } }; }, ), - PATCH: new Endpoint( + PATCH: createEndpoint( { query: { sessionId: { type: "string" } }, body: { @@ -31,7 +27,7 @@ export const paramsRoute = { async ({ query, body }) => { const session = sessions.get(query.sessionId); if (!session?.userId) { - return { status: 401, type: "text/plain", body: "Must be logged in" }; + return { status: 401, body: { type: "text/plain", data: "Must be logged in" } }; } const chat = await bot.api.getChat(session.userId); if (chat.type !== "private") { @@ -39,16 +35,16 @@ export const paramsRoute = { } const userName = chat.username; if (!userName) { - return { status: 403, type: "text/plain", body: "Must have a username" }; + return { status: 403, body: { type: "text/plain", data: "Must have a username" } }; } const config = await getConfig(); if (!config?.adminUsernames?.includes(userName)) { - return { status: 403, type: "text/plain", body: "Must be an admin" }; + return { status: 403, body: { type: "text/plain", data: "Must be an admin" } }; } - logger().info(`User ${userName} updated default params: ${JSON.stringify(body)}`); - const defaultParams = deepMerge(config.defaultParams ?? {}, body); + logger().info(`User ${userName} updated default params: ${JSON.stringify(body.data)}`); + const defaultParams = deepMerge(config.defaultParams ?? {}, body.data); await setConfig({ defaultParams }); - return { status: 200, type: "application/json", body: config.defaultParams }; + return { status: 200, body: { type: "application/json", data: config.defaultParams } }; }, ), -} satisfies Route; +}); diff --git a/api/api.ts b/api/serveApi.ts similarity index 69% rename from api/api.ts rename to api/serveApi.ts index a13f176..0cbecec 100644 --- a/api/api.ts +++ b/api/serveApi.ts @@ -1,14 +1,14 @@ -import { Api } from "t_rest/server"; +import { createPathFilter } from "t_rest/server"; import { jobsRoute } from "./jobsRoute.ts"; import { sessionsRoute } from "./sessionsRoute.ts"; import { usersRoute } from "./usersRoute.ts"; import { paramsRoute } from "./paramsRoute.ts"; -export const api = new Api({ +export const serveApi = createPathFilter({ "jobs": jobsRoute, "sessions": sessionsRoute, "users": usersRoute, "settings/params": paramsRoute, }); -export type ErisApi = typeof api; +export type ApiHandler = typeof serveApi; diff --git a/api/sessionsRoute.ts b/api/sessionsRoute.ts index 1276626..9861acd 100644 --- a/api/sessionsRoute.ts +++ b/api/sessionsRoute.ts @@ -1,5 +1,5 @@ // deno-lint-ignore-file require-await -import { Endpoint, Route } from "t_rest/server"; +import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server"; import { ulid } from "ulid"; export const sessions = new Map(); @@ -8,25 +8,30 @@ export interface Session { userId?: number; } -export const sessionsRoute = { - POST: new Endpoint( - { query: null, body: null }, - async () => { - const id = ulid(); - const session: Session = {}; - sessions.set(id, session); - return { status: 200, type: "application/json", body: { id, ...session } }; - }, - ), - GET: new Endpoint( - { query: { sessionId: { type: "string" } }, body: null }, - async ({ query }) => { - const id = query.sessionId; - const session = sessions.get(id); - if (!session) { - return { status: 401, type: "text/plain", body: "Session not found" }; - } - return { status: 200, type: "application/json", body: { id, ...session } }; - }, - ), -} satisfies Route; +export const sessionsRoute = createPathFilter({ + "": createMethodFilter({ + POST: createEndpoint( + { query: null, body: null }, + async () => { + const id = ulid(); + const session: Session = {}; + sessions.set(id, session); + return { status: 200, body: { type: "application/json", data: { id, ...session } } }; + }, + ), + }), + + "{sessionId}": createMethodFilter({ + GET: createEndpoint( + { query: null, body: null }, + async ({ params }) => { + const id = params.sessionId; + const session = sessions.get(id); + if (!session) { + return { status: 401, body: { type: "text/plain", data: "Session not found" } }; + } + return { status: 200, body: { type: "application/json", data: { id, ...session } } }; + }, + ), + }), +}); diff --git a/api/usersRoute.ts b/api/usersRoute.ts index 61d38d5..9cb3066 100644 --- a/api/usersRoute.ts +++ b/api/usersRoute.ts @@ -1,36 +1,36 @@ import { encode } from "std/encoding/base64.ts"; -import { Endpoint, Route } from "t_rest/server"; +import { createEndpoint, createMethodFilter, createPathFilter } from "t_rest/server"; import { getConfig } from "../app/config.ts"; import { bot } from "../bot/mod.ts"; -export const usersRoute = { - GET: new Endpoint( - { query: { userId: { type: "number" } }, body: null }, - async ({ query }) => { - const chat = await bot.api.getChat(query.userId); - if (chat.type !== "private") { - throw new Error("Chat is not private"); - } - const photoData = chat.photo?.small_file_id - ? encode( - await fetch( - `https://api.telegram.org/file/bot${bot.token}/${await bot.api.getFile( - chat.photo.small_file_id, - ).then((file) => file.file_path)}`, - ).then((resp) => resp.arrayBuffer()), - ) - : undefined; - const config = await getConfig(); - const isAdmin = config?.adminUsernames?.includes(chat.username); - return { - status: 200, - type: "application/json", - body: { - ...chat, - photoData, - isAdmin, - }, - }; - }, - ), -} satisfies Route; +export const usersRoute = createPathFilter({ + "{userId}": createMethodFilter({ + GET: createEndpoint( + { query: null, body: null }, + async ({ params }) => { + const chat = await bot.api.getChat(params.userId); + if (chat.type !== "private") { + throw new Error("Chat is not private"); + } + const photoData = chat.photo?.small_file_id + ? encode( + await fetch( + `https://api.telegram.org/file/bot${bot.token}/${await bot.api.getFile( + chat.photo.small_file_id, + ).then((file) => file.file_path)}`, + ).then((resp) => resp.arrayBuffer()), + ) + : undefined; + const config = await getConfig(); + const isAdmin = config?.adminUsernames?.includes(chat.username); + return { + status: 200, + body: { + type: "application/json", + data: { ...chat, photoData, isAdmin }, + }, + }; + }, + ), + }), +}); diff --git a/deno.json b/deno.json index 21d9e00..fe295f8 100644 --- a/deno.json +++ b/deno.json @@ -1,6 +1,7 @@ { "tasks": { - "start": "deno run --unstable --allow-env --allow-read --allow-write --allow-net main.ts" + "start": "deno run --unstable --allow-env --allow-read --allow-write --allow-net main.ts", + "check": "deno check --unstable main.ts && deno check --unstable ui/main.tsx" }, "compilerOptions": { "jsx": "react" @@ -33,9 +34,9 @@ "png_chunks_extract": "https://esm.sh/png-chunks-extract@1.0.0", "png_chunk_text": "https://esm.sh/png-chunk-text@1.0.0", "openapi_fetch": "https://esm.sh/openapi-fetch@0.7.6", - "t_rest/server": "https://esm.sh/ty-rest@0.2.1/server?dev", + "t_rest/server": "https://esm.sh/ty-rest@0.3.1/server?dev", - "t_rest/client": "https://esm.sh/ty-rest@0.2.1/client?dev", + "t_rest/client": "https://esm.sh/ty-rest@0.3.1/client?dev", "react": "https://esm.sh/react@18.2.0?dev", "react-dom/client": "https://esm.sh/react-dom@18.2.0/client?dev", "react-router-dom": "https://esm.sh/react-router-dom@6.16.0?dev", diff --git a/ui/App.tsx b/ui/App.tsx index 0b72901..b03ffb7 100644 --- a/ui/App.tsx +++ b/ui/App.tsx @@ -4,7 +4,7 @@ import useLocalStorage from "use-local-storage"; import { AppHeader } from "./AppHeader.tsx"; import { QueuePage } from "./QueuePage.tsx"; import { SettingsPage } from "./SettingsPage.tsx"; -import { apiClient, handleResponse } from "./apiClient.tsx"; +import { fetchApi, handleResponse } from "./apiClient.tsx"; export function App() { // store session ID in the local storage @@ -13,7 +13,7 @@ export function App() { // initialize a new session when there is no session ID useEffect(() => { if (!sessionId) { - apiClient.fetch("sessions", "POST", {}).then(handleResponse).then((session) => { + fetchApi("sessions", "POST", {}).then(handleResponse).then((session) => { console.log("Initialized session", session.id); setSessionId(session.id); }); diff --git a/ui/AppHeader.tsx b/ui/AppHeader.tsx index 4b6d6f9..8e6edd6 100644 --- a/ui/AppHeader.tsx +++ b/ui/AppHeader.tsx @@ -2,7 +2,7 @@ import { cx } from "@twind/core"; import React, { ReactNode } from "react"; import { NavLink } from "react-router-dom"; import useSWR from "swr"; -import { apiClient, handleResponse } from "./apiClient.tsx"; +import { fetchApi, handleResponse } from "./apiClient.tsx"; function NavTab(props: { to: string; children: ReactNode }) { return ( @@ -21,16 +21,16 @@ export function AppHeader( const { className, sessionId, onLogOut } = props; const session = useSWR( - sessionId ? ["sessions", "GET", { query: { sessionId } }] as const : null, - (args) => apiClient.fetch(...args).then(handleResponse), + sessionId ? ["sessions/{sessionId}", "GET", { params: { sessionId } }] as const : null, + (args) => fetchApi(...args).then(handleResponse), { onError: () => onLogOut() }, ); const user = useSWR( session.data?.userId - ? ["users", "GET", { query: { userId: session.data.userId } }] as const + ? ["users/{userId}", "GET", { params: { userId: String(session.data.userId) } }] as const : null, - (args) => apiClient.fetch(...args).then(handleResponse), + (args) => fetchApi(...args).then(handleResponse), ); return ( diff --git a/ui/QueuePage.tsx b/ui/QueuePage.tsx index 65b1d5b..c2fe3fc 100644 --- a/ui/QueuePage.tsx +++ b/ui/QueuePage.tsx @@ -3,12 +3,12 @@ import FlipMove from "react-flip-move"; import useSWR from "swr"; import { getFlagEmoji } from "../utils/getFlagEmoji.ts"; import { Progress } from "./Progress.tsx"; -import { apiClient, handleResponse } from "./apiClient.tsx"; +import { fetchApi, handleResponse } from "./apiClient.tsx"; export function QueuePage() { const jobs = useSWR( ["jobs", "GET", {}] as const, - (args) => apiClient.fetch(...args).then(handleResponse), + (args) => fetchApi(...args).then(handleResponse), { refreshInterval: 2000 }, ); diff --git a/ui/SettingsPage.tsx b/ui/SettingsPage.tsx index a687c7d..5c0d601 100644 --- a/ui/SettingsPage.tsx +++ b/ui/SettingsPage.tsx @@ -1,23 +1,23 @@ import { cx } from "@twind/core"; import React, { ReactNode, useState } from "react"; import useSWR from "swr"; -import { apiClient, handleResponse } from "./apiClient.tsx"; +import { fetchApi, handleResponse } from "./apiClient.tsx"; export function SettingsPage(props: { sessionId: string }) { const { sessionId } = props; const session = useSWR( - ["sessions", "GET", { query: { sessionId } }] as const, - (args) => apiClient.fetch(...args).then(handleResponse), + sessionId ? ["sessions/{sessionId}", "GET", { params: { sessionId } }] as const : null, + (args) => fetchApi(...args).then(handleResponse), ); const user = useSWR( session.data?.userId - ? ["users", "GET", { query: { userId: session.data.userId } }] as const + ? ["users/{userId}", "GET", { params: { userId: String(session.data.userId) } }] as const : null, - (args) => apiClient.fetch(...args).then(handleResponse), + (args) => fetchApi(...args).then(handleResponse), ); const params = useSWR( ["settings/params", "GET", {}] as const, - (args) => apiClient.fetch(...args).then(handleResponse), + (args) => fetchApi(...args).then(handleResponse), ); const [changedParams, setChangedParams] = useState>({}); const [error, setError] = useState(); @@ -28,10 +28,9 @@ export function SettingsPage(props: { sessionId: string }) { onSubmit={(e) => { e.preventDefault(); params.mutate(() => - apiClient.fetch("settings/params", "PATCH", { + fetchApi("settings/params", "PATCH", { query: { sessionId }, - type: "application/json", - body: changedParams ?? {}, + body: { type: "application/json", data: changedParams ?? {} }, }).then(handleResponse) ) .then(() => setChangedParams({})) diff --git a/ui/apiClient.tsx b/ui/apiClient.tsx index 1fcdc8f..d851fec 100644 --- a/ui/apiClient.tsx +++ b/ui/apiClient.tsx @@ -1,12 +1,15 @@ -import { Client } from "t_rest/client"; -import { ApiResponse } from "t_rest/server"; -import { ErisApi } from "../api/api.ts"; +import { createFetcher, Output } from "t_rest/client"; +import { ApiHandler } from "../api/serveApi.ts"; -export const apiClient = new Client(`${location.origin}/api/`); +export const fetchApi = createFetcher({ + baseUrl: `${location.origin}/api/`, +}); -export function handleResponse(response: T): (T & { status: 200 })["body"] { +export function handleResponse( + response: T, +): (T & { status: 200 })["body"]["data"] { if (response.status !== 200) { - throw new Error(String(response.body)); + throw new Error(String(response.body.data)); } - return response.body; + return response.body.data; }