diff --git a/package-lock.json b/package-lock.json index 132df69..ddee082 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,17 +1,17 @@ { "name": "pam-hapi", - "version": "0.5.0", + "version": "0.5.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "pam-hapi", - "version": "0.5.0", + "version": "0.5.4", "license": "GPL-3.0", "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", @@ -2081,9 +2081,9 @@ } }, "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.4.0.tgz", + "integrity": "sha512-kCKF62fwtzwYm0IGBNjRUjtJgMfGapII+FslMHIjMR5KTnwEmBmWLDRSnc3XSNP8bNy34tekgQyDT0hr7pERRQ==", "license": "BSD-2-Clause", "engines": { "node": ">=12" diff --git a/package.json b/package.json index 520f7b0..639f384 100644 --- a/package.json +++ b/package.json @@ -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": { @@ -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", diff --git a/src/config.ts b/src/config.ts index 1fdf7af..bc30f97 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,5 @@ import {EnvService} from '@services/envService'; +import packageJson from "~~/package.json"; const env = EnvService.instance; @@ -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'], diff --git a/src/controllers/haController.ts b/src/controllers/haController.ts index ee6002b..157f07d 100644 --- a/src/controllers/haController.ts +++ b/src/controllers/haController.ts @@ -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; } }; \ No newline at end of file diff --git a/src/errors/PandaScoreError.ts b/src/errors/PandaScoreError.ts index 67579b2..c7a715c 100644 --- a/src/errors/PandaScoreError.ts +++ b/src/errors/PandaScoreError.ts @@ -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; } + } \ No newline at end of file diff --git a/src/middlewares/exceptions.handler.ts b/src/middlewares/exceptions.handler.ts index 1aa771c..d7157ce 100644 --- a/src/middlewares/exceptions.handler.ts +++ b/src/middlewares/exceptions.handler.ts @@ -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 = ( @@ -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, }); }; \ No newline at end of file diff --git a/src/modules/logger.ts b/src/modules/logger.ts index 591aa54..cba3cb9 100644 --- a/src/modules/logger.ts +++ b/src/modules/logger.ts @@ -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, } }); @@ -23,7 +25,7 @@ export class Logger { getLogger(serviceName: string) { return this.logger.child({ - 'service.name': serviceName + 'app.service.name': serviceName }); } diff --git a/src/repositories/pandaScoreRepository.ts b/src/repositories/pandaScoreRepository.ts index 87f3c1c..1ef2862 100644 --- a/src/repositories/pandaScoreRepository.ts +++ b/src/repositories/pandaScoreRepository.ts @@ -20,8 +20,42 @@ export class PandaScoreRepository { * `search[name]=` */ async fetchTeams(query: string): Promise { - 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; + }); } /** @@ -31,11 +65,41 @@ export class PandaScoreRepository { * @throws {PandaScoreError} */ async fetchMatches(query: string): Promise { - 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; + }); } } diff --git a/src/services/envService.ts b/src/services/envService.ts index e64f113..0bf60e8 100644 --- a/src/services/envService.ts +++ b/src/services/envService.ts @@ -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(); @@ -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, } });