From 1904aab11c359308a91f74d30b894075c711b87f Mon Sep 17 00:00:00 2001 From: Benoit Serrano Date: Fri, 18 Oct 2024 19:12:44 +0200 Subject: [PATCH 1/7] add script to import db --- package.json | 3 ++- src/lib/api.ts | 26 ++++++++++++++++++++++++++ src/scripts/importDb.ts | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) create mode 100644 src/lib/api.ts create mode 100644 src/scripts/importDb.ts diff --git a/package.json b/package.json index 1635a25..332f2d4 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "migration:run": "npm run build && typeorm migration:run -d dist/src/dataSource.js", "migration:create": " typeorm migration:create src/migrations/$npm_config_name", "migration:revert": " npm run build && typeorm migration:revert -d dist/src/dataSource.js", - "typeorm": "typeorm-ts-node-esm" + "typeorm": "typeorm-ts-node-esm", + "script:importDb": "npm run build && node dist/src/scripts/importDb.js" }, "author": "", "license": "ISC", diff --git a/src/lib/api.ts b/src/lib/api.ts new file mode 100644 index 0000000..2f3f962 --- /dev/null +++ b/src/lib/api.ts @@ -0,0 +1,26 @@ +import { Client } from '../modules/client'; +import { Ping } from '../modules/ping'; + +const api = { + fetchAllClients, + fetchAllPings, +}; +const BASE_URL = 'https://ping-storage.osc-fr1.scalingo.io'; + +async function fetchAllClients(): Promise { + const URL = `${BASE_URL}/api/clients`; + + const response = await fetch(URL); + const parsedData = await response.json(); + return parsedData; +} + +async function fetchAllPings(): Promise { + const URL = `${BASE_URL}/api/pings`; + + const response = await fetch(URL); + const parsedData = await response.json(); + return parsedData; +} + +export { api }; diff --git a/src/scripts/importDb.ts b/src/scripts/importDb.ts new file mode 100644 index 0000000..3fd4310 --- /dev/null +++ b/src/scripts/importDb.ts @@ -0,0 +1,35 @@ +import { dataSource } from '../dataSource'; +import { api } from '../lib/api'; +import { Client } from '../modules/client'; +import { Ping } from '../modules/ping'; + +async function importDb() { + console.log('Initializing database...'); + await dataSource.initialize(); + console.log('Database initialized!'); + const clientRepository = dataSource.getRepository(Client); + const pingRepository = dataSource.getRepository(Ping); + + console.log('Erasing local database...'); + + await clientRepository.delete({}); + await pingRepository.delete({}); + + console.log('Fetching clients...'); + const allClients = await api.fetchAllClients(); + console.log(`${allClients.length} clients fetched! Inserting them in database...`); + + await clientRepository.insert(allClients); + console.log('Clients inserted! Now fetching pings...'); + + const allPings = await api.fetchAllPings(); + + console.log(`${allPings.length} pings fetched! Inserting them in database...`); + + await pingRepository.insert(allPings); + console.log(`${allPings.length} pings inserted!`); + + console.log('Done!'); +} + +importDb(); From ed1d0e29f9c020e50b05c388a93358d70c8f63d4 Mon Sep 17 00:00:00 2001 From: Benoit Serrano Date: Fri, 18 Oct 2024 19:19:56 +0200 Subject: [PATCH 2/7] add route clients --- package-lock.json | 23 ++++++++++++++++++++++ package.json | 2 ++ src/app/app.ts | 4 ++-- src/client/src/lib/api.ts | 7 ++++++- src/client/src/pages/Clients.tsx | 26 +++++++++++++++++++++++++ src/client/src/routes/routeElements.tsx | 2 ++ src/client/src/routes/routeKeys.ts | 2 +- src/client/src/routes/routePaths.ts | 5 ++++- src/client/src/routes/routeTitles.ts | 1 + src/config.ts | 1 + 10 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 src/client/src/pages/Clients.tsx diff --git a/package-lock.json b/package-lock.json index 601a832..bb377f2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@types/express": "^4.17.13", "@types/node": "^20.3.1", "body-parser": "^1.20.2", + "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.1", "http-status": "^1.6.2", @@ -24,6 +25,7 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@types/cors": "^2.8.17", "nodemon": "^3.1.4", "ts-node": "^10.9.1", "typescript": "^4.6.4" @@ -600,6 +602,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/express": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", @@ -1328,6 +1339,18 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", diff --git a/package.json b/package.json index 332f2d4..f142961 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "license": "ISC", "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@types/cors": "^2.8.17", "nodemon": "^3.1.4", "ts-node": "^10.9.1", "typescript": "^4.6.4" @@ -26,6 +27,7 @@ "@types/express": "^4.17.13", "@types/node": "^20.3.1", "body-parser": "^1.20.2", + "cors": "^2.8.5", "dotenv": "^16.0.3", "express": "^4.18.1", "http-status": "^1.6.2", diff --git a/src/app/app.ts b/src/app/app.ts index 20689c6..fa39ae0 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,5 +1,5 @@ import Express, { Response } from 'express'; -import path from 'path'; +import cors from 'cors'; import bodyParser from 'body-parser'; import { config } from '../config'; import { dataSource } from '../dataSource'; @@ -14,7 +14,7 @@ async function runApp() { const app = Express(); - app.use('/api', bodyParser.json(), router); + app.use('/api', bodyParser.json(), cors({ origin: config.HOST_URL }), router); app.listen(config.PORT, async () => { logger.info(`Server is running on port ${config.PORT}`); diff --git a/src/client/src/lib/api.ts b/src/client/src/lib/api.ts index 20dab96..dcc52dd 100644 --- a/src/client/src/lib/api.ts +++ b/src/client/src/lib/api.ts @@ -1,7 +1,7 @@ import { config } from '../config'; import { localStorage } from './localStorage'; -const api = {}; +const api = { getClients }; const BASE_URL = `${config.API_URL}/api`; @@ -45,4 +45,9 @@ async function performApiCall( return response.json(); } +async function getClients() { + const URL = `${BASE_URL}/clients`; + return performApiCall(URL, 'GET'); +} + export { api }; diff --git a/src/client/src/pages/Clients.tsx b/src/client/src/pages/Clients.tsx new file mode 100644 index 0000000..f9ff539 --- /dev/null +++ b/src/client/src/pages/Clients.tsx @@ -0,0 +1,26 @@ +import { useQuery } from '@tanstack/react-query'; +import { api } from '../lib/api'; + +type clientType = { + id: string; + name: string; +}; + +function Clients() { + const query = useQuery({ queryFn: api.getClients, queryKey: ['clients'] }); + + if (!query.data) { + return
Loading...
; + } + return ( +
+
    + {query.data.map((client) => ( +
  • {client.name}
  • + ))} +
+
+ ); +} + +export { Clients }; diff --git a/src/client/src/routes/routeElements.tsx b/src/client/src/routes/routeElements.tsx index a26619c..c0055c5 100644 --- a/src/client/src/routes/routeElements.tsx +++ b/src/client/src/routes/routeElements.tsx @@ -1,8 +1,10 @@ +import { Clients } from '../pages/Clients'; import { Home } from '../pages/Home'; import { ROUTE_KEYS } from './routeKeys'; const ROUTE_ELEMENTS: Record<(typeof ROUTE_KEYS)[number], { element: JSX.Element }> = { HOME: { element: }, + CLIENTS: { element: }, }; export { ROUTE_ELEMENTS }; diff --git a/src/client/src/routes/routeKeys.ts b/src/client/src/routes/routeKeys.ts index b7c7cc8..6ae7fa6 100644 --- a/src/client/src/routes/routeKeys.ts +++ b/src/client/src/routes/routeKeys.ts @@ -1,3 +1,3 @@ -const ROUTE_KEYS = ['HOME'] as const; +const ROUTE_KEYS = ['HOME', 'CLIENTS'] as const; export { ROUTE_KEYS }; diff --git a/src/client/src/routes/routePaths.ts b/src/client/src/routes/routePaths.ts index 5995d93..0ac4df1 100644 --- a/src/client/src/routes/routePaths.ts +++ b/src/client/src/routes/routePaths.ts @@ -2,7 +2,10 @@ import { ROUTE_KEYS } from './routeKeys'; const ROUTE_PATHS: Record<(typeof ROUTE_KEYS)[number], { path: string }> = { HOME: { - path: '/*', + path: '/', + }, + CLIENTS: { + path: '/clients', }, }; diff --git a/src/client/src/routes/routeTitles.ts b/src/client/src/routes/routeTitles.ts index 5b8902b..095952c 100644 --- a/src/client/src/routes/routeTitles.ts +++ b/src/client/src/routes/routeTitles.ts @@ -2,6 +2,7 @@ import { ROUTE_KEYS } from './routeKeys'; const ROUTE_TITLES: Record<(typeof ROUTE_KEYS)[number], string> = { HOME: 'Accueil', + CLIENTS: 'Liste des clients', }; export { ROUTE_TITLES }; diff --git a/src/config.ts b/src/config.ts index c5791fa..9b238cf 100644 --- a/src/config.ts +++ b/src/config.ts @@ -16,6 +16,7 @@ if (process.env.DATABASE_URL) { const config = { PORT: process.env.PORT || 3000, + HOST_URL: process.env.HOST_URL || '', DATABASE_HOST: process.env.DATABASE_HOST || '', DATABASE_PASSWORD: process.env.DATABASE_PASSWORD || '', DATABASE_USER: process.env.DATABASE_USER || '', From 25423941cc3a0b605bd016a40e5ec52ece43629c Mon Sep 17 00:00:00 2001 From: Benoit Serrano Date: Sun, 20 Oct 2024 11:30:08 +0100 Subject: [PATCH 3/7] add events system --- src/app/app.ts | 2 +- src/client/src/lib/api.ts | 17 +++--- src/client/src/lib/pathHandler.test.ts | 49 +++++++++++++++++ src/client/src/lib/pathHandler.ts | 65 +++++++++++++++++++++++ src/client/src/pages/ClientSummary.tsx | 42 +++++++++++++++ src/client/src/pages/Clients.tsx | 10 +++- src/client/src/routes/routeElements.tsx | 2 + src/client/src/routes/routeKeys.ts | 2 +- src/client/src/routes/routePaths.ts | 3 ++ src/client/src/routes/routeTitles.ts | 1 + src/dataSource.ts | 3 +- src/migrations/1729418677813-add-event.ts | 22 ++++++++ src/modules/client/client.controller.ts | 10 ++++ src/modules/client/client.service.ts | 10 ++++ src/modules/event/Event.entity.ts | 21 ++++++++ src/modules/event/event.service.ts | 45 ++++++++++++++++ src/modules/event/index.ts | 4 ++ src/modules/event/types.ts | 6 +++ src/modules/ping/ping.service.ts | 16 ++++++ src/router/clientRoutes.ts | 10 ++++ 20 files changed, 330 insertions(+), 10 deletions(-) create mode 100644 src/client/src/lib/pathHandler.test.ts create mode 100644 src/client/src/lib/pathHandler.ts create mode 100644 src/client/src/pages/ClientSummary.tsx create mode 100644 src/migrations/1729418677813-add-event.ts create mode 100644 src/modules/event/Event.entity.ts create mode 100644 src/modules/event/event.service.ts create mode 100644 src/modules/event/index.ts create mode 100644 src/modules/event/types.ts diff --git a/src/app/app.ts b/src/app/app.ts index fa39ae0..100f49c 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,4 +1,4 @@ -import Express, { Response } from 'express'; +import Express from 'express'; import cors from 'cors'; import bodyParser from 'body-parser'; import { config } from '../config'; diff --git a/src/client/src/lib/api.ts b/src/client/src/lib/api.ts index dcc52dd..ef3f968 100644 --- a/src/client/src/lib/api.ts +++ b/src/client/src/lib/api.ts @@ -1,10 +1,20 @@ import { config } from '../config'; import { localStorage } from './localStorage'; -const api = { getClients }; +const api = { getClients, getClientSummary }; const BASE_URL = `${config.API_URL}/api`; +async function getClients() { + const URL = `${BASE_URL}/clients`; + return performApiCall(URL, 'GET'); +} + +async function getClientSummary(clientId: string) { + const URL = `${BASE_URL}/clients/${clientId}/summary`; + return performApiCall(URL, 'GET'); +} + async function performApiCall( url: string, method: 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE', @@ -45,9 +55,4 @@ async function performApiCall( return response.json(); } -async function getClients() { - const URL = `${BASE_URL}/clients`; - return performApiCall(URL, 'GET'); -} - export { api }; diff --git a/src/client/src/lib/pathHandler.test.ts b/src/client/src/lib/pathHandler.test.ts new file mode 100644 index 0000000..7b487c3 --- /dev/null +++ b/src/client/src/lib/pathHandler.test.ts @@ -0,0 +1,49 @@ +import { pathHandler } from './pathHandler'; + +describe('pathHandler', () => { + describe('getRoutePath', () => { + it('should return the generic route path if no parameters provided', () => { + const path = pathHandler.getRoutePath('CLIENTS'); + + expect(path).toBe('/clients'); + }); + + it('should return the route path with parameter', () => { + const path = pathHandler.getRoutePath('CLIENT_SUMMARY', { + clientId: '219a36c4-a04e-4877-b300-000a27c0830f', + }); + + expect(path).toBe('/clients/219a36c4-a04e-4877-b300-000a27c0830f'); + }); + }); + + describe('extractParameters', () => { + it('should return path with no parameter if path has no parameter', () => { + const path = pathHandler.getRoutePath('CLIENTS'); + + const parsedPath = pathHandler.parsePath(path); + + expect(parsedPath?.routeKey).toEqual('CLIENTS'); + expect(parsedPath?.parameters).toEqual({}); + }); + + it('should return home if path is home', () => { + const path = pathHandler.getRoutePath('HOME'); + + const parsedPath = pathHandler.parsePath(path); + + expect(parsedPath?.routeKey).toEqual('HOME'); + expect(parsedPath?.parameters).toEqual({}); + }); + + it('should return path with parameter if path has one parameter', () => { + const clientId = `${Math.floor(Math.random() * 10000) + 1}`; + const path = pathHandler.getRoutePath('CLIENT_SUMMARY', { clientId }); + + const parsedPath = pathHandler.parsePath(path); + + expect(parsedPath?.routeKey).toEqual('CLIENT_SUMMARY'); + expect(parsedPath?.parameters).toEqual({ clientId }); + }); + }); +}); diff --git a/src/client/src/lib/pathHandler.ts b/src/client/src/lib/pathHandler.ts new file mode 100644 index 0000000..ac54fa4 --- /dev/null +++ b/src/client/src/lib/pathHandler.ts @@ -0,0 +1,65 @@ +import { ROUTE_KEYS } from '../routes/routeKeys'; +import { ROUTE_PATHS } from '../routes/routePaths'; + +const pathHandler = { + getRoutePath, + parsePath, +}; + +function getRoutePath>( + routeKey: (typeof ROUTE_KEYS)[number], + parameters?: paramsT, + queryParameters?: Record, +) { + let path = ROUTE_PATHS[routeKey].path; + if (parameters) { + Object.keys(parameters).forEach((key) => { + path = path.replace(new RegExp(':' + key), parameters[key]); + }); + } + if (queryParameters) { + path = path + '?'; + const queryParameterKeys = Object.keys(queryParameters); + for (let i = 0; i < queryParameterKeys.length; i++) { + const key = queryParameterKeys[i]; + const value = queryParameters[key]; + if (i > 0) { + path += '&'; + } + path += `${key}=${value}`; + } + } + return path; +} + +function parsePath( + path: string, +): { parameters: Record; routeKey: (typeof ROUTE_KEYS)[number] } | undefined { + const splitActualPath = path.split('/'); + + routeKeysLoop: for (const ROUTE_KEY of ROUTE_KEYS) { + const parameters: any = {}; + + const ROUTE_PATH = ROUTE_PATHS[ROUTE_KEY].path; + const splitCanonicalPath = ROUTE_PATH.split('/'); + if (splitActualPath.length !== splitCanonicalPath.length) { + continue; + } + const chunkCount = splitActualPath.length; + for (let i = 0; i < chunkCount; i++) { + const actualPathChunk = splitActualPath[i]; + const canonicalPathChunk = splitCanonicalPath[i]; + if (canonicalPathChunk.length > 0 && canonicalPathChunk[0] === ':') { + const parameterKey = canonicalPathChunk.substring(1); + const parameterValue = actualPathChunk; + parameters[parameterKey] = parameterValue; + } else if (canonicalPathChunk !== actualPathChunk) { + continue routeKeysLoop; + } + } + return { parameters, routeKey: ROUTE_KEY }; + } + return undefined; +} + +export { pathHandler }; diff --git a/src/client/src/pages/ClientSummary.tsx b/src/client/src/pages/ClientSummary.tsx new file mode 100644 index 0000000..03133a4 --- /dev/null +++ b/src/client/src/pages/ClientSummary.tsx @@ -0,0 +1,42 @@ +import { useQuery } from '@tanstack/react-query'; +import { useParams } from 'react-router-dom'; +import { api } from '../lib/api'; + +type statusValueType = 'up' | 'down'; + +// type elementaryStatusType = { timestamp: number; statusValue: statusValueType }; + +type eventType = { timestamp: number; kind: statusValueType; title: string }; + +type clientSummaryType = { + id: string; + name: string; + currentStatusValue: statusValueType; + // uptime: { + // last24Hours: elementaryStatusType[]; + // last90Days: elementaryStatusType[]; + // }; + // overallUptime: { + // last24Hours: number; + // last7Days: number; + // last30Days: number; + // last90Days: number; + // }; + events: eventType[]; +}; + +function ClientSummary() { + const params = useParams<{ clientId: string }>(); + const clientId = params.clientId as string; + const query = useQuery({ + queryFn: () => api.getClientSummary(clientId), + queryKey: ['clients', clientId, 'summary'], + }); + + if (!query.data) { + return
Loading...
; + } + return
; +} + +export { ClientSummary }; diff --git a/src/client/src/pages/Clients.tsx b/src/client/src/pages/Clients.tsx index f9ff539..750272c 100644 --- a/src/client/src/pages/Clients.tsx +++ b/src/client/src/pages/Clients.tsx @@ -1,5 +1,7 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../lib/api'; +import { Link } from 'react-router-dom'; +import { pathHandler } from '../lib/pathHandler'; type clientType = { id: string; @@ -16,7 +18,13 @@ function Clients() {
    {query.data.map((client) => ( -
  • {client.name}
  • +
  • + + {client.name} + +
  • ))}
diff --git a/src/client/src/routes/routeElements.tsx b/src/client/src/routes/routeElements.tsx index c0055c5..1bfcebf 100644 --- a/src/client/src/routes/routeElements.tsx +++ b/src/client/src/routes/routeElements.tsx @@ -1,3 +1,4 @@ +import { ClientSummary } from '../pages/ClientSummary'; import { Clients } from '../pages/Clients'; import { Home } from '../pages/Home'; import { ROUTE_KEYS } from './routeKeys'; @@ -5,6 +6,7 @@ import { ROUTE_KEYS } from './routeKeys'; const ROUTE_ELEMENTS: Record<(typeof ROUTE_KEYS)[number], { element: JSX.Element }> = { HOME: { element: }, CLIENTS: { element: }, + CLIENT_SUMMARY: { element: }, }; export { ROUTE_ELEMENTS }; diff --git a/src/client/src/routes/routeKeys.ts b/src/client/src/routes/routeKeys.ts index 6ae7fa6..b895ce2 100644 --- a/src/client/src/routes/routeKeys.ts +++ b/src/client/src/routes/routeKeys.ts @@ -1,3 +1,3 @@ -const ROUTE_KEYS = ['HOME', 'CLIENTS'] as const; +const ROUTE_KEYS = ['HOME', 'CLIENTS', 'CLIENT_SUMMARY'] as const; export { ROUTE_KEYS }; diff --git a/src/client/src/routes/routePaths.ts b/src/client/src/routes/routePaths.ts index 0ac4df1..f876574 100644 --- a/src/client/src/routes/routePaths.ts +++ b/src/client/src/routes/routePaths.ts @@ -4,6 +4,9 @@ const ROUTE_PATHS: Record<(typeof ROUTE_KEYS)[number], { path: string }> = { HOME: { path: '/', }, + CLIENT_SUMMARY: { + path: '/clients/:clientId', + }, CLIENTS: { path: '/clients', }, diff --git a/src/client/src/routes/routeTitles.ts b/src/client/src/routes/routeTitles.ts index 095952c..41dc84b 100644 --- a/src/client/src/routes/routeTitles.ts +++ b/src/client/src/routes/routeTitles.ts @@ -3,6 +3,7 @@ import { ROUTE_KEYS } from './routeKeys'; const ROUTE_TITLES: Record<(typeof ROUTE_KEYS)[number], string> = { HOME: 'Accueil', CLIENTS: 'Liste des clients', + CLIENT_SUMMARY: 'Résumé', }; export { ROUTE_TITLES }; diff --git a/src/dataSource.ts b/src/dataSource.ts index 99e6130..d516512 100644 --- a/src/dataSource.ts +++ b/src/dataSource.ts @@ -3,6 +3,7 @@ import { config } from './config'; import { Client } from './modules/client'; import { Ping } from './modules/ping'; +import { Event } from './modules/event'; const dataSource = new DataSource({ type: 'postgres', @@ -13,7 +14,7 @@ const dataSource = new DataSource({ database: config.DATABASE_NAME, logging: ['warn', 'error'], connectTimeoutMS: 20000, - entities: [Client, Ping], + entities: [Client, Ping, Event], subscribers: [], migrations: ['**/migrations/*.js'], }); diff --git a/src/migrations/1729418677813-add-event.ts b/src/migrations/1729418677813-add-event.ts new file mode 100644 index 0000000..dcd68a8 --- /dev/null +++ b/src/migrations/1729418677813-add-event.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddEvent1729418677813 implements MigrationInterface { + name = 'AddEvent1729418677813' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TYPE "public"."event_kind_enum" AS ENUM('up', 'down')`); + await queryRunner.query(`CREATE TABLE "event" ("id" SERIAL NOT NULL, "title" character varying NOT NULL, "kind" "public"."event_kind_enum" NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "clientId" uuid NOT NULL, CONSTRAINT "PK_30c2f3bbaf6d34a55f8ae6e4614" PRIMARY KEY ("id"))`); + await queryRunner.query(`ALTER TABLE "ping" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "ping" ADD "createdAt" TIMESTAMP NOT NULL DEFAULT now()`); + await queryRunner.query(`ALTER TABLE "event" ADD CONSTRAINT "FK_332ae914d279c823c7ae4197d5d" FOREIGN KEY ("clientId") REFERENCES "client"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "event" DROP CONSTRAINT "FK_332ae914d279c823c7ae4197d5d"`); + await queryRunner.query(`ALTER TABLE "ping" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "ping" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + await queryRunner.query(`DROP TABLE "event"`); + await queryRunner.query(`DROP TYPE "public"."event_kind_enum"`); + } + +} diff --git a/src/modules/client/client.controller.ts b/src/modules/client/client.controller.ts index 822552e..3dabc28 100644 --- a/src/modules/client/client.controller.ts +++ b/src/modules/client/client.controller.ts @@ -9,6 +9,8 @@ function buildClientController() { createClient, assertIsClientUp, getAllClients, + getClients, + getClientSummary, }; return clientController; @@ -24,4 +26,12 @@ function buildClientController() { async function getAllClients() { return clientService.getAllClients(); } + + async function getClients() { + return clientService.getAllClients(); + } + + async function getClientSummary(params: { urlParams: { clientId: Client['id'] } }) { + return clientService.getClientSummary(params.urlParams.clientId); + } } diff --git a/src/modules/client/client.service.ts b/src/modules/client/client.service.ts index dece91b..1d52272 100644 --- a/src/modules/client/client.service.ts +++ b/src/modules/client/client.service.ts @@ -1,4 +1,5 @@ import { dataSource } from '../../dataSource'; +import { buildEventService } from '../event'; import { buildPingService } from '../ping'; import { Client } from './Client.entity'; @@ -7,10 +8,13 @@ export { buildClientService }; function buildClientService() { const clientRepository = dataSource.getRepository(Client); const pingService = buildPingService(); + const eventService = buildEventService(); const clientService = { createClient, assertIsClientUp, getAllClients, + getClientSummary, + getClientCurrentStatus, }; return clientService; @@ -19,6 +23,10 @@ function buildClientService() { return clientRepository.find({}); } + async function getClientCurrentStatus(clientId: Client['id']) { + const lastEvent = await eventService.getLastEvent(clientId); + } + async function createClient(name: Client['name']) { const result = await clientRepository.insert({ name }); return { clientId: result.identifiers[0].id }; @@ -29,4 +37,6 @@ function buildClientService() { await pingService.assertHasClientBeenPingedRecently(client.id); return { ok: true }; } + + async function getClientSummary(clientId: Client['id']) {} } diff --git a/src/modules/event/Event.entity.ts b/src/modules/event/Event.entity.ts new file mode 100644 index 0000000..79ce567 --- /dev/null +++ b/src/modules/event/Event.entity.ts @@ -0,0 +1,21 @@ +import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; +import { Client } from '../client'; +import { EVENT_KINDS, eventKindType } from './types'; + +@Entity() +export class Event { + @PrimaryGeneratedColumn() + id: number; + + @Column() + title: string; + + @Column('enum', { enum: EVENT_KINDS }) + kind: eventKindType; + + @ManyToOne(() => Client, { onDelete: 'CASCADE', nullable: false }) + client: Client; + + @CreateDateColumn({ type: 'timestamp' }) + createdAt: string; +} diff --git a/src/modules/event/event.service.ts b/src/modules/event/event.service.ts new file mode 100644 index 0000000..4507b57 --- /dev/null +++ b/src/modules/event/event.service.ts @@ -0,0 +1,45 @@ +import { dataSource } from '../../dataSource'; +import { Client } from '../client'; +import { Event } from './Event.entity'; + +export { buildEventService }; + +function buildEventService() { + const eventRepository = dataSource.getRepository(Event); + + const eventService = { + createEvent, + getLastEvent, + getEvents, + }; + + return eventService; + + async function getLastEvent(clientId: Client['id']) { + return eventRepository.findOne({ + where: { client: { id: clientId } }, + order: { createdAt: 'DESC' }, + relations: ['client'], + }); + } + + async function getEvents(clientId: Client['id']) { + return eventRepository.find({ + where: { client: { id: clientId } }, + order: { createdAt: 'DESC' }, + relations: ['client'], + }); + } + + async function createEvent( + clientId: Client['id'], + params: { title: Event['title']; kind: Event['kind'] }, + ) { + await eventRepository.insert({ + client: { id: clientId }, + title: params.title, + kind: params.kind, + }); + return true; + } +} diff --git a/src/modules/event/index.ts b/src/modules/event/index.ts new file mode 100644 index 0000000..dd660b0 --- /dev/null +++ b/src/modules/event/index.ts @@ -0,0 +1,4 @@ +import { Event } from './Event.entity'; +import { buildEventService } from './event.service'; + +export { Event, buildEventService }; diff --git a/src/modules/event/types.ts b/src/modules/event/types.ts new file mode 100644 index 0000000..2ae4f72 --- /dev/null +++ b/src/modules/event/types.ts @@ -0,0 +1,6 @@ +const EVENT_KINDS = ['up', 'down'] as const; + +type eventKindType = (typeof EVENT_KINDS)[number]; + +export { EVENT_KINDS }; +export type { eventKindType }; diff --git a/src/modules/ping/ping.service.ts b/src/modules/ping/ping.service.ts index 2414cc3..1f94ed8 100644 --- a/src/modules/ping/ping.service.ts +++ b/src/modules/ping/ping.service.ts @@ -2,6 +2,7 @@ import { MoreThan } from 'typeorm'; import { dataSource } from '../../dataSource'; import { Client } from '../client'; import { Ping } from './Ping.entity'; +import { buildEventService } from '../event'; export { buildPingService }; @@ -9,6 +10,7 @@ const MAX_DELAY_SINCE_LAST_PING = 120 * 1000; function buildPingService() { const pingRepository = dataSource.getRepository(Ping); + const eventService = buildEventService(); const pingService = { createPing, @@ -23,7 +25,21 @@ function buildPingService() { } async function createPing(clientId: Client['id']) { + await pingRepository.delete({ client: { id: clientId } }); await pingRepository.insert({ client: { id: clientId } }); + + const lastEvent = await eventService.getLastEvent(clientId); + if (!lastEvent) { + await eventService.createEvent(clientId, { title: 'Service en route !', kind: 'up' }); + } else { + if (lastEvent.kind === 'down') { + await eventService.createEvent(clientId, { + title: 'Le service est revenu', + kind: 'up', + }); + } + } + return true; } diff --git a/src/router/clientRoutes.ts b/src/router/clientRoutes.ts index eb58f5f..d1f1f8b 100644 --- a/src/router/clientRoutes.ts +++ b/src/router/clientRoutes.ts @@ -24,6 +24,16 @@ const clientRoutes: Array> = [ { method: 'GET', path: '/clients', + controller: clientController.getClients, + }, + { + method: 'GET', + path: '/clients/:clientId/summary', + controller: clientController.getClientSummary, + }, + { + method: 'GET', + path: '/all-clients', controller: clientController.getAllClients, }, ]; From 134f60fbfd6330f6073ba7d8b900111a69630262 Mon Sep 17 00:00:00 2001 From: Benoit Serrano Date: Sun, 20 Oct 2024 11:58:17 +0100 Subject: [PATCH 4/7] add frequency and grace period --- ...29420898909-add-frequency-and-grace-period.ts | 16 ++++++++++++++++ src/modules/client/Client.entity.ts | 6 ++++++ src/modules/client/client.service.ts | 4 ++-- src/modules/ping/ping.service.ts | 7 ++++--- 4 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 src/migrations/1729420898909-add-frequency-and-grace-period.ts diff --git a/src/migrations/1729420898909-add-frequency-and-grace-period.ts b/src/migrations/1729420898909-add-frequency-and-grace-period.ts new file mode 100644 index 0000000..cc545a9 --- /dev/null +++ b/src/migrations/1729420898909-add-frequency-and-grace-period.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddFrequencyAndGracePeriod1729420898909 implements MigrationInterface { + name = 'AddFrequencyAndGracePeriod1729420898909' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "client" ADD "frequency" integer NOT NULL DEFAULT '1'`); + await queryRunner.query(`ALTER TABLE "client" ADD "gracePeriod" integer NOT NULL DEFAULT '1'`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "client" DROP COLUMN "gracePeriod"`); + await queryRunner.query(`ALTER TABLE "client" DROP COLUMN "frequency"`); + } + +} diff --git a/src/modules/client/Client.entity.ts b/src/modules/client/Client.entity.ts index 5ec0195..2bfe04d 100644 --- a/src/modules/client/Client.entity.ts +++ b/src/modules/client/Client.entity.ts @@ -7,4 +7,10 @@ export class Client { @Column({ unique: true }) name: string; + + @Column({ default: 1 }) + frequency: number; + + @Column({ default: 1 }) + gracePeriod: number; } diff --git a/src/modules/client/client.service.ts b/src/modules/client/client.service.ts index 1d52272..12e84c6 100644 --- a/src/modules/client/client.service.ts +++ b/src/modules/client/client.service.ts @@ -33,8 +33,8 @@ function buildClientService() { } async function assertIsClientUp(name: Client['name']) { - const client = await clientRepository.findOneByOrFail({ name }); - await pingService.assertHasClientBeenPingedRecently(client.id); + const client = await clientRepository.findOneOrFail({ where: { name } }); + await pingService.assertHasClientBeenPingedRecently(client); return { ok: true }; } diff --git a/src/modules/ping/ping.service.ts b/src/modules/ping/ping.service.ts index 1f94ed8..e8998f9 100644 --- a/src/modules/ping/ping.service.ts +++ b/src/modules/ping/ping.service.ts @@ -43,19 +43,20 @@ function buildPingService() { return true; } - async function assertHasClientBeenPingedRecently(clientId: Client['id']) { + async function assertHasClientBeenPingedRecently(client: Client) { const now = new Date(); + const MAX_DELAY_SINCE_LAST_PING = (client.frequency * 60 + client.gracePeriod * 60) * 1000; const lastPingThresholdDate = new Date(now.getTime() - MAX_DELAY_SINCE_LAST_PING); const pings = await pingRepository.find({ relations: ['client'], where: { - client: { id: clientId }, + client: { id: client.id }, createdAt: MoreThan(lastPingThresholdDate.toISOString()), }, }); if (pings.length === 0) { throw new Error( - `No ping found ${MAX_DELAY_SINCE_LAST_PING} ms ago for clientId ${clientId}`, + `No ping found ${MAX_DELAY_SINCE_LAST_PING} ms ago for client "${client.name}"`, ); } } From 2759cb8ee80d1aa1924fd8331c25ce1429b48398 Mon Sep 17 00:00:00 2001 From: Benoit Serrano Date: Sun, 20 Oct 2024 12:05:58 +0100 Subject: [PATCH 5/7] move cors to actual dependencies --- package-lock.json | 3 +-- package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index bb377f2..caea7de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { + "@types/cors": "^2.8.17", "@types/express": "^4.17.13", "@types/node": "^20.3.1", "body-parser": "^1.20.2", @@ -25,7 +26,6 @@ }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@types/cors": "^2.8.17", "nodemon": "^3.1.4", "ts-node": "^10.9.1", "typescript": "^4.6.4" @@ -606,7 +606,6 @@ "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, "dependencies": { "@types/node": "*" } diff --git a/package.json b/package.json index f142961..8a2aed9 100644 --- a/package.json +++ b/package.json @@ -18,12 +18,12 @@ "license": "ISC", "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", - "@types/cors": "^2.8.17", "nodemon": "^3.1.4", "ts-node": "^10.9.1", "typescript": "^4.6.4" }, "dependencies": { + "@types/cors": "^2.8.17", "@types/express": "^4.17.13", "@types/node": "^20.3.1", "body-parser": "^1.20.2", From 4717f00d98e6ceece8d4a41afe7489bc586b1d96 Mon Sep 17 00:00:00 2001 From: Benoit Serrano Date: Sun, 20 Oct 2024 12:50:34 +0100 Subject: [PATCH 6/7] use last pinged at instead of pings --- .../1729424213523-add-last-pinged-at.ts | 13 +++++ src/modules/client/Client.entity.ts | 3 ++ src/modules/client/client.controller.ts | 5 ++ src/modules/client/client.service.ts | 48 ++++++++++++++++--- src/router/clientRoutes.ts | 5 ++ src/router/pingRoutes.ts | 10 ++-- 6 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 src/migrations/1729424213523-add-last-pinged-at.ts diff --git a/src/migrations/1729424213523-add-last-pinged-at.ts b/src/migrations/1729424213523-add-last-pinged-at.ts new file mode 100644 index 0000000..37b2ab8 --- /dev/null +++ b/src/migrations/1729424213523-add-last-pinged-at.ts @@ -0,0 +1,13 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddLastPingedAt1729424213523 implements MigrationInterface { + name = 'AddLastPingedAt1729424213523'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "client" ADD "lastPingedAt" TIMESTAMP`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "client" DROP COLUMN "lastPingedAt"`); + } +} diff --git a/src/modules/client/Client.entity.ts b/src/modules/client/Client.entity.ts index 2bfe04d..42914e5 100644 --- a/src/modules/client/Client.entity.ts +++ b/src/modules/client/Client.entity.ts @@ -13,4 +13,7 @@ export class Client { @Column({ default: 1 }) gracePeriod: number; + + @Column({ type: 'timestamp', nullable: true }) + lastPingedAt: string | null; } diff --git a/src/modules/client/client.controller.ts b/src/modules/client/client.controller.ts index 3dabc28..c542e51 100644 --- a/src/modules/client/client.controller.ts +++ b/src/modules/client/client.controller.ts @@ -11,6 +11,7 @@ function buildClientController() { getAllClients, getClients, getClientSummary, + pingClient, }; return clientController; @@ -31,6 +32,10 @@ function buildClientController() { return clientService.getAllClients(); } + async function pingClient(params: { urlParams: { clientId: Client['id'] } }) { + return clientService.pingClient(params.urlParams.clientId); + } + async function getClientSummary(params: { urlParams: { clientId: Client['id'] } }) { return clientService.getClientSummary(params.urlParams.clientId); } diff --git a/src/modules/client/client.service.ts b/src/modules/client/client.service.ts index 12e84c6..d54225f 100644 --- a/src/modules/client/client.service.ts +++ b/src/modules/client/client.service.ts @@ -14,7 +14,7 @@ function buildClientService() { assertIsClientUp, getAllClients, getClientSummary, - getClientCurrentStatus, + pingClient, }; return clientService; @@ -23,18 +23,54 @@ function buildClientService() { return clientRepository.find({}); } - async function getClientCurrentStatus(clientId: Client['id']) { - const lastEvent = await eventService.getLastEvent(clientId); - } - async function createClient(name: Client['name']) { const result = await clientRepository.insert({ name }); return { clientId: result.identifiers[0].id }; } + async function pingClient(clientId: Client['id']) { + const now = new Date(); + const result = await clientRepository.update( + { id: clientId }, + { lastPingedAt: now.toISOString() }, + ); + if (result.affected !== 1) { + throw new Error(`client id ${clientId} does not exist`); + } + + const lastEvent = await eventService.getLastEvent(clientId); + if (!lastEvent) { + await eventService.createEvent(clientId, { title: 'Service en route !', kind: 'up' }); + } else { + if (lastEvent.kind === 'down') { + await eventService.createEvent(clientId, { + title: 'Le service est revenu', + kind: 'up', + }); + } + } + + return true; + } + async function assertIsClientUp(name: Client['name']) { const client = await clientRepository.findOneOrFail({ where: { name } }); - await pingService.assertHasClientBeenPingedRecently(client); + if (!client.lastPingedAt) { + throw new Error(`Client "${client.name}" has never been pinged`); + } + const now = new Date(); + const MAX_DELAY_SINCE_LAST_PING = (client.frequency * 60 + client.gracePeriod * 60) * 1000; + const lastPingThresholdDate = new Date(now.getTime() - MAX_DELAY_SINCE_LAST_PING); + const lastPingedAt = new Date(client.lastPingedAt); + + if (lastPingedAt.getTime() < lastPingThresholdDate.getTime()) { + throw new Error( + `Last ping found for client "${ + client.name + }" was too long ago: ${lastPingedAt.toLocaleString()}`, + ); + } + return { ok: true }; } diff --git a/src/router/clientRoutes.ts b/src/router/clientRoutes.ts index d1f1f8b..78d0f16 100644 --- a/src/router/clientRoutes.ts +++ b/src/router/clientRoutes.ts @@ -36,6 +36,11 @@ const clientRoutes: Array> = [ path: '/all-clients', controller: clientController.getAllClients, }, + { + method: 'POST', + path: '/clients/:clientId/pings', + controller: clientController.pingClient, + }, ]; export { clientRoutes }; diff --git a/src/router/pingRoutes.ts b/src/router/pingRoutes.ts index 03dbc3a..50ba4cf 100644 --- a/src/router/pingRoutes.ts +++ b/src/router/pingRoutes.ts @@ -4,11 +4,11 @@ import { routeType } from './types'; const pingController = buildPingController(); const pingRoutes: Array> = [ - { - method: 'POST', - path: '/clients/:clientId/pings', - controller: pingController.createPing, - }, + // { + // method: 'POST', + // path: '/clients/:clientId/pings', + // controller: pingController.createPing, + // }, { method: 'GET', path: '/pings', From 3e5ea82baa04d8312623986860dba84f404e97d8 Mon Sep 17 00:00:00 2001 From: Benoit Serrano Date: Sun, 20 Oct 2024 13:05:50 +0100 Subject: [PATCH 7/7] delete pings and add event route for importDb script --- src/dataSource.ts | 3 +- src/lib/api.ts | 10 ++-- src/migrations/1729425458873-delete-pings.ts | 19 ++++++ src/modules/client/client.service.ts | 2 - src/modules/event/event.controller.ts | 16 +++++ src/modules/event/event.service.ts | 7 +++ src/modules/event/index.ts | 4 +- src/modules/ping/Ping.entity.ts | 14 ----- src/modules/ping/index.ts | 5 -- src/modules/ping/ping.controller.ts | 21 ------- src/modules/ping/ping.service.ts | 63 -------------------- src/router/eventRoutes.ts | 15 +++++ src/router/pingRoutes.ts | 19 ------ src/router/routes.ts | 4 +- src/scripts/importDb.ts | 16 ++--- 15 files changed, 75 insertions(+), 143 deletions(-) create mode 100644 src/migrations/1729425458873-delete-pings.ts create mode 100644 src/modules/event/event.controller.ts delete mode 100644 src/modules/ping/Ping.entity.ts delete mode 100644 src/modules/ping/index.ts delete mode 100644 src/modules/ping/ping.controller.ts delete mode 100644 src/modules/ping/ping.service.ts create mode 100644 src/router/eventRoutes.ts delete mode 100644 src/router/pingRoutes.ts diff --git a/src/dataSource.ts b/src/dataSource.ts index d516512..be93dd6 100644 --- a/src/dataSource.ts +++ b/src/dataSource.ts @@ -2,7 +2,6 @@ import { DataSource } from 'typeorm'; import { config } from './config'; import { Client } from './modules/client'; -import { Ping } from './modules/ping'; import { Event } from './modules/event'; const dataSource = new DataSource({ @@ -14,7 +13,7 @@ const dataSource = new DataSource({ database: config.DATABASE_NAME, logging: ['warn', 'error'], connectTimeoutMS: 20000, - entities: [Client, Ping, Event], + entities: [Client, Event], subscribers: [], migrations: ['**/migrations/*.js'], }); diff --git a/src/lib/api.ts b/src/lib/api.ts index 2f3f962..675e696 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -1,22 +1,22 @@ import { Client } from '../modules/client'; -import { Ping } from '../modules/ping'; +import { Event } from '../modules/event'; const api = { fetchAllClients, - fetchAllPings, + fetchAllEvents, }; const BASE_URL = 'https://ping-storage.osc-fr1.scalingo.io'; async function fetchAllClients(): Promise { - const URL = `${BASE_URL}/api/clients`; + const URL = `${BASE_URL}/api/all-clients`; const response = await fetch(URL); const parsedData = await response.json(); return parsedData; } -async function fetchAllPings(): Promise { - const URL = `${BASE_URL}/api/pings`; +async function fetchAllEvents(): Promise { + const URL = `${BASE_URL}/api/all-events`; const response = await fetch(URL); const parsedData = await response.json(); diff --git a/src/migrations/1729425458873-delete-pings.ts b/src/migrations/1729425458873-delete-pings.ts new file mode 100644 index 0000000..5b63243 --- /dev/null +++ b/src/migrations/1729425458873-delete-pings.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class DeletePings1729425458873 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "ping" DROP CONSTRAINT "FK_eaee4e2004faa8d8af53024ed8f"`, + ); + await queryRunner.query(`DROP TABLE "ping"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "ping" ("id" SERIAL NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "clientId" uuid NOT NULL, CONSTRAINT "PK_b01cab9d614b77bac5973937663" PRIMARY KEY ("id"))`, + ); + await queryRunner.query( + `ALTER TABLE "ping" ADD CONSTRAINT "FK_eaee4e2004faa8d8af53024ed8f" FOREIGN KEY ("clientId") REFERENCES "client"("id") ON DELETE CASCADE ON UPDATE NO ACTION`, + ); + } +} diff --git a/src/modules/client/client.service.ts b/src/modules/client/client.service.ts index d54225f..f9e69b2 100644 --- a/src/modules/client/client.service.ts +++ b/src/modules/client/client.service.ts @@ -1,13 +1,11 @@ import { dataSource } from '../../dataSource'; import { buildEventService } from '../event'; -import { buildPingService } from '../ping'; import { Client } from './Client.entity'; export { buildClientService }; function buildClientService() { const clientRepository = dataSource.getRepository(Client); - const pingService = buildPingService(); const eventService = buildEventService(); const clientService = { createClient, diff --git a/src/modules/event/event.controller.ts b/src/modules/event/event.controller.ts new file mode 100644 index 0000000..d556e03 --- /dev/null +++ b/src/modules/event/event.controller.ts @@ -0,0 +1,16 @@ +import { buildEventService } from './event.service'; + +export { buildEventController }; + +function buildEventController() { + const eventService = buildEventService(); + const eventController = { + getAllEvents, + }; + + return eventController; + + async function getAllEvents() { + return eventService.getAllEvents(); + } +} diff --git a/src/modules/event/event.service.ts b/src/modules/event/event.service.ts index 4507b57..0c8a79e 100644 --- a/src/modules/event/event.service.ts +++ b/src/modules/event/event.service.ts @@ -11,6 +11,7 @@ function buildEventService() { createEvent, getLastEvent, getEvents, + getAllEvents, }; return eventService; @@ -31,6 +32,12 @@ function buildEventService() { }); } + async function getAllEvents() { + return eventRepository.find({ + relations: ['client'], + }); + } + async function createEvent( clientId: Client['id'], params: { title: Event['title']; kind: Event['kind'] }, diff --git a/src/modules/event/index.ts b/src/modules/event/index.ts index dd660b0..5d2ec9e 100644 --- a/src/modules/event/index.ts +++ b/src/modules/event/index.ts @@ -1,4 +1,4 @@ import { Event } from './Event.entity'; import { buildEventService } from './event.service'; - -export { Event, buildEventService }; +import { buildEventController } from './event.controller'; +export { Event, buildEventService, buildEventController }; diff --git a/src/modules/ping/Ping.entity.ts b/src/modules/ping/Ping.entity.ts deleted file mode 100644 index 89bc118..0000000 --- a/src/modules/ping/Ping.entity.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn } from 'typeorm'; -import { Client } from '../client'; - -@Entity() -export class Ping { - @PrimaryGeneratedColumn() - id: number; - - @ManyToOne(() => Client, { onDelete: 'CASCADE', nullable: false }) - client: Client; - - @CreateDateColumn({ type: 'timestamp' }) - createdAt: string; -} diff --git a/src/modules/ping/index.ts b/src/modules/ping/index.ts deleted file mode 100644 index 94c272d..0000000 --- a/src/modules/ping/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Ping } from './Ping.entity'; -import { buildPingController } from './ping.controller'; -import { buildPingService } from './ping.service'; - -export { Ping, buildPingController, buildPingService }; diff --git a/src/modules/ping/ping.controller.ts b/src/modules/ping/ping.controller.ts deleted file mode 100644 index af9fe64..0000000 --- a/src/modules/ping/ping.controller.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { buildPingService } from './ping.service'; - -export { buildPingController }; - -function buildPingController() { - const pingService = buildPingService(); - const pingController = { - createPing, - getAllPings, - }; - - return pingController; - - async function createPing(params: { urlParams: { clientId: string } }) { - return pingService.createPing(params.urlParams.clientId); - } - - async function getAllPings() { - return pingService.getAllPings(); - } -} diff --git a/src/modules/ping/ping.service.ts b/src/modules/ping/ping.service.ts deleted file mode 100644 index e8998f9..0000000 --- a/src/modules/ping/ping.service.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { MoreThan } from 'typeorm'; -import { dataSource } from '../../dataSource'; -import { Client } from '../client'; -import { Ping } from './Ping.entity'; -import { buildEventService } from '../event'; - -export { buildPingService }; - -const MAX_DELAY_SINCE_LAST_PING = 120 * 1000; - -function buildPingService() { - const pingRepository = dataSource.getRepository(Ping); - const eventService = buildEventService(); - - const pingService = { - createPing, - getAllPings, - assertHasClientBeenPingedRecently, - }; - - return pingService; - - async function getAllPings() { - return pingRepository.find({ relations: ['client'] }); - } - - async function createPing(clientId: Client['id']) { - await pingRepository.delete({ client: { id: clientId } }); - await pingRepository.insert({ client: { id: clientId } }); - - const lastEvent = await eventService.getLastEvent(clientId); - if (!lastEvent) { - await eventService.createEvent(clientId, { title: 'Service en route !', kind: 'up' }); - } else { - if (lastEvent.kind === 'down') { - await eventService.createEvent(clientId, { - title: 'Le service est revenu', - kind: 'up', - }); - } - } - - return true; - } - - async function assertHasClientBeenPingedRecently(client: Client) { - const now = new Date(); - const MAX_DELAY_SINCE_LAST_PING = (client.frequency * 60 + client.gracePeriod * 60) * 1000; - const lastPingThresholdDate = new Date(now.getTime() - MAX_DELAY_SINCE_LAST_PING); - const pings = await pingRepository.find({ - relations: ['client'], - where: { - client: { id: client.id }, - createdAt: MoreThan(lastPingThresholdDate.toISOString()), - }, - }); - if (pings.length === 0) { - throw new Error( - `No ping found ${MAX_DELAY_SINCE_LAST_PING} ms ago for client "${client.name}"`, - ); - } - } -} diff --git a/src/router/eventRoutes.ts b/src/router/eventRoutes.ts new file mode 100644 index 0000000..c63ad7a --- /dev/null +++ b/src/router/eventRoutes.ts @@ -0,0 +1,15 @@ +import Joi from 'joi'; +import { buildEventController } from '../modules/event'; +import { routeType } from './types'; + +const eventController = buildEventController(); + +const eventRoutes: Array> = [ + { + method: 'GET', + path: '/all-events', + controller: eventController.getAllEvents, + }, +]; + +export { eventRoutes }; diff --git a/src/router/pingRoutes.ts b/src/router/pingRoutes.ts deleted file mode 100644 index 50ba4cf..0000000 --- a/src/router/pingRoutes.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { buildPingController } from '../modules/ping'; -import { routeType } from './types'; - -const pingController = buildPingController(); - -const pingRoutes: Array> = [ - // { - // method: 'POST', - // path: '/clients/:clientId/pings', - // controller: pingController.createPing, - // }, - { - method: 'GET', - path: '/pings', - controller: pingController.getAllPings, - }, -]; - -export { pingRoutes }; diff --git a/src/router/routes.ts b/src/router/routes.ts index eccc3fc..a40c320 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -1,5 +1,5 @@ import { clientRoutes } from './clientRoutes'; -import { pingRoutes } from './pingRoutes'; +import { eventRoutes } from './eventRoutes'; import { routeType } from './types'; const routes = buildRoutes(); @@ -7,7 +7,7 @@ const routes = buildRoutes(); function buildRoutes() { const routes: routeType[] = []; routes.push(...clientRoutes); - routes.push(...pingRoutes); + routes.push(...eventRoutes); return routes; } diff --git a/src/scripts/importDb.ts b/src/scripts/importDb.ts index 3fd4310..7f31e32 100644 --- a/src/scripts/importDb.ts +++ b/src/scripts/importDb.ts @@ -1,33 +1,33 @@ import { dataSource } from '../dataSource'; import { api } from '../lib/api'; import { Client } from '../modules/client'; -import { Ping } from '../modules/ping'; +import { Event } from '../modules/event'; async function importDb() { console.log('Initializing database...'); await dataSource.initialize(); console.log('Database initialized!'); const clientRepository = dataSource.getRepository(Client); - const pingRepository = dataSource.getRepository(Ping); + const eventRepository = dataSource.getRepository(Event); console.log('Erasing local database...'); await clientRepository.delete({}); - await pingRepository.delete({}); + await eventRepository.delete({}); console.log('Fetching clients...'); const allClients = await api.fetchAllClients(); console.log(`${allClients.length} clients fetched! Inserting them in database...`); await clientRepository.insert(allClients); - console.log('Clients inserted! Now fetching pings...'); + console.log('Clients inserted! Now fetching events...'); - const allPings = await api.fetchAllPings(); + const allEvents = await api.fetchAllEvents(); - console.log(`${allPings.length} pings fetched! Inserting them in database...`); + console.log(`${allEvents.length} events fetched! Inserting them in database...`); - await pingRepository.insert(allPings); - console.log(`${allPings.length} pings inserted!`); + await eventRepository.insert(allEvents); + console.log(`${allEvents.length} events inserted!`); console.log('Done!'); }