Files

215 lines
5.2 KiB
JavaScript
Raw Permalink Normal View History

2026-01-27 18:04:50 -06:00
const express = require("express");
const router = express.Router();
const { query } = require("../db");
const { v4: uuidv4 } = require("uuid");
const {
success,
error,
notFound,
badRequest,
} = require("../utils/responseHandler");
const {
buildWhereClause,
buildPagination,
buildSearchCondition,
} = require("../utils/queryBuilder");
const { authenticate } = require("../middleware/auth");
// Common SQL fragment
const SELECT_SONG_FIELDS = "SELECT *, chords as key_chord FROM songs";
// GET search songs (for worship list song picker)
router.get("/search", async (req, res) => {
try {
const { q } = req.query;
if (!q || q.trim() === "") {
return success(res, { songs: [], total: 0 });
}
const searchTerm = `%${q.toLowerCase()}%`;
const searchCondition = buildSearchCondition(
searchTerm,
["title", "artist", "singer"],
1,
);
const result = await query(
`${SELECT_SONG_FIELDS}
WHERE ${searchCondition}
ORDER BY title ASC LIMIT 20`,
[searchTerm],
);
success(res, { songs: result.rows, total: result.rowCount });
} catch (err) {
error(res, "Failed to search songs");
}
});
// GET all songs
router.get("/", async (req, res) => {
try {
const { search, artist, band, limit = 100, offset = 0 } = req.query;
const params = [];
const conditions = [];
if (search) {
params.push(`%${search.toLowerCase()}%`);
conditions.push(
`(LOWER(title) LIKE $${params.length} OR LOWER(lyrics) LIKE $${params.length})`,
);
}
if (artist) {
params.push(`%${artist.toLowerCase()}%`);
conditions.push(`LOWER(artist) LIKE $${params.length}`);
}
if (band) {
params.push(`%${band.toLowerCase()}%`);
conditions.push(`LOWER(band) LIKE $${params.length}`);
}
const whereClause = buildWhereClause(conditions);
const { clause: paginationClause, params: paginationParams } =
buildPagination(limit, offset, params.length + 1);
const result = await query(
`${SELECT_SONG_FIELDS}${whereClause} ORDER BY title ASC${paginationClause}`,
[...params, ...paginationParams],
);
success(res, { songs: result.rows, total: result.rowCount });
} catch (err) {
error(res, "Failed to fetch songs");
}
});
// GET single song by ID
router.get("/:id", async (req, res) => {
try {
const result = await query(`${SELECT_SONG_FIELDS} WHERE id = $1`, [
req.params.id,
]);
if (result.rows.length === 0) {
return notFound(res, "Song");
}
success(res, { song: result.rows[0] });
} catch (err) {
error(res, "Failed to fetch song");
}
});
// POST create new song
router.post("/", authenticate, async (req, res) => {
try {
const { title, artist, band, singer, lyrics, chords, key_chord, memo } =
req.body;
if (!title) {
return badRequest(res, "Title is required");
}
const id = uuidv4();
const now = Math.floor(Date.now() / 1000);
const chordsValue = chords || key_chord || "";
const result = await query(
`INSERT INTO songs (id, title, artist, band, singer, lyrics, chords, memo, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
RETURNING *, chords as key_chord`,
[
id,
title,
artist || "",
band || "",
singer || "",
lyrics || "",
chordsValue,
memo || "",
now,
now,
],
);
success(res, { song: result.rows[0] }, 201);
} catch (err) {
error(res, "Failed to create song");
}
});
// PUT update song
router.put("/:id", authenticate, async (req, res) => {
try {
const { title, artist, band, singer, lyrics, chords, key_chord, memo } =
req.body;
const now = Math.floor(Date.now() / 1000);
const chordsValue = chords || key_chord;
const result = await query(
`UPDATE songs
SET title = COALESCE($1, title),
artist = COALESCE($2, artist),
band = COALESCE($3, band),
singer = COALESCE($4, singer),
lyrics = COALESCE($5, lyrics),
chords = COALESCE($6, chords),
memo = COALESCE($7, memo),
updated_at = $8
WHERE id = $9
RETURNING *, chords as key_chord`,
[
title,
artist,
band,
singer,
lyrics,
chordsValue,
memo,
now,
req.params.id,
],
);
if (result.rows.length === 0) {
return notFound(res, "Song");
}
success(res, { song: result.rows[0] });
} catch (err) {
error(res, "Failed to update song");
}
});
// DELETE song
router.delete("/:id", authenticate, async (req, res) => {
try {
const result = await query("DELETE FROM songs WHERE id = $1 RETURNING id", [
req.params.id,
]);
if (result.rows.length === 0) {
return notFound(res, "Song");
}
success(res, { message: "Song deleted" });
} catch (err) {
error(res, "Failed to delete song");
}
});
// GET song count
router.get("/stats/count", async (req, res) => {
try {
const result = await query("SELECT COUNT(*) as count FROM songs");
success(res, { count: parseInt(result.rows[0].count) });
} catch (err) {
error(res, "Failed to count songs");
}
});
module.exports = router;