Skip to content

Commit

Permalink
Merge pull request #7 from ALPHACamp/refactor/modularize_apiServer
Browse files Browse the repository at this point in the history
Refactor/modularize api server
  • Loading branch information
tim80411 authored Jul 16, 2023
2 parents 79b5ba9 + a41bcce commit 8f027f9
Show file tree
Hide file tree
Showing 19 changed files with 633 additions and 125 deletions.
323 changes: 307 additions & 16 deletions package-lock.json

Large diffs are not rendered by default.

8 changes: 6 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@
"discord.js": "^14.9.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"firebase-admin": "^11.7.0"
"express-async-errors": "^3.1.1",
"firebase-admin": "^11.7.0",
"pino": "^8.14.1",
"pino-pretty": "^10.0.1",
"uuid": "^9.0.0"
}
}
}
37 changes: 37 additions & 0 deletions src/apiServer/apiServer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const app = require('./express');
const http = require('http');

const logger = require('../lib/logger.js');

class ApiServer {
constructor(port) {
logger.info('Configure API Server ...', { port, node_env: process.env.NODE_ENV });

this.port = port;
this.httpServer = null;
this.db = null;

this.#createServer();
this.#bindEvents();
}

#createServer() {
this.httpServer = http.createServer(app);
}

#bindEvents() {
logger.info('Binding Events ...');
this.httpServer.on('listening', () => {
logger.info(`Server started on port: ${this.port} for NODE_ENV:${process.env.NODE_ENV}`);
});
this.httpServer.on('error', (err) => {
logger.error('Server error:', err);
});
}

start() {
this.httpServer.listen(this.port);
}
}

module.exports = ApiServer;
33 changes: 33 additions & 0 deletions src/apiServer/express.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const express = require('express');
require('express-async-errors'); // for catching async error
const cors = require('cors');
const router = require('./routes/index.js');
const errorHandler = require('./middlewares/errorHandler.js');
const resMethod = require('./middlewares/resMethod.js');
const reqInitializer = require('./middlewares/reqInitializer.js');

const app = express();

app.use(cors({
exposedHeaders: ['X-Request-Id'],
}));


app.use(reqInitializer);
app.use(resMethod);
app.use('/api', router);

app.use(errorHandler);

// 404 handler
app.use('*', (req, res) => res.status(404).send({
success: false,
message: '指定路徑並不存在',
error: {
code: 404,
type: 'RouteNotFound',
debugInfo: {},
},
}));

module.exports = app;
26 changes: 26 additions & 0 deletions src/apiServer/middlewares/errorHandler.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const logger = require('../../lib/logger.js');
const Errors = require('../../lib/Errors.js');

/**
* @type {import('express').ErrorRequestHandler}
*/
module.exports = function errorHandler(err, req, res, next) {
const errorMessage = err.msg || err.message;
const data = {};
if (process.env.NODE_ENV === 'dev') {
data.stack = err.stack;
console.log(err);
}

logger.info(`General error handler for error: ${errorMessage}`, data);
if (res.headersSent) {
return next(err);
} else if (err instanceof Errors.GeneralError) {
return res.fail(err);
}

// 當錯誤並非預定義時
logger.error({ msg: 'Exception', errMsg: err.message, errName: err.name });
res.status(501);
return res.send(err.message);
};
31 changes: 31 additions & 0 deletions src/apiServer/middlewares/reqInitializer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
const { v4: uuidv4 } = require('uuid');
const logger = require('../../lib/logger');

/**
* @type {import('express').Handler}
*/
module.exports = function reqInitializer(req, res, next) {
const start = process.hrtime.bigint();
req.requestId = uuidv4();

const { method, query = {}, body = {}, requestId, originalUrl } = req;
logger.debug({ msg: `${method} ${originalUrl} [STARTED]`, requestId, query, body });

res.on('finish', () => {
const durationInMilliseconds = getDurationInMilliseconds(start);
logger.info({ msg: `${method} ${originalUrl} [FINISHED] ${durationInMilliseconds.toLocaleString()} ms`, requestId });
});

res.on('close', () => {
const durationInMilliseconds = getDurationInMilliseconds(start);
logger.info({ msg: `${method} ${originalUrl} [CLOSED] ${durationInMilliseconds.toLocaleString()} ms`, requestId });
});

res.setHeader('X-Request-Id', req.requestId);
next();
};

function getDurationInMilliseconds(start) {
const diff = process.hrtime.bigint() - start;
return Number(diff) / 1000000;
}
29 changes: 29 additions & 0 deletions src/apiServer/middlewares/resMethod.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* @type {import('express').Handler}
*/
module.exports = function resMethod(req, res, next) {
res.success = function success({ data, ...info }) {
res.status(200).send({
success: true,
data,
...info,
});
};

/**
* @param {Error} error
*/
res.fail = function fail(error) {
res.status(error.code).send({
success: false,
message: error.message,
error: {
code: error.code,
type: error.name,
debugInfo: error.data,
}
});
};

next();
};
10 changes: 10 additions & 0 deletions src/apiServer/routes/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
const express = require('express');
const router = express.Router();

router.use('/leaderboard', require('./leaderboard.js'));
router.get('/healthCheck', (req, res) => {
return res.success({ data: 'ok' });
});


module.exports = router;
81 changes: 81 additions & 0 deletions src/apiServer/routes/leaderboard.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
const express = require('express');
const router = express.Router();
const Discord = require('discord.js');

const { db } = require('../../config/db');
const { pageSize } = require('../../const.js');

function validate(req, res, next) {
const { date, discordId, page } = req.query;
if (page && typeof Number(page) && Number(page) <= 0) {
return res.status(400).json('page must be valid number');
}

const dateRegex = /([12]\d{3}-(0[1-9]|1[0-2]))/;
if (date && !dateRegex.test(date)) {
return res.status(400).json('date must be in YYYY-MM format');
}

if (discordId && typeof discordId !== 'string') {
return res.status(400).json('discordId must be string');
}

next();
}



router.get('/', validate, async function (req, res) {
const currentYearAndMonth = new Date().toISOString().slice(0, 7);
const { date, discordId, page = 1 } = req.query;
let ref = db
.collection(`leaderboard-${currentYearAndMonth}`)
.where('period', '=', date || currentYearAndMonth);

const countSnapshot = await ref.count().get();
const totalDocsCount = countSnapshot.data().count;
const totalPages = Math.ceil(totalDocsCount / 10);
const offset = (page - 1) * pageSize;

if (discordId) {
ref = ref.where('discordId', '=', discordId);
}

const querySnapshot = await ref
.orderBy('point', 'desc')
.limit(pageSize)
.offset(offset)
.get();

const client = new Discord.Client({
intents: []
});
client.login(process.env.DISCORD_TOKEN);

const fetchUserDetails = async (doc) => {
const data = doc.data();
const guild = await client.guilds.fetch(process.env.DISCORD_GUILDID);
const member = await guild.members.fetch(data.discordId);

return {
id: doc.id,
name: member.nickname || member.user.username,
avatarURL: member.user.displayAvatarURL(),
...data
};
};

const promises = querySnapshot.docs.map(fetchUserDetails);
const results = await Promise.all(promises);

return res.success({
data: results,
offset,
pageSize,
totalPages,
currentPage: page,
totalDataCount: totalDocsCount
});
});

module.exports = router;
21 changes: 10 additions & 11 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
// Require the necessary discord.js classes
require('dotenv').config();
const express = require('express');
var cors = require('cors');
const app = express();
const { readdirSync } = require('node:fs');
const { join } = require('node:path');
const {
Expand All @@ -15,6 +12,8 @@ const {
} = require('discord.js');
const { db } = require('./config/db.js');
const { FieldValue } = require('firebase-admin/firestore');
const logger = require('./lib/logger.js');

const fs = require('fs');
const path = require('path');
const client = new Client({
Expand Down Expand Up @@ -53,7 +52,7 @@ for (const file of commandFiles) {
if ('data' in command && 'execute' in command) {
client.commands.set(command.data.name, command);
} else {
console.log(
logger.info(
`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`
);
}
Expand All @@ -66,7 +65,7 @@ client.on(Events.MessageReactionAdd, async (reaction, user) => {
try {
await reaction.fetch();
} catch (error) {
console.error('Something went wrong when fetching the message:', error);
logger.error('Something went wrong when fetching the message:', error);
// Return as `reaction.message.author` may be undefined/null
return;
}
Expand Down Expand Up @@ -139,10 +138,10 @@ client.on(Events.MessageReactionRemove, async (reaction, user) => {
// Log in to Discord with your client's token
client.login(process.env.DISCORD_TOKEN);

const apiRoutes = require('./routes/index');

app.use('/api', cors(), apiRoutes);

app.listen(process.env.PORT || 3306, () => {
console.log(`server is running on ${process.env.PORT || 3306}`);
});
/**
* API Server Execution
*/
const ApiServer = require('./apiServer/apiServer.js');
const server = new ApiServer(process.env.PORT || 3306);
server.start();
4 changes: 3 additions & 1 deletion src/commands/leaderboard/rank.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
require('dotenv').config();
const { SlashCommandBuilder } = require('discord.js');
const { db } = require('../../config/db.js');
const logger = require('../../lib/logger.js');

module.exports = {
data: new SlashCommandBuilder()
.setName('rank')
Expand Down Expand Up @@ -46,6 +48,6 @@ async function queryUserRankAndPoint(discordId) {

return { userRank, userPoint };
} catch (error) {
console.error('查詢用戶排名時發生錯誤:', error);
logger.error('查詢用戶排名時發生錯誤:', error);
}
}
10 changes: 6 additions & 4 deletions src/deploy-commands.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ const { REST, Routes } = require('discord.js');
const fs = require('node:fs');
const path = require('node:path');

const logger = require('./lib/logger.js');

const commands = [];
// Grab all the command files from the commands directory you created earlier
const foldersPath = path.join(__dirname, 'commands');
Expand All @@ -20,7 +22,7 @@ for (const folder of commandFolders) {
if ('data' in command && 'execute' in command) {
commands.push(command.data.toJSON());
} else {
console.log(
logger.info(
`[WARNING] The command at ${filePath} is missing a required "data" or "execute" property.`
);
}
Expand All @@ -33,7 +35,7 @@ const rest = new REST().setToken(process.env.DISCORD_TOKEN);
// and deploy your commands!
(async () => {
try {
console.log(
logger.info(
`Started refreshing ${commands.length} application (/) commands.`
);

Expand All @@ -46,11 +48,11 @@ const rest = new REST().setToken(process.env.DISCORD_TOKEN);
{ body: commands }
);

console.log(
logger.info(
`Successfully reloaded ${data.length} application (/) commands.`
);
} catch (error) {
// And of course, make sure you catch and log any errors!
console.error(error);
logger.error(error);
}
})();
Loading

0 comments on commit 8f027f9

Please sign in to comment.