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:
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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 });
|
||||
};
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user