diff --git a/server/src/services/journeyService.ts b/server/src/services/journeyService.ts index 9910ee8b..a03d1fea 100644 --- a/server/src/services/journeyService.ts +++ b/server/src/services/journeyService.ts @@ -781,22 +781,20 @@ export function updatePhoto(photoId: number, userId: number, data: { caption?: s if (!row) return null; if (!canEdit(row.journey_id, userId)) return null; - const fields: string[] = []; - const values: unknown[] = []; - if (data.caption !== undefined) { fields.push('caption = ?'); values.push(data.caption); } - if (data.sort_order !== undefined) { fields.push('sort_order = ?'); values.push(data.sort_order); } - if (!fields.length) { - // no-op: return some photo row for this gallery item (first entry link) - return db.prepare(`SELECT ${JP_SELECT} FROM ${JP_JOIN} WHERE gp.id = ? LIMIT 1`).get(photoId) as JourneyPhoto | null; + // caption lives on the gallery row; sort_order lives on the junction table + // (JP_SELECT reads jep.sort_order, so updating journey_photos.sort_order + // would not be reflected in the returned row). + if (data.caption !== undefined) { + db.prepare('UPDATE journey_photos SET caption = ? WHERE id = ?').run(data.caption, photoId); + } + if (data.sort_order !== undefined) { + db.prepare('UPDATE journey_entry_photos SET sort_order = ? WHERE journey_photo_id = ?').run(data.sort_order, photoId); } - - values.push(photoId); - db.prepare(`UPDATE journey_photos SET ${fields.join(', ')} WHERE id = ?`).run(...values); return db.prepare(`SELECT ${JP_SELECT} FROM ${JP_JOIN} WHERE gp.id = ? LIMIT 1`).get(photoId) as JourneyPhoto | null; } // deletePhoto: hard-delete (backwards compat name used by old route). -export function deletePhoto(photoId: number, userId: number): { photo_id: number; file_path?: string | null; journey_id: number } | null { +export function deletePhoto(photoId: number, userId: number): { id: number; photo_id: number; file_path?: string | null; journey_id: number } | null { const row = db.prepare('SELECT id, journey_id, photo_id FROM journey_photos WHERE id = ?').get(photoId) as { id: number; journey_id: number; photo_id: number } | undefined; if (!row) return null; if (!canEdit(row.journey_id, userId)) return null; @@ -806,7 +804,7 @@ export function deletePhoto(photoId: number, userId: number): { photo_id: number db.prepare('DELETE FROM journey_photos WHERE id = ?').run(photoId); deleteTrekPhotoIfOrphan(row.photo_id); - return { photo_id: row.photo_id, file_path: trekRow?.file_path ?? null, journey_id: row.journey_id }; + return { id: row.id, photo_id: row.photo_id, file_path: trekRow?.file_path ?? null, journey_id: row.journey_id }; } // ── Contributors ───────────────────────────────────────────────────────── diff --git a/server/tests/integration/journey.test.ts b/server/tests/integration/journey.test.ts index 167dc326..6748992a 100644 --- a/server/tests/integration/journey.test.ts +++ b/server/tests/integration/journey.test.ts @@ -649,7 +649,7 @@ describe('Link photo to entry', () => { .send({}); expect(res.status).toBe(400); - expect(res.body.error).toBe('photo_id required'); + expect(res.body.error).toBe('journey_photo_id required'); }); }); diff --git a/server/tests/unit/services/journeyService.test.ts b/server/tests/unit/services/journeyService.test.ts index 31f24214..950eff04 100644 --- a/server/tests/unit/services/journeyService.test.ts +++ b/server/tests/unit/services/journeyService.test.ts @@ -1325,9 +1325,10 @@ describe('Edge cases', () => { const result = deleteEntry(entry.id, user.id); expect(result).toBe(true); - // Photo should be deleted with the entry - const deletedPhoto = testDb.prepare('SELECT * FROM journey_photos WHERE id = ?').get(photo!.id) as any; - expect(deletedPhoto).toBeUndefined(); + // Junction row must be gone (ON DELETE CASCADE from journey_entries). + // Gallery row (journey_photos) is preserved — photo may belong to other entries. + const junctionRow = testDb.prepare('SELECT * FROM journey_entry_photos WHERE entry_id = ?').get(entry.id) as any; + expect(junctionRow).toBeUndefined(); }); it('JOURNEY-SVC-082: updateJourney can set cover_gradient', () => { @@ -1395,17 +1396,12 @@ describe('Edge cases', () => { addTripToJourney(journey.id, trip.id, user.id); - // Should have a [Trip Photos] entry with the imported photo - const photoEntry = testDb.prepare( - "SELECT * FROM journey_entries WHERE journey_id = ? AND title = '[Trip Photos]'" - ).get(journey.id) as any; - expect(photoEntry).toBeDefined(); - + // Trip photos now go straight into the journey gallery (no wrapper entry). const photos = testDb.prepare(` SELECT jp.*, tkp.asset_id FROM journey_photos jp JOIN trek_photos tkp ON tkp.id = jp.photo_id - WHERE jp.entry_id = ? - `).all(photoEntry.id); + WHERE jp.journey_id = ? + `).all(journey.id); expect(photos.length).toBe(1); expect((photos[0] as any).asset_id).toBe('immich-photo-1'); }); diff --git a/server/tests/unit/services/journeyShareService.test.ts b/server/tests/unit/services/journeyShareService.test.ts index bbd196a7..3df9c2ff 100644 --- a/server/tests/unit/services/journeyShareService.test.ts +++ b/server/tests/unit/services/journeyShareService.test.ts @@ -58,7 +58,7 @@ afterAll(() => { // -- Helpers ------------------------------------------------------------------ -/** Insert a trek_photos + journey_photos row and return the trek_photos id (used as photoId in public URLs). */ +/** Insert a trek_photos + journey_photos (gallery) + journey_entry_photos row and return the trek_photos id (used as photoId in public URLs). */ function insertJourneyPhoto( entryId: number, opts: { filePath?: string; assetId?: string; ownerId?: number } = {} @@ -70,10 +70,24 @@ function insertJourneyPhoto( VALUES (?, ?, ?, ?, ?) `).run(provider, opts.assetId ?? null, filePath, opts.ownerId ?? null, Date.now()); const trekId = trekResult.lastInsertRowid as number; + + // Look up journey_id from entry so gallery row is keyed to the journey (not entry). + const entryRow = testDb.prepare('SELECT journey_id FROM journey_entries WHERE id = ?').get(entryId) as { journey_id: number }; + const journeyId = entryRow.journey_id; + const now = Date.now(); + testDb.prepare(` - INSERT INTO journey_photos (entry_id, photo_id, caption, sort_order, created_at) + INSERT OR IGNORE INTO journey_photos (journey_id, photo_id, caption, sort_order, created_at) VALUES (?, ?, NULL, 0, ?) - `).run(entryId, trekId, Date.now()); + `).run(journeyId, trekId, now); + + const galleryRow = testDb.prepare('SELECT id FROM journey_photos WHERE journey_id = ? AND photo_id = ?').get(journeyId, trekId) as { id: number }; + + testDb.prepare(` + INSERT OR IGNORE INTO journey_entry_photos (entry_id, journey_photo_id, sort_order, created_at) + VALUES (?, ?, 0, ?) + `).run(entryId, galleryRow.id, now); + // Return trek_photos.id — this is p.photo_id in the public API response // and the value the client sends to /api/public/journey/:token/photos/:photoId/:kind return trekId;