Files
picpeak/backend/src/utils/categoryOrder.js
T
Luca a4b4485d32 fix(categories): address PR #790 review — event ownership, migration renumber, nits
- 🔴 Event ownership: GET /event/:eventId and DELETE /reorder/:eventId now use
  requireEventOwnership; POST /reorder (event_id in body) gets the equivalent
  inline check (super_admin bypasses; others limited to owned/ownerless events).
  New test covers a settings.edit-holding non-super_admin blocked (403) on all
  three per-event routes.
- 🔴 Migration renumber: 158→159, 159→160 (upstream #788 already took 158);
  headers + the test's require path updated.
- 🟢 Nits: stale inline "Drag the arrows" fallback → "Use the arrows" (matches
  en.json; control is click-only); invalid bg-accent-dark/150 → bg-accent-dark.
2026-07-10 20:10:40 +02:00

64 lines
2.5 KiB
JavaScript

/**
* Category order resolution (#782).
*
* Resolves an event's categories into their effective display order, layering:
* 1. per-event override — event_category_order.position, when the event has
* been customised;
* 2. the global default — photo_categories.display_order (migration 159);
* 3. name.
*
* Globals and event-specific categories are ordered together so a custom order
* can interleave them into the flow of the day. Shared by the admin event view
* and the public gallery so the two never diverge.
*/
const { db } = require('../database/db');
const { formatBoolean } = require('./dbCompat');
const { hasColumnCached } = require('./schemaCache');
/**
* @param {number|string} eventId
* @param {object} [opts]
* @param {number[]|null} [opts.onlyIds] restrict to these category ids (the
* public gallery only shows categories that actually have photos).
* @param {string[]|null} [opts.select] qualified columns to select (default
* `c.*`). Always aliased to the `photo_categories as c` table.
* @returns rows with an added `override_position` (null when not customised).
*/
async function getEventCategoriesOrdered(eventId, { onlyIds = null, select = null } = {}) {
const eid = parseInt(eventId, 10);
const base = db('photo_categories as c').where(function () {
this.where('c.is_global', formatBoolean(true)).orWhere('c.event_id', eid);
});
if (onlyIds) base.whereIn('c.id', onlyIds);
// Fail safe: if the override table isn't present yet (half-applied migration),
// fall back to the global-default order so the public gallery never 500s.
const overrideReady = await hasColumnCached('event_category_order', 'position');
if (!overrideReady) {
return base
.select(select || 'c.*')
.orderBy('c.is_global', 'desc')
.orderBy('c.display_order', 'asc')
.orderBy('c.name', 'asc');
}
const cols = select ? [...select] : ['c.*'];
cols.push('o.position as override_position');
return base
.leftJoin('event_category_order as o', function () {
this.on('o.category_id', 'c.id').andOnVal('o.event_id', '=', eid);
})
.select(cols)
// Overridden categories first (in their pinned order), then the rest by the
// global default. CASE keeps NULL-ordering portable across SQLite + Postgres.
.orderByRaw('CASE WHEN o.position IS NULL THEN 1 ELSE 0 END ASC')
.orderBy('o.position', 'asc')
.orderBy('c.is_global', 'desc')
.orderBy('c.display_order', 'asc')
.orderBy('c.name', 'asc');
}
module.exports = { getEventCategoriesOrdered };