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>
343 lines
11 KiB
TypeScript
343 lines
11 KiB
TypeScript
import * as dotenv from 'dotenv';
|
|
import {Guid} from 'guid-typescript';
|
|
import {Event} from './event.interface.js';
|
|
import {NachklangCalendarDB} from '../Calendar.db.js';
|
|
|
|
|
|
dotenv.config();
|
|
|
|
/**
|
|
* Returns all events for the given calendar
|
|
* @param calendarId The calendar Id
|
|
*/
|
|
export const getAllEvents = async (calendarId: number): Promise<Event[]> => {
|
|
let conn = await NachklangCalendarDB.getConnection();
|
|
let eventRows: Event[] = [];
|
|
try {
|
|
const calendarQuery = 'SELECT calendar_id, includes_calendars FROM calendars WHERE calendar_id = ?';
|
|
const calendarRes = await conn.query(calendarQuery, calendarId);
|
|
let calendarsToFetch: number[] = [calendarId];
|
|
for(let row of calendarRes) {
|
|
let includes: number[] = JSON.parse(row.includes_calendars);
|
|
calendarsToFetch = [...calendarsToFetch, ...includes];
|
|
}
|
|
|
|
const eventsQuery = `
|
|
SELECT e.calendar_id, e.uuid, e.created_date, e.created_by_id, u.full_name as created_by_name, u2.full_name as last_modified_by_name, v.* FROM events e
|
|
INNER JOIN (
|
|
SELECT event_id, MAX(event_version_id) AS latest_version
|
|
FROM event_versions
|
|
GROUP BY event_id
|
|
) latest_versions
|
|
ON e.event_id = latest_versions.event_id
|
|
INNER JOIN event_versions v
|
|
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
|
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
|
LEFT OUTER JOIN users u2 ON u2.user_id = v.version_created_by_id
|
|
WHERE e.calendar_id IN (?) AND v.status = 'PUBLIC'
|
|
ORDER BY e.event_id`;
|
|
const eventsRes = await conn.query(eventsQuery, [calendarsToFetch]);
|
|
|
|
for (let row of eventsRes) {
|
|
eventRows.push({
|
|
eventId: row.event_id,
|
|
calendarId: row.calendar_id,
|
|
uuid: row.uuid,
|
|
name: row.name,
|
|
description: row.description,
|
|
startDateTime: row.start_datetime,
|
|
endDateTime: row.end_datetime,
|
|
createdDate: row.created_date,
|
|
lastModifiedDate: row.version_created_at,
|
|
location: row.location,
|
|
createdBy: row.created_by_name,
|
|
createdById: row.created_by_id,
|
|
lastModifiedBy: row.last_modified_by_name,
|
|
lastModifiedById: row.version_created_by_id,
|
|
url: row.url,
|
|
wholeDay: row.whole_day,
|
|
repeatFrequency: row.repeat_frequency
|
|
});
|
|
}
|
|
|
|
return eventRows;
|
|
} catch (err) {
|
|
throw err;
|
|
} finally {
|
|
// Return connection
|
|
await conn.end();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns all events for the given calendar for the admin UI (therefore includes admin relevant information and
|
|
* ignores the calendar includes
|
|
* @param calendarId
|
|
*/
|
|
export const getAllEventsAdmin = async (calendarId: number): Promise<Event[]> => {
|
|
let conn = await NachklangCalendarDB.getConnection();
|
|
let eventRows: Event[] = [];
|
|
try {
|
|
const eventsQuery = `
|
|
SELECT e.calendar_id, e.uuid, e.created_date, e.created_by_id, u.full_name as created_by_name, u2.full_name as last_modified_by_name, v.* FROM events e
|
|
INNER JOIN (
|
|
SELECT event_id, MAX(event_version_id) AS latest_version
|
|
FROM event_versions
|
|
GROUP BY event_id
|
|
) latest_versions
|
|
ON e.event_id = latest_versions.event_id
|
|
INNER JOIN event_versions v
|
|
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
|
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
|
LEFT OUTER JOIN users u2 ON u2.user_id = v.version_created_by_id
|
|
WHERE e.calendar_id = ?
|
|
ORDER BY e.event_id`;
|
|
const eventsRes = await conn.query(eventsQuery, calendarId);
|
|
|
|
for (let row of eventsRes) {
|
|
eventRows.push({
|
|
eventId: row.event_id,
|
|
calendarId: row.calendar_id,
|
|
uuid: row.uuid,
|
|
name: row.name,
|
|
description: row.description,
|
|
startDateTime: row.start_datetime,
|
|
endDateTime: row.end_datetime,
|
|
createdDate: row.created_date,
|
|
lastModifiedDate: row.version_created_at,
|
|
location: row.location,
|
|
createdBy: row.created_by_name,
|
|
createdById: row.created_by_id,
|
|
lastModifiedBy: row.last_modified_by_name,
|
|
lastModifiedById: row.version_created_by_id,
|
|
url: row.url,
|
|
wholeDay: row.whole_day,
|
|
repeatFrequency: row.repeat_frequency,
|
|
status: row.status
|
|
});
|
|
}
|
|
|
|
return eventRows;
|
|
} catch (err) {
|
|
throw err;
|
|
} finally {
|
|
// Return connection
|
|
await conn.end();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Returns a single event by id (latest version, any status), or null if it
|
|
* doesn't exist. Unlike getAllEvents/getAllEventsAdmin this isn't scoped to
|
|
* a calendar - callers that need to enforce calendar/status visibility
|
|
* should check the returned event's calendarId/status themselves.
|
|
* @param eventId The event id
|
|
*/
|
|
export const getEventById = async (eventId: number): Promise<Event | null> => {
|
|
let conn = await NachklangCalendarDB.getConnection();
|
|
try {
|
|
const eventsQuery = `
|
|
SELECT e.calendar_id, e.uuid, e.created_date, e.created_by_id, u.full_name as created_by_name, u2.full_name as last_modified_by_name, v.* FROM events e
|
|
INNER JOIN (
|
|
SELECT event_id, MAX(event_version_id) AS latest_version
|
|
FROM event_versions
|
|
GROUP BY event_id
|
|
) latest_versions
|
|
ON e.event_id = latest_versions.event_id
|
|
INNER JOIN event_versions v
|
|
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
|
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
|
LEFT OUTER JOIN users u2 ON u2.user_id = v.version_created_by_id
|
|
WHERE e.event_id = ?`;
|
|
const eventsRes = await conn.query(eventsQuery, eventId);
|
|
|
|
if (eventsRes.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const row = eventsRes[0];
|
|
return {
|
|
eventId: row.event_id,
|
|
calendarId: row.calendar_id,
|
|
uuid: row.uuid,
|
|
name: row.name,
|
|
description: row.description,
|
|
startDateTime: row.start_datetime,
|
|
endDateTime: row.end_datetime,
|
|
createdDate: row.created_date,
|
|
lastModifiedDate: row.version_created_at,
|
|
location: row.location,
|
|
createdBy: row.created_by_name,
|
|
createdById: row.created_by_id,
|
|
lastModifiedBy: row.last_modified_by_name,
|
|
lastModifiedById: row.version_created_by_id,
|
|
url: row.url,
|
|
wholeDay: row.whole_day,
|
|
repeatFrequency: row.repeat_frequency,
|
|
status: row.status
|
|
} as Event;
|
|
} catch (err) {
|
|
throw err;
|
|
} finally {
|
|
// Return connection
|
|
await conn.end();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Create the given event in the database
|
|
* @param event The event to create
|
|
*/
|
|
export const createEvent = async (event: Event): Promise<number> => {
|
|
let conn = await NachklangCalendarDB.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
let eventUUID = Guid.create().toString();
|
|
const eventsQuery = 'INSERT INTO events (calendar_id, uuid, created_by_id) VALUES (?,?,?) RETURNING event_id';
|
|
const eventsRes = await conn.execute(eventsQuery, [event.calendarId, eventUUID, event.createdById]);
|
|
|
|
const versionQuery = 'INSERT INTO event_versions (event_id, name, description, start_datetime, end_datetime, whole_day, repeat_frequency, location, url, status, version_created_by_id) VALUES (?,?,?,?,?,?,?,?,?,?,?);'
|
|
await conn.execute(versionQuery, [eventsRes[0].event_id, event.name, event.description, event.startDateTime, event.endDateTime, event.wholeDay, event.repeatFrequency, event.location, event.url, event.status, event.createdById]);
|
|
|
|
await conn.commit();
|
|
|
|
return eventsRes[0].event_id;
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Update the given event in the database
|
|
* @param event The event to update
|
|
*/
|
|
export const updateEvent = async (event: Event): Promise<number> => {
|
|
let conn = await NachklangCalendarDB.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
const versionQuery = 'INSERT INTO event_versions (event_id, name, description, start_datetime, end_datetime, whole_day, repeat_frequency, location, url, status, version_created_by_id) VALUES (?,?,?,?,?,?,?,?,?,?,?);'
|
|
const versionRes = await conn.execute(versionQuery, [event.eventId, event.name, event.description, event.startDateTime, event.endDateTime, event.wholeDay, event.repeatFrequency, event.location, event.url, event.status, event.createdById]);
|
|
|
|
await conn.commit();
|
|
|
|
return versionRes.affectedRows;
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Deletes the given event from the database
|
|
* @param event The event to delete
|
|
*/
|
|
export const deleteEvent = async (event: Event): Promise<boolean> => {
|
|
let conn = await NachklangCalendarDB.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
const versionQuery = 'INSERT INTO event_versions (event_id, status, version_created_by_id) VALUES (?,?,?);'
|
|
const versionRes = await conn.execute(versionQuery, [event.eventId, 'DELETED', event.createdById]);
|
|
|
|
await conn.commit();
|
|
|
|
return versionRes.affectedRows === 1;
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Moves an event to the specified calendar
|
|
* @param event The event to move. Has to have the target calendar set already.
|
|
*/
|
|
export const moveEvent = async (event: Event): Promise<boolean> => {
|
|
let conn = await NachklangCalendarDB.getConnection();
|
|
try {
|
|
await conn.beginTransaction();
|
|
const eventQuery = 'UPDATE events SET calendar_id = ? WHERE event_id = ?';
|
|
const eventRes = await conn.execute(eventQuery, [event.calendarId, event.eventId]);
|
|
|
|
await conn.commit();
|
|
|
|
return eventRes.affectedRows === 1;
|
|
} catch (err) {
|
|
await conn.rollback();
|
|
throw err;
|
|
} finally {
|
|
await conn.end();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the next upcoming event for the given calendar
|
|
* @param calendarId The calendar Id
|
|
*/
|
|
export const getNextUpcomingEvent = async (calendarId: number): Promise<Event | null> => {
|
|
let conn = await NachklangCalendarDB.getConnection();
|
|
try {
|
|
const calendarQuery = 'SELECT calendar_id, includes_calendars FROM calendars WHERE calendar_id = ?';
|
|
const calendarRes = await conn.query(calendarQuery, calendarId);
|
|
let calendarsToFetch: number[] = [calendarId];
|
|
for(let row of calendarRes) {
|
|
let includes: number[] = JSON.parse(row.includes_calendars);
|
|
calendarsToFetch = [...calendarsToFetch, ...includes];
|
|
}
|
|
|
|
const now = new Date();
|
|
const eventsQuery = `
|
|
SELECT e.calendar_id, e.uuid, e.created_date, e.created_by_id, u.full_name as created_by_name, u2.full_name as last_modified_by_name, v.* FROM events e
|
|
INNER JOIN (
|
|
SELECT event_id, MAX(event_version_id) AS latest_version
|
|
FROM event_versions
|
|
GROUP BY event_id
|
|
) latest_versions
|
|
ON e.event_id = latest_versions.event_id
|
|
INNER JOIN event_versions v
|
|
ON v.event_id = latest_versions.event_id AND v.event_version_id = latest_versions.latest_version
|
|
LEFT OUTER JOIN users u ON u.user_id = e.created_by_id
|
|
LEFT OUTER JOIN users u2 ON u2.user_id = v.version_created_by_id
|
|
WHERE e.calendar_id IN (?) AND v.status = 'PUBLIC' AND v.start_datetime > ?
|
|
ORDER BY v.start_datetime ASC
|
|
LIMIT 1`;
|
|
const eventsRes = await conn.query(eventsQuery, [calendarsToFetch, now]);
|
|
|
|
if (eventsRes.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const row = eventsRes[0];
|
|
return {
|
|
eventId: row.event_id,
|
|
calendarId: row.calendar_id,
|
|
uuid: row.uuid,
|
|
name: row.name,
|
|
description: row.description,
|
|
startDateTime: row.start_datetime,
|
|
endDateTime: row.end_datetime,
|
|
createdDate: row.created_date,
|
|
lastModifiedDate: row.version_created_at,
|
|
location: row.location,
|
|
createdBy: row.created_by_name,
|
|
createdById: row.created_by_id,
|
|
lastModifiedBy: row.last_modified_by_name,
|
|
lastModifiedById: row.version_created_by_id,
|
|
url: row.url,
|
|
wholeDay: row.whole_day,
|
|
repeatFrequency: row.repeat_frequency
|
|
} as Event;
|
|
} catch (err) {
|
|
throw err;
|
|
} finally {
|
|
// Return connection
|
|
await conn.end();
|
|
}
|
|
}
|