3ea9e630ed
Prep PR for the admin auth module (docs/plan-admin-auth.md step 1).
better-auth 1.7 ships ESM only, so the API moves off CommonJS:
- "type": "module", module nodenext, target ES2024, .js suffixes on all
relative imports, require('mariadb'|'cors') replaced by imports, and
export= packages (winston, app-root-path, bcrypt) consumed via default
imports. The logger now uses appRoot.path explicitly.
- TypeScript 5.9, @types/node 26, tslint removed. Node 26 pinned via
engines and .nvmrc (Plesk runs 26).
- Jest 28 + ts-jest replaced by vitest 5. Eight test files depend on
hoisted module mocks with static imports and resetModules + require,
which Jest's ESM mode does not support; vitest keeps them nearly
verbatim. Coverage via @vitest/coverage-v8 (lcov), Sonar generic report
via vitest-sonar-reporter, so sonar-project.properties is unchanged.
vitest.config.ts sets FEEDBACK_IP_SALT so the suite passes without a
local .env.
- dotenv 8 -> 16 and axios 0.24 -> 1.x: their old typings are not
resolvable under nodenext.
- autoCommit: false dropped from the pool configs; it is not a mariadb
connector option and was silently ignored.
tsc clean, 96/96 tests green, compiled app boots and serves /, /docs and
CORS under Node ESM.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
99 lines
3.1 KiB
TypeScript
99 lines
3.1 KiB
TypeScript
import {NachklangFeedbackDB} from '../Feedback.db.js';
|
|
import {QuestionType} from '../feedback.interface.js';
|
|
import {AdminQuestion} from './admin.interface.js';
|
|
|
|
const mapRow = (row: any): AdminQuestion => ({
|
|
questionId: row.question_id,
|
|
label: row.label,
|
|
helpText: row.help_text,
|
|
questionType: row.question_type,
|
|
isArchived: !!row.is_archived
|
|
});
|
|
|
|
export const listQuestions = async (includeArchived: boolean): Promise<AdminQuestion[]> => {
|
|
let conn = await NachklangFeedbackDB.getConnection();
|
|
try {
|
|
const query = includeArchived
|
|
? 'SELECT * FROM questions ORDER BY created_at DESC'
|
|
: 'SELECT * FROM questions WHERE is_archived = 0 ORDER BY created_at DESC';
|
|
const rows = await conn.query(query);
|
|
return rows.map(mapRow);
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
};
|
|
|
|
export const createQuestion = async (label: string, helpText: string | null, questionType: QuestionType): Promise<number> => {
|
|
let conn = await NachklangFeedbackDB.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
const res = await conn.query(
|
|
'INSERT INTO questions (label, help_text, question_type) VALUES (?,?,?) RETURNING question_id',
|
|
[label, helpText, questionType]
|
|
);
|
|
await conn.commit();
|
|
return res[0].question_id;
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Edits label/help text only. question_type is immutable after creation -
|
|
* changing it would invalidate existing answers' question_type_snapshot
|
|
* semantics. The admin UI offers "archive and create new" instead.
|
|
*/
|
|
export const updateQuestion = async (questionId: number, label: string, helpText: string | null): Promise<boolean> => {
|
|
let conn = await NachklangFeedbackDB.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
const res = await conn.query('UPDATE questions SET label = ?, help_text = ? WHERE question_id = ?', [label, helpText, questionId]);
|
|
await conn.commit();
|
|
return res.affectedRows > 0;
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
};
|
|
|
|
export type RemoveQuestionResult = 'ARCHIVED' | 'DELETED' | 'NOT_FOUND';
|
|
|
|
/**
|
|
* Archives (soft delete) a question. Hard-deletes it instead if it has
|
|
* never been assigned to any event, so an admin's typo doesn't have to
|
|
* live forever in the library.
|
|
*/
|
|
export const removeQuestion = async (questionId: number): Promise<RemoveQuestionResult> => {
|
|
let conn = await NachklangFeedbackDB.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
|
|
const existsRows = await conn.query('SELECT 1 FROM questions WHERE question_id = ?', [questionId]);
|
|
if (existsRows.length === 0) {
|
|
await conn.rollback();
|
|
return 'NOT_FOUND';
|
|
}
|
|
|
|
const usageRows = await conn.query('SELECT 1 FROM event_questions WHERE question_id = ? LIMIT 1', [questionId]);
|
|
if (usageRows.length === 0) {
|
|
await conn.query('DELETE FROM questions WHERE question_id = ?', [questionId]);
|
|
await conn.commit();
|
|
return 'DELETED';
|
|
}
|
|
|
|
await conn.query('UPDATE questions SET is_archived = 1 WHERE question_id = ?', [questionId]);
|
|
await conn.commit();
|
|
return 'ARCHIVED';
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
};
|