mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-22 15:06:32 +00:00
refactor(journey): journey stats, entries, photos, contributors and suggestions through repositories
This commit is contained in:
@@ -42,6 +42,71 @@ interface JourneyEntryPhotosExistsKyselyDB {
|
||||
journey_entry_photos: { entry_id: number };
|
||||
}
|
||||
|
||||
/** JG64's narrow `journeyStats` projection — NOT the same text as JG14/JG71 (a wider `SELECT *`); a distinct, narrower column list read directly off the current source, not the inventory's (stale) "same text as JG14" annotation. */
|
||||
export interface StatsEntryRow {
|
||||
id: number;
|
||||
title: string | null;
|
||||
location_name: string | null;
|
||||
location_lat: number | null;
|
||||
location_lng: number | null;
|
||||
entry_date: string | null;
|
||||
source_trip_id: number | null;
|
||||
source_place_id: number | null;
|
||||
stats_excluded: number;
|
||||
}
|
||||
|
||||
/** JG65's per-trip stats row (`journeyStats`). */
|
||||
export interface StatsTripRow {
|
||||
id: number;
|
||||
title: string | null;
|
||||
start: string | null;
|
||||
end: string | null;
|
||||
}
|
||||
|
||||
/** JG66's per-place stats row (`journeyStats`), one row per place at its earliest day. */
|
||||
export interface StatsPlaceRow {
|
||||
id: number;
|
||||
name: string | null;
|
||||
lat: number | null;
|
||||
lng: number | null;
|
||||
tripId: number | null;
|
||||
day: string | null;
|
||||
ord: number | null;
|
||||
}
|
||||
|
||||
/** JG120/JG121's trip-picker row (`getSuggestions`/`listUserTrips`), the place-count-annotated trip summary. */
|
||||
export interface TripPickerRow {
|
||||
id: number;
|
||||
title: string;
|
||||
start_date: string | null;
|
||||
end_date: string | null;
|
||||
cover_image: string | null;
|
||||
place_count: number;
|
||||
}
|
||||
|
||||
/** The narrow `journey_entries`/`journey_trips`/`trips`/`places`/`day_assignments`/`days` shape `journeyStats`'s cross-domain reads (JG65-67) need. */
|
||||
interface StatsKyselyDB {
|
||||
journey_trips: { journey_id: number; trip_id: number };
|
||||
trips: { id: number; title: string | null; start_date: string | null; end_date: string | null };
|
||||
places: { id: number; trip_id: number; name: string | null; lat: number | null; lng: number | null };
|
||||
day_assignments: { id: number; place_id: number; day_id: number; order_index: number | null };
|
||||
days: { id: number; date: string | null };
|
||||
}
|
||||
|
||||
/** JG70's chunked cached-country probe (`journeyStats`, `place_regions` — atlas-owned, 3f-DONE, read-only here). */
|
||||
interface PlaceRegionsKyselyDB {
|
||||
place_regions: { place_id: number; country_code: string | null };
|
||||
}
|
||||
|
||||
/** The narrow `trips`/`trip_members`/`places`/`day_assignments`/`journey_trips` shape JG120/JG121's trip-picker reads need. */
|
||||
interface TripPickerKyselyDB {
|
||||
trips: { id: number; title: string; start_date: string | null; end_date: string | null; cover_image: string | null; user_id: number };
|
||||
trip_members: { trip_id: number; user_id: number };
|
||||
places: { id: number; trip_id: number };
|
||||
day_assignments: { id: number; place_id: number };
|
||||
journey_trips: { trip_id: number };
|
||||
}
|
||||
|
||||
/**
|
||||
* `journey_entries` — first cut (Plan 3g Task 1, R9's Part A): every
|
||||
* insert/find/update/delete the sync engine (`syncTripPlaces`/
|
||||
@@ -235,6 +300,288 @@ export class JourneyEntriesRepository extends TrekRepository<JourneyEntries> {
|
||||
return !!row;
|
||||
}
|
||||
|
||||
// ── Plan 3g Task 2 (Part B) — appended-only below this line. Every method
|
||||
// above this belongs to Task 1 (Part A) and is untouched by this task. ──
|
||||
|
||||
/**
|
||||
* JG64 — `journeyStats`'s own entries read. Read directly off the current
|
||||
* source rather than the inventory's "same text as JG14" annotation,
|
||||
* which is stale at HEAD: the actual statement is a NARROWER, distinct
|
||||
* column list (`id, title, location_name, location_lat, location_lng,
|
||||
* entry_date, source_trip_id, source_place_id, stats_excluded`), not
|
||||
* `SELECT *`. `stats_excluded` stays a plain column projection — filtered
|
||||
* in the SERVICE, never the WHERE clause (plan3g-inputs.md correction #3:
|
||||
* the caller needs the excluded rows too, for the response's `excluded`
|
||||
* field).
|
||||
*/
|
||||
async listStatsRows(journeyId: number): Promise<StatsEntryRow[]> {
|
||||
return await this.qb('je')
|
||||
.select(['je.id', 'je.title', 'je.location_name', 'je.location_lat', 'je.location_lng', 'je.entry_date', 'je.sourceTrip', 'je.sourcePlace', 'je.stats_excluded'])
|
||||
.where({ journey: journeyId, dismissed: 0 })
|
||||
.orderBy({ entry_date: 'asc', sort_order: 'asc', id: 'asc' })
|
||||
.execute<StatsEntryRow[]>('all', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* JG65 — `journeyStats`'s per-trip read, undated trips sorted last (the
|
||||
* `start_date IS NULL` ordering trick, recomputed as its own ORDER BY
|
||||
* expression rather than referencing the SELECT list's own alias — Kysely
|
||||
* has no typed "order by select alias" form, and recomputing the same
|
||||
* `t.start_date`/`t.id` references is equivalent SQL). Housed on this
|
||||
* repository (not `JourneyTripsRepository`, Task 1's own file, outside
|
||||
* this task's named file set) the same way `JourneyTripsRepository`
|
||||
* itself reaches `places`/`day_assignments`/`days` for the sync engine —
|
||||
* a repository may query any table over `this.kysely()`.
|
||||
*/
|
||||
async listStatsTrips(journeyId: number): Promise<StatsTripRow[]> {
|
||||
const rows = await this.kysely<StatsKyselyDB>()
|
||||
.selectFrom('journey_trips as jt')
|
||||
.innerJoin('trips as t', 't.id', 'jt.trip_id')
|
||||
.select(['t.id', 't.title', 't.start_date as start', 't.end_date as end'])
|
||||
.where('jt.journey_id', '=', journeyId)
|
||||
.orderBy((eb) => eb('t.start_date', 'is', null), 'asc')
|
||||
.orderBy('t.start_date', 'asc')
|
||||
.orderBy('t.id', 'asc')
|
||||
.execute();
|
||||
return rows as StatsTripRow[];
|
||||
}
|
||||
|
||||
/**
|
||||
* JG66 — `journeyStats`'s per-place read: one row per place at its
|
||||
* earliest day (`GROUP BY p.id`, aggregated so a hotel spanning several
|
||||
* nights' assignments counts once, not once per night).
|
||||
*/
|
||||
async listStatsPlaces(journeyId: number): Promise<StatsPlaceRow[]> {
|
||||
const rows = await this.kysely<StatsKyselyDB>()
|
||||
.selectFrom('journey_trips as jt')
|
||||
.innerJoin('places as p', 'p.trip_id', 'jt.trip_id')
|
||||
.leftJoin('day_assignments as da', 'da.place_id', 'p.id')
|
||||
.leftJoin('days as d', 'd.id', 'da.day_id')
|
||||
.select((eb) => ['p.id', 'p.name', 'p.lat', 'p.lng', 'p.trip_id as tripId', eb.fn.min<string | null>('d.date').as('day'), eb.fn.min<number | null>('da.order_index').as('ord')])
|
||||
.where('jt.journey_id', '=', journeyId)
|
||||
.groupBy('p.id')
|
||||
.orderBy((eb) => eb(eb.fn.min<string | null>('d.date'), 'is', null), 'asc')
|
||||
.orderBy((eb) => eb.fn.min('d.date'), 'asc')
|
||||
.orderBy((eb) => eb.fn.min('da.order_index'), 'asc')
|
||||
.orderBy('p.id', 'asc')
|
||||
.execute();
|
||||
return rows as StatsPlaceRow[];
|
||||
}
|
||||
|
||||
/** JG67 — `journeyStats`'s place count: `SELECT COUNT(*) AS n FROM journey_trips jt JOIN places p ON p.trip_id=jt.trip_id WHERE jt.journey_id=?`. */
|
||||
async countStatsPlaces(journeyId: number): Promise<number> {
|
||||
const row = await this.kysely<StatsKyselyDB>()
|
||||
.selectFrom('journey_trips as jt')
|
||||
.innerJoin('places as p', 'p.trip_id', 'jt.trip_id')
|
||||
.select((eb) => eb.fn.countAll<number>().as('n'))
|
||||
.where('jt.journey_id', '=', journeyId)
|
||||
.executeTakeFirst();
|
||||
return row?.n ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* JG70 — `journeyStats`'s cached-country probe, chunked in groups of 400
|
||||
* (SQLite's bound-variable-count limit is why it's chunked at all — do
|
||||
* not collapse into one unchunked `IN`). Returns every matching row
|
||||
* unfiltered (including a null/empty `country_code`) — the `if
|
||||
* (r.country_code)` filter and the `.toUpperCase()` call both stay in the
|
||||
* SERVICE, matching the legacy code's own JS-side handling.
|
||||
*/
|
||||
async listCachedCountriesForPlaceIds(placeIds: number[]): Promise<{ place_id: number; country_code: string | null }[]> {
|
||||
const out: { place_id: number; country_code: string | null }[] = [];
|
||||
for (let i = 0; i < placeIds.length; i += 400) {
|
||||
const chunk = placeIds.slice(i, i + 400);
|
||||
if (!chunk.length) continue;
|
||||
const rows = await this.kysely<PlaceRegionsKyselyDB>()
|
||||
.selectFrom('place_regions')
|
||||
.select(['place_id', 'country_code'])
|
||||
.where('place_id', 'in', chunk)
|
||||
.execute();
|
||||
out.push(...rows);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** JG76/JG77/JG80/JG84/JG94/JG95/JG97/JG103 — `SELECT * FROM journey_entries WHERE id = ?`, the widest dup group in this file. */
|
||||
async findById(id: number): Promise<JourneyEntry | null> {
|
||||
const row = await this.qb('je').select(['je.*']).where({ id }).execute<JourneyEntry | undefined>('get', false);
|
||||
return row ?? null;
|
||||
}
|
||||
|
||||
/** JG75 — `createEntry`'s INSERT (19 columns). `country_code` is resolved by the SERVICE's `countryFor` before this call, matching {@link insertSkeleton}'s own contract. */
|
||||
async insertEntry(data: {
|
||||
journey_id: number;
|
||||
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;
|
||||
country_code: string | null;
|
||||
mood: string | null;
|
||||
weather: string | null;
|
||||
tags: string | null;
|
||||
pros_cons: string | null;
|
||||
visibility: string;
|
||||
sort_order: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}): Promise<number> {
|
||||
return await this.insert({
|
||||
journey: data.journey_id,
|
||||
author: data.author_id,
|
||||
type: data.type,
|
||||
title: data.title,
|
||||
story: data.story,
|
||||
entry_date: data.entry_date,
|
||||
entry_time: data.entry_time,
|
||||
location_name: data.location_name,
|
||||
location_lat: data.location_lat,
|
||||
location_lng: data.location_lng,
|
||||
country_code: data.country_code,
|
||||
mood: data.mood,
|
||||
weather: data.weather,
|
||||
tags: data.tags,
|
||||
pros_cons: data.pros_cons,
|
||||
visibility: data.visibility,
|
||||
sort_order: data.sort_order,
|
||||
created_at: data.created_at,
|
||||
updated_at: data.updated_at,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* JG78 — `updateEntry`'s dynamic UPDATE (R6, `presenceSet`). The SERVICE
|
||||
* resolves every field to its final bound value first (`tags`/`pros_cons`
|
||||
* JSON-encoded, `stats_excluded`/`dismissed` boolean-coerced to 0/1,
|
||||
* `country_code` recomputed whenever either `location_lat`/`location_lng`
|
||||
* is present in the patch, the skeleton→entry promotion) — this writes
|
||||
* exactly the patch it is handed, same contract every other
|
||||
* `presenceSet`-fed repository method in this program follows.
|
||||
*/
|
||||
async updateFields(
|
||||
id: number,
|
||||
patch: Partial<{
|
||||
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;
|
||||
sort_order: number;
|
||||
stats_excluded: number;
|
||||
dismissed: number;
|
||||
country_code: string | null;
|
||||
updated_at: number;
|
||||
}>,
|
||||
): Promise<void> {
|
||||
await this.nativeUpdate({ id }, patch);
|
||||
}
|
||||
|
||||
/** JG81 — `reorderEntries`'s ownership-verification read: `SELECT id FROM journey_entries WHERE id IN (${placeholders}) AND journey_id = ?`. */
|
||||
async listIdsIn(journeyId: number, ids: number[]): Promise<number[]> {
|
||||
const rows = await this.qb('je')
|
||||
.select(['je.id'])
|
||||
.where({ id: { $in: ids }, journey: journeyId })
|
||||
.execute<{ id: number }[]>('all', false);
|
||||
return rows.map((r) => r.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* JG82 — `reorderEntries`'s per-id sort_order write, called in a loop
|
||||
* inside the SERVICE's own `uow.transactional` block (the legacy
|
||||
* statement prepared once and reused across the loop; this program's
|
||||
* repository layer has no direct "one prepared statement, many binds"
|
||||
* equivalent, so a `nativeUpdate` call per id is the accepted substitute
|
||||
* per the task brief — still one write per id, still inside the SAME
|
||||
* transaction, still in the caller's own `orderedIds` array order).
|
||||
*/
|
||||
async updateSortOrder(id: number, sortOrder: number, updatedAt: number): Promise<void> {
|
||||
await this.nativeUpdate({ id }, { sort_order: sortOrder, updated_at: updatedAt });
|
||||
}
|
||||
|
||||
/** JG85 — `deleteEntry`'s revert-to-skeleton write (a filled, trip-sourced entry is "deleted" by reverting it, not removed — {@link deleteById} (JG50/61/86) is the true hard delete, for entries with no trip origin). */
|
||||
async revertToSkeleton(id: number, updatedAt: number): Promise<void> {
|
||||
await this.nativeUpdate({ id }, { type: 'skeleton', story: null, mood: null, weather: null, pros_cons: null, visibility: 'private', updated_at: updatedAt });
|
||||
}
|
||||
|
||||
/** JG87 — `promoteSkeletonIfNeeded`'s write: `UPDATE journey_entries SET type = ?, updated_at = ? WHERE id = ?`, called only when the entry is still a skeleton (the SERVICE's own `if (entry.type !== 'skeleton') return;` guard, unchanged). */
|
||||
async markAsEntry(id: number, updatedAt: number): Promise<void> {
|
||||
await this.nativeUpdate({ id }, { type: 'entry', updated_at: updatedAt });
|
||||
}
|
||||
|
||||
/**
|
||||
* JG120 — `getSuggestions`'s trip-picker read: recently-ended trips not
|
||||
* yet linked to any journey. `date('now')` is resolved in the SERVICE via
|
||||
* `todayUtc()` (`@trek/shared`) and bound as a plain parameter, the SAME
|
||||
* pattern Plan 3f's atlas conversion already established for a bare
|
||||
* `date('now')` comparison (`Trips.repository.ts#lastStartedTrip`'s own
|
||||
* docstring) — no new dialect helper. Housed here for the same
|
||||
* cross-domain-reach-via-`this.kysely()` reason as {@link listStatsTrips}
|
||||
* above (`TripsRepository`/`JourneyTripsRepository` are both outside this
|
||||
* task's named file set).
|
||||
*/
|
||||
async listSuggestedTrips(userId: number, since: string, today: string): Promise<TripPickerRow[]> {
|
||||
const rows = await this.kysely<TripPickerKyselyDB>()
|
||||
.selectFrom('trips as t')
|
||||
.leftJoin('trip_members as tm', (join) => join.onRef('tm.trip_id', '=', 't.id').on('tm.user_id', '=', userId))
|
||||
.select((eb) => [
|
||||
't.id',
|
||||
't.title',
|
||||
't.start_date',
|
||||
't.end_date',
|
||||
't.cover_image',
|
||||
eb
|
||||
.selectFrom('places as p')
|
||||
.innerJoin('day_assignments as da', 'da.place_id', 'p.id')
|
||||
.select((eb2) => eb2.fn.countAll<number>().as('c'))
|
||||
.whereRef('p.trip_id', '=', 't.id')
|
||||
.as('place_count'),
|
||||
])
|
||||
.where((eb) => eb.or([eb('t.user_id', '=', userId), eb('tm.user_id', '=', userId)]))
|
||||
.where('t.end_date', 'is not', null)
|
||||
.where('t.end_date', '>=', since)
|
||||
.where('t.end_date', '<=', today)
|
||||
.where((eb) => eb('t.id', 'not in', eb.selectFrom('journey_trips').select('trip_id')))
|
||||
.orderBy('t.end_date', 'desc')
|
||||
.execute();
|
||||
return rows as TripPickerRow[];
|
||||
}
|
||||
|
||||
/** JG121 — `listUserTrips`'s trip-picker source list: every trip the caller owns or is a member of, place-count-annotated, no date filter (unlike JG120). */
|
||||
async listUserTripsPicker(userId: number): Promise<TripPickerRow[]> {
|
||||
const rows = await this.kysely<TripPickerKyselyDB>()
|
||||
.selectFrom('trips as t')
|
||||
.leftJoin('trip_members as tm', (join) => join.onRef('tm.trip_id', '=', 't.id').on('tm.user_id', '=', userId))
|
||||
.select((eb) => [
|
||||
't.id',
|
||||
't.title',
|
||||
't.start_date',
|
||||
't.end_date',
|
||||
't.cover_image',
|
||||
eb
|
||||
.selectFrom('places as p')
|
||||
.innerJoin('day_assignments as da', 'da.place_id', 'p.id')
|
||||
.select((eb2) => eb2.fn.countAll<number>().as('c'))
|
||||
.whereRef('p.trip_id', '=', 't.id')
|
||||
.as('place_count'),
|
||||
])
|
||||
.where((eb) => eb.or([eb('t.user_id', '=', userId), eb('tm.user_id', '=', userId)]))
|
||||
.orderBy('t.start_date', 'desc')
|
||||
.execute();
|
||||
return rows as TripPickerRow[];
|
||||
}
|
||||
|
||||
/** JG63 — `journeyTracks`: every place with route geometry belonging to a trip this journey's entries reference. */
|
||||
async listTracksSource(journeyId: number): Promise<JourneyTrackSourceRow[]> {
|
||||
const rows = await this.kysely<JourneyTracksKyselyDB>()
|
||||
|
||||
@@ -1,4 +1,186 @@
|
||||
import type { JourneyPhoto } from '../../types';
|
||||
import type { JourneyEntryPhotos } from '../entities/JourneyEntryPhotos.entity';
|
||||
import { TrekRepository } from './_shared/trek-repository';
|
||||
|
||||
export class JourneyEntryPhotosRepository extends TrekRepository<JourneyEntryPhotos> {}
|
||||
/** The narrow `journey_entry_photos`/`journey_photos`/`trek_photos`/`journey_entries` shape the `JP_SELECT`/`JP_JOIN`-shaped reads need. */
|
||||
interface JourneyPhotoJoinKyselyDB {
|
||||
journey_entry_photos: { entry_id: number; journey_photo_id: number; sort_order: number | null; created_at: number };
|
||||
journey_photos: { id: number; journey_id: number; photo_id: number; caption: string | null; shared: number | null; sort_order: number | null; created_at: 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;
|
||||
};
|
||||
journey_entries: { id: number; journey_id: number };
|
||||
}
|
||||
|
||||
/** `JP_SELECT`'s exact column list (`journey-domain.service.ts`'s module const), aliased `jep`/`gp`/`tp` — shared by every `JP_SELECT`/`JP_JOIN`-shaped read below. */
|
||||
const JP_COLUMNS = [
|
||||
'gp.id', 'jep.entry_id', 'gp.photo_id', 'gp.caption', 'jep.sort_order', 'gp.shared', 'gp.created_at',
|
||||
'tp.provider', 'tp.asset_id', 'tp.owner_id', 'tp.file_path', 'tp.thumbnail_path', 'tp.width', 'tp.height',
|
||||
'tp.media_type', 'tp.duration_ms', 'tp.taken_at', 'tp.lat', 'tp.lng',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* `journey_entry_photos` — the entry↔gallery-photo junction (Plan 3g Task 2,
|
||||
* Part B). A genuine TWO-column composite primary key (`entry` +
|
||||
* `journeyPhoto`, both `.primary()`, R3 — pinned against
|
||||
* `Migration20200101020300_journey_gallery_refactor.ts:41-47`'s
|
||||
* `PRIMARY KEY(entry_id, journey_photo_id)` by Task 0). {@link insertIgnore}
|
||||
* (JG92) is checked against Task 1's `JourneyTripsRepository.insertIgnore`
|
||||
* (the IGNORE-shaped composite-PK reference — `JourneyContributorsRepository
|
||||
* .upsertContributor` is R3's OTHER pinned reference, but it is the
|
||||
* MERGE/REPLACE-shaped one for JG117; JG92 is `INSERT OR IGNORE`, the same
|
||||
* semantics as JG30, so it is checked against THAT reference, per
|
||||
* `task-2-report.md`).
|
||||
*/
|
||||
export class JourneyEntryPhotosRepository extends TrekRepository<JourneyEntryPhotos> {
|
||||
/**
|
||||
* JG15/JG72 — `getJourneyFull`'s and `listEntries`'s per-entry photo join:
|
||||
* `SELECT {JP_SELECT} FROM {JP_JOIN} WHERE jep.entry_id IN (SELECT id FROM
|
||||
* journey_entries WHERE journey_id = ?) ORDER BY jep.sort_order ASC`. The
|
||||
* legacy `entry_id IN (subquery)` becomes an inner join to
|
||||
* `journey_entries` filtered by `journey_id` — `journey_entries.id` is a
|
||||
* plain PK, so the join can never fan a `jep` row out, the same
|
||||
* "join can't duplicate" reasoning `JourneysRepository.listForUser`'s
|
||||
* docstring gives for its own subquery-shaped legacy WHERE.
|
||||
*/
|
||||
async listForJourney(journeyId: number): Promise<JourneyPhoto[]> {
|
||||
const rows = await this.kysely<JourneyPhotoJoinKyselyDB>()
|
||||
.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')
|
||||
.innerJoin('journey_entries as je', 'je.id', 'jep.entry_id')
|
||||
.select(JP_COLUMNS)
|
||||
.where('je.journey_id', '=', journeyId)
|
||||
.orderBy('jep.sort_order', 'asc')
|
||||
.execute();
|
||||
return rows as JourneyPhoto[];
|
||||
}
|
||||
|
||||
/**
|
||||
* JG93 — `linkGalleryPhotoToEntry`'s post-insert read: `SELECT {JP_SELECT}
|
||||
* FROM {JP_JOIN} WHERE jep.entry_id = ? AND jep.journey_photo_id = ?`.
|
||||
* Named `findLink`, not `findOne` — `TrekRepository`/`EntityRepository`
|
||||
* already declare `findOne` with MikroORM's own generic signature, the
|
||||
* same collision `JourneyContributorsRepository.upsertContributor`'s own
|
||||
* docstring names for `upsert`.
|
||||
*/
|
||||
async findLink(entryId: number, journeyPhotoId: number): Promise<JourneyPhoto | undefined> {
|
||||
const row = await this.kysely<JourneyPhotoJoinKyselyDB>()
|
||||
.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('jep.entry_id', '=', entryId)
|
||||
.where('jep.journey_photo_id', '=', journeyPhotoId)
|
||||
.executeTakeFirst();
|
||||
return row as JourneyPhoto | undefined;
|
||||
}
|
||||
|
||||
/** JG113 — `updatePhoto`'s return read: `SELECT {JP_SELECT} FROM {JP_JOIN} WHERE gp.id = ? LIMIT 1`. */
|
||||
async findOneByGalleryId(galleryId: number): Promise<JourneyPhoto | undefined> {
|
||||
const row = await this.kysely<JourneyPhotoJoinKyselyDB>()
|
||||
.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.id', '=', galleryId)
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
return row as JourneyPhoto | undefined;
|
||||
}
|
||||
|
||||
/** 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<number | null> {
|
||||
const row = await this.kysely<Pick<JourneyPhotoJoinKyselyDB, 'journey_entry_photos'>>()
|
||||
.selectFrom('journey_entry_photos')
|
||||
.select((eb) => eb.fn.max<number | null>('sort_order').as('m'))
|
||||
.where('entry_id', '=', entryId)
|
||||
.executeTakeFirst();
|
||||
return row?.m ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JG92 — `linkGalleryPhotoToEntry`'s `INSERT OR IGNORE INTO
|
||||
* journey_entry_photos (entry_id, journey_photo_id, sort_order,
|
||||
* created_at) VALUES (?,?,?,?)`, the composite-PK conflict target
|
||||
* (`entry`+`journeyPhoto`). `INSERT OR IGNORE` never touches an existing
|
||||
* row, so — like Task 1's `JourneyTripsRepository.insertIgnore` (the
|
||||
* reference this is checked against) — there is no R3 reset-column trap
|
||||
* here.
|
||||
*/
|
||||
async insertIgnore(entryId: number, journeyPhotoId: number, sortOrder: number, createdAt: number): Promise<void> {
|
||||
await this.upsert(
|
||||
{ entry: entryId, journeyPhoto: journeyPhotoId, sort_order: sortOrder, created_at: createdAt },
|
||||
{ onConflictFields: ['entry', 'journeyPhoto'], onConflictAction: 'ignore' },
|
||||
);
|
||||
}
|
||||
|
||||
/** JG96 — `addProviderPhoto`'s idempotency guard: `SELECT 1 FROM journey_entry_photos jep JOIN journey_photos gp ON gp.id=jep.journey_photo_id WHERE jep.entry_id=? AND gp.photo_id=?` (`gp.photo_id` is the `trek_photos` id, not the gallery row's own id). */
|
||||
async existsLink(entryId: number, trekPhotoId: number): Promise<boolean> {
|
||||
const row = await this.kysely<Pick<JourneyPhotoJoinKyselyDB, 'journey_entry_photos' | 'journey_photos'>>()
|
||||
.selectFrom('journey_entry_photos as jep')
|
||||
.innerJoin('journey_photos as gp', 'gp.id', 'jep.journey_photo_id')
|
||||
.select('jep.entry_id')
|
||||
.where('jep.entry_id', '=', entryId)
|
||||
.where('gp.photo_id', '=', trekPhotoId)
|
||||
.executeTakeFirst();
|
||||
return !!row;
|
||||
}
|
||||
|
||||
/**
|
||||
* JG112 — `updatePhoto`'s sort-order write: `UPDATE journey_entry_photos
|
||||
* SET sort_order = ? WHERE journey_photo_id = ?`. **The gotcha the file's
|
||||
* own doc comment calls out**: `sort_order` for the entry↔photo junction
|
||||
* lives on THIS table, not `journey_photos` (`JourneyPhotosRepository`'s
|
||||
* `sort_order` column is a different value, the gallery's own display
|
||||
* order) — `updatePhoto`'s caption write (JG111) and this one target two
|
||||
* DIFFERENT tables for two DIFFERENT columns both named "sort order".
|
||||
* Unscoped by `entry_id` on purpose (matching the legacy statement
|
||||
* exactly): a gallery photo linked to more than one entry has every link
|
||||
* row's `sort_order` updated together.
|
||||
*/
|
||||
async updateSortOrder(journeyPhotoId: number, sortOrder: number): Promise<void> {
|
||||
await this.nativeUpdate({ journeyPhoto: journeyPhotoId }, { sort_order: sortOrder });
|
||||
}
|
||||
|
||||
/** JG104 — `unlinkPhotoFromEntry`: `DELETE FROM journey_entry_photos WHERE entry_id=? AND journey_photo_id=?`. Returns the affected-row count — the SERVICE branches on `changes > 0`, unchanged. */
|
||||
async deleteLink(entryId: number, journeyPhotoId: number): Promise<number> {
|
||||
return await this.nativeDelete({ entry: entryId, journeyPhoto: journeyPhotoId });
|
||||
}
|
||||
|
||||
/**
|
||||
* JG69 — `journeyStats`'s "a photograph per stop" source: the entry's own
|
||||
* linked photos, earliest first, videos excluded (a video poster inside a
|
||||
* four-millimetre map marker "is not a photograph" per the doc comment).
|
||||
* The SERVICE keeps only the FIRST row per `entryId` (a `Map` that only
|
||||
* sets on first-seen, relying on this method's own ordering) — this
|
||||
* method returns every row, unchanged.
|
||||
*/
|
||||
async listFirstPhotoPerEntry(journeyId: number): Promise<{ entryId: number; photoId: number }[]> {
|
||||
const rows = await this.kysely<JourneyPhotoJoinKyselyDB>()
|
||||
.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(['jep.entry_id as entryId', 'gp.photo_id as photoId'])
|
||||
.where('gp.journey_id', '=', journeyId)
|
||||
.where((eb) => eb.or([eb('tp.media_type', 'is', null), eb('tp.media_type', '=', 'image')]))
|
||||
.orderBy('jep.entry_id', 'asc')
|
||||
.orderBy('jep.sort_order', 'asc')
|
||||
.orderBy('gp.sort_order', 'asc')
|
||||
.orderBy('gp.id', 'asc')
|
||||
.execute();
|
||||
return rows as { entryId: number; photoId: number }[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,285 @@
|
||||
import type { Platform } from '@mikro-orm/core';
|
||||
import type { ExpressionBuilder } from 'kysely';
|
||||
import type { GalleryPhoto } from '../../types';
|
||||
import type { JourneyPhotos } from '../entities/JourneyPhotos.entity';
|
||||
import { concatKysely, unixEpochToIsoKysely } from '../dialect/sql-functions';
|
||||
import { type AssertRowKeys } from './_shared/rows';
|
||||
import { TrekRepository } from './_shared/trek-repository';
|
||||
|
||||
export class JourneyPhotosRepository extends TrekRepository<JourneyPhotos> {}
|
||||
/** `SELECT * FROM journey_photos WHERE id = ?`'s row (JG105) — every scalar column of the entity. */
|
||||
export interface JourneyPhotoFullRow {
|
||||
id: number;
|
||||
journey_id: number;
|
||||
photo_id: number;
|
||||
caption: string | null;
|
||||
shared: number | null;
|
||||
sort_order: number | null;
|
||||
provider: string | null;
|
||||
asset_id: string | null;
|
||||
owner_id: number | null;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
const _journeyPhotoFullRowKeys: AssertRowKeys<JourneyPhotoFullRow, JourneyPhotos> = true;
|
||||
|
||||
/** The narrow `journey_photos`/`trek_photos`/`journey_entry_photos`/`journey_entries` shape the gallery reads (JG19/JG101/JG102, `galleryChronologicalOrderExpr`) need. */
|
||||
interface GalleryKyselyDB {
|
||||
journey_photos: {
|
||||
id: number;
|
||||
journey_id: number;
|
||||
photo_id: number;
|
||||
caption: string | null;
|
||||
shared: number | null;
|
||||
sort_order: number | null;
|
||||
created_at: 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;
|
||||
};
|
||||
journey_entry_photos: { entry_id: number; journey_photo_id: number };
|
||||
journey_entries: { id: number; entry_date: string; entry_time: string | null };
|
||||
}
|
||||
|
||||
/** `GALLERY_SELECT`'s exact column list (`journey-domain.service.ts`'s module const), aliased `gp`/`tp` — shared by every gallery-shaped read below. */
|
||||
const GALLERY_COLUMNS = [
|
||||
'gp.id', 'gp.journey_id', 'gp.photo_id', 'gp.caption', 'gp.shared', 'gp.sort_order', 'gp.created_at',
|
||||
'tp.provider', 'tp.asset_id', 'tp.owner_id', 'tp.file_path', 'tp.thumbnail_path', 'tp.width', 'tp.height',
|
||||
'tp.media_type', 'tp.duration_ms', 'tp.taken_at', 'tp.lat', 'tp.lng',
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* `GALLERY_CHRONOLOGICAL_ORDER` (R1), the Kysely rebuild Task 0 proved
|
||||
* row-order-identical to the legacy SQL text against a seeded gallery
|
||||
* (SQLF-070, `sql-functions.test.ts`) — pasted verbatim from Task 0's report
|
||||
* (renaming its `GalleryOrderTestDB` to this file's own `GalleryKyselyDB`,
|
||||
* a structural superset that still satisfies the fixed `gp`/`tp` alias
|
||||
* contract). NOT exported: Task 0 deliberately did not add a shared,
|
||||
* exported builder ("re-verify it typechecks against your own repository's
|
||||
* DB interface" — a naive `ExpressionBuilder<DB, 'journey_photos' |
|
||||
* 'trek_photos'>` signature compiles under vitest's SWC transform but fails
|
||||
* `tsc`, the exact trap this fixed-alias intersection typing avoids). Task
|
||||
* 3 builds its own copy for JS15 (`getPublicJourney`'s gallery read) in
|
||||
* `JourneyShareTokens.repository.ts`, tailored to THAT file's own DB
|
||||
* interface, not a second caller of this one.
|
||||
*/
|
||||
function galleryChronologicalOrderExpr(
|
||||
platform: Platform,
|
||||
eb: ExpressionBuilder<GalleryKyselyDB & { gp: GalleryKyselyDB['journey_photos']; tp: GalleryKyselyDB['trek_photos'] }, 'gp' | 'tp'>,
|
||||
) {
|
||||
return eb.fn.coalesce(
|
||||
eb.fn<string | null>('nullif', [eb.ref('tp.taken_at'), eb.val('')]),
|
||||
eb
|
||||
.selectFrom('journey_entry_photos as jep')
|
||||
.innerJoin('journey_entries as je', 'je.id', 'jep.entry_id')
|
||||
.select((eb2) =>
|
||||
eb2.fn
|
||||
.min<string | null>(
|
||||
concatKysely(
|
||||
platform,
|
||||
eb2,
|
||||
{ column: 'je.entry_date' },
|
||||
{ value: 'T' },
|
||||
{ expression: eb2.fn.coalesce(eb2.fn<string | null>('nullif', [eb2.ref('je.entry_time'), eb2.val('')]), eb2.val('00:00')) },
|
||||
),
|
||||
)
|
||||
.as('min_dt'),
|
||||
)
|
||||
.whereRef('jep.journey_photo_id', '=', 'gp.id'),
|
||||
unixEpochToIsoKysely(platform, eb, 'gp.created_at'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* `journey_photos` — the gallery table (Plan 3g Task 2, Part B). A plain
|
||||
* single-column `id` PK with a `uniques: [{ properties: ['journey', 'photo'] }]`
|
||||
* constraint — R3's composite-PK upsert ruling does NOT apply here (that
|
||||
* ruling covers `JourneyTrips`/`JourneyEntryPhotos`/`JourneyContributors`
|
||||
* only, all genuine composite PKs; this table's `INSERT OR IGNORE` conflict
|
||||
* target is a UNIQUE index instead, the same `onConflictFields` shape
|
||||
* `TripMembers.repository.ts`'s own `UNIQUE(trip_id, user_id)` precedent
|
||||
* uses over a plain `id` PK).
|
||||
*/
|
||||
export class JourneyPhotosRepository extends TrekRepository<JourneyPhotos> {
|
||||
/** JG68 — `journeyStats`'s photo count: `SELECT COUNT(*) AS n FROM journey_photos WHERE journey_id = ?`. */
|
||||
async countForJourney(journeyId: number): Promise<number> {
|
||||
return await this.count({ journey: journeyId });
|
||||
}
|
||||
|
||||
/** 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<number | null> {
|
||||
const row = await this.kysely<GalleryKyselyDB>()
|
||||
.selectFrom('journey_photos')
|
||||
.select((eb) => eb.fn.max<number | null>('sort_order').as('m'))
|
||||
.where('journey_id', '=', journeyId)
|
||||
.executeTakeFirst();
|
||||
return row?.m ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* JG89/JG100 — the two `INSERT OR IGNORE INTO journey_photos` column-set
|
||||
* variants (`ensureInGallery`'s 6-column form with `caption`,
|
||||
* `uploadGalleryPhotos`'s 5-column form without it — caller passes
|
||||
* `caption: null` for that one) collapse into ONE method: `INSERT OR
|
||||
* IGNORE` never touches an existing row (unlike `INSERT OR REPLACE`'s
|
||||
* delete+reinsert), so there is no R3 reset-column trap here the way
|
||||
* `JourneyContributorsRepository.upsertContributor` has for
|
||||
* `hide_skeletons` — checked against this table's own DDL, confirmed no
|
||||
* such risk (Task 1's report, deviation 5).
|
||||
*/
|
||||
async insertIgnore(data: {
|
||||
journey_id: number;
|
||||
photo_id: number;
|
||||
caption: string | null;
|
||||
shared: number;
|
||||
sort_order: number;
|
||||
created_at: number;
|
||||
}): Promise<void> {
|
||||
await this.upsert(
|
||||
{
|
||||
journey: data.journey_id,
|
||||
photo: data.photo_id,
|
||||
caption: data.caption,
|
||||
shared: data.shared,
|
||||
sort_order: data.sort_order,
|
||||
created_at: data.created_at,
|
||||
},
|
||||
{ onConflictFields: ['journey', 'photo'], onConflictAction: 'ignore' },
|
||||
);
|
||||
}
|
||||
|
||||
/** JG90 — `ensureInGallery`'s post-insert id resolution: `SELECT id FROM journey_photos WHERE journey_id = ? AND photo_id = ?`. */
|
||||
async findIdByJourneyAndPhoto(journeyId: number, photoId: number): Promise<number | undefined> {
|
||||
const row = await this.qb('gp')
|
||||
.select(['gp.id'])
|
||||
.where({ journey: journeyId, photo: photoId })
|
||||
.execute<{ id: number } | undefined>('get', false);
|
||||
return row?.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* JG98/JG110 — `linkPhotoToEntry`'s and `updatePhoto`'s scope-check read:
|
||||
* `SELECT id, journey_id FROM journey_photos WHERE id = ?`, one statement
|
||||
* text. Selects the RELATION property (`gp.journey`), not the
|
||||
* `persist(false)` shadow column name (`gp.journey_id`) directly — the
|
||||
* QueryBuilder only resolves a shadow FK column through its owning
|
||||
* relation property (§7's own trap: "read through the relation, not the
|
||||
* shadow column"); selecting the shadow name itself either throws ("No
|
||||
* fields selected") or silently drops the key from the row, both caught
|
||||
* live by this file's own test run.
|
||||
*/
|
||||
async findScopeById(id: number): Promise<{ id: number; journey_id: number } | undefined> {
|
||||
return await this.qb('gp')
|
||||
.select(['gp.id', 'gp.journey'])
|
||||
.where({ id })
|
||||
.execute<{ id: number; journey_id: number } | undefined>('get', false);
|
||||
}
|
||||
|
||||
/** JG114 — `deletePhoto`'s wider scope-check read: `SELECT id, journey_id, photo_id FROM journey_photos WHERE id = ?` — a DIFFERENT column set from {@link findScopeById} (JG98/JG110), not a dup. Same relation-property-not-shadow-column rule as {@link findScopeById}. */
|
||||
async findScopeWithPhotoId(id: number): Promise<{ id: number; journey_id: number; photo_id: number } | undefined> {
|
||||
return await this.qb('gp')
|
||||
.select(['gp.id', 'gp.journey', 'gp.photo'])
|
||||
.where({ id })
|
||||
.execute<{ id: number; journey_id: number; photo_id: number } | undefined>('get', false);
|
||||
}
|
||||
|
||||
/** JG105 — `deleteGalleryPhoto`'s pre-delete read: `SELECT * FROM journey_photos WHERE id = ?`. */
|
||||
async findFull(id: number): Promise<JourneyPhotoFullRow | undefined> {
|
||||
return await this.qb('gp')
|
||||
.select(['gp.*'])
|
||||
.where({ id })
|
||||
.execute<JourneyPhotoFullRow | undefined>('get', false);
|
||||
}
|
||||
|
||||
/** JG108 — `setPhotoProvider`'s `trek_photos.id` resolution: `SELECT photo_id FROM journey_photos WHERE id = ?`. Relation property (`gp.photo`), not the shadow column — see {@link findScopeById}'s docstring. */
|
||||
async findPhotoIdById(id: number): Promise<number | undefined> {
|
||||
const row = await this.qb('gp')
|
||||
.select(['gp.photo'])
|
||||
.where({ id })
|
||||
.execute<{ photo_id: number } | undefined>('get', false);
|
||||
return row?.photo_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* JG109 — `setPhotoProvider`'s denormalized-cache write. §7: `provider`/
|
||||
* `asset_id`/`owner_id` are a DELIBERATE cache of the same columns on
|
||||
* `trek_photos` ("also denorm on gallery row for fast reads"), preserved
|
||||
* exactly — never "fixed" into reading through the `photo` relation.
|
||||
*/
|
||||
async updateProvider(id: number, provider: string, assetId: string, ownerId: number): Promise<void> {
|
||||
await this.nativeUpdate({ id }, { provider, asset_id: assetId, owner_id: ownerId });
|
||||
}
|
||||
|
||||
/** JG111 — `updatePhoto`'s caption write: `UPDATE journey_photos SET caption = ? WHERE id = ?`. */
|
||||
async updateCaption(id: number, caption: string): Promise<void> {
|
||||
await this.nativeUpdate({ id }, { caption });
|
||||
}
|
||||
|
||||
/**
|
||||
* JG107/JG116 — `deleteGalleryPhoto`'s and `deletePhoto`'s hard delete,
|
||||
* one statement text (`deletePhoto`'s own comment: "backwards-compat name
|
||||
* used by old route"). Cascades `journey_entry_photos` via the entity's
|
||||
* `deleteRule('cascade')`.
|
||||
*/
|
||||
async deleteById(id: number): Promise<void> {
|
||||
await this.nativeDelete({ id });
|
||||
}
|
||||
|
||||
/** JG101 — `uploadGalleryPhotos`'s post-insert gallery-shaped read: `SELECT {GALLERY_SELECT} FROM {GALLERY_JOIN} WHERE gp.journey_id = ? AND gp.photo_id = ?`. */
|
||||
async galleryReadByJourneyAndPhoto(journeyId: number, photoId: number): Promise<GalleryPhoto | undefined> {
|
||||
const row = await this.kysely<GalleryKyselyDB>()
|
||||
.selectFrom('journey_photos as gp')
|
||||
.innerJoin('trek_photos as tp', 'tp.id', 'gp.photo_id')
|
||||
.select(GALLERY_COLUMNS)
|
||||
.where('gp.journey_id', '=', journeyId)
|
||||
.where('gp.photo_id', '=', photoId)
|
||||
.executeTakeFirst();
|
||||
return row as GalleryPhoto | undefined;
|
||||
}
|
||||
|
||||
/** JG102 — `addProviderPhotoToGallery`'s post-insert gallery-shaped read by gallery row id: `SELECT {GALLERY_SELECT} FROM {GALLERY_JOIN} WHERE gp.id = ?`. */
|
||||
async galleryReadOne(id: number): Promise<GalleryPhoto | undefined> {
|
||||
const row = await this.kysely<GalleryKyselyDB>()
|
||||
.selectFrom('journey_photos as gp')
|
||||
.innerJoin('trek_photos as tp', 'tp.id', 'gp.photo_id')
|
||||
.select(GALLERY_COLUMNS)
|
||||
.where('gp.id', '=', id)
|
||||
.executeTakeFirst();
|
||||
return row as GalleryPhoto | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* JG19 — `getJourneyFull`'s gallery read, folding in `GALLERY_CHRONOLOGICAL_ORDER`
|
||||
* (R1). Task 1's report left this raw for this task to finish (its own
|
||||
* named file set did not include this repository); this is the FIRST real
|
||||
* consumer of Task 0's Kysely rebuild in the codebase. Unbounded (no
|
||||
* `LIMIT`/`OFFSET` — Task 0 confirmed neither consuming site paginates),
|
||||
* so the RS20 pagination rule does not independently force this ruling —
|
||||
* R1 holds anyway (a full-table-read-then-sort-in-JS is exactly what RS20
|
||||
* exists to prevent in general).
|
||||
*/
|
||||
async galleryRead(journeyId: number): Promise<GalleryPhoto[]> {
|
||||
const platform = this.getEntityManager().getPlatform();
|
||||
const rows = await this.kysely<GalleryKyselyDB>()
|
||||
.selectFrom('journey_photos as gp')
|
||||
.innerJoin('trek_photos as tp', 'tp.id', 'gp.photo_id')
|
||||
.select(GALLERY_COLUMNS)
|
||||
.where('gp.journey_id', '=', journeyId)
|
||||
.orderBy((eb) => galleryChronologicalOrderExpr(platform, eb), 'asc')
|
||||
.orderBy('gp.sort_order', 'asc')
|
||||
.orderBy('gp.id', 'asc')
|
||||
.execute();
|
||||
return rows as GalleryPhoto[];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,6 +234,21 @@ export class PlacesRepository extends TrekRepository<Places> {
|
||||
.execute<PlaceRow | undefined>('get', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan 3g Task 2 (JG44) — `journey-domain.service.ts#onPlaceUpdated`'s
|
||||
* `SELECT * FROM places WHERE id = ?`, unscoped (no `trip_id` — the sync
|
||||
* engine already resolved the place from its own `journey_entries.source_place_id`
|
||||
* column, so the id alone is trusted here the same way `TripsRepository
|
||||
* .findRaw` trusts a bare trip id). ONE append to this file for JG44 per
|
||||
* the 3g plan's task split — every other method here belongs to Plan 3c.
|
||||
*/
|
||||
async findRaw(id: number): Promise<PlaceRow | undefined> {
|
||||
return this.qb('p')
|
||||
.select(['p.*'])
|
||||
.where('p.id = ?', [id])
|
||||
.execute<PlaceRow | undefined>('get', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* PL17/PL20 — `SELECT google_place_id, image_url FROM places WHERE id = ?
|
||||
* AND trip_id = ?`, the reclaim-candidate projection `remove`/`removeMany`
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ import {
|
||||
import { createTestBudgetItemsRepo } from './files-repos';
|
||||
import {
|
||||
createTestJourneysRepo, createTestJourneyContributorsRepo, createTestJourneyTripsRepo, createTestJourneyEntriesRepo,
|
||||
createTestJourneyPhotosRepo, createTestJourneyEntryPhotosRepo,
|
||||
} from './journey-repos';
|
||||
|
||||
/**
|
||||
@@ -55,6 +56,8 @@ export async function accommodationsOver(dbs: DatabaseService): Promise<Accommod
|
||||
dbs, realtime, new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection),
|
||||
await createTestJourneysRepo(dbs.connection), await createTestJourneyContributorsRepo(dbs.connection),
|
||||
await createTestJourneyTripsRepo(dbs.connection), await createTestJourneyEntriesRepo(dbs.connection), await createTestTripsRepo(dbs.connection),
|
||||
// Plan 3g Task 2 constructor-ripple: JourneyPhotosRepository/JourneyEntryPhotosRepository/PlacesRepository.
|
||||
await createTestJourneyPhotosRepo(dbs.connection), await createTestJourneyEntryPhotosRepo(dbs.connection), await createTestPlacesRepo(dbs.connection),
|
||||
),
|
||||
await createTestUnitOfWork(dbs.connection),
|
||||
await createTestDayAssignmentsRepo(dbs.connection),
|
||||
|
||||
@@ -8,6 +8,10 @@ import { JourneyTrips } from '../../src/db/entities/JourneyTrips.entity';
|
||||
import type { JourneyTripsRepository } from '../../src/db/repositories/JourneyTrips.repository';
|
||||
import { JourneyEntries } from '../../src/db/entities/JourneyEntries.entity';
|
||||
import type { JourneyEntriesRepository } from '../../src/db/repositories/JourneyEntries.repository';
|
||||
import { JourneyPhotos } from '../../src/db/entities/JourneyPhotos.entity';
|
||||
import type { JourneyPhotosRepository } from '../../src/db/repositories/JourneyPhotos.repository';
|
||||
import { JourneyEntryPhotos } from '../../src/db/entities/JourneyEntryPhotos.entity';
|
||||
import type { JourneyEntryPhotosRepository } from '../../src/db/repositories/JourneyEntryPhotos.repository';
|
||||
|
||||
/**
|
||||
* Plan 3g Task 1 (`JourneyDomainService` Part A) test-only repository
|
||||
@@ -42,3 +46,17 @@ export function createTestJourneyTripsRepo(db: Database.Database): Promise<Journ
|
||||
export function createTestJourneyEntriesRepo(db: Database.Database): Promise<JourneyEntriesRepository> {
|
||||
return sharedTestOrm(db).then((t) => t.repo(JourneyEntries));
|
||||
}
|
||||
|
||||
/**
|
||||
* Plan 3g Task 2 (`JourneyDomainService` Part B) additions — the two
|
||||
* repositories Task 1 left as empty stubs for this task to populate
|
||||
* (`JourneyPhotos.repository.ts`/`JourneyEntryPhotos.repository.ts`), same
|
||||
* `sharedTestOrm` binding as the four factories above.
|
||||
*/
|
||||
export function createTestJourneyPhotosRepo(db: Database.Database): Promise<JourneyPhotosRepository> {
|
||||
return sharedTestOrm(db).then((t) => t.repo(JourneyPhotos));
|
||||
}
|
||||
|
||||
export function createTestJourneyEntryPhotosRepo(db: Database.Database): Promise<JourneyEntryPhotosRepository> {
|
||||
return sharedTestOrm(db).then((t) => t.repo(JourneyEntryPhotos));
|
||||
}
|
||||
|
||||
@@ -64,6 +64,7 @@ import {
|
||||
} from '../../helpers/test-uow';
|
||||
import {
|
||||
createTestJourneysRepo, createTestJourneyContributorsRepo, createTestJourneyTripsRepo, createTestJourneyEntriesRepo,
|
||||
createTestJourneyPhotosRepo, createTestJourneyEntryPhotosRepo,
|
||||
} from '../../helpers/journey-repos';
|
||||
|
||||
let svc: AssignmentsService;
|
||||
@@ -86,6 +87,8 @@ beforeAll(async () => {
|
||||
dbs, realtime, new TrekPhotoRegistrationService((await sharedTestOrm(dbs.connection)).repo(TrekPhotos), (await sharedTestOrm(dbs.connection)).repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection),
|
||||
await createTestJourneysRepo(dbs.connection), await createTestJourneyContributorsRepo(dbs.connection),
|
||||
await createTestJourneyTripsRepo(dbs.connection), await createTestJourneyEntriesRepo(dbs.connection), await createTestTripsRepo(dbs.connection),
|
||||
// Plan 3g Task 2 constructor-ripple: JourneyPhotosRepository/JourneyEntryPhotosRepository/PlacesRepository.
|
||||
await createTestJourneyPhotosRepo(dbs.connection), await createTestJourneyEntryPhotosRepo(dbs.connection), await createTestPlacesRepo(dbs.connection),
|
||||
),
|
||||
await createTestUnitOfWork(dbs.connection),
|
||||
await createTestDayAssignmentsRepo(dbs.connection),
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Unit tests for JourneyDomainService (JOURNEY-SVC-001 through JOURNEY-SVC-038).
|
||||
* Uses a real in-memory SQLite DB so SQL logic is exercised faithfully.
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterAll } from 'vitest';
|
||||
import { describe, it, expect, vi, beforeAll, beforeEach, afterEach, afterAll } from 'vitest';
|
||||
|
||||
// -- DB setup -----------------------------------------------------------------
|
||||
|
||||
@@ -55,9 +55,10 @@ import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { db as dbConn } from '../../../src/db/database';
|
||||
import { createTestUnitOfWork, sharedTestOrm, createTestTripsRepo } from '../../helpers/test-uow';
|
||||
import { createTestUnitOfWork, sharedTestOrm, createTestTripsRepo, createTestPlacesRepo } from '../../helpers/test-uow';
|
||||
import {
|
||||
createTestJourneysRepo, createTestJourneyContributorsRepo, createTestJourneyTripsRepo, createTestJourneyEntriesRepo,
|
||||
createTestJourneyPhotosRepo, createTestJourneyEntryPhotosRepo,
|
||||
} from '../../helpers/journey-repos';
|
||||
|
||||
let dbs: DatabaseService;
|
||||
@@ -70,6 +71,12 @@ let contributorsRepoDirect: Awaited<ReturnType<typeof createTestJourneyContribut
|
||||
let journeyTripsRepoDirect: Awaited<ReturnType<typeof createTestJourneyTripsRepo>>;
|
||||
let entriesRepoDirect: Awaited<ReturnType<typeof createTestJourneyEntriesRepo>>;
|
||||
let tripsRepoDirect: Awaited<ReturnType<typeof createTestTripsRepo>>;
|
||||
// Plan 3g Task 2's own additions — the two repositories Task 1 left as
|
||||
// empty stubs, plus `PlacesRepository` (JG44's `findRaw`), held here for
|
||||
// the same reason as the five above.
|
||||
let photosRepoDirect: Awaited<ReturnType<typeof createTestJourneyPhotosRepo>>;
|
||||
let entryPhotosRepoDirect: Awaited<ReturnType<typeof createTestJourneyEntryPhotosRepo>>;
|
||||
let placesRepoDirect: Awaited<ReturnType<typeof createTestPlacesRepo>>;
|
||||
|
||||
// Plan 3g Task 1 (Part A, R9's construction/wrapping pattern): the SAME
|
||||
// direct-construction shape every converted service in this program uses for
|
||||
@@ -95,9 +102,17 @@ beforeAll(async () => {
|
||||
journeyTripsRepoDirect = await createTestJourneyTripsRepo(testDb);
|
||||
entriesRepoDirect = await createTestJourneyEntriesRepo(testDb);
|
||||
tripsRepoDirect = await createTestTripsRepo(testDb);
|
||||
photosRepoDirect = await createTestJourneyPhotosRepo(testDb);
|
||||
entryPhotosRepoDirect = await createTestJourneyEntryPhotosRepo(testDb);
|
||||
placesRepoDirect = await createTestPlacesRepo(testDb);
|
||||
svc = new JourneyDomainService(
|
||||
dbs, new RealtimeService(), new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs), await createTestUnitOfWork(testDb),
|
||||
journeysRepoDirect, contributorsRepoDirect, journeyTripsRepoDirect, entriesRepoDirect, tripsRepoDirect,
|
||||
// Plan 3g Task 2's own append to this SAME construction call — the two
|
||||
// repositories this task builds, plus `PlacesRepository` (JG44) — per
|
||||
// R9, Task 2 only appends to what Task 1's construction already
|
||||
// established, never re-touches the wrapping mechanism itself.
|
||||
photosRepoDirect, entryPhotosRepoDirect, placesRepoDirect,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3070,3 +3085,105 @@ describe('Plan 3g Task 1 — reconcileTripSkeletons: all three branches in one c
|
||||
expect(entries.some((e) => e.source_place_id === keepPlace.id)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Plan 3g Task 2 — the four `uow.transactional` blocks (JG-TX1..4) and
|
||||
// JG112's mutation proof. Appended per R9 (Task 1 owns the construction/
|
||||
// wrapping pattern above; this only adds new coverage).
|
||||
describe('Plan 3g Task 2 — transaction rollback proofs (JG-TX1..4)', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('JG-TX1 (addPhoto): a failure after ensureInGallery\'s insert rolls the insert back — no orphan journey_photos row', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const journey = await svc.createJourney(user.id, { title: 'J' });
|
||||
const entry = await svc.createEntry(journey.id, user.id, { entry_date: '2026-01-01' });
|
||||
|
||||
vi.spyOn(photosRepoDirect, 'findIdByJourneyAndPhoto').mockRejectedValueOnce(new Error('boom'));
|
||||
|
||||
await expect(svc.addPhoto(entry!.id, user.id, 'journey/tx1.jpg')).rejects.toThrow('boom');
|
||||
|
||||
const rows = testDb.prepare('SELECT * FROM journey_photos WHERE journey_id = ?').all(journey.id);
|
||||
expect(rows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('JG-TX2 (addProviderPhoto): a failure after ensureInGallery\'s insert rolls the insert back — no orphan journey_photos row', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const journey = await svc.createJourney(user.id, { title: 'J' });
|
||||
const entry = await svc.createEntry(journey.id, user.id, { entry_date: '2026-01-01' });
|
||||
|
||||
vi.spyOn(photosRepoDirect, 'findIdByJourneyAndPhoto').mockRejectedValueOnce(new Error('boom'));
|
||||
|
||||
await expect(svc.addProviderPhoto(entry!.id, user.id, 'immich', 'asset-tx2')).rejects.toThrow('boom');
|
||||
|
||||
const rows = testDb.prepare('SELECT * FROM journey_photos WHERE journey_id = ?').all(journey.id);
|
||||
expect(rows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('JG-TX3 (addProviderPhotoToGallery): a failure after ensureInGallery\'s insert rolls the insert back — no orphan journey_photos row', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const journey = await svc.createJourney(user.id, { title: 'J' });
|
||||
|
||||
vi.spyOn(photosRepoDirect, 'findIdByJourneyAndPhoto').mockRejectedValueOnce(new Error('boom'));
|
||||
|
||||
await expect(svc.addProviderPhotoToGallery(journey.id, user.id, 'immich', 'asset-tx3')).rejects.toThrow('boom');
|
||||
|
||||
const rows = testDb.prepare('SELECT * FROM journey_photos WHERE journey_id = ?').all(journey.id);
|
||||
expect(rows).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('JG-TX4 (reorderEntries): a failure touching the journey after the sort_order loop rolls EVERY sort_order write back, not just the last one', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const journey = await svc.createJourney(user.id, { title: 'J' });
|
||||
const e1 = createJourneyEntry(testDb, journey.id, user.id, { entry_date: '2026-01-01' });
|
||||
const e2 = createJourneyEntry(testDb, journey.id, user.id, { entry_date: '2026-01-01' });
|
||||
const e3 = createJourneyEntry(testDb, journey.id, user.id, { entry_date: '2026-01-01' });
|
||||
testDb.prepare('UPDATE journey_entries SET sort_order = 0 WHERE id IN (?, ?, ?)').run(e1.id, e2.id, e3.id);
|
||||
|
||||
vi.spyOn(journeysRepoDirect, 'updateFields').mockRejectedValueOnce(new Error('boom'));
|
||||
|
||||
await expect(svc.reorderEntries(journey.id, user.id, [e3.id, e1.id, e2.id])).rejects.toThrow('boom');
|
||||
|
||||
// None of the three sort_order writes the loop made before the journey
|
||||
// touch failed should have survived the rollback — every entry is still
|
||||
// at its pre-call sort_order (0), not just the last one in the loop.
|
||||
const rows = testDb.prepare('SELECT id, sort_order FROM journey_entries WHERE journey_id = ? ORDER BY id').all(journey.id) as {
|
||||
id: number;
|
||||
sort_order: number;
|
||||
}[];
|
||||
for (const r of rows) expect(r.sort_order).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Plan 3g Task 2 — JG112 mutation proof (sort_order table targeting)', () => {
|
||||
it('updatePhoto\'s sort_order write lands on journey_entry_photos (entry-scoped), never journey_photos (the gallery row\'s own, unrelated sort_order)', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const journey = await svc.createJourney(user.id, { title: 'J' });
|
||||
const entry = await svc.createEntry(journey.id, user.id, { entry_date: '2026-01-01' });
|
||||
const [photo] = await svc.uploadGalleryPhotos(journey.id, user.id, [{ path: 'journey/jg112.jpg' }]);
|
||||
await svc.linkPhotoToEntry(entry!.id, photo.id, user.id);
|
||||
|
||||
const galleryBefore = (
|
||||
testDb.prepare('SELECT sort_order FROM journey_photos WHERE id = ?').get(photo.id) as { sort_order: number }
|
||||
).sort_order;
|
||||
|
||||
const updated = await svc.updatePhoto(photo.id, user.id, { sort_order: 7 });
|
||||
|
||||
// The junction row (this entry's own view of the photo's position) moved.
|
||||
const junctionRow = testDb
|
||||
.prepare('SELECT sort_order FROM journey_entry_photos WHERE journey_photo_id = ?')
|
||||
.get(photo.id) as { sort_order: number };
|
||||
expect(junctionRow.sort_order).toBe(7);
|
||||
expect(updated!.sort_order).toBe(7);
|
||||
|
||||
// A copy-paste bug swapping JG111/JG112's target tables would leave the
|
||||
// junction row untouched and instead bump `journey_photos.sort_order`
|
||||
// (the gallery's OWN, unrelated ordering column, same name, different
|
||||
// table) to 7 — asserting it is STILL the pre-call value is what a
|
||||
// table-swap regression would fail.
|
||||
const galleryAfter = (
|
||||
testDb.prepare('SELECT sort_order FROM journey_photos WHERE id = ?').get(photo.id) as { sort_order: number }
|
||||
).sort_order;
|
||||
expect(galleryAfter).toBe(galleryBefore);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -68,9 +68,10 @@ import { TrekPhotos } from '../../../src/db/entities/TrekPhotos.entity';
|
||||
import { TripPhotos } from '../../../src/db/entities/TripPhotos.entity';
|
||||
import { JourneyDomainService } from '../../../src/nest/journey/journey-domain.service';
|
||||
import { db as dbConn } from '../../../src/db/database';
|
||||
import { createTestUnitOfWork, sharedTestOrm, createTestTripsRepo } from '../../helpers/test-uow';
|
||||
import { createTestUnitOfWork, sharedTestOrm, createTestTripsRepo, createTestPlacesRepo } from '../../helpers/test-uow';
|
||||
import {
|
||||
createTestJourneysRepo, createTestJourneyContributorsRepo, createTestJourneyTripsRepo, createTestJourneyEntriesRepo,
|
||||
createTestJourneyPhotosRepo, createTestJourneyEntryPhotosRepo,
|
||||
} from '../../helpers/journey-repos';
|
||||
|
||||
const dbs = new DatabaseService(dbConn);
|
||||
@@ -99,6 +100,8 @@ beforeAll(async () => {
|
||||
dbs, new RealtimeService(), new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs), await createTestUnitOfWork(testDb),
|
||||
await createTestJourneysRepo(testDb), await createTestJourneyContributorsRepo(testDb),
|
||||
await createTestJourneyTripsRepo(testDb), await createTestJourneyEntriesRepo(testDb), await createTestTripsRepo(testDb),
|
||||
// Plan 3g Task 2 constructor-ripple: JourneyPhotosRepository/JourneyEntryPhotosRepository/PlacesRepository.
|
||||
await createTestJourneyPhotosRepo(testDb), await createTestJourneyEntryPhotosRepo(testDb), await createTestPlacesRepo(testDb),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ import { createTestUnitOfWork, createTestAppSettingsRepo, createTestUsersRepo, c
|
||||
import { createTestBudgetItemsRepo } from '../../helpers/files-repos';
|
||||
import {
|
||||
createTestJourneysRepo, createTestJourneyContributorsRepo, createTestJourneyTripsRepo, createTestJourneyEntriesRepo,
|
||||
createTestJourneyPhotosRepo, createTestJourneyEntryPhotosRepo,
|
||||
} from '../../helpers/journey-repos';
|
||||
|
||||
const dbs = new DatabaseService(testDb);
|
||||
@@ -99,6 +100,8 @@ async function svc(searchNominatim: MapsService['searchNominatim']): Promise<Pla
|
||||
dbs, new RealtimeService(), new TrekPhotoRegistrationService((await sharedTestOrm(dbs.connection)).repo(TrekPhotos), (await sharedTestOrm(dbs.connection)).repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection),
|
||||
await createTestJourneysRepo(dbs.connection), await createTestJourneyContributorsRepo(dbs.connection),
|
||||
await createTestJourneyTripsRepo(dbs.connection), await createTestJourneyEntriesRepo(dbs.connection), await createTestTripsRepo(dbs.connection),
|
||||
// Plan 3g Task 2 constructor-ripple: JourneyPhotosRepository/JourneyEntryPhotosRepository/PlacesRepository.
|
||||
await createTestJourneyPhotosRepo(dbs.connection), await createTestJourneyEntryPhotosRepo(dbs.connection), await createTestPlacesRepo(dbs.connection),
|
||||
),
|
||||
storageFx.storage,
|
||||
await accommodationsOver(dbs), await createTestUnitOfWork(dbs.connection),
|
||||
|
||||
@@ -83,6 +83,7 @@ import { createTestUnitOfWork, createTestAppSettingsRepo, createTestUsersRepo, c
|
||||
import { createTestBudgetItemsRepo } from '../../helpers/files-repos';
|
||||
import {
|
||||
createTestJourneysRepo, createTestJourneyContributorsRepo, createTestJourneyTripsRepo, createTestJourneyEntriesRepo,
|
||||
createTestJourneyPhotosRepo, createTestJourneyEntryPhotosRepo,
|
||||
} from '../../helpers/journey-repos';
|
||||
import type { AppSettingsRepository } from '../../../src/db/repositories/AppSettings.repository';
|
||||
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
|
||||
@@ -141,6 +142,8 @@ async function makePlacesService(
|
||||
dbs, new RealtimeService(), new TrekPhotoRegistrationService((await sharedTestOrm(dbs.connection)).repo(TrekPhotos), (await sharedTestOrm(dbs.connection)).repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection),
|
||||
await createTestJourneysRepo(dbs.connection), await createTestJourneyContributorsRepo(dbs.connection),
|
||||
await createTestJourneyTripsRepo(dbs.connection), await createTestJourneyEntriesRepo(dbs.connection), await createTestTripsRepo(dbs.connection),
|
||||
// Plan 3g Task 2 constructor-ripple: JourneyPhotosRepository/JourneyEntryPhotosRepository/PlacesRepository.
|
||||
await createTestJourneyPhotosRepo(dbs.connection), await createTestJourneyEntryPhotosRepo(dbs.connection), await createTestPlacesRepo(dbs.connection),
|
||||
),
|
||||
placesStorageFx.storage,
|
||||
await accommodationsOver(dbs), await createTestUnitOfWork(dbs.connection),
|
||||
|
||||
@@ -106,6 +106,7 @@ import {
|
||||
import { createTestTodoItemsRepo, createTestTodoCategoryAssigneesRepo } from '../../helpers/todo-repos';
|
||||
import {
|
||||
createTestJourneysRepo, createTestJourneyContributorsRepo, createTestJourneyTripsRepo, createTestJourneyEntriesRepo,
|
||||
createTestJourneyPhotosRepo, createTestJourneyEntryPhotosRepo,
|
||||
} from '../../helpers/journey-repos';
|
||||
|
||||
// Real sibling services over the same in-memory DB — the aggregation runs the
|
||||
@@ -157,6 +158,8 @@ beforeAll(async () => {
|
||||
dbs(), new RealtimeService(), new TrekPhotoRegistrationService((await sharedTestOrm(testDb)).repo(TrekPhotos), (await sharedTestOrm(testDb)).repo(TripPhotos), dbs()), await createTestUnitOfWork(dbs().connection),
|
||||
await createTestJourneysRepo(dbs().connection), await createTestJourneyContributorsRepo(dbs().connection),
|
||||
await createTestJourneyTripsRepo(dbs().connection), await createTestJourneyEntriesRepo(dbs().connection), await createTestTripsRepo(dbs().connection),
|
||||
// Plan 3g Task 2 constructor-ripple: JourneyPhotosRepository/JourneyEntryPhotosRepository/PlacesRepository.
|
||||
await createTestJourneyPhotosRepo(dbs().connection), await createTestJourneyEntryPhotosRepo(dbs().connection), await createTestPlacesRepo(dbs().connection),
|
||||
),
|
||||
makeStorageFixture('').storage,
|
||||
await accommodationsOver(dbs()), await createTestUnitOfWork(dbs().connection),
|
||||
|
||||
@@ -121,6 +121,7 @@ import {
|
||||
import { createTestVacayHolidayCalendarsRepo, createTestSchoolHolidayRegionsRepo } from '../../helpers/school-holidays-repos';
|
||||
import {
|
||||
createTestJourneysRepo, createTestJourneyContributorsRepo, createTestJourneyTripsRepo, createTestJourneyEntriesRepo,
|
||||
createTestJourneyPhotosRepo, createTestJourneyEntryPhotosRepo,
|
||||
} from '../../helpers/journey-repos';
|
||||
|
||||
// Real sibling services over the same in-memory DB — updateTrip's date-shift
|
||||
@@ -200,6 +201,8 @@ beforeAll(async () => {
|
||||
dbs(), new RealtimeService(), new TrekPhotoRegistrationService(dbsEm!.getRepository(TrekPhotos), dbsEm!.getRepository(TripPhotos), dbs()), await createTestUnitOfWork(dbs().connection),
|
||||
await createTestJourneysRepo(dbs().connection), await createTestJourneyContributorsRepo(dbs().connection),
|
||||
await createTestJourneyTripsRepo(dbs().connection), await createTestJourneyEntriesRepo(dbs().connection), await createTestTripsRepo(dbs().connection),
|
||||
// Plan 3g Task 2 constructor-ripple: JourneyPhotosRepository/JourneyEntryPhotosRepository/PlacesRepository.
|
||||
await createTestJourneyPhotosRepo(dbs().connection), await createTestJourneyEntryPhotosRepo(dbs().connection), await createTestPlacesRepo(dbs().connection),
|
||||
),
|
||||
makeStorageFixture('').storage,
|
||||
await accommodationsOver(dbs()), await createTestUnitOfWork(dbs().connection),
|
||||
|
||||
@@ -74,6 +74,7 @@ import {
|
||||
import { createTestBudgetItemsRepo } from '../../helpers/files-repos';
|
||||
import {
|
||||
createTestJourneysRepo, createTestJourneyContributorsRepo, createTestJourneyTripsRepo, createTestJourneyEntriesRepo,
|
||||
createTestJourneyPhotosRepo, createTestJourneyEntryPhotosRepo,
|
||||
} from '../../helpers/journey-repos';
|
||||
import {
|
||||
createTestPackingItemsRepo,
|
||||
@@ -140,6 +141,8 @@ beforeAll(async () => {
|
||||
dbs, realtime, new TrekPhotoRegistrationService(t.repo(TrekPhotos), t.repo(TripPhotos), dbs), await createTestUnitOfWork(dbs.connection),
|
||||
await createTestJourneysRepo(dbs.connection), await createTestJourneyContributorsRepo(dbs.connection),
|
||||
await createTestJourneyTripsRepo(dbs.connection), await createTestJourneyEntriesRepo(dbs.connection), await createTestTripsRepo(dbs.connection),
|
||||
// Plan 3g Task 2 constructor-ripple: JourneyPhotosRepository/JourneyEntryPhotosRepository/PlacesRepository.
|
||||
await createTestJourneyPhotosRepo(dbs.connection), await createTestJourneyEntryPhotosRepo(dbs.connection), await createTestPlacesRepo(dbs.connection),
|
||||
),
|
||||
makeStorageFixture('').storage,
|
||||
await accommodationsOver(dbs), await createTestUnitOfWork(dbs.connection),
|
||||
|
||||
Reference in New Issue
Block a user