Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions backend/src/Database/connection.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@
const mongoose = require('mongoose');

let isConnected = false;

const connectDB = async () => {
if (isConnected) {
return;
}

try {
const mongoUrl = process.env.MONGODB_URI || process.env.MONGO_URL;
if (!mongoUrl) {
throw new Error("MONGODB_URI or MONGO_URL not found in environment variables");
}

await mongoose.connect(mongoUrl);
await mongoose.connect(mongoUrl, {
maxPoolSize: 10,
serverSelectionTimeoutMS: 5000,
});

isConnected = true;
console.log(`MongoDB connected successfully ${mongoose.connection.host}`);
} catch (err) {
console.error(`MongoDB connection failed: ${err.message}`);
// In production/serverless, we shouldn't necessarily exit the process
// instead let the error propagate or handled by the app
if (process.env.NODE_ENV !== 'production') {
process.exit(1);
}
Expand Down
22 changes: 17 additions & 5 deletions backend/src/__tests__/cacheManager.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,27 @@ describe('CacheManager', () => {
});

describe('purgePattern', () => {
it('should delete keys matching a pattern', async () => {
it('should delete keys matching a pattern using scanIterator', async () => {
const mockKeys = ['key1', 'key2'];
const spyKeys = vi.spyOn(redisClient, 'keys').mockResolvedValue(mockKeys);
const spyDel = vi.spyOn(redisClient, 'del').mockResolvedValue(2);

// Mocking the async iterator for scanIterator
const spyScan = vi.spyOn(redisClient, 'scanIterator').mockReturnValue((async function* () {
for (const key of mockKeys) {
yield key;
}
})());

const spyDel = vi.spyOn(redisClient, 'del').mockResolvedValue(1);

await CacheManager.purgePattern('pattern:*');

expect(spyKeys).toHaveBeenCalledWith('pattern:*');
expect(spyDel).toHaveBeenCalledWith(mockKeys);
expect(spyScan).toHaveBeenCalledWith({
MATCH: 'pattern:*',
COUNT: 100
});
expect(spyDel).toHaveBeenCalledTimes(2);
expect(spyDel).toHaveBeenCalledWith('key1');
expect(spyDel).toHaveBeenCalledWith('key2');
});
});
});
1 change: 1 addition & 0 deletions backend/src/controllers/song.controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ const createSong = async (req, res, next) => {
if (!title) return res.status(400).json({ success: false, message: "Title is required" });
if (!artist) return res.status(400).json({ success: false, message: "Artist is required" });
if (!duration) return res.status(400).json({ success: false, message: "Duration is required" });
if (!albumId || albumId === "none") return res.status(400).json({ success: false, message: "Album is required" });

const audioUrl = await uploadToCloudinary(req.files.audioFile);
const imageUrl = await uploadToCloudinary(req.files.imageFile);
Expand Down
19 changes: 14 additions & 5 deletions backend/src/lib/cacheManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,23 @@ const CacheManager = {
},

/**
* Purges keys using wildcards (Note: keys() is O(N), use sparingly)
* Purges keys using non-blocking SCAN (O(1) per step)
*/
async purgePattern(pattern) {
try {
const keys = await redisClient.keys(pattern);
if (keys.length > 0) {
await redisClient.del(keys);
console.log(`Purged ${keys.length} keys matching ${pattern}`);
let totalPurged = 0;
const iterator = await redisClient.scanIterator({
MATCH: pattern,
COUNT: 100
});

for await (const key of iterator) {
await redisClient.del(key);
totalPurged++;
}

if (totalPurged > 0) {
console.log(`Purged ${totalPurged} keys matching ${pattern}`);
}
} catch (err) {
console.error(`Cache Purge Error [${pattern}]:`, err);
Expand Down
2 changes: 1 addition & 1 deletion backend/src/models/song.model.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const songSchema = new mongoose.Schema({
albumId: {
type: mongoose.Schema.Types.ObjectId,
ref: 'Album',
required: false,
required: true,
},
creator: {
type: String, // clerkId
Expand Down
11 changes: 6 additions & 5 deletions frontend/src/pages/admin/components/AddSongDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,16 @@ const AddSongDialog = () => {
return toast.error("Artist is required");
}

if (!newSong.album || newSong.album === "none") {
return toast.error("Please select an album");
}

const formData = new FormData();

formData.append("title", newSong.title);
formData.append("artist", newSong.artist);
formData.append("duration", newSong.duration);
if (newSong.album && newSong.album !== "none") {
formData.append("albumId", newSong.album);
}
formData.append("albumId", newSong.album);

formData.append("audioFile", files.audio);
formData.append("imageFile", files.image);
Expand Down Expand Up @@ -354,7 +356,7 @@ const AddSongDialog = () => {
</div>

<div className='space-y-2'>
<label className='text-sm font-medium'>Album (Optional)</label>
<label className='text-sm font-medium'>Album</label>
<Select
value={newSong.album}
onValueChange={(value) => setNewSong({ ...newSong, album: value })}
Expand All @@ -363,7 +365,6 @@ const AddSongDialog = () => {
<SelectValue placeholder='Select album' />
</SelectTrigger>
<SelectContent className='bg-zinc-800 border-zinc-700'>
<SelectItem value='none'>No Album (Single)</SelectItem>
{albums.map((album) => (
<SelectItem key={album._id} value={album._id}>
{album.title}
Expand Down
10 changes: 5 additions & 5 deletions frontend/src/pages/admin/components/EditSongDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,10 @@ const EditSongDialog = ({ song }: EditSongDialogProps) => {
const handleSubmit = async () => {
try {
const data = new FormData();
data.append("title", formData.title);
data.append("artist", formData.artist);
data.append("duration", formData.duration);
if (!formData.title.trim()) return toast.error("Title is required");
if (!formData.artist.trim()) return toast.error("Artist is required");
if (!formData.album || formData.album === "none") return toast.error("Album is required");

if (formData.album && formData.album !== "none") data.append("albumId", formData.album);
if (files.image) data.append("imageFile", files.image);

Expand Down Expand Up @@ -138,7 +139,7 @@ const EditSongDialog = ({ song }: EditSongDialogProps) => {
</div>

<div className='space-y-2'>
<label className='text-sm font-medium' htmlFor='album'>Album (Optional)</label>
<label className='text-sm font-medium' htmlFor='album'>Album</label>
<Select
value={formData.album}
onValueChange={(value) => setFormData({ ...formData, album: value })}
Expand All @@ -147,7 +148,6 @@ const EditSongDialog = ({ song }: EditSongDialogProps) => {
<SelectValue placeholder='Select album' />
</SelectTrigger>
<SelectContent className='bg-zinc-800 border-zinc-700'>
<SelectItem value='none'>No Album</SelectItem>
{albums.map((album) => (
<SelectItem key={album._id} value={album._id}>
{album.title}
Expand Down
Loading