b05f6b9da0
Jenkins Production Deployment
Co-authored-by: Patrick Müller <mail@pmueller.me> Reviewed-on: #7 Co-authored-by: Patrick Mueller <patrick@mueller-patrick.tech> Co-committed-by: Patrick Mueller <patrick@mueller-patrick.tech>
105 lines
3.4 KiB
TypeScript
105 lines
3.4 KiB
TypeScript
import {NachklangFeedbackDB} from '../Feedback.db';
|
|
import {EventConfig, EventSummary, Question, Song} from '../feedback.interface';
|
|
|
|
/**
|
|
* Returns all events currently eligible to receive feedback:
|
|
* published, on or after their concert day, and before the deadline.
|
|
*/
|
|
export const getEligibleEvents = async (): Promise<EventSummary[]> => {
|
|
let conn = await NachklangFeedbackDB.getConnection();
|
|
try {
|
|
const query = `
|
|
SELECT slug, name, subtitle, event_date, feedback_deadline, poster_image_url
|
|
FROM events
|
|
WHERE is_published = 1 AND event_date <= CURDATE() AND feedback_deadline >= NOW()
|
|
ORDER BY event_date DESC`;
|
|
const rows = await conn.query(query);
|
|
return rows.map((row: any) => ({
|
|
slug: row.slug,
|
|
name: row.name,
|
|
subtitle: row.subtitle,
|
|
eventDate: row.event_date,
|
|
feedbackDeadline: row.feedback_deadline,
|
|
posterImageUrl: row.poster_image_url
|
|
}));
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
};
|
|
|
|
export type EventLookupResult =
|
|
| { status: 'OK'; eventId: number; event: EventConfig }
|
|
| { status: 'NOT_FOUND' }
|
|
| { status: 'CLOSED' };
|
|
|
|
/**
|
|
* Resolves a slug to its full public config: meta, ordered setlist, ordered
|
|
* active questions. Distinguishes "unknown slug" from "known but outside
|
|
* its feedback window" so callers can respond 404 vs 410. Also used
|
|
* internally by the submission flow, which additionally needs `eventId`.
|
|
*/
|
|
export const getEventConfigBySlug = async (slug: string): Promise<EventLookupResult> => {
|
|
let conn = await NachklangFeedbackDB.getConnection();
|
|
try {
|
|
const eventQuery = `
|
|
SELECT event_id, slug, name, subtitle, event_date, feedback_deadline, intro_text, poster_image_url, is_published
|
|
FROM events WHERE slug = ?`;
|
|
const eventRows = await conn.query(eventQuery, [slug]);
|
|
if (eventRows.length === 0) {
|
|
return {status: 'NOT_FOUND'};
|
|
}
|
|
const eventRow = eventRows[0];
|
|
|
|
const eligibleQuery = `
|
|
SELECT 1 FROM events
|
|
WHERE event_id = ? AND is_published = 1 AND event_date <= CURDATE() AND feedback_deadline >= NOW()`;
|
|
const eligibleRows = await conn.query(eligibleQuery, [eventRow.event_id]);
|
|
if (eligibleRows.length === 0) {
|
|
return {status: 'CLOSED'};
|
|
}
|
|
|
|
const songsQuery = 'SELECT song_id, title, composer, position FROM songs WHERE event_id = ? ORDER BY position ASC';
|
|
const songRows = await conn.query(songsQuery, [eventRow.event_id]);
|
|
const songs: Song[] = songRows.map((row: any) => ({
|
|
songId: row.song_id,
|
|
title: row.title,
|
|
composer: row.composer,
|
|
position: row.position
|
|
}));
|
|
|
|
const questionsQuery = `
|
|
SELECT eq.event_question_id, eq.position, q.question_id, q.question_type, q.label, q.help_text
|
|
FROM event_questions eq
|
|
INNER JOIN questions q ON q.question_id = eq.question_id
|
|
WHERE eq.event_id = ? AND eq.is_active = 1
|
|
ORDER BY eq.position ASC`;
|
|
const questionRows = await conn.query(questionsQuery, [eventRow.event_id]);
|
|
const questions: Question[] = questionRows.map((row: any) => ({
|
|
eventQuestionId: row.event_question_id,
|
|
questionId: row.question_id,
|
|
type: row.question_type,
|
|
label: row.label,
|
|
helpText: row.help_text,
|
|
position: row.position
|
|
}));
|
|
|
|
return {
|
|
status: 'OK',
|
|
eventId: eventRow.event_id,
|
|
event: {
|
|
slug: eventRow.slug,
|
|
name: eventRow.name,
|
|
subtitle: eventRow.subtitle,
|
|
eventDate: eventRow.event_date,
|
|
feedbackDeadline: eventRow.feedback_deadline,
|
|
posterImageUrl: eventRow.poster_image_url,
|
|
introText: eventRow.intro_text,
|
|
songs,
|
|
questions
|
|
}
|
|
};
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
};
|