diff --git a/server/src/db/repositories/JourneyEntries.repository.ts b/server/src/db/repositories/JourneyEntries.repository.ts index 54d6e2d04..ae0a4e415 100644 --- a/server/src/db/repositories/JourneyEntries.repository.ts +++ b/server/src/db/repositories/JourneyEntries.repository.ts @@ -124,6 +124,26 @@ export class JourneyEntriesRepository extends TrekRepository { .execute('all', false); } + /** + * JS13 (Plan 4 Task 8b relocation) — the public `getPublicJourney` route's + * entry list: `SELECT je.* FROM journey_entries je WHERE je.journey_id=? + * AND je.type != 'skeleton' AND je.dismissed=0 ORDER BY je.entry_date, + * je.sort_order`. A narrower filter than {@link listForJourney}/JG14 (adds + * the `type != 'skeleton'` exclusion — skeletons never appear publicly — + * and has no `id` ORDER BY tiebreak), so this is a distinct method, not a + * call to {@link listForJourney}. Previously lived as + * `JourneyShareTokens.repository.ts`'s own fallback stub from 3g Task 3 + * (this repository was still mid-flight when that task landed) — + * relocated here now that it is stable. + */ + async listPublicEntries(journeyId: number): Promise { + return await this.qb('je') + .select(['je.*']) + .where({ journey: journeyId, type: { $ne: 'skeleton' }, dismissed: 0 }) + .orderBy({ entry_date: 'asc', sort_order: 'asc' }) + .execute('all', false); + } + /** JG22 — `getJourneyFull`'s `dismissed_count`: `SELECT COUNT(*) AS n FROM journey_entries WHERE journey_id = ? AND dismissed = 1`. */ async countDismissed(journeyId: number): Promise { return await this.count({ journey: journeyId, dismissed: 1 }); diff --git a/server/src/db/repositories/JourneyEntryPhotos.repository.ts b/server/src/db/repositories/JourneyEntryPhotos.repository.ts index e8d487c42..8fb471be4 100644 --- a/server/src/db/repositories/JourneyEntryPhotos.repository.ts +++ b/server/src/db/repositories/JourneyEntryPhotos.repository.ts @@ -31,6 +31,40 @@ const JP_COLUMNS = [ 'tp.media_type', 'tp.duration_ms', 'tp.taken_at', 'tp.lat', 'tp.lng', ] as const; +/** + * JS14 (Plan 4 Task 8b relocation) — `JourneyShareService.getPublicJourney`'s + * per-entry photo row. Column-for-column identical to `JP_COLUMNS` above + * (only the widened nullability on a couple of fields to match the public + * route's original hand-kept shape) — {@link + * JourneyEntryPhotosRepository.listForPublicJourney} reuses `JP_COLUMNS` + * rather than a second, hand-duplicated select list. Previously lived as + * `JourneyShareTokens.repository.ts`'s own `JourneyPublicEntryPhotoRow` + + * `listEntryPhotosForPublicJourney`, a fallback stub from 3g Task 3 (this + * repository was still mid-flight when that task landed) — relocated here + * now that it is stable. + */ +export interface JourneyPublicEntryPhotoRow { + id: number; + entry_id: number; + photo_id: number; + caption: string | null; + sort_order: number | null; + shared: number; + created_at: number; + provider: string; + asset_id: string | null; + owner_id: number | null; + file_path: string | null; + thumbnail_path: string | null; + width: number | null; + height: number | null; + media_type: string | null; + duration_ms: number | null; + taken_at: string | null; + lat: number | null; + lng: number | null; +} + /** * `journey_entry_photos` — the entry↔gallery-photo junction (Plan 3g Task 2, * Part B). A genuine TWO-column composite primary key (`entry` + @@ -101,6 +135,28 @@ export class JourneyEntryPhotosRepository extends TrekRepository { + const rows = await this.kysely() + .selectFrom('journey_entry_photos as jep') + .innerJoin('journey_photos as gp', 'gp.id', 'jep.journey_photo_id') + .innerJoin('trek_photos as tp', 'tp.id', 'gp.photo_id') + .select(JP_COLUMNS) + .where('gp.journey_id', '=', journeyId) + .orderBy('jep.sort_order', 'asc') + .execute(); + return rows as unknown as JourneyPublicEntryPhotoRow[]; + } + /** JG91 — `linkGalleryPhotoToEntry`'s next-sort-order probe: `SELECT MAX(sort_order) as m FROM journey_entry_photos WHERE entry_id = ?`. */ async maxSortOrderForEntry(entryId: number): Promise { const row = await this.kysely>() diff --git a/server/src/db/repositories/JourneyPhotos.repository.ts b/server/src/db/repositories/JourneyPhotos.repository.ts index d9f75bd7c..f2123de09 100644 --- a/server/src/db/repositories/JourneyPhotos.repository.ts +++ b/server/src/db/repositories/JourneyPhotos.repository.ts @@ -50,6 +50,20 @@ interface GalleryKyselyDB { }; journey_entry_photos: { entry_id: number; journey_photo_id: number }; journey_entries: { id: number; entry_date: string; entry_time: string | null }; + journeys: { id: number; user_id: number }; +} + +/** JS7 (Plan 4 Task 8b relocation) — the public photo-validation join's row (`journey_photos` + `trek_photos`). */ +export interface JourneyPublicPhotoValidationRow { + photo_id: number; + owner_id: number | null; + journey_id: number; +} + +/** JS10 (Plan 4 Task 8b relocation) — the public asset-validation join's row (`journey_photos` + `trek_photos` + `journeys`). */ +export interface JourneyPublicAssetValidationRow { + owner_id: number | null; + journey_owner_id: number; } /** `GALLERY_SELECT`'s exact column list (`journey-domain.service.ts`'s module const), aliased `gp`/`tp` — shared by every gallery-shaped read below. */ @@ -169,6 +183,49 @@ export class JourneyPhotosRepository extends TrekRepository { return rows.map((r) => r.journey_id); } + /** + * JS7 (Plan 4 Task 8b relocation) — `JourneyShareService + * .validateShareTokenForPhoto`'s photo/journey resolution: `SELECT + * gp.photo_id, tkp.owner_id, gp.journey_id FROM journey_photos gp JOIN + * trek_photos tkp ON tkp.id=gp.photo_id WHERE gp.photo_id=? AND + * gp.journey_id=?`. Public/anonymous — reachable from + * `JourneyPublicController` with no authentication, gated only by the + * unguessable share token the service already checked before calling + * this. Previously lived as `JourneyShareTokens.repository.ts`'s own + * fallback stub from 3g Task 3 (this repository was still mid-flight when + * that task landed) — relocated here now that it is stable. + */ + async findGalleryPhotoForValidation(photoId: number, journeyId: number): Promise { + return await this.kysely() + .selectFrom('journey_photos as gp') + .innerJoin('trek_photos as tkp', 'tkp.id', 'gp.photo_id') + .select(['gp.photo_id', 'tkp.owner_id', 'gp.journey_id']) + .where('gp.photo_id', '=', photoId) + .where('gp.journey_id', '=', journeyId) + .executeTakeFirst(); + } + + /** + * JS10 (Plan 4 Task 8b relocation) — `JourneyShareService + * .validateShareTokenForAsset`'s owner resolution: `SELECT tkp.owner_id, + * j.user_id AS journey_owner_id FROM journey_photos gp JOIN trek_photos + * tkp ON tkp.id=gp.photo_id JOIN journeys j ON j.id=gp.journey_id WHERE + * tkp.asset_id=? AND gp.journey_id=?`. Public/anonymous, security-critical + * — the service never trusts a caller-supplied owner id; only this join + * resolves `ownerId`. Relocated the same way as {@link + * findGalleryPhotoForValidation}/JS7. + */ + async findAssetForValidation(assetId: string, journeyId: number): Promise { + return await this.kysely() + .selectFrom('journey_photos as gp') + .innerJoin('trek_photos as tkp', 'tkp.id', 'gp.photo_id') + .innerJoin('journeys as j', 'j.id', 'gp.journey_id') + .select(['tkp.owner_id', 'j.user_id as journey_owner_id']) + .where('tkp.asset_id', '=', assetId) + .where('gp.journey_id', '=', journeyId) + .executeTakeFirst(); + } + /** JG88/JG99 — `ensureInGallery`'s and `uploadGalleryPhotos`'s next-sort-order probe: `SELECT MAX(sort_order) as m FROM journey_photos WHERE journey_id = ?`, one statement text. */ async maxSortOrder(journeyId: number): Promise { const row = await this.kysely() diff --git a/server/src/db/repositories/JourneyShareTokens.repository.ts b/server/src/db/repositories/JourneyShareTokens.repository.ts index c6770353d..16304958a 100644 --- a/server/src/db/repositories/JourneyShareTokens.repository.ts +++ b/server/src/db/repositories/JourneyShareTokens.repository.ts @@ -1,4 +1,3 @@ -import type { JourneyEntry } from '../../types'; import type { JourneyShareTokens } from '../entities/JourneyShareTokens.entity'; import { TrekRepository } from './_shared/trek-repository'; @@ -30,42 +29,6 @@ export interface JourneyShareTokenAccessRow { share_gallery: number | null; } -/** JS7 — the photo-validation join's row (`journey_photos` + `trek_photos`). */ -export interface JourneyPublicPhotoValidationRow { - photo_id: number; - owner_id: number | null; - journey_id: number; -} - -/** JS10 — the asset-validation join's row (`journey_photos` + `trek_photos` + `journeys`). */ -export interface JourneyPublicAssetValidationRow { - owner_id: number | null; - journey_owner_id: number; -} - -/** JS14 — the public per-entry photo read. Column-for-column identical to `journey-domain.service.ts`'s `JP_SELECT` (only the `trek_photos` join alias differs, `tkp` vs `tp`) — see task-3-report.md for the reuse decision. */ -export interface JourneyPublicEntryPhotoRow { - id: number; - entry_id: number; - photo_id: number; - caption: string | null; - sort_order: number | null; - shared: number; - created_at: number; - provider: string; - asset_id: string | null; - owner_id: number | null; - file_path: string | null; - thumbnail_path: string | null; - width: number | null; - height: number | null; - media_type: string | null; - duration_ms: number | null; - taken_at: string | null; - lat: number | null; - lng: number | null; -} - /** JS15 — the public gallery read (`GALLERY_SELECT` shape, `GALLERY_CHRONOLOGICAL_ORDER` applied). */ export interface JourneyPublicGalleryRow { id: number; @@ -89,82 +52,6 @@ export interface JourneyPublicGalleryRow { lng: number | null; } -/** - * The narrow Kysely shape the public-surface methods below need. `journeys`, - * `journey_photos`, `journey_entry_photos`, `journey_entries` and - * `trek_photos` are NOT this repository's own tables — Task 2's - * `JourneyPhotosRepository`/`JourneyEntryPhotosRepository`/ - * `JourneyEntriesRepository` own them, but Task 2 was still mid-flight - * (uncommitted) when this task ran, so per the brief's own fallback - * instruction these six public/anonymous statements (JS7/JS10/JS13/JS14/ - * JS15, plus JS8/JS12 which reuse Task 1's already-stable - * `JourneysRepository.findById` instead) get a typed Kysely read here - * rather than depending on an in-flight repository. Flagged in - * task-3-report.md for a possible later relocation once Task 2 lands. - */ -interface JourneyPublicKyselyDB { - journey_entries: { - id: number; - journey_id: number; - source_trip_id: number | null; - source_place_id: number | null; - source_assignment_id: number | null; - author_id: number; - type: string; - title: string | null; - story: string | null; - entry_date: string; - entry_time: string | null; - location_name: string | null; - location_lat: number | null; - location_lng: number | null; - mood: string | null; - weather: string | null; - tags: string | null; - pros_cons: string | null; - visibility: string | null; - sort_order: number | null; - created_at: number; - updated_at: number; - stats_excluded: number; - dismissed: number; - country_code: string | null; - }; - journey_photos: { - id: number; - journey_id: number; - photo_id: number; - caption: string | null; - shared: number; - sort_order: number | null; - created_at: number; - }; - journey_entry_photos: { - entry_id: number; - journey_photo_id: number; - sort_order: number | null; - }; - journeys: { - id: number; - user_id: number; - }; - trek_photos: { - id: number; - provider: string; - asset_id: string | null; - owner_id: number | null; - file_path: string | null; - thumbnail_path: string | null; - width: number | null; - height: number | null; - media_type: string | null; - duration_ms: number | null; - taken_at: string | null; - lat: number | null; - lng: number | null; - }; -} - // L2 — `galleryChronologicalOrderExpr`/`listGalleryForPublicJourney` (JS15) // used to be a byte-identical duplicate of `JourneyPhotosRepository`'s own // `galleryChronologicalOrderExpr`/`galleryRead` (same `GALLERY_COLUMNS` @@ -179,15 +66,17 @@ interface JourneyPublicKyselyDB { // public gallery response. /** - * `journey_share_tokens` — public share links (Plan 3g Task 3, R4). Six of - * its methods (`findAccessByToken`, `findByToken` and the five cross-table - * public reads below) are reachable from `JourneyPublicController` with NO - * authentication at all, gated purely by the unguessable token. Every token - * lookup here is a plain `.where({token})`/`.where('token', '=', token)` - * equality — no `LIKE`, no `COLLATE NOCASE`, no case-folding of any kind - * (R4) — and every one of these returns `undefined` on a miss, never throws, - * so a missing/revoked/wrong-case/NUL token 404s through the controller - * instead of 500ing. + * `journey_share_tokens` — public share links (Plan 3g Task 3, R4). + * `findAccessByToken`/`findByToken` are reachable from + * `JourneyPublicController` with NO authentication at all, gated purely by + * the unguessable token — a plain `.where({token})`/`.where('token', '=', + * token)` equality, no `LIKE`, no `COLLATE NOCASE`, no case-folding of any + * kind (R4), returning `undefined` on a miss, never throwing, so a + * missing/revoked/wrong-case/NUL token 404s through the controller instead + * of 500ing. The five cross-table public reads (JS7/JS10/JS13/JS14/JS15) + * that used to live here as a fallback Kysely stub (3g Task 3, before Task + * 2's own repositories landed) have all been relocated to their natural + * home repositories — see the bottom of this file (Plan 4 Task 8b). */ export class JourneyShareTokensRepository extends TrekRepository { /** JS1 — `createOrUpdateJourneyShareLink`'s existing-link read: `SELECT token, share_timeline, share_gallery, share_map, newest_first FROM journey_share_tokens WHERE journey_id = ?`. */ @@ -260,72 +149,19 @@ export class JourneyShareTokensRepository extends TrekRepository('get', false); } - /** JS7 — `validateShareTokenForPhoto`'s photo/journey resolution: `SELECT gp.photo_id, tkp.owner_id, gp.journey_id FROM journey_photos gp JOIN trek_photos tkp ON tkp.id=gp.photo_id WHERE gp.photo_id=? AND gp.journey_id=?`. */ - async findGalleryPhotoForValidation(photoId: number, journeyId: number): Promise { - return await this.kysely() - .selectFrom('journey_photos as gp') - .innerJoin('trek_photos as tkp', 'tkp.id', 'gp.photo_id') - .select(['gp.photo_id', 'tkp.owner_id', 'gp.journey_id']) - .where('gp.photo_id', '=', photoId) - .where('gp.journey_id', '=', journeyId) - .executeTakeFirst(); - } + // JS7/JS10 — `validateShareTokenForPhoto`'s/`validateShareTokenForAsset`'s + // photo/asset resolution moved to `JourneyPhotosRepository + // .findGalleryPhotoForValidation`/`.findAssetForValidation` (Plan 4 Task + // 8b relocation — this repository's own fallback stub from 3g Task 3 is + // gone now that `JourneyPhotosRepository` is stable). - /** - * JS10 — `validateShareTokenForAsset`'s owner resolution: `SELECT - * tkp.owner_id, j.user_id AS journey_owner_id FROM journey_photos gp JOIN - * trek_photos tkp ON tkp.id=gp.photo_id JOIN journeys j ON j.id=gp.journey_id - * WHERE tkp.asset_id=? AND gp.journey_id=?`. Security-critical (the - * service never trusts a caller-supplied owner id — this join is the only - * source of the resolved `ownerId`). - */ - async findAssetForValidation(assetId: string, journeyId: number): Promise { - return await this.kysely() - .selectFrom('journey_photos as gp') - .innerJoin('trek_photos as tkp', 'tkp.id', 'gp.photo_id') - .innerJoin('journeys as j', 'j.id', 'gp.journey_id') - .select(['tkp.owner_id', 'j.user_id as journey_owner_id']) - .where('tkp.asset_id', '=', assetId) - .where('gp.journey_id', '=', journeyId) - .executeTakeFirst(); - } + // JS13 — `getPublicJourney`'s entry list moved to + // `JourneyEntriesRepository.listPublicEntries` (Plan 4 Task 8b, same + // reason as JS7/JS10 above). - /** JS13 — `getPublicJourney`'s entry list: `SELECT je.* FROM journey_entries je WHERE je.journey_id=? AND je.type != 'skeleton' AND je.dismissed=0 ORDER BY je.entry_date, je.sort_order`. Skeletons never appear publicly. */ - async listPublicEntries(journeyId: number): Promise { - const rows = await this.kysely() - .selectFrom('journey_entries as je') - .selectAll('je') - .where('je.journey_id', '=', journeyId) - .where('je.type', '!=', 'skeleton') - .where('je.dismissed', '=', 0) - .orderBy('je.entry_date', 'asc') - .orderBy('je.sort_order', 'asc') - .execute(); - return rows as unknown as JourneyEntry[]; - } - - /** - * JS14 — `getPublicJourney`'s per-entry photo read. The select list is - * byte-for-byte the same columns, in the same order, as `JP_SELECT` - * (`journey-domain.service.ts`) — only the `trek_photos` join alias - * differs (`tkp` here, `tp` there). See task-3-report.md for why this - * stays a separate, hand-kept query rather than reusing Task 2's - * `JourneyEntryPhotosRepository` (not yet landed when this task ran). - */ - async listEntryPhotosForPublicJourney(journeyId: number): Promise { - return await this.kysely() - .selectFrom('journey_entry_photos as jep') - .innerJoin('journey_photos as gp', 'gp.id', 'jep.journey_photo_id') - .innerJoin('trek_photos as tkp', 'tkp.id', 'gp.photo_id') - .select([ - 'gp.id', 'jep.entry_id', 'gp.photo_id', 'gp.caption', 'jep.sort_order', 'gp.shared', 'gp.created_at', - 'tkp.provider', 'tkp.asset_id', 'tkp.owner_id', 'tkp.file_path', 'tkp.thumbnail_path', 'tkp.width', 'tkp.height', - 'tkp.media_type', 'tkp.duration_ms', 'tkp.taken_at', 'tkp.lat', 'tkp.lng', - ]) - .where('gp.journey_id', '=', journeyId) - .orderBy('jep.sort_order', 'asc') - .execute(); - } + // JS14 — `getPublicJourney`'s per-entry photo read moved to + // `JourneyEntryPhotosRepository.listForPublicJourney` (Plan 4 Task 8b, + // dedupe against that repository's own `JP_COLUMNS`). // JS15 — `getPublicJourney`'s gallery read moved to // `JourneyPhotosRepository.galleryRead` (L2, above). diff --git a/server/src/nest/journey/journey-share.service.ts b/server/src/nest/journey/journey-share.service.ts index 9db0c3e5e..4d0947847 100644 --- a/server/src/nest/journey/journey-share.service.ts +++ b/server/src/nest/journey/journey-share.service.ts @@ -8,12 +8,16 @@ import { UnitOfWork } from '../database/unit-of-work'; import { Journeys } from '../../db/entities/Journeys.entity'; import type { JourneysRepository } from '../../db/repositories/Journeys.repository'; import { JourneyShareTokens } from '../../db/entities/JourneyShareTokens.entity'; -import type { - JourneyPublicEntryPhotoRow, - JourneyShareTokensRepository, -} from '../../db/repositories/JourneyShareTokens.repository'; +import type { JourneyShareTokensRepository } from '../../db/repositories/JourneyShareTokens.repository'; import { JourneyPhotos } from '../../db/entities/JourneyPhotos.entity'; import type { JourneyPhotosRepository } from '../../db/repositories/JourneyPhotos.repository'; +import { JourneyEntries } from '../../db/entities/JourneyEntries.entity'; +import type { JourneyEntriesRepository } from '../../db/repositories/JourneyEntries.repository'; +import { JourneyEntryPhotos } from '../../db/entities/JourneyEntryPhotos.entity'; +import type { + JourneyEntryPhotosRepository, + JourneyPublicEntryPhotoRow, +} from '../../db/repositories/JourneyEntryPhotos.repository'; interface JourneySharePermissions { share_timeline?: boolean; @@ -61,6 +65,14 @@ export class JourneyShareService { // byte-identical copy of the ORDER BY builder and the gallery query. private readonly uow: UnitOfWork, @InjectRepository(JourneyPhotos) private readonly photosRepo: JourneyPhotosRepository, + // Plan 4 Task 8b constructor-ripple: JS7/JS10 (`photosRepo`, above) and + // JS13/JS14 (`entriesRepo`/`entryPhotosRepo`, below) relocated off + // `JourneyShareTokensRepository`'s own fallback stub onto their natural + // home repositories — both already registered in `journey-domain.module + // .ts`'s `forFeature` array for `JourneyDomainService`'s own use, no + // module change needed. + @InjectRepository(JourneyEntries) private readonly entriesRepo: JourneyEntriesRepository, + @InjectRepository(JourneyEntryPhotos) private readonly entryPhotosRepo: JourneyEntryPhotosRepository, ) {} async createOrUpdateJourneyShareLink( @@ -171,8 +183,8 @@ export class JourneyShareService { // already strips photos when it is off. Enumerable photo ids otherwise stay // fetchable after the owner disables the gallery. if (!row.share_gallery) return null; - // JS7. - const photo = await this.shareTokensRepo.findGalleryPhotoForValidation(photoId, row.journey_id); + // JS7 (Plan 4 Task 8b: relocated to `JourneyPhotosRepository`). + const photo = await this.photosRepo.findGalleryPhotoForValidation(photoId, row.journey_id); if (!photo) return null; // JS8 — reuses `JourneysRepository.findById` (JG5's dup group), no // `canAccessJourney` involved: public means public. @@ -187,10 +199,11 @@ export class JourneyShareService { // Same as the unified photo proxy: no asset bytes leave the host unless the // owner shared the gallery. if (!row.share_gallery) return null; - // JS10 — security-critical: whose provider credentials get tried must - // never come from a number an anonymous caller put in the URL. Only this - // join resolves `ownerId`; a caller-supplied value never reaches it. - const photo = await this.shareTokensRepo.findAssetForValidation(assetId, row.journey_id); + // JS10 (Plan 4 Task 8b: relocated to `JourneyPhotosRepository`) — + // security-critical: whose provider credentials get tried must never + // come from a number an anonymous caller put in the URL. Only this join + // resolves `ownerId`; a caller-supplied value never reaches it. + const photo = await this.photosRepo.findAssetForValidation(assetId, row.journey_id); // Only resolve assets that actually belong to this shared journey. if (!photo) return null; // trek_photos.owner_id can be NULL. The journey's owner is the fallback, the @@ -207,11 +220,11 @@ export class JourneyShareService { const journey = await this.journeysRepo.findById(row.journey_id); if (!journey) return null; - // Entries with photos — JS13. - const entries = await this.shareTokensRepo.listPublicEntries(row.journey_id); + // Entries with photos — JS13 (Plan 4 Task 8b: relocated to `JourneyEntriesRepository`). + const entries = await this.entriesRepo.listPublicEntries(row.journey_id); - // JS14. - const photos = await this.shareTokensRepo.listEntryPhotosForPublicJourney(row.journey_id); + // JS14 (Plan 4 Task 8b: relocated to `JourneyEntryPhotosRepository`, dedupe against `JP_COLUMNS`). + const photos = await this.entryPhotosRepo.listForPublicJourney(row.journey_id); const photosByEntry: Record = {}; for (const p of photos) { diff --git a/server/tests/helpers/mcp-test-controllers.ts b/server/tests/helpers/mcp-test-controllers.ts index 3280edbd3..c1d97f470 100644 --- a/server/tests/helpers/mcp-test-controllers.ts +++ b/server/tests/helpers/mcp-test-controllers.ts @@ -139,7 +139,7 @@ import { import { createTestOrm } from './test-orm'; import { createTestTripFilesRepo, createTestFileLinksRepo, createTestBudgetItemsRepo } from './files-repos'; import { budgetRepoArgs } from './budget-repos'; -import { createTestShareTokensRepo, shareServiceRepoArgs } from './share-repos'; +import { createTestShareTokensRepo, shareServiceRepoArgs, createTestPluginsRepo, createTestPluginUserErasureQueueRepo } from './share-repos'; import { createTestCollabMessageReactionsRepo, createTestCollabNotesRepo, createTestCollabPollsRepo, createTestCollabPollVotesRepo, createTestCollabLinksRepo, createTestCollabMessagesRepo, @@ -223,7 +223,7 @@ export async function createMcpTestRegistry(): Promise { permissionsService, new TripMembershipService(await createTestTripsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection)), new WebauthnConfigService(appSettings), - new UserCleanupService(dbService, budgetService, await createTestUnitOfWork(dbService.connection), usersRepo, await createTestTripMembersRepo(dbService.connection), await createTestBudgetItemsRepo(dbService.connection), await createTestJourneyShareTokensRepo(dbService.connection), await createTestJourneysRepo(dbService.connection), await createTestJourneyEntriesRepo(dbService.connection), await createTestJourneyContributorsRepo(dbService.connection), await createTestShareTokensRepo(dbService.connection)), + new UserCleanupService(dbService, budgetService, await createTestUnitOfWork(dbService.connection), usersRepo, await createTestTripMembersRepo(dbService.connection), await createTestBudgetItemsRepo(dbService.connection), await createTestJourneyShareTokensRepo(dbService.connection), await createTestJourneysRepo(dbService.connection), await createTestJourneyEntriesRepo(dbService.connection), await createTestJourneyContributorsRepo(dbService.connection), await createTestShareTokensRepo(dbService.connection), await createTestPluginsRepo(dbService.connection), await createTestPluginUserErasureQueueRepo(dbService.connection)), new MailerService(usersRepo, settingsRepo, appSettings), new EphemeralTokenService(), new AllowedFileTypesService(appSettings), await createTestUnitOfWork(dbService.connection), @@ -322,7 +322,7 @@ export async function createMcpTestRegistry(): Promise { ); // Built after it: a hotel booking writes the stay's day stop through this one. const reservationsService = new ReservationsService(permissionsService, budgetService, realtimeService, notificationsStub(), new ReservationsReadService(await createTestReservationsRepo(dbService.connection), await createTestReservationEndpointsRepo(dbService.connection), await createTestReservationTravelersRepo(dbService.connection)), accommodationsService, await createTestUnitOfWork(dbService.connection), await createTestReservationsRepo(dbService.connection), await createTestReservationEndpointsRepo(dbService.connection), await createTestReservationTravelersRepo(dbService.connection), await createTestReservationDayPositionsRepo(dbService.connection), await createTestDayAccommodationsRepo(dbService.connection), await createTestDaysRepo(dbService.connection), await createTestPlacesRepo(dbService.connection), await createTestDayAssignmentsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection), await createTestUsersRepo(dbService.connection), await createTestTripsRepo(dbService.connection), await createTestBudgetItemsRepo(dbService.connection)); - const membersService = new TripMembersService(budgetService, new UserCleanupService(dbService, budgetService, await createTestUnitOfWork(dbService.connection), usersRepo, await createTestTripMembersRepo(dbService.connection), await createTestBudgetItemsRepo(dbService.connection), await createTestJourneyShareTokensRepo(dbService.connection), await createTestJourneysRepo(dbService.connection), await createTestJourneyEntriesRepo(dbService.connection), await createTestJourneyContributorsRepo(dbService.connection), await createTestShareTokensRepo(dbService.connection)), permissionsService, realtimeService, notificationsStub(), await createTestUnitOfWork(dbService.connection), await createTestTripsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection), usersRepo); + const membersService = new TripMembersService(budgetService, new UserCleanupService(dbService, budgetService, await createTestUnitOfWork(dbService.connection), usersRepo, await createTestTripMembersRepo(dbService.connection), await createTestBudgetItemsRepo(dbService.connection), await createTestJourneyShareTokensRepo(dbService.connection), await createTestJourneysRepo(dbService.connection), await createTestJourneyEntriesRepo(dbService.connection), await createTestJourneyContributorsRepo(dbService.connection), await createTestShareTokensRepo(dbService.connection), await createTestPluginsRepo(dbService.connection), await createTestPluginUserErasureQueueRepo(dbService.connection)), permissionsService, realtimeService, notificationsStub(), await createTestUnitOfWork(dbService.connection), await createTestTripsRepo(dbService.connection), await createTestTripMembersRepo(dbService.connection), usersRepo); const tripsService = new TripsService( dbService, reservationsService, @@ -484,6 +484,10 @@ export async function createMcpTestRegistry(): Promise { await createTestJourneyShareTokensRepo(dbService.connection), await createTestJourneysRepo(dbService.connection), // task-5-fix-brief constructor-ripple: `UnitOfWork` (L1) + `JourneyPhotosRepository` (L2). await createTestUnitOfWork(dbService.connection), await createTestJourneyPhotosRepo(dbService.connection), + // Plan 4 Task 8b constructor-ripple: `JourneyEntriesRepository` (JS13) + // + `JourneyEntryPhotosRepository` (JS14), relocated off + // `JourneyShareTokensRepository`'s own fallback stub. + await createTestJourneyEntriesRepo(dbService.connection), await createTestJourneyEntryPhotosRepo(dbService.connection), ), addonsService, authService, captureBackfill), new MemoriesMcp(immichService, synologyService, addonsService, mcpOrm.repo(PhotoProviders)), new NotificationsMcp(await makeNotificationsService(dbService, realtimeService), authService), diff --git a/server/tests/unit/nest/journey-share.service.test.ts b/server/tests/unit/nest/journey-share.service.test.ts index d5fe44293..0d00c3953 100644 --- a/server/tests/unit/nest/journey-share.service.test.ts +++ b/server/tests/unit/nest/journey-share.service.test.ts @@ -48,6 +48,8 @@ import { } from '../../helpers/journey-repos'; import { createTestJourneyShareTokensRepo } from '../../helpers/journey-share-repos'; import type { JourneyPublicGalleryRow, JourneyShareTokensRepository } from '../../../src/db/repositories/JourneyShareTokens.repository'; +import type { JourneyEntriesRepository } from '../../../src/db/repositories/JourneyEntries.repository'; +import type { JourneyEntryPhotosRepository } from '../../../src/db/repositories/JourneyEntryPhotos.repository'; import { GALLERY_CHRONOLOGICAL_ORDER } from '../../../src/nest/journey/journey-gallery-order'; const dbs = new DatabaseService(dbConn); @@ -79,6 +81,10 @@ beforeAll(async () => { // task-5-fix-brief constructor-ripple: `UnitOfWork` (L1's transactional // create/update) + `JourneyPhotosRepository` (L2's `galleryRead` reuse). uow, await createTestJourneyPhotosRepo(testDb), + // Plan 4 Task 8b constructor-ripple: `JourneyEntriesRepository` (JS13) + + // `JourneyEntryPhotosRepository` (JS14), relocated off + // `JourneyShareTokensRepository`'s own fallback stub. + await createTestJourneyEntriesRepo(testDb), await createTestJourneyEntryPhotosRepo(testDb), ); }); @@ -774,10 +780,18 @@ describe('parity — JourneyShareTokensRepository reads match the legacy stateme // pins that method's output against the legacy statement instead of the // (now-deleted) `JourneyShareTokensRepository.listGalleryForPublicJourney`. let journeyPhotosRepo: Awaited>; + // Plan 4 Task 8b — JS7/JS10/JS13/JS14 relocated the same way JS15 was: P04 + // and P05 below now pin `JourneyEntriesRepository.listPublicEntries` and + // `JourneyEntryPhotosRepository.listForPublicJourney` instead of the + // (now-deleted) `JourneyShareTokensRepository` fallback stub methods. + let journeyEntriesRepo: JourneyEntriesRepository; + let journeyEntryPhotosRepo: JourneyEntryPhotosRepository; beforeAll(async () => { shareTokensRepo = await createTestJourneyShareTokensRepo(testDb); journeyPhotosRepo = await createTestJourneyPhotosRepo(testDb); + journeyEntriesRepo = await createTestJourneyEntriesRepo(testDb); + journeyEntryPhotosRepo = await createTestJourneyEntryPhotosRepo(testDb); }); it('JOURNEY-SHARE-P01: findFlagsByJourneyId (JS1) matches the legacy 5-column read', async () => { @@ -837,10 +851,10 @@ describe('parity — JourneyShareTokensRepository reads match the legacy stateme `) .all(journey.id); - expect(await shareTokensRepo.listPublicEntries(journey.id)).toEqual(legacy); + expect(await journeyEntriesRepo.listPublicEntries(journey.id)).toEqual(legacy); }); - it('JOURNEY-SHARE-P05: listEntryPhotosForPublicJourney (JS14) matches the legacy JP_SELECT-shaped statement', async () => { + it('JOURNEY-SHARE-P05: JourneyEntryPhotosRepository.listForPublicJourney (JS14) matches the legacy JP_SELECT-shaped statement', async () => { const { user } = createUser(testDb); const journey = createJourney(testDb, user.id); const entry = createJourneyEntry(testDb, journey.id, user.id, { type: 'entry', entry_date: '2026-01-01' }); @@ -859,7 +873,7 @@ describe('parity — JourneyShareTokensRepository reads match the legacy stateme `) .all(journey.id); - expect(await shareTokensRepo.listEntryPhotosForPublicJourney(journey.id)).toEqual(legacy); + expect(await journeyEntryPhotosRepo.listForPublicJourney(journey.id)).toEqual(legacy); }); it('JOURNEY-SHARE-P06: JourneyPhotosRepository.galleryRead (JS15, GALLERY_CHRONOLOGICAL_ORDER) matches the legacy statement — a photo linked to an entry AND an unattached gallery photo', async () => {