CJS -> ESM

This commit is contained in:
Kaushik Narayan R 2025-03-05 12:19:35 -07:00
parent 5a8611afbc
commit bcc39d5f38
33 changed files with 228 additions and 282 deletions

View File

@ -1,6 +1,7 @@
require("dotenv-flow").config(); import dotenvFlow from "dotenv-flow";
dotenvFlow.config();
const path = require("path"); import { resolve } from "path";
module.exports = { export default {
"config": path.resolve("config", "sequelize.js") "config": resolve("config", "sequelize.js")
}; };

View File

@ -1,10 +1,11 @@
const axios = require("axios"); import axios from "axios";
const rateLimit = require("axios-rate-limit"); import rateLimit from "axios-rate-limit";
const { baseAPIURL, accountsAPIURL } = require("../constants"); import { baseAPIURL, accountsAPIURL } from "../constants.js";
const logger = require("../utils/logger")(module); import curriedLogger from "../utils/logger.js";
const logger = curriedLogger(import.meta);
const authInstance = axios.create({ export const authInstance = axios.create({
baseURL: accountsAPIURL, baseURL: accountsAPIURL,
timeout: 20000, timeout: 20000,
headers: { headers: {
@ -21,7 +22,7 @@ const uncappedAxiosInstance = axios.create({
}, },
}); });
const axiosInstance = rateLimit(uncappedAxiosInstance, { export const axiosInstance = rateLimit(uncappedAxiosInstance, {
maxRequests: 10, maxRequests: 10,
perMilliseconds: 5000, perMilliseconds: 5000,
}); });
@ -50,8 +51,3 @@ axiosInstance.interceptors.response.use(
return Promise.reject(error); return Promise.reject(error);
} }
); );
module.exports = {
authInstance,
axiosInstance
};

View File

@ -1,9 +1,10 @@
const logger = require("../utils/logger")(module); import curriedLogger from "../utils/logger.js";
const logger = curriedLogger(import.meta);
const typedefs = require("../typedefs"); import * as typedefs from "../typedefs.js";
const { axiosInstance } = require("./axios"); import { axiosInstance } from "./axios.js";
const logPrefix = "Spotify API: "; const logPrefix = "Spotify API: ";
@ -17,7 +18,7 @@ const logPrefix = "Spotify API: ";
* @param {any} data request body * @param {any} data request body
* @param {boolean} inlineData true if data is to be placed inside config * @param {boolean} inlineData true if data is to be placed inside config
*/ */
const singleRequest = async (req, res, method, path, config = {}, data = null, inlineData = false) => { export const singleRequest = async (req, res, method, path, config = {}, data = null, inlineData = false) => {
let resp; let resp;
config.headers = { ...config.headers, ...req.sessHeaders }; config.headers = { ...config.headers, ...req.sessHeaders };
try { try {
@ -62,7 +63,7 @@ const singleRequest = async (req, res, method, path, config = {}, data = null, i
}; };
} }
const getUserProfile = async (req, res) => { export const getUserProfile = async (req, res) => {
const response = await singleRequest(req, res, const response = await singleRequest(req, res,
"GET", "/me", "GET", "/me",
{ headers: { Authorization: `Bearer ${req.session.accessToken}` } } { headers: { Authorization: `Bearer ${req.session.accessToken}` } }
@ -70,7 +71,7 @@ const getUserProfile = async (req, res) => {
return res.headersSent ? null : response.data; return res.headersSent ? null : response.data;
} }
const getUserPlaylistsFirstPage = async (req, res) => { export const getUserPlaylistsFirstPage = async (req, res) => {
const response = await singleRequest(req, res, const response = await singleRequest(req, res,
"GET", "GET",
`/users/${req.session.user.id}/playlists`, `/users/${req.session.user.id}/playlists`,
@ -83,13 +84,13 @@ const getUserPlaylistsFirstPage = async (req, res) => {
return res.headersSent ? null : response.data; return res.headersSent ? null : response.data;
} }
const getUserPlaylistsNextPage = async (req, res, nextURL) => { export const getUserPlaylistsNextPage = async (req, res, nextURL) => {
const response = await singleRequest( const response = await singleRequest(
req, res, "GET", nextURL); req, res, "GET", nextURL);
return res.headersSent ? null : response.data; return res.headersSent ? null : response.data;
} }
const getPlaylistDetailsFirstPage = async (req, res, initialFields, playlistID) => { export const getPlaylistDetailsFirstPage = async (req, res, initialFields, playlistID) => {
const response = await singleRequest(req, res, const response = await singleRequest(req, res,
"GET", "GET",
`/playlists/${playlistID}/`, `/playlists/${playlistID}/`,
@ -101,13 +102,13 @@ const getPlaylistDetailsFirstPage = async (req, res, initialFields, playlistID)
return res.headersSent ? null : response.data; return res.headersSent ? null : response.data;
} }
const getPlaylistDetailsNextPage = async (req, res, nextURL) => { export const getPlaylistDetailsNextPage = async (req, res, nextURL) => {
const response = await singleRequest( const response = await singleRequest(
req, res, "GET", nextURL); req, res, "GET", nextURL);
return res.headersSent ? null : response.data; return res.headersSent ? null : response.data;
} }
const addItemsToPlaylist = async (req, res, nextBatch, playlistID) => { export const addItemsToPlaylist = async (req, res, nextBatch, playlistID) => {
const response = await singleRequest(req, res, const response = await singleRequest(req, res,
"POST", "POST",
`/playlists/${playlistID}/tracks`, `/playlists/${playlistID}/tracks`,
@ -117,7 +118,7 @@ const addItemsToPlaylist = async (req, res, nextBatch, playlistID) => {
return res.headersSent ? null : response.data; return res.headersSent ? null : response.data;
} }
const removeItemsFromPlaylist = async (req, res, nextBatch, playlistID, snapshotID) => { export const removeItemsFromPlaylist = async (req, res, nextBatch, playlistID, snapshotID) => {
// API doesn't document this kind of deletion via the 'positions' field // 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 // but see here: https://github.com/spotipy-dev/spotipy/issues/95#issuecomment-2263634801
const response = await singleRequest(req, res, const response = await singleRequest(req, res,
@ -130,7 +131,7 @@ const removeItemsFromPlaylist = async (req, res, nextBatch, playlistID, snapshot
return res.headersSent ? null : response.data; return res.headersSent ? null : response.data;
} }
const checkPlaylistEditable = async (req, res, playlistID, userID) => { export const checkPlaylistEditable = async (req, res, playlistID, userID) => {
let checkFields = ["collaborative", "owner(id)"]; let checkFields = ["collaborative", "owner(id)"];
const checkFromData = await getPlaylistDetailsFirstPage(req, res, checkFields.join(), playlistID); const checkFromData = await getPlaylistDetailsFirstPage(req, res, checkFields.join(), playlistID);
@ -150,15 +151,3 @@ const checkPlaylistEditable = async (req, res, playlistID, userID) => {
return true; return true;
} }
} }
module.exports = {
singleRequest,
getUserProfile,
getUserPlaylistsFirstPage,
getUserPlaylistsNextPage,
getPlaylistDetailsFirstPage,
getPlaylistDetailsNextPage,
addItemsToPlaylist,
removeItemsFromPlaylist,
checkPlaylistEditable,
}

View File

@ -1,12 +1,13 @@
const logger = require("../utils/logger")(module); import curriedLogger from "../utils/logger.js";
const logger = curriedLogger(import.meta);
const typedefs = require("../typedefs"); import * as typedefs from "../typedefs.js";
/** /**
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const __controller_func = async (req, res) => { export const __controller_func = async (req, res) => {
try { try {
} catch (error) { } catch (error) {
@ -15,7 +16,3 @@ const __controller_func = async (req, res) => {
return; return;
} }
} }
module.exports = {
__controller_func
};

View File

@ -1,6 +1,7 @@
const router = require("express").Router(); import { Router } from "express";
const router = Router();
const { validate } = require("../validators"); import { validate } from "../validators/index.js";
router.get( router.get(
@ -10,4 +11,4 @@ router.post(
); );
module.exports = router; export default router;

View File

@ -1,13 +1,13 @@
const { body, header, param, query } = require("express-validator"); import { body, header, param, query } from "express-validator";
const typedefs = require("../typedefs"); import * as typedefs from "../typedefs.js";
/** /**
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
* @param {typedefs.Next} next * @param {typedefs.Next} next
*/ */
const __validator_func = async (req, res, next) => { export const __validator_func = async (req, res, next) => {
await body("field_name") await body("field_name")
.notEmpty() .notEmpty()
.withMessage("field_name not defined in body") .withMessage("field_name not defined in body")
@ -15,7 +15,3 @@ const __validator_func = async (req, res, next) => {
next(); next();
} }
module.exports = {
__validator_func
}

3
config/dotenv.js Normal file
View File

@ -0,0 +1,3 @@
// https://github.com/motdotla/dotenv/issues/133#issuecomment-255298822
import DotenvFlow from "dotenv-flow";
export default DotenvFlow.config();

View File

@ -1,4 +1,5 @@
const logger = require("../utils/logger")(module); import curriedLogger from "../utils/logger.js";
const logger = curriedLogger(import.meta);
const connConfigs = { const connConfigs = {
development: { development: {
@ -8,7 +9,7 @@ const connConfigs = {
host: process.env.DB_HOST || "127.0.0.1", host: process.env.DB_HOST || "127.0.0.1",
port: process.env.DB_PORT || 5432, port: process.env.DB_PORT || 5432,
}, },
staging: { test: {
use_env_variable: "DB_URL", // use connection string for non-dev env use_env_variable: "DB_URL", // use connection string for non-dev env
}, },
production: { production: {
@ -25,4 +26,4 @@ for (const conf in connConfigs) {
connConfigs[conf]["dialect"] = process.env.DB_DIALECT || "postgres"; connConfigs[conf]["dialect"] = process.env.DB_DIALECT || "postgres";
} }
module.exports = connConfigs; export default connConfigs;

View File

@ -1,9 +1,9 @@
const accountsAPIURL = "https://accounts.spotify.com"; export const accountsAPIURL = "https://accounts.spotify.com";
const baseAPIURL = "https://api.spotify.com/v1"; export const baseAPIURL = "https://api.spotify.com/v1";
const sessionName = "spotify-manager"; export const sessionName = "spotify-manager";
const stateKey = "spotify_auth_state"; export const stateKey = "spotify_auth_state";
const scopes = { export const scopes = {
// ImageUpload: "ugc-image-upload", // ImageUpload: "ugc-image-upload",
AccessPrivatePlaylists: "playlist-read-private", AccessPrivatePlaylists: "playlist-read-private",
AccessCollaborativePlaylists: "playlist-read-collaborative", AccessCollaborativePlaylists: "playlist-read-collaborative",
@ -15,11 +15,3 @@ const scopes = {
AccessLibrary: "user-library-read", AccessLibrary: "user-library-read",
AccessUser: "user-read-private", AccessUser: "user-read-private",
}; };
module.exports = {
accountsAPIURL,
baseAPIURL,
sessionName,
stateKey,
scopes
};

View File

@ -1,18 +1,19 @@
const { authInstance } = require("../api/axios"); import { authInstance } from "../api/axios.js";
const typedefs = require("../typedefs"); import * as typedefs from "../typedefs.js";
const { scopes, stateKey, accountsAPIURL, sessionName } = require("../constants"); import { scopes, stateKey, accountsAPIURL, sessionName } from "../constants.js";
const generateRandString = require("../utils/generateRandString"); import generateRandString from "../utils/generateRandString.js";
const { getUserProfile } = require("../api/spotify"); import { getUserProfile } from "../api/spotify.js";
const logger = require("../utils/logger")(module); import curriedLogger from "../utils/logger.js";
const logger = curriedLogger(import.meta);
/** /**
* Stateful redirect to Spotify login with credentials * Stateful redirect to Spotify login with credentials
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const login = (_req, res) => { export const login = (_req, res) => {
try { try {
const state = generateRandString(16); const state = generateRandString(16);
res.cookie(stateKey, state); res.cookie(stateKey, state);
@ -41,7 +42,7 @@ const login = (_req, res) => {
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const callback = async (req, res) => { export const callback = async (req, res) => {
try { try {
const { code, state, error } = req.query; const { code, state, error } = req.query;
const storedState = req.cookies ? req.cookies[stateKey] : null; const storedState = req.cookies ? req.cookies[stateKey] : null;
@ -104,7 +105,7 @@ const callback = async (req, res) => {
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const refresh = async (req, res) => { export const refresh = async (req, res) => {
try { try {
const authForm = { const authForm = {
refresh_token: req.session.refreshToken, refresh_token: req.session.refreshToken,
@ -139,10 +140,10 @@ const refresh = async (req, res) => {
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const logout = async (req, res) => { export const logout = async (req, res) => {
try { try {
const delSession = req.session.destroy((error) => { const delSession = req.session.destroy((error) => {
if(Object.keys(error).length) { if (Object.keys(error).length) {
res.status(500).send({ message: "Internal Server Error" }); res.status(500).send({ message: "Internal Server Error" });
logger.error("Error while logging out", { error }); logger.error("Error while logging out", { error });
return; return;
@ -160,10 +161,3 @@ const logout = async (req, res) => {
return; return;
} }
} }
module.exports = {
login,
callback,
refresh,
logout
};

View File

@ -1,25 +1,25 @@
const typedefs = require("../typedefs"); import * as typedefs from "../typedefs.js";
const logger = require("../utils/logger")(module); import curriedLogger from "../utils/logger.js";
const logger = curriedLogger(import.meta);
const { getUserPlaylistsFirstPage, getUserPlaylistsNextPage, getPlaylistDetailsFirstPage, getPlaylistDetailsNextPage, addItemsToPlaylist, removeItemsFromPlaylist, checkPlaylistEditable } = require("../api/spotify"); import { getUserPlaylistsFirstPage, getUserPlaylistsNextPage, getPlaylistDetailsFirstPage, getPlaylistDetailsNextPage, addItemsToPlaylist, removeItemsFromPlaylist, checkPlaylistEditable } from "../api/spotify.js";
const { parseSpotifyLink } = require("../utils/spotifyURITransformer"); import { parseSpotifyLink } from "../utils/spotifyURITransformer.js";
const { randomBool, sleep } = require("../utils/flake"); import { randomBool, sleep } from "../utils/flake.js";
const myGraph = require("../utils/graph"); import myGraph from "../utils/graph.js";
const { Op } = require("sequelize"); import { Op } from "sequelize";
const { sequelize } = require("../models");
/** @type {typedefs.Model} */ import models, { sequelize } from "../models/index.js";
const Playlists = require("../models").playlists; const Playlists = models.playlists;
/** @type {typedefs.Model} */ const Links = models.links;
const Links = require("../models").links;
/** /**
* Sync user's Spotify data * Sync user's Spotify data
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const updateUser = async (req, res) => { export const updateUser = async (req, res) => {
try { try {
let currentPlaylists = []; let currentPlaylists = [];
const uID = req.session.user.id; const uID = req.session.user.id;
@ -177,7 +177,7 @@ const updateUser = async (req, res) => {
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const fetchUser = async (req, res) => { export const fetchUser = async (req, res) => {
try { try {
// if (randomBool(0.5)) { // if (randomBool(0.5)) {
// res.status(404).send({ message: "Not Found" }); // res.status(404).send({ message: "Not Found" });
@ -219,7 +219,7 @@ const fetchUser = async (req, res) => {
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const createLink = async (req, res) => { export const createLink = async (req, res) => {
try { try {
// await sleep(1000); // await sleep(1000);
const uID = req.session.user.id; const uID = req.session.user.id;
@ -310,7 +310,7 @@ const createLink = async (req, res) => {
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const removeLink = async (req, res) => { export const removeLink = async (req, res) => {
try { try {
const uID = req.session.user.id; const uID = req.session.user.id;
@ -475,7 +475,7 @@ const _populateSingleLinkCore = async (req, res, link) => {
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const populateSingleLink = async (req, res) => { export const populateSingleLink = async (req, res) => {
try { try {
const uID = req.session.user.id; const uID = req.session.user.id;
const link = { from: req.body.from, to: req.body.to }; const link = { from: req.body.from, to: req.body.to };
@ -591,7 +591,7 @@ const _pruneSingleLinkCore = async (req, res, link) => {
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const pruneSingleLink = async (req, res) => { export const pruneSingleLink = async (req, res) => {
try { try {
const uID = req.session.user.id; const uID = req.session.user.id;
const link = { from: req.body.from, to: req.body.to }; const link = { from: req.body.from, to: req.body.to };
@ -643,12 +643,3 @@ const pruneSingleLink = async (req, res) => {
return; return;
} }
} }
module.exports = {
updateUser,
fetchUser,
createLink,
removeLink,
populateSingleLink,
pruneSingleLink,
};

View File

@ -1,15 +1,16 @@
const logger = require("../utils/logger")(module); import curriedLogger from "../utils/logger.js";
const logger = curriedLogger(import.meta);
const typedefs = require("../typedefs"); import * as typedefs from "../typedefs.js";
const { getUserPlaylistsFirstPage, getUserPlaylistsNextPage, getPlaylistDetailsFirstPage, getPlaylistDetailsNextPage } = require("../api/spotify"); import { getUserPlaylistsFirstPage, getUserPlaylistsNextPage, getPlaylistDetailsFirstPage, getPlaylistDetailsNextPage } from "../api/spotify.js";
const { parseSpotifyLink } = require("../utils/spotifyURITransformer"); import { parseSpotifyLink } from "../utils/spotifyURITransformer.js";
/** /**
* Retrieve list of all of user's playlists * Retrieve list of all of user's playlists
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const fetchUserPlaylists = async (req, res) => { export const fetchUserPlaylists = async (req, res) => {
try { try {
let userPlaylists = {}; let userPlaylists = {};
@ -65,7 +66,7 @@ const fetchUserPlaylists = async (req, res) => {
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
*/ */
const fetchPlaylistDetails = async (req, res) => { export const fetchPlaylistDetails = async (req, res) => {
try { try {
let playlist = {}; let playlist = {};
/** @type {typedefs.URIObject} */ /** @type {typedefs.URIObject} */
@ -152,8 +153,3 @@ const fetchPlaylistDetails = async (req, res) => {
return; return;
} }
} }
module.exports = {
fetchUserPlaylists,
fetchPlaylistDetails
};

View File

@ -1,23 +1,24 @@
require("dotenv-flow").config(); import _ from "./config/dotenv.js";
const util = require("util"); import { promisify } from "util";
const express = require("express"); import express from "express";
const session = require("express-session"); import session from "express-session";
const cors = require("cors"); import cors from "cors";
const cookieParser = require("cookie-parser"); import cookieParser from "cookie-parser";
const helmet = require("helmet"); import helmet from "helmet";
const { createClient } = require('redis'); import { createClient } from 'redis';
const { RedisStore } = require("connect-redis"); import { RedisStore } from "connect-redis";
const { sessionName } = require("./constants"); import { sessionName } from "./constants.js";
const db = require("./models"); import { sequelize } from "./models/index.js";
const { isAuthenticated } = require("./middleware/authCheck"); import { isAuthenticated } from "./middleware/authCheck.js";
const { getUserProfile } = require("./api/spotify"); import { getUserProfile } from "./api/spotify.js";
const logger = require("./utils/logger")(module); import curriedLogger from "./utils/logger.js";
const logger = curriedLogger(import.meta);
const app = express(); const app = express();
@ -73,7 +74,7 @@ app.use(express.json());
app.use(express.urlencoded({ extended: true })); app.use(express.urlencoded({ extended: true }));
// Static // Static
app.use(express.static(__dirname + "/static")); app.use(express.static(import.meta.dirname + "/static"));
// Healthcheck // Healthcheck
app.use("/health", (req, res) => { app.use("/health", (req, res) => {
@ -92,11 +93,13 @@ app.use("/auth-health", isAuthenticated, async (req, res) => {
return; return;
} }
}); });
import authRoutes from "./routes/auth.js";
import playlistRoutes from "./routes/playlists.js";
import operationRoutes from "./routes/operations.js";
// Routes // Routes
app.use("/api/auth/", require("./routes/auth")); app.use("/api/auth/", authRoutes);
app.use("/api/playlists", isAuthenticated, require("./routes/playlists")); app.use("/api/playlists", isAuthenticated, playlistRoutes);
app.use("/api/operations", isAuthenticated, require("./routes/operations")); app.use("/api/operations", isAuthenticated, operationRoutes);
// Fallbacks // Fallbacks
app.use((req, res) => { app.use((req, res) => {
@ -119,8 +122,8 @@ const cleanupFunc = (signal) => {
Promise.allSettled([ Promise.allSettled([
redisClient.disconnect, redisClient.disconnect,
db.sequelize.close(), sequelize.close(),
util.promisify(server.close), promisify(server.close),
]).then(() => { ]).then(() => {
logger.info("Cleaned up, exiting."); logger.info("Cleaned up, exiting.");
process.exit(0); process.exit(0);

View File

@ -1,6 +1,7 @@
const { sessionName } = require("../constants"); import { sessionName } from "../constants.js";
const typedefs = require("../typedefs"); import * as typedefs from "../typedefs.js";
const logger = require("../utils/logger")(module); import curriedLogger from "../utils/logger.js";
const logger = curriedLogger(import.meta);
/** /**
* middleware to check if access token is present * middleware to check if access token is present
@ -8,7 +9,7 @@ const logger = require("../utils/logger")(module);
* @param {typedefs.Res} res * @param {typedefs.Res} res
* @param {typedefs.Next} next * @param {typedefs.Next} next
*/ */
const isAuthenticated = (req, res, next) => { export const isAuthenticated = (req, res, next) => {
if (req.session.accessToken) { if (req.session.accessToken) {
req.sessHeaders = { req.sessHeaders = {
"Authorization": `Bearer ${req.session.accessToken}`, "Authorization": `Bearer ${req.session.accessToken}`,
@ -30,7 +31,3 @@ const isAuthenticated = (req, res, next) => {
}); });
} }
} }
module.exports = {
isAuthenticated,
}

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
/** @type {import("sequelize-cli").Migration} */ /** @type {import("sequelize-cli").Migration} */
module.exports = { export default {
async up(queryInterface, Sequelize) { up: async function (queryInterface, Sequelize) {
await queryInterface.createTable("playlists", { await queryInterface.createTable("playlists", {
id: { id: {
allowNull: false, allowNull: false,
@ -28,7 +28,7 @@ module.exports = {
} }
}); });
}, },
async down(queryInterface, Sequelize) { down: async function (queryInterface, Sequelize) {
await queryInterface.dropTable("playlists"); await queryInterface.dropTable("playlists");
} }
}; };

View File

@ -1,7 +1,7 @@
"use strict"; "use strict";
/** @type {import("sequelize-cli").Migration} */ /** @type {import("sequelize-cli").Migration} */
module.exports = { export default {
async up(queryInterface, Sequelize) { up: async function (queryInterface, Sequelize) {
await queryInterface.createTable("links", { await queryInterface.createTable("links", {
id: { id: {
allowNull: false, allowNull: false,
@ -28,7 +28,7 @@ module.exports = {
} }
}); });
}, },
async down(queryInterface, Sequelize) { down: async function (queryInterface, Sequelize) {
await queryInterface.dropTable("links"); await queryInterface.dropTable("links");
} }
}; };

View File

@ -1,11 +1,16 @@
"use strict"; "use strict";
const fs = require("fs"); import { readdirSync } from "fs";
const path = require("path"); import { basename as _basename } from "path";
const Sequelize = require("sequelize"); const basename = _basename(import.meta.filename);
const logger = require("../utils/logger")(module);
const basename = path.basename(__filename); import Sequelize from "sequelize";
import curriedLogger from "../utils/logger.js";
const logger = curriedLogger(import.meta);
import seqConfig from "../config/sequelize.js"
const env = process.env.NODE_ENV || "development"; const env = process.env.NODE_ENV || "development";
const config = require(__dirname + "/../config/sequelize.js")[env]; const config = seqConfig[env];
const db = {}; const db = {};
let sequelize; let sequelize;
@ -26,15 +31,22 @@ if (config.use_env_variable) {
})(); })();
// Read model definitions from folder // Read model definitions from folder
fs const modelFiles = readdirSync(import.meta.dirname)
.readdirSync(__dirname) .filter(
.filter(file => { (file) => file.indexOf('.') !== 0
return (file.indexOf(".") !== 0) && (file !== basename) && (file.slice(-3) === ".js"); && file !== basename
}) && file.slice(-3) === '.js',
.forEach(file => { );
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model; await Promise.all(modelFiles.map(async file => {
}); const model = await import(`./${file}`);
if (!model.default) {
return;
}
const namedModel = model.default(sequelize, Sequelize.DataTypes);
db[namedModel.name] = namedModel;
}))
// Setup defined associations // Setup defined associations
Object.keys(db).forEach(modelName => { Object.keys(db).forEach(modelName => {
@ -43,7 +55,9 @@ Object.keys(db).forEach(modelName => {
} }
}); });
// clean ts up
db.sequelize = sequelize; db.sequelize = sequelize;
db.Sequelize = Sequelize; db.Sequelize = Sequelize;
export { sequelize as sequelize };
module.exports = db; export { Sequelize as Sequelize };
export default db;

View File

@ -1,8 +1,6 @@
"use strict"; "use strict";
const { import { Model } from "sequelize";
Model export default (sequelize, DataTypes) => {
} = require("sequelize");
module.exports = (sequelize, DataTypes) => {
class links extends Model { class links extends Model {
/** /**
* Helper method for defining associations. * Helper method for defining associations.

View File

@ -1,8 +1,6 @@
"use strict"; "use strict";
const { import { Model } from "sequelize";
Model export default (sequelize, DataTypes) => {
} = require("sequelize");
module.exports = (sequelize, DataTypes) => {
class playlists extends Model { class playlists extends Model {
/** /**
* Helper method for defining associations. * Helper method for defining associations.

View File

@ -2,7 +2,8 @@
"name": "spotify-manager", "name": "spotify-manager",
"version": "0", "version": "0",
"description": "Personal Spotify playlist manager", "description": "Personal Spotify playlist manager",
"main": "index.js", "exports": "./index.js",
"type": "module",
"scripts": { "scripts": {
"dev": "cross-env NODE_ENV=development nodemon --delay 2 --exitcrash index.js", "dev": "cross-env NODE_ENV=development nodemon --delay 2 --exitcrash index.js",
"test_setup": "npm i && cross-env NODE_ENV=test npx sequelize-cli db:migrate", "test_setup": "npm i && cross-env NODE_ENV=test npx sequelize-cli db:migrate",

View File

@ -1,8 +1,9 @@
const router = require("express").Router(); import { Router } from "express";
const router = Router();
const { login, callback, refresh, logout } = require("../controllers/auth"); import { login, callback, refresh, logout } from "../controllers/auth.js";
const { isAuthenticated } = require("../middleware/authCheck"); import { isAuthenticated } from "../middleware/authCheck.js";
const validator = require("../validators"); import { validate } from "../validators/index.js";
router.get( router.get(
"/login", "/login",
@ -25,4 +26,4 @@ router.get(
logout logout
); );
module.exports = router; export default router;

View File

@ -1,8 +1,10 @@
const router = require("express").Router(); import { Router } from "express";
const router = Router();
const { updateUser, fetchUser, createLink, removeLink, populateSingleLink, pruneSingleLink } = require("../controllers/operations"); import { updateUser, fetchUser, createLink, removeLink, populateSingleLink, pruneSingleLink } from "../controllers/operations.js";
const { validate } = require("../validators"); import { createLinkValidator, removeLinkValidator, populateSingleLinkValidator, pruneSingleLinkValidator } from "../validators/operations.js";
const { createLinkValidator, removeLinkValidator, populateSingleLinkValidator, pruneSingleLinkValidator } = require("../validators/operations");
import { validate } from "../validators/index.js";
router.put( router.put(
"/update", "/update",
@ -42,4 +44,4 @@ router.put(
pruneSingleLink pruneSingleLink
); );
module.exports = router; export default router;

View File

@ -1,8 +1,10 @@
const router = require("express").Router(); import { Router } from "express";
const router = Router();
const { fetchUserPlaylists, fetchPlaylistDetails } = require("../controllers/playlists"); import { fetchUserPlaylists, fetchPlaylistDetails } from "../controllers/playlists.js";
const { getPlaylistDetailsValidator } = require("../validators/playlists"); import { getPlaylistDetailsValidator } from "../validators/playlists.js";
const { validate } = require("../validators");
import { validate } from "../validators/index.js";
router.get( router.get(
"/me", "/me",
@ -16,4 +18,4 @@ router.get(
fetchPlaylistDetails fetchPlaylistDetails
); );
module.exports = router; export default router;

View File

@ -64,4 +64,4 @@
* }} PlaylistDetails * }} PlaylistDetails
*/ */
exports.unused = {}; export default {};

View File

@ -1,7 +1,3 @@
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const randomBool = (chance_of_failure = 0.25) => Math.random() < chance_of_failure; export const randomBool = (chance_of_failure = 0.25) => Math.random() < chance_of_failure;
module.exports = {
sleep, randomBool
};

View File

@ -3,7 +3,7 @@
* @param {number} length The length of the string * @param {number} length The length of the string
* @return {string} The generated string * @return {string} The generated string
*/ */
module.exports = (length) => { export default (length) => {
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let text = ""; let text = "";

View File

@ -1,6 +1,7 @@
const logger = require("./logger")(module); import curriedLogger from "./logger.js";
const logger = curriedLogger(import.meta);
const typedefs = require("../typedefs"); import * as typedefs from "../typedefs.js";
/** /**
* Directed graph, may or may not be connected. * Directed graph, may or may not be connected.
@ -21,7 +22,7 @@ const typedefs = require("../typedefs");
* console.log(g.detectCycle()); // true * console.log(g.detectCycle()); // true
* ``` * ```
*/ */
class myGraph { export class myGraph {
/** /**
* @param {string[]} nodes Graph nodes IDs * @param {string[]} nodes Graph nodes IDs
* @param {{ from: string, to: string }[]} edges Graph edges b/w nodes * @param {{ from: string, to: string }[]} edges Graph edges b/w nodes
@ -156,4 +157,4 @@ class myGraph {
} }
} }
module.exports = myGraph; export default myGraph;

View File

@ -5,7 +5,7 @@
* @param {string} delimiter Delimiter of final string * @param {string} delimiter Delimiter of final string
* @returns {string} * @returns {string}
*/ */
const getNestedValuesString = (obj, delimiter = ", ") => { export const getNestedValuesString = (obj, delimiter = ", ") => {
let values = []; let values = [];
for (key in obj) { for (key in obj) {
if (typeof obj[key] !== "object") { if (typeof obj[key] !== "object") {
@ -17,7 +17,3 @@ const getNestedValuesString = (obj, delimiter = ", ") => {
return values.join(delimiter); return values.join(delimiter);
} }
module.exports = {
getNestedValuesString
}

View File

@ -1,10 +1,10 @@
const path = require("path"); import path from "path";
import { createLogger, transports, config, format } from "winston";
import * as typedefs from "../typedefs.js";
const { createLogger, transports, config, format } = require("winston");
const { combine, label, timestamp, printf, errors } = format; const { combine, label, timestamp, printf, errors } = format;
const typedefs = require("../typedefs");
const getLabel = (callingModule) => { const getLabel = (callingModule) => {
if (!callingModule.filename) return "repl"; if (!callingModule.filename) return "repl";
const parts = callingModule.filename?.split(path.sep); const parts = callingModule.filename?.split(path.sep);
@ -35,9 +35,9 @@ const logFormat = printf(({ level, message, label, timestamp, ...meta }) => {
/** /**
* Creates a curried function, and call it with the module in use to get logs with filename * Creates a curried function, and call it with the module in use to get logs with filename
* @param {typedefs.Module} callingModule The module from which the logger is called * @param {typedefs.Module} callingModule The module from which the logger is called (ESM - import.meta)
*/ */
const curriedLogger = (callingModule) => { export const curriedLogger = (callingModule) => {
let winstonLogger = createLogger({ let winstonLogger = createLogger({
levels: config.npm.levels, levels: config.npm.levels,
format: combine( format: combine(
@ -49,12 +49,12 @@ const curriedLogger = (callingModule) => {
transports: [ transports: [
new transports.Console({ level: "info" }), new transports.Console({ level: "info" }),
new transports.File({ new transports.File({
filename: __dirname + "/../logs/debug.log", filename: import.meta.dirname + "/../logs/debug.log",
level: "debug", level: "debug",
maxsize: 10485760, maxsize: 10485760,
}), }),
new transports.File({ new transports.File({
filename: __dirname + "/../logs/error.log", filename: import.meta.dirname + "/../logs/error.log",
level: "error", level: "error",
maxsize: 1048576, maxsize: 1048576,
}), }),
@ -64,4 +64,4 @@ const curriedLogger = (callingModule) => {
return winstonLogger; return winstonLogger;
} }
module.exports = curriedLogger; export default curriedLogger;

View File

@ -1,4 +1,4 @@
const typedefs = require("../typedefs"); import * as typedefs from "../typedefs.js";
/** @type {RegExp} */ /** @type {RegExp} */
const base62Pattern = /^[A-Za-z0-9]+$/; const base62Pattern = /^[A-Za-z0-9]+$/;
@ -10,7 +10,7 @@ const base62Pattern = /^[A-Za-z0-9]+$/;
* @returns {typedefs.URIObject} * @returns {typedefs.URIObject}
* @throws {TypeError} If the input is not a valid Spotify URI * @throws {TypeError} If the input is not a valid Spotify URI
*/ */
const parseSpotifyURI = (uri) => { export const parseSpotifyURI = (uri) => {
const parts = uri.split(":"); const parts = uri.split(":");
if (parts[0] !== "spotify") { if (parts[0] !== "spotify") {
@ -59,7 +59,7 @@ const parseSpotifyURI = (uri) => {
* @returns {typedefs.URIObject} * @returns {typedefs.URIObject}
* @throws {TypeError} If the input is not a valid Spotify link * @throws {TypeError} If the input is not a valid Spotify link
*/ */
const parseSpotifyLink = (link) => { export const parseSpotifyLink = (link) => {
const localPattern = /^https:\/\/open\.spotify\.com\/local\/([^\/]*)\/([^\/]*)\/([^\/]+)\/(\d+)$/; const localPattern = /^https:\/\/open\.spotify\.com\/local\/([^\/]*)\/([^\/]*)\/([^\/]+)\/(\d+)$/;
const standardPattern = /^https:\/\/open\.spotify\.com\/([^\/]+)\/([^\/?]+)/; const standardPattern = /^https:\/\/open\.spotify\.com\/([^\/]+)\/([^\/?]+)/;
@ -106,7 +106,7 @@ const parseSpotifyLink = (link) => {
* @param {typedefs.URIObject} uriObj * @param {typedefs.URIObject} uriObj
* @returns {string} * @returns {string}
*/ */
const buildSpotifyURI = (uriObj) => { export const buildSpotifyURI = (uriObj) => {
if (uriObj.is_local) { if (uriObj.is_local) {
const artist = encodeURIComponent(uriObj.artist ?? ""); const artist = encodeURIComponent(uriObj.artist ?? "");
const album = encodeURIComponent(uriObj.album ?? ""); const album = encodeURIComponent(uriObj.album ?? "");
@ -122,7 +122,7 @@ const buildSpotifyURI = (uriObj) => {
* @param {typedefs.URIObject} uriObj * @param {typedefs.URIObject} uriObj
* @returns {string} * @returns {string}
*/ */
const buildSpotifyLink = (uriObj) => { export const buildSpotifyLink = (uriObj) => {
if (uriObj.is_local) { if (uriObj.is_local) {
const artist = encodeURIComponent(uriObj.artist ?? ""); const artist = encodeURIComponent(uriObj.artist ?? "");
const album = encodeURIComponent(uriObj.album ?? ""); const album = encodeURIComponent(uriObj.album ?? "");
@ -132,10 +132,3 @@ const buildSpotifyLink = (uriObj) => {
} }
return `https://open.spotify.com/${uriObj.type}/${uriObj.id}` return `https://open.spotify.com/${uriObj.type}/${uriObj.id}`
} }
module.exports = {
parseSpotifyURI,
parseSpotifyLink,
buildSpotifyURI,
buildSpotifyLink
}

View File

@ -1,9 +1,9 @@
const { validationResult } = require("express-validator"); import { validationResult } from "express-validator";
const { getNestedValuesString } = require("../utils/jsonTransformer"); import * as typedefs from "../typedefs.js";
const logger = require("../utils/logger")(module); import { getNestedValuesString } from "../utils/jsonTransformer.js";
import curriedLogger from "../utils/logger.js";
const typedefs = require("../typedefs"); const logger = curriedLogger(import.meta);
/** /**
* Refer: https://stackoverflow.com/questions/58848625/access-messages-in-express-validator * Refer: https://stackoverflow.com/questions/58848625/access-messages-in-express-validator
@ -12,7 +12,7 @@ const typedefs = require("../typedefs");
* @param {typedefs.Res} res * @param {typedefs.Res} res
* @param {typedefs.Next} next * @param {typedefs.Next} next
*/ */
const validate = (req, res, next) => { export const validate = (req, res, next) => {
const errors = validationResult(req); const errors = validationResult(req);
if (errors.isEmpty()) { if (errors.isEmpty()) {
return next(); return next();
@ -40,7 +40,3 @@ const validate = (req, res, next) => {
logger.warn("invalid request", { extractedErrors }); logger.warn("invalid request", { extractedErrors });
return; return;
} }
module.exports = {
validate
};

View File

@ -1,13 +1,12 @@
const { body, header, param, query } = require("express-validator"); import { body, header, param, query } from "express-validator";
import * as typedefs from "../typedefs.js";
const typedefs = require("../typedefs");
/** /**
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
* @param {typedefs.Next} next * @param {typedefs.Next} next
*/ */
const createLinkValidator = async (req, res, next) => { export const createLinkValidator = async (req, res, next) => {
await body("from") await body("from")
.notEmpty() .notEmpty()
.withMessage("from not defined in body") .withMessage("from not defined in body")
@ -23,9 +22,6 @@ const createLinkValidator = async (req, res, next) => {
next(); next();
} }
module.exports = { export { createLinkValidator as removeLinkValidator };
createLinkValidator, export { createLinkValidator as populateSingleLinkValidator };
removeLinkValidator: createLinkValidator, export { createLinkValidator as pruneSingleLinkValidator };
populateSingleLinkValidator: createLinkValidator,
pruneSingleLinkValidator: createLinkValidator,
}

View File

@ -1,13 +1,12 @@
const { body, header, param, query } = require("express-validator"); import { body, header, param, query } from "express-validator";
import * as typedefs from "../typedefs.js";
const typedefs = require("../typedefs");
/** /**
* @param {typedefs.Req} req * @param {typedefs.Req} req
* @param {typedefs.Res} res * @param {typedefs.Res} res
* @param {typedefs.Next} next * @param {typedefs.Next} next
*/ */
const getPlaylistDetailsValidator = async (req, res, next) => { export const getPlaylistDetailsValidator = async (req, res, next) => {
await query("playlist_link") await query("playlist_link")
.notEmpty() .notEmpty()
.withMessage("playlist_link not defined in query") .withMessage("playlist_link not defined in query")
@ -16,7 +15,3 @@ const getPlaylistDetailsValidator = async (req, res, next) => {
.run(req); .run(req);
next(); next();
} }
module.exports = {
getPlaylistDetailsValidator
}