From ac92950177e83ad1064308d045131a1e61689d1f Mon Sep 17 00:00:00 2001 From: Justin Lin Date: Wed, 16 Apr 2025 16:24:29 +0800 Subject: [PATCH] feat: migrate quest api to trpc --- app/src/components/QuestIntro.vue | 4 +-- app/src/components/QuestMap.vue | 4 +-- app/src/store/session.ts | 9 +++--- app/src/utils/api.ts | 11 ------- packages/constants/index.ts | 2 ++ packages/constants/src/quest.ts | 6 ++++ packages/constants/tsconfig.json | 4 ++- server/src/api/controllers/questController.ts | 28 ----------------- server/src/api/routes/index.ts | 2 -- server/src/api/routes/questRouter.ts | 10 ------- server/src/api/validators/questValidator.ts | 7 ----- server/src/api/validators/sessionValidator.ts | 7 ++--- server/src/routers/index.ts | 2 ++ server/src/routers/questRouter.ts | 30 +++++++++++++++++++ 14 files changed, 55 insertions(+), 71 deletions(-) create mode 100644 packages/constants/src/quest.ts delete mode 100644 server/src/api/controllers/questController.ts delete mode 100644 server/src/api/routes/questRouter.ts delete mode 100644 server/src/api/validators/questValidator.ts create mode 100644 server/src/routers/questRouter.ts diff --git a/app/src/components/QuestIntro.vue b/app/src/components/QuestIntro.vue index efb5ab7..e9f62e8 100644 --- a/app/src/components/QuestIntro.vue +++ b/app/src/components/QuestIntro.vue @@ -2,8 +2,8 @@ import { useRouter } from 'vue-router' import { useToast } from 'vue-toastification' import { ref, defineProps, onMounted } from 'vue' -import { getQuest } from '../utils/api' import MarkdownText from './MarkdownText.vue' +import { trpc } from '../utils/trpc' const props = defineProps({ questTitle: { @@ -49,7 +49,7 @@ const handleColour = () => { } } onMounted(async () => { - const quest = await getQuest(questId) + const quest = await trpc.quests.getQuestDetail.query(questId) questInstruction.value = quest.instruction handleColour() }) diff --git a/app/src/components/QuestMap.vue b/app/src/components/QuestMap.vue index 95a003f..4a0846a 100644 --- a/app/src/components/QuestMap.vue +++ b/app/src/components/QuestMap.vue @@ -2,7 +2,7 @@ import { ref, onMounted, watch, computed } from 'vue' import { useToast } from 'vue-toastification' import { DAG } from '@linux-odyssey/utils' -import { getQuests } from '../utils/api' +import { trpc } from '../utils/trpc' import useUserProfile from '../store/userProfile' import QuestIntro from './QuestIntro.vue' @@ -64,7 +64,7 @@ async function computeGraphData() { if (!store.progress) return try { - const quests = await getQuests() + const quests = await trpc.quests.getQuests.query() const dag = new DAG(quests) const nodesValues = dag.getNodes() diff --git a/app/src/store/session.ts b/app/src/store/session.ts index d6ced19..150c064 100644 --- a/app/src/store/session.ts +++ b/app/src/store/session.ts @@ -1,10 +1,11 @@ import { defineStore } from 'pinia' import { FileGraph, FileObject } from '@linux-odyssey/file-graph' -import { IQuest } from '@linux-odyssey/models' -import { createSession, getActiveSession, getQuest } from '../utils/api' +import { QuestDetailResponse } from '@linux-odyssey/constants' +import { createSession, getActiveSession } from '../utils/api' import Socket from '../utils/socket' import SocketTerminal from '../utils/terminal' import { Session, StageResponse } from '../types' +import { trpc } from '../utils/trpc' const socket = new Socket() const term = new SocketTerminal(40, 80) @@ -13,7 +14,7 @@ let hasSetup = false interface Store { session: Session | null questId: string - quest: IQuest | null + quest: QuestDetailResponse | null } const useSession = defineStore('session', { @@ -24,7 +25,7 @@ const useSession = defineStore('session', { }), actions: { async setQuest(questId: string) { - this.quest = await getQuest(questId) + this.quest = await trpc.quests.getQuestDetail.query(questId) this.questId = questId }, async setSession(session: Session) { diff --git a/app/src/utils/api.ts b/app/src/utils/api.ts index 81f2121..f17d3d9 100644 --- a/app/src/utils/api.ts +++ b/app/src/utils/api.ts @@ -1,5 +1,4 @@ import axios from 'axios' -import { IQuest } from '@linux-odyssey/models' import { GetSessionsRequest } from '@linux-odyssey/constants' import { Session } from '../types' import { UserProfile } from '../store/userProfile' @@ -18,11 +17,6 @@ export async function getUserProfile(): Promise { return res.data } -export async function getQuest(id: string): Promise { - const res = await api.get(`/quests/${id}`) - return res.data -} - export async function createSession(questId: string): Promise { const res = await api.post('/sessions', { questId }) return res.data @@ -43,8 +37,3 @@ export async function getActiveSession( }) return res.data } - -export async function getQuests(): Promise { - const res = await api.get('/quests') - return res.data -} diff --git a/packages/constants/index.ts b/packages/constants/index.ts index c555e8e..563849a 100644 --- a/packages/constants/index.ts +++ b/packages/constants/index.ts @@ -1,5 +1,7 @@ import { z } from 'zod' +export * from './src/quest.js' + export const passwordPolicy = { minLength: 8, minNumbers: 1, diff --git a/packages/constants/src/quest.ts b/packages/constants/src/quest.ts new file mode 100644 index 0000000..69adbe0 --- /dev/null +++ b/packages/constants/src/quest.ts @@ -0,0 +1,6 @@ +export interface QuestDetailResponse { + _id: string + title: string + instruction: string + requirements: string[] +} diff --git a/packages/constants/tsconfig.json b/packages/constants/tsconfig.json index 318a4cf..4972b05 100644 --- a/packages/constants/tsconfig.json +++ b/packages/constants/tsconfig.json @@ -2,8 +2,10 @@ "extends": "../../tsconfig.json", "compilerOptions": { "composite": true, + "module": "Node16", + "moduleResolution": "node16", "outDir": "./dist" }, - "include": ["*.js", "*.ts"], + "include": ["*.js", "*.ts", "src/**/*.ts"], "exclude": ["node_modules", "dist"] } diff --git a/server/src/api/controllers/questController.ts b/server/src/api/controllers/questController.ts deleted file mode 100644 index e9a7660..0000000 --- a/server/src/api/controllers/questController.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { Quest } from '@linux-odyssey/models' -import { matchedData } from 'express-validator' -import type { Request, Response } from 'express' -import { asyncHandler } from '../../middleware/error.js' - -export const getQuests = asyncHandler(async (req: Request, res: Response) => { - const quests = await Quest.find({}) - res.json( - quests.map(({ title, _id, requirements }) => ({ - _id, - title, - requirements, - })) - ) -}) - -export const getQuestDetail = asyncHandler( - async (req: Request, res: Response) => { - const { questId } = matchedData(req) - const quest = await Quest.findById(questId) - if (!quest) { - res.status(404).json({ message: 'Quest not found.' }) - return - } - const { _id, title, instruction, requirements } = quest - res.json({ _id, title, instruction, requirements }) - } -) diff --git a/server/src/api/routes/index.ts b/server/src/api/routes/index.ts index 1ced879..fb3b318 100644 --- a/server/src/api/routes/index.ts +++ b/server/src/api/routes/index.ts @@ -1,7 +1,6 @@ import { Router } from 'express' import passport from 'passport' import sessions from './sessionRouter.js' -import quests from './questRouter.js' import commands from './commandRouter.js' import authRouter from './authRouter.js' import authRequired from '../../middleware/authRequired.js' @@ -29,7 +28,6 @@ router.use( router.use('/auth', authRouter) router.use('/sessions', authRequired, sessions) -router.use('/quests', quests) router.use('/users', userRouter) router.get('/survey', (req, res) => { diff --git a/server/src/api/routes/questRouter.ts b/server/src/api/routes/questRouter.ts deleted file mode 100644 index 4749f54..0000000 --- a/server/src/api/routes/questRouter.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Router } from 'express' -import * as questController from '../controllers/questController.js' -import { questIdValidator } from '../validators/questValidator.js' - -const router = Router() - -router.get('/', questController.getQuests) -router.get('/:questId', questIdValidator, questController.getQuestDetail) - -export default router diff --git a/server/src/api/validators/questValidator.ts b/server/src/api/validators/questValidator.ts deleted file mode 100644 index 3a2a18e..0000000 --- a/server/src/api/validators/questValidator.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { check } from 'express-validator' -import { noError } from '../../middleware/validator.js' - -export const checkQuestId = () => - check('questId').notEmpty().isString().isLowercase().isLength({ max: 64 }) - -export const questIdValidator = [checkQuestId(), noError] diff --git a/server/src/api/validators/sessionValidator.ts b/server/src/api/validators/sessionValidator.ts index 558b5b6..509defb 100644 --- a/server/src/api/validators/sessionValidator.ts +++ b/server/src/api/validators/sessionValidator.ts @@ -1,5 +1,4 @@ import { check } from 'express-validator' -import { checkQuestId } from './questValidator.js' import { noError } from '../../middleware/validator.js' const checkSessionStatus = () => @@ -8,7 +7,7 @@ const checkSessionStatus = () => export const checkSessionId = () => check('sessionId').notEmpty().isString().isMongoId() -export const sessionListValidator = [checkQuestId(), checkSessionStatus()] +export const sessionListValidator = [checkSessionStatus()] export const sessionDetailValidator = [checkSessionId()] -export const createSessionValidator = [checkQuestId(), noError] -export const activeSessionValidator = [checkQuestId(), noError] +export const createSessionValidator = [noError] +export const activeSessionValidator = [noError] diff --git a/server/src/routers/index.ts b/server/src/routers/index.ts index 2d7bd5f..288d2f3 100644 --- a/server/src/routers/index.ts +++ b/server/src/routers/index.ts @@ -1,5 +1,6 @@ import { router, publicProcedure } from '../trpc.js' import { linkRouter } from './linkRouter.js' +import { questRouter } from './questRouter.js' export const appRouter = router({ userList: publicProcedure.query(async (opts) => { @@ -7,6 +8,7 @@ export const appRouter = router({ return opts.ctx }), links: linkRouter, + quests: questRouter, }) export type AppRouter = typeof appRouter diff --git a/server/src/routers/questRouter.ts b/server/src/routers/questRouter.ts new file mode 100644 index 0000000..0c39a1b --- /dev/null +++ b/server/src/routers/questRouter.ts @@ -0,0 +1,30 @@ +import { TRPCError } from '@trpc/server' +import { Quest } from '@linux-odyssey/models' +import { QuestDetailResponse } from '@linux-odyssey/constants' +import { z } from 'zod' +import { router, publicProcedure } from '../trpc.js' + +export const questRouter = router({ + getQuests: publicProcedure.query(async () => { + const quests = await Quest.find({}) + return quests.map(({ title, _id, requirements }) => ({ + _id, + title, + requirements, + })) + }), + getQuestDetail: publicProcedure + .input(z.string()) + .query(async (opts): Promise => { + const id = opts.input + const quest = await Quest.findById(id) + if (!quest) { + throw new TRPCError({ + code: 'NOT_FOUND', + message: 'Quest not found.', + }) + } + const { _id, title, instruction, requirements } = quest + return { _id, title, instruction, requirements } + }), +})