diff --git a/controllers/apiCall.js b/controllers/apiCall.js index 5588616..f45ad46 100644 --- a/controllers/apiCall.js +++ b/controllers/apiCall.js @@ -5,16 +5,18 @@ const typedefs = require("../typedefs"); const { axiosInstance } = require("../utils/axios"); +const logPrefix = "Spotify API: "; + /** * Spotify API - one-off request handler - * @param {typedefs.Req} req needed for auto-placing headers from middleware - * @param {typedefs.Res} res handle failure responses here itself - * @param {typedefs.AxiosMethod} method HTTP method + * @param {typedefs.Req} req convenient auto-placing headers from middleware (not a good approach?) + * @param {typedefs.Res} res handle failure responses here itself (not a good approach?) + * @param {import('axios').Method} method HTTP method * @param {string} path request path - * @param {typedefs.AxiosRequestConfig} config request params, headers, etc. + * @param {import('axios').AxiosRequestConfig} config request params, headers, etc. * @param {any} data request body * @param {boolean} inlineData true if data is to be placed inside config - * @returns {Promise<{ success: boolean, resp?: any }>} + * @returns {Promise<{ success: boolean, resp: any }>} */ const singleRequest = async (req, res, method, path, config = {}, data = null, inlineData = false) => { let resp; @@ -24,30 +26,44 @@ const singleRequest = async (req, res, method, path, config = {}, data = null, i if (data) config.data = data ?? null; resp = await axiosInstance[method.toLowerCase()](path, config); - } else { + } else resp = await axiosInstance[method.toLowerCase()](path, data, config); - } - if (resp.status >= 400 && resp.status < 500) { - res.status(resp.status).send(resp.data); - logger.debug("4XX Response", { resp }); - return { success: false }; - } - else if (resp.status >= 500) { - res.sendStatus(resp.status); - logger.warn("5XX Response", { resp }); - return { success: false }; - } - - logger.debug("Response received."); + logger.debug(logPrefix + "Successful response received."); return { success: true, resp }; } catch (error) { - res.sendStatus(500); - logger.error("Request threw an error.", { error }); + if (error.response) { + // Non 2XX response received + let logMsg; + if (error.response.status >= 400 && error.response.status < 600) { + res.status(error.response.status).send(error.response.data); + logMsg = '' + error.response.status + } + else { + res.sendStatus(error.response.status); + logMsg = "???"; + } + logger.error(logPrefix + logMsg, { + response: { + data: error.response.data, + status: error.response.status, + } + }); + } else if (error.request) { + // No response received + res.sendStatus(504); + logger.warn(logPrefix + "No response"); + } else { + // Something happened in setting up the request that triggered an Error + res.sendStatus(500); + logger.error(logPrefix + "Request failed?"); + } + return { success: false }; - } + }; } + module.exports = { singleRequest, } \ No newline at end of file diff --git a/controllers/auth.js b/controllers/auth.js index 9705f51..3d8e317 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -1,9 +1,10 @@ const { authInstance, axiosInstance } = require("../utils/axios"); const typedefs = require("../typedefs"); -const { scopes, stateKey, accountsAPIURL, sessionAgeInSeconds } = require('../constants'); +const { scopes, stateKey, accountsAPIURL, sessionAgeInSeconds, sessionName } = require('../constants'); const generateRandString = require('../utils/generateRandString'); +const { singleRequest } = require("./apiCall"); const logger = require('../utils/logger')(module); /** @@ -17,7 +18,7 @@ const login = (_req, res) => { res.cookie(stateKey, state); const scope = Object.values(scopes).join(' '); - return res.redirect( + res.redirect( `${accountsAPIURL}/authorize?` + new URLSearchParams({ response_type: 'code', @@ -27,9 +28,11 @@ const login = (_req, res) => { state: state }).toString() ); + return; } catch (error) { + res.sendStatus(500); logger.error('login', { error }); - return res.sendStatus(500); + return; } } @@ -45,11 +48,13 @@ const callback = async (req, res) => { // check state if (state === null || state !== storedState) { + res.redirect(409, '/'); logger.error('state mismatch'); - return res.redirect(409, '/'); + return; } else if (error) { + res.status(401).send("Auth callback error"); logger.error('callback error', { error }); - return res.status(401).send("Auth callback error"); + return; } else { // get auth tokens res.clearCookie(stateKey); @@ -65,7 +70,7 @@ const callback = async (req, res) => { const tokenResponse = await authInstance.post('/api/token', authPayload); if (tokenResponse.status === 200) { - logger.info('New login.'); + logger.debug('Tokens obtained.'); req.session.accessToken = tokenResponse.data.access_token; req.session.refreshToken = tokenResponse.data.refresh_token; req.session.cookie.maxAge = 7 * 24 * 60 * 60 * 1000 // 1 week @@ -74,30 +79,27 @@ const callback = async (req, res) => { res.status(tokenResponse.status).send('Error: Login failed'); } - const userResponse = await axiosInstance.get( - "/me", - { - headers: { - 'Authorization': `Bearer ${req.session.accessToken}` - } - } + const userResp = await singleRequest(req, res, + "GET", "/me", + { headers: { Authorization: `Bearer ${req.session.accessToken}` } } ); - if (userResponse.status >= 400 && userResponse.status < 500) - return res.status(userResponse.status).send(userResponse.data); - else if (userResponse.status >= 500) - return res.sendStatus(userResponse.status); + if (!userResp.success) return; + const userData = userResp.resp.data; /** @type {typedefs.User} */ req.session.user = { - username: userResponse.data.display_name, - id: userResponse.data.id, + username: userData.display_name, + id: userData.id, }; - return res.sendStatus(200); + res.sendStatus(200); + logger.info("New login.", { username: userData.display_name }); + return; } } catch (error) { + res.sendStatus(500); logger.error('callback', { error }); - return res.sendStatus(500); + return; } } @@ -121,15 +123,18 @@ const refresh = async (req, res) => { req.session.accessToken = response.data.access_token; req.session.refreshToken = response.data.refresh_token ?? req.session.refreshToken; // refresh token rotation + res.sendStatus(200); logger.info(`Access token refreshed${(response.data.refresh_token !== null) ? ' and refresh token updated' : ''}.`); - return res.sendStatus(200); + return; } else { + res.status(response.status).send('Error: Refresh token flow failed.'); logger.error('refresh failed', { statusCode: response.status }); - return res.status(response.status).send('Error: Refresh token flow failed.'); + return; } } catch (error) { + res.sendStatus(500); logger.error('refresh', { error }); - return res.sendStatus(500); + return; } }; @@ -142,17 +147,20 @@ const logout = async (req, res) => { try { const delSession = req.session.destroy((err) => { if (err) { + res.sendStatus(500); logger.error("Error while logging out", { err }); - return res.sendStatus(500); + return; } else { + res.clearCookie(sessionName); + res.sendStatus(200); logger.info("Logged out.", { sessionID: delSession.id }); - res.clearCookie("connect.sid"); - return res.sendStatus(200); + return; } }) } catch (error) { + res.sendStatus(500); logger.error('logout', { error }); - return res.sendStatus(500); + return; } } diff --git a/controllers/operations.js b/controllers/operations.js index 2f66a30..4c2f070 100644 --- a/controllers/operations.js +++ b/controllers/operations.js @@ -1,12 +1,11 @@ const typedefs = require("../typedefs"); const logger = require("../utils/logger")(module); -const { axiosInstance } = require("../utils/axios"); -const myGraph = require("../utils/graph"); +const { singleRequest } = require("./apiCall"); const { parseSpotifyLink } = require("../utils/spotifyURITransformer"); +const myGraph = require("../utils/graph"); const { Op } = require("sequelize"); -const { singleRequest } = require("./apiCall"); /** @type {typedefs.Model} */ const Playlists = require("../models").playlists; /** @type {typedefs.Model} */ @@ -23,43 +22,34 @@ const updateUser = async (req, res) => { const uID = req.session.user.id; // get first 50 - const response = await axiosInstance.get( + const response = await singleRequest(req, res, + "GET", `/users/${uID}/playlists`, { params: { offset: 0, limit: 50, }, - headers: req.sessHeaders - } - ); + }); + if (!response.success) return; + const respData = response.resp.data; - if (response.status >= 400 && response.status < 500) - return res.status(response.status).send(response.data); - else if (response.status >= 500) - return res.sendStatus(response.status); - - currentPlaylists = response.data.items.map(playlist => { + currentPlaylists = respData.items.map(playlist => { return { playlistID: playlist.id, playlistName: playlist.name } }); - nextURL = response.data.next; + let nextURL = respData.next; // keep getting batches of 50 till exhausted while (nextURL) { - const nextResponse = await axiosInstance.get( - nextURL, // absolute URL from previous response which has params - { headers: req.sessHeaders } - ); - if (response.status >= 400 && response.status < 500) - return res.status(response.status).send(response.data); - else if (response.status >= 500) - return res.sendStatus(response.status); + const nextResp = await singleRequest(req, res, "GET", nextURL); + if (!nextResp.success) return; + const nextData = nextResp.resp.data; currentPlaylists.push( - ...nextResponse.data.items.map(playlist => { + ...nextData.items.map(playlist => { return { playlistID: playlist.id, playlistName: playlist.name @@ -67,7 +57,7 @@ const updateUser = async (req, res) => { }) ); - nextURL = nextResponse.data.next; + nextURL = nextData.next; } let oldPlaylists = await Playlists.findAll({ @@ -117,8 +107,9 @@ const updateUser = async (req, res) => { where: { playlistID: toRemovePlIDs } }); if (cleanedUser !== toRemovePls.length) { + res.sendStatus(500); logger.error("Could not remove all old playlists", { error: new Error("Playlists.destroy failed?") }); - return res.sendStatus(500); + return; } } @@ -128,16 +119,19 @@ const updateUser = async (req, res) => { { validate: true } ); if (updatedUser.length !== toAddPls.length) { + res.sendStatus(500); logger.error("Could not add all new playlists", { error: new Error("Playlists.bulkCreate failed?") }); - return res.sendStatus(500); + return; } } + res.status(200).send({ removedLinks }); logger.info("Updated user data", { delLinks: removedLinks, delPls: cleanedUser, addPls: updatedUser.length }); - return res.status(200).send({ removedLinks }); + return; } catch (error) { + res.sendStatus(500); logger.error('updateUser', { error }); - return res.sendStatus(500); + return; } } @@ -166,14 +160,16 @@ const fetchUser = async (req, res) => { }, }); - logger.info("Fetched user data", { pls: currentPlaylists.length, links: currentLinks.length }); - return res.status(200).send({ + res.status(200).send({ playlists: currentPlaylists, links: currentLinks }); + logger.info("Fetched user data", { pls: currentPlaylists.length, links: currentLinks.length }); + return; } catch (error) { + res.sendStatus(500); logger.error('fetchUser', { error }); - return res.sendStatus(500); + return; } } @@ -191,11 +187,15 @@ const createLink = async (req, res) => { fromPl = parseSpotifyLink(req.body["from"]); toPl = parseSpotifyLink(req.body["to"]); if (fromPl.type !== "playlist" || toPl.type !== "playlist") { - return res.status(400).send({ message: "Link is not a playlist" }); + res.status(400).send({ message: "Link is not a playlist" }); + logger.warn("non-playlist link provided", { from: fromPl, to: toPl }); + + return; } } catch (error) { + res.status(400).send({ message: "Invalid Spotify playlist link" }); logger.error("parseSpotifyLink", { error }); - return res.status(400).send({ message: "Invalid Spotify playlist link" }); + return; } let playlists = await Playlists.findAll({ @@ -207,8 +207,9 @@ const createLink = async (req, res) => { // if playlists are unknown if (![fromPl, toPl].every(pl => playlists.includes(pl.id))) { + res.sendStatus(404); logger.error("unknown playlists, resync"); - return res.sendStatus(404); + return; } // check if exists @@ -222,8 +223,9 @@ const createLink = async (req, res) => { } }); if (existingLink) { + res.sendStatus(409); logger.error("link already exists"); - return res.sendStatus(409); + return; } const allLinks = await Links.findAll({ @@ -235,8 +237,9 @@ const createLink = async (req, res) => { const newGraph = new myGraph(playlists, [...allLinks, { from: fromPl.id, to: toPl.id }]); if (newGraph.detectCycle()) { + res.status(400).send({ message: "Proposed link cannot cause a cycle in the graph" }); logger.error("potential cycle detected"); - return res.status(400).send({ message: "Proposed link cannot cause a cycle in the graph" }); + return; } const newLink = await Links.create({ @@ -245,15 +248,18 @@ const createLink = async (req, res) => { to: toPl.id }); if (!newLink) { + res.sendStatus(500); logger.error("Could not create link", { error: new Error("Links.create failed?") }); - return res.sendStatus(500); + return; } + res.sendStatus(201); logger.info("Created link"); - return res.sendStatus(201); + return; } catch (error) { + res.sendStatus(500); logger.error('createLink', { error }); - return res.sendStatus(500); + return; } } @@ -272,11 +278,14 @@ const removeLink = async (req, res) => { fromPl = parseSpotifyLink(req.body["from"]); toPl = parseSpotifyLink(req.body["to"]); if (fromPl.type !== "playlist" || toPl.type !== "playlist") { - return res.status(400).send({ message: "Link is not a playlist" }); + res.status(400).send({ message: "Link is not a playlist" }); + logger.warn("non-playlist link provided", { from: fromPl, to: toPl }); + return; } } catch (error) { + res.status(400).send({ message: "Invalid Spotify playlist link" }); logger.error("parseSpotifyLink", { error }); - return res.status(400).send({ message: "Invalid Spotify playlist link" }); + return; } // check if exists @@ -290,8 +299,9 @@ const removeLink = async (req, res) => { } }); if (!existingLink) { + res.sendStatus(409); logger.error("link does not exist"); - return res.sendStatus(409); + return; } const removedLink = await Links.destroy({ @@ -304,15 +314,18 @@ const removeLink = async (req, res) => { } }); if (!removedLink) { + res.sendStatus(500); logger.error("Could not remove link", { error: new Error("Links.destroy failed?") }); - return res.sendStatus(500); + return; } + res.sendStatus(200); logger.info("Deleted link"); - return res.sendStatus(200); + return; } catch (error) { + res.sendStatus(500); logger.error('removeLink', { error }); - return res.sendStatus(500); + return; } } @@ -397,28 +410,26 @@ const populateMissingInLink = async (req, res) => { let initialFields = ["tracks(next,items(is_local,track(uri)))"]; let mainFields = ["next", "items(is_local,track(uri))"]; - const fromData = await axiosInstance.get( + + const fromResp = await singleRequest(req, res, + "GET", `/playlists/${fromPl.id}/`, { params: { fields: initialFields.join() }, - headers: req.sessHeaders - } - ); - if (fromData.status >= 400 && fromData.status < 500) - return res.status(fromData.status).send(fromData.data); - else if (fromData.status >= 500) - return res.sendStatus(fromData.status); + }); + if (!fromResp.success) return; + const fromData = fromResp.resp.data; let fromPlaylist = {}; // varying fields again smh - if (fromData.data.tracks.next) { - fromPlaylist.next = new URL(fromData.data.tracks.next); + if (fromData.tracks.next) { + fromPlaylist.next = new URL(fromData.tracks.next); fromPlaylist.next.searchParams.set("fields", mainFields.join()); fromPlaylist.next = fromPlaylist.next.href; } - fromPlaylist.tracks = fromData.data.tracks.items.map((playlist_item) => { + fromPlaylist.tracks = fromData.tracks.items.map((playlist_item) => { return { is_local: playlist_item.is_local, uri: playlist_item.track.uri @@ -428,18 +439,13 @@ const populateMissingInLink = async (req, res) => { // keep getting batches of 50 till exhausted while (fromPlaylist.next) { - const nextResponse = await axiosInstance.get( - fromPlaylist.next, // absolute URL from previous response which has params - { headers: req.sessHeaders } - ); - - if (nextResponse.status >= 400 && nextResponse.status < 500) - return res.status(nextResponse.status).send(nextResponse.data); - else if (nextResponse.status >= 500) - return res.sendStatus(nextResponse.status); + const nextResp = await singleRequest(req, res, + "GET", fromPlaylist.next); + if (!nextResp.success) return; + const nextData = nextResp.resp.data; fromPlaylist.tracks.push( - ...nextResponse.data.items.map((playlist_item) => { + ...nextData.items.map((playlist_item) => { return { is_local: playlist_item.is_local, uri: playlist_item.track.uri @@ -447,32 +453,30 @@ const populateMissingInLink = async (req, res) => { }) ); - fromPlaylist.next = nextResponse.data.next; + fromPlaylist.next = nextData.next; } delete fromPlaylist.next; - const toData = await axiosInstance.get( + + const toResp = await singleRequest(req, res, + "GET", `/playlists/${toPl.id}/`, { params: { fields: initialFields.join() }, - headers: req.sessHeaders - } - ); - if (toData.status >= 400 && toData.status < 500) - return res.status(toData.status).send(toData.data); - else if (toData.status >= 500) - return res.sendStatus(toData.status); + }); + if (!toResp.success) return; + const toData = toResp.resp.data; let toPlaylist = {}; // varying fields again smh - if (toData.data.tracks.next) { - toPlaylist.next = new URL(toData.data.tracks.next); + if (toData.tracks.next) { + toPlaylist.next = new URL(toData.tracks.next); toPlaylist.next.searchParams.set("fields", mainFields.join()); toPlaylist.next = toPlaylist.next.href; } - toPlaylist.tracks = toData.data.tracks.items.map((playlist_item) => { + toPlaylist.tracks = toData.tracks.items.map((playlist_item) => { return { is_local: playlist_item.is_local, uri: playlist_item.track.uri @@ -481,18 +485,12 @@ const populateMissingInLink = async (req, res) => { // keep getting batches of 50 till exhausted while (toPlaylist.next) { - const nextResponse = await axiosInstance.get( - toPlaylist.next, // absolute URL from previous response which has params - { headers: req.sessHeaders } - ); - - if (nextResponse.status >= 400 && nextResponse.status < 500) - return res.status(nextResponse.status).send(nextResponse.data); - else if (nextResponse.status >= 500) - return res.sendStatus(nextResponse.status); - + const nextResp = await singleRequest(req, res, + "GET", toPlaylist.next); + if (!nextResp.success) return; + const nextData = nextResp.resp.data; toPlaylist.tracks.push( - ...nextResponse.data.items.map((playlist_item) => { + ...nextData.items.map((playlist_item) => { return { is_local: playlist_item.is_local, uri: playlist_item.track.uri @@ -500,7 +498,7 @@ const populateMissingInLink = async (req, res) => { }) ); - toPlaylist.next = nextResponse.data.next; + toPlaylist.next = nextData.next; } delete toPlaylist.next; @@ -613,50 +611,42 @@ const pruneExcessInLink = async (req, res) => { let initialFields = ["snapshot_id", "tracks(next,items(is_local,track(uri)))"]; let mainFields = ["next", "items(is_local,track(uri))"]; - const fromData = await axiosInstance.get( + + const fromResp = await singleRequest(req, res, + "GET", `/playlists/${fromPl.id}/`, { params: { fields: initialFields.join() }, - headers: req.sessHeaders - } - ); - if (fromData.status >= 400 && fromData.status < 500) - return res.status(fromData.status).send(fromData.data); - else if (fromData.status >= 500) - return res.sendStatus(fromData.status); + }); + if (!fromResp.success) return; + const fromData = fromResp.resp.data; let fromPlaylist = {}; // varying fields again smh - fromPlaylist.snapshot_id = fromData.data.snapshot_id; - if (fromData.data.tracks.next) { - fromPlaylist.next = new URL(fromData.data.tracks.next); + fromPlaylist.snapshot_id = fromData.snapshot_id; + if (fromData.tracks.next) { + fromPlaylist.next = new URL(fromData.tracks.next); fromPlaylist.next.searchParams.set("fields", mainFields.join()); fromPlaylist.next = fromPlaylist.next.href; } - fromPlaylist.tracks = fromData.data.tracks.items.map((playlist_item) => { + fromPlaylist.tracks = fromData.tracks.items.map((playlist_item) => { return { is_local: playlist_item.is_local, uri: playlist_item.track.uri } }); - // keep getting batches of 50 till exhausted while (fromPlaylist.next) { - const nextResponse = await axiosInstance.get( - fromPlaylist.next, // absolute URL from previous response which has params - { headers: req.sessHeaders } - ); - - if (nextResponse.status >= 400 && nextResponse.status < 500) - return res.status(nextResponse.status).send(nextResponse.data); - else if (nextResponse.status >= 500) - return res.sendStatus(nextResponse.status); + const nextResp = await singleRequest(req, res, + "GET", fromPlaylist.next); + if (!nextResp.success) return; + const nextData = nextResp.resp.data; fromPlaylist.tracks.push( - ...nextResponse.data.items.map((playlist_item) => { + ...nextData.items.map((playlist_item) => { return { is_local: playlist_item.is_local, uri: playlist_item.track.uri @@ -664,33 +654,29 @@ const pruneExcessInLink = async (req, res) => { }) ); - fromPlaylist.next = nextResponse.data.next; + fromPlaylist.next = nextData.next; } delete fromPlaylist.next; - const toData = await axiosInstance.get( + const toResp = await singleRequest(req, res, + "GET", `/playlists/${toPl.id}/`, { params: { fields: initialFields.join() }, - headers: req.sessHeaders - } - ); - if (toData.status >= 400 && toData.status < 500) - return res.status(toData.status).send(toData.data); - else if (toData.status >= 500) - return res.sendStatus(toData.status); - + }); + if (!toResp.success) return; + const toData = toResp.resp.data; let toPlaylist = {}; // varying fields again smh - toPlaylist.snapshot_id = toData.data.snapshot_id; - if (toData.data.tracks.next) { - toPlaylist.next = new URL(toData.data.tracks.next); + toPlaylist.snapshot_id = toData.snapshot_id; + if (toData.tracks.next) { + toPlaylist.next = new URL(toData.tracks.next); toPlaylist.next.searchParams.set("fields", mainFields.join()); toPlaylist.next = toPlaylist.next.href; } - toPlaylist.tracks = toData.data.tracks.items.map((playlist_item) => { + toPlaylist.tracks = toData.tracks.items.map((playlist_item) => { return { is_local: playlist_item.is_local, uri: playlist_item.track.uri @@ -699,18 +685,13 @@ const pruneExcessInLink = async (req, res) => { // keep getting batches of 50 till exhausted while (toPlaylist.next) { - const nextResponse = await axiosInstance.get( - toPlaylist.next, // absolute URL from previous response which has params - { headers: req.sessHeaders } - ); - - if (nextResponse.status >= 400 && nextResponse.status < 500) - return res.status(nextResponse.status).send(nextResponse.data); - else if (nextResponse.status >= 500) - return res.sendStatus(nextResponse.status); + const nextResp = await singleRequest(req, res, + "GET", toPlaylist.next); + if (!nextResp.success) return; + const nextData = nextResp.resp.data; toPlaylist.tracks.push( - ...nextResponse.data.items.map((playlist_item) => { + ...nextData.items.map((playlist_item) => { return { is_local: playlist_item.is_local, uri: playlist_item.track.uri @@ -718,7 +699,7 @@ const pruneExcessInLink = async (req, res) => { }) ); - toPlaylist.next = nextResponse.data.next; + toPlaylist.next = nextData.next; } delete toPlaylist.next; diff --git a/controllers/playlists.js b/controllers/playlists.js index 843f7a8..40361c5 100644 --- a/controllers/playlists.js +++ b/controllers/playlists.js @@ -1,7 +1,7 @@ const logger = require("../utils/logger")(module); const typedefs = require("../typedefs"); -const { axiosInstance } = require('../utils/axios'); +const { singleRequest } = require("./apiCall"); const { parseSpotifyLink } = require("../utils/spotifyURITransformer"); /** @@ -14,26 +14,21 @@ const getUserPlaylists = async (req, res) => { let userPlaylists = {}; // get first 50 - const response = await axiosInstance.get( + const response = await singleRequest(req, res, + "GET", `/users/${req.session.user.id}/playlists`, { params: { offset: 0, - limit: 50, + limit: 100, }, - headers: req.sessHeaders - } - ); + }); + if (!response.success) return; + const respData = response.resp.data; - if (response.status >= 400 && response.status < 500) - return res.status(response.status).send(response.data); - else if (response.status >= 500) - return res.sendStatus(response.status); + userPlaylists.total = respData.total; - userPlaylists.total = response.data.total; - - /** @type {typedefs.SimplifiedPlaylist[]} */ - userPlaylists.items = response.data.items.map((playlist) => { + userPlaylists.items = respData.items.map((playlist) => { return { uri: playlist.uri, images: playlist.images, @@ -42,21 +37,16 @@ const getUserPlaylists = async (req, res) => { } }); - userPlaylists.next = response.data.next; - + userPlaylists.next = respData.next; // keep getting batches of 50 till exhausted while (userPlaylists.next) { - const nextResponse = await axiosInstance.get( - userPlaylists.next, // absolute URL from previous response which has params - { headers: req.sessHeaders } - ); - if (response.status >= 400 && response.status < 500) - return res.status(response.status).send(response.data); - else if (response.status >= 500) - return res.sendStatus(response.status); + const nextResp = await singleRequest(req, res, + "GET", userPlaylists.next); + if (!nextResp.success) return; + const nextData = nextResp.resp.data; userPlaylists.items.push( - ...nextResponse.data.items.map((playlist) => { + ...nextData.items.map((playlist) => { return { uri: playlist.uri, images: playlist.images, @@ -66,7 +56,7 @@ const getUserPlaylists = async (req, res) => { }) ); - userPlaylists.next = nextResponse.data.next; + userPlaylists.next = nextData.next; } delete userPlaylists.next; @@ -75,8 +65,9 @@ const getUserPlaylists = async (req, res) => { logger.debug("Fetched user's playlists", { num: userPlaylists.total }); return; } catch (error) { + res.sendStatus(500); logger.error('getUserPlaylists', { error }); - return res.sendStatus(500); + return; } } @@ -87,7 +78,7 @@ const getUserPlaylists = async (req, res) => { */ const getPlaylistDetails = async (req, res) => { try { - /** @type {typedefs.Playlist} */ + /** @type {typedefs.PlaylistDetails} */ let playlist = {}; /** @type {typedefs.URIObject} */ let uri; @@ -108,39 +99,36 @@ const getPlaylistDetails = async (req, res) => { return; } - const response = await axiosInstance.get( + const response = await singleRequest(req, res, + "GET", `/playlists/${uri.id}/`, { params: { fields: initialFields.join() }, - headers: req.sessHeaders - } - ); - if (response.status >= 400 && response.status < 500) - return res.status(response.status).send(response.data); - else if (response.status >= 500) - return res.sendStatus(response.status); + }); + if (!response.success) return; + const respData = response.resp.data; // TODO: this whole section needs to be DRYer // look into serializr - playlist.name = response.data.name; - playlist.description = response.data.description; - playlist.collaborative = response.data.collaborative; - playlist.public = response.data.public; - playlist.images = { ...response.data.images }; - playlist.owner = { ...response.data.owner }; - playlist.snapshot_id = response.data.snapshot_id; - playlist.total = response.data.tracks.total; + playlist.name = respData.name; + playlist.description = respData.description; + playlist.collaborative = respData.collaborative; + playlist.public = respData.public; + playlist.images = [...respData.images]; + playlist.owner = { ...respData.owner }; + playlist.snapshot_id = respData.snapshot_id; + playlist.total = respData.tracks.total; // previous fields get carried over to the next URL, but most of these fields are not present in the new endpoint // API shouldn't be returning such URLs, the problem's in the API ig... - if (response.data.tracks.next) { - playlist.next = new URL(response.data.tracks.next); + if (respData.tracks.next) { + playlist.next = new URL(respData.tracks.next); playlist.next.searchParams.set("fields", mainFields.join()); playlist.next = playlist.next.href; } - playlist.tracks = response.data.tracks.items.map((playlist_item) => { + playlist.tracks = respData.tracks.items.map((playlist_item) => { return { is_local: playlist_item.is_local, track: { @@ -154,18 +142,14 @@ const getPlaylistDetails = async (req, res) => { // keep getting batches of 50 till exhausted while (playlist.next) { - const nextResponse = await axiosInstance.get( - playlist.next, // absolute URL from previous response which has params - { headers: req.sessHeaders } - ); - - if (nextResponse.status >= 400 && nextResponse.status < 500) - return res.status(nextResponse.status).send(nextResponse.data); - else if (nextResponse.status >= 500) - return res.sendStatus(nextResponse.status); + const nextResp = await singleRequest(req, res, + "GET", playlist.next + ) + if (!nextResp.success) return; + const nextData = nextResp.resp.data; playlist.tracks.push( - ...nextResponse.data.items.map((playlist_item) => { + ...nextData.items.map((playlist_item) => { return { is_local: playlist_item.is_local, track: { @@ -177,7 +161,7 @@ const getPlaylistDetails = async (req, res) => { }) ); - playlist.next = nextResponse.data.next; + playlist.next = nextData.next; } delete playlist.next; diff --git a/index.js b/index.js index cb00514..4d94ed7 100644 --- a/index.js +++ b/index.js @@ -67,7 +67,6 @@ app.use((_req, res) => { const port = process.env.PORT || 3000; const server = app.listen(port, () => { - logger.debug("-", { _: "_".repeat(100) }); logger.info(`App Listening on port ${port}`); }); diff --git a/typedefs.js b/typedefs.js index e2ade3e..5ebe5e9 100644 --- a/typedefs.js +++ b/typedefs.js @@ -5,9 +5,6 @@ * @typedef {import('express').Response} Res * @typedef {import('express').NextFunction} Next * - * @typedef {import('axios').Method} AxiosMethod - * @typedef {import('axios').AxiosRequestConfig} AxiosRequestConfig - * * @typedef {import("sequelize").Sequelize} Sequelize * @typedef {import("sequelize").Model} Model * @typedef {import("sequelize").QueryInterface} QueryInterface @@ -25,7 +22,7 @@ * }} URIObject * * @typedef {{ - * display_name: string, + * username: string, * uri: string * }} User * @@ -70,7 +67,7 @@ * owner: User, * images: ImageObject[], * tracks: PlaylistTrack[], - * }} Playlist + * }} PlaylistDetails */ exports.unused = {}; \ No newline at end of file diff --git a/utils/axios.js b/utils/axios.js index 92b4aab..294d428 100644 --- a/utils/axios.js +++ b/utils/axios.js @@ -25,6 +25,7 @@ axiosInstance.interceptors.request.use(config => { url: config.url, method: config.method, params: config.params ?? {}, + headers: Object.keys(config.headers), }); return config; }); @@ -32,9 +33,15 @@ axiosInstance.interceptors.request.use(config => { axiosInstance.interceptors.response.use( (response) => response, (error) => { - logger.warn("AxiosError", { req: error.config }); - if (error.response) - return error.response; + logger.warn("AxiosError", { + error: { + name: error.name, + code: error.code, + message: error.message, + stack: error.stack, + }, + req: error.config, + }); return Promise.reject(error); } ); diff --git a/utils/logger.js b/utils/logger.js index 9f06351..6b8daba 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -6,27 +6,17 @@ const { colorize, combine, label, timestamp, printf, errors } = format; const typedefs = require("../typedefs"); const getLabel = (callingModule) => { - const parts = callingModule.filename.split(path.sep); + if (!callingModule.filename) return "repl"; + const parts = callingModule.filename?.split(path.sep); return path.join(parts[parts.length - 2], parts.pop()); }; -const allowedErrorKeys = ["name", "message", "stack"]; - -const logMetaReplacer = (key, value) => { - if (key === "error") { - return { - name: value.name, - message: value.message, - stack: value.stack, - }; - } - return value; -} +const allowedErrorKeys = ["name", "code", "message", "stack"]; const metaFormat = (meta) => { if (Object.keys(meta).length > 0) - return '\n' + JSON.stringify(meta, logMetaReplacer, "\t") + '\n'; - return '\n'; + return '\n' + JSON.stringify(meta, null, "\t"); + return ''; } const logFormat = printf(({ level, message, label, timestamp, ...meta }) => { @@ -36,6 +26,9 @@ const logFormat = printf(({ level, message, label, timestamp, ...meta }) => { delete meta.error[key]; } } + const { stack, ...rest } = meta.error; + return `${timestamp} [${label}] ${level}: ${message}${metaFormat(rest)}\n` + + `${stack}`; } return `${timestamp} [${label}] ${level}: ${message}${metaFormat(meta)}`; }); @@ -46,12 +39,12 @@ const logFormat = printf(({ level, message, label, timestamp, ...meta }) => { * @returns {typedefs.Logger} */ const logger = (callingModule) => { - return createLogger({ + let tmpLogger = createLogger({ levels: config.npm.levels, format: combine( errors({ stack: true }), label({ label: getLabel(callingModule) }), - timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }), + timestamp({ format: 'YYYY-MM-DD HH:mm:ss' }), logFormat, ), transports: [ @@ -68,6 +61,8 @@ const logger = (callingModule) => { }), ] }); + tmpLogger.on('error', (error) => tmpLogger.crit("Error inside logger", { error })); + return tmpLogger; } module.exports = logger; \ No newline at end of file diff --git a/validators/index.js b/validators/index.js index 8a2f904..6d5d3b5 100644 --- a/validators/index.js +++ b/validators/index.js @@ -21,10 +21,12 @@ const validate = (req, res, next) => { [err.path]: err.msg })); - return res.status(400).json({ + res.status(400).json({ message: getNestedValuesString(extractedErrors), errors: extractedErrors }); + logger.warn("invalid request", { extractedErrors }); + return; } module.exports = {