Skip to content

Commit

Permalink
restructure files for tests; test getLatestAvailableReports()
Browse files Browse the repository at this point in the history
  • Loading branch information
Maxime Chaillet committed Jan 31, 2025
1 parent 6ae7b69 commit ddd4a08
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 110 deletions.
2 changes: 1 addition & 1 deletion alerting/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
"migration:revert": "yarn typeorm migration:revert",
"typeorm-seeding": "ts-node $(yarn bin typeorm-seeding)",
"alert-worker": "yarn ts-node src/alert-worker.ts",
"aa-storm-alert-worker": "yarn ts-node src/aa-storm-alert-worker.ts",
"aa-storm-alert-worker": "yarn ts-node src/aa-storm-alert-runner.ts",
"docker-alert": "source ../api/set_envs.sh && docker compose run --entrypoint 'yarn alert-worker' alerting-node"
},
"dependencies": {
Expand Down
3 changes: 3 additions & 0 deletions alerting/src/aa-storm-alert-runner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { run } from './aa-storm-alert/worker';

run();
93 changes: 93 additions & 0 deletions alerting/src/aa-storm-alert/alert.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
jest.mock('node-fetch');
import nodeFetch from 'node-fetch';
import { getLatestAvailableReports } from './alert';

describe('alert mechanism', () => {
describe('getLatestAvailableReports()', () => {
const mockedFetch = nodeFetch as unknown as jest.Mock;
afterEach(() => {
jest.resetAllMocks();
});

const tests = [
{
data: {
'2025-01-31': {
elvis: [
{
ref_time: '2025-01-31T06:00:00Z',
state: 'monitoring',
path: 'elvis/2025-01-31T06:00:00Z.json',
},
],
},
},
expected: [
{
ref_time: '2025-01-31T06:00:00Z',
state: 'monitoring',
path: 'elvis/2025-01-31T06:00:00Z.json',
},
],
},
{
data: {
'2025-01-30': {
'07-20242025': [
{
ref_time: '2025-01-30T06:00:00Z',
state: 'monitoring',
path: '07-20242025/2025-01-30T06:00:00Z.json',
},
{
ref_time: '2025-01-30T12:00:00Z',
state: 'monitoring',
path: '07-20242025/2025-01-30T12:00:00Z.json',
},
],
elvis: [
{
ref_time: '2025-01-30T06:00:00Z',
state: 'monitoring',
path: 'elvis/2025-01-30T06:00:00Z.json',
},
{
ref_time: '2025-01-30T18:00:00Z',
state: 'monitoring',
path: 'elvis/2025-01-30T18:00:00Z.json',
},
],
},
},
expected: [
{
ref_time: '2025-01-30T12:00:00Z',
state: 'monitoring',
path: '07-20242025/2025-01-30T12:00:00Z.json',
},
{
ref_time: '2025-01-30T18:00:00Z',
state: 'monitoring',
path: 'elvis/2025-01-30T18:00:00Z.json',
},
],
},
];
it.each(tests)('get latest reports', async ({ data, expected }) => {
mockedFetch.mockResolvedValue({
json: () => data,
});

const result = await getLatestAvailableReports();
expect(result).toEqual(expected);
});

it('it returns an empty array when request fails', async () => {
mockedFetch.mockRejectedValue(null);
const result = await getLatestAvailableReports();
expect(result).toEqual([]);
});
});

// describe('');
});
Original file line number Diff line number Diff line change
@@ -1,33 +1,78 @@
import { Repository } from 'typeorm';
import {
EmailPayload,
ShortReport,
ShortReportsResponseBody,
} from '../types/aa-storm-email';
import nodeFetch from 'node-fetch';
import { createConnection, Repository } from 'typeorm';
import { StormDataResponseBody, WindState } from './types/rawStormDataTypes';
import { LatestAAStormReports } from './entities/latestAAStormReports.entity';
import { LatestAAStormReports } from '../entities/latestAAStormReports.entity';
import { StormDataResponseBody, WindState } from '../types/rawStormDataTypes';

interface EmailPayload {}
// @ts-ignore
global.fetch = nodeFetch;

interface ShortReport {
ref_time: string;
state: string;
path: string;
function fetchAllReports(): Promise<ShortReportsResponseBody | null> {
return fetch(
'https://data.earthobservation.vam.wfp.org/public-share/aa/ts/outputs/dates.json',
)
.then((data) => data.json())
.catch(() => {
console.error('Error fetching all reports');
return null;
});
}

interface ShortReportsResponseBody {
[date: string]: {
[stormName: string]: ShortReport[];
};
// fetch and extract the more recent short report for each reported storm
export async function getLatestAvailableReports() {
// fetch all reports
const allReports = await fetchAllReports();

if (!allReports) {
return [];
}

// filter latest reports for all storms using day
const latestReportsDate = Object.keys(allReports).reduce(
(latestDateReports, currentDateReports) =>
new Date(currentDateReports) > new Date(latestDateReports)
? currentDateReports
: latestDateReports,
);

const latestDayReports = allReports[latestReportsDate];

// for each storm of the last day, keep only the latest report by time

return Object.keys(latestDayReports).map((stormName) => {
const stormShortReports = latestDayReports[stormName];

// get the latest report for that storm
const latestReport = stormShortReports.reduce(
(latestShortReport, currentShortReport) =>
new Date(currentShortReport.ref_time) >
new Date(latestShortReport.ref_time)
? currentShortReport
: latestShortReport,
);
return latestReport;
});
}

// @ts-ignore
global.fetch = nodeFetch;
export async function filterAlreadyProcessedReports(
availableReports: ShortReport[],
latestStormReportsRepository: Repository<LatestAAStormReports>,
) {
// get the last reports which have been processed for email alert system
const latestProcessedReports = await latestStormReportsRepository.find();

function fetchAllReports(): Promise<ShortReportsResponseBody | null> {
try {
return fetch(
'https://data.earthobservation.vam.wfp.org/public-share/aa/ts/outputs/dates.json',
).then((data) => data.json());
} catch {
return Promise.reject(null);
}
const latestProcessedReportsPaths = latestProcessedReports.map(
(item) => item.reportIdentifier,
);

return availableReports.filter(
(availableReport) =>
!latestProcessedReportsPaths.includes(availableReport.path),
);
}

function isEmailNeededByReport(report: StormDataResponseBody) {
Expand Down Expand Up @@ -88,61 +133,7 @@ function isEmailNeededByReport(report: StormDataResponseBody) {
return false;
}

// fetch and extract the more recent short report for each reported storm
async function fetchLatestAvailableReports() {
// fetch all reports
const allReports = await fetchAllReports();

if (!allReports) {
console.error('Error fetching all reports');
return [];
}

// filter latest reports for all storms using day
const latestReportsDate = Object.keys(allReports).reduce(
(latestDateReports, currentDateReports) =>
new Date(currentDateReports) > new Date(latestDateReports)
? currentDateReports
: latestDateReports,
);

const latestDayReports = allReports[latestReportsDate];

// for each storm of the last day, keep only the latest report by time

return Object.keys(latestDayReports).map((stormName) => {
const stormShortReports = latestDayReports[stormName];

// get the latest report for that storm
const latestReport = stormShortReports.reduce(
(latestShortReport, currentShortReport) =>
new Date(currentShortReport.ref_time) >
new Date(latestShortReport.ref_time)
? currentShortReport
: latestShortReport,
);
return latestReport;
});
}

async function filterAlreadyProcessedReports(
availableReports: ShortReport[],
latestStormReportsRepository: Repository<LatestAAStormReports>,
) {
// get the last reports which have been processed for email alert system
const latestProcessedReports = await latestStormReportsRepository.find();

const latestProcessedReportsPaths = latestProcessedReports.map(
(item) => item.reportIdentifier,
);

return availableReports.filter(
(availableReport) =>
!latestProcessedReportsPaths.includes(availableReport.path),
);
}

async function buildEmailPayloads(
export async function buildEmailPayloads(
shortReports: ShortReport[],
): Promise<EmailPayload[]> {
try {
Expand Down Expand Up @@ -170,35 +161,3 @@ async function buildEmailPayloads(
return [];
}
}

async function run() {
// create a connection to the remote db
const connection = await createConnection();
const latestStormReportsRepository =
connection.getRepository(LatestAAStormReports);

const latestAvailableReports = await fetchLatestAvailableReports();

// filter reports which have been already processed

const filteredAvailableReports = await filterAlreadyProcessedReports(
latestAvailableReports,
latestStormReportsRepository,
);

// check whether an email should be sent
buildEmailPayloads(filteredAvailableReports).then((emailPayloads) => {
console.log('emailPayload', emailPayloads);

// create templates
// send emails
});

// drop all latest storm reports stored to prevent accumulation of useless data in this table by time
await latestStormReportsRepository.clear();
await latestStormReportsRepository.save(
latestAvailableReports.map((report) => ({ reportIdentifier: report.path })),
);
}

run();
45 changes: 45 additions & 0 deletions alerting/src/aa-storm-alert/worker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createConnection, Repository } from 'typeorm';
import { StormDataResponseBody, WindState } from '../types/rawStormDataTypes';
import { LatestAAStormReports } from '../entities/latestAAStormReports.entity';
import { StormAlertData } from '../types/email';
import {
buildEmailPayloads,
filterAlreadyProcessedReports,
getLatestAvailableReports,
} from './alert';

// Replace with real function when available
function sendStormAlertEmail(data: StormAlertData) {
//nothing yet
}

export async function run() {
// create a connection to the remote db
const connection = await createConnection();
const latestStormReportsRepository =
connection.getRepository(LatestAAStormReports);

const latestAvailableReports = await getLatestAvailableReports();

// filter reports which have been already processed

const filteredAvailableReports = await filterAlreadyProcessedReports(
latestAvailableReports,
latestStormReportsRepository,
);

// check whether an email should be sent
const emailPayloads = await buildEmailPayloads(filteredAvailableReports);

console.log('emailPayload', emailPayloads);

// create templates
sendStormAlertEmail(emailPayloads);
// send emails

// drop all latest storm reports stored to prevent accumulation of useless data in this table by time
await latestStormReportsRepository.clear();
await latestStormReportsRepository.save(
latestAvailableReports.map((report) => ({ reportIdentifier: report.path })),
);
}
13 changes: 13 additions & 0 deletions alerting/src/types/aa-storm-email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface EmailPayload {}

export interface ShortReport {
ref_time: string;
state: string;
path: string;
}

export interface ShortReportsResponseBody {
[date: string]: {
[stormName: string]: ShortReport[];
};
}
13 changes: 13 additions & 0 deletions alerting/src/types/email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export interface StormAlertData {
email: string;
cycloneName: string;
cycloneTime: string;
activatedTriggers?: {
districts48kt: string[];
districts64kt: string[];
windspeed: string;
};
redirectUrl: string;
base64Image: string;
readiness: boolean;
}

0 comments on commit ddd4a08

Please sign in to comment.