Add Feedback domain module: public submission flow, admin CRUD, reporting (#7)
Jenkins Production Deployment
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>
This commit was merged in pull request #7.
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
/**
|
||||
* Required External Modules and Interfaces
|
||||
*/
|
||||
import express, {Request, Response} from 'express';
|
||||
import * as QuestionsAdminService from './questions.admin.service';
|
||||
import {sendServerError} from '../feedback.errors';
|
||||
|
||||
/**
|
||||
* Router Definition
|
||||
*/
|
||||
export const questionsAdminRouter = express.Router();
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /feedback/admin/questions:
|
||||
* get:
|
||||
* summary: List the question library
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: query
|
||||
* name: includeArchived
|
||||
* schema:
|
||||
* type: boolean
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Success
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: array
|
||||
* items:
|
||||
* $ref: '#/components/schemas/AdminQuestion'
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* post:
|
||||
* summary: Create a question
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [label, questionType]
|
||||
* properties:
|
||||
* label:
|
||||
* type: string
|
||||
* helpText:
|
||||
* type: string
|
||||
* questionType:
|
||||
* $ref: '#/components/schemas/QuestionType'
|
||||
* responses:
|
||||
* 201:
|
||||
* description: Created
|
||||
* 400:
|
||||
* description: Missing or invalid fields
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
questionsAdminRouter.get('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const includeArchived = req.query.includeArchived === 'true';
|
||||
res.status(200).send(await QuestionsAdminService.listQuestions(includeArchived));
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
const VALID_TYPES = ['SONG_PICK', 'SONG_RATING', 'FREE_TEXT'];
|
||||
|
||||
questionsAdminRouter.post('/', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {label, helpText, questionType} = req.body || {};
|
||||
if (!label || !VALID_TYPES.includes(questionType)) {
|
||||
res.status(400).send({status: 'BAD_REQUEST', message: 'label and a valid questionType are required'});
|
||||
return;
|
||||
}
|
||||
const questionId = await QuestionsAdminService.createQuestion(label, helpText || null, questionType);
|
||||
res.status(201).send({questionId});
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @swagger
|
||||
* /feedback/admin/questions/{questionId}:
|
||||
* put:
|
||||
* summary: Edit a question's label/help text
|
||||
* description: question_type is immutable after creation - the admin UI offers "archive and create new" instead.
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: questionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* requestBody:
|
||||
* required: true
|
||||
* content:
|
||||
* application/json:
|
||||
* schema:
|
||||
* type: object
|
||||
* required: [label]
|
||||
* properties:
|
||||
* label:
|
||||
* type: string
|
||||
* helpText:
|
||||
* type: string
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Updated
|
||||
* 400:
|
||||
* description: Missing label
|
||||
* 404:
|
||||
* description: Unknown question
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
* delete:
|
||||
* summary: Archive (or hard-delete) a question
|
||||
* description: Archives the question if it has ever been used; hard-deletes it if it has never been assigned to any event.
|
||||
* tags: [feedback-admin]
|
||||
* parameters:
|
||||
* - $ref: '#/components/parameters/SessionIdHeader'
|
||||
* - $ref: '#/components/parameters/SessionKeyHeader'
|
||||
* - in: path
|
||||
* name: questionId
|
||||
* required: true
|
||||
* schema:
|
||||
* type: integer
|
||||
* responses:
|
||||
* 200:
|
||||
* description: Archived or deleted
|
||||
* 404:
|
||||
* description: Unknown question
|
||||
* 401:
|
||||
* description: Unauthorized
|
||||
*/
|
||||
questionsAdminRouter.put('/:questionId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const {label, helpText} = req.body || {};
|
||||
if (!label) {
|
||||
res.status(400).send({status: 'BAD_REQUEST', message: 'label is required'});
|
||||
return;
|
||||
}
|
||||
const updated = await QuestionsAdminService.updateQuestion(Number(req.params.questionId), label, helpText || null);
|
||||
if (!updated) {
|
||||
res.status(404).send({status: 'NOT_FOUND'});
|
||||
return;
|
||||
}
|
||||
res.status(200).send({status: 'OK'});
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
|
||||
questionsAdminRouter.delete('/:questionId', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const result = await QuestionsAdminService.removeQuestion(Number(req.params.questionId));
|
||||
if (result === 'NOT_FOUND') {
|
||||
res.status(404).send({status: 'NOT_FOUND'});
|
||||
return;
|
||||
}
|
||||
res.status(200).send({status: result});
|
||||
} catch (e: any) {
|
||||
sendServerError(res, e);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user