first commit

This commit is contained in:
Nathan FONTEYNE 2026-07-08 11:05:21 +02:00
commit 244cdbedd7
44 changed files with 3386 additions and 0 deletions

View file

@ -0,0 +1,8 @@
const pool = require('../db/pool');
async function findAll() {
const { rows } = await pool.query('SELECT id, name FROM instruments ORDER BY name');
return rows;
}
module.exports = { findAll };

View file

@ -0,0 +1,118 @@
const pool = require('../db/pool');
async function findNext() {
const { rows } = await pool.query(
`SELECT id, name, venue, concert_date, created_by, created_at, updated_at
FROM setlists
WHERE concert_date >= CURRENT_DATE
ORDER BY concert_date ASC
LIMIT 1`
);
return rows[0] || null;
}
async function findHistory() {
const { rows } = await pool.query(
`SELECT id, name, venue, concert_date, created_by, created_at, updated_at
FROM setlists
WHERE concert_date < CURRENT_DATE
ORDER BY concert_date DESC`
);
return rows;
}
async function findById(id) {
const { rows } = await pool.query(
`SELECT id, name, venue, concert_date, created_by, created_at, updated_at
FROM setlists WHERE id = $1`,
[id]
);
return rows[0] || null;
}
async function findSongs(setlistId) {
const { rows } = await pool.query(
`SELECT ss.id, ss.setlist_id, ss.song_id, s.title, s.artist, ss.position, ss.note, ss.is_encore
FROM setlist_songs ss
JOIN songs s ON s.id = ss.song_id
WHERE ss.setlist_id = $1
ORDER BY ss.is_encore, ss.position`,
[setlistId]
);
return rows;
}
async function create({ name, venue, concertDate, createdBy }) {
const { rows } = await pool.query(
`INSERT INTO setlists (name, venue, concert_date, created_by)
VALUES ($1, $2, $3, $4)
RETURNING id, name, venue, concert_date, created_by, created_at, updated_at`,
[name || null, venue || null, concertDate, createdBy]
);
return rows[0];
}
async function update(id, { name, venue, concertDate }) {
const { rows } = await pool.query(
`UPDATE setlists SET name = $2, venue = $3, concert_date = $4, updated_at = now()
WHERE id = $1
RETURNING id, name, venue, concert_date, created_by, created_at, updated_at`,
[id, name || null, venue || null, concertDate]
);
return rows[0] || null;
}
async function remove(id) {
await pool.query('DELETE FROM setlists WHERE id = $1', [id]);
}
async function replaceSongs(setlistId, songEntries) {
const client = await pool.connect();
try {
await client.query('BEGIN');
await client.query('DELETE FROM setlist_songs WHERE setlist_id = $1', [setlistId]);
for (const entry of songEntries) {
await client.query(
`INSERT INTO setlist_songs (setlist_id, song_id, position, note, is_encore)
VALUES ($1, $2, $3, $4, $5)`,
[setlistId, entry.songId, entry.position, entry.note || null, !!entry.isEncore]
);
}
await client.query('COMMIT');
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
async function addSong(setlistId, { songId, position, note, isEncore }) {
const { rows } = await pool.query(
`INSERT INTO setlist_songs (setlist_id, song_id, position, note, is_encore)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, setlist_id, song_id, position, note, is_encore`,
[setlistId, songId, position, note || null, !!isEncore]
);
return rows[0];
}
async function removeSong(setlistId, setlistSongId) {
await pool.query('DELETE FROM setlist_songs WHERE id = $1 AND setlist_id = $2', [
setlistSongId,
setlistId,
]);
}
module.exports = {
findNext,
findHistory,
findById,
findSongs,
create,
update,
remove,
replaceSongs,
addSong,
removeSong,
};

View file

@ -0,0 +1,86 @@
const pool = require('../db/pool');
async function findAll() {
const { rows } = await pool.query(`
SELECT s.id, s.title, s.artist, s.notes, s.created_at,
COUNT(st.id)::int AS tutorial_count
FROM songs s
LEFT JOIN song_tutorials st ON st.song_id = s.id
GROUP BY s.id
ORDER BY s.title
`);
return rows;
}
async function findById(id) {
const { rows } = await pool.query(
'SELECT id, title, artist, notes, added_by, created_at, updated_at FROM songs WHERE id = $1',
[id]
);
return rows[0] || null;
}
async function create({ title, artist, notes, addedBy }) {
const { rows } = await pool.query(
`INSERT INTO songs (title, artist, notes, added_by)
VALUES ($1, $2, $3, $4)
RETURNING id, title, artist, notes, added_by, created_at, updated_at`,
[title, artist, notes || null, addedBy]
);
return rows[0];
}
async function update(id, { title, artist, notes }) {
const { rows } = await pool.query(
`UPDATE songs SET title = $2, artist = $3, notes = $4, updated_at = now()
WHERE id = $1
RETURNING id, title, artist, notes, added_by, created_at, updated_at`,
[id, title, artist, notes || null]
);
return rows[0] || null;
}
async function remove(id) {
await pool.query('DELETE FROM songs WHERE id = $1', [id]);
}
async function findTutorials(songId) {
const { rows } = await pool.query(
`SELECT st.id, st.song_id, st.instrument_id, i.name AS instrument_name,
st.url, st.label, st.added_by, st.created_at
FROM song_tutorials st
JOIN instruments i ON i.id = st.instrument_id
WHERE st.song_id = $1
ORDER BY i.name`,
[songId]
);
return rows;
}
async function addTutorial(songId, { instrumentId, url, label, addedBy }) {
const { rows } = await pool.query(
`INSERT INTO song_tutorials (song_id, instrument_id, url, label, added_by)
VALUES ($1, $2, $3, $4, $5)
RETURNING id, song_id, instrument_id, url, label, added_by, created_at`,
[songId, instrumentId, url, label || null, addedBy]
);
return rows[0];
}
async function removeTutorial(songId, tutorialId) {
await pool.query('DELETE FROM song_tutorials WHERE id = $1 AND song_id = $2', [
tutorialId,
songId,
]);
}
module.exports = {
findAll,
findById,
create,
update,
remove,
findTutorials,
addTutorial,
removeTutorial,
};

View file

@ -0,0 +1,127 @@
const pool = require('../db/pool');
async function findAll() {
const { rows } = await pool.query(`
SELECT sg.id, sg.title, sg.artist, sg.youtube_url, sg.status, sg.promoted_song_id,
sg.created_at, sg.suggested_by, u.name AS suggested_by_name,
COUNT(*) FILTER (WHERE v.vote = 'approve')::int AS approve_count,
COUNT(*) FILTER (WHERE v.vote = 'reject')::int AS reject_count
FROM suggestions sg
JOIN users u ON u.id = sg.suggested_by
LEFT JOIN suggestion_votes v ON v.suggestion_id = sg.id
GROUP BY sg.id, u.name
ORDER BY sg.created_at DESC
`);
return rows;
}
async function findById(id) {
const { rows } = await pool.query(
`SELECT sg.id, sg.title, sg.artist, sg.youtube_url, sg.status, sg.promoted_song_id,
sg.created_at, sg.suggested_by, u.name AS suggested_by_name
FROM suggestions sg
JOIN users u ON u.id = sg.suggested_by
WHERE sg.id = $1`,
[id]
);
return rows[0] || null;
}
async function findVotes(suggestionId) {
const { rows } = await pool.query(
`SELECT v.id, v.user_id, u.name AS voter_name, v.vote, v.comment, v.created_at, v.updated_at
FROM suggestion_votes v
JOIN users u ON u.id = v.user_id
WHERE v.suggestion_id = $1
ORDER BY v.created_at`,
[suggestionId]
);
return rows;
}
async function create({ title, artist, youtubeUrl, suggestedBy }) {
const { rows } = await pool.query(
`INSERT INTO suggestions (title, artist, youtube_url, suggested_by)
VALUES ($1, $2, $3, $4)
RETURNING id, title, artist, youtube_url, status, promoted_song_id, created_at, suggested_by`,
[title, artist || null, youtubeUrl, suggestedBy]
);
return rows[0];
}
async function updateStatus(id, status) {
const { rows } = await pool.query(
`UPDATE suggestions SET status = $2, updated_at = now() WHERE id = $1
RETURNING id, title, artist, youtube_url, status, promoted_song_id, created_at, suggested_by`,
[id, status]
);
return rows[0] || null;
}
async function remove(id) {
await pool.query('DELETE FROM suggestions WHERE id = $1', [id]);
}
async function upsertVote(suggestionId, userId, { vote, comment }) {
const { rows } = await pool.query(
`INSERT INTO suggestion_votes (suggestion_id, user_id, vote, comment)
VALUES ($1, $2, $3, $4)
ON CONFLICT (suggestion_id, user_id)
DO UPDATE SET vote = $3, comment = $4, updated_at = now()
RETURNING id, suggestion_id, user_id, vote, comment, created_at, updated_at`,
[suggestionId, userId, vote, comment || null]
);
return rows[0];
}
async function removeVote(suggestionId, userId) {
await pool.query('DELETE FROM suggestion_votes WHERE suggestion_id = $1 AND user_id = $2', [
suggestionId,
userId,
]);
}
async function promoteToSong(suggestionId, addedBy) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const { rows: sugRows } = await client.query('SELECT * FROM suggestions WHERE id = $1 FOR UPDATE', [
suggestionId,
]);
const suggestion = sugRows[0];
if (!suggestion) {
await client.query('ROLLBACK');
return null;
}
const { rows: songRows } = await client.query(
`INSERT INTO songs (title, artist, added_by)
VALUES ($1, $2, $3)
RETURNING id, title, artist, notes, added_by, created_at, updated_at`,
[suggestion.title, suggestion.artist || suggestion.title, addedBy]
);
const song = songRows[0];
await client.query(
`UPDATE suggestions SET status = 'approved', promoted_song_id = $2, updated_at = now() WHERE id = $1`,
[suggestionId, song.id]
);
await client.query('COMMIT');
return song;
} catch (err) {
await client.query('ROLLBACK');
throw err;
} finally {
client.release();
}
}
module.exports = {
findAll,
findById,
findVotes,
create,
updateStatus,
remove,
upsertVote,
removeVote,
promoteToSong,
};

View file

@ -0,0 +1,23 @@
const pool = require('../db/pool');
async function findById(id) {
const { rows } = await pool.query(
'SELECT id, authentik_sub, name, email, is_admin, created_at FROM users WHERE id = $1',
[id]
);
return rows[0] || null;
}
async function upsertFromClaims({ sub, name, email, isAdmin }) {
const { rows } = await pool.query(
`INSERT INTO users (authentik_sub, name, email, is_admin)
VALUES ($1, $2, $3, $4)
ON CONFLICT (authentik_sub)
DO UPDATE SET name = $2, email = $3, is_admin = $4, updated_at = now()
RETURNING id, authentik_sub, name, email, is_admin, created_at`,
[sub, name, email, isAdmin]
);
return rows[0];
}
module.exports = { findById, upsertFromClaims };