From fc67b979349218cf9b6852ded68725a6e962adbb Mon Sep 17 00:00:00 2001 From: Rupam Kairi Date: Thu, 26 Dec 2024 07:44:42 +0530 Subject: [PATCH 1/3] Squashed fires api features Versioning & GeoJSON return Api versioning introduced (path /v1, /v2). Fires are returned as GeoJSON. handling query parameter span fixed output format file moved, removes /rest segment accounted for remoteIds & CORS. --- .prettierignore | 1 + apps/server/src/pages/api/v1/fires/index.ts | 162 ++++++++++++++++++++ 2 files changed, 163 insertions(+) create mode 100644 .prettierignore create mode 100644 apps/server/src/pages/api/v1/fires/index.ts diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..fa29cdff --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +** \ No newline at end of file diff --git a/apps/server/src/pages/api/v1/fires/index.ts b/apps/server/src/pages/api/v1/fires/index.ts new file mode 100644 index 00000000..077ae0c4 --- /dev/null +++ b/apps/server/src/pages/api/v1/fires/index.ts @@ -0,0 +1,162 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { prisma } from "../../../../server/db"; +import { logger } from "../../../../server/logger"; + +type ResponseData = + | GeoJSON.GeoJSON + | { + message?: string; + error?: object | unknown; + }; + +export default async function firesBySiteHandler( + req: NextApiRequest, + res: NextApiResponse +) { + 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], + })) + ); + + 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; +} From 98b795447b9463b6c04db96770a9e7f07f95dc6f Mon Sep 17 00:00:00 2001 From: Rupam Kairi Date: Mon, 13 Jan 2025 18:51:31 +0530 Subject: [PATCH 2/3] Conditional caching by env var Caching setting by environment variables Caching env variable --- apps/server/src/env.mjs | 5 +++-- apps/server/src/pages/api/v1/fires/index.ts | 22 +++++++++++++++------ 2 files changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/server/src/env.mjs b/apps/server/src/env.mjs index b58bbce1..ebadc4f1 100644 --- a/apps/server/src/env.mjs +++ b/apps/server/src/env.mjs @@ -38,7 +38,8 @@ 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.string().optional(), }); /** @@ -58,7 +59,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, diff --git a/apps/server/src/pages/api/v1/fires/index.ts b/apps/server/src/pages/api/v1/fires/index.ts index 077ae0c4..1bece7f7 100644 --- a/apps/server/src/pages/api/v1/fires/index.ts +++ b/apps/server/src/pages/api/v1/fires/index.ts @@ -1,6 +1,9 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "../../../../server/db"; import { logger } from "../../../../server/logger"; +import * as process from "node:process"; + + type ResponseData = | GeoJSON.GeoJSON @@ -9,6 +12,11 @@ type ResponseData = error?: object | unknown; }; +let CACHING = true; +if(process.env.PUBLIC_API_CACHING && process.env.PUBLIC_API_CACHING?.toLowerCase() === "false") { + CACHING = false; +} + export default async function firesBySiteHandler( req: NextApiRequest, res: NextApiResponse @@ -63,12 +71,14 @@ export default async function firesBySiteHandler( })) ); - 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"); + 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) { From 5e2b6d98fcae902df53dfde21b7a73af44d8d60a Mon Sep 17 00:00:00 2001 From: Rupam Kairi Date: Tue, 14 Jan 2025 22:20:35 +0530 Subject: [PATCH 3/3] Schema changes & fix for optional --- apps/server/src/env.mjs | 9 +++++++-- apps/server/src/pages/api/v1/fires/index.ts | 8 ++------ 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/apps/server/src/env.mjs b/apps/server/src/env.mjs index ebadc4f1..8d4493cd 100644 --- a/apps/server/src/env.mjs +++ b/apps/server/src/env.mjs @@ -39,7 +39,11 @@ const server = z.object({ NEXT_PUBLIC_LOGTAIL_SOURCE_TOKEN: z.string().optional(), WHATSAPP_ENDPOINT_URL: z.string(), WHATSAPP_ENDPOINT_AUTH_TOKEN: z.string(), - PUBLIC_API_CACHING: z.string().optional(), + 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"; + }), }); /** @@ -80,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 }; diff --git a/apps/server/src/pages/api/v1/fires/index.ts b/apps/server/src/pages/api/v1/fires/index.ts index 1bece7f7..c0d91988 100644 --- a/apps/server/src/pages/api/v1/fires/index.ts +++ b/apps/server/src/pages/api/v1/fires/index.ts @@ -1,8 +1,7 @@ import type { NextApiRequest, NextApiResponse } from "next"; import { prisma } from "../../../../server/db"; import { logger } from "../../../../server/logger"; -import * as process from "node:process"; - +import { env } from "../../../../env.mjs" type ResponseData = @@ -12,10 +11,7 @@ type ResponseData = error?: object | unknown; }; -let CACHING = true; -if(process.env.PUBLIC_API_CACHING && process.env.PUBLIC_API_CACHING?.toLowerCase() === "false") { - CACHING = false; -} +const CACHING = env.PUBLIC_API_CACHING ?? true; export default async function firesBySiteHandler( req: NextApiRequest,