feat: add eslint config
This commit is contained in:
parent
434da70b06
commit
5e290945fb
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,3 +1,4 @@
|
||||
node_modules
|
||||
.env
|
||||
.idea/
|
||||
dist
|
||||
|
||||
1
.husky/commit-msg
Normal file
1
.husky/commit-msg
Normal file
@ -0,0 +1 @@
|
||||
npx --no -- commitlint --edit $1
|
||||
1
.husky/pre-commit
Normal file
1
.husky/pre-commit
Normal file
@ -0,0 +1 @@
|
||||
npx lint-staged
|
||||
29
.prettierrc
Normal file
29
.prettierrc
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"semi": false,
|
||||
"trailingComma": "es5",
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"tabWidth": 2,
|
||||
"useTabs": false,
|
||||
"arrowParens": "always",
|
||||
"endOfLine": "lf",
|
||||
"bracketSpacing": true,
|
||||
"bracketSameLine": false,
|
||||
"proseWrap": "preserve",
|
||||
"quoteProps": "as-needed",
|
||||
"overrides": [
|
||||
{
|
||||
"files": "*.prisma",
|
||||
"options": {
|
||||
"tabWidth": 2
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["*.json", "*.jsonc"],
|
||||
"options": {
|
||||
"tabWidth": 2,
|
||||
"printWidth": 100
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
22
commitlint.config.js
Normal file
22
commitlint.config.js
Normal file
@ -0,0 +1,22 @@
|
||||
module.exports = {
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
rules: {
|
||||
'type-enum': [
|
||||
2,
|
||||
'always',
|
||||
[
|
||||
'feat', // 新功能
|
||||
'fix', // 修复bug
|
||||
'docs', // 文档变更
|
||||
'style', // 代码格式(不影响代码运行)
|
||||
'refactor', // 重构
|
||||
'perf', // 性能优化
|
||||
'test', // 测试相关
|
||||
'chore', // 构建过程或辅助工具的变动
|
||||
'revert', // 回退
|
||||
'build', // 构建系统或外部依赖项的更改
|
||||
],
|
||||
],
|
||||
'subject-case': [0], // 关闭主题大小写限制
|
||||
},
|
||||
}
|
||||
168
eslint.config.mjs
Normal file
168
eslint.config.mjs
Normal file
@ -0,0 +1,168 @@
|
||||
import eslint from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import prettierConfig from 'eslint-config-prettier'
|
||||
import pluginNode from 'eslint-plugin-n'
|
||||
import pluginSecurity from 'eslint-plugin-security'
|
||||
|
||||
export default tseslint.config(
|
||||
{
|
||||
ignores: [
|
||||
'**/node_modules/**',
|
||||
'**/dist/**',
|
||||
'**/build/**',
|
||||
'**/.git/**',
|
||||
'**/coverage/**',
|
||||
'**/*.config.js',
|
||||
'**/*.config.mjs',
|
||||
'prisma/migrations/**',
|
||||
],
|
||||
},
|
||||
|
||||
eslint.configs.recommended,
|
||||
|
||||
...tseslint.configs.strictTypeChecked,
|
||||
...tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
{
|
||||
files: ['**/*.ts', '**/*.js', '**/*.mjs'],
|
||||
|
||||
plugins: {
|
||||
'@typescript-eslint': tseslint.plugin,
|
||||
n: pluginNode,
|
||||
security: pluginSecurity,
|
||||
},
|
||||
|
||||
languageOptions: {
|
||||
parser: tseslint.parser,
|
||||
parserOptions: {
|
||||
project: './tsconfig.json',
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
ecmaVersion: 'latest',
|
||||
sourceType: 'module',
|
||||
},
|
||||
globals: {
|
||||
// Node.js 全局变量
|
||||
console: 'readonly',
|
||||
process: 'readonly',
|
||||
Buffer: 'readonly',
|
||||
__dirname: 'readonly',
|
||||
__filename: 'readonly',
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
'n/no-process-exit': 'error',
|
||||
'n/no-deprecated-api': 'error',
|
||||
'n/no-unpublished-import': [
|
||||
'error',
|
||||
{
|
||||
allowModules: [
|
||||
'fastify',
|
||||
'@fastify/autoload',
|
||||
'@fastify/cors',
|
||||
'@fastify/jwt',
|
||||
'@fastify/sensible',
|
||||
'prisma',
|
||||
'@prisma/client',
|
||||
'vitest',
|
||||
'tsx',
|
||||
],
|
||||
},
|
||||
],
|
||||
'n/prefer-global/buffer': ['error', 'always'],
|
||||
'n/prefer-global/console': ['error', 'always'],
|
||||
'n/prefer-global/process': ['error', 'always'],
|
||||
|
||||
'no-console': ['warn', { allow: ['warn', 'error', 'info'] }],
|
||||
'no-await-in-loop': 'warn',
|
||||
'no-return-await': 'off',
|
||||
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/require-await': 'warn',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/return-await': ['error', 'in-try-catch'],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_|^reply|^request',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
|
||||
'@typescript-eslint/explicit-function-return-type': 'off',
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'warn',
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{
|
||||
prefer: 'type-imports',
|
||||
fixStyle: 'separate-type-imports',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/no-unnecessary-type-assertion': 'error',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'warn',
|
||||
'@typescript-eslint/prefer-optional-chain': 'warn',
|
||||
'@typescript-eslint/no-non-null-assertion': 'warn',
|
||||
|
||||
'no-throw-literal': 'off',
|
||||
'@typescript-eslint/only-throw-error': 'error',
|
||||
'@typescript-eslint/prefer-promise-reject-errors': 'error',
|
||||
|
||||
'security/detect-object-injection': 'warn',
|
||||
'security/detect-non-literal-regexp': 'warn',
|
||||
'security/detect-unsafe-regex': 'error',
|
||||
'security/detect-buffer-noassert': 'error',
|
||||
'security/detect-eval-with-expression': 'error',
|
||||
'security/detect-non-literal-fs-filename': 'warn',
|
||||
'security/detect-possible-timing-attacks': 'warn',
|
||||
|
||||
'no-nested-ternary': 'warn',
|
||||
'max-depth': ['warn', 4],
|
||||
'max-lines-per-function': [
|
||||
'warn',
|
||||
{
|
||||
max: 150,
|
||||
skipBlankLines: true,
|
||||
skipComments: true,
|
||||
},
|
||||
],
|
||||
complexity: ['warn', 15],
|
||||
|
||||
'sort-imports': [
|
||||
'error',
|
||||
{
|
||||
ignoreCase: true,
|
||||
ignoreDeclarationSort: true,
|
||||
},
|
||||
],
|
||||
|
||||
'@typescript-eslint/no-confusing-void-expression': ['error', { ignoreArrowShorthand: true }],
|
||||
'@typescript-eslint/promise-function-async': 'error',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ['**/*.test.ts', '**/*.spec.ts', '**/tests/**/*.ts'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-non-null-assertion': 'off',
|
||||
'@typescript-eslint/no-unsafe-assignment': 'off',
|
||||
'@typescript-eslint/no-unsafe-member-access': 'off',
|
||||
'max-lines-per-function': 'off',
|
||||
'n/no-unpublished-import': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
files: ['*.config.js', '*.config.mjs', '*.config.ts'],
|
||||
rules: {
|
||||
'n/no-unpublished-import': 'off',
|
||||
'@typescript-eslint/no-var-requires': 'off',
|
||||
'@typescript-eslint/no-require-imports': 'off',
|
||||
},
|
||||
},
|
||||
|
||||
prettierConfig
|
||||
)
|
||||
2620
package-lock.json
generated
2620
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
53
package.json
53
package.json
@ -9,7 +9,14 @@
|
||||
"build": "prisma generate && tsc",
|
||||
"start": "node dist/server.js",
|
||||
"prisma:generate": "prisma generate",
|
||||
"prisma:migrate": "prisma migrate dev"
|
||||
"prisma:migrate": "prisma migrate dev",
|
||||
"lint": "eslint .",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"format": "prettier --write .",
|
||||
"format:check": "prettier --check .",
|
||||
"type-check": "tsc --noEmit",
|
||||
"validate": "npm run type-check && npm run lint && npm run format:check",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"author": "Google LLC",
|
||||
"license": "Apache-2.0",
|
||||
@ -17,22 +24,54 @@
|
||||
"@fastify/cors": "^11.1.0",
|
||||
"@fastify/jwt": "^10.0.0",
|
||||
"@fastify/swagger": "^9.5.2",
|
||||
"@octokit/rest": "^19.0.13",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@octokit/types": "^15.0.1",
|
||||
"@prisma/client": "^6.18.0",
|
||||
"@upstash/redis": "^1.35.6",
|
||||
"axios": "^1.12.2",
|
||||
"dotenv": "^17.2.3",
|
||||
"fastify": "^5.6.1",
|
||||
"fastify-type-provider-zod": "^6.0.0",
|
||||
"fastify-type-provider-zod": "^6.1.0",
|
||||
"prettier-plugin-eslint": "^1.0.2",
|
||||
"zod": "^4.1.12"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.12.12",
|
||||
"nodemon": "^3.1.0",
|
||||
"@commitlint/cli": "^20.1.0",
|
||||
"@commitlint/config-conventional": "^20.0.0",
|
||||
"@eslint/js": "^9.38.0",
|
||||
"@types/node": "^24.9.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
||||
"@typescript-eslint/parser": "^8.46.2",
|
||||
"eslint": "^9.38.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-helpers": "^1.2.1",
|
||||
"eslint-plugin-fastify-security-rules": "^1.2.0",
|
||||
"eslint-plugin-n": "^17.23.1",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"eslint-plugin-security": "^3.0.1",
|
||||
"eslint-plugin-security-node": "^1.1.4",
|
||||
"fastify-tsconfig": "^3.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.6",
|
||||
"nodemon": "^3.1.10",
|
||||
"pino-pretty": "^13.1.2",
|
||||
"prettier": "^3.6.2",
|
||||
"prisma": "^6.18.0",
|
||||
"ts-node-dev": "^2.0.0",
|
||||
"typescript": "^5.4.5"
|
||||
"typescript": "^5.9.3",
|
||||
"typescript-eslint": "^8.46.2"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.{ts,js,mjs}": [
|
||||
"eslint --fix",
|
||||
"prettier --write"
|
||||
],
|
||||
"*.{json,md,yml,yaml}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.prisma": [
|
||||
"prisma format"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
5094
pnpm-lock.yaml
generated
5094
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
28
src/index.ts
28
src/index.ts
@ -1,22 +1,24 @@
|
||||
import Fastify from 'fastify';
|
||||
import Fastify from 'fastify'
|
||||
const fastify = Fastify({
|
||||
logger: true
|
||||
});
|
||||
logger: true,
|
||||
})
|
||||
|
||||
fastify.get('/', async (request, reply) => {
|
||||
const name = process.env.NAME || 'World';
|
||||
return `Hello ${name}!`;
|
||||
});
|
||||
fastify.get('/', async () => {
|
||||
const name = process.env['NAME'] || 'World'
|
||||
return `Hello ${name}!`
|
||||
})
|
||||
|
||||
const port = parseInt(process.env.PORT || '3000');
|
||||
const port = parseInt(process.env['PORT'] || '3000')
|
||||
|
||||
const start = async () => {
|
||||
try {
|
||||
await fastify.listen({ port });
|
||||
await fastify.listen({ port })
|
||||
} catch (err) {
|
||||
fastify.log.error(err);
|
||||
process.exit(1);
|
||||
fastify.log.error(err)
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit(1)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
start();
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
start().then((r) => console.log(r))
|
||||
|
||||
@ -1,9 +1,17 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { PrismaClient } from '@prisma/client'
|
||||
|
||||
// Prevent creating multiple instances of PrismaClient in development
|
||||
// when using hot-reloading (ts-node-dev / nodemon).
|
||||
const g = global as any;
|
||||
const prismaClientSingleton = () => {
|
||||
return new PrismaClient()
|
||||
}
|
||||
|
||||
export const prisma: PrismaClient = g.__prisma ?? new PrismaClient();
|
||||
declare const globalThis: {
|
||||
prismaGlobal: ReturnType<typeof prismaClientSingleton> | undefined
|
||||
} & typeof global
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') g.__prisma = prisma;
|
||||
const prisma = globalThis.prismaGlobal ?? prismaClientSingleton()
|
||||
|
||||
export default prisma
|
||||
|
||||
if (process.env['NODE_ENV'] !== 'production') {
|
||||
globalThis.prismaGlobal = prisma
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Redis } from '@upstash/redis';
|
||||
import 'dotenv/config';
|
||||
import { Redis } from '@upstash/redis'
|
||||
import 'dotenv/config'
|
||||
|
||||
export const redis = new Redis({
|
||||
url: process.env.UPSTASH_REDIS_URL!,
|
||||
token: process.env.UPSTASH_REDIS_TOKEN!,
|
||||
});
|
||||
url: process.env.UPSTASH_REDIS_URL,
|
||||
token: process.env.UPSTASH_REDIS_TOKEN,
|
||||
})
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify'
|
||||
|
||||
/**
|
||||
* This middleware function verifies the JWT token from the request.
|
||||
@ -9,9 +9,10 @@ import { FastifyRequest, FastifyReply } from 'fastify';
|
||||
export const authMiddleware = async (req: FastifyRequest, reply: FastifyReply) => {
|
||||
try {
|
||||
// This will verify the token and attach the user payload to `req.user`
|
||||
await req.jwtVerify();
|
||||
await req.jwtVerify()
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (error) {
|
||||
// If verification fails, send an unauthorized error
|
||||
reply.status(401).send({ error: 'Unauthorized' });
|
||||
reply.status(401).send({ error: 'Unauthorized' })
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { FastifyInstance } from 'fastify';
|
||||
import type { FastifyInstance } from 'fastify'
|
||||
|
||||
export async function errorMiddleware(app: FastifyInstance) {
|
||||
app.setErrorHandler((error, request, reply) => {
|
||||
app.log.error(error);
|
||||
reply.status(500).send({ error: 'Internal Server Error' });
|
||||
});
|
||||
app.setErrorHandler((error, _, reply) => {
|
||||
app.log.error(error)
|
||||
reply.status(500).send({ error: 'Internal Server Error' })
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import { FastifyRequest, FastifyReply} from 'fastify';
|
||||
import * as crypto from 'crypto';
|
||||
import type { FastifyReply, FastifyRequest } from 'fastify'
|
||||
import * as crypto from 'crypto'
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-expect-error
|
||||
export function verifyGithubWebhook(req: FastifyRequest, reply: FastifyReply) {
|
||||
const signature = req.headers['x-hub-signature-256'] as string;
|
||||
const signature = req.headers['x-hub-signature-256'] as string
|
||||
if (!signature) {
|
||||
return reply.status(401).send({ error: 'No signature found' });
|
||||
return reply.status(401).send({ error: 'No signature found' })
|
||||
}
|
||||
|
||||
const secret = process.env.GITHUB_WEBHOOK_SECRET!;
|
||||
const hmac = crypto.createHmac('sha256', secret);
|
||||
const digest = `sha256=${hmac.update(JSON.stringify(req.body)).digest('hex')}`;
|
||||
const secret = process.env['GITHUB_WEBHOOK_SECRET']
|
||||
const hmac = crypto.createHmac('sha256', secret!)
|
||||
const digest = `sha256=${hmac.update(JSON.stringify(req.body)).digest('hex')}`
|
||||
|
||||
if (digest !== signature) {
|
||||
return reply.status(401).send({ error: 'Invalid signature' });
|
||||
return reply.status(401).send({ error: 'Invalid signature' })
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
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';
|
||||
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'
|
||||
import { z } from 'zod'
|
||||
import { exchangeCodeForToken, getGithubUser } from '@services/github.service'
|
||||
import prisma from '@/lib/prisma'
|
||||
import { createResponseSchema, ErrorCode, errorResponse, successResponse } from '@/types/response'
|
||||
|
||||
// Schema for login request body
|
||||
const loginBodySchema = z.object({
|
||||
code: z.string().min(1, 'Authorization code is required'),
|
||||
});
|
||||
})
|
||||
|
||||
// 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());
|
||||
const loginSuccessSchema = createResponseSchema(loginDataSchema)
|
||||
const loginErrorSchema = createResponseSchema(z.null())
|
||||
|
||||
export const authRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.post(
|
||||
@ -34,16 +34,16 @@ export const authRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
},
|
||||
async (request, reply) => {
|
||||
// ✅ Type is automatically inferred from Zod schema
|
||||
const { code } = request.body;
|
||||
const { code } = request.body
|
||||
|
||||
try {
|
||||
const accessToken = await exchangeCodeForToken(code);
|
||||
const githubUser = await getGithubUser(accessToken);
|
||||
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: String(githubUser.id) },
|
||||
});
|
||||
})
|
||||
|
||||
if (!user) {
|
||||
user = await prisma.user.create({
|
||||
@ -52,7 +52,7 @@ export const authRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
username: githubUser.login,
|
||||
avatarUrl: githubUser.avatar_url,
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Generate JWT token
|
||||
@ -63,16 +63,14 @@ export const authRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
avatarUrl: user.avatarUrl,
|
||||
accessToken,
|
||||
},
|
||||
{ expiresIn: '1d' },
|
||||
);
|
||||
{ expiresIn: '1d' }
|
||||
)
|
||||
|
||||
return successResponse({ token });
|
||||
return successResponse({ token })
|
||||
} catch (error) {
|
||||
app.log.error({ error }, 'Authentication Error');
|
||||
return reply
|
||||
.status(500)
|
||||
.send(errorResponse(ErrorCode.AUTH_FAILED, 'Authentication failed'));
|
||||
app.log.error({ error }, 'Authentication Error')
|
||||
return reply.status(500).send(errorResponse(ErrorCode.AUTH_FAILED, 'Authentication failed'))
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
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';
|
||||
import type { 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({
|
||||
owner: z.string(),
|
||||
repo: z.string(),
|
||||
});
|
||||
})
|
||||
|
||||
// Schema for repository statistics response
|
||||
const repoStatsSchema = z.object({
|
||||
@ -18,7 +18,7 @@ const repoStatsSchema = z.object({
|
||||
commits: z.number(),
|
||||
releases: z.number(),
|
||||
contributors: z.number(),
|
||||
});
|
||||
})
|
||||
|
||||
// Schema for repository list response
|
||||
const repoListItemSchema = z.object({
|
||||
@ -26,7 +26,7 @@ const repoListItemSchema = z.object({
|
||||
name: z.string(),
|
||||
full_name: z.string(),
|
||||
private: z.boolean(),
|
||||
html_url: z.string().url(),
|
||||
html_url: z.url(),
|
||||
description: z.string().nullable(),
|
||||
stargazers_count: z.number(),
|
||||
watchers_count: z.number(),
|
||||
@ -34,19 +34,19 @@ const repoListItemSchema = z.object({
|
||||
language: z.string().nullable(),
|
||||
owner: z.object({
|
||||
login: z.string(),
|
||||
avatar_url: z.string().url(),
|
||||
avatar_url: z.url(),
|
||||
}),
|
||||
});
|
||||
})
|
||||
|
||||
const reposListSchema = z.array(repoListItemSchema);
|
||||
const reposListSchema = z.array(repoListItemSchema)
|
||||
|
||||
// Use the unified response format
|
||||
const reposListResponseSchema = createResponseSchema(reposListSchema);
|
||||
const repoStatsResponseSchema = createResponseSchema(repoStatsSchema);
|
||||
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);
|
||||
app.addHook('preHandler', authMiddleware)
|
||||
|
||||
// Route to list repositories for the authenticated user
|
||||
app.get(
|
||||
@ -61,12 +61,12 @@ export const repoRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
},
|
||||
},
|
||||
async (request) => {
|
||||
const { accessToken, username } = request.user!;
|
||||
const githubService = new GitHubService(accessToken, username);
|
||||
const repos = await githubService.getRepositories();
|
||||
return successResponse(repos);
|
||||
},
|
||||
);
|
||||
const { accessToken, username } = request.user
|
||||
const githubService = new GitHubService(accessToken, username)
|
||||
const repos = await githubService.getRepositories()
|
||||
return successResponse(repos)
|
||||
}
|
||||
)
|
||||
|
||||
// Route to get statistics for a specific repository
|
||||
app.get(
|
||||
@ -83,11 +83,11 @@ export const repoRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
},
|
||||
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 successResponse(stats);
|
||||
},
|
||||
);
|
||||
};
|
||||
const { owner, repo } = request.params
|
||||
const { accessToken, username } = request.user
|
||||
const githubService = new GitHubService(accessToken, username)
|
||||
const stats = await githubService.getRepositoryStats(owner, repo)
|
||||
return successResponse(stats)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,27 +1,27 @@
|
||||
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';
|
||||
import type { 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'
|
||||
|
||||
// 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);
|
||||
const overviewStatsResponseSchema = createResponseSchema(overviewStatsSchema)
|
||||
const activityStatsResponseSchema = createResponseSchema(activityStatsSchema)
|
||||
|
||||
export const statsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.addHook('preHandler', authMiddleware);
|
||||
app.addHook('preHandler', authMiddleware)
|
||||
|
||||
app.get(
|
||||
'/overview',
|
||||
@ -35,12 +35,12 @@ export const statsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
},
|
||||
},
|
||||
async (request) => {
|
||||
const { accessToken, username } = request.user!;
|
||||
const statsService = new StatsService(accessToken, username);
|
||||
const overviewStats = await statsService.getOverviewStats();
|
||||
return successResponse(overviewStats);
|
||||
},
|
||||
);
|
||||
const { accessToken, username } = request.user
|
||||
const statsService = new StatsService(accessToken, username)
|
||||
const overviewStats = await statsService.getOverviewStats()
|
||||
return successResponse(overviewStats)
|
||||
}
|
||||
)
|
||||
|
||||
app.get(
|
||||
'/activity',
|
||||
@ -54,10 +54,10 @@ export const statsRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
},
|
||||
},
|
||||
async (request) => {
|
||||
const { accessToken, username } = request.user!;
|
||||
const statsService = new StatsService(accessToken, username);
|
||||
const activityStats = await statsService.getActivityStats();
|
||||
return successResponse(activityStats);
|
||||
},
|
||||
);
|
||||
};
|
||||
const { accessToken, username } = request.user
|
||||
const statsService = new StatsService(accessToken, username)
|
||||
const activityStats = await statsService.getActivityStats()
|
||||
return successResponse(activityStats)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,26 +1,27 @@
|
||||
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';
|
||||
import type { FastifyPluginAsyncZod } from 'fastify-type-provider-zod'
|
||||
import { z } from 'zod'
|
||||
import { verifyGithubWebhook } from '@/middlewares/verify-webhook.middleware'
|
||||
import { handleWebhook } from '@/services/webhook.service'
|
||||
import type { GitHubWebhookPayload } from '@/types/github'
|
||||
import { createResponseSchema, ErrorCode, errorResponse, successResponse } from '@/types/response'
|
||||
|
||||
// Schema for webhook payload (flexible since payloads vary by event type)
|
||||
const webhookBodySchema = z.record(z.string(), z.any());
|
||||
const webhookBodySchema = z.record(z.string(), z.any())
|
||||
|
||||
// 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());
|
||||
const webhookSuccessSchema = createResponseSchema(webhookDataSchema)
|
||||
const webhookErrorSchema = createResponseSchema(z.null())
|
||||
|
||||
export const webhookRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
app.post(
|
||||
'/github',
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
preHandler: verifyGithubWebhook,
|
||||
schema: {
|
||||
description: 'Handle GitHub webhook events',
|
||||
@ -35,14 +36,14 @@ export const webhookRoutes: FastifyPluginAsyncZod = async (app) => {
|
||||
async (request, reply) => {
|
||||
try {
|
||||
// Type cast to GitHubWebhookPayload for service function
|
||||
await handleWebhook(request.body as GitHubWebhookPayload);
|
||||
return successResponse({ message: 'Webhook received' });
|
||||
await handleWebhook(request.body as GitHubWebhookPayload)
|
||||
return successResponse({ message: 'Webhook received' })
|
||||
} catch (error) {
|
||||
app.log.error({ error }, 'Webhook processing error');
|
||||
app.log.error({ error }, 'Webhook processing error')
|
||||
return reply
|
||||
.status(500)
|
||||
.send(errorResponse(ErrorCode.INTERNAL_ERROR, 'Webhook processing failed'));
|
||||
.send(errorResponse(ErrorCode.INTERNAL_ERROR, 'Webhook processing failed'))
|
||||
}
|
||||
},
|
||||
);
|
||||
};
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
100
src/server.ts
100
src/server.ts
@ -1,19 +1,16 @@
|
||||
import fastify from 'fastify';
|
||||
import {
|
||||
serializerCompiler,
|
||||
validatorCompiler,
|
||||
ZodTypeProvider
|
||||
} from 'fastify-type-provider-zod';
|
||||
import fastifyJwt from '@fastify/jwt';
|
||||
import fastifyCors from '@fastify/cors';
|
||||
import 'dotenv/config';
|
||||
import fastify from 'fastify'
|
||||
import type { ZodTypeProvider } from 'fastify-type-provider-zod'
|
||||
import { serializerCompiler, validatorCompiler } from 'fastify-type-provider-zod'
|
||||
import fastifyJwt from '@fastify/jwt'
|
||||
import fastifyCors from '@fastify/cors'
|
||||
import 'dotenv/config'
|
||||
|
||||
import { authRoutes } from './routes/auth';
|
||||
import { repoRoutes } from './routes/repos';
|
||||
import { statsRoutes } from './routes/stats';
|
||||
import { webhookRoutes } from './routes/webhooks';
|
||||
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';
|
||||
import swagger from '@fastify/swagger'
|
||||
|
||||
// Main function to bootstrap the server
|
||||
async function bootstrap() {
|
||||
@ -28,21 +25,21 @@ async function bootstrap() {
|
||||
},
|
||||
},
|
||||
},
|
||||
}).withTypeProvider<ZodTypeProvider>();
|
||||
}).withTypeProvider<ZodTypeProvider>()
|
||||
|
||||
// Set the validator and serializer for Zod
|
||||
app.setValidatorCompiler(validatorCompiler);
|
||||
app.setSerializerCompiler(serializerCompiler);
|
||||
app.setValidatorCompiler(validatorCompiler)
|
||||
app.setSerializerCompiler(serializerCompiler)
|
||||
|
||||
// Register plugins
|
||||
await app.register(fastifyJwt, {
|
||||
secret: process.env.JWT_SECRET!,
|
||||
});
|
||||
secret: process.env.JWT_SECRET,
|
||||
})
|
||||
|
||||
await app.register(fastifyCors, {
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
|
||||
credentials: true,
|
||||
});
|
||||
})
|
||||
|
||||
// Register Swagger for OpenAPI spec generation
|
||||
await app.register(swagger, {
|
||||
@ -67,11 +64,11 @@ async function bootstrap() {
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
})
|
||||
|
||||
// --- Docs portal ---
|
||||
// Top-level docs landing page that links to multiple renderers (ReDoc, Stoplight Elements, RapiDoc)
|
||||
app.get('/docs', (req, reply) => {
|
||||
app.get('/docs', (_, reply) => {
|
||||
reply.type('text/html').send(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
@ -118,11 +115,11 @@ async function bootstrap() {
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
`)
|
||||
})
|
||||
|
||||
// --- RapiDoc route (moved from /docs to /docs/rapidoc) ---
|
||||
app.get('/docs/rapidoc', (req, reply) => {
|
||||
app.get('/docs/rapidoc', (_, reply) => {
|
||||
reply.type('text/html').send(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
@ -141,12 +138,12 @@ async function bootstrap() {
|
||||
> </rapi-doc>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
`)
|
||||
})
|
||||
|
||||
// --- 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) => {
|
||||
app.get('/docs/redoc', (_, reply) => {
|
||||
reply.type('text/html').send(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
@ -175,12 +172,12 @@ async function bootstrap() {
|
||||
</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) => {
|
||||
app.get('/docs/stoplight', (_, reply) => {
|
||||
reply.type('text/html').send(`
|
||||
<!doctype html>
|
||||
<html>
|
||||
@ -227,35 +224,36 @@ async function bootstrap() {
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
`);
|
||||
});
|
||||
`)
|
||||
})
|
||||
|
||||
app.get('/docs/json', (req, reply) => {
|
||||
reply.send(app.swagger());
|
||||
});
|
||||
app.get('/docs/json', (_, 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' });
|
||||
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' });
|
||||
});
|
||||
app.setErrorHandler((error, _, reply) => {
|
||||
app.log.error(error)
|
||||
reply.status(500).send({ error: 'Internal Server Error' })
|
||||
})
|
||||
|
||||
return app;
|
||||
return app
|
||||
}
|
||||
|
||||
// Start the server
|
||||
(async () => {
|
||||
await (async () => {
|
||||
try {
|
||||
const app = await bootstrap();
|
||||
await app.listen({ port: 3333, host: '0.0.0.0' });
|
||||
const app = await bootstrap()
|
||||
await app.listen({ port: 3333, host: '0.0.0.0' })
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
console.error(err)
|
||||
// eslint-disable-next-line n/no-process-exit
|
||||
process.exit(1)
|
||||
}
|
||||
})();
|
||||
})()
|
||||
|
||||
@ -1,20 +1,22 @@
|
||||
import { Redis } from '@upstash/redis';
|
||||
import { Redis } from '@upstash/redis'
|
||||
|
||||
export const redis = new Redis({
|
||||
url: process.env.UPSTASH_REDIS_URL!,
|
||||
token: process.env.UPSTASH_REDIS_TOKEN!,
|
||||
});
|
||||
url: process.env.UPSTASH_REDIS_URL,
|
||||
token: process.env.UPSTASH_REDIS_TOKEN,
|
||||
})
|
||||
|
||||
export class CacheService {
|
||||
async get(key: string) {
|
||||
return await redis.get(key);
|
||||
return redis.get(key)
|
||||
}
|
||||
|
||||
async set(key: string, value: any, ttl?: number) {
|
||||
// Only pass ex option if ttl is defined
|
||||
if (ttl !== undefined) {
|
||||
return await redis.set(key, value, { ex: ttl });
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return redis.set(key, value, { ex: ttl })
|
||||
}
|
||||
return await redis.set(key, value);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return redis.set(key, value)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,14 +1,13 @@
|
||||
import { Octokit } from '@octokit/rest';
|
||||
import { Endpoints } from '@octokit/types';
|
||||
import axios from 'axios';
|
||||
import { redis } from '../lib/redis';
|
||||
import { GitHubUser } from '../types/github';
|
||||
import { Octokit } from '@octokit/rest'
|
||||
import type { Endpoints } from '@octokit/types'
|
||||
import axios from 'axios'
|
||||
import { redis } from '@/lib/redis'
|
||||
import type { GitHubUser } from '@/types/github'
|
||||
|
||||
const GITHUB_API_BASE_URL = 'https://api.github.com';
|
||||
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<string> {
|
||||
const response = await axios.post(
|
||||
@ -20,41 +19,43 @@ export async function exchangeCodeForToken(code: string): Promise<string> {
|
||||
},
|
||||
{
|
||||
headers: { Accept: 'application/json' },
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
return response.data.access_token;
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-member-access
|
||||
return response.data?.access_token
|
||||
}
|
||||
|
||||
export async function getGithubUser(accessToken: string): Promise<GitHubUser> {
|
||||
const response = await axios.get<GitHubUser>(`${GITHUB_API_BASE_URL}/user`, {
|
||||
headers: { Authorization: `Bearer ${accessToken}` },
|
||||
});
|
||||
})
|
||||
|
||||
return response.data;
|
||||
return response.data
|
||||
}
|
||||
|
||||
export class GitHubService {
|
||||
private octokit: Octokit;
|
||||
private username: string;
|
||||
private octokit: Octokit
|
||||
private username: string
|
||||
|
||||
constructor(accessToken: string, username: string) {
|
||||
this.octokit = new Octokit({ auth: accessToken });
|
||||
this.username = username;
|
||||
this.octokit = new Octokit({ auth: accessToken })
|
||||
this.username = username
|
||||
}
|
||||
|
||||
async getRepositories(): Promise<ReposListForAuthenticatedUserResponse> {
|
||||
const cacheKey = `repos:${this.username}`;
|
||||
const cachedRepos = await redis.get(cacheKey);
|
||||
const cacheKey = `repos:${this.username}`
|
||||
const cachedRepos = await redis.get(cacheKey)
|
||||
|
||||
if (cachedRepos) {
|
||||
return JSON.parse(cachedRepos as string);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
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
|
||||
const { data } = await this.octokit.repos.listForAuthenticatedUser()
|
||||
await redis.set(cacheKey, JSON.stringify(data), { ex: 3600 }) // Cache for 1 hour
|
||||
|
||||
return data;
|
||||
return data
|
||||
}
|
||||
|
||||
async getRepositoryStats(owner: string, repo: string, since?: string) {
|
||||
@ -63,7 +64,7 @@ export class GitHubService {
|
||||
this.octokit.repos.listCommits({ owner, repo, since }),
|
||||
this.octokit.repos.listReleases({ owner, repo }),
|
||||
this.octokit.repos.listContributors({ owner, repo }),
|
||||
]);
|
||||
])
|
||||
|
||||
const stats = {
|
||||
stars: repoData.data.stargazers_count,
|
||||
@ -72,8 +73,8 @@ export class GitHubService {
|
||||
commits: commits.data.length,
|
||||
releases: releases.data.length,
|
||||
contributors: contributors.data.length,
|
||||
};
|
||||
}
|
||||
|
||||
return stats;
|
||||
return stats
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,42 +1,42 @@
|
||||
import { prisma } from '../lib/prisma';
|
||||
import { GitHubService } from './github.service';
|
||||
import { GitHubService } from './github.service'
|
||||
|
||||
export class StatsService {
|
||||
private githubService: GitHubService;
|
||||
private githubService: GitHubService
|
||||
|
||||
constructor(accessToken: string, username: string) {
|
||||
this.githubService = new GitHubService(accessToken, username);
|
||||
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);
|
||||
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 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 commitPromises = repos.map(async (repo) => {
|
||||
const [owner, repoName] = repo.full_name.split('/')
|
||||
if (!owner || !repoName) {
|
||||
return { commits: 0 }
|
||||
}
|
||||
return this.githubService.getRepositoryStats(owner, repoName, thirtyDaysAgo)
|
||||
})
|
||||
|
||||
const allStats = await Promise.all(commitPromises);
|
||||
const allStats = await Promise.all(commitPromises)
|
||||
|
||||
const totalCommits = allStats.reduce((acc, stats) => acc + stats.commits, 0);
|
||||
const totalCommits = allStats.reduce((acc, stats) => acc + stats.commits, 0)
|
||||
|
||||
return {
|
||||
totalCommitsLast30Days: totalCommits,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,17 +1,17 @@
|
||||
import { prisma } from '../lib/prisma';
|
||||
import { GitHubWebhookPayload } from '../types/github';
|
||||
import prisma from '@/lib/prisma'
|
||||
import type { GitHubWebhookPayload } from '@/types/github'
|
||||
|
||||
export async function handleWebhook(payload: GitHubWebhookPayload) {
|
||||
const { action, pull_request, repository } = payload;
|
||||
const { action, pull_request, repository } = payload
|
||||
|
||||
if (action === 'opened' || action === 'closed') {
|
||||
const { number, title, state, user, created_at, closed_at } = pull_request;
|
||||
const { number, title, state, user, created_at, closed_at } = pull_request
|
||||
|
||||
await prisma.pullRequest.upsert({
|
||||
where: { repositoryId_number: { repositoryId: repository.id, number } },
|
||||
update: {
|
||||
state,
|
||||
closedAt: closed_at ? new Date(closed_at) : null
|
||||
update: {
|
||||
state,
|
||||
closedAt: closed_at ? new Date(closed_at) : null,
|
||||
},
|
||||
create: {
|
||||
number,
|
||||
@ -40,6 +40,6 @@ export async function handleWebhook(payload: GitHubWebhookPayload) {
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,14 +2,35 @@
|
||||
* Represents the user data structure returned from the GitHub API.
|
||||
*/
|
||||
export interface GitHubUser {
|
||||
id: number;
|
||||
login: string;
|
||||
name: string | null;
|
||||
avatar_url: string;
|
||||
id: number
|
||||
login: string
|
||||
name: string | null
|
||||
avatar_url: string
|
||||
}
|
||||
|
||||
/**
|
||||
* A generic type for the GitHub webhook payload.
|
||||
* The actual structure varies widely depending on the event type.
|
||||
*/
|
||||
export type GitHubWebhookPayload = Record<string, any>;
|
||||
export interface GitHubWebhookPayload {
|
||||
action: string
|
||||
pull_request: {
|
||||
number: number
|
||||
title: string
|
||||
state: string
|
||||
user: {
|
||||
id: number
|
||||
login: string
|
||||
avatar_url: string
|
||||
}
|
||||
created_at: string
|
||||
closed_at: string | null
|
||||
}
|
||||
repository: {
|
||||
id: number
|
||||
name: string
|
||||
owner: {
|
||||
login: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { z } from 'zod';
|
||||
import { z } from 'zod'
|
||||
|
||||
/**
|
||||
* Standard API Response Format
|
||||
@ -8,12 +8,12 @@ import { z } from 'zod';
|
||||
*/
|
||||
|
||||
// Generic response schema creator
|
||||
export function createResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
|
||||
export function createResponseSchema<T extends z.ZodType>(dataSchema: T) {
|
||||
return z.object({
|
||||
code: z.number(),
|
||||
msg: z.string(),
|
||||
data: dataSchema,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
// Success response with data
|
||||
@ -22,7 +22,7 @@ export function successResponse<T>(data: T) {
|
||||
code: 0,
|
||||
msg: '',
|
||||
data,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Error response
|
||||
@ -31,7 +31,7 @@ export function errorResponse(code: number, msg: string) {
|
||||
code,
|
||||
msg,
|
||||
data: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Common error codes
|
||||
@ -45,11 +45,11 @@ export const ErrorCode = {
|
||||
AUTH_FAILED: 1001,
|
||||
GITHUB_API_ERROR: 1002,
|
||||
WEBHOOK_VERIFICATION_FAILED: 1003,
|
||||
} as const;
|
||||
} as const
|
||||
|
||||
// Error response schema
|
||||
export const errorResponseSchema = z.object({
|
||||
code: z.number(),
|
||||
msg: z.string(),
|
||||
data: z.null(),
|
||||
});
|
||||
})
|
||||
|
||||
@ -1,18 +1,58 @@
|
||||
{
|
||||
"extends": "fastify-tsconfig",
|
||||
"compilerOptions": {
|
||||
"target": "ES2020", // More modern target
|
||||
"module": "commonjs",
|
||||
"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
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"target": "ES2022",
|
||||
"lib": ["ES2022"],
|
||||
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictBindCallApply": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
|
||||
"esModuleInterop": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": false,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["src/*"],
|
||||
"@config/*": ["src/config/*"],
|
||||
"@routes/*": ["src/routes/*"],
|
||||
"@services/*": ["src/services/*"],
|
||||
"@utils/*": ["src/utils/*"],
|
||||
"@types/*": ["src/types/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts", // Include all .ts files in the src directory
|
||||
"src/types/**/*.d.ts" // Include our custom type definitions
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
"include": ["src/**/*.ts", "src/**/*.d.ts", "node_modules/@prisma/client"],
|
||||
"exclude": ["node_modules", "dist", "build", "coverage", "**/*.test.ts", "**/*.spec.ts"],
|
||||
"ts-node": {
|
||||
"require": ["tsconfig-paths/register"],
|
||||
"transpileOnly": true,
|
||||
"files": true
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user