refactor(slug): extract shared slugify util + scope adminPhotos category lookup (#525)

Folds all three follow-up items tracked in #525 into one commit:

1. Mirror PR #500's category scoping on adminPhotos.js. The admin
   upload route at adminPhotos.js:231 still accepted any category_id
   without event scoping — quietly less strict than the public v1
   API after #500 landed. Same one-liner fix (event_id OR is_global)
   with a matching 400 response shape so admin + v1 stay consistent.

2. Extract a shared slugify() in backend/src/utils/slug.js with the
   NFD-strip-combining-marks fix from #502, and route 5 callers
   through it:
     - adminEvents.js (event-name slug)
     - events.js      (event-create slug)
     - v1/events.js   (replaces local slugify helper)
     - adminArchives.js (archive→category slug)
   For pure-ASCII input the output is byte-identical to each old
   inline pipeline, so existing slugs in the DB keep round-tripping
   cleanly via lookup. Accented inputs now transliterate (Família
   → familia) instead of dropping the diacritic (Família → f-mlia).
   adminCategories.js stays with its own pipeline (underscores-as-
   word-chars semantics differ from the events-style transform —
   changing would silently shift wedding_party → wedding-party on
   new inserts). xmpGenerator.sanitizeKeyword stays unchanged for
   the same compat-cautious reason.

3. Cover the v1 upload happy path. Existing test only exercised the
   400-out-of-scope branch. Add two happy-path cases that stub
   sharp / generateThumbnail / storage.putFromFile and pin the
   response shape (id, category_id, type, etc.) plus the collage-
   slug → type='collage' flip. Temp file recreated in beforeEach
   because the handler unlinks it on success.

Tests:
- New slug.test.js: 22 cases pinning ASCII parity with the legacy
  pipeline (so the refactor is provably non-breaking for existing
  data) and the corrected accent handling across de/es/fr/nl/pt
  inputs, plus CJK and edge-case behaviour.
- events.category.test.js: 4 tests total (2 existing + 2 new happy
  path).
- galleryOgService.shareImage.test.js: 11 (3 added in #521 + 8 pre-
  existing) still pass.

37 tests pass across the three touched files.

Refs: #525, follows up #500 and #502
This commit is contained in:
Paul Nothaft
2026-05-18 23:50:35 +02:00
parent 4b4ecfdf71
commit e8c2212dad
8 changed files with 277 additions and 19 deletions
+35
View File
@@ -0,0 +1,35 @@
/**
* URL-safe slug generation shared across event, archive, and v1 upload
* routes (#525 follow-up to #502). Previously every caller had its own
* inline `name.toLowerCase().replace(/[^a-z0-9]/g, '-')` pipeline, each
* with the same latent bug: JS's `\w` and the ASCII alphanumeric class
* silently drop non-ASCII letters instead of transliterating them
* (`Decoração` → `decorao`, `Família` → `f-mlia`).
*
* Fix mirrors #502: NFD-normalize so accented characters split into a
* base letter + combining mark, then strip the combining-mark range
* (U+0300U+036F) so the ASCII base survives. Single regex pass after
* that — `[^a-z0-9]+` collapses any run of non-alphanumerics into one
* dash, no separate collapse step needed.
*
* For pure-ASCII input the output is byte-identical to the previous
* inline pipelines, so existing slugs continue to round-trip cleanly
* via lookups; only new inserts with non-ASCII names start producing
* the corrected slugs.
*
* Not exported as the default category slug — `adminCategories.js`
* intentionally preserves underscores (the legacy category pipeline
* used `\w` not `[a-z0-9]`), so changing it here would silently shift
* "wedding_party" → "wedding-party" on new inserts. Categories keep
* their own pipeline as fixed in #502.
*/
function slugify(input) {
return String(input ?? '')
.normalize('NFD')
.replace(/[̀-ͯ]/g, '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-|-$/g, '');
}
module.exports = { slugify };