215 lines
5.2 KiB
JavaScript
215 lines
5.2 KiB
JavaScript
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;
|