bugfixes, improving API request wrapper

This commit is contained in:
Kaushik Narayan R 2025-03-12 22:30:26 -07:00
parent ca1ad74834
commit 7eec2adc7a
11 changed files with 105 additions and 64 deletions

View File

@ -1,5 +1,4 @@
import dotenvFlow from "dotenv-flow";
dotenvFlow.config();
import _ from "./config/dotenv.ts";
import { resolve } from "path";
export default {

View File

@ -1,6 +1,10 @@
import { axiosInstance } from "./axios.ts";
import { type AxiosResponse, type AxiosRequestConfig } from "axios";
import {
type AxiosResponse,
type AxiosRequestConfig,
type RawAxiosRequestHeaders,
} from "axios";
import type {
AddItemsToPlaylist,
EndpointHandlerBaseArgs,
@ -9,7 +13,6 @@ import type {
GetPlaylist,
GetPlaylistItems,
RemovePlaylistItems,
Req,
Res,
} from "spotify_manager/index.d.ts";
@ -34,7 +37,7 @@ enum allowedMethods {
* @param inlineData true if `data` is to be placed inside config (say, axios' delete method)
*/
const singleRequest = async <RespDataType>(
req: Req,
authHeaders: RawAxiosRequestHeaders,
res: Res,
method: allowedMethods,
path: string,
@ -43,7 +46,7 @@ const singleRequest = async <RespDataType>(
inlineData: boolean = false
): Promise<AxiosResponse<RespDataType, any> | null> => {
let resp: AxiosResponse<RespDataType, any>;
config.headers = { ...config.headers, ...req.session.authHeaders };
config.headers = { ...config.headers, ...authHeaders };
try {
if (!data || inlineData) {
if (data) config.data = data ?? null;
@ -87,15 +90,12 @@ const singleRequest = async <RespDataType>(
interface GetCurrentUsersProfileArgs extends EndpointHandlerBaseArgs {}
const getCurrentUsersProfile: (
opts: GetCurrentUsersProfileArgs
) => Promise<GetCurrentUsersProfile | null> = async ({ req, res }) => {
) => Promise<GetCurrentUsersProfile | null> = async ({ authHeaders, res }) => {
const response = await singleRequest<GetCurrentUsersProfile>(
req,
authHeaders,
res,
allowedMethods.Get,
"/me",
{
headers: { Authorization: `Bearer ${req.session.accessToken}` },
}
"/me"
);
return response ? response.data : null;
};
@ -104,9 +104,12 @@ interface GetCurrentUsersPlaylistsFirstPageArgs
extends EndpointHandlerBaseArgs {}
const getCurrentUsersPlaylistsFirstPage: (
opts: GetCurrentUsersPlaylistsFirstPageArgs
) => Promise<GetCurrentUsersPlaylists | null> = async ({ req, res }) => {
) => Promise<GetCurrentUsersPlaylists | null> = async ({
authHeaders,
res,
}) => {
const response = await singleRequest<GetCurrentUsersPlaylists>(
req,
authHeaders,
res,
allowedMethods.Get,
`/me/playlists`,
@ -126,12 +129,12 @@ interface GetCurrentUsersPlaylistsNextPageArgs extends EndpointHandlerBaseArgs {
const getCurrentUsersPlaylistsNextPage: (
opts: GetCurrentUsersPlaylistsNextPageArgs
) => Promise<GetCurrentUsersPlaylists | null> = async ({
req,
authHeaders,
res,
nextURL,
}) => {
const response = await singleRequest<GetCurrentUsersPlaylists>(
req,
authHeaders,
res,
allowedMethods.Get,
nextURL
@ -146,13 +149,13 @@ interface GetPlaylistDetailsFirstPageArgs extends EndpointHandlerBaseArgs {
const getPlaylistDetailsFirstPage: (
opts: GetPlaylistDetailsFirstPageArgs
) => Promise<GetPlaylist | null> = async ({
req,
authHeaders,
res,
initialFields,
playlistID,
}) => {
const response = await singleRequest<GetPlaylist>(
req,
authHeaders,
res,
allowedMethods.Get,
`/playlists/${playlistID}/`,
@ -170,9 +173,13 @@ interface GetPlaylistDetailsNextPageArgs extends EndpointHandlerBaseArgs {
}
const getPlaylistDetailsNextPage: (
opts: GetPlaylistDetailsNextPageArgs
) => Promise<GetPlaylistItems | null> = async ({ req, res, nextURL }) => {
) => Promise<GetPlaylistItems | null> = async ({
authHeaders,
res,
nextURL,
}) => {
const response = await singleRequest<GetPlaylistItems>(
req,
authHeaders,
res,
allowedMethods.Get,
nextURL
@ -187,13 +194,13 @@ interface AddItemsToPlaylistArgs extends EndpointHandlerBaseArgs {
const addItemsToPlaylist: (
opts: AddItemsToPlaylistArgs
) => Promise<AddItemsToPlaylist | null> = async ({
req,
authHeaders,
res,
nextBatch,
playlistID,
}) => {
const response = await singleRequest<AddItemsToPlaylist>(
req,
authHeaders,
res,
allowedMethods.Post,
`/playlists/${playlistID}/tracks`,
@ -212,7 +219,7 @@ interface RemovePlaylistItemsArgs extends EndpointHandlerBaseArgs {
const removePlaylistItems: (
opts: RemovePlaylistItemsArgs
) => Promise<RemovePlaylistItems | null> = async ({
req,
authHeaders,
res,
nextBatch,
playlistID,
@ -221,7 +228,7 @@ const removePlaylistItems: (
// API doesn't document this kind of deletion via the 'positions' field
// but see here: https://github.com/spotipy-dev/spotipy/issues/95#issuecomment-2263634801
const response = await singleRequest<RemovePlaylistItems>(
req,
authHeaders,
res,
allowedMethods.Delete,
`/playlists/${playlistID}/tracks`,
@ -243,11 +250,11 @@ interface CheckPlaylistEditableArgs extends EndpointHandlerBaseArgs {
}
const checkPlaylistEditable: (
opts: CheckPlaylistEditableArgs
) => Promise<boolean> = async ({ req, res, playlistID, userID }) => {
) => Promise<boolean> = async ({ authHeaders, res, playlistID, userID }) => {
let checkFields = ["collaborative", "owner(id)"];
const checkFromData = await getPlaylistDetailsFirstPage({
req,
authHeaders,
res,
initialFields: checkFields.join(),
playlistID,

View File

@ -1,7 +1,6 @@
import type { RequestHandler } from "express";
import curriedLogger from "../utils/logger.ts";
const logger = curriedLogger(import.meta.filename);
import logger from "../utils/logger.ts";
const __controller_func: RequestHandler = async (req, res) => {
try {

View File

@ -48,6 +48,7 @@ const callback: RequestHandler = async (req, res) => {
try {
const { code, state, error } = req.query;
const storedState = req.cookies ? req.cookies[stateKey] : null;
let authHeaders;
// check state
if (state === null || state !== storedState) {
@ -76,14 +77,18 @@ const callback: RequestHandler = async (req, res) => {
logger.debug("Tokens obtained.");
req.session.accessToken = tokenResponse.data.access_token;
req.session.refreshToken = tokenResponse.data.refresh_token;
authHeaders = {
Authorization: `Bearer ${req.session.accessToken}`,
};
} else {
logger.error("login failed", { statusCode: tokenResponse.status });
res
.status(tokenResponse.status)
.send({ message: "Error: Login failed" });
return null;
}
const userData = await getCurrentUsersProfile({ req, res });
const userData = await getCurrentUsersProfile({ authHeaders, res });
if (!userData) return null;
req.session.user = {

View File

@ -37,13 +37,20 @@ import logger from "../utils/logger.ts";
*/
const updateUser: RequestHandler = async (req, res) => {
try {
let currentPlaylists: PlaylistModel_Pl[] = [];
if (!req.session.user)
throw new ReferenceError("sessionData does not have user object");
throw new ReferenceError("session does not have user object");
const { authHeaders } = req.session;
if (!authHeaders)
throw new ReferenceError("session does not have auth headers");
const uID = req.session.user.id;
let currentPlaylists: PlaylistModel_Pl[] = [];
// get first 50
const respData = await getCurrentUsersPlaylistsFirstPage({ req, res });
const respData = await getCurrentUsersPlaylistsFirstPage({
authHeaders,
res,
});
if (!respData) return null;
currentPlaylists = respData.items.map((playlist) => {
@ -57,7 +64,7 @@ const updateUser: RequestHandler = async (req, res) => {
// keep getting batches of 50 till exhausted
while (nextURL) {
const nextData = await getCurrentUsersPlaylistsNextPage({
req,
authHeaders,
res,
nextURL,
});
@ -217,7 +224,7 @@ const fetchUser: RequestHandler = async (req, res) => {
// return null;
// }
if (!req.session.user)
throw new ReferenceError("sessionData does not have user object");
throw new ReferenceError("session does not have user object");
const uID = req.session.user.id;
const currentPlaylists = await Playlists.findAll({
@ -259,7 +266,7 @@ const createLink: RequestHandler = async (req, res) => {
try {
// await sleep(1000);
if (!req.session.user)
throw new ReferenceError("sessionData does not have user object");
throw new ReferenceError("session does not have user object");
const uID = req.session.user.id;
let fromPl, toPl;
@ -351,7 +358,7 @@ const createLink: RequestHandler = async (req, res) => {
const removeLink: RequestHandler = async (req, res) => {
try {
if (!req.session.user)
throw new Error("sessionData does not have user object");
throw new ReferenceError("session does not have user object");
const uID = req.session.user.id;
let fromPl, toPl;
@ -416,12 +423,16 @@ interface _GetPlaylistTracks {
}
const _getPlaylistTracks: (
opts: _GetPlaylistTracksArgs
) => Promise<_GetPlaylistTracks | null> = async ({ req, res, playlistID }) => {
) => Promise<_GetPlaylistTracks | null> = async ({
authHeaders,
res,
playlistID,
}) => {
let initialFields = ["snapshot_id,tracks(next,items(is_local,track(uri)))"];
let mainFields = ["next", "items(is_local,track(uri))"];
const respData = await getPlaylistDetailsFirstPage({
req,
authHeaders,
res,
initialFields: initialFields.join(),
playlistID,
@ -460,7 +471,7 @@ const _getPlaylistTracks: (
// keep getting batches of 50 till exhausted
while (nextURL) {
const nextData = await getPlaylistDetailsNextPage({
req,
authHeaders,
res,
nextURL,
});
@ -514,7 +525,7 @@ interface _PopulateSingleLinkCoreArgs extends EndpointHandlerBaseArgs {
const _populateSingleLinkCore: (
opts: _PopulateSingleLinkCoreArgs
) => Promise<{ toAddNum: number; localNum: number } | null> = async ({
req,
authHeaders,
res,
link,
}) => {
@ -523,12 +534,12 @@ const _populateSingleLinkCore: (
toPl = link.to;
const fromPlaylist = await _getPlaylistTracks({
req,
authHeaders,
res,
playlistID: fromPl.id,
});
const toPlaylist = await _getPlaylistTracks({
req,
authHeaders,
res,
playlistID: toPl.id,
});
@ -547,7 +558,7 @@ const _populateSingleLinkCore: (
while (toTrackURIs.length > 0) {
const nextBatch = toTrackURIs.splice(0, 100);
const addData = await addItemsToPlaylist({
req,
authHeaders,
res,
nextBatch,
playlistID: fromPl.id,
@ -566,8 +577,11 @@ const _populateSingleLinkCore: (
const populateSingleLink: RequestHandler = async (req, res) => {
try {
if (!req.session.user)
throw new Error("sessionData does not have user object");
throw new ReferenceError("session does not have user object");
const uID = req.session.user.id;
const { authHeaders } = req.session;
if (!authHeaders)
throw new ReferenceError("session does not have auth headers");
const link = { from: req.body.from, to: req.body.to };
let fromPl, toPl;
@ -599,7 +613,7 @@ const populateSingleLink: RequestHandler = async (req, res) => {
if (
!(await checkPlaylistEditable({
req,
authHeaders,
res,
playlistID: fromPl.id,
userID: uID,
@ -608,7 +622,7 @@ const populateSingleLink: RequestHandler = async (req, res) => {
return null;
const result = await _populateSingleLinkCore({
req,
authHeaders,
res,
link: { from: fromPl, to: toPl },
});
@ -651,18 +665,22 @@ interface _PruneSingleLinkCoreArgs extends EndpointHandlerBaseArgs {
*/
const _pruneSingleLinkCore: (
opts: _PruneSingleLinkCoreArgs
) => Promise<{ toDelNum: number } | null> = async ({ req, res, link }) => {
) => Promise<{ toDelNum: number } | null> = async ({
authHeaders,
res,
link,
}) => {
try {
const fromPl = link.from,
toPl = link.to;
const fromPlaylist = await _getPlaylistTracks({
req,
authHeaders,
res,
playlistID: fromPl.id,
});
const toPlaylist = await _getPlaylistTracks({
req,
authHeaders,
res,
playlistID: toPl.id,
});
@ -684,7 +702,7 @@ const _pruneSingleLinkCore: (
while (indexes.length > 0) {
const nextBatch = indexes.splice(Math.max(indexes.length - 100, 0), 100);
const delResponse = await removePlaylistItems({
req,
authHeaders,
res,
nextBatch,
playlistID: toPl.id,
@ -705,8 +723,11 @@ const _pruneSingleLinkCore: (
const pruneSingleLink: RequestHandler = async (req, res) => {
try {
if (!req.session.user)
throw new Error("sessionData does not have user object");
throw new ReferenceError("session does not have user object");
const uID = req.session.user.id;
const { authHeaders } = req.session;
if (!authHeaders)
throw new ReferenceError("session does not have auth headers");
const link = { from: req.body.from, to: req.body.to };
let fromPl, toPl;
@ -738,7 +759,7 @@ const pruneSingleLink: RequestHandler = async (req, res) => {
if (
!(await checkPlaylistEditable({
req,
authHeaders,
res,
playlistID: toPl.id,
userID: uID,
@ -747,7 +768,7 @@ const pruneSingleLink: RequestHandler = async (req, res) => {
return null;
const result = await _pruneSingleLinkCore({
req,
authHeaders,
res,
link: {
from: fromPl,

View File

@ -16,8 +16,14 @@ import logger from "../utils/logger.ts";
*/
const fetchUserPlaylists: RequestHandler = async (req, res) => {
try {
const { authHeaders } = req.session;
if (!authHeaders)
throw new ReferenceError("session does not have auth headers");
// get first 50
const respData = await getCurrentUsersPlaylistsFirstPage({ req, res });
const respData = await getCurrentUsersPlaylistsFirstPage({
authHeaders,
res,
});
if (!respData) return null;
let tmpData = structuredClone(respData);
@ -32,7 +38,7 @@ const fetchUserPlaylists: RequestHandler = async (req, res) => {
// keep getting batches of 50 till exhausted
while (nextURL) {
const nextData = await getCurrentUsersPlaylistsNextPage({
req,
authHeaders,
res,
nextURL,
});
@ -51,4 +57,5 @@ const fetchUserPlaylists: RequestHandler = async (req, res) => {
return null;
}
};
export { fetchUserPlaylists };

View File

@ -91,7 +91,10 @@ app.use("/health", (_req, res) => {
});
app.use("/auth-health", isAuthenticated, async (req, res) => {
try {
const respData = await getCurrentUsersProfile({ req, res });
const { authHeaders } = req.session;
if (!authHeaders)
throw new ReferenceError("session does not have auth headers");
const respData = await getCurrentUsersProfile({ authHeaders, res });
if (!respData) return null;
res.status(200).send({ message: "OK" });
return null;

View File

@ -1,4 +1,3 @@
import type { AxiosRequestHeaders } from "axios";
import type { RequestHandler } from "express";
import { sessionName } from "../constants.ts";
@ -9,7 +8,7 @@ export const isAuthenticated: RequestHandler = (req, res, next) => {
if (req.session.accessToken) {
req.session.authHeaders = {
Authorization: `Bearer ${req.session.accessToken}`,
} as AxiosRequestHeaders;
};
next();
} else {
const delSession = req.session.destroy((error) => {

View File

@ -1,10 +1,10 @@
import { Router } from "express";
const router: Router = Router();
const playlistRouter: Router = Router();
import { fetchUserPlaylists } from "../controllers/playlists.ts";
// import { validate } from "../validators/index.ts";
router.get("/me", fetchUserPlaylists);
playlistRouter.get("/me", fetchUserPlaylists);
export default router;
export default playlistRouter;

View File

@ -1,4 +1,4 @@
import type { AxiosRequestHeaders } from "axios";
import type { RawAxiosRequestHeaders } from "axios";
import type { User } from "spotify_manager/index.d.ts";
declare module "express-session" {
@ -6,7 +6,7 @@ declare module "express-session" {
interface SessionData {
accessToken: string;
refreshToken: string;
authHeaders: AxiosRequestHeaders;
authHeaders: RawAxiosRequestHeaders;
user: User;
}
}

View File

@ -1,3 +1,4 @@
import type { RawAxiosRequestHeaders } from "axios";
import type { Request, Response, NextFunction } from "express";
export type Req = Request;
@ -5,6 +6,6 @@ export type Res = Response;
export type Next = NextFunction;
export interface EndpointHandlerBaseArgs {
req: Req;
authHeaders: RawAxiosRequestHeaders;
res: Res;
}