From 57f964744f366a96f031fdc067a30feed4e5767a Mon Sep 17 00:00:00 2001 From: lijiayang050403 Date: Fri, 24 Oct 2025 04:55:08 +0000 Subject: [PATCH] feat: up --- design.md | 553 +++++++++++++++++++++++++ package-lock.json | 715 +++++++++++++++++++++++++++------ package.json | 15 +- src/routes/repos.ts | 93 +++-- src/routes/stats.ts | 18 +- src/server.ts | 129 ++++-- src/services/github.service.ts | 29 +- src/services/stats.service.ts | 42 ++ src/types/fastify.d.ts | 14 + tsconfig.json | 19 +- 10 files changed, 1416 insertions(+), 211 deletions(-) create mode 100644 design.md create mode 100644 src/services/stats.service.ts create mode 100644 src/types/fastify.d.ts diff --git a/design.md b/design.md new file mode 100644 index 0000000..872ea82 --- /dev/null +++ b/design.md @@ -0,0 +1,553 @@ +好的!我理解了,你需要: + +1. **前端**:现代化框架(React/Vue) +2. **后端**:独立的轻量后端(不用 Next.js API Routes) +3. **部署**:都要简单方便 + +让我重新设计一个最佳方案!🎯 **最终推荐架构** + +* * * + +### **技术选型** + + 前端 (SPA): + - Framework: Vite + React 18 + TypeScript + - UI: Tailwind CSS + shadcn/ui + - State: Zustand + TanStack Query + - Charts: Recharts + D3.js + - 部署: Cloudflare Pages / Vercel (静态托管) + + 后端 (API): + - Framework: Fastify + TypeScript (比Express快2倍) + - GitHub SDK: Octokit + - Database: PostgreSQL (Supabase免费托管) + - Cache: Upstash Redis (Serverless) + - 部署: Railway.app ⭐ (一键部署,免费额度够用) + + 认证: + - GitHub OAuth 2.0 + - JWT Token + + 实时通信: + - Server-Sent Events (SSE) - 比WebSocket简单 + - 或 Pusher (免费版) + +* * * + +🏗️ **系统架构图** + + ┌─────────────────────────────────────────────────────────────┐ + │ 用户浏览器 │ + │ │ + │ ┌──────────────────────────────────────────────────────┐ │ + │ │ React SPA (Vite) │ │ + │ │ │ │ + │ │ ┌──────────┐ ┌───────────┐ ┌──────────────┐ │ │ + │ │ │Dashboard │ │Repository │ │Analytics │ │ │ + │ │ │ Page │ │Detail Page│ │ Page │ │ │ + │ │ └────┬─────┘ └─────┬─────┘ └──────┬───────┘ │ │ + │ │ │ │ │ │ │ + │ │ └──────────────┴────────────────┘ │ │ + │ │ │ │ │ + │ │ ┌─────────▼──────────┐ │ │ + │ │ │ TanStack Query │ (数据获取层) │ │ + │ │ │ + Axios/Fetch │ │ │ + │ │ └─────────┬──────────┘ │ │ + │ └──────────────────────┼───────────────────────────────┘ │ + └─────────────────────────┼──────────────────────────────────┘ + │ + HTTPS (REST API) + │ + ┌─────────────────────────▼──────────────────────────────────┐ + │ 后端服务器 (Railway) │ + │ │ + │ ┌────────────────────────────────────────────────────┐ │ + │ │ Fastify API Server (Node.js) │ │ + │ │ │ │ + │ │ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │ │ + │ │ │ Auth │ │ Repos │ │ Webhooks │ │ │ + │ │ │ /login │ │ /repos/* │ │ /webhooks/* │ │ │ + │ │ └────┬─────┘ └────┬─────┘ └──────┬───────┘ │ │ + │ │ │ │ │ │ │ + │ │ └─────────────┴────────────────┘ │ │ + │ │ │ │ │ + │ │ ┌──────────▼──────────┐ │ │ + │ │ │ Service Layer │ │ │ + │ │ │ • GitHubService │ │ │ + │ │ │ • CacheService │ │ │ + │ │ │ • WebhookService │ │ │ + │ │ └──────────┬──────────┘ │ │ + │ └─────────────────────┼────────────────────────────────┘ │ + │ │ │ + │ ┌──────────────┼──────────────┐ │ + │ │ │ │ │ + │ ┌────▼─────┐ ┌────▼────┐ ┌──────▼──────┐ │ + │ │ Octokit │ │ Prisma │ │ Redis Cache │ │ + │ │ GitHub │ │ ORM │ │ (Upstash) │ │ + │ │ API │ └────┬────┘ └─────────────┘ │ + │ └──────────┘ │ │ + └───────────────────────┼─────────────────────────────────────┘ + │ + ┌──────▼──────┐ + │ PostgreSQL │ + │ (Supabase) │ + └─────────────┘ + + ┌────────────────────────────────────────────┐ + │ 外部服务 │ + │ │ + │ ┌──────────────┐ ┌──────────────────┐ │ + │ │ GitHub API │ │ GitHub Webhooks │ │ + │ │ (公开接口) │ │ (实时事件推送) │ │ + │ └──────────────┘ └──────────────────┘ │ + └────────────────────────────────────────────┘ + +* * * + +📂 **项目结构** + + github-dashboard/ + ├── frontend/ # 前端项目 (Vite) + │ ├── src/ + │ │ ├── components/ + │ │ │ ├── dashboard/ + │ │ │ │ ├── StatsCard.tsx + │ │ │ │ ├── ActivityChart.tsx + │ │ │ │ └── RepoList.tsx + │ │ │ ├── charts/ + │ │ │ └── ui/ # shadcn/ui 组件 + │ │ ├── pages/ + │ │ │ ├── Dashboard.tsx + │ │ │ ├── RepoDetail.tsx + │ │ │ └── Analytics.tsx + │ │ ├── hooks/ + │ │ │ ├── useRepos.ts + │ │ │ └── useRepoStats.ts + │ │ ├── lib/ + │ │ │ ├── api.ts # API 请求封装 + │ │ │ └── auth.ts # 认证逻辑 + │ │ ├── stores/ + │ │ │ └── authStore.ts # Zustand store + │ │ ├── App.tsx + │ │ └── main.tsx + │ ├── package.json + │ ├── vite.config.ts + │ └── tailwind.config.js + │ + ├── backend/ # 后端项目 (Fastify) + │ ├── src/ + │ │ ├── routes/ + │ │ │ ├── auth.ts # POST /auth/login + │ │ │ ├── repos.ts # GET /repos, /repos/:id + │ │ │ ├── webhooks.ts # POST /webhooks/github + │ │ │ └── stats.ts # GET /stats/* + │ │ ├── services/ + │ │ │ ├── github.service.ts + │ │ │ ├── cache.service.ts + │ │ │ └── webhook.service.ts + │ │ ├── middlewares/ + │ │ │ ├── auth.middleware.ts + │ │ │ └── error.middleware.ts + │ │ ├── prisma/ + │ │ │ └── schema.prisma + │ │ ├── types/ + │ │ │ └── index.ts + │ │ ├── utils/ + │ │ │ └── logger.ts + │ │ ├── app.ts # Fastify 实例 + │ │ └── server.ts # 启动文件 + │ ├── package.json + │ ├── tsconfig.json + │ └── .env.example + │ + └── README.md + +* * * + +🚀 **部署方案** + +### **前端部署:Cloudflare Pages** ⭐ + + # 1. 构建 + cd frontend + npm run build + + # 2. Cloudflare Pages 自动部署 + # - 连接 GitHub 仓库 + # - 构建命令: npm run build + # - 输出目录: dist + # - 自动部署: 推送到 main 分支即触发 + +**优势:** + +* ✅ 完全免费 +* ✅ 全球 CDN(超快) +* ✅ 自动 HTTPS +* ✅ 无限带宽 +* ✅ 支持自定义域名 + +* * * + +### **后端部署:Railway.app** ⭐⭐⭐ + + # 1. 安装 Railway CLI + npm install -g @railway/cli + + # 2. 登录 + railway login + + # 3. 初始化项目 + cd backend + railway init + + # 4. 部署 + railway up + + # 5. 添加数据库 + railway add postgresql + + # 6. 设置环境变量 + railway variables set GITHUB_CLIENT_ID=xxx + railway variables set GITHUB_CLIENT_SECRET=xxx + +**Railway 配置文件 (`railway.json`)**: { "$schema": "https://railway.app/railway.schema.json", "build": { "builder": "NIXPACKS" }, "deploy": { "startCommand": "npm run start", "restartPolicyType": "ON_FAILURE", "restartPolicyMaxRetries": 10 } } + +**优势:** + +* ✅ 免费额度:$5/月(足够小项目) +* ✅ 一键部署,零配置 +* ✅ 自动 HTTPS +* ✅ 内置 PostgreSQL +* ✅ 环境变量管理 +* ✅ 自动 Git 同步部署 + +* * * + +### **替代方案对比** + +| 平台 | 免费额度 | 部署难度 | 启动速度 | 推荐度 | +| --- | --- | --- | --- | --- | +| **Railway** | $5/月 | ⭐⭐⭐⭐⭐ | 快 | ⭐⭐⭐⭐⭐ | +| Render | 750h/月 | ⭐⭐⭐⭐ | 慢(冷启动) | ⭐⭐⭐⭐ | +| Fly.io | 3个免费实例 | ⭐⭐⭐ | 快 | ⭐⭐⭐⭐ | +| Heroku | 无免费版 | ⭐⭐⭐⭐ | 中等 | ⭐⭐ | + +* * * + +💻 **核心代码示例** + +### **后端 - Fastify 入口 (`backend/src/app.ts`)** + + import Fastify from 'fastify'; + import cors from '@fastify/cors'; + import jwt from '@fastify/jwt'; + import { authRoutes } from './routes/auth'; + import { repoRoutes } from './routes/repos'; + import { webhookRoutes } from './routes/webhooks'; + + export function createApp() { + const app = Fastify({ + logger: true, + }); + + // 插件 + app.register(cors, { + origin: process.env.FRONTEND_URL || 'http://localhost:5173', + credentials: true, + }); + + app.register(jwt, { + secret: process.env.JWT_SECRET!, + }); + + // 路由 + app.register(authRoutes, { prefix: '/api/auth' }); + app.register(repoRoutes, { prefix: '/api/repos' }); + app.register(webhookRoutes, { prefix: '/api/webhooks' }); + + // 健康检查 + app.get('/health', async () => { + return { status: 'ok', timestamp: new Date().toISOString() }; + }); + + return app; + } + +### **后端 - GitHub Service (`backend/src/services/github.service.ts`)** + + import { Octokit } from '@octokit/rest'; + import { Redis } from '@upstash/redis'; + + const redis = new Redis({ + url: process.env.UPSTASH_REDIS_URL!, + token: process.env.UPSTASH_REDIS_TOKEN!, + }); + + export class GitHubService { + private octokit: Octokit; + + constructor(accessToken: string) { + this.octokit = new Octokit({ auth: accessToken }); + } + + async getRepositories() { + const cacheKey = 'repos:list'; + + // 尝试从缓存读取 + const cached = await redis.get(cacheKey); + if (cached) return cached; + + // 从 GitHub API 获取 + const { data } = await this.octokit.repos.listForAuthenticatedUser({ + sort: 'updated', + per_page: 100, + }); + + // 缓存5分钟 + await redis.set(cacheKey, data, { ex: 300 }); + + return data; + } + + async getRepositoryStats(owner: string, repo: string) { + const cacheKey = `repo:${owner}/${repo}:stats`; + const cached = await redis.get(cacheKey); + if (cached) return cached; + + const [repoData, issues, pulls, commits] = await Promise.all([ + this.octokit.repos.get({ owner, repo }), + this.octokit.issues.listForRepo({ owner, repo, state: 'open' }), + this.octokit.pulls.list({ owner, repo, state: 'open' }), + this.octokit.repos.listCommits({ + owner, + repo, + per_page: 100, + since: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString() + }), + ]); + + const stats = { + name: repoData.data.name, + stars: repoData.data.stargazers_count, + forks: repoData.data.forks_count, + openIssues: issues.data.length, + openPRs: pulls.data.length, + commitsLastMonth: commits.data.length, + language: repoData.data.language, + updatedAt: repoData.data.updated_at, + }; + + await redis.set(cacheKey, stats, { ex: 300 }); + return stats; + } + } + +### **前端 - API 客户端 (`frontend/src/lib/api.ts`)** + + import axios from 'axios'; + + const api = axios.create({ + baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api', + timeout: 10000, + }); + + // 请求拦截器:添加 token + api.interceptors.request.use((config) => { + const token = localStorage.getItem('token'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; + }); + + // 响应拦截器:处理错误 + api.interceptors.response.use( + (response) => response, + (error) => { + if (error.response?.status === 401) { + localStorage.removeItem('token'); + window.location.href = '/login'; + } + return Promise.reject(error); + } + ); + + export default api; + +### **前端 - 数据获取 Hook (`frontend/src/hooks/useRepos.ts`)** + + import { useQuery } from '@tanstack/react-query'; + import api from '@/lib/api'; + + interface Repository { + id: number; + name: string; + full_name: string; + description: string; + stargazers_count: number; + open_issues_count: number; + language: string; + } + + export function useRepositories() { + return useQuery({ + queryKey: ['repositories'], + queryFn: async () => { + const { data } = await api.get('/repos'); + return data; + }, + staleTime: 5 * 60 * 1000, // 5分钟 + refetchOnWindowFocus: false, + }); + } + + export function useRepositoryStats(owner: string, repo: string) { + return useQuery({ + queryKey: ['repo-stats', owner, repo], + queryFn: async () => { + const { data } = await api.get(`/repos/${owner}/${repo}/stats`); + return data; + }, + enabled: !!owner && !!repo, + staleTime: 5 * 60 * 1000, + }); + } + +* * * + +🔐 **认证流程** + + ┌────────┐ ┌──────────┐ ┌─────────┐ + │ 前端 │ │ 后端 │ │ GitHub │ + └───┬────┘ └────┬─────┘ └────┬────┘ + │ │ │ + │ 1. 点击 "Login with GitHub" │ │ + ├──────────────────────────>│ │ + │ │ │ + │ 2. 重定向到 GitHub OAuth │ │ + ├───────────────────────────┼─────────────────────────>│ + │ │ │ + │ 3. 用户授权 │ │ + │ │<──────────────────────────┤ + │ │ │ + │ 4. GitHub 回调 + code │ │ + │<───────────────────────────┤ │ + │ │ │ + │ 5. 发送 code 到后端 │ │ + ├──────────────────────────>│ │ + │ │ │ + │ │ 6. 用 code 换 access_token│ + │ ├─────────────────────────>│ + │ │ │ + │ │<──────────────────────────┤ + │ │ │ + │ 7. 返回 JWT token │ │ + │<───────────────────────────┤ │ + │ │ │ + │ 8. 保存 token 到 localStorage │ + │ │ │ + +* * * + +📊 **数据流** + + 前端 Dashboard 页面加载: + + 1. 检查 localStorage 是否有 token + ├─ 无 → 跳转登录页 + └─ 有 → 继续 + + 2. 发起并行请求(TanStack Query): + ├─ GET /api/repos (获取仓库列表) + ├─ GET /api/stats/overview (总体统计) + └─ GET /api/stats/activity (活动数据) + + 3. 后端处理: + ├─ 验证 JWT token + ├─ 检查 Redis 缓存 + │ ├─ 命中 → 直接返回 + │ └─ 未命中 → 调用 GitHub API + 写入缓存 + └─ 返回数据 + + 4. 前端渲染: + ├─ TanStack Query 自动管理加载/错误状态 + ├─ 渲染图表(Recharts) + └─ 显示数据卡片 + +* * * + +🎯 **开发步骤** + +### **1. 初始化项目** + + # 创建项目目录 + mkdir github-dashboard + cd github-dashboard + + # 初始化前端 + npm create vite@latest frontend -- --template react-ts + cd frontend + npm install + npm install @tanstack/react-query axios zustand + npm install -D tailwindcss postcss autoprefixer + npx tailwindcss init -p + + # 初始化后端 + cd .. + mkdir backend && cd backend + npm init -y + npm install fastify @fastify/cors @fastify/jwt + npm install @octokit/rest @upstash/redis @prisma/client + npm install -D typescript @types/node tsx prisma + npx prisma init + +### **2. 配置 Railway 部署** + + cd backend + + # 创建 railway.json + cat > railway.json << EOF + { + "build": { + "builder": "NIXPACKS" + }, + "deploy": { + "startCommand": "npm run start" + } + } + EOF + + # 部署 + railway login + railway init + railway up + +### **3. 配置 Cloudflare Pages** + + cd frontend + + # 构建测试 + npm run build + + # 在 Cloudflare Pages Dashboard: + # 1. 连接 GitHub 仓库 + # 2. 设置构建命令: npm run build + # 3. 设置输出目录: dist + # 4. 添加环境变量: VITE_API_URL=你的Railway后端URL + +* * * + +🎉 **总结** + +这个架构的优势: + +✅ **前后端分离** - 各自独立部署和扩展✅ **轻量高效** - Fastify 性能优秀,Vite 开发体验好✅ **部署简单** - Railway + Cloudflare Pages 都是一键部署✅ **成本低廉** - 免费额度足够个人/小团队使用✅ **可扩展性** - 需要时可轻松升级到付费版✅ **现代化** - TypeScript 全栈,类型安全 + +需要我提供: + +1. ✅ 完整的项目初始化脚本? +2. ✅ Prisma Schema 数据库设计? +3. ✅ 前端 Dashboard 组件代码? +4. ✅ 详细的部署文档? + +选择任意方向深入!🚀 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e45d685..3a95bd8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,15 +11,20 @@ "dependencies": { "@fastify/cors": "^11.1.0", "@fastify/jwt": "^10.0.0", - "@octokit/rest": "^22.0.0", + "@fastify/swagger": "^9.5.2", + "@octokit/rest": "^19.0.13", "@prisma/client": "^6.18.0", "@upstash/redis": "^1.35.6", - "fastify": "^5.6.1" + "axios": "^1.12.2", + "dotenv": "^17.2.3", + "fastify": "^5.6.1", + "fastify-type-provider-zod": "^6.0.0", + "zod": "^4.1.12" }, "devDependencies": { "@types/node": "^20.12.12", - "dotenv": "^17.2.3", "nodemon": "^3.1.0", + "pino-pretty": "^13.1.2", "prisma": "^6.18.0", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" @@ -183,6 +188,28 @@ "ipaddr.js": "^2.1.0" } }, + "node_modules/@fastify/swagger": { + "version": "9.5.2", + "resolved": "https://registry.npmjs.org/@fastify/swagger/-/swagger-9.5.2.tgz", + "integrity": "sha512-8e8w/LItg/cF6IR/hYKtnt+E0QImees5o3YWJsTLxaIk+tzNUEc6Z2Ursi4oOHWwUlKjUCnV6yh5z5ZdxvlsWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "dependencies": { + "fastify-plugin": "^5.0.0", + "json-schema-resolver": "^3.0.0", + "openapi-types": "^12.1.3", + "rfdc": "^1.3.1", + "yaml": "^2.4.2" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -217,145 +244,160 @@ } }, "node_modules/@octokit/auth-token": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", - "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz", + "integrity": "sha512-TWFX7cZF2LXoCvdmJWY7XVPi74aSY0+FfBZNSXEXFkMpjcqsQwDSYVv5FhRFaI0V1ECnwbz4j59T/G+rXNWaIQ==", "engines": { - "node": ">= 20" + "node": ">= 14" } }, "node_modules/@octokit/core": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.5.tgz", - "integrity": "sha512-t54CUOsFMappY1Jbzb7fetWeO0n6K0k/4+/ZpkS+3Joz8I4VcvY9OiEBFRYISqaI2fq5sCiPtAjRDOzVYG8m+Q==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-4.2.4.tgz", + "integrity": "sha512-rYKilwgzQ7/imScn3M9/pFfUf4I1AZEH3KhyJmtPdE2zfaXAn2mFfUy4FbKewzc2We5y/LlKLj36fWJLKC2SIQ==", "dependencies": { - "@octokit/auth-token": "^6.0.0", - "@octokit/graphql": "^9.0.2", - "@octokit/request": "^10.0.4", - "@octokit/request-error": "^7.0.1", - "@octokit/types": "^15.0.0", - "before-after-hook": "^4.0.0", - "universal-user-agent": "^7.0.0" + "@octokit/auth-token": "^3.0.0", + "@octokit/graphql": "^5.0.0", + "@octokit/request": "^6.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^9.0.0", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 20" + "node": ">= 14" } }, "node_modules/@octokit/endpoint": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.1.tgz", - "integrity": "sha512-7P1dRAZxuWAOPI7kXfio88trNi/MegQ0IJD3vfgC3b+LZo1Qe6gRJc2v0mz2USWWJOKrB2h5spXCzGbw+fAdqA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-7.0.6.tgz", + "integrity": "sha512-5L4fseVRUsDFGR00tMWD/Trdeeihn999rTMGRMC1G/Ldi1uWlWJzI98H4Iak5DB/RVvQuyMYKqSK/R6mbSOQyg==", "dependencies": { - "@octokit/types": "^15.0.0", - "universal-user-agent": "^7.0.2" + "@octokit/types": "^9.0.0", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 20" + "node": ">= 14" } }, "node_modules/@octokit/graphql": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.2.tgz", - "integrity": "sha512-iz6KzZ7u95Fzy9Nt2L8cG88lGRMr/qy1Q36ih/XVzMIlPDMYwaNLE/ENhqmIzgPrlNWiYJkwmveEetvxAgFBJw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-5.0.6.tgz", + "integrity": "sha512-Fxyxdy/JH0MnIB5h+UQ3yCoh1FG4kWXfFKkpWqjZHw/p+Kc8Y44Hu/kCgNBT6nU1shNumEchmW/sUO1JuQnPcw==", "dependencies": { - "@octokit/request": "^10.0.4", - "@octokit/types": "^15.0.0", - "universal-user-agent": "^7.0.0" + "@octokit/request": "^6.0.0", + "@octokit/types": "^9.0.0", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 20" + "node": ">= 14" } }, "node_modules/@octokit/openapi-types": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz", - "integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==" + "version": "18.1.1", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-18.1.1.tgz", + "integrity": "sha512-VRaeH8nCDtF5aXWnjPuEMIYf1itK/s3JYyJcWFJT8X9pSNnBtriDf7wlEWsGuhPLl4QIH4xM8fqTXDwJ3Mu6sw==" }, "node_modules/@octokit/plugin-paginate-rest": { - "version": "13.2.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.2.1.tgz", - "integrity": "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw==", + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-6.1.2.tgz", + "integrity": "sha512-qhrmtQeHU/IivxucOV1bbI/xZyC/iOBhclokv7Sut5vnejAIAEXVcGQeRpQlU39E0WwK9lNvJHphHri/DB6lbQ==", "dependencies": { - "@octokit/types": "^15.0.1" + "@octokit/tsconfig": "^1.0.2", + "@octokit/types": "^9.2.3" }, "engines": { - "node": ">= 20" + "node": ">= 14" }, "peerDependencies": { - "@octokit/core": ">=6" + "@octokit/core": ">=4" } }, "node_modules/@octokit/plugin-request-log": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", - "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", - "engines": { - "node": ">= 20" - }, + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", "peerDependencies": { - "@octokit/core": ">=6" + "@octokit/core": ">=3" } }, "node_modules/@octokit/plugin-rest-endpoint-methods": { - "version": "16.1.1", - "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.1.1.tgz", - "integrity": "sha512-VztDkhM0ketQYSh5Im3IcKWFZl7VIrrsCaHbDINkdYeiiAsJzjhS2xRFCSJgfN6VOcsoW4laMtsmf3HcNqIimg==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-7.2.3.tgz", + "integrity": "sha512-I5Gml6kTAkzVlN7KCtjOM+Ruwe/rQppp0QU372K1GP7kNOYEKe8Xn5BW4sE62JAHdwpq95OQK/qGNyKQMUzVgA==", "dependencies": { - "@octokit/types": "^15.0.1" + "@octokit/types": "^10.0.0" }, "engines": { - "node": ">= 20" + "node": ">= 14" }, "peerDependencies": { - "@octokit/core": ">=6" + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods/node_modules/@octokit/types": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-10.0.0.tgz", + "integrity": "sha512-Vm8IddVmhCgU1fxC1eyinpwqzXPEYu0NrYzD3YZjlGjyftdLBTeqNblRC0jmJmgxbJIsQlyogVeGnrNaaMVzIg==", + "dependencies": { + "@octokit/openapi-types": "^18.0.0" } }, "node_modules/@octokit/request": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.5.tgz", - "integrity": "sha512-TXnouHIYLtgDhKo+N6mXATnDBkV05VwbR0TtMWpgTHIoQdRQfCSzmy/LGqR1AbRMbijq/EckC/E3/ZNcU92NaQ==", + "version": "6.2.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-6.2.8.tgz", + "integrity": "sha512-ow4+pkVQ+6XVVsekSYBzJC0VTVvh/FCTUUgTsboGq+DTeWdyIFV8WSCdo0RIxk6wSkBTHqIK1mYuY7nOBXOchw==", "dependencies": { - "@octokit/endpoint": "^11.0.1", - "@octokit/request-error": "^7.0.1", - "@octokit/types": "^15.0.0", - "fast-content-type-parse": "^3.0.0", - "universal-user-agent": "^7.0.2" + "@octokit/endpoint": "^7.0.0", + "@octokit/request-error": "^3.0.0", + "@octokit/types": "^9.0.0", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" }, "engines": { - "node": ">= 20" + "node": ">= 14" } }, "node_modules/@octokit/request-error": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.1.tgz", - "integrity": "sha512-CZpFwV4+1uBrxu7Cw8E5NCXDWFNf18MSY23TdxCBgjw1tXXHvTrZVsXlW8hgFTOLw8RQR1BBrMvYRtuyaijHMA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-3.0.3.tgz", + "integrity": "sha512-crqw3V5Iy2uOU5Np+8M/YexTlT8zxCfI+qu+LxUB7SZpje4Qmx3mub5DfEKSO8Ylyk0aogi6TYdf6kxzh2BguQ==", "dependencies": { - "@octokit/types": "^15.0.0" + "@octokit/types": "^9.0.0", + "deprecation": "^2.0.0", + "once": "^1.4.0" }, "engines": { - "node": ">= 20" + "node": ">= 14" } }, "node_modules/@octokit/rest": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.0.tgz", - "integrity": "sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA==", + "version": "19.0.13", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-19.0.13.tgz", + "integrity": "sha512-/EzVox5V9gYGdbAI+ovYj3nXQT1TtTHRT+0eZPcuC05UFSWO3mdO9UY1C0i2eLF9Un1ONJkAk+IEtYGAC+TahA==", "dependencies": { - "@octokit/core": "^7.0.2", - "@octokit/plugin-paginate-rest": "^13.0.1", - "@octokit/plugin-request-log": "^6.0.0", - "@octokit/plugin-rest-endpoint-methods": "^16.0.0" + "@octokit/core": "^4.2.1", + "@octokit/plugin-paginate-rest": "^6.1.2", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^7.1.2" }, "engines": { - "node": ">= 20" + "node": ">= 14" } }, + "node_modules/@octokit/tsconfig": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@octokit/tsconfig/-/tsconfig-1.0.2.tgz", + "integrity": "sha512-I0vDR0rdtP8p2lGMzvsJzbhdOWy405HcGovrspJ8RRibHnyRgggUSNO5AIox5LmqiwmatHKYsvj6VGFHkqS7lA==" + }, "node_modules/@octokit/types": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.1.tgz", - "integrity": "sha512-sdiirM93IYJ9ODDCBgmRPIboLbSkpLa5i+WLuXH8b8Atg+YMLAyLvDDhNWLV4OYd08tlvYfVm/dw88cqHWtw1Q==", + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-9.3.2.tgz", + "integrity": "sha512-D4iHGTdAnEEVsB8fl95m1hiz7D5YiRdQ9b/OEb3BYRVwbLsGHcRVPz+u+BgRLNk0Q0/4iZCBqDN96j2XNxfXrA==", "dependencies": { - "@octokit/openapi-types": "^26.0.0" + "@octokit/openapi-types": "^18.0.0" } }, "node_modules/@pinojs/redact": { @@ -590,6 +632,11 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/atomic-sleep": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", @@ -607,6 +654,16 @@ "fastq": "^1.17.1" } }, + "node_modules/axios": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", + "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -614,9 +671,9 @@ "dev": true }, "node_modules/before-after-hook": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", - "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==" + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" }, "node_modules/binary-extensions": { "version": "2.3.0", @@ -731,6 +788,18 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -764,6 +833,23 @@ "consola": "^3.2.3" } }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -799,6 +885,31 @@ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", "dev": true }, + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/deepmerge-ts": { "version": "7.1.5", "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", @@ -814,6 +925,19 @@ "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", "devOptional": true }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -841,7 +965,6 @@ "version": "17.2.3", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", - "dev": true, "engines": { "node": ">=12" }, @@ -849,6 +972,19 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -885,6 +1021,56 @@ "node": ">=14" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -913,20 +1099,11 @@ "node": ">=8.0.0" } }, - "node_modules/fast-content-type-parse": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", - "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ] + "node_modules/fast-copy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz", + "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==", + "dev": true }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", @@ -983,6 +1160,12 @@ "fast-decode-uri-component": "^1.0.1" } }, + "node_modules/fast-safe-stringify": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", + "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==", + "dev": true + }, "node_modules/fast-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", @@ -1056,6 +1239,20 @@ } ] }, + "node_modules/fastify-type-provider-zod": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/fastify-type-provider-zod/-/fastify-type-provider-zod-6.0.0.tgz", + "integrity": "sha512-Bz+Qll2XuvvueHz0yhcr67V/43q1VecSyIqZm+P8OL8KZHznUXECZXkuwQePR5b6fWY/kzhhadmgNs9dB/Nifg==", + "dependencies": { + "@fastify/error": "^4.2.0" + }, + "peerDependencies": { + "@fastify/swagger": ">=9.5.1", + "fastify": "^5.0.0", + "openapi-types": "^12.1.3", + "zod": ">=4.1.5" + } + }, "node_modules/fastparallel": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/fastparallel/-/fastparallel-2.4.1.tgz", @@ -1107,6 +1304,40 @@ "node": ">=20" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -1131,11 +1362,45 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/giget": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", @@ -1186,6 +1451,17 @@ "node": ">= 6" } }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/has-flag": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", @@ -1195,11 +1471,35 @@ "node": ">=4" } }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "dependencies": { "function-bind": "^1.1.2" }, @@ -1207,6 +1507,12 @@ "node": ">= 0.4" } }, + "node_modules/help-me": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz", + "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", + "dev": true + }, "node_modules/ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -1294,6 +1600,14 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -1303,6 +1617,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/json-schema-ref-resolver": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", @@ -1321,6 +1644,22 @@ "dequal": "^2.0.3" } }, + "node_modules/json-schema-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-resolver/-/json-schema-resolver-3.0.0.tgz", + "integrity": "sha512-HqMnbz0tz2DaEJ3ntsqtx3ezzZyDE7G56A/pPY/NGmrPu76UzsWquOpHFRAf5beTNXoH2LU5cQePVvRli1nchA==", + "dependencies": { + "debug": "^4.1.1", + "fast-uri": "^3.0.5", + "rfdc": "^1.1.4" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/Eomm/json-schema-resolver?sponsor=1" + } + }, "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", @@ -1367,6 +1706,33 @@ "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -1413,6 +1779,30 @@ "obliterator": "^2.0.4" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/node-fetch-native": { "version": "1.6.7", "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", @@ -1447,29 +1837,6 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/nodemon/node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -1521,11 +1888,15 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } }, + "node_modules/openapi-types": { + "version": "12.1.3", + "resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz", + "integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==" + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", @@ -1594,6 +1965,42 @@ "split2": "^4.0.0" } }, + "node_modules/pino-pretty": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.2.tgz", + "integrity": "sha512-3cN0tCakkT4f3zo9RXDIhy6GTvtYD6bK4CRBLN9j3E/ePqN1tugAXD5rGVfoChW6s0hiek+eyYlLNqc/BG7vBQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.7", + "dateformat": "^4.6.3", + "fast-copy": "^3.0.2", + "fast-safe-stringify": "^2.1.1", + "help-me": "^5.0.0", + "joycon": "^3.1.1", + "minimist": "^1.2.6", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pump": "^3.0.0", + "secure-json-parse": "^4.0.0", + "sonic-boom": "^4.0.1", + "strip-json-comments": "^5.0.2" + }, + "bin": { + "pino-pretty": "bin.js" + } + }, + "node_modules/pino-pretty/node_modules/strip-json-comments": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz", + "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/pino-std-serializers": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz", @@ -1650,12 +2057,27 @@ } ] }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/pure-rand": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", @@ -1995,6 +2417,11 @@ "nodetouch": "bin/nodetouch.js" } }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -2124,9 +2551,9 @@ "dev": true }, "node_modules/universal-user-agent": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", - "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==" + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.1.tgz", + "integrity": "sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==" }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", @@ -2134,11 +2561,24 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/xtend": { "version": "4.0.2", @@ -2148,6 +2588,17 @@ "node": ">=0.4" } }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -2156,6 +2607,14 @@ "engines": { "node": ">=6" } + }, + "node_modules/zod": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz", + "integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index fc0c7b9..1718269 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "main": "dist/server.js", "scripts": { "dev": "ts-node-dev --respawn --transpile-only --exit-child src/server.ts", - "build": "tsc", + "build": "prisma generate && tsc", "start": "node dist/server.js", "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate dev" @@ -14,19 +14,24 @@ "author": "Google LLC", "license": "Apache-2.0", "dependencies": { - "dotenv": "^17.2.3", "@fastify/cors": "^11.1.0", "@fastify/jwt": "^10.0.0", - "@octokit/rest": "^22.0.0", + "@fastify/swagger": "^9.5.2", + "@octokit/rest": "^19.0.13", "@prisma/client": "^6.18.0", "@upstash/redis": "^1.35.6", - "fastify": "^5.6.1" + "axios": "^1.12.2", + "dotenv": "^17.2.3", + "fastify": "^5.6.1", + "fastify-type-provider-zod": "^6.0.0", + "zod": "^4.1.12" }, "devDependencies": { "@types/node": "^20.12.12", "nodemon": "^3.1.0", + "pino-pretty": "^13.1.2", "prisma": "^6.18.0", "ts-node-dev": "^2.0.0", "typescript": "^5.4.5" } -} +} \ No newline at end of file diff --git a/src/routes/repos.ts b/src/routes/repos.ts index 5e9c9ab..7e33f15 100644 --- a/src/routes/repos.ts +++ b/src/routes/repos.ts @@ -1,43 +1,67 @@ import { FastifyInstance } from 'fastify'; +import { z } from 'zod'; import { GitHubService } from '../services/github.service'; import { authMiddleware } from '../middlewares/auth.middleware'; -// Schema for the URL parameters -const repoParamsSchema = { - type: 'object', - required: ['owner', 'repo'], - properties: { - owner: { type: 'string' }, - repo: { type: 'string' }, - }, -} as const; // Using 'as const' for stronger type inference +// Schema for request parameters +const repoParamsSchema = z.object({ + owner: z.string(), + repo: z.string(), +}); -// Schema for the response of the repository stats endpoint -const repoStatsSchema = { - type: 'object', - properties: { - stars: { type: 'number' }, - forks: { type: 'number' }, - openIssues: { type: 'number' }, - commits: { type: 'number' }, - releases: { type: 'number' }, - contributors: { type: 'number' }, - }, -} as const; +// Schema for repository statistics response +const repoStatsSchema = z.object({ + stars: z.number(), + forks: z.number(), + openIssues: z.number(), + commits: z.number(), + releases: z.number(), + contributors: z.number(), +}); + +// Schema for repository list response +const repoListItemSchema = z.object({ + id: z.number(), + name: z.string(), + full_name: z.string(), + private: z.boolean(), + html_url: z.string().url(), + description: z.string().nullable(), + stargazers_count: z.number(), + watchers_count: z.number(), + forks_count: z.number(), + language: z.string().nullable(), + owner: z.object({ + login: z.string(), + avatar_url: z.string().url(), + }), +}); + +const reposListResponseSchema = z.array(repoListItemSchema); export async function repoRoutes(app: FastifyInstance) { + // All routes in this plugin require authentication app.addHook('preHandler', authMiddleware); - // This route retrieves all repositories for the authenticated user. - // The response type is inferred from Octokit, so a detailed schema is omitted for brevity, - // but in a real-world scenario, you might want to define the properties you actually use. - app.get('/', async (request, reply) => { - const githubService = new GitHubService(request.user.accessToken); - const repos = await githubService.getRepositories(); - reply.send(repos); - }); + // Route to list repositories for the authenticated user + app.get( + '/', + { + schema: { + response: { + 200: reposListResponseSchema, + }, + }, + }, + async (request, reply) => { + const { accessToken, username } = request.user!; + const githubService = new GitHubService(accessToken, username); + const repos = await githubService.getRepositories(); + return repos; + }, + ); - // This route gets specific stats for a single repository. + // Route to get statistics for a specific repository app.get( '/:owner/:repo/stats', { @@ -49,13 +73,10 @@ export async function repoRoutes(app: FastifyInstance) { }, }, async (request, reply) => { - // request.params is now strongly typed based on repoParamsSchema! - const { owner, repo } = request.params; - const githubService = new GitHubService(request.user.accessToken); - + const { owner, repo } = request.params as z.infer; + const { accessToken, username } = request.user!; + const githubService = new GitHubService(accessToken, username); const stats = await githubService.getRepositoryStats(owner, repo); - - // The return type is validated against repoStatsSchema return stats; }, ); diff --git a/src/routes/stats.ts b/src/routes/stats.ts index 72682de..a4542c1 100644 --- a/src/routes/stats.ts +++ b/src/routes/stats.ts @@ -1,5 +1,21 @@ import { FastifyInstance } from 'fastify'; +import { StatsService } from '../services/stats.service'; +import { authMiddleware } from '../middlewares/auth.middleware'; export async function statsRoutes(app: FastifyInstance) { - // TODO: Implement stats routes + app.addHook('preHandler', authMiddleware); + + app.get('/overview', async (request, reply) => { + const { accessToken, username } = request.user!; + const statsService = new StatsService(accessToken, username); + const overviewStats = await statsService.getOverviewStats(); + reply.send(overviewStats); + }); + + app.get('/activity', async (request, reply) => { + const { accessToken, username } = request.user!; + const statsService = new StatsService(accessToken, username); + const activityStats = await statsService.getActivityStats(); + reply.send(activityStats); + }); } diff --git a/src/server.ts b/src/server.ts index ef3105e..b4f054b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,40 +1,123 @@ import fastify from 'fastify'; -import { authRoutes } from './routes/auth'; -import { webhookRoutes } from './routes/webhooks'; -import { repoRoutes } from './routes/repos'; +import { + serializerCompiler, + validatorCompiler, + ZodTypeProvider +} from 'fastify-type-provider-zod'; import fastifyJwt from '@fastify/jwt'; import fastifyCors from '@fastify/cors'; import 'dotenv/config'; -declare module 'fastify' { - interface FastifyRequest { - user?: { - sub: string; - username: string; - avatarUrl: string; - accessToken: string; - }; - } -} +import { authRoutes } from './routes/auth'; +import { repoRoutes } from './routes/repos'; +import { statsRoutes } from './routes/stats'; +import { webhookRoutes } from './routes/webhooks'; +import swagger from '@fastify/swagger'; + +// Main function to bootstrap the server async function bootstrap() { + // Initialize Fastify with ZodTypeProvider const app = fastify({ - logger: true, - }); + logger: { + transport: { + target: 'pino-pretty', + options: { + translateTime: 'HH:MM:ss Z', + ignore: 'pid,hostname', + }, + }, + }, + }).withTypeProvider(); - app.register(fastifyJwt, { + // Set the validator and serializer for Zod + app.setValidatorCompiler(validatorCompiler); + app.setSerializerCompiler(serializerCompiler); + + // Register plugins + await app.register(fastifyJwt, { secret: process.env.JWT_SECRET!, }); - app.register(fastifyCors, { - origin: process.env.FRONTEND_URL, + await app.register(fastifyCors, { + origin: process.env.FRONTEND_URL || 'http://localhost:3000', + credentials: true, }); - app.register(authRoutes, { prefix: '/auth' }); - app.register(webhookRoutes, { prefix: '/webhooks' }); - app.register(repoRoutes, { prefix: '/repos' }); + // Register Swagger for OpenAPI spec generation + await app.register(swagger, { + openapi: { + info: { + title: 'Project Dash API', + description: 'Backend API documentation for Project Dash application.', + version: '1.0.0', + }, + components: { + securitySchemes: { + bearerAuth: { + type: 'http', + scheme: 'bearer', + bearerFormat: 'JWT', + }, + }, + }, + security: [ + { + bearerAuth: [], + }, + ], + }, + }); - await app.listen({ port: 3333, host: '0.0.0.0' }); + // --- API Documentation with RapiDoc --- + app.get('/docs', (req, reply) => { + reply.type('text/html').send(` + + + + + + + + + + + `); + }); + + app.get('/docs/json', (req, reply) => { + reply.send(app.swagger()); + }); + + // Register routes + await app.register(authRoutes, { prefix: '/auth' }); + await app.register(repoRoutes, { prefix: '/repos' }); + await app.register(statsRoutes, { prefix: '/stats' }); + await app.register(webhookRoutes, { prefix: '/webhooks' }); + + // Add a generic error handler + app.setErrorHandler((error, request, reply) => { + app.log.error(error); + reply.status(500).send({ error: 'Internal Server Error' }); + }); + + return app; } -bootstrap(); +// Start the server +(async () => { + try { + const app = await bootstrap(); + await app.listen({ port: 3333, host: '0.0.0.0' }); + } catch (err) { + console.error(err); + process.exit(1); + } +})(); diff --git a/src/services/github.service.ts b/src/services/github.service.ts index fd00b1e..bbffcc6 100644 --- a/src/services/github.service.ts +++ b/src/services/github.service.ts @@ -7,7 +7,8 @@ import { GitHubUser } from '../types/github'; const GITHUB_API_BASE_URL = 'https://api.github.com'; // Define a type for the repository list response data for clarity -type ReposListForAuthenticatedUserResponse = Endpoints['GET /user/repos']['response']['data']; +type ReposListForAuthenticatedUserResponse = + Endpoints['GET /user/repos']['response']['data']; export async function exchangeCodeForToken(code: string): Promise { const response = await axios.post( @@ -35,27 +36,31 @@ export async function getGithubUser(accessToken: string): Promise { export class GitHubService { private octokit: Octokit; + private username: string; - constructor(accessToken: string) { + constructor(accessToken: string, username: string) { this.octokit = new Octokit({ auth: accessToken }); + this.username = username; } async getRepositories(): Promise { + const cacheKey = `repos:${this.username}`; + const cachedRepos = await redis.get(cacheKey); + + if (cachedRepos) { + return JSON.parse(cachedRepos as string); + } + const { data } = await this.octokit.repos.listForAuthenticatedUser(); + await redis.set(cacheKey, JSON.stringify(data), { ex: 3600 }); // Cache for 1 hour + return data; } - async getRepositoryStats(owner: string, repo: string) { - const cacheKey = `repo-stats:${owner}:${repo}`; - const cachedStats = await redis.get(cacheKey); - - if (cachedStats) { - return JSON.parse(cachedStats as string); - } - + async getRepositoryStats(owner: string, repo: string, since?: string) { const [repoData, commits, releases, contributors] = await Promise.all([ this.octokit.repos.get({ owner, repo }), - this.octokit.repos.listCommits({ owner, repo }), + this.octokit.repos.listCommits({ owner, repo, since }), this.octokit.repos.listReleases({ owner, repo }), this.octokit.repos.listContributors({ owner, repo }), ]); @@ -69,8 +74,6 @@ export class GitHubService { contributors: contributors.data.length, }; - await redis.set(cacheKey, JSON.stringify(stats), { ex: 3600 }); // Cache for 1 hour - return stats; } } diff --git a/src/services/stats.service.ts b/src/services/stats.service.ts new file mode 100644 index 0000000..0c2d53e --- /dev/null +++ b/src/services/stats.service.ts @@ -0,0 +1,42 @@ +import { prisma } from '../lib/prisma'; +import { GitHubService } from './github.service'; + +export class StatsService { + private githubService: GitHubService; + + constructor(accessToken: string, username: string) { + this.githubService = new GitHubService(accessToken, username); + } + + async getOverviewStats() { + const repos = await this.githubService.getRepositories(); + const totalStars = repos.reduce((acc, repo) => acc + (repo.stargazers_count || 0), 0); + const totalForks = repos.reduce((acc, repo) => acc + (repo.forks_count || 0), 0); + + return { + totalRepos: repos.length, + totalStars, + totalForks, + }; + } + + async getActivityStats() { + const repos = await this.githubService.getRepositories(); + const thirtyDaysAgo = new Date( + Date.now() - 30 * 24 * 60 * 60 * 1000, + ).toISOString(); + + const commitPromises = repos.map((repo) => { + const [owner, repoName] = repo.full_name.split('/'); + return this.githubService.getRepositoryStats(owner, repoName, thirtyDaysAgo); + }); + + const allStats = await Promise.all(commitPromises); + + const totalCommits = allStats.reduce((acc, stats) => acc + stats.commits, 0); + + return { + totalCommitsLast30Days: totalCommits, + }; + } +} diff --git a/src/types/fastify.d.ts b/src/types/fastify.d.ts new file mode 100644 index 0000000..5a54f0e --- /dev/null +++ b/src/types/fastify.d.ts @@ -0,0 +1,14 @@ +import '@fastify/jwt'; + +declare module '@fastify/jwt' { + interface FastifyJWT { + // This is the type for the payload that is signed + // and the type of the `request.user` object. + user: { + sub: string; + username: string; + avatarUrl: string; + accessToken: string; + }; + } +} diff --git a/tsconfig.json b/tsconfig.json index 878433b..64f3ffd 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,9 +1,18 @@ { "compilerOptions": { - "target": "es5", + "target": "ES2020", // More modern target "module": "commonjs", - "sourceMap": true, - "outDir": "dist", - "esModuleInterop": true - } + "strict": true, // Enable all strict type-checking options + "esModuleInterop": true, + "skipLibCheck": true, // Skip type checking of declaration files + "forceConsistentCasingInFileNames": true, + "outDir": "./dist", + "rootDir": "./src", + "sourceMap": true + }, + "include": [ + "src/**/*.ts", // Include all .ts files in the src directory + "src/types/**/*.d.ts" // Include our custom type definitions + ], + "exclude": ["node_modules"] }