Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into Survey-nomenclature
Browse files Browse the repository at this point in the history
  • Loading branch information
MattG57 committed Nov 27, 2024
2 parents 1d8ff0a + 46254e8 commit f16ea07
Show file tree
Hide file tree
Showing 90 changed files with 3,370 additions and 2,715 deletions.
47 changes: 0 additions & 47 deletions backend/__tests__/survey.test.ts

This file was deleted.

15 changes: 14 additions & 1 deletion backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
"octokit": "^4.0.2",
"sequelize": "^6.37.5",
"smee-client": "^2.0.4",
"update-dotenv": "^1.1.1"
"update-dotenv": "^1.1.1",
"why-is-node-running": "^3.2.1"
},
"devDependencies": {
"@eslint/js": "^9.14.0",
Expand Down
205 changes: 158 additions & 47 deletions backend/src/app.ts
Original file line number Diff line number Diff line change
@@ -1,59 +1,170 @@
import 'dotenv/config'
import express from 'express';
import express, { Express } from 'express';
import rateLimit from 'express-rate-limit';
import bodyParser from 'body-parser';
import cors from 'cors';
import path, { dirname } from 'path';
import { fileURLToPath } from 'url';
import * as http from 'http';
import { AddressInfo } from 'net';
import apiRoutes from "./routes/index.js"
import { dbConnect } from './database.js';
import setup from './services/setup.js';
import settingsService from './services/settings.service.js';
import SmeeService from './services/smee.js';
import Database from './database.js';
import logger, { expressLoggerMiddleware } from './services/logger.js';
import { fileURLToPath } from 'url';
import GitHub from './github.js';
import WebhookService from './services/smee.js';
import SettingsService from './services/settings.service.js';
import whyIsNodeRunning from 'why-is-node-running';

class App {
eListener?: http.Server;
baseUrl?: string;

const PORT = Number(process.env.PORT) || 80;

export const app = express();
app.use(cors());
app.use(expressLoggerMiddleware);

(async () => {
await dbConnect();
logger.info('DB Connected ✅');
await settingsService.initializeSettings();
logger.info('Settings loaded ✅');
await SmeeService.createSmeeWebhookProxy(PORT);
logger.info('Created Smee webhook proxy ✅');

try {
await setup.createAppFromEnv();
logger.info('Created GitHub App from environment ✅');
} catch (error) {
logger.info('Failed to create app from environment. This is expected if the app is not yet installed.', error);
constructor(
public e: Express,
public port: number,
public database: Database,
public github: GitHub,
public settingsService: SettingsService
) {
this.e = e;
this.port = port;
}

app.use((req, res, next) => {
if (req.path === '/api/github/webhooks') {
return next();
}
bodyParser.json()(req, res, next);
}, bodyParser.urlencoded({ extended: true }));
app.use('/api', apiRoutes);

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const frontendPath = path.resolve(__dirname, '../../frontend/dist/github-value/browser');

app.use(express.static(frontendPath));
app.get('*', rateLimit({
windowMs: 15 * 60 * 1000, max: 5000,
}), (_, res) => res.sendFile(path.join(frontendPath, 'index.html')));

app.listen(PORT, () => {
logger.info(`Server is running at http://localhost:${PORT} 🚀`);
if (process.env.WEB_URL) {
logger.debug(`Frontend is running at ${process.env.WEB_URL} 🚀`);
public async start() {
try {
this.setupExpress();
await this.database.connect();

await this.initializeSettings();
logger.info('Settings initialized');

await this.github.connect();
logger.info('Created GitHub App from environment');

return this.e;
} catch (error) {
await this.github.smee.connect();
logger.debug(error);
logger.error('Failed to start application ❌');
if (error instanceof Error) {
logger.error(error.message);
}
}
}

public stop() {
whyIsNodeRunning()
this.database.disconnect();
this.github.disconnect();
this.eListener?.close(() => {
logger.info('Server closed');
process.exit(0);
});
}

private setupExpress() {
this.e.use(cors());
this.e.use(expressLoggerMiddleware);
this.e.use((req, res, next) => {
if (req.path === '/api/github/webhooks') {
return next();
}
bodyParser.json()(req, res, next);
}, bodyParser.urlencoded({ extended: true }));

this.e.use('/api', apiRoutes);

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const frontendPath = path.resolve(__dirname, '../../frontend/dist/github-value/browser');
this.e.use(express.static(frontendPath));
this.e.get('*', rateLimit({
windowMs: 15 * 60 * 1000, max: 5000,
}), (_, res) => res.sendFile(path.join(frontendPath, 'index.html')));

const listener = this.e.listen(this.port, () => {
const address = listener.address() as AddressInfo;
logger.info(`Server is running at http://${address.address === '::' ? 'localhost' : address.address}:${address.port} 🚀`);
});
this.eListener = listener;
}

private initializeSettings() {
this.settingsService.initialize()
.then(async (settings) => {
if (settings.webhookProxyUrl) {
this.github.smee.options.url = settings.webhookProxyUrl
}
if (settings.webhookSecret) {
this.github.setInput({
webhooks: {
secret: settings.webhookSecret
}
});
}
if (settings.metricsCronExpression) {
this.github.cronExpression = settings.metricsCronExpression;
}
if (settings.baseUrl) {
this.baseUrl = settings.baseUrl;
}
})
.finally(async () => {
await this.github.smee.connect()
await this.settingsService.updateSetting('webhookSecret', this.github.input.webhooks?.secret || '');
await this.settingsService.updateSetting('webhookProxyUrl', this.github.smee.options.url!);
await this.settingsService.updateSetting('metricsCronExpression', this.github.cronExpression!);
});
}
}

const port = Number(process.env.PORT) || 80;
const e = express();
const app = new App(
e,
port,
new Database(process.env.JAWSDB_URL ? process.env.JAWSDB_URL : {
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT) || 3306,
username: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD,
database: process.env.MYSQL_DATABASE || 'value'
}),
new GitHub(
{
appId: process.env.GITHUB_APP_ID,
privateKey: process.env.GITHUB_APP_PRIVATE_KEY,
webhooks: {
secret: process.env.GITHUB_WEBHOOK_SECRET
}
},
e,
new WebhookService({
url: process.env.WEBHOOK_PROXY_URL,
path: '/api/github/webhooks',
port
})
), new SettingsService({
baseUrl: process.env.BASE_URL,
webhookProxyUrl: process.env.GITHUB_WEBHOOK_PROXY_URL,
webhookSecret: process.env.GITHUB_WEBHOOK_SECRET,
metricsCronExpression: '0 0 * * *',
devCostPerYear: '100000',
developerCount: '100',
hoursPerYear: '2080',
percentTimeSaved: '20',
percentCoding: '20'
})
);
app.start();
logger.info('App started');

export default app;

['SIGTERM', 'SIGINT', 'SIGQUIT'].forEach(signal => {
process.on(signal, () => {
logger.info(`Received ${signal}. Stopping the app...`);
app.stop();
process.exit(signal === 'uncaughtException' ? 1 : 0);
});
})();
});
4 changes: 2 additions & 2 deletions backend/src/controllers/metrics.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import MetricsService from '../services/metrics.service.js';
class MetricsController {
async getMetrics(req: Request, res: Response): Promise<void> {
try {
const metrics = await MetricsService.queryMetrics(req.query)
const metrics = await MetricsService.getMetrics(req.query)
res.status(200).json(metrics);
} catch (error) {
res.status(500).json(error);
Expand All @@ -13,7 +13,7 @@ class MetricsController {

async getMetricsTotals(req: Request, res: Response): Promise<void> {
try {
const metrics = await MetricsService.queryMetricsTotals(req.query)
const metrics = await MetricsService.getMetricsTotals(req.query)
res.status(200).json(metrics);
} catch (error) {
res.status(500).json(error);
Expand Down
29 changes: 6 additions & 23 deletions backend/src/controllers/seats.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import SeatsService from '../services/copilot.seats.service.js';

class SeatsController {
async getAllSeats(req: Request, res: Response): Promise<void> {
const org = req.query.org?.toString()
try {
const seats = await SeatsService.getAllSeats();
const seats = await SeatsService.getAllSeats(org);
res.status(200).json(seats);
} catch (error) {
res.status(500).json(error);
Expand All @@ -23,48 +24,30 @@ class SeatsController {
}

async getActivity(req: Request, res: Response): Promise<void> {
const org = req.query.org?.toString()
const { daysInactive, precision } = req.query;
const _daysInactive = Number(daysInactive);
if (!daysInactive || isNaN(_daysInactive)) {
res.status(400).json({ error: 'daysInactive query parameter is required' });
return;
}
try {
const activityDays = await SeatsService.getAssigneesActivity(_daysInactive, precision as 'hour' | 'day' | 'minute');
const activityDays = await SeatsService.getMembersActivity(org, _daysInactive, precision as 'hour' | 'day' | 'minute');
res.status(200).json(activityDays);
} catch (error) {
res.status(500).json(error);
}
}

async getActivityTotals(req: Request, res: Response): Promise<void> {
const org = req.query.org?.toString()
try {
const totals = await SeatsService.getAssigneesActivityTotals();
const totals = await SeatsService.getMembersActivityTotals(org);
res.status(200).json(totals);
} catch (error) {
res.status(500).json(error);
}
}

async getActivityHighcharts(req: Request, res: Response): Promise<void> {
try {
const { daysInactive } = req.query;
const _daysInactive = Number(daysInactive);
if (!daysInactive || isNaN(_daysInactive)) {
res.status(400).json({ error: 'daysInactive query parameter is required' });
return;
}
const activityDays = await SeatsService.getAssigneesActivity(_daysInactive);
const activeData = Object.entries(activityDays).reduce((acc, [date, data]) => {
acc.push([new Date(date).getTime(), data.totalActive]);
return acc;
}, [] as [number, number][]);
res.status(200).json(activeData);
} catch (error) {
res.status(500).json(error);
}
}

}

export default new SeatsController();
Loading

0 comments on commit f16ea07

Please sign in to comment.