1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-07-03 15:16:40 -04:00

convert APIs to edge functions (#1648)

This commit is contained in:
2023-06-25 11:26:29 -04:00
committed by GitHub
parent 62419f1380
commit aa64279dd8
10 changed files with 305 additions and 429 deletions

View File

@ -1,6 +1,7 @@
import { NextResponse } from "next/server";
import queryString from "query-string";
import { logServerError } from "../../lib/helpers/sentry";
import type { NextApiRequest, NextApiResponse } from "next";
import { baseUrl } from "../../lib/config";
import type { NextRequest } from "next/server";
// fallback to dummy secret for testing: https://docs.hcaptcha.com/#integration-testing-test-keys
const HCAPTCHA_SITE_KEY =
@ -11,56 +12,55 @@ const HCAPTCHA_API_ENDPOINT = "https://hcaptcha.com/siteverify";
const { AIRTABLE_API_KEY, AIRTABLE_BASE } = process.env;
const AIRTABLE_API_ENDPOINT = "https://api.airtable.com/v0/";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
// disable caching on both ends
res.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate");
res.setHeader("Pragma", "no-cache");
export const config = {
runtime: "edge",
};
// redirect GET requests to this endpoint to the contact form itself
if (req.method === "GET") {
return res.redirect(302, "/contact/");
}
const { body } = req;
// these are both backups to client-side validations just in case someone squeezes through without them. the codes
// are identical so they're caught in the same fashion.
if (!body.name || !body.email || !body.message) {
// all fields are required
throw new Error("USER_MISSING_DATA");
}
if (!body["h-captcha-response"] || !(await validateCaptcha(body["h-captcha-response"]))) {
// either the captcha is wrong or completely missing
throw new Error("USER_INVALID_CAPTCHA");
}
// sent directly to airtable
const airtableResult = await sendToAirtable({
Name: body.name,
Email: body.email,
Message: body.message,
});
// throw an internal error, not user's fault
if (airtableResult !== true) {
throw new Error("AIRTABLE_API_ERROR");
}
// success! let the client know
return res.status(200).json({ success: true });
} catch (error) {
// extract just the error message to send back to client
const message = error instanceof Error ? error.message : "UNKNOWN_EXCEPTION";
// log errors (except PEBCAK) to console and sentry
if (!message.startsWith("USER_")) {
await logServerError(error);
}
// 500 Internal Server Error
return res.status(500).json({ success: false, message });
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextRequest) => {
// redirect GET requests to this endpoint to the contact form itself
if (req.method === "GET") {
return NextResponse.redirect(`${baseUrl}/contact/`);
}
// possible weirdness? https://github.com/orgs/vercel/discussions/78#discussioncomment-5089059
const data = await req.json();
// these are both backups to client-side validations just in case someone squeezes through without them. the codes
// are identical so they're caught in the same fashion.
if (!data.name || !data.email || !data.message) {
// all fields are required
throw new Error("MISSING_DATA");
}
if (!data["h-captcha-response"] || !(await validateCaptcha(data["h-captcha-response"]))) {
// either the captcha is wrong or completely missing
throw new Error("INVALID_CAPTCHA");
}
// sent directly to airtable
const airtableResult = await sendToAirtable({
Name: data.name,
Email: data.email,
Message: data.message,
});
// throw an internal error, not user's fault
if (airtableResult !== true) {
throw new Error("AIRTABLE_API_ERROR");
}
// success! let the client know
return NextResponse.json(
{ success: true },
{
status: 201,
headers: {
// disable caching on both ends. see:
// https://vercel.com/docs/concepts/functions/edge-functions/edge-caching
"Cache-Control": "private, no-cache, no-store, must-revalidate",
},
}
);
};
const validateCaptcha = async (formResponse: unknown): Promise<unknown> => {
@ -95,5 +95,3 @@ const sendToAirtable = async (data: unknown): Promise<boolean> => {
return response.ok;
};
export default handler;

View File

@ -1,32 +1,30 @@
import { NextResponse } from "next/server";
import { prisma } from "../../lib/helpers/prisma";
import { logServerError } from "../../lib/helpers/sentry";
import type { NextApiRequest, NextApiResponse } from "next";
import type { NextRequest } from "next/server";
import type { PageStats } from "../../types";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
if (!req.query?.slug) {
return res.status(400).json({ message: "Missing `slug` parameter." });
}
export const config = {
runtime: "edge",
regions: ["iad1"], // the vercel postgres database lives in DC
};
// add one to this page's count and return the new number
const result = await incrementPageHits(req.query.slug as string);
// eslint-disable-next-line import/no-anonymous-default-export
export default async (req: NextRequest) => {
const slug = req.nextUrl.searchParams.get("slug");
// disable caching on both ends
res.setHeader("Cache-Control", "private, no-cache, no-store, must-revalidate");
// send result as JSON
return res.status(200).json(result);
} catch (error) {
// extract just the error message to send back to client
const message = error instanceof Error ? error.message : error;
// log full error to console and sentry
await logServerError(error);
// 500 Internal Server Error
return res.status(500).json({ message });
if (!slug) {
return NextResponse.json({ message: "Missing `slug` parameter." }, { status: 400 });
}
// add one to this page's count and return the new number
return NextResponse.json(await incrementPageHits(slug), {
status: 200,
headers: {
// disable caching on both ends. see:
// https://vercel.com/docs/concepts/functions/edge-functions/edge-caching
"Cache-Control": "private, no-cache, no-store, must-revalidate",
},
});
};
const incrementPageHits = async (slug: string): Promise<PageStats> => {
@ -43,5 +41,3 @@ const incrementPageHits = async (slug: string): Promise<PageStats> => {
// send client the *new* hit count
return { hits };
};
export default handler;

View File

@ -1,71 +1,29 @@
import { prisma } from "../../lib/helpers/prisma";
import { getAllNotes } from "../../lib/helpers/parse-notes";
import { logServerError } from "../../lib/helpers/sentry";
import { NOTES_DIR } from "../../lib/config/constants";
import type { NextApiRequest, NextApiResponse } from "next";
import type { DetailedPageStats, SiteStats } from "../../types";
import { NextResponse } from "next/server";
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
// return overall site stats
const result = await getSiteStats();
// let Vercel edge cache results for 15 mins
res.setHeader("Cache-Control", "public, max-age=0, s-maxage=900, stale-while-revalidate");
// send result as JSON
return res.status(200).json(result);
} catch (error) {
// extract just the error message to send back to client
const message = error instanceof Error ? error.message : error;
// log full error to console and sentry
await logServerError(error);
// 500 Internal Server Error
return res.status(500).json({ message });
}
export const config = {
runtime: "edge",
regions: ["iad1"], // the vercel postgres database lives in DC
};
const getSiteStats = async (): Promise<SiteStats> => {
// eslint-disable-next-line import/no-anonymous-default-export
export default async () => {
// simultaneously fetch the entire hits db and notes from the filesystem
const [hits, notes] = await Promise.all([
prisma.hits.findMany({
orderBy: [
{
hits: "desc",
},
],
}),
getAllNotes(),
]);
const pages: DetailedPageStats[] = [];
const total = { hits: 0 };
hits.forEach((record) => {
// match slugs from getAllNotes() with db results to populate some metadata
// TODO: add support for pages other than notes.
const match = notes.find((note) => `${NOTES_DIR}/${note.slug}` === record.slug);
// don't reveal via API if the db entry doesn't belong to a valid page
if (!match) {
return;
}
// merge record with its matching front matter data
pages.push({
...record,
title: match.title,
url: match.permalink,
date: match.date,
});
// add these hits to running tally
total.hits += record.hits;
const pages = await prisma.hits.findMany({
orderBy: [
{
hits: "desc",
},
],
});
return { total, pages };
};
const total = { hits: 0 };
export default handler;
// calculate total hits
pages.forEach((page) => {
// add these hits to running tally
total.hits += page.hits;
});
return NextResponse.json({ total, pages }, { status: 200 });
};

View File

@ -2,8 +2,7 @@
// Heavily inspired by @leerob: https://leerob.io/snippets/spotify
import queryString from "query-string";
import { logServerError } from "../../lib/helpers/sentry";
import type { NextApiRequest, NextApiResponse } from "next";
import { NextResponse } from "next/server";
import type { Track } from "../../types";
const { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN } = process.env;
@ -32,25 +31,16 @@ type SpotifyTrackSchema = {
};
};
const handler = async (req: NextApiRequest, res: NextApiResponse) => {
try {
// let Vercel edge cache results for 5 mins
res.setHeader("Cache-Control", "public, max-age=0, s-maxage=300, stale-while-revalidate");
export const config = {
runtime: "edge",
};
const token = await getAccessToken();
const playing = await getNowPlaying(token);
const top = await getTopTracks(token);
// eslint-disable-next-line import/no-anonymous-default-export
export default async () => {
const token = await getAccessToken();
const [playing, top] = await Promise.all([getNowPlaying(token), getTopTracks(token)]);
return res.status(200).json({ playing, top });
} catch (error) {
const message = error instanceof Error ? error.message : "Unknown error.";
// log full error to console and sentry
await logServerError(error);
// 500 Internal Server Error
return res.status(500).json({ message });
}
return NextResponse.json({ playing, top }, { status: 200 });
};
const getAccessToken = async () => {
@ -125,5 +115,3 @@ const getTopTracks = async (token: string): Promise<Track[]> => {
return tracks;
};
export default handler;