ci(server): refuse SQL text in repositories and seeders, move the gallery oracle to tests

The repository rule that keeps dialect SQL in sql-functions.ts only
matched raw() and the sql tag, so a SQL string handed straight to the
connection, em.getConnection().execute('INSERT OR IGNORE ...'), passed
unseen, and the seeders sat outside every repository rule. A new block
refuses a string or template whose text starts like a statement as the
first argument of execute, run, all or get in src/db/repositories and
src/db/seeders; a result mode such as qb.execute('run') stays legal.
The files that already hold such SQL are named one by one so the list
can only shrink: MaintenanceRepository (PRAGMA, VACUUM INTO),
DemoRepository and six of the seven seeders.

The selector groups are named once in eslint.config.mjs, and the
selector test now also checks the new block per path, its exceptions,
and that raw() stays forbidden in repositories.

journey-gallery-order.ts exported raw SQLite ORDER BY text from
src/nest that no production code read any more since the gallery reads
moved onto JourneyPhotosRepository. Two parity tests still compare the
Kysely rebuild against it, so it moves to tests/helpers as their oracle.
This commit is contained in:
Maurice
2026-10-08 11:59:47 +02:00
parent 3fbe0418eb
commit a86aa4e819
9 changed files with 115 additions and 12 deletions
+1 -1
View File
@@ -736,7 +736,7 @@ export function lowerTrimParam(platform: Platform, value: string): RawQueryFragm
// ---------------------------------------------------------------------------
// Plan 3g Task 0 (R1) — no consumer yet: Task 1 (`getJourneyFull`'s gallery
// read) and Task 3 (`getPublicJourney`'s gallery read) each fold this into
// their own rebuild of `journey-gallery-order.ts`'s `GALLERY_CHRONOLOGICAL_ORDER`
// their own rebuild of the legacy `GALLERY_CHRONOLOGICAL_ORDER` (tests/helpers/)
// text (a `COALESCE(NULLIF(tp.taken_at, ''), <correlated MIN+concat subquery>,
// <this>)` ORDER BY, worked out in full in Task 0's report for both to paste
// in verbatim, neither re-deriving it). Added here, Kysely-only (no MikroORM
@@ -86,10 +86,10 @@ function countryNamesFor(points: { country: string | null }[]): Record<string, s
// `GALLERY_JOIN` raw-SQL-text consts these two comment paragraphs used to
// document are GONE — every site that folded them into a `this.db.prepare(...)`
// call now reads through `JourneyPhotosRepository`/`JourneyEntryPhotosRepository`
// instead (JG15/19/72/88-102/108-116, `journey-share.service.ts`'s own
// GALLERY_CHRONOLOGICAL_ORDER import for JS15 is unaffected — that constant
// lives in `journey-gallery-order.ts`, a separate file this task does not
// touch). The two comment paragraphs above are kept for the column-shape
// instead (JG15/19/72/88-102/108-116; the legacy GALLERY_CHRONOLOGICAL_ORDER
// text survives only as the parity tests' oracle in
// tests/helpers/legacy-gallery-order.ts, read by no production code).
// The two comment paragraphs above are kept for the column-shape
// documentation; the repositories' own docstrings are the source of truth
// for the exact column lists now.
@@ -1,38 +0,0 @@
/**
* How the journey gallery is ordered (#2200).
*
* `journey_photos.sort_order` is nothing but MAX+1 at insert time and no surface
* ever writes it again, so ordering by it is ordering by "whenever this got
* added". Upload day one's pictures after day two's and the gallery stays
* shuffled, with nothing in the UI to straighten it out.
*
* The anchor is instead the best time TREK knows for the picture:
*
* 1. its capture time, when EXIF or the photo provider gave one up,
* 2. otherwise the date of the earliest entry it hangs on, which is where the
* journey itself says the reader was standing when it was taken,
* 3. otherwise the moment it was added, the old behaviour and the only thing
* left to go on for a loose photo whose EXIF was stripped.
*
* All three are compared as ISO text. Entry dates are local wall clock and
* capture times are UTC, which only disagree in the hours either side of
* midnight, and a gallery is read by the day.
*
* `sort_order`, then the row id, break ties: insertion order still decides
* between two pictures taken in the same minute, and the id makes the order
* total, so paging through the gallery cannot show or skip a photo twice.
*
* Consumers must alias journey_photos as `gp` and trek_photos as `tp`.
*/
export const GALLERY_CHRONOLOGICAL_ORDER = `
ORDER BY COALESCE(
NULLIF(tp.taken_at, ''),
(SELECT MIN(je.entry_date || 'T' || COALESCE(NULLIF(je.entry_time, ''), '00:00'))
FROM journey_entry_photos jep
JOIN journey_entries je ON je.id = jep.entry_id
WHERE jep.journey_photo_id = gp.id),
strftime('%Y-%m-%dT%H:%M:%SZ', gp.created_at / 1000, 'unixepoch')
) ASC,
gp.sort_order ASC,
gp.id ASC
`;