Skip to content

Commit

Permalink
[Monorepo] put API client code + token code at use site and refactor …
Browse files Browse the repository at this point in the history
…to move out api-clients (#428)

Closes DG-143

## What changed? Why?
Refactors `api/networkClients` to own package (`api-clients`) and
removes non-generic code from there. Adds `invariant` too for future use
and adds it in a few places. Fixes up some ESLint + TSConfig so the
warnings about react not being present are gone!
  • Loading branch information
dgattey committed Dec 30, 2023
1 parent 480f5fa commit 716717f
Show file tree
Hide file tree
Showing 63 changed files with 537 additions and 321 deletions.
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"clean": "rm -rf .next"
},
"dependencies": {
"api-clients": "workspace:*",
"@contentful/rich-text-react-renderer": "15.19.0",
"@contentful/rich-text-types": "16.3.0",
"@emotion/cache": "11.11.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Asset } from 'api/server/contentful/api.generated';
import type { MyLocationQuery } from 'api/server/contentful/fetchCurrentLocation.generated';
import type { Asset } from './api.generated';
import type { MyLocationQuery } from './fetchCurrentLocation.generated';

/**
* Represents a location along with some metadata
Expand Down
15 changes: 15 additions & 0 deletions apps/web/src/api/server/contentful/contentfulClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createClient } from 'api-clients/authenticatedGraphQLClient';
import { invariant } from 'shared-core/helpers/invariant';

const SPACE_ID = process.env.CONTENTFUL_SPACE_ID;
const ACCESS_TOKEN = process.env.CONTENTFUL_ACCESS_TOKEN;
invariant(SPACE_ID, 'Missing CONTENTFUL_SPACE_ID env variable');
invariant(ACCESS_TOKEN, 'Missing CONTENTFUL_ACCESS_TOKEN env variable');

/**
* Use this GraphQL client to make requests to Contentful from the server.
*/
export const contentfulClient = createClient({
endpoint: `https://graphql.contentful.com/content/v1/spaces/${SPACE_ID}`,
accessToken: ACCESS_TOKEN,
});
4 changes: 2 additions & 2 deletions apps/web/src/api/server/contentful/fetchCurrentLocation.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { gql } from 'graphql-request';
import { isNotNullish } from 'shared-core/helpers/typeguards';
import type { MapLocation } from 'api/types/MapLocation';
import { contentfulClient } from '../networkClients/contentfulClient';
import { contentfulClient } from './contentfulClient';
import type { MapLocation } from './MapLocation';
import type { MyLocationQuery } from './fetchCurrentLocation.generated';

/**
Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/api/server/contentful/fetchFooterLinks.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { gql } from 'graphql-request';
import { isNotNullish } from 'shared-core/helpers/typeguards';
import { isLink } from 'api/parsers';
import { contentfulClient } from '../networkClients/contentfulClient';
import { contentfulClient } from './contentfulClient';
import { isLink } from './parsers';
import type { Link } from './api.generated';
import type { FooterQuery } from './fetchFooterLinks.generated';

Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/api/server/contentful/fetchIntroContent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { gql } from 'graphql-request';
import { isTextBlock } from 'api/parsers';
import { contentfulClient } from '../networkClients/contentfulClient';
import { contentfulClient } from './contentfulClient';
import { isTextBlock } from './parsers';
import type { TextBlock } from './api.generated';
import type { IntroBlockQuery } from './fetchIntroContent.generated';

Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/api/server/contentful/fetchPrivacyContent.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { gql } from 'graphql-request';
import { isTextBlock } from 'api/parsers';
import { contentfulClient } from '../networkClients/contentfulClient';
import { contentfulClient } from './contentfulClient';
import { isTextBlock } from './parsers';
import type { TextBlock } from './api.generated';
import type { PrivacyBlockQuery } from './fetchPrivacyContent.generated';

Expand Down
4 changes: 2 additions & 2 deletions apps/web/src/api/server/contentful/fetchProjects.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { gql } from 'graphql-request';
import { isProject } from 'api/parsers';
import { contentfulClient } from '../networkClients/contentfulClient';
import { contentfulClient } from './contentfulClient';
import { isProject } from './parsers';
import type { Project } from './api.generated';
import type { ProjectsQuery } from './fetchProjects.generated';

Expand Down
File renamed without changes.
2 changes: 1 addition & 1 deletion apps/web/src/api/server/github/fetchRepoVersion.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { gql } from 'graphql-request';
import { githubClient } from '../networkClients/githubClient';
import { githubClient } from './githubClient';
import type { GithubRepoVersionQuery } from './fetchRepoVersion.generated';

/**
Expand Down
13 changes: 13 additions & 0 deletions apps/web/src/api/server/github/githubClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { invariant } from 'shared-core/helpers/invariant';
import { createClient } from 'api-clients/authenticatedGraphQLClient';

const ACCESS_TOKEN = process.env.GITHUB_AUTHENTICATION_TOKEN;
invariant(ACCESS_TOKEN, 'Missing GITHUB_AUTHENTICATION_TOKEN env variable');

/**
* Use this GraphQL client to make requests to Github from the server.
*/
export const githubClient = createClient({
endpoint: 'https://api.github.com/graphql',
accessToken: ACCESS_TOKEN,
});

This file was deleted.

43 changes: 0 additions & 43 deletions apps/web/src/api/server/networkClients/authenticatedRestClient.ts

This file was deleted.

15 changes: 0 additions & 15 deletions apps/web/src/api/server/networkClients/contentfulClient.ts

This file was deleted.

13 changes: 0 additions & 13 deletions apps/web/src/api/server/networkClients/githubClient.ts

This file was deleted.

9 changes: 0 additions & 9 deletions apps/web/src/api/server/networkClients/spotifyClient.ts

This file was deleted.

13 changes: 0 additions & 13 deletions apps/web/src/api/server/networkClients/stravaClient.ts

This file was deleted.

File renamed without changes.
8 changes: 4 additions & 4 deletions apps/web/src/api/server/spotify/fetchRecentlyPlayed.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { CurrentlyPlaying } from 'api/types/spotify/CurrentlyPlaying';
import type { RecentlyPlayed } from 'api/types/spotify/RecentlyPlayed';
import type { Track } from 'api/types/spotify/Track';
import { spotifyClient } from '../networkClients/spotifyClient';
import { spotifyClient } from './spotifyClient';
import type { CurrentlyPlaying } from './CurrentlyPlaying';
import type { RecentlyPlayed } from './RecentlyPlayed';
import type { Track } from './Track';

const CURRENTLY_PLAYING_RESOURCE = 'me/player/currently-playing';
const RECENTLY_PLAYED_RESOURCE = 'me/player/recently-played?limit=1';
Expand Down
69 changes: 69 additions & 0 deletions apps/web/src/api/server/spotify/spotifyClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { invariant } from 'shared-core/helpers/invariant';
import { createClient } from 'api-clients/authenticatedRestClient';

/**
* This is what Spotify's refresh token API returns, as raw data
*/
type RawSpotifyRefreshToken = {
token_type: string;
access_token: string;
expires_in: number;
};

/**
* We "expire" Spotify tokens 30 seconds early so we don't run into problems near the end
* of the window. Probably unneeded but it's just math.
*/
const GRACE_PERIOD_IN_MS = 30_000;

/**
* All the env variables we later use
*/
const { SPOTIFY_CLIENT_ID, SPOTIFY_CLIENT_SECRET } = process.env;
invariant(SPOTIFY_CLIENT_ID, 'Missing SPOTIFY_CLIENT_ID env variable');
invariant(SPOTIFY_CLIENT_SECRET, 'Missing SPOTIFY_CLIENT_SECRET env variable');

/**
* Spotify client needs a base 64 encoded string for the client id:secret
*/
const SPOTIFY_CLIENT_AUTH = Buffer.from(`${SPOTIFY_CLIENT_ID}:${SPOTIFY_CLIENT_SECRET}`).toString(
'base64',
);

/**
* Given a number of seconds in which something will expire, this function
* creates a timestamp from that in milliseconds at which things expire.
* We intentionally expire with an extra grace period to ensure round trips +
* other things don't eat up processing time and cause us to miss the expiry timestamp.
*/
function createExpirationDate(expiryDistanceInSeconds: number) {
return new Date(Date.now() - GRACE_PERIOD_IN_MS + expiryDistanceInSeconds * 1000);
}

/**
* A REST client set up to make authed calls to Spotify
*/
export const spotifyClient = createClient({
endpoint: 'https://api.spotify.com/v1',
accessKey: 'spotify',
refreshTokenConfig: {
endpoint: 'https://accounts.spotify.com/api/token',
headers: {
Authorization: `Basic ${SPOTIFY_CLIENT_AUTH}`,
},
validate: (rawData, refreshToken) => {
const { token_type: tokenType, access_token: accessToken, expires_in: expiresIn } =
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
rawData as RawSpotifyRefreshToken;
invariant(tokenType === 'Bearer', `Invalid token type from Spotify ${tokenType}`);
invariant(accessToken, 'Missing access token from Spotify');

// Spotify refresh tokens don't expire + we create our own expiry stamp
return {
refreshToken,
accessToken,
expiryAt: createExpirationDate(expiresIn),
};
},
},
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { StravaDetailedActivity } from 'db/models/StravaDetailedActivity';
import { stravaClient } from 'api/server/networkClients/stravaClient';
import { stravaClient } from 'api/server/strava/stravaClient';
import { paredStravaActivity } from './paredStravaActivity';

/**
Expand Down
54 changes: 54 additions & 0 deletions apps/web/src/api/server/strava/stravaClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { invariant } from 'shared-core/helpers/invariant';
import { createClient } from 'api-clients/authenticatedRestClient';

/**
* This is what Strava's refresh token API returns, as raw data
*/
type RawStravaRefreshToken = {
token_type: string;
access_token: string;
refresh_token: string;
expires_at: number;
expires_in: number;
};

const { STRAVA_CLIENT_ID, STRAVA_CLIENT_SECRET, STRAVA_TOKEN_NAME } = process.env;
invariant(STRAVA_TOKEN_NAME, 'Missing Strava token name (used as access key)');
invariant(STRAVA_CLIENT_ID, 'Missing Strava client id');
invariant(STRAVA_CLIENT_SECRET, 'Missing Strava client secret');

/**
* A REST client set up to make authed calls to Strava
*/
export const stravaClient = createClient({
endpoint: 'https://www.strava.com/api/v3',
accessKey: STRAVA_TOKEN_NAME,
refreshTokenConfig: {
endpoint: 'https://www.strava.com/api/v3/oauth/token',
data: {
client_id: STRAVA_CLIENT_ID,
client_secret: STRAVA_CLIENT_SECRET,
},
validate: (rawData) => {
const {
token_type: tokenType,
refresh_token: refreshToken,
access_token: accessToken,
expires_at: expiresAt,
} =
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
rawData as RawStravaRefreshToken;
invariant(tokenType === 'Bearer', `Invalid token type from Strava ${tokenType}`);
invariant(refreshToken, 'Missing refresh token from Strava');
invariant(accessToken, 'Missing access token from Strava');
invariant(expiresAt, 'Missing expires at from Strava');

return {
refreshToken,
accessToken,
// expiresAt is a timestamp in seconds!
expiryAt: new Date(expiresAt * 1000),
};
},
},
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { db } from 'db';
import type { StravaWebhookEvent } from 'api/types/StravaWebhookEvent';
import type { StravaWebhookEvent } from './StravaWebhookEvent';
import { fetchStravaActivityFromApi } from './fetchStravaActivityFromApi';

// If an update was applied this number of ms or less ago, drop the update
Expand Down
Loading

1 comment on commit 716717f

@vercel
Copy link

@vercel vercel bot commented on 716717f Dec 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

dg – ./

dg.vercel.app
dg-git-main-dgattey.vercel.app
dg-dgattey.vercel.app
dylangattey.com

Please sign in to comment.