diff --git a/.sequelizerc b/.sequelizerc index 915c148..382d2a5 100644 --- a/.sequelizerc +++ b/.sequelizerc @@ -1,6 +1,7 @@ -require("dotenv-flow").config(); +import dotenvFlow from "dotenv-flow"; +dotenvFlow.config(); -const path = require("path"); -module.exports = { - "config": path.resolve("config", "sequelize.js") +import { resolve } from "path"; +export default { + "config": resolve("config", "sequelize.js") }; diff --git a/api/axios.js b/api/axios.js index 63e46c0..0c87d97 100644 --- a/api/axios.js +++ b/api/axios.js @@ -1,10 +1,11 @@ -const axios = require("axios"); -const rateLimit = require("axios-rate-limit"); +import axios from "axios"; +import rateLimit from "axios-rate-limit"; -const { baseAPIURL, accountsAPIURL } = require("../constants"); -const logger = require("../utils/logger")(module); +import { baseAPIURL, accountsAPIURL } from "../constants.js"; +import curriedLogger from "../utils/logger.js"; +const logger = curriedLogger(import.meta); -const authInstance = axios.create({ +export const authInstance = axios.create({ baseURL: accountsAPIURL, timeout: 20000, headers: { @@ -21,7 +22,7 @@ const uncappedAxiosInstance = axios.create({ }, }); -const axiosInstance = rateLimit(uncappedAxiosInstance, { +export const axiosInstance = rateLimit(uncappedAxiosInstance, { maxRequests: 10, perMilliseconds: 5000, }); @@ -50,8 +51,3 @@ axiosInstance.interceptors.response.use( return Promise.reject(error); } ); - -module.exports = { - authInstance, - axiosInstance -}; diff --git a/api/spotify.js b/api/spotify.js index 7a3e8cc..920235a 100644 --- a/api/spotify.js +++ b/api/spotify.js @@ -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: "; @@ -17,7 +18,7 @@ const logPrefix = "Spotify API: "; * @param {any} data request body * @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; config.headers = { ...config.headers, ...req.sessHeaders }; 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, "GET", "/me", { headers: { Authorization: `Bearer ${req.session.accessToken}` } } @@ -70,7 +71,7 @@ const getUserProfile = async (req, res) => { return res.headersSent ? null : response.data; } -const getUserPlaylistsFirstPage = async (req, res) => { +export const getUserPlaylistsFirstPage = async (req, res) => { const response = await singleRequest(req, res, "GET", `/users/${req.session.user.id}/playlists`, @@ -83,13 +84,13 @@ const getUserPlaylistsFirstPage = async (req, res) => { return res.headersSent ? null : response.data; } -const getUserPlaylistsNextPage = async (req, res, nextURL) => { +export const getUserPlaylistsNextPage = async (req, res, nextURL) => { const response = await singleRequest( req, res, "GET", nextURL); 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, "GET", `/playlists/${playlistID}/`, @@ -101,13 +102,13 @@ const getPlaylistDetailsFirstPage = async (req, res, initialFields, playlistID) return res.headersSent ? null : response.data; } -const getPlaylistDetailsNextPage = async (req, res, nextURL) => { +export const getPlaylistDetailsNextPage = async (req, res, nextURL) => { const response = await singleRequest( req, res, "GET", nextURL); 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, "POST", `/playlists/${playlistID}/tracks`, @@ -117,7 +118,7 @@ const addItemsToPlaylist = async (req, res, nextBatch, playlistID) => { 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 // but see here: https://github.com/spotipy-dev/spotipy/issues/95#issuecomment-2263634801 const response = await singleRequest(req, res, @@ -130,7 +131,7 @@ const removeItemsFromPlaylist = async (req, res, nextBatch, playlistID, snapshot 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)"]; const checkFromData = await getPlaylistDetailsFirstPage(req, res, checkFields.join(), playlistID); @@ -150,15 +151,3 @@ const checkPlaylistEditable = async (req, res, playlistID, userID) => { return true; } } - -module.exports = { - singleRequest, - getUserProfile, - getUserPlaylistsFirstPage, - getUserPlaylistsNextPage, - getPlaylistDetailsFirstPage, - getPlaylistDetailsNextPage, - addItemsToPlaylist, - removeItemsFromPlaylist, - checkPlaylistEditable, -} diff --git a/boilerplates/controller.js b/boilerplates/controller.js index 9619ed6..540233c 100644 --- a/boilerplates/controller.js +++ b/boilerplates/controller.js @@ -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.Res} res */ -const __controller_func = async (req, res) => { +export const __controller_func = async (req, res) => { try { } catch (error) { @@ -15,7 +16,3 @@ const __controller_func = async (req, res) => { return; } } - -module.exports = { - __controller_func -}; diff --git a/boilerplates/route.js b/boilerplates/route.js index a4ef0a5..1dbb54f 100644 --- a/boilerplates/route.js +++ b/boilerplates/route.js @@ -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( @@ -10,4 +11,4 @@ router.post( ); -module.exports = router; +export default router; diff --git a/boilerplates/validator.js b/boilerplates/validator.js index 75aa736..3537edc 100644 --- a/boilerplates/validator.js +++ b/boilerplates/validator.js @@ -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.Res} res * @param {typedefs.Next} next */ -const __validator_func = async (req, res, next) => { +export const __validator_func = async (req, res, next) => { await body("field_name") .notEmpty() .withMessage("field_name not defined in body") @@ -15,7 +15,3 @@ const __validator_func = async (req, res, next) => { next(); } - -module.exports = { - __validator_func -} diff --git a/config/dotenv.js b/config/dotenv.js new file mode 100644 index 0000000..7e9843a --- /dev/null +++ b/config/dotenv.js @@ -0,0 +1,3 @@ +// https://github.com/motdotla/dotenv/issues/133#issuecomment-255298822 +import DotenvFlow from "dotenv-flow"; +export default DotenvFlow.config(); diff --git a/config/sequelize.js b/config/sequelize.js index 903e981..87a0ba0 100644 --- a/config/sequelize.js +++ b/config/sequelize.js @@ -1,4 +1,5 @@ -const logger = require("../utils/logger")(module); +import curriedLogger from "../utils/logger.js"; +const logger = curriedLogger(import.meta); const connConfigs = { development: { @@ -8,7 +9,7 @@ const connConfigs = { host: process.env.DB_HOST || "127.0.0.1", port: process.env.DB_PORT || 5432, }, - staging: { + test: { use_env_variable: "DB_URL", // use connection string for non-dev env }, production: { @@ -25,4 +26,4 @@ for (const conf in connConfigs) { connConfigs[conf]["dialect"] = process.env.DB_DIALECT || "postgres"; } -module.exports = connConfigs; +export default connConfigs; diff --git a/constants.js b/constants.js index ae6d41e..c1940ad 100644 --- a/constants.js +++ b/constants.js @@ -1,9 +1,9 @@ -const accountsAPIURL = "https://accounts.spotify.com"; -const baseAPIURL = "https://api.spotify.com/v1"; -const sessionName = "spotify-manager"; -const stateKey = "spotify_auth_state"; +export const accountsAPIURL = "https://accounts.spotify.com"; +export const baseAPIURL = "https://api.spotify.com/v1"; +export const sessionName = "spotify-manager"; +export const stateKey = "spotify_auth_state"; -const scopes = { +export const scopes = { // ImageUpload: "ugc-image-upload", AccessPrivatePlaylists: "playlist-read-private", AccessCollaborativePlaylists: "playlist-read-collaborative", @@ -15,11 +15,3 @@ const scopes = { AccessLibrary: "user-library-read", AccessUser: "user-read-private", }; - -module.exports = { - accountsAPIURL, - baseAPIURL, - sessionName, - stateKey, - scopes -}; diff --git a/controllers/auth.js b/controllers/auth.js index 40e4d01..4420df5 100644 --- a/controllers/auth.js +++ b/controllers/auth.js @@ -1,18 +1,19 @@ -const { authInstance } = require("../api/axios"); +import { authInstance } from "../api/axios.js"; -const typedefs = require("../typedefs"); -const { scopes, stateKey, accountsAPIURL, sessionName } = require("../constants"); +import * as typedefs from "../typedefs.js"; +import { scopes, stateKey, accountsAPIURL, sessionName } from "../constants.js"; -const generateRandString = require("../utils/generateRandString"); -const { getUserProfile } = require("../api/spotify"); -const logger = require("../utils/logger")(module); +import generateRandString from "../utils/generateRandString.js"; +import { getUserProfile } from "../api/spotify.js"; +import curriedLogger from "../utils/logger.js"; +const logger = curriedLogger(import.meta); /** * Stateful redirect to Spotify login with credentials * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const login = (_req, res) => { +export const login = (_req, res) => { try { const state = generateRandString(16); res.cookie(stateKey, state); @@ -41,7 +42,7 @@ const login = (_req, res) => { * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const callback = async (req, res) => { +export const callback = async (req, res) => { try { const { code, state, error } = req.query; const storedState = req.cookies ? req.cookies[stateKey] : null; @@ -104,7 +105,7 @@ const callback = async (req, res) => { * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const refresh = async (req, res) => { +export const refresh = async (req, res) => { try { const authForm = { refresh_token: req.session.refreshToken, @@ -139,10 +140,10 @@ const refresh = async (req, res) => { * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const logout = async (req, res) => { +export const logout = async (req, res) => { try { const delSession = req.session.destroy((error) => { - if(Object.keys(error).length) { + if (Object.keys(error).length) { res.status(500).send({ message: "Internal Server Error" }); logger.error("Error while logging out", { error }); return; @@ -160,10 +161,3 @@ const logout = async (req, res) => { return; } } - -module.exports = { - login, - callback, - refresh, - logout -}; diff --git a/controllers/operations.js b/controllers/operations.js index dd451fa..37b571a 100644 --- a/controllers/operations.js +++ b/controllers/operations.js @@ -1,25 +1,25 @@ -const typedefs = require("../typedefs"); -const logger = require("../utils/logger")(module); +import * as typedefs from "../typedefs.js"; +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"); -const { randomBool, sleep } = require("../utils/flake"); -const myGraph = require("../utils/graph"); +import { parseSpotifyLink } from "../utils/spotifyURITransformer.js"; +import { randomBool, sleep } from "../utils/flake.js"; +import myGraph from "../utils/graph.js"; -const { Op } = require("sequelize"); -const { sequelize } = require("../models"); -/** @type {typedefs.Model} */ -const Playlists = require("../models").playlists; -/** @type {typedefs.Model} */ -const Links = require("../models").links; +import { Op } from "sequelize"; + +import models, { sequelize } from "../models/index.js"; +const Playlists = models.playlists; +const Links = models.links; /** * Sync user's Spotify data * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const updateUser = async (req, res) => { +export const updateUser = async (req, res) => { try { let currentPlaylists = []; const uID = req.session.user.id; @@ -177,7 +177,7 @@ const updateUser = async (req, res) => { * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const fetchUser = async (req, res) => { +export const fetchUser = async (req, res) => { try { // if (randomBool(0.5)) { // res.status(404).send({ message: "Not Found" }); @@ -219,7 +219,7 @@ const fetchUser = async (req, res) => { * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const createLink = async (req, res) => { +export const createLink = async (req, res) => { try { // await sleep(1000); const uID = req.session.user.id; @@ -310,7 +310,7 @@ const createLink = async (req, res) => { * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const removeLink = async (req, res) => { +export const removeLink = async (req, res) => { try { const uID = req.session.user.id; @@ -475,7 +475,7 @@ const _populateSingleLinkCore = async (req, res, link) => { * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const populateSingleLink = async (req, res) => { +export const populateSingleLink = async (req, res) => { try { const uID = req.session.user.id; 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.Res} res */ -const pruneSingleLink = async (req, res) => { +export const pruneSingleLink = async (req, res) => { try { const uID = req.session.user.id; const link = { from: req.body.from, to: req.body.to }; @@ -643,12 +643,3 @@ const pruneSingleLink = async (req, res) => { return; } } - -module.exports = { - updateUser, - fetchUser, - createLink, - removeLink, - populateSingleLink, - pruneSingleLink, -}; diff --git a/controllers/playlists.js b/controllers/playlists.js index 652176c..7efd048 100644 --- a/controllers/playlists.js +++ b/controllers/playlists.js @@ -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"); -const { getUserPlaylistsFirstPage, getUserPlaylistsNextPage, getPlaylistDetailsFirstPage, getPlaylistDetailsNextPage } = require("../api/spotify"); -const { parseSpotifyLink } = require("../utils/spotifyURITransformer"); +import * as typedefs from "../typedefs.js"; +import { getUserPlaylistsFirstPage, getUserPlaylistsNextPage, getPlaylistDetailsFirstPage, getPlaylistDetailsNextPage } from "../api/spotify.js"; +import { parseSpotifyLink } from "../utils/spotifyURITransformer.js"; /** * Retrieve list of all of user's playlists * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const fetchUserPlaylists = async (req, res) => { +export const fetchUserPlaylists = async (req, res) => { try { let userPlaylists = {}; @@ -65,7 +66,7 @@ const fetchUserPlaylists = async (req, res) => { * @param {typedefs.Req} req * @param {typedefs.Res} res */ -const fetchPlaylistDetails = async (req, res) => { +export const fetchPlaylistDetails = async (req, res) => { try { let playlist = {}; /** @type {typedefs.URIObject} */ @@ -152,8 +153,3 @@ const fetchPlaylistDetails = async (req, res) => { return; } } - -module.exports = { - fetchUserPlaylists, - fetchPlaylistDetails -}; diff --git a/index.js b/index.js index 9809886..a7b2dd7 100644 --- a/index.js +++ b/index.js @@ -1,23 +1,24 @@ -require("dotenv-flow").config(); +import _ from "./config/dotenv.js"; -const util = require("util"); -const express = require("express"); -const session = require("express-session"); +import { promisify } from "util"; +import express from "express"; +import session from "express-session"; -const cors = require("cors"); -const cookieParser = require("cookie-parser"); -const helmet = require("helmet"); +import cors from "cors"; +import cookieParser from "cookie-parser"; +import helmet from "helmet"; -const { createClient } = require('redis'); -const { RedisStore } = require("connect-redis"); +import { createClient } from 'redis'; +import { RedisStore } from "connect-redis"; -const { sessionName } = require("./constants"); -const db = require("./models"); +import { sessionName } from "./constants.js"; +import { sequelize } from "./models/index.js"; -const { isAuthenticated } = require("./middleware/authCheck"); -const { getUserProfile } = require("./api/spotify"); +import { isAuthenticated } from "./middleware/authCheck.js"; +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(); @@ -73,7 +74,7 @@ app.use(express.json()); app.use(express.urlencoded({ extended: true })); // Static -app.use(express.static(__dirname + "/static")); +app.use(express.static(import.meta.dirname + "/static")); // Healthcheck app.use("/health", (req, res) => { @@ -92,11 +93,13 @@ app.use("/auth-health", isAuthenticated, async (req, res) => { return; } }); - +import authRoutes from "./routes/auth.js"; +import playlistRoutes from "./routes/playlists.js"; +import operationRoutes from "./routes/operations.js"; // Routes -app.use("/api/auth/", require("./routes/auth")); -app.use("/api/playlists", isAuthenticated, require("./routes/playlists")); -app.use("/api/operations", isAuthenticated, require("./routes/operations")); +app.use("/api/auth/", authRoutes); +app.use("/api/playlists", isAuthenticated, playlistRoutes); +app.use("/api/operations", isAuthenticated, operationRoutes); // Fallbacks app.use((req, res) => { @@ -119,8 +122,8 @@ const cleanupFunc = (signal) => { Promise.allSettled([ redisClient.disconnect, - db.sequelize.close(), - util.promisify(server.close), + sequelize.close(), + promisify(server.close), ]).then(() => { logger.info("Cleaned up, exiting."); process.exit(0); diff --git a/middleware/authCheck.js b/middleware/authCheck.js index 481ebe6..72c49e0 100644 --- a/middleware/authCheck.js +++ b/middleware/authCheck.js @@ -1,6 +1,7 @@ -const { sessionName } = require("../constants"); -const typedefs = require("../typedefs"); -const logger = require("../utils/logger")(module); +import { sessionName } from "../constants.js"; +import * as typedefs from "../typedefs.js"; +import curriedLogger from "../utils/logger.js"; +const logger = curriedLogger(import.meta); /** * middleware to check if access token is present @@ -8,7 +9,7 @@ const logger = require("../utils/logger")(module); * @param {typedefs.Res} res * @param {typedefs.Next} next */ -const isAuthenticated = (req, res, next) => { +export const isAuthenticated = (req, res, next) => { if (req.session.accessToken) { req.sessHeaders = { "Authorization": `Bearer ${req.session.accessToken}`, @@ -30,7 +31,3 @@ const isAuthenticated = (req, res, next) => { }); } } - -module.exports = { - isAuthenticated, -} diff --git a/migrations/20240727162141-create-playlists.js b/migrations/20240727162141-create-playlists.js index 372f380..875f096 100644 --- a/migrations/20240727162141-create-playlists.js +++ b/migrations/20240727162141-create-playlists.js @@ -1,7 +1,7 @@ "use strict"; /** @type {import("sequelize-cli").Migration} */ -module.exports = { - async up(queryInterface, Sequelize) { +export default { + up: async function (queryInterface, Sequelize) { await queryInterface.createTable("playlists", { id: { allowNull: false, @@ -28,7 +28,7 @@ module.exports = { } }); }, - async down(queryInterface, Sequelize) { + down: async function (queryInterface, Sequelize) { await queryInterface.dropTable("playlists"); } }; diff --git a/migrations/20240730101615-create-links.js b/migrations/20240730101615-create-links.js index 3c6823d..52aed92 100644 --- a/migrations/20240730101615-create-links.js +++ b/migrations/20240730101615-create-links.js @@ -1,7 +1,7 @@ "use strict"; /** @type {import("sequelize-cli").Migration} */ -module.exports = { - async up(queryInterface, Sequelize) { +export default { + up: async function (queryInterface, Sequelize) { await queryInterface.createTable("links", { id: { allowNull: false, @@ -28,7 +28,7 @@ module.exports = { } }); }, - async down(queryInterface, Sequelize) { + down: async function (queryInterface, Sequelize) { await queryInterface.dropTable("links"); } }; diff --git a/models/index.js b/models/index.js index cf8b61f..ec21585 100644 --- a/models/index.js +++ b/models/index.js @@ -1,11 +1,16 @@ "use strict"; -const fs = require("fs"); -const path = require("path"); -const Sequelize = require("sequelize"); -const logger = require("../utils/logger")(module); -const basename = path.basename(__filename); +import { readdirSync } from "fs"; +import { basename as _basename } from "path"; +const basename = _basename(import.meta.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 config = require(__dirname + "/../config/sequelize.js")[env]; +const config = seqConfig[env]; const db = {}; let sequelize; @@ -26,15 +31,22 @@ if (config.use_env_variable) { })(); // Read model definitions from folder -fs - .readdirSync(__dirname) - .filter(file => { - return (file.indexOf(".") !== 0) && (file !== basename) && (file.slice(-3) === ".js"); - }) - .forEach(file => { - const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes); - db[model.name] = model; - }); +const modelFiles = readdirSync(import.meta.dirname) + .filter( + (file) => file.indexOf('.') !== 0 + && file !== basename + && file.slice(-3) === '.js', + ); + +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 Object.keys(db).forEach(modelName => { @@ -43,7 +55,9 @@ Object.keys(db).forEach(modelName => { } }); +// clean ts up db.sequelize = sequelize; db.Sequelize = Sequelize; - -module.exports = db; +export { sequelize as sequelize }; +export { Sequelize as Sequelize }; +export default db; diff --git a/models/links.js b/models/links.js index 4ac3b36..9e83b9e 100644 --- a/models/links.js +++ b/models/links.js @@ -1,8 +1,6 @@ "use strict"; -const { - Model -} = require("sequelize"); -module.exports = (sequelize, DataTypes) => { +import { Model } from "sequelize"; +export default (sequelize, DataTypes) => { class links extends Model { /** * Helper method for defining associations. @@ -22,4 +20,4 @@ module.exports = (sequelize, DataTypes) => { modelName: "links", }); return links; -}; \ No newline at end of file +}; diff --git a/models/playlists.js b/models/playlists.js index e696665..57fa253 100644 --- a/models/playlists.js +++ b/models/playlists.js @@ -1,8 +1,6 @@ "use strict"; -const { - Model -} = require("sequelize"); -module.exports = (sequelize, DataTypes) => { +import { Model } from "sequelize"; +export default (sequelize, DataTypes) => { class playlists extends Model { /** * Helper method for defining associations. @@ -22,4 +20,4 @@ module.exports = (sequelize, DataTypes) => { modelName: "playlists", }); return playlists; -}; \ No newline at end of file +}; diff --git a/package.json b/package.json index 7c096f7..f66eba5 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,8 @@ "name": "spotify-manager", "version": "0", "description": "Personal Spotify playlist manager", - "main": "index.js", + "exports": "./index.js", + "type": "module", "scripts": { "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", diff --git a/routes/auth.js b/routes/auth.js index f3954ed..db2d4fd 100644 --- a/routes/auth.js +++ b/routes/auth.js @@ -1,8 +1,9 @@ -const router = require("express").Router(); +import { Router } from "express"; +const router = Router(); -const { login, callback, refresh, logout } = require("../controllers/auth"); -const { isAuthenticated } = require("../middleware/authCheck"); -const validator = require("../validators"); +import { login, callback, refresh, logout } from "../controllers/auth.js"; +import { isAuthenticated } from "../middleware/authCheck.js"; +import { validate } from "../validators/index.js"; router.get( "/login", @@ -25,4 +26,4 @@ router.get( logout ); -module.exports = router; +export default router; diff --git a/routes/operations.js b/routes/operations.js index 3a0efab..bd4f95a 100644 --- a/routes/operations.js +++ b/routes/operations.js @@ -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"); -const { validate } = require("../validators"); -const { createLinkValidator, removeLinkValidator, populateSingleLinkValidator, pruneSingleLinkValidator } = require("../validators/operations"); +import { updateUser, fetchUser, createLink, removeLink, populateSingleLink, pruneSingleLink } from "../controllers/operations.js"; +import { createLinkValidator, removeLinkValidator, populateSingleLinkValidator, pruneSingleLinkValidator } from "../validators/operations.js"; + +import { validate } from "../validators/index.js"; router.put( "/update", @@ -42,4 +44,4 @@ router.put( pruneSingleLink ); -module.exports = router; +export default router; diff --git a/routes/playlists.js b/routes/playlists.js index e386dea..5e19e48 100644 --- a/routes/playlists.js +++ b/routes/playlists.js @@ -1,8 +1,10 @@ -const router = require("express").Router(); +import { Router } from "express"; +const router = Router(); -const { fetchUserPlaylists, fetchPlaylistDetails } = require("../controllers/playlists"); -const { getPlaylistDetailsValidator } = require("../validators/playlists"); -const { validate } = require("../validators"); +import { fetchUserPlaylists, fetchPlaylistDetails } from "../controllers/playlists.js"; +import { getPlaylistDetailsValidator } from "../validators/playlists.js"; + +import { validate } from "../validators/index.js"; router.get( "/me", @@ -16,4 +18,4 @@ router.get( fetchPlaylistDetails ); -module.exports = router; +export default router; diff --git a/typedefs.js b/typedefs.js index 29f1495..0b4c01f 100644 --- a/typedefs.js +++ b/typedefs.js @@ -1,10 +1,10 @@ /** * @typedef {import("module")} Module - * + * * @typedef {import("express").Request} Req * @typedef {import("express").Response} Res * @typedef {import("express").NextFunction} Next - * + * * @typedef {{ * type: string, * is_local: boolean, @@ -14,25 +14,25 @@ * title?: string, * duration?: number * }} URIObject - * + * * @typedef {{ * username: string, * uri: string * }} User - * + * * @typedef {{ * name: string, * uri: string, * }} SimplifiedPlaylist - * + * * @typedef {{ * name: string * }} Album - * + * * @typedef {{ * name: string * }} Artist - * + * * @typedef {{ * uri: string, * name: string, @@ -40,18 +40,18 @@ * album: Album, * is_local: boolean, * }} Track - * + * * @typedef {{ * added_at: string, * track: Track, * }} PlaylistTrack - * + * * @typedef {{ * url: string, * height: number, * width: number * }} ImageObject - * + * * @typedef {{ * uri: string, * name: string, @@ -64,4 +64,4 @@ * }} PlaylistDetails */ -exports.unused = {}; \ No newline at end of file +export default {}; diff --git a/utils/flake.js b/utils/flake.js index e229209..15cb218 100644 --- a/utils/flake.js +++ b/utils/flake.js @@ -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; - -module.exports = { - sleep, randomBool -}; +export const randomBool = (chance_of_failure = 0.25) => Math.random() < chance_of_failure; diff --git a/utils/generateRandString.js b/utils/generateRandString.js index 119aa17..8322637 100644 --- a/utils/generateRandString.js +++ b/utils/generateRandString.js @@ -3,7 +3,7 @@ * @param {number} length The length of the string * @return {string} The generated string */ -module.exports = (length) => { +export default (length) => { const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let text = ""; diff --git a/utils/graph.js b/utils/graph.js index 8dcceea..9276245 100644 --- a/utils/graph.js +++ b/utils/graph.js @@ -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. @@ -21,7 +22,7 @@ const typedefs = require("../typedefs"); * console.log(g.detectCycle()); // true * ``` */ -class myGraph { +export class myGraph { /** * @param {string[]} nodes Graph nodes IDs * @param {{ from: string, to: string }[]} edges Graph edges b/w nodes @@ -156,4 +157,4 @@ class myGraph { } } -module.exports = myGraph; +export default myGraph; diff --git a/utils/jsonTransformer.js b/utils/jsonTransformer.js index 33444c9..54bbda3 100644 --- a/utils/jsonTransformer.js +++ b/utils/jsonTransformer.js @@ -5,7 +5,7 @@ * @param {string} delimiter Delimiter of final string * @returns {string} */ -const getNestedValuesString = (obj, delimiter = ", ") => { +export const getNestedValuesString = (obj, delimiter = ", ") => { let values = []; for (key in obj) { if (typeof obj[key] !== "object") { @@ -17,7 +17,3 @@ const getNestedValuesString = (obj, delimiter = ", ") => { return values.join(delimiter); } - -module.exports = { - getNestedValuesString -} diff --git a/utils/logger.js b/utils/logger.js index 96f4412..d96b546 100644 --- a/utils/logger.js +++ b/utils/logger.js @@ -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 typedefs = require("../typedefs"); - const getLabel = (callingModule) => { if (!callingModule.filename) return "repl"; 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 - * @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({ levels: config.npm.levels, format: combine( @@ -49,12 +49,12 @@ const curriedLogger = (callingModule) => { transports: [ new transports.Console({ level: "info" }), new transports.File({ - filename: __dirname + "/../logs/debug.log", + filename: import.meta.dirname + "/../logs/debug.log", level: "debug", maxsize: 10485760, }), new transports.File({ - filename: __dirname + "/../logs/error.log", + filename: import.meta.dirname + "/../logs/error.log", level: "error", maxsize: 1048576, }), @@ -64,4 +64,4 @@ const curriedLogger = (callingModule) => { return winstonLogger; } -module.exports = curriedLogger; +export default curriedLogger; diff --git a/utils/spotifyUriTransformer.js b/utils/spotifyUriTransformer.js index 9611607..7c76bd9 100644 --- a/utils/spotifyUriTransformer.js +++ b/utils/spotifyUriTransformer.js @@ -1,4 +1,4 @@ -const typedefs = require("../typedefs"); +import * as typedefs from "../typedefs.js"; /** @type {RegExp} */ const base62Pattern = /^[A-Za-z0-9]+$/; @@ -10,7 +10,7 @@ const base62Pattern = /^[A-Za-z0-9]+$/; * @returns {typedefs.URIObject} * @throws {TypeError} If the input is not a valid Spotify URI */ -const parseSpotifyURI = (uri) => { +export const parseSpotifyURI = (uri) => { const parts = uri.split(":"); if (parts[0] !== "spotify") { @@ -59,7 +59,7 @@ const parseSpotifyURI = (uri) => { * @returns {typedefs.URIObject} * @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 standardPattern = /^https:\/\/open\.spotify\.com\/([^\/]+)\/([^\/?]+)/; @@ -106,7 +106,7 @@ const parseSpotifyLink = (link) => { * @param {typedefs.URIObject} uriObj * @returns {string} */ -const buildSpotifyURI = (uriObj) => { +export const buildSpotifyURI = (uriObj) => { if (uriObj.is_local) { const artist = encodeURIComponent(uriObj.artist ?? ""); const album = encodeURIComponent(uriObj.album ?? ""); @@ -122,7 +122,7 @@ const buildSpotifyURI = (uriObj) => { * @param {typedefs.URIObject} uriObj * @returns {string} */ -const buildSpotifyLink = (uriObj) => { +export const buildSpotifyLink = (uriObj) => { if (uriObj.is_local) { const artist = encodeURIComponent(uriObj.artist ?? ""); const album = encodeURIComponent(uriObj.album ?? ""); @@ -132,10 +132,3 @@ const buildSpotifyLink = (uriObj) => { } return `https://open.spotify.com/${uriObj.type}/${uriObj.id}` } - -module.exports = { - parseSpotifyURI, - parseSpotifyLink, - buildSpotifyURI, - buildSpotifyLink -} diff --git a/validators/index.js b/validators/index.js index c0ff66e..75f09f6 100644 --- a/validators/index.js +++ b/validators/index.js @@ -1,9 +1,9 @@ -const { validationResult } = require("express-validator"); +import { validationResult } from "express-validator"; -const { getNestedValuesString } = require("../utils/jsonTransformer"); -const logger = require("../utils/logger")(module); - -const typedefs = require("../typedefs"); +import * as typedefs from "../typedefs.js"; +import { getNestedValuesString } from "../utils/jsonTransformer.js"; +import curriedLogger from "../utils/logger.js"; +const logger = curriedLogger(import.meta); /** * 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.Next} next */ -const validate = (req, res, next) => { +export const validate = (req, res, next) => { const errors = validationResult(req); if (errors.isEmpty()) { return next(); @@ -40,7 +40,3 @@ const validate = (req, res, next) => { logger.warn("invalid request", { extractedErrors }); return; } - -module.exports = { - validate -}; diff --git a/validators/operations.js b/validators/operations.js index 82e5111..e0b7041 100644 --- a/validators/operations.js +++ b/validators/operations.js @@ -1,13 +1,12 @@ -const { body, header, param, query } = require("express-validator"); - -const typedefs = require("../typedefs"); +import { body, header, param, query } from "express-validator"; +import * as typedefs from "../typedefs.js"; /** * @param {typedefs.Req} req * @param {typedefs.Res} res * @param {typedefs.Next} next */ -const createLinkValidator = async (req, res, next) => { +export const createLinkValidator = async (req, res, next) => { await body("from") .notEmpty() .withMessage("from not defined in body") @@ -23,9 +22,6 @@ const createLinkValidator = async (req, res, next) => { next(); } -module.exports = { - createLinkValidator, - removeLinkValidator: createLinkValidator, - populateSingleLinkValidator: createLinkValidator, - pruneSingleLinkValidator: createLinkValidator, -} +export { createLinkValidator as removeLinkValidator }; +export { createLinkValidator as populateSingleLinkValidator }; +export { createLinkValidator as pruneSingleLinkValidator }; diff --git a/validators/playlists.js b/validators/playlists.js index c9cbad4..07a1100 100644 --- a/validators/playlists.js +++ b/validators/playlists.js @@ -1,13 +1,12 @@ -const { body, header, param, query } = require("express-validator"); - -const typedefs = require("../typedefs"); +import { body, header, param, query } from "express-validator"; +import * as typedefs from "../typedefs.js"; /** * @param {typedefs.Req} req * @param {typedefs.Res} res * @param {typedefs.Next} next */ -const getPlaylistDetailsValidator = async (req, res, next) => { +export const getPlaylistDetailsValidator = async (req, res, next) => { await query("playlist_link") .notEmpty() .withMessage("playlist_link not defined in query") @@ -16,7 +15,3 @@ const getPlaylistDetailsValidator = async (req, res, next) => { .run(req); next(); } - -module.exports = { - getPlaylistDetailsValidator -}