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:
@@ -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;
|
||||
};
|
@@ -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;
|
||||
};
|
@@ -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
14
api/types/hits.d.ts
vendored
@@ -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[];
|
||||
};
|
21
api/types/projects.d.ts
vendored
21
api/types/projects.d.ts
vendored
@@ -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
42
api/types/tracks.d.ts
vendored
@@ -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;
|
||||
};
|
Reference in New Issue
Block a user