migration to typescript

This commit is contained in:
JakkrapartXD 2026-01-09 06:28:15 +00:00
parent 924000b084
commit 9fde77468a
41 changed files with 11952 additions and 10164 deletions

View file

@ -1,51 +0,0 @@
const { PrismaClient } = require('@prisma/client');
const logger = require('./logger');
const prisma = new PrismaClient({
log: [
{
emit: 'event',
level: 'query',
},
{
emit: 'event',
level: 'error',
},
{
emit: 'event',
level: 'info',
},
{
emit: 'event',
level: 'warn',
},
],
});
// Log Prisma queries in development
if (process.env.NODE_ENV === 'development') {
prisma.$on('query', (e) => {
logger.debug(`Query: ${e.query}`);
logger.debug(`Duration: ${e.duration}ms`);
});
}
prisma.$on('error', (e) => {
logger.error(`Prisma Error: ${e.message}`);
});
prisma.$on('warn', (e) => {
logger.warn(`Prisma Warning: ${e.message}`);
});
// Test connection
prisma.$connect()
.then(() => {
logger.info('✅ Database connected successfully');
})
.catch((error) => {
logger.error('❌ Database connection failed:', error);
process.exit(1);
});
module.exports = prisma;

View file

@ -0,0 +1,40 @@
import { PrismaClient } from '@prisma/client';
import { logger } from './logger';
const prisma = new PrismaClient({
log: [
{
emit: 'event',
level: 'query',
},
{
emit: 'event',
level: 'error',
},
{
emit: 'event',
level: 'warn',
},
],
});
// Log queries in development
if (process.env.NODE_ENV === 'development') {
prisma.$on('query' as never, (e: any) => {
logger.debug('Prisma Query', {
query: e.query,
params: e.params,
duration: `${e.duration}ms`
});
});
}
prisma.$on('error' as never, (e: any) => {
logger.error('Prisma Error', { error: e });
});
prisma.$on('warn' as never, (e: any) => {
logger.warn('Prisma Warning', { warning: e });
});
export { prisma };

View file

@ -0,0 +1,75 @@
import dotenv from 'dotenv';
dotenv.config();
// Validate required environment variables
const requiredEnvVars = [
'DATABASE_URL',
'JWT_SECRET',
'PORT'
];
requiredEnvVars.forEach(key => {
if (!process.env[key]) {
throw new Error(`Missing required environment variable: ${key}`);
}
});
export const config = {
// Application
nodeEnv: process.env.NODE_ENV || 'development',
port: parseInt(process.env.PORT || '4000', 10),
appUrl: process.env.APP_URL || 'http://localhost:4000',
// Database
databaseUrl: process.env.DATABASE_URL!,
// Redis
redis: {
url: process.env.REDIS_URL || 'redis://localhost:6379',
password: process.env.REDIS_PASSWORD
},
// MinIO/S3
s3: {
endpoint: process.env.S3_ENDPOINT || 'http://localhost:9000',
accessKey: process.env.S3_ACCESS_KEY || 'minioadmin',
secretKey: process.env.S3_SECRET_KEY || 'minioadmin',
bucket: process.env.S3_BUCKET || 'e-learning',
useSSL: process.env.S3_USE_SSL === 'true'
},
// JWT
jwt: {
secret: process.env.JWT_SECRET!,
expiresIn: process.env.JWT_EXPIRES_IN || '24h',
refreshExpiresIn: process.env.JWT_REFRESH_EXPIRES_IN || '7d'
},
// Email
smtp: {
host: process.env.SMTP_HOST || 'localhost',
port: parseInt(process.env.SMTP_PORT || '1025', 10),
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
from: process.env.SMTP_FROM || 'noreply@elearning.local'
},
// File Upload
upload: {
maxVideoSize: parseInt(process.env.MAX_VIDEO_SIZE || '524288000', 10), // 500MB
maxAttachmentSize: parseInt(process.env.MAX_ATTACHMENT_SIZE || '104857600', 10), // 100MB
maxAttachmentsPerLesson: parseInt(process.env.MAX_ATTACHMENTS_PER_LESSON || '10', 10)
},
// CORS
cors: {
origin: process.env.CORS_ORIGIN || 'http://localhost:3000'
},
// Rate Limiting
rateLimit: {
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS || '900000', 10), // 15 minutes
maxRequests: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS || '100', 10)
}
};

View file

@ -1,45 +0,0 @@
const winston = require('winston');
const logLevels = {
error: 0,
warn: 1,
info: 2,
http: 3,
debug: 4,
};
const logColors = {
error: 'red',
warn: 'yellow',
info: 'green',
http: 'magenta',
debug: 'blue',
};
winston.addColors(logColors);
const format = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.colorize({ all: true }),
winston.format.printf(
(info) => `${info.timestamp} ${info.level}: ${info.message}`
)
);
const transports = [
new winston.transports.Console(),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error',
}),
new winston.transports.File({ filename: 'logs/all.log' }),
];
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
levels: logLevels,
format,
transports,
});
module.exports = logger;

View file

@ -0,0 +1,44 @@
import winston from 'winston';
import { config } from './index';
const logFormat = winston.format.combine(
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.errors({ stack: true }),
winston.format.splat(),
winston.format.json()
);
const consoleFormat = winston.format.combine(
winston.format.colorize(),
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }),
winston.format.printf(({ timestamp, level, message, ...meta }) => {
let msg = `${timestamp} [${level}]: ${message}`;
if (Object.keys(meta).length > 0) {
msg += ` ${JSON.stringify(meta)}`;
}
return msg;
})
);
export const logger = winston.createLogger({
level: config.nodeEnv === 'production' ? 'info' : 'debug',
format: logFormat,
transports: [
new winston.transports.Console({
format: consoleFormat
}),
new winston.transports.File({
filename: 'logs/error.log',
level: 'error'
}),
new winston.transports.File({
filename: 'logs/combined.log'
})
]
});
// Don't log to files in development
if (config.nodeEnv !== 'production') {
logger.clear();
logger.add(new winston.transports.Console({ format: consoleFormat }));
}

View file

@ -1,36 +0,0 @@
const redis = require('redis');
const logger = require('./logger');
let redisClient = null;
async function connectRedis() {
try {
redisClient = redis.createClient({
url: process.env.REDIS_URL || 'redis://localhost:6379',
});
redisClient.on('error', (err) => {
logger.error('Redis Client Error:', err);
});
redisClient.on('connect', () => {
logger.info('✅ Redis connected successfully');
});
await redisClient.connect();
return redisClient;
} catch (error) {
logger.error('❌ Redis connection failed:', error);
// Don't exit, allow app to run without Redis
return null;
}
}
function getRedisClient() {
return redisClient;
}
module.exports = {
connectRedis,
getRedisClient,
};

View file

@ -1,216 +0,0 @@
const swaggerJsdoc = require('swagger-jsdoc');
const options = {
definition: {
openapi: '3.0.0',
info: {
title: 'E-Learning Platform API',
version: '1.0.0',
description: 'API documentation for E-Learning Platform Backend',
contact: {
name: 'API Support',
email: 'support@elearning.local',
},
license: {
name: 'MIT',
url: 'https://opensource.org/licenses/MIT',
},
},
servers: [
{
url: process.env.APP_URL || 'http://localhost:4000',
description: 'Development server',
},
],
components: {
securitySchemes: {
bearerAuth: {
type: 'http',
scheme: 'bearer',
bearerFormat: 'JWT',
description: 'Enter your JWT token',
},
},
schemas: {
Error: {
type: 'object',
properties: {
error: {
type: 'object',
properties: {
code: {
type: 'string',
example: 'VALIDATION_ERROR',
},
message: {
type: 'string',
example: 'Invalid input data',
},
details: {
type: 'array',
items: {
type: 'object',
properties: {
field: {
type: 'string',
},
message: {
type: 'string',
},
},
},
},
},
},
},
},
User: {
type: 'object',
properties: {
id: {
type: 'integer',
example: 1,
},
username: {
type: 'string',
example: 'john_doe',
},
email: {
type: 'string',
format: 'email',
example: 'john@example.com',
},
role_id: {
type: 'integer',
example: 3,
},
is_active: {
type: 'boolean',
example: true,
},
created_at: {
type: 'string',
format: 'date-time',
},
updated_at: {
type: 'string',
format: 'date-time',
},
role: {
$ref: '#/components/schemas/Role',
},
profile: {
$ref: '#/components/schemas/Profile',
},
},
},
Role: {
type: 'object',
properties: {
id: {
type: 'integer',
example: 3,
},
code: {
type: 'string',
example: 'STUDENT',
},
name: {
type: 'object',
properties: {
th: {
type: 'string',
example: 'นักเรียน',
},
en: {
type: 'string',
example: 'Student',
},
},
},
description: {
type: 'object',
properties: {
th: {
type: 'string',
},
en: {
type: 'string',
},
},
},
},
},
Profile: {
type: 'object',
properties: {
id: {
type: 'integer',
},
user_id: {
type: 'integer',
},
first_name: {
type: 'string',
example: 'John',
},
last_name: {
type: 'string',
example: 'Doe',
},
phone: {
type: 'string',
example: '+66812345678',
},
avatar_url: {
type: 'string',
format: 'uri',
},
bio: {
type: 'object',
properties: {
th: {
type: 'string',
},
en: {
type: 'string',
},
},
},
},
},
AuthResponse: {
type: 'object',
properties: {
user: {
$ref: '#/components/schemas/User',
},
accessToken: {
type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
refreshToken: {
type: 'string',
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
},
},
},
},
},
tags: [
{
name: 'Authentication',
description: 'User authentication and authorization endpoints',
},
{
name: 'Health',
description: 'System health check endpoints',
},
],
},
apis: ['./src/routes/*.js', './src/app.js'],
};
const swaggerSpec = swaggerJsdoc(options);
module.exports = swaggerSpec;