Skip to content

Commit

Permalink
Merge pull request #183 from Plant-for-the-Planet-org/feature/REST-en…
Browse files Browse the repository at this point in the history
…dpoint-fires

Feature/rest endpoint fires
  • Loading branch information
sagararyal authored Jan 14, 2025
2 parents cba2786 + 5e2b6d9 commit acc69c8
Show file tree
Hide file tree
Showing 3 changed files with 178 additions and 3 deletions.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**
12 changes: 9 additions & 3 deletions apps/server/src/env.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,12 @@ const server = z.object({
CRON_KEY: z.string().optional(),
NEXT_PUBLIC_LOGTAIL_SOURCE_TOKEN: z.string().optional(),
WHATSAPP_ENDPOINT_URL: z.string(),
WHATSAPP_ENDPOINT_AUTH_TOKEN: z.string()
WHATSAPP_ENDPOINT_AUTH_TOKEN: z.string(),
PUBLIC_API_CACHING: z.union([z.literal("true"), z.literal("false")]).optional()
.transform((val) => {
if (val === undefined) return true; // since it is optional by default caching kept true
return val === "true";
}),
});

/**
Expand All @@ -58,7 +63,7 @@ const client = z.object({
const processEnv = {
DATABASE_PRISMA_URL: process.env.DATABASE_PRISMA_URL,
DATABASE_URL: process.env.DATABASE_URL ? process.env.DATABASE_URL : process.env.DATABASE_PRISMA_URL,
DATABASE_URL_NON_POOLING: process.env.DATABASE_URL_NON_POOLING ? process.env.DATABASE_URL_NON_POOLING : process.env.DATABASE_URL,
DATABASE_URL_NON_POOLING: process.env.DATABASE_URL_NON_POOLING ? process.env.DATABASE_URL_NON_POOLING : process.env.DATABASE_URL,
// DATABASE_PRISMA_URL is set by VERCEL POSTGRES and had pooling built in.
NODE_ENV: process.env.NODE_ENV,
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
Expand All @@ -79,7 +84,8 @@ const processEnv = {
CRON_KEY: process.env.CRON_KEY,
NEXT_PUBLIC_LOGTAIL_SOURCE_TOKEN: process.env.NEXT_PUBLIC_LOGTAIL_SOURCE_TOKEN,
WHATSAPP_ENDPOINT_URL: process.env.WHATSAPP_ENDPOINT_URL,
WHATSAPP_ENDPOINT_AUTH_TOKEN: process.env.WHATSAPP_ENDPOINT_AUTH_TOKEN
WHATSAPP_ENDPOINT_AUTH_TOKEN: process.env.WHATSAPP_ENDPOINT_AUTH_TOKEN,
PUBLIC_API_CACHING: process.env.PUBLIC_API_CACHING
};


Expand Down
168 changes: 168 additions & 0 deletions apps/server/src/pages/api/v1/fires/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../../server/db";
import { logger } from "../../../../server/logger";
import { env } from "../../../../env.mjs"


type ResponseData =
| GeoJSON.GeoJSON
| {
message?: string;
error?: object | unknown;
};

const CACHING = env.PUBLIC_API_CACHING ?? true;

export default async function firesBySiteHandler(
req: NextApiRequest,
res: NextApiResponse<ResponseData>
) {
try {
checkCORS(req, res);
checkMethods(req, res, ["GET"]);

let siteId = req.query.siteId as string;
const remoteId = req.query.remoteId as string;

if (!siteId && !remoteId)
return res.status(400).json({ message: "No site provided." });

const foundSite = await prisma.site.findFirst({ where: { OR: [{ id: siteId }, { remoteId: remoteId }] } });
if (!foundSite) return res.status(404).json({ message: "Site not found." });
siteId = foundSite.id

const span = handleParameter_span(req.query.span?.toString());

const alertsForSite = await prisma.siteAlert.findMany({
where: {
siteId: siteId,
eventDate: {
gte: span,
},
},
select: {
id: true,
eventDate: true,
type: true,
latitude: true,
longitude: true,
detectedBy: true,
confidence: true,
distance: true,
},
});

const fires = generateGeoJson(
alertsForSite.map((alert) => ({
id: alert.id,
eventDate: alert.eventDate,
type: alert.type,
detectedBy: alert.detectedBy,
confidence: alert.confidence,
distance: alert.distance,
})),
alertsForSite.map((alert) => ({
type: "Point",
coordinates: [alert.longitude, alert.latitude],
}))
);

if(CACHING) {
res.setHeader(
"Cache-Control",
"public, max-age=7200 s-maxage=3600, stale-while-revalidate=7200"
);
res.setHeader("CDN-Cache-Control", "max-age=7200");
res.setHeader("Cloudflare-CDN-Cache-Control", "max-age=7200");
}

res.status(200).json(fires);
} catch (error) {
console.log(error);
res.status(400).json({ message: "Failed!", error });
}
}

// Suggesting these type of utils - We might later need to organise them
function checkMethods(
req: NextApiRequest,
res: NextApiResponse,
allowedMethods: string[]
) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
if (!allowedMethods.includes(req.method!)) {
return res.status(405).json({ message: "Method Not Allowed" });
}
}

function checkCORS(req: NextApiRequest, res: NextApiResponse) {
res.setHeader('Access-Control-Allow-Credentials', 'true')
res.setHeader('Access-Control-Allow-Origin', '*')
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE')
res.setHeader('Access-Control-Allow-Headers', '*')
if (req.method === 'OPTIONS') {
res.status(200).end()
return
}
}

function checkAuthorization(req: NextApiRequest, res: NextApiResponse) {
const authorization = req.headers.authorization;
const accessToken = authorization?.split(" ")[1];
if (!accessToken) {
return res.status(401).json({ message: "Unauthorized" });
}

// Some token verification mechanism...
}

export function handleParameter_span(span?: string) {
let spanToDate = new Date();
switch (span?.toLowerCase()) {
case "24h":
spanToDate = new Date(spanToDate.getTime() - 1000 * 60 * 60 * 24);
break;
case "7d":
spanToDate = new Date(spanToDate.getTime() - 1000 * 60 * 60 * 24 * 7);
break;
case "30d":
spanToDate = new Date(spanToDate.getTime() - 1000 * 60 * 60 * 24 * 30);
break;
case "1y":
spanToDate = new Date(spanToDate.getTime() - 1000 * 60 * 60 * 24 * 365);
break;
default:
logger("Does not match any possible values, using default 7D", "Log");
spanToDate = new Date(spanToDate.getTime() - 1000 * 60 * 60 * 24 * 7);
}

return spanToDate;
}

export function generateGeoJson(
properties: GeoJSON.GeoJsonProperties[] = [],
points: GeoJSON.Point[] = []
) {
const geoJson: GeoJSON.GeoJSON = {
type: "FeatureCollection",
features: [],
};

if (!properties.length || !points.length) {
// throw new Error("Properties and geometries should have valid values.")
}

if (properties.length !== points.length) {
// throw new Error("Properties and geometries length should be equal.")
}

for (let i = 0; i < properties.length; i++) {
geoJson.features.push({
type: "Feature",
properties: properties[i],
geometry: points[i],
});
}

return geoJson;
}

0 comments on commit acc69c8

Please sign in to comment.