refactor: zod schema
This commit is contained in:
parent
57f964744f
commit
434da70b06
7
.env.example
Normal file
7
.env.example
Normal file
@ -0,0 +1,7 @@
|
||||
DATABASE_URL="your-database-url"
|
||||
JWT_SECRET="your-jwt-secret"
|
||||
GITHUB_CLIENT_ID="your-github-client-id"
|
||||
GITHUB_CLIENT_SECRET="your-github-client-secret"
|
||||
FRONTEND_URL="http://localhost:5173"
|
||||
UPSTASH_REDIS_URL="http://localhost:6379"
|
||||
UPSTASH_REDIS_TOKEN="your-upstash-redis-token"
|
||||
123
.idx/airules.md
123
.idx/airules.md
@ -1,123 +0,0 @@
|
||||
# Gemini AI Rules for Node.js with Express Projects
|
||||
|
||||
## 1. Persona & Expertise
|
||||
|
||||
You are an expert back-end developer with a deep specialization in Node.js and the Express framework. You are proficient in building robust, scalable, and secure APIs. Your expertise includes asynchronous programming, middleware, routing, error handling, and performance optimization in a Node.js environment. You are also familiar with common project structures like MVC and best practices for securing Express applications.
|
||||
|
||||
## 2. Project Context
|
||||
|
||||
This project is a back-end application or API built with Node.js and the Express framework. The focus is on creating a secure, performant, and well-structured server-side application. Assume the project uses modern JavaScript (ES6+) or TypeScript.
|
||||
|
||||
## 3. Coding Standards & Best Practices
|
||||
|
||||
### General
|
||||
- **Language:** Use modern JavaScript (ES6+) or TypeScript, depending on the project's configuration.
|
||||
- **Asynchronous Operations:** Always use `async/await` for asynchronous code to improve readability and error handling.
|
||||
- **Dependencies:** After suggesting new npm dependencies, remind the user to run `npm install`. Regularly audit dependencies for vulnerabilities using `npm audit`.
|
||||
- **Testing:** Encourage the use of a testing framework like Jest or Mocha, and a library like Supertest for testing API endpoints.
|
||||
|
||||
### Node.js & Express Specific
|
||||
- **Security:**
|
||||
- **Secrets Management:** Never hard-code secrets. Use environment variables (and a `.env` file) for all sensitive information.
|
||||
- **Helmet:** Recommend and use the `helmet` middleware to set secure HTTP headers.
|
||||
- **Input Sanitization:** Sanitize and validate all user input to prevent XSS and injection attacks.
|
||||
- **Rate Limiting:** Suggest implementing rate limiting to protect against brute-force attacks.
|
||||
- **Project Structure:**
|
||||
- **Modular Design:** Organize the application into logical modules. Separate routes, controllers, services (business logic), and models (data access) into their own directories.
|
||||
- **Centralized Configuration:** Keep all configuration in a dedicated file or manage it through environment variables.
|
||||
- **Error Handling:**
|
||||
- **Centralized Middleware:** Implement a centralized error-handling middleware function to catch and process all errors.
|
||||
- **Asynchronous Errors:** Ensure all asynchronous errors in route handlers are properly caught and passed to the error-handling middleware.
|
||||
- **Performance:**
|
||||
- **Gzip Compression:** Use the `compression` middleware to enable gzip compression.
|
||||
- **Caching:** Recommend caching strategies for frequently accessed data.
|
||||
- **Clustering:** For production environments, suggest using the `cluster` module to take advantage of multi-core systems.
|
||||
|
||||
### Building AI Features with the Gemini SDK (`@google/generative-ai`)
|
||||
|
||||
You can easily integrate powerful generative AI features into your Express application using the official Google AI Gemini SDK.
|
||||
|
||||
**1. Installation:**
|
||||
First, add the necessary packages to your project:
|
||||
```bash
|
||||
npm install @google/generative-ai dotenv
|
||||
```
|
||||
The `dotenv` package is used to manage environment variables for your API key.
|
||||
|
||||
**2. Secure API Key Setup:**
|
||||
Never hard-code your API key. Create a `.env` file in your project's root directory and add your key:
|
||||
```
|
||||
# .env
|
||||
GEMINI_API_KEY="YOUR_API_KEY"
|
||||
```
|
||||
Make sure to add `.env` to your `.gitignore` file to keep it out of version control.
|
||||
|
||||
**3. Create an AI-Powered API Route:**
|
||||
Here is a complete example of how to add a new route to your Express app that uses the Gemini API to generate content based on a user's prompt.
|
||||
|
||||
**File: `index.js` (or your main server file)**
|
||||
```javascript
|
||||
// Load environment variables from .env file
|
||||
require('dotenv').config();
|
||||
|
||||
const express = require('express');
|
||||
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
||||
|
||||
const app = express();
|
||||
// Middleware to parse JSON request bodies
|
||||
app.use(express.json());
|
||||
|
||||
// Check for API key on startup
|
||||
if (!process.env.GEMINI_API_KEY) {
|
||||
throw new Error('GEMINI_API_KEY environment variable is not set.');
|
||||
}
|
||||
|
||||
// Initialize the Google AI client with the API key
|
||||
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY);
|
||||
|
||||
// Define a POST route to handle content generation
|
||||
app.post('/api/generate', async (req, res) => {
|
||||
try {
|
||||
const { prompt } = req.body;
|
||||
|
||||
if (!prompt) {
|
||||
return res.status(400).json({ error: 'Prompt is required' });
|
||||
}
|
||||
|
||||
// Use a recent, powerful model
|
||||
const model = genAI.getGenerativeModel({ model: 'gemini-1.5-pro' });
|
||||
const result = await model.generateContent(prompt);
|
||||
const response = await result.response;
|
||||
const text = response.text();
|
||||
|
||||
// Send the generated text back to the client
|
||||
res.json({ generatedText: text });
|
||||
} catch (error) {
|
||||
console.error('Error calling Gemini API:', error);
|
||||
res.status(500).json({ error: 'Failed to generate content' });
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.env.PORT || 3000;
|
||||
app.listen(port, () => {
|
||||
console.log(`Server is running on http://localhost:${port}`);
|
||||
});
|
||||
```
|
||||
|
||||
**4. How to Test the Endpoint:**
|
||||
You can use a tool like `curl` to test your new endpoint:
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/generate \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"prompt": "Write a short poem about Node.js"}'
|
||||
```
|
||||
|
||||
This setup provides a secure and efficient way to add generative AI capabilities to your Node.js and Express backend.
|
||||
|
||||
## 4. Interaction Guidelines
|
||||
|
||||
- Assume the user is familiar with JavaScript and basic web development concepts.
|
||||
- Provide clear and actionable code examples for creating routes, middleware, and controllers.
|
||||
- Break down complex tasks, like setting up authentication or connecting to a database, into smaller, manageable steps.
|
||||
- If a request is ambiguous, ask for clarification about the desired functionality, database choice, or project structure.
|
||||
- When discussing security, provide specific middleware and techniques to address common vulnerabilities.
|
||||
28
.idx/dev.nix
28
.idx/dev.nix
@ -1,28 +0,0 @@
|
||||
# To learn more about how to use Nix to configure your environment
|
||||
# see: https://developers.google.com/idx/guides/customize-idx-env
|
||||
{ pkgs, ... }: {
|
||||
# Which nixpkgs channel to use.
|
||||
channel = "stable-23.11"; # or "unstable"
|
||||
# Use https://search.nixos.org/packages to find packages
|
||||
packages = [
|
||||
pkgs.nodejs_20
|
||||
];
|
||||
# Sets environment variables in the workspace
|
||||
env = {};
|
||||
idx = {
|
||||
# Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
|
||||
extensions = [
|
||||
"google.gemini-cli-vscode-ide-companion"
|
||||
];
|
||||
workspace = {
|
||||
# Runs when a workspace is first created with this `dev.nix` file
|
||||
onCreate = {
|
||||
npm-install = "npm ci --no-audit --prefer-offline --no-progress --timing";
|
||||
};
|
||||
# Runs when a workspace is (re)started
|
||||
onStart= {
|
||||
run-server = "npm run dev";
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@ -18,6 +18,7 @@
|
||||
"@fastify/jwt": "^10.0.0",
|
||||
"@fastify/swagger": "^9.5.2",
|
||||
"@octokit/rest": "^19.0.13",
|
||||
"@octokit/types": "^15.0.1",
|
||||
"@prisma/client": "^6.18.0",
|
||||
"@upstash/redis": "^1.35.6",
|
||||
"axios": "^1.12.2",
|
||||
|
||||
1907
pnpm-lock.yaml
generated
Normal file
1907
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
4
pnpm-workspace.yaml
Normal file
4
pnpm-workspace.yaml
Normal file
@ -0,0 +1,4 @@
|
||||
onlyBuiltDependencies:
|
||||
- '@prisma/client'
|
||||
- '@prisma/engines'
|
||||
- prisma
|
||||
@ -1,3 +1,9 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
export const prisma = new PrismaClient();
|
||||
// Prevent creating multiple instances of PrismaClient in development
|
||||
// when using hot-reloading (ts-node-dev / nodemon).
|
||||
const g = global as any;
|
||||
|
||||
export const prisma: PrismaClient = g.__prisma ?? new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') g.__prisma = prisma;
|
||||
|
||||
@ -14,6 +14,4 @@ export function verifyGithubWebhook(req: FastifyRequest, reply: FastifyReply) {
|
||||
if (digest !== signature) {
|
||||
return reply.status(401).send({ error: 'Invalid signature' });
|
||||
}
|
||||
|
||||
done();
|
||||
}
|
||||
|
||||
@ -1,67 +1,78 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
||||
import { z } from 'zod';
|
||||
import { exchangeCodeForToken, getGithubUser } from '../services/github.service';
|
||||
import { prisma } from '../lib/prisma';
|
||||
import { createResponseSchema, successResponse, errorResponse, ErrorCode } from '../types/response';
|
||||
|
||||
const loginSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
required: ['code'],
|
||||
properties: {
|
||||
code: { type: 'string' },
|
||||
},
|
||||
},
|
||||
response: {
|
||||
200: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
token: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as const;
|
||||
// Schema for login request body
|
||||
const loginBodySchema = z.object({
|
||||
code: z.string().min(1, 'Authorization code is required'),
|
||||
});
|
||||
|
||||
export async function authRoutes(app: FastifyInstance) {
|
||||
// Schema for login response data
|
||||
const loginDataSchema = z.object({
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
// Use the unified response format
|
||||
const loginSuccessSchema = createResponseSchema(loginDataSchema);
|
||||
const loginErrorSchema = createResponseSchema(z.null());
|
||||
|
||||
export const authRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.post(
|
||||
'/login',
|
||||
{ schema: loginSchema },
|
||||
{
|
||||
schema: {
|
||||
description: 'Login with GitHub OAuth code',
|
||||
tags: ['auth'],
|
||||
body: loginBodySchema,
|
||||
response: {
|
||||
200: loginSuccessSchema,
|
||||
500: loginErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
// The type for request.body is inferred from the schema
|
||||
// ✅ Type is automatically inferred from Zod schema
|
||||
const { code } = request.body;
|
||||
|
||||
try {
|
||||
const accessToken = await exchangeCodeForToken(code);
|
||||
const githubUser = await getGithubUser(accessToken);
|
||||
|
||||
// Convert number to string as Prisma schema expects String
|
||||
let user = await prisma.user.findUnique({
|
||||
where: { githubId: githubUser.id },
|
||||
where: { githubId: String(githubUser.id) },
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
user = await prisma.user.create({
|
||||
data: {
|
||||
githubId: githubUser.id, // Corrected: Use number directly
|
||||
githubId: String(githubUser.id),
|
||||
username: githubUser.login,
|
||||
avatarUrl: githubUser.avatar_url,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// The payload now matches the global FastifyJWT type
|
||||
// Generate JWT token
|
||||
const token = app.jwt.sign(
|
||||
{
|
||||
sub: user.id,
|
||||
username: user.username,
|
||||
avatarUrl: user.avatarUrl,
|
||||
accessToken, // The user's GitHub token
|
||||
accessToken,
|
||||
},
|
||||
{ expiresIn: '1d' },
|
||||
);
|
||||
|
||||
return { token };
|
||||
return successResponse({ token });
|
||||
} catch (error) {
|
||||
app.log.error('Authentication Error:', error);
|
||||
reply.status(500).send({ error: 'Authentication failed' });
|
||||
app.log.error({ error }, 'Authentication Error');
|
||||
return reply
|
||||
.status(500)
|
||||
.send(errorResponse(ErrorCode.AUTH_FAILED, 'Authentication failed'));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
||||
import { z } from 'zod';
|
||||
import { GitHubService } from '../services/github.service';
|
||||
import { authMiddleware } from '../middlewares/auth.middleware';
|
||||
import { createResponseSchema, successResponse } from '../types/response';
|
||||
|
||||
// Schema for request parameters
|
||||
const repoParamsSchema = z.object({
|
||||
@ -37,9 +38,13 @@ const repoListItemSchema = z.object({
|
||||
}),
|
||||
});
|
||||
|
||||
const reposListResponseSchema = z.array(repoListItemSchema);
|
||||
const reposListSchema = z.array(repoListItemSchema);
|
||||
|
||||
export async function repoRoutes(app: FastifyInstance) {
|
||||
// Use the unified response format
|
||||
const reposListResponseSchema = createResponseSchema(reposListSchema);
|
||||
const repoStatsResponseSchema = createResponseSchema(repoStatsSchema);
|
||||
|
||||
export const repoRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
// All routes in this plugin require authentication
|
||||
app.addHook('preHandler', authMiddleware);
|
||||
|
||||
@ -48,16 +53,18 @@ export async function repoRoutes(app: FastifyInstance) {
|
||||
'/',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get repositories for authenticated user',
|
||||
tags: ['repos'],
|
||||
response: {
|
||||
200: reposListResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
async (request) => {
|
||||
const { accessToken, username } = request.user!;
|
||||
const githubService = new GitHubService(accessToken, username);
|
||||
const repos = await githubService.getRepositories();
|
||||
return repos;
|
||||
return successResponse(repos);
|
||||
},
|
||||
);
|
||||
|
||||
@ -66,18 +73,21 @@ export async function repoRoutes(app: FastifyInstance) {
|
||||
'/:owner/:repo/stats',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get statistics for a specific repository',
|
||||
tags: ['repos'],
|
||||
params: repoParamsSchema,
|
||||
response: {
|
||||
200: repoStatsSchema,
|
||||
200: repoStatsResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
const { owner, repo } = request.params as z.infer<typeof repoParamsSchema>;
|
||||
async (request) => {
|
||||
// ✅ Type is automatically inferred from Zod schema
|
||||
const { owner, repo } = request.params;
|
||||
const { accessToken, username } = request.user!;
|
||||
const githubService = new GitHubService(accessToken, username);
|
||||
const stats = await githubService.getRepositoryStats(owner, repo);
|
||||
return stats;
|
||||
return successResponse(stats);
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,21 +1,63 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
||||
import { z } from 'zod';
|
||||
import { StatsService } from '../services/stats.service';
|
||||
import { authMiddleware } from '../middlewares/auth.middleware';
|
||||
import { createResponseSchema, successResponse } from '../types/response';
|
||||
|
||||
export async function statsRoutes(app: FastifyInstance) {
|
||||
// Schema for overview stats response
|
||||
const overviewStatsSchema = z.object({
|
||||
totalRepos: z.number(),
|
||||
totalStars: z.number(),
|
||||
totalForks: z.number(),
|
||||
});
|
||||
|
||||
// Schema for activity stats response
|
||||
const activityStatsSchema = z.object({
|
||||
totalCommitsLast30Days: z.number(),
|
||||
});
|
||||
|
||||
// Use the unified response format
|
||||
const overviewStatsResponseSchema = createResponseSchema(overviewStatsSchema);
|
||||
const activityStatsResponseSchema = createResponseSchema(activityStatsSchema);
|
||||
|
||||
export const statsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
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(
|
||||
'/overview',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get overview statistics for authenticated user',
|
||||
tags: ['stats'],
|
||||
response: {
|
||||
200: overviewStatsResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request) => {
|
||||
const { accessToken, username } = request.user!;
|
||||
const statsService = new StatsService(accessToken, username);
|
||||
const overviewStats = await statsService.getOverviewStats();
|
||||
return successResponse(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);
|
||||
});
|
||||
}
|
||||
app.get(
|
||||
'/activity',
|
||||
{
|
||||
schema: {
|
||||
description: 'Get activity statistics for authenticated user',
|
||||
tags: ['stats'],
|
||||
response: {
|
||||
200: activityStatsResponseSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request) => {
|
||||
const { accessToken, username } = request.user!;
|
||||
const statsService = new StatsService(accessToken, username);
|
||||
const activityStats = await statsService.getActivityStats();
|
||||
return successResponse(activityStats);
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
@ -1,34 +1,48 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import { FastifyPluginAsyncZod } from 'fastify-type-provider-zod';
|
||||
import { z } from 'zod';
|
||||
import { verifyGithubWebhook } from '../middlewares/verify-webhook.middleware';
|
||||
import { handleWebhook } from '../services/webhook.service';
|
||||
import { GitHubWebhookPayload } from '../types/github';
|
||||
import { createResponseSchema, successResponse, errorResponse, ErrorCode } from '../types/response';
|
||||
|
||||
// Define a basic schema to expect a JSON object payload.
|
||||
const webhookSchema = {
|
||||
body: {
|
||||
type: 'object',
|
||||
// Since webhook payloads vary, we allow any properties.
|
||||
// In a real application, you might add more specific validation
|
||||
// based on the event types you expect to handle.
|
||||
additionalProperties: true,
|
||||
},
|
||||
} as const;
|
||||
// Schema for webhook payload (flexible since payloads vary by event type)
|
||||
const webhookBodySchema = z.record(z.string(), z.any());
|
||||
|
||||
export async function webhookRoutes(app: FastifyInstance) {
|
||||
// Schema for webhook response data
|
||||
const webhookDataSchema = z.object({
|
||||
message: z.string(),
|
||||
});
|
||||
|
||||
// Use the unified response format
|
||||
const webhookSuccessSchema = createResponseSchema(webhookDataSchema);
|
||||
const webhookErrorSchema = createResponseSchema(z.null());
|
||||
|
||||
export const webhookRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.post(
|
||||
'/github',
|
||||
{
|
||||
preHandler: verifyGithubWebhook,
|
||||
schema: webhookSchema,
|
||||
schema: {
|
||||
description: 'Handle GitHub webhook events',
|
||||
tags: ['webhooks'],
|
||||
body: webhookBodySchema,
|
||||
response: {
|
||||
200: webhookSuccessSchema,
|
||||
500: webhookErrorSchema,
|
||||
},
|
||||
},
|
||||
},
|
||||
async (request, reply) => {
|
||||
try {
|
||||
// request.body is now safely typed as Record<string, any>
|
||||
await handleWebhook(request.body);
|
||||
reply.status(200).send({ message: 'Webhook received' });
|
||||
// Type cast to GitHubWebhookPayload for service function
|
||||
await handleWebhook(request.body as GitHubWebhookPayload);
|
||||
return successResponse({ message: 'Webhook received' });
|
||||
} catch (error) {
|
||||
app.log.error('Webhook processing error:', error);
|
||||
reply.status(500).send({ error: 'Webhook processing failed' });
|
||||
app.log.error({ error }, 'Webhook processing error');
|
||||
return reply
|
||||
.status(500)
|
||||
.send(errorResponse(ErrorCode.INTERNAL_ERROR, 'Webhook processing failed'));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
140
src/server.ts
140
src/server.ts
@ -69,8 +69,60 @@ async function bootstrap() {
|
||||
},
|
||||
});
|
||||
|
||||
// --- API Documentation with RapiDoc ---
|
||||
// --- Docs portal ---
|
||||
// Top-level docs landing page that links to multiple renderers (ReDoc, Stoplight Elements, RapiDoc)
|
||||
app.get('/docs', (req, reply) => {
|
||||
reply.type('text/html').send(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>API Documentation</title>
|
||||
<style>
|
||||
body { font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; margin:0; padding:40px; background:#0f172a; color:#e6eef8 }
|
||||
.container { max-width:900px; margin:0 auto; }
|
||||
h1 { margin:0 0 8px; font-size:28px; }
|
||||
p { color:#cbd5e1 }
|
||||
.cards { display:flex; gap:16px; margin-top:24px; }
|
||||
.card { background:linear-gradient(180deg,#0b1220,#061226); padding:18px; border-radius:8px; flex:1; box-shadow:0 6px 18px rgba(2,6,23,0.6); }
|
||||
.card a { color:#7dd3fc; text-decoration:none; font-weight:600 }
|
||||
.logo { height:36px; margin-bottom:12px }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<img class="logo" src="https://raw.githubusercontent.com/readmeio/presskit/master/logos/readme-logo-white.svg" alt="Docs" />
|
||||
<h1>API Documentation</h1>
|
||||
<p>Choose a renderer. <strong>ReDoc</strong> is visually polished; <strong>Stoplight</strong> gives a modern interactive portal similar to Knife4j.</p>
|
||||
|
||||
<div class="cards">
|
||||
<div class="card">
|
||||
<h3>ReDoc — Readable & clean</h3>
|
||||
<p style="color:#9fb4c9">Great for well-structured API reference. Non-interactive but very pretty.</p>
|
||||
<p><a href="/docs/redoc">Open ReDoc →</a></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Stoplight Elements — Interactive</h3>
|
||||
<p style="color:#9fb4c9">Modern UI with playground/try-it-out support and a Knife4j-like feel.</p>
|
||||
<p><a href="/docs/stoplight">Open Stoplight →</a></p>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>RapiDoc — Lightweight</h3>
|
||||
<p style="color:#9fb4c9">Existing lightweight renderer included for quick inspection.</p>
|
||||
<p><a href="/docs/rapidoc">Open RapiDoc →</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
// --- RapiDoc route (moved from /docs to /docs/rapidoc) ---
|
||||
app.get('/docs/rapidoc', (req, reply) => {
|
||||
reply.type('text/html').send(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
@ -92,6 +144,92 @@ async function bootstrap() {
|
||||
`);
|
||||
});
|
||||
|
||||
// --- API Documentation with ReDoc (alternative nicer UI) ---
|
||||
// ReDoc gives a clean, modern single-page OpenAPI rendering similar to Knife4j's look.
|
||||
app.get('/docs/redoc', (req, reply) => {
|
||||
reply.type('text/html').send(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>API Docs — ReDoc</title>
|
||||
<!-- ReDoc standalone bundle from CDN -->
|
||||
<script src="https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js"></script>
|
||||
<style>
|
||||
body { margin: 0; padding: 0; }
|
||||
#redoc-container { height: 100vh; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="redoc-container"></div>
|
||||
<script>
|
||||
// Render ReDoc into the container and point it at the generated OpenAPI JSON
|
||||
Redoc.init('/docs/json', {
|
||||
scrollYOffset: 50,
|
||||
theme: {
|
||||
colors: { primary: { main: '#2b6cb0' } },
|
||||
typography: { fontSize: '14px' }
|
||||
}
|
||||
}, document.getElementById('redoc-container'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
// --- Stoplight Elements (interactive, Knife4j-like feel) ---
|
||||
// Uses Stoplight Elements Web Components from CDN to render a modern interactive portal.
|
||||
app.get('/docs/stoplight', (req, reply) => {
|
||||
reply.type('text/html').send(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>API Docs — Stoplight</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css" />
|
||||
<!-- Load Stoplight Elements as an ES module; add ?module for ESM entry on unpkg -->
|
||||
<script type="module" src="https://unpkg.com/@stoplight/elements/web-components.min.js?module"></script>
|
||||
<style>
|
||||
body { margin:0; font-family: Inter, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial; }
|
||||
header { padding:12px 20px; background:#0b1220; color:#e6eef8; display:flex; align-items:center; gap:12px }
|
||||
header h1 { margin:0; font-size:16px }
|
||||
main { height: calc(100vh - 56px); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<img src="https://raw.githubusercontent.com/stoplightio/elements/main/docs/images/logo.svg" alt="stoplight" style="height:28px"/>
|
||||
<h1>Interactive API Docs</h1>
|
||||
</header>
|
||||
<main>
|
||||
<!-- elements-api is the web component that renders an API description URL -->
|
||||
<elements-api api-description-url="/docs/json" router="hash"></elements-api>
|
||||
<noscript>
|
||||
<div style="padding:20px;color:#374151;background:#f8fafc;border-radius:6px;margin:16px">JavaScript is required to render interactive docs. You can view the OpenAPI JSON directly: <a href="/docs/json">/docs/json</a></div>
|
||||
</noscript>
|
||||
<script>
|
||||
// If the web component failed to register (CDN blocked), show a fallback notice after a short timeout
|
||||
setTimeout(() => {
|
||||
if (!customElements.get('elements-api')) {
|
||||
const fallback = document.createElement('div');
|
||||
fallback.style.padding = '16px';
|
||||
fallback.style.background = '#fff3cd';
|
||||
fallback.style.color = '#6b4226';
|
||||
fallback.style.borderRadius = '6px';
|
||||
fallback.style.margin = '16px';
|
||||
fallback.innerHTML = 'Interactive docs failed to load from CDN. You can view the OpenAPI JSON directly: <a href="/docs/json">/docs/json</a>';
|
||||
document.querySelector('main').appendChild(fallback);
|
||||
}
|
||||
}, 1200);
|
||||
</script>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
|
||||
app.get('/docs/json', (req, reply) => {
|
||||
reply.send(app.swagger());
|
||||
});
|
||||
|
||||
@ -11,6 +11,10 @@ export class CacheService {
|
||||
}
|
||||
|
||||
async set(key: string, value: any, ttl?: number) {
|
||||
return await redis.set(key, value, { ex: ttl });
|
||||
// Only pass ex option if ttl is defined
|
||||
if (ttl !== undefined) {
|
||||
return await redis.set(key, value, { ex: ttl });
|
||||
}
|
||||
return await redis.set(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
55
src/types/response.ts
Normal file
55
src/types/response.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* Standard API Response Format
|
||||
* - code: 0 for success, non-zero for errors
|
||||
* - msg: empty for success, error message for failures
|
||||
* - data: response payload
|
||||
*/
|
||||
|
||||
// Generic response schema creator
|
||||
export function createResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
|
||||
return z.object({
|
||||
code: z.number(),
|
||||
msg: z.string(),
|
||||
data: dataSchema,
|
||||
});
|
||||
}
|
||||
|
||||
// Success response with data
|
||||
export function successResponse<T>(data: T) {
|
||||
return {
|
||||
code: 0,
|
||||
msg: '',
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
||||
// Error response
|
||||
export function errorResponse(code: number, msg: string) {
|
||||
return {
|
||||
code,
|
||||
msg,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
|
||||
// Common error codes
|
||||
export const ErrorCode = {
|
||||
SUCCESS: 0,
|
||||
INVALID_PARAMS: 400,
|
||||
UNAUTHORIZED: 401,
|
||||
FORBIDDEN: 403,
|
||||
NOT_FOUND: 404,
|
||||
INTERNAL_ERROR: 500,
|
||||
AUTH_FAILED: 1001,
|
||||
GITHUB_API_ERROR: 1002,
|
||||
WEBHOOK_VERIFICATION_FAILED: 1003,
|
||||
} as const;
|
||||
|
||||
// Error response schema
|
||||
export const errorResponseSchema = z.object({
|
||||
code: z.number(),
|
||||
msg: z.string(),
|
||||
data: z.null(),
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user