Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "pam-hapi",
"version": "0.5.0",
"version": "0.5.4",
"description": "Personal API for Home Assistant",
"main": "server.js",
"scripts": {
Expand All @@ -24,7 +24,7 @@
"dependencies": {
"@elastic/ecs-pino-format": "^1.5.0",
"cors": "^2.8.6",
"dotenv": "^17.3.1",
"dotenv": "^17.4.0",
"express": "^5.2.1",
"pino": "^10.3.1",
"swagger-jsdoc": "^6.2.8",
Expand Down
31 changes: 15 additions & 16 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {EnvService} from '@services/envService';
import packageJson from "~~/package.json";

const env = EnvService.instance;

Expand All @@ -11,31 +12,29 @@ const config: Config = {
port: Number(env.get('application.port')),
nodeEnv: env.get('application.env'),
};
const url = () => {
switch (config.nodeEnv) {
case 'production':
return `http://pami-serv:${config.port}/api/v1`;
case 'development':
case 'docker':
default:
return `http://localhost:${config.port}/api/v1`;
}
}

const swaggerConfig = {
definition: {
openapi: '3.0.0',
info: {
title: 'PAM\'HAPI',
version: '0.5.0',
version: packageJson.version,
},
servers: [
{
url: 'http://localhost:3000/api/v1',
description: 'Development server',
},
{
url: 'http://localhost:7001/api/v1',
description: 'Docker',
},
{
url: 'http://pami-serv:7000/api/v1',
description: 'HA server (prod)',
},
{
url: 'http://pami-serv:7001/api/v1',
description: 'HA server (dev)',
},
url: url(),
description: 'Url d\'accès',
}
],
},
apis: ['./src/routes/*.ts', './src/models/**/*.ts', './src/entities/**/*.ts'],
Expand Down
17 changes: 17 additions & 0 deletions src/controllers/haController.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,31 @@
import {HaService} from '@services/haService';
import {NextFunction, Request, Response} from 'express';
import qs from 'qs';
import {Logger} from "@modules/logger";
import {PandaScoreError} from "@errors/PandaScoreError";

const service = new HaService();
const logger = Logger.instance.getLogger('HaController');

export const getHaConf = async (req: Request, res: Response, next: NextFunction) => {
try {
const queryParam = qs.parse(qs.stringify(req.query));
res.json(await service.getHaConf(queryParam.weekday as string));
} catch (error) {
if (error instanceof PandaScoreError) {
throw error;
}
logger.error(`Error fetching HA configuration: ${error instanceof Error ? error.message : JSON.stringify(error)}`);
const err = new PandaScoreError('HaConfFetchError', 'getHaConf');
if (error instanceof Error) {
err.message = `Error fetching HA configuration: ${error.message}`;
err.stack = error.stack;
err.cause = error.cause;
err.name = error.name;
} else {
err.stack = JSON.stringify(error);
}
next(error);
throw err;
}
};
20 changes: 14 additions & 6 deletions src/errors/PandaScoreError.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
export class PandaScoreError extends Error {
error: string;
import {AppError} from "@middlewares/exceptions.handler";

constructor(error: string, message: string) {
super(message);
export class PandaScoreError implements AppError {
cause?: unknown;
name: string;
message: string;
reason: string;
stack?: string;
status?: number;
thrownBy: string;

this.error = error;
constructor(reason: string, thrownBy: string, message?: string) {
this.message = message || `An error occurred for the reason ${reason}`;
this.name = 'PandaScoreError';
Object.setPrototypeOf(this, PandaScoreError.prototype);
this.reason = reason;
this.thrownBy = thrownBy;
}

}
26 changes: 18 additions & 8 deletions src/middlewares/exceptions.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import {NextFunction, Request, Response} from 'express';
import {Logger} from "@modules/logger";

export interface AppError extends Error {
reason: string;
status?: number;
thrownBy: string;
}

export const errorHandler = (
Expand All @@ -16,16 +18,24 @@ export const errorHandler = (
return next(err);
}

const log = Logger.instance.getLogger('ErrorHandler');
const from = err.stack
?.split('\n')[1]
.split('/')
.find(v => /\d*\.ts/.test(v))
?? 'unknown source';
const log = Logger.instance.getLogger('ErrorHandler').child({
'error.cause': err.cause,
'error.message': err.message,
'error.name': err.name,
'error.reason': err.reason,
'error.stack': err.stack,
'error.status': err.status ?? 500,
'error.thrownBy': err.thrownBy,
});

log.error(`Error from ${from} : ${err.message}`);
log.trace(err.stack);
log.error(err.message);
res.status(err.status ?? 500).json({
message: err.message || 'Internal Server Error',
reason: err.reason || 'UnknownError',
'error.cause': err.cause,
'error.name': err.name,
'error.stack': err.stack,
'error.status': err.status ?? 500,
'error.thrownBy': err.thrownBy,
});
};
8 changes: 5 additions & 3 deletions src/modules/logger.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import ecsFormat from "@elastic/ecs-pino-format";
import pino from "pino";
import {EnvService} from "@services/envService";
import packageJson from '~~/package.json';

export class Logger {
private static _: Logger;
private readonly logger = pino({
...ecsFormat(),
name: 'pino@^10.3.1',
name: `pino@${packageJson.dependencies.pino}`,
level: EnvService.instance.get('log.level'),
base: {
'service.tag': EnvService.instance.get('elasticsearch.service.tag')
'app.service.tag': EnvService.instance.get('elasticsearch.service.tag'),
'app.version': packageJson.version,
}
});

Expand All @@ -23,7 +25,7 @@ export class Logger {

getLogger(serviceName: string) {
return this.logger.child({
'service.name': serviceName
'app.service.name': serviceName
});
}

Expand Down
80 changes: 72 additions & 8 deletions src/repositories/pandaScoreRepository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,42 @@ export class PandaScoreRepository {
* `search[name]=<team_name>`
*/
async fetchTeams(query: string): Promise<TeamEntity[]> {
const response = await fetch(`${BASE_URL}/teams?${query}`, options);
return await response.json() as TeamEntity[];
return fetch(`${BASE_URL}/teams?${query}`, options)
.catch((error: Error) => {
const err = error as PandaScoreError;
err.thrownBy = `fetchTeams(${query})`;
err.reason = 'fetch failed'
throw err;
})
.then(response => response.json().catch(error => {
const err = error as PandaScoreError;
err.thrownBy = `fetchTeams(${query})`;
err.reason = 'invalid JSON response';
throw err;
}))
.then(data => {
if (Array.isArray(data)) {
return data as TeamEntity[];
}
if (JSON.stringify(data).includes('Invalid credentials')) {
const err = new PandaScoreError('InvalidToken', `fetchTeams(${query})`);
err.message = `Received response from PandaScore API. Please check your environment variables.`;
err.stack = JSON.stringify(data);
err.status = 503;
throw err;
}
if (data as { error: string, status: number }) {
const err = new PandaScoreError('PandaScoreAPIError', `fetchTeams(${query})`);
err.message = `Received error response from PandaScore API`;
err.stack = JSON.stringify(data);
err.status = (data as { error: string, status: number }).status;
throw err;
}
const err = new PandaScoreError('UnexpectedResponse', `fetchTeams(${query})`);
err.message = `Response data is not an array of TeamEntity`;
err.stack = JSON.stringify(data);
throw err;
});
}

/**
Expand All @@ -31,11 +65,41 @@ export class PandaScoreRepository {
* @throws {PandaScoreError}
*/
async fetchMatches(query: string): Promise<MatchEntity[]> {
const response = await fetch(`${BASE_URL}/matches?${query}`, options);
if (!response.ok) {
const body = await response.json() as { error: string, message: string };
throw new PandaScoreError(body.error, body.message);
}
return (await response.json() as MatchEntity[]) ?? [];
return fetch(`${BASE_URL}/matches?${query}`, options)
.catch((error: Error) => {
const err = error as PandaScoreError;
err.thrownBy = `fetchMatches(${query})`;
err.reason = 'fetch failed'
throw err;
})
.then(response => response.json().catch(error => {
const err = error as PandaScoreError;
err.thrownBy = `fetchTeams(${query})`;
err.reason = 'invalid JSON response';
throw err;
}))
.then(data => {
if (Array.isArray(data)) {
return data as MatchEntity[];
}
if (JSON.stringify(data).includes('Invalid credentials')) {
const err = new PandaScoreError('InvalidToken', `fetchMatches(${query})`);
err.message = `Received response from PandaScore API. Please check your environment variables.`;
err.stack = JSON.stringify(data);
err.status = 503;
throw err;
}
if (data as { error: string, status: number }) {
const err = new PandaScoreError('PandaScoreAPIError', `fetchMatches(${query})`);
err.message = `Received error response from PandaScore API`;
err.stack = JSON.stringify(data);
err.status = (data as { error: string, status: number }).status;
throw err;
}
const err = new PandaScoreError('UnexpectedResponse', `fetchMatches(${query})`);
err.message = `Response data is not an array of TeamEntity`;
err.stack = JSON.stringify(data);
throw err;
});
}
}
8 changes: 5 additions & 3 deletions src/services/envService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import dotenv from 'dotenv';
import pino from "pino";
import ecsFormat from "@elastic/ecs-pino-format";
import packageJson from "~~/package.json";

dotenv.config();

Expand All @@ -9,11 +10,12 @@ export class EnvService {
private readonly env: NodeJS.ProcessEnv;
private static readonly log = pino({
...ecsFormat(),
name: 'pino@^10.3.1',
name: `pino@${packageJson.dependencies.pino}`,
level: process.env['log.level'] ?? 'info',
base: {
'service.name': 'EnvService',
'service.tag': process.env['elasticsearch.service.tag']
'app.service.name': 'EnvService',
'app.service.tag': process.env['elasticsearch.service.tag'],
'app.version': packageJson.version,
}
});

Expand Down
Loading