From 675795286e581bbcbc181079529d744d62783795 Mon Sep 17 00:00:00 2001 From: Kieran Osgood Date: Thu, 24 Oct 2024 12:08:39 +0100 Subject: [PATCH] refactor(api/spotify): extra succinct functions from GET --- src/components/about/spotify.tsx | 4 +- src/mocks/handlers.ts | 6 +- src/pages/api/spotify.ts | 118 +++++++++++++++++++++---------- src/pages/api/stackoverflow.ts | 1 - 4 files changed, 85 insertions(+), 44 deletions(-) diff --git a/src/components/about/spotify.tsx b/src/components/about/spotify.tsx index 2c7a0c2..c0c1062 100644 --- a/src/components/about/spotify.tsx +++ b/src/components/about/spotify.tsx @@ -1,8 +1,8 @@ import type { z } from "astro/zod"; -import type { TrackSchema } from "src/pages/api/spotify"; +import type { Track } from "src/pages/api/spotify"; type SpotifyProps = { - track: z.infer; + track: z.infer; }; export default function Spotify(props: SpotifyProps) { diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts index ba50d0f..6429338 100644 --- a/src/mocks/handlers.ts +++ b/src/mocks/handlers.ts @@ -1,6 +1,6 @@ import type { z } from "astro/zod"; import { http, HttpResponse } from "msw"; -import type { TokenSchema, RecentlyPlayedSchema } from "src/pages/api/spotify"; +import type { Tokens, RecentlyPlayed } from "src/pages/api/spotify"; import type { StackOverflow } from "src/pages/api/stackoverflow"; export const handlers = [ @@ -34,14 +34,14 @@ export const handlers = [ }), http.post("https://accounts.spotify.com/api/token", () => { - const result: z.infer = { + const result: z.infer = { access_token: "token", }; return HttpResponse.json(result); }), http.get("https://api.spotify.com/v1/me/player/recently-played", () => { - const result: z.infer = { + const result: z.infer = { items: [ { track: { diff --git a/src/pages/api/spotify.ts b/src/pages/api/spotify.ts index 5c112ce..5728a41 100644 --- a/src/pages/api/spotify.ts +++ b/src/pages/api/spotify.ts @@ -5,37 +5,33 @@ const RECENTLY_PLAYED_ENDPOINT = "https://api.spotify.com/v1/me/player/recently-played"; const REFRESH_ENDPOINT = "https://accounts.spotify.com/api/token"; +const authOptions = new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: import.meta.env.SPOTIFY_WEB_API_REFRESH_TOKEN, +}); + // NOTE: these schemas are intentionally sparse and missing properties -export const TrackSchema = z.object({ +export const Track = z.object({ artists: z.array(z.object({ name: z.string() })), external_urls: z.object({ spotify: z.string() }), name: z.string(), }); -export type Track = z.infer; +export type Track = z.infer; -export const TokenSchema = z.object({ +export const Tokens = z.object({ access_token: z.z.string(), }); +type Tokens = z.infer; -export const RecentlyPlayedSchema = z.object({ - items: z.array(z.object({ track: TrackSchema })), +export const RecentlyPlayed = z.object({ + items: z.array(z.object({ track: Track })), }); +type RecentlyPlayed = z.infer; -const authOptions = new URLSearchParams({ - grant_type: "refresh_token", - refresh_token: import.meta.env.SPOTIFY_WEB_API_REFRESH_TOKEN, -}); +type SuccessOrError = [null, error: Response] | [result: T, null]; -/** - Access tokens only last for 1hour so no use storing them in env - Instead we just request new access_token when we need the spotify data - as we don't want to store the tokens client side (allows users to view my data) - */ -const GET: APIRoute & { - Schema: typeof TrackSchema; -} = async (): Promise => { - // TODO: cache this to avoid hitting rate limits +async function fetchToken(): Promise> { const tokenResponse = await fetch(REFRESH_ENDPOINT, { method: "POST", body: authOptions.toString(), @@ -43,38 +39,84 @@ const GET: APIRoute & { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8", Authorization: import.meta.env.SPOTIFY_AUTHORIZATION, }, - }) - .then((response) => response.json()) - .then((response) => TokenSchema.safeParse(response)); + }); - if (!tokenResponse.success) { - console.error("Retrieve token failed: ", tokenResponse.error.format()); - return new Response(null, { status: 401 }); + if (tokenResponse.ok === false) { + console.error(`[${fetchToken.name}][FetchFail] !ok: `, tokenResponse); + return [null, new Response(null, { status: tokenResponse.status })]; + } + + const result = Tokens.safeParse(await tokenResponse.json()); + + if (result.success === false) { + console.error( + `[${fetchToken.name}][SchemaFail] !success: `, + result.error.format(), + ); + return [null, new Response(null, { status: 401 })]; } + + return [result.data, null]; +} + +async function fetchRecentlyPlayed( + tokens: Tokens, +): Promise> { const url = new URL(RECENTLY_PLAYED_ENDPOINT); url.searchParams.set("limit", "1"); - // TODO: cache this to avoid hitting rate limits - const recentlyPlayed = await fetch(url, { + const recentlyPlayedResponse = await fetch(url, { method: "get", - headers: { - Authorization: `Bearer ${tokenResponse.data.access_token}`, - }, - }) - .then((response) => response.json()) - .then((response) => RecentlyPlayedSchema.safeParse(response)); + headers: { Authorization: `Bearer ${tokens.access_token}` }, + }); + + if (recentlyPlayedResponse.ok === false) { + console.error( + `[${fetchRecentlyPlayed.name}][FetchFail] !ok: `, + recentlyPlayedResponse, + ); + return [ + null, + new Response(null, { status: recentlyPlayedResponse.status }), + ]; + } - if (!recentlyPlayed.success) { + const recentlyPlayedResult = RecentlyPlayed.safeParse( + await recentlyPlayedResponse.json(), + ); + + if (recentlyPlayedResult.success === false) { console.error( - "Recently played has changed shape: ", - recentlyPlayed.error.format(), + `[${fetchRecentlyPlayed.name}][SchemaFail] !success: `, + recentlyPlayedResult.error.format(), ); - return new Response(null, { status: 403 }); + return [null, new Response(null, { status: 403 })]; + } + + return [recentlyPlayedResult.data, null]; +} + +type GET = APIRoute & { Schema: typeof Track }; +/** + Access tokens only last for 1hour so no use storing them in env + Instead we just request new access_token when we need the spotify data + as we don't want to store the tokens client side (allows users to view my data) + */ +const GET: GET = async () => { + const [tokens, tokenError] = await fetchToken(); + if (tokenError) { + return tokenError; + } + + const [recentlyPlayed, recentlyPlayedError] = + await fetchRecentlyPlayed(tokens); + if (recentlyPlayedError) { + return recentlyPlayedError; } - return new Response(JSON.stringify(recentlyPlayed.data.items?.[0]?.track)); + return new Response(JSON.stringify(recentlyPlayed.items?.[0]?.track)); }; -GET.Schema = TrackSchema; +GET.Schema = Track; export { GET }; diff --git a/src/pages/api/stackoverflow.ts b/src/pages/api/stackoverflow.ts index 4a53c9d..617425d 100644 --- a/src/pages/api/stackoverflow.ts +++ b/src/pages/api/stackoverflow.ts @@ -14,7 +14,6 @@ export type StackOverflow = z.infer; const GET: APIRoute & { Schema: typeof StackOverflow; } = async (): Promise => { - // TODO: cache this to avoid hitting rate limits const stackOverflowResponse = await fetch(ROUTE, { cache: "force-cache" }); if (!stackOverflowResponse.ok) {