Skip to content

Commit

Permalink
Merge pull request #2 from numerique-gouv/add-event-system
Browse files Browse the repository at this point in the history
Add event system
  • Loading branch information
BenoitSerrano authored Oct 20, 2024
2 parents 33c3a2a + 3e5ea82 commit e9ada82
Show file tree
Hide file tree
Showing 36 changed files with 581 additions and 120 deletions.
22 changes: 22 additions & 0 deletions package-lock.json

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

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -22,9 +23,11 @@
"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",
"cors": "^2.8.5",
"dotenv": "^16.0.3",
"express": "^4.18.1",
"http-status": "^1.6.2",
Expand Down
6 changes: 3 additions & 3 deletions src/app/app.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Express, { Response } from 'express';
import path from 'path';
import Express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import { config } from '../config';
import { dataSource } from '../dataSource';
Expand All @@ -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}`);
Expand Down
12 changes: 11 additions & 1 deletion src/client/src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import { config } from '../config';
import { localStorage } from './localStorage';

const api = {};
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',
Expand Down
49 changes: 49 additions & 0 deletions src/client/src/lib/pathHandler.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
});
});
65 changes: 65 additions & 0 deletions src/client/src/lib/pathHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ROUTE_KEYS } from '../routes/routeKeys';
import { ROUTE_PATHS } from '../routes/routePaths';

const pathHandler = {
getRoutePath,
parsePath,
};

function getRoutePath<paramsT extends Record<string, string>>(
routeKey: (typeof ROUTE_KEYS)[number],
parameters?: paramsT,
queryParameters?: Record<string, string>,
) {
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<string, string>; 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 };
42 changes: 42 additions & 0 deletions src/client/src/pages/ClientSummary.tsx
Original file line number Diff line number Diff line change
@@ -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<clientSummaryType>({
queryFn: () => api.getClientSummary(clientId),
queryKey: ['clients', clientId, 'summary'],
});

if (!query.data) {
return <div>Loading...</div>;
}
return <div></div>;
}

export { ClientSummary };
34 changes: 34 additions & 0 deletions src/client/src/pages/Clients.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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;
name: string;
};

function Clients() {
const query = useQuery<clientType[]>({ queryFn: api.getClients, queryKey: ['clients'] });

if (!query.data) {
return <div>Loading...</div>;
}
return (
<div>
<ul>
{query.data.map((client) => (
<li key={client.id}>
<Link
to={pathHandler.getRoutePath('CLIENT_SUMMARY', { clientId: client.id })}
>
{client.name}
</Link>
</li>
))}
</ul>
</div>
);
}

export { Clients };
4 changes: 4 additions & 0 deletions src/client/src/routes/routeElements.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { ClientSummary } from '../pages/ClientSummary';
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: <Home /> },
CLIENTS: { element: <Clients /> },
CLIENT_SUMMARY: { element: <ClientSummary /> },
};

export { ROUTE_ELEMENTS };
2 changes: 1 addition & 1 deletion src/client/src/routes/routeKeys.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
const ROUTE_KEYS = ['HOME'] as const;
const ROUTE_KEYS = ['HOME', 'CLIENTS', 'CLIENT_SUMMARY'] as const;

export { ROUTE_KEYS };
8 changes: 7 additions & 1 deletion src/client/src/routes/routePaths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import { ROUTE_KEYS } from './routeKeys';

const ROUTE_PATHS: Record<(typeof ROUTE_KEYS)[number], { path: string }> = {
HOME: {
path: '/*',
path: '/',
},
CLIENT_SUMMARY: {
path: '/clients/:clientId',
},
CLIENTS: {
path: '/clients',
},
};

Expand Down
2 changes: 2 additions & 0 deletions src/client/src/routes/routeTitles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ 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 };
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 || '',
Expand Down
4 changes: 2 additions & 2 deletions src/dataSource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ 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({
type: 'postgres',
Expand All @@ -13,7 +13,7 @@ const dataSource = new DataSource({
database: config.DATABASE_NAME,
logging: ['warn', 'error'],
connectTimeoutMS: 20000,
entities: [Client, Ping],
entities: [Client, Event],
subscribers: [],
migrations: ['**/migrations/*.js'],
});
Expand Down
26 changes: 26 additions & 0 deletions src/lib/api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Client } from '../modules/client';
import { Event } from '../modules/event';

const api = {
fetchAllClients,
fetchAllEvents,
};
const BASE_URL = 'https://ping-storage.osc-fr1.scalingo.io';

async function fetchAllClients(): Promise<Client[]> {
const URL = `${BASE_URL}/api/all-clients`;

const response = await fetch(URL);
const parsedData = await response.json();
return parsedData;
}

async function fetchAllEvents(): Promise<Event[]> {
const URL = `${BASE_URL}/api/all-events`;

const response = await fetch(URL);
const parsedData = await response.json();
return parsedData;
}

export { api };
Loading

0 comments on commit e9ada82

Please sign in to comment.