Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
root = true

[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 2
trim_trailing_whitespace = true
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
PORT=4000
NASA_BASE_URL=""
NASA_API_KEY=""
NASA_API_SOL=""
SENTRY_DSN=""
14 changes: 14 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
name: Lint

on: [push, pull_request]

jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci --legacy-peer-deps
- run: npm run build:lint
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
.env
dist
3 changes: 3 additions & 0 deletions .gitignote
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules
dist
.env
4 changes: 4 additions & 0 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

npm run precommit
7 changes: 7 additions & 0 deletions app/config/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export default {
sentryDsn: process.env.SENTRY_DSN,
port: process.env.PORT,
nasaBaseUrl: process.env.NASA_BASE_URL,
nasaApiKey: process.env.NASA_API_KEY,
nasaSol: process.env.NASA_API_SOL,
};
55 changes: 55 additions & 0 deletions app/controllers/meteor.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Request, Response, NextFunction } from "express";
import { getMeteorsData } from "../services/meteor.service";
import { makeDateRange } from "../utils/dateUtils";

export const getMeteors = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
const date = String(req.query.date);
const [startDate, endDate] = makeDateRange(date);
const count = req.query.count === "true";
const wereDangerousMeteors = req.query["were-dangerous-meteors"] === "true";

const result = await getMeteorsData({
startDate,
endDate,
count,
wereDangerousMeteors,
});

res.json(result);
} catch (err) {
next(err);
}
};

export const renderMeteors = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
const date = String(req.query.date);
const [startDate, endDate] = makeDateRange(date);
const count = req.query.count === "true";
const wereDangerousMeteors = req.query["were-dangerous-meteors"] === "true";

const meteors = await getMeteorsData({
startDate,
endDate,
count,
wereDangerousMeteors,
});

res.render("meteors.njk", {
meteors: meteors.data,
wereDangerousMeteors: meteors.wereDangerousMeteors,
count: meteors.count,
});
} catch (err) {
next(err);
}
};
46 changes: 46 additions & 0 deletions app/controllers/rover.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Request, Response, NextFunction } from "express";
import { getRoverImageData } from "../services/rover.service";

export const getRoverImage = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
const { api_key } = req.body;
const result = await getRoverImageData(api_key);

res.json(result);
} catch (err) {
next(err);
}
};

export const renderRoverImageForm = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
res.render("roverImageForm.njk");
} catch (err) {
next(err);
}
};

export const renderRoverImage = async (
req: Request,
res: Response,
next: NextFunction,
) => {
try {
const { api_key } = req.body;
const image = await getRoverImageData(api_key);

res.render("roverImage.njk", {
image,
});
} catch (err) {
next(err);
}
};
21 changes: 21 additions & 0 deletions app/middlewares/validator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Request, Response, NextFunction } from "express";
import { Schema } from "joi";
import Exception from "../utils/exception";

const validator = (schema: Schema, property = "body") => {
return (req: Request, res: Response, next: NextFunction) => {
const data = property === "body" ? req.body : req.query;
const { error } = schema.validate(data, { abortEarly: false });

if (error) {
throw new Exception(
400,
`${error.details.map(({ message }) => message)}`,
);
}

next();
};
};

export default validator;
14 changes: 14 additions & 0 deletions app/repositories/meteor.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import axios from "axios";
import config from "../config/config";

export const findAllMeteors = async (startDate: string, endDate: string) => {
const result = await axios.get(`${config.nasaBaseUrl}/neo/rest/v1/feed`, {
params: {
start_date: startDate,
end_date: endDate,
api_key: config.nasaApiKey,
},
});

return result?.data;
};
16 changes: 16 additions & 0 deletions app/repositories/rover.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import axios from "axios";
import config from "../config/config";

export const findRoverPhotos = async (apiKey: string) => {
const result = await axios.get(
`${config.nasaBaseUrl}/mars-photos/api/v1/rovers/curiosity/photos`,
{
params: {
sol: config.nasaSol,
api_key: apiKey,
},
},
);

return result?.data?.photos;
};
62 changes: 62 additions & 0 deletions app/services/meteor.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { findAllMeteors } from "../repositories/meteor.repository";
import Exception from "../utils/exception";
import { NearEarthObjects } from "../types/nearEarthObjects";
import { Meteor } from "../types/meteor";

export const getMeteorsData = async ({
startDate,
endDate,
count,
wereDangerousMeteors,
}: {
startDate: string;
endDate: string;
count: boolean;
wereDangerousMeteors: boolean;
}) => {
const response = await findAllMeteors(startDate, endDate);

const nearEarthObjects: NearEarthObjects = response?.near_earth_objects;

if (!nearEarthObjects) {
throw new Exception(400, "No data available for these parameters");
}

const data: Meteor[] = Object.values(nearEarthObjects)
.map((dateValues) => {
return dateValues.map((meteor) => ({
id: meteor.id,
name: meteor.name,
diameter_meters:
meteor.estimated_diameter?.meters?.estimated_diameter_max,
is_potentially_hazardous_asteroid:
meteor.is_potentially_hazardous_asteroid,
close_approach_date_full:
meteor.close_approach_data[0].close_approach_date_full,
relative_velocity_kps:
meteor.close_approach_data[0].relative_velocity.kilometers_per_second,
}));
})
.flat();

let responseData: {
data: Meteor[];
count?: number;
wereDangerousMeteors?: boolean;
} = { data };

if (count) {
responseData = { ...responseData, count: data.length };
}

if (wereDangerousMeteors) {
responseData = {
...responseData,
wereDangerousMeteors: data.some(
(meteor) => meteor.is_potentially_hazardous_asteroid,
),
};
}

return responseData;
};
13 changes: 13 additions & 0 deletions app/services/rover.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Photo } from "../types/photo";
import { findRoverPhotos } from "../repositories/rover.repository";

export const getRoverImageData = async (apiKey: string) => {
const photos = await findRoverPhotos(apiKey);

const photo = photos.reduce((mostRecent: Photo, currentPhoto: Photo) => {
return new Date(currentPhoto.earth_date) > new Date(mostRecent.earth_date)
? currentPhoto
: mostRecent;
});
return photo?.img_src;
};
8 changes: 8 additions & 0 deletions app/types/meteor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface Meteor {
id: string;
name: string;
diameter_meters: number;
is_potentially_hazardous_asteroid: boolean;
close_approach_date_full: string;
relative_velocity_kps: string;
}
24 changes: 24 additions & 0 deletions app/types/nearEarthObjects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export type NearEarthObjects = Record<string, NearEarthObject[]>;

interface NearEarthObject {
id: string;
name: string;
estimated_diameter: EstimatedDiameter;
is_potentially_hazardous_asteroid: boolean;
close_approach_data: CloseApproachData[];
is_sentry_object: boolean;
}

interface EstimatedDiameter {
meters: {
estimated_diameter_min: number;
estimated_diameter_max: number;
};
}

interface CloseApproachData {
close_approach_date_full: string;
relative_velocity: {
kilometers_per_second: string;
};
}
4 changes: 4 additions & 0 deletions app/types/photo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface Photo {
earth_date: string;
img_src: string;
}
18 changes: 18 additions & 0 deletions app/utils/dateUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { isMatch, format } from "date-fns";

const parseDate = (date: string) => {
const hasMatch = isMatch(date, "yyyy-MM-dd");
if (hasMatch) {
return date;
} else {
return format(new Date(), "yyyy-MM-dd");
}
};

export const makeDateRange = (date: string): string[] => {
if (Array.isArray(date)) {
return [parseDate(date[0]), parseDate(date[1])];
}

return [parseDate(date), parseDate(date)];
};
26 changes: 26 additions & 0 deletions app/utils/errorHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Request, Response, NextFunction } from "express";
import * as Sentry from "@sentry/node";
import Exception from "./exception";

const errorHandler = (
err: Error | Exception,
req: Request,
res: Response,
next: NextFunction,
) => {
if (res.headersSent) {
return next(err);
}

console.error(err.stack);

Sentry.captureException(err);

if (err instanceof Exception) {
res.status(err.statusCode || 500).json({ error: err.message });
}

res.status(500).json({ error: err.message || "Internal Server Error" });
};

export default errorHandler;
8 changes: 8 additions & 0 deletions app/utils/exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export default class Exception extends Error {
statusCode: number;

constructor(statusCode: number, message: string) {
super(message);
this.statusCode = statusCode;
}
}
Loading