1
mirror of https://github.com/jakejarvis/jarv.is.git synced 2025-07-21 19:21:18 -04:00

switch back to JS for serverless functions (for now) (#549)

Vercel's TS transpiliation is too flaky: https://github.com/vercel/vercel/discussions/6665
This commit is contained in:
2021-09-09 17:30:38 -04:00
committed by GitHub
parent aa9f347c3d
commit 4ce401b9ef
15 changed files with 107 additions and 410 deletions

View File

@@ -1,11 +1,9 @@
import * as Sentry from "@sentry/node";
import { VercelRequest, VercelResponse } from "@vercel/node";
import { Client, query as q } from "faunadb";
import fetch from "node-fetch";
import parser from "fast-xml-parser";
import { decode } from "html-entities";
import type { PageStats, OverallStats } from "./types/hits";
import faunadb from "faunadb";
const q = faunadb.query;
const baseUrl = "https://jarv.is/";
@@ -14,8 +12,7 @@ Sentry.init({
environment: process.env.NODE_ENV || process.env.VERCEL_ENV || "",
});
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export default async (req: VercelRequest, res: VercelResponse) => {
export default async (req, res) => {
try {
// some rudimentary error handling
if (!process.env.FAUNADB_SERVER_SECRET) {
@@ -25,12 +22,11 @@ export default async (req: VercelRequest, res: VercelResponse) => {
throw new Error(`Method ${req.method} not allowed.`);
}
const client = new Client({
const client = new faunadb.Client({
secret: process.env.FAUNADB_SERVER_SECRET,
});
const { slug } = req.query;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let result: any;
let result;
if (!slug || slug === "/") {
// return overall site stats if slug not specified
@@ -65,9 +61,8 @@ export default async (req: VercelRequest, res: VercelResponse) => {
}
};
const incrementPageHits = async (slug: string | string[], client: Client): Promise<PageStats> => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
const result = await client.query<any>(
const incrementPageHits = async (slug, client) => {
const result = await client.query(
q.Let(
{ match: q.Match(q.Index("hits_by_slug"), slug) },
q.If(
@@ -85,14 +80,12 @@ const incrementPageHits = async (slug: string | string[], client: Client): Promi
);
// send client the *new* hit count
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
return result.data;
};
const getSiteStats = async (client: Client): Promise<OverallStats> => {
const getSiteStats = async (client) => {
// get database and RSS results asynchronously
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-assignment
const [feed, result] = await Promise.all<{ [key: string]: any }, any>([
const [feed, result] = await Promise.all([
parser.parse(await (await fetch(baseUrl + "feed.xml")).text()), // this is messy but it works :)
client.query(
q.Map(
@@ -102,25 +95,18 @@ const getSiteStats = async (client: Client): Promise<OverallStats> => {
),
]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
const pages: PageStats[] = result.data;
const stats: OverallStats = {
const pages = result.data;
const stats = {
total: { hits: 0 },
pages,
};
pages.map((p: PageStats) => {
pages.map((p) => {
// match URLs from RSS feed with db to populate some metadata
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call
const match = feed.rss.channel.item.find(
(x: { link: string }) => x.link === baseUrl + p.slug + "/"
);
const match = feed.rss.channel.item.find((x) => x.link === baseUrl + p.slug + "/");
if (match) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
p.title = decode(match.title);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
p.url = match.link;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
p.date = new Date(match.pubDate);
}
@@ -131,9 +117,7 @@ const getSiteStats = async (client: Client): Promise<OverallStats> => {
});
// sort by hits (descending)
stats.pages.sort((a: { hits: number }, b: { hits: number }) => {
return a.hits > b.hits ? -1 : 1;
});
stats.pages.sort((a, b) => (a.hits > b.hits ? -1 : 1));
return stats;
};

View File

@@ -1,16 +1,12 @@
import * as Sentry from "@sentry/node";
import { VercelRequest, VercelResponse } from "@vercel/node";
import { graphql, GraphQlQueryResponseData } from "@octokit/graphql";
import type { Repository, GHRepoSchema } from "./types/projects";
import { graphql } from "@octokit/graphql";
Sentry.init({
dsn: process.env.SENTRY_DSN || "",
environment: process.env.NODE_ENV || process.env.VERCEL_ENV || "",
});
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export default async (req: VercelRequest, res: VercelResponse) => {
export default async (req, res) => {
try {
// some rudimentary error handling
if (req.method !== "GET") {
@@ -48,9 +44,9 @@ export default async (req: VercelRequest, res: VercelResponse) => {
}
};
const fetchRepos = async (sort: string): Promise<Repository[]> => {
const fetchRepos = async (sort) => {
// https://docs.github.com/en/graphql/reference/objects#repository
const { user } = await graphql<GraphQlQueryResponseData>(
const { user } = await graphql(
`
query ($username: String!, $sort: String, $limit: Int) {
user(login: $username) {
@@ -90,18 +86,15 @@ const fetchRepos = async (sort: string): Promise<Repository[]> => {
}
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
const repos: Repository[] = user.repositories.edges.map(
({ node: repo }: { [key: string]: Readonly<GHRepoSchema> }) => ({
name: repo.name,
url: repo.url,
description: repo.description,
updatedAt: new Date(repo.pushedAt),
stars: repo.stargazerCount,
forks: repo.forkCount,
language: repo.primaryLanguage,
})
);
const repos = user.repositories.edges.map(({ node: repo }) => ({
name: repo.name,
url: repo.url,
description: repo.description,
updatedAt: new Date(repo.pushedAt),
stars: repo.stargazerCount,
forks: repo.forkCount,
language: repo.primaryLanguage,
}));
return repos;
};

View File

@@ -2,18 +2,9 @@
// Heavily inspired by @leerob: https://leerob.io/snippets/spotify
import * as Sentry from "@sentry/node";
import { VercelRequest, VercelResponse } from "@vercel/node";
import fetch from "node-fetch";
import * as queryString from "query-string";
import type {
Track,
SpotifyTrackSchema,
SpotifyActivitySchema,
SpotifyTokenSchema,
SpotifyTopSchema,
} from "./types/tracks";
const { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET, SPOTIFY_REFRESH_TOKEN } = process.env;
const basic = Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString("base64");
@@ -30,8 +21,7 @@ Sentry.init({
environment: process.env.NODE_ENV || process.env.VERCEL_ENV || "",
});
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export default async (req: VercelRequest, res: VercelResponse) => {
export default async (req, res) => {
try {
// some rudimentary error handling
if (req.method !== "GET") {
@@ -80,7 +70,7 @@ export default async (req: VercelRequest, res: VercelResponse) => {
}
};
const getAccessToken = async (): Promise<SpotifyTokenSchema> => {
const getAccessToken = async () => {
const response = await fetch(TOKEN_ENDPOINT, {
method: "POST",
headers: {
@@ -93,10 +83,10 @@ const getAccessToken = async (): Promise<SpotifyTokenSchema> => {
}),
});
return response.json() as Promise<SpotifyTokenSchema>;
return response.json();
};
const getNowPlaying = async (): Promise<Track> => {
const getNowPlaying = async () => {
const { access_token } = await getAccessToken();
const response = await fetch(NOW_PLAYING_ENDPOINT, {
@@ -112,7 +102,7 @@ const getNowPlaying = async (): Promise<Track> => {
return { isPlaying: false };
}
const active = (await response.json()) as SpotifyActivitySchema;
const active = await response.json();
if (active.is_playing === true && active.item) {
return {
@@ -128,7 +118,7 @@ const getNowPlaying = async (): Promise<Track> => {
}
};
const getTopTracks = async (): Promise<Track[]> => {
const getTopTracks = async () => {
const { access_token } = await getAccessToken();
const response = await fetch(TOP_TRACKS_ENDPOINT, {
@@ -140,9 +130,9 @@ const getTopTracks = async (): Promise<Track[]> => {
},
});
const { items } = (await response.json()) as SpotifyTopSchema;
const { items } = await response.json();
const tracks: Track[] = items.map((track: Readonly<SpotifyTrackSchema>) => ({
const tracks = items.map((track) => ({
artist: track.artists.map((_artist) => _artist.name).join(", "),
title: track.name,
album: track.album.name,

14
api/types/hits.d.ts vendored
View File

@@ -1,14 +0,0 @@
export type PageStats = {
slug: string;
hits: number;
title?: string;
url?: URL;
date?: Date;
};
export type OverallStats = {
total: {
hits: number;
};
pages: PageStats[];
};

View File

@@ -1,21 +0,0 @@
import type { Language } from "@octokit/graphql-schema";
type BaseRepoInfo = {
name: string;
url: URL;
description: string;
};
export type GHRepoSchema = Required<BaseRepoInfo> & {
primaryLanguage?: Language;
stargazerCount: number;
forkCount: number;
pushedAt: Date;
};
export type Repository = Required<BaseRepoInfo> & {
language?: Language;
stars: number;
forks: number;
updatedAt: Date;
};

42
api/types/tracks.d.ts vendored
View File

@@ -1,42 +0,0 @@
export type SpotifyTrackSchema = {
name: string;
artists: Array<{
name: string;
}>;
album: {
name: string;
images?: Array<{
url: URL;
}>;
};
imageUrl?: URL;
external_urls: {
spotify: URL;
};
};
export type SpotifyActivitySchema = {
is_playing: boolean;
item?: SpotifyTrackSchema;
};
export type SpotifyTokenSchema = {
access_token: string;
token_type: string;
scope: string;
expires_in: number;
refresh_token: string;
};
export type SpotifyTopSchema = {
items: SpotifyTrackSchema[];
};
export type Track = {
isPlaying?: boolean;
artist?: string;
title?: string;
album?: string;
imageUrl?: URL;
songUrl?: URL;
};