mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 07:26:30 +00:00
feat(db): snake_case entity shape and the first three repositories (Days, DayNotes, Trips)
This commit is contained in:
@@ -1,33 +1,37 @@
|
||||
import { type Ref, defineEntity, p, EntityRepository } from '@mikro-orm/core';
|
||||
import { type Ref, defineEntity, p } from '@mikro-orm/core';
|
||||
import { DayNotesRepository } from '../repositories/DayNotes.repository';
|
||||
import { DbTimestampType } from '../types';
|
||||
import { Days } from './Days.entity';
|
||||
import { Trips } from './Trips.entity';
|
||||
|
||||
export class DayNotes {
|
||||
id?: number | null;
|
||||
day!: Ref<Days>;
|
||||
day_id!: number;
|
||||
trip!: Ref<Trips>;
|
||||
trip_id!: number;
|
||||
text!: string;
|
||||
time?: string | null;
|
||||
icon?: string | null = '📝';
|
||||
sortOrder?: unknown | null;
|
||||
createdAt?: Date | null;
|
||||
sort_order?: number | null;
|
||||
created_at?: string | null;
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
export class DayNotesRepository extends EntityRepository<DayNotes> {}
|
||||
|
||||
export const DayNotesSchema = defineEntity({
|
||||
class: DayNotes,
|
||||
repository: () => DayNotesRepository,
|
||||
properties: {
|
||||
id: p.integer().primary().autoincrement(),
|
||||
day: () => p.manyToOne(Days).ref().deleteRule('cascade').index('idx_day_notes_day_id'),
|
||||
trip: () => p.manyToOne(Trips).ref().deleteRule('cascade'),
|
||||
day: () => p.manyToOne(Days).ref().hidden().deleteRule('cascade').index('idx_day_notes_day_id'),
|
||||
day_id: p.integer().persist(false),
|
||||
trip: () => p.manyToOne(Trips).ref().hidden().deleteRule('cascade'),
|
||||
trip_id: p.integer().persist(false),
|
||||
text: p.text(),
|
||||
time: p.text().nullable(),
|
||||
icon: p.text().nullable(),
|
||||
sortOrder: p.double().nullable().default(0),
|
||||
createdAt: p.datetime().nullable().onCreate(() => new Date()),
|
||||
sort_order: p.double().nullable().default(0),
|
||||
created_at: p.type(DbTimestampType).nullable().defaultRaw('CURRENT_TIMESTAMP'),
|
||||
color: p.text().nullable(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Collection, type Ref, defineEntity, p, EntityRepository } from '@mikro-orm/core';
|
||||
import { Collection, type Ref, defineEntity, p } from '@mikro-orm/core';
|
||||
import { DaysRepository } from '../repositories/Days.repository';
|
||||
import { DayAccommodations } from './DayAccommodations.entity';
|
||||
import { DayAssignments } from './DayAssignments.entity';
|
||||
import { DayNotes } from './DayNotes.entity';
|
||||
@@ -12,45 +13,45 @@ import { Trips } from './Trips.entity';
|
||||
export class Days {
|
||||
id?: number | null;
|
||||
trip!: Ref<Trips>;
|
||||
dayNumber!: number;
|
||||
trip_id!: number;
|
||||
day_number!: number;
|
||||
date?: string | null;
|
||||
notes?: string | null;
|
||||
title?: string | null;
|
||||
defaultTransportMode?: string | null;
|
||||
dayAccommodationsCollection = new Collection<DayAccommodations>(this);
|
||||
dayAccommodationsCollection1 = new Collection<DayAccommodations>(this);
|
||||
dayAssignmentsCollection = new Collection<DayAssignments>(this);
|
||||
dayNotesCollection = new Collection<DayNotes>(this);
|
||||
photosCollection = new Collection<Photos>(this);
|
||||
reservationDayPositionsCollection = new Collection<ReservationDayPositions>(this);
|
||||
reservationsCollection = new Collection<Reservations>(this);
|
||||
reservationsCollection1 = new Collection<Reservations>(this);
|
||||
roadtripDayTracks: Ref<RoadtripDayTracks> | null = null;
|
||||
roadtripViasCollection = new Collection<RoadtripVias>(this);
|
||||
default_transport_mode?: string | null;
|
||||
day_accommodations_collection = new Collection<DayAccommodations>(this);
|
||||
day_accommodations_collection1 = new Collection<DayAccommodations>(this);
|
||||
day_assignments_collection = new Collection<DayAssignments>(this);
|
||||
day_notes_collection = new Collection<DayNotes>(this);
|
||||
photos_collection = new Collection<Photos>(this);
|
||||
reservation_day_positions_collection = new Collection<ReservationDayPositions>(this);
|
||||
reservations_collection = new Collection<Reservations>(this);
|
||||
reservations_collection1 = new Collection<Reservations>(this);
|
||||
roadtrip_day_tracks: Ref<RoadtripDayTracks> | null = null;
|
||||
roadtrip_vias_collection = new Collection<RoadtripVias>(this);
|
||||
}
|
||||
|
||||
export class DaysRepository extends EntityRepository<Days> {}
|
||||
|
||||
export const DaysSchema = defineEntity({
|
||||
class: Days,
|
||||
repository: () => DaysRepository,
|
||||
properties: {
|
||||
id: p.integer().primary().autoincrement(),
|
||||
trip: () => p.manyToOne(Trips).ref().deleteRule('cascade').index('idx_days_trip_id'),
|
||||
dayNumber: p.integer(),
|
||||
trip: () => p.manyToOne(Trips).ref().hidden().deleteRule('cascade').index('idx_days_trip_id'),
|
||||
trip_id: p.integer().persist(false),
|
||||
day_number: p.integer(),
|
||||
date: p.text().nullable(),
|
||||
notes: p.text().nullable(),
|
||||
title: p.text().nullable(),
|
||||
defaultTransportMode: p.text().nullable(),
|
||||
dayAccommodationsCollection: () => p.oneToMany(DayAccommodations).mappedBy('startDay'),
|
||||
dayAccommodationsCollection1: () => p.oneToMany(DayAccommodations).mappedBy('endDay'),
|
||||
dayAssignmentsCollection: () => p.oneToMany(DayAssignments).mappedBy('day'),
|
||||
dayNotesCollection: () => p.oneToMany(DayNotes).mappedBy('day'),
|
||||
photosCollection: () => p.oneToMany(Photos).mappedBy('day'),
|
||||
reservationDayPositionsCollection: () => p.oneToMany(ReservationDayPositions).mappedBy('day'),
|
||||
reservationsCollection: () => p.oneToMany(Reservations).mappedBy('day'),
|
||||
reservationsCollection1: () => p.oneToMany(Reservations).mappedBy('endDay'),
|
||||
roadtripDayTracks: () => p.oneToOne(RoadtripDayTracks).ref().mappedBy('day'),
|
||||
roadtripViasCollection: () => p.oneToMany(RoadtripVias).mappedBy('day'),
|
||||
default_transport_mode: p.text().nullable(),
|
||||
day_accommodations_collection: () => p.oneToMany(DayAccommodations).hidden().mappedBy('startDay'),
|
||||
day_accommodations_collection1: () => p.oneToMany(DayAccommodations).hidden().mappedBy('endDay'),
|
||||
day_assignments_collection: () => p.oneToMany(DayAssignments).hidden().mappedBy('day'),
|
||||
day_notes_collection: () => p.oneToMany(DayNotes).hidden().mappedBy('day'),
|
||||
photos_collection: () => p.oneToMany(Photos).hidden().mappedBy('day'),
|
||||
reservation_day_positions_collection: () => p.oneToMany(ReservationDayPositions).hidden().mappedBy('day'),
|
||||
reservations_collection: () => p.oneToMany(Reservations).hidden().mappedBy('day'),
|
||||
reservations_collection1: () => p.oneToMany(Reservations).hidden().mappedBy('endDay'),
|
||||
roadtrip_day_tracks: () => p.oneToOne(RoadtripDayTracks).ref().hidden().mappedBy('day'),
|
||||
roadtrip_vias_collection: () => p.oneToMany(RoadtripVias).hidden().mappedBy('day'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Collection, type Ref, defineEntity, p, EntityRepository } from '@mikro-orm/core';
|
||||
import { Collection, type Ref, defineEntity, p } from '@mikro-orm/core';
|
||||
import { TripsRepository } from '../repositories/Trips.repository';
|
||||
import { DbTimestampType } from '../types';
|
||||
import { BudgetCategoryOrder } from './BudgetCategoryOrder.entity';
|
||||
import { BudgetItems } from './BudgetItems.entity';
|
||||
import { BudgetSettlements } from './BudgetSettlements.entity';
|
||||
@@ -34,51 +36,50 @@ import { Users } from './Users.entity';
|
||||
export class Trips {
|
||||
id?: number | null;
|
||||
user!: Ref<Users>;
|
||||
user_id!: number;
|
||||
title!: string;
|
||||
description?: string | null;
|
||||
startDate?: string | null;
|
||||
endDate?: string | null;
|
||||
start_date?: string | null;
|
||||
end_date?: string | null;
|
||||
currency?: string | null = 'EUR';
|
||||
coverImage?: string | null;
|
||||
isArchived?: number | null = 0;
|
||||
reminderDays?: number | null = 3;
|
||||
feedToken?: string | null;
|
||||
createdAt?: Date | null;
|
||||
updatedAt?: Date | null;
|
||||
budgetCategoryOrderCollection = new Collection<BudgetCategoryOrder>(this);
|
||||
budgetItemsCollection = new Collection<BudgetItems>(this);
|
||||
budgetSettlementsCollection = new Collection<BudgetSettlements>(this);
|
||||
collabLinksCollection = new Collection<CollabLinks>(this);
|
||||
collabMessagesCollection = new Collection<CollabMessages>(this);
|
||||
collabNotesCollection = new Collection<CollabNotes>(this);
|
||||
collabPollsCollection = new Collection<CollabPolls>(this);
|
||||
dawarichVisitSuggestionsCollection = new Collection<DawarichVisitSuggestions>(this);
|
||||
dayAccommodationsCollection = new Collection<DayAccommodations>(this);
|
||||
dayNotesCollection = new Collection<DayNotes>(this);
|
||||
daysCollection = new Collection<Days>(this);
|
||||
inviteTokensCollection = new Collection<InviteTokens>(this);
|
||||
journeyEntriesCollection = new Collection<JourneyEntries>(this);
|
||||
journeyTripsCollection = new Collection<JourneyTrips>(this);
|
||||
packingBagsCollection = new Collection<PackingBags>(this);
|
||||
packingCategoryAssigneesCollection = new Collection<PackingCategoryAssignees>(this);
|
||||
packingItemsCollection = new Collection<PackingItems>(this);
|
||||
photosCollection = new Collection<Photos>(this);
|
||||
placesCollection = new Collection<Places>(this);
|
||||
reservationsCollection = new Collection<Reservations>(this);
|
||||
roadtripDayBoundariesCollection = new Collection<RoadtripDayBoundaries>(this);
|
||||
roadtripPreferencesCollection = new Collection<RoadtripPreferences>(this);
|
||||
shareTokensCollection = new Collection<ShareTokens>(this);
|
||||
todoCategoryAssigneesCollection = new Collection<TodoCategoryAssignees>(this);
|
||||
todoItemsCollection = new Collection<TodoItems>(this);
|
||||
tripAlbumLinksCollection = new Collection<TripAlbumLinks>(this);
|
||||
tripFilesCollection = new Collection<TripFiles>(this);
|
||||
tripInviteTokensCollection = new Collection<TripInviteTokens>(this);
|
||||
tripMembersCollection = new Collection<TripMembers>(this);
|
||||
tripPhotosCollection = new Collection<TripPhotos>(this);
|
||||
cover_image?: string | null;
|
||||
is_archived?: number | null = 0;
|
||||
reminder_days?: number | null = 3;
|
||||
feed_token?: string | null;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
budget_category_order_collection = new Collection<BudgetCategoryOrder>(this);
|
||||
budget_items_collection = new Collection<BudgetItems>(this);
|
||||
budget_settlements_collection = new Collection<BudgetSettlements>(this);
|
||||
collab_links_collection = new Collection<CollabLinks>(this);
|
||||
collab_messages_collection = new Collection<CollabMessages>(this);
|
||||
collab_notes_collection = new Collection<CollabNotes>(this);
|
||||
collab_polls_collection = new Collection<CollabPolls>(this);
|
||||
dawarich_visit_suggestions_collection = new Collection<DawarichVisitSuggestions>(this);
|
||||
day_accommodations_collection = new Collection<DayAccommodations>(this);
|
||||
day_notes_collection = new Collection<DayNotes>(this);
|
||||
days_collection = new Collection<Days>(this);
|
||||
invite_tokens_collection = new Collection<InviteTokens>(this);
|
||||
journey_entries_collection = new Collection<JourneyEntries>(this);
|
||||
journey_trips_collection = new Collection<JourneyTrips>(this);
|
||||
packing_bags_collection = new Collection<PackingBags>(this);
|
||||
packing_category_assignees_collection = new Collection<PackingCategoryAssignees>(this);
|
||||
packing_items_collection = new Collection<PackingItems>(this);
|
||||
photos_collection = new Collection<Photos>(this);
|
||||
places_collection = new Collection<Places>(this);
|
||||
reservations_collection = new Collection<Reservations>(this);
|
||||
roadtrip_day_boundaries_collection = new Collection<RoadtripDayBoundaries>(this);
|
||||
roadtrip_preferences_collection = new Collection<RoadtripPreferences>(this);
|
||||
share_tokens_collection = new Collection<ShareTokens>(this);
|
||||
todo_category_assignees_collection = new Collection<TodoCategoryAssignees>(this);
|
||||
todo_items_collection = new Collection<TodoItems>(this);
|
||||
trip_album_links_collection = new Collection<TripAlbumLinks>(this);
|
||||
trip_files_collection = new Collection<TripFiles>(this);
|
||||
trip_invite_tokens_collection = new Collection<TripInviteTokens>(this);
|
||||
trip_members_collection = new Collection<TripMembers>(this);
|
||||
trip_photos_collection = new Collection<TripPhotos>(this);
|
||||
}
|
||||
|
||||
export class TripsRepository extends EntityRepository<Trips> {}
|
||||
|
||||
export const TripsSchema = defineEntity({
|
||||
class: Trips,
|
||||
repository: () => TripsRepository,
|
||||
@@ -86,52 +87,53 @@ export const TripsSchema = defineEntity({
|
||||
{
|
||||
name: 'idx_trips_feed_token',
|
||||
where: 'feed_token IS NOT NULL',
|
||||
properties: ['feedToken'],
|
||||
properties: ['feed_token'],
|
||||
},
|
||||
],
|
||||
properties: {
|
||||
id: p.integer().primary().autoincrement(),
|
||||
user: () => p.manyToOne(Users).ref().deleteRule('cascade').index('idx_trips_user_id'),
|
||||
user: () => p.manyToOne(Users).ref().hidden().deleteRule('cascade').index('idx_trips_user_id'),
|
||||
user_id: p.integer().persist(false),
|
||||
title: p.text(),
|
||||
description: p.text().nullable(),
|
||||
startDate: p.text().nullable(),
|
||||
endDate: p.text().nullable(),
|
||||
start_date: p.text().nullable(),
|
||||
end_date: p.text().nullable(),
|
||||
currency: p.text().nullable(),
|
||||
coverImage: p.text().nullable(),
|
||||
isArchived: p.integer().nullable(),
|
||||
reminderDays: p.integer().nullable(),
|
||||
feedToken: p.text().nullable(),
|
||||
createdAt: p.datetime().nullable().onCreate(() => new Date()).index('idx_trips_created_at'),
|
||||
updatedAt: p.datetime().nullable().onCreate(() => new Date()),
|
||||
budgetCategoryOrderCollection: () => p.oneToMany(BudgetCategoryOrder).mappedBy('trip'),
|
||||
budgetItemsCollection: () => p.oneToMany(BudgetItems).mappedBy('trip'),
|
||||
budgetSettlementsCollection: () => p.oneToMany(BudgetSettlements).mappedBy('trip'),
|
||||
collabLinksCollection: () => p.oneToMany(CollabLinks).mappedBy('trip'),
|
||||
collabMessagesCollection: () => p.oneToMany(CollabMessages).mappedBy('trip'),
|
||||
collabNotesCollection: () => p.oneToMany(CollabNotes).mappedBy('trip'),
|
||||
collabPollsCollection: () => p.oneToMany(CollabPolls).mappedBy('trip'),
|
||||
dawarichVisitSuggestionsCollection: () => p.oneToMany(DawarichVisitSuggestions).mappedBy('trip'),
|
||||
dayAccommodationsCollection: () => p.oneToMany(DayAccommodations).mappedBy('trip'),
|
||||
dayNotesCollection: () => p.oneToMany(DayNotes).mappedBy('trip'),
|
||||
daysCollection: () => p.oneToMany(Days).mappedBy('trip'),
|
||||
inviteTokensCollection: () => p.oneToMany(InviteTokens).mappedBy('trip'),
|
||||
journeyEntriesCollection: () => p.oneToMany(JourneyEntries).mappedBy('sourceTrip'),
|
||||
journeyTripsCollection: () => p.oneToMany(JourneyTrips).mappedBy('trip'),
|
||||
packingBagsCollection: () => p.oneToMany(PackingBags).mappedBy('trip'),
|
||||
packingCategoryAssigneesCollection: () => p.oneToMany(PackingCategoryAssignees).mappedBy('trip'),
|
||||
packingItemsCollection: () => p.oneToMany(PackingItems).mappedBy('trip'),
|
||||
photosCollection: () => p.oneToMany(Photos).mappedBy('trip'),
|
||||
placesCollection: () => p.oneToMany(Places).mappedBy('trip'),
|
||||
reservationsCollection: () => p.oneToMany(Reservations).mappedBy('trip'),
|
||||
roadtripDayBoundariesCollection: () => p.oneToMany(RoadtripDayBoundaries).mappedBy('trip'),
|
||||
roadtripPreferencesCollection: () => p.oneToMany(RoadtripPreferences).mappedBy('trip'),
|
||||
shareTokensCollection: () => p.oneToMany(ShareTokens).mappedBy('trip'),
|
||||
todoCategoryAssigneesCollection: () => p.oneToMany(TodoCategoryAssignees).mappedBy('trip'),
|
||||
todoItemsCollection: () => p.oneToMany(TodoItems).mappedBy('trip'),
|
||||
tripAlbumLinksCollection: () => p.oneToMany(TripAlbumLinks).mappedBy('trip'),
|
||||
tripFilesCollection: () => p.oneToMany(TripFiles).mappedBy('trip'),
|
||||
tripInviteTokensCollection: () => p.oneToMany(TripInviteTokens).mappedBy('trip'),
|
||||
tripMembersCollection: () => p.oneToMany(TripMembers).mappedBy('trip'),
|
||||
tripPhotosCollection: () => p.oneToMany(TripPhotos).mappedBy('trip'),
|
||||
cover_image: p.text().nullable(),
|
||||
is_archived: p.integer().nullable(),
|
||||
reminder_days: p.integer().nullable(),
|
||||
feed_token: p.text().nullable(),
|
||||
created_at: p.type(DbTimestampType).nullable().index('idx_trips_created_at').defaultRaw('CURRENT_TIMESTAMP'),
|
||||
updated_at: p.type(DbTimestampType).nullable().defaultRaw('CURRENT_TIMESTAMP'),
|
||||
budget_category_order_collection: () => p.oneToMany(BudgetCategoryOrder).hidden().mappedBy('trip'),
|
||||
budget_items_collection: () => p.oneToMany(BudgetItems).hidden().mappedBy('trip'),
|
||||
budget_settlements_collection: () => p.oneToMany(BudgetSettlements).hidden().mappedBy('trip'),
|
||||
collab_links_collection: () => p.oneToMany(CollabLinks).hidden().mappedBy('trip'),
|
||||
collab_messages_collection: () => p.oneToMany(CollabMessages).hidden().mappedBy('trip'),
|
||||
collab_notes_collection: () => p.oneToMany(CollabNotes).hidden().mappedBy('trip'),
|
||||
collab_polls_collection: () => p.oneToMany(CollabPolls).hidden().mappedBy('trip'),
|
||||
dawarich_visit_suggestions_collection: () => p.oneToMany(DawarichVisitSuggestions).hidden().mappedBy('trip'),
|
||||
day_accommodations_collection: () => p.oneToMany(DayAccommodations).hidden().mappedBy('trip'),
|
||||
day_notes_collection: () => p.oneToMany(DayNotes).hidden().mappedBy('trip'),
|
||||
days_collection: () => p.oneToMany(Days).hidden().mappedBy('trip'),
|
||||
invite_tokens_collection: () => p.oneToMany(InviteTokens).hidden().mappedBy('trip'),
|
||||
journey_entries_collection: () => p.oneToMany(JourneyEntries).hidden().mappedBy('sourceTrip'),
|
||||
journey_trips_collection: () => p.oneToMany(JourneyTrips).hidden().mappedBy('trip'),
|
||||
packing_bags_collection: () => p.oneToMany(PackingBags).hidden().mappedBy('trip'),
|
||||
packing_category_assignees_collection: () => p.oneToMany(PackingCategoryAssignees).hidden().mappedBy('trip'),
|
||||
packing_items_collection: () => p.oneToMany(PackingItems).hidden().mappedBy('trip'),
|
||||
photos_collection: () => p.oneToMany(Photos).hidden().mappedBy('trip'),
|
||||
places_collection: () => p.oneToMany(Places).hidden().mappedBy('trip'),
|
||||
reservations_collection: () => p.oneToMany(Reservations).hidden().mappedBy('trip'),
|
||||
roadtrip_day_boundaries_collection: () => p.oneToMany(RoadtripDayBoundaries).hidden().mappedBy('trip'),
|
||||
roadtrip_preferences_collection: () => p.oneToMany(RoadtripPreferences).hidden().mappedBy('trip'),
|
||||
share_tokens_collection: () => p.oneToMany(ShareTokens).hidden().mappedBy('trip'),
|
||||
todo_category_assignees_collection: () => p.oneToMany(TodoCategoryAssignees).hidden().mappedBy('trip'),
|
||||
todo_items_collection: () => p.oneToMany(TodoItems).hidden().mappedBy('trip'),
|
||||
trip_album_links_collection: () => p.oneToMany(TripAlbumLinks).hidden().mappedBy('trip'),
|
||||
trip_files_collection: () => p.oneToMany(TripFiles).hidden().mappedBy('trip'),
|
||||
trip_invite_tokens_collection: () => p.oneToMany(TripInviteTokens).hidden().mappedBy('trip'),
|
||||
trip_members_collection: () => p.oneToMany(TripMembers).hidden().mappedBy('trip'),
|
||||
trip_photos_collection: () => p.oneToMany(TripPhotos).hidden().mappedBy('trip'),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import type { DayNotes } from '../entities/DayNotes.entity';
|
||||
import { toRow } from './_shared/rows';
|
||||
import { EntityRepository } from '@mikro-orm/sql';
|
||||
|
||||
export interface DayNoteRow {
|
||||
id: number;
|
||||
day_id: number;
|
||||
trip_id: number;
|
||||
text: string;
|
||||
time: string | null;
|
||||
icon: string | null;
|
||||
sort_order: number | null;
|
||||
created_at: string | null;
|
||||
color: string | null;
|
||||
}
|
||||
|
||||
export class DayNotesRepository extends EntityRepository<DayNotes> {
|
||||
/**
|
||||
* `INSERT INTO day_notes (day_id, trip_id, text, time, icon, sort_order, color)`
|
||||
* — `created_at` is left to `DEFAULT CURRENT_TIMESTAMP`, then the row is
|
||||
* re-read so the returned row is the stored one.
|
||||
*
|
||||
* `created_at` alone would not need the re-read: a `defaultRaw` column the
|
||||
* insert does not name is in its `returning` clause, so it is already the
|
||||
* stored text by the time `flush()` resolves. The re-read is there for the
|
||||
* columns that are not — a nullable column this insert never named would
|
||||
* otherwise be `undefined` on the entity and missing from the row.
|
||||
*/
|
||||
async createNote(input: {
|
||||
day_id: number;
|
||||
trip_id: number;
|
||||
text: string;
|
||||
time?: string | null;
|
||||
icon?: string | null;
|
||||
sort_order?: number | null;
|
||||
color?: string | null;
|
||||
}): Promise<DayNoteRow> {
|
||||
const note = this.create({
|
||||
day: input.day_id,
|
||||
day_id: input.day_id,
|
||||
trip: input.trip_id,
|
||||
trip_id: input.trip_id,
|
||||
text: input.text,
|
||||
time: input.time ?? null,
|
||||
icon: input.icon ?? '📝',
|
||||
sort_order: input.sort_order ?? 0,
|
||||
color: input.color ?? null,
|
||||
});
|
||||
await this.getEntityManager().persist(note).flush();
|
||||
await this.getEntityManager().refresh(note);
|
||||
return toRow(note) as DayNoteRow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import type { Days } from '../entities/Days.entity';
|
||||
import { toRow } from './_shared/rows';
|
||||
import { EntityRepository } from '@mikro-orm/sql';
|
||||
|
||||
/** A `days` row as the API emits it. */
|
||||
export interface DayRow {
|
||||
id: number;
|
||||
trip_id: number;
|
||||
day_number: number;
|
||||
date: string | null;
|
||||
notes: string | null;
|
||||
title: string | null;
|
||||
default_transport_mode: string | null;
|
||||
}
|
||||
|
||||
export class DaysRepository extends EntityRepository<Days> {
|
||||
/** `SELECT * FROM days WHERE trip_id = ? ORDER BY day_number ASC` */
|
||||
async listByTrip(trip_id: number): Promise<DayRow[]> {
|
||||
const days = await this.find({ trip: trip_id }, { orderBy: { day_number: 'asc' } });
|
||||
return days.map((d) => toRow(d) as DayRow);
|
||||
}
|
||||
|
||||
/** `SELECT * FROM days WHERE id = ? AND trip_id = ?` */
|
||||
async findInTrip(id: number, trip_id: number): Promise<DayRow | undefined> {
|
||||
const day = await this.findOne({ id, trip: trip_id });
|
||||
return day ? (toRow(day) as DayRow) : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* The trip's highest `day_number`, 0 when it has no days.
|
||||
*
|
||||
* The legacy statement was `SELECT MAX(day_number) ... WHERE trip_id = ?`;
|
||||
* this reads the same value as the first row of a descending order, which
|
||||
* needs no SQL function spelled outside `db/dialect/sql-functions.ts` and no
|
||||
* entity hydrated into the identity map.
|
||||
*/
|
||||
async maxDayNumber(trip_id: number): Promise<number> {
|
||||
const row = await this.qb('d')
|
||||
.select('d.day_number')
|
||||
.where({ trip: trip_id })
|
||||
.orderBy({ day_number: 'desc' })
|
||||
.limit(1)
|
||||
.execute<{ day_number: number | null } | undefined>('get', false);
|
||||
return row?.day_number ?? 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* `INSERT INTO days (trip_id, day_number, date, notes) VALUES (?, ?, ?, ?)`
|
||||
* followed by the legacy re-select, so the caller gets the stored row (with
|
||||
* every column default applied) and not what it asked to store.
|
||||
*
|
||||
* The re-select is not optional: the insert's `returning` clause carries only
|
||||
* the generated and `defaultRaw` columns, so a column this insert never named
|
||||
* (`title`, `default_transport_mode`) would still be `undefined` on the
|
||||
* entity and would simply be missing from the row.
|
||||
*/
|
||||
async createDay(input: {
|
||||
trip_id: number;
|
||||
day_number: number;
|
||||
date?: string | null;
|
||||
notes?: string | null;
|
||||
}): Promise<DayRow> {
|
||||
const day = this.create({
|
||||
trip: input.trip_id,
|
||||
trip_id: input.trip_id,
|
||||
day_number: input.day_number,
|
||||
date: input.date ?? null,
|
||||
notes: input.notes ?? null,
|
||||
});
|
||||
await this.getEntityManager().persist(day).flush();
|
||||
await this.getEntityManager().refresh(day);
|
||||
return toRow(day) as DayRow;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { Trips } from '../entities/Trips.entity';
|
||||
import { EntityRepository } from '@mikro-orm/sql';
|
||||
|
||||
/** What a trip-scoped request learns about the trip once access is verified. */
|
||||
export interface TripAccess {
|
||||
id: number;
|
||||
user_id: number;
|
||||
currency: string | null;
|
||||
}
|
||||
|
||||
export class TripsRepository extends EntityRepository<Trips> {
|
||||
/**
|
||||
* The trip if the user owns it or is a member of it, else undefined.
|
||||
*
|
||||
* Byte-for-byte the legacy `canAccessTrip` statement:
|
||||
* SELECT t.id, t.user_id, t.currency FROM trips t
|
||||
* LEFT JOIN trip_members m ON m.trip_id = t.id AND m.user_id = ?
|
||||
* WHERE t.id = ? AND (t.user_id = ? OR m.user_id IS NOT NULL)
|
||||
*/
|
||||
async findAccessible(trip_id: number, user_id: number): Promise<TripAccess | undefined> {
|
||||
const row = await this.qb('t')
|
||||
.select(['t.id', 't.user', 't.currency'])
|
||||
.leftJoin('t.trip_members_collection', 'm', { 'm.user': user_id })
|
||||
.where({ 't.id': trip_id })
|
||||
.andWhere({ $or: [{ 't.user': user_id }, { 'm.user': { $ne: null } }] })
|
||||
// `mapResults: false` leaves the driver's row alone: the keys are the
|
||||
// column names, which is why the result type is spelled out here rather
|
||||
// than inferred from the entity's properties.
|
||||
.execute<{ id: number; user_id: number; currency: string | null } | undefined>('get', false);
|
||||
if (!row) return undefined;
|
||||
return { id: row.id, user_id: row.user_id, currency: row.currency ?? null };
|
||||
}
|
||||
|
||||
/** `SELECT id FROM trips WHERE id = ? AND user_id = ?` */
|
||||
async isOwner(trip_id: number, user_id: number): Promise<boolean> {
|
||||
return (await this.count({ id: trip_id, user: user_id })) > 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { wrap, type EntityDTO } from '@mikro-orm/core';
|
||||
|
||||
/**
|
||||
* The plain row a service (and, through it, the API) sees.
|
||||
*
|
||||
* `toObject()` drops the ORM machinery — `Ref` wrappers, hidden relations,
|
||||
* collections that were never loaded — and leaves the column-named scalars,
|
||||
* which is exactly the `SELECT *` row the legacy code returned. Repositories
|
||||
* return this, never a live entity, so a service cannot lazy-load or mutate
|
||||
* state behind the repository's back.
|
||||
*/
|
||||
export function toRow<T extends object>(entity: T): EntityDTO<T> {
|
||||
return wrap(entity).toObject() as EntityDTO<T>;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createTestDb, resetTestDb } from '../../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
|
||||
import { createDay, createTrip, createUser } from '../../../helpers/factories';
|
||||
import { DayNotes } from '../../../../src/db/entities/DayNotes.entity';
|
||||
import type { DayNotesRepository } from '../../../../src/db/repositories/DayNotes.repository';
|
||||
import { DB_TIMESTAMP_RE } from '../../../../src/db/types';
|
||||
|
||||
const testDb = createTestDb();
|
||||
let t: TestOrm;
|
||||
let notes: DayNotesRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await createTestOrm(testDb);
|
||||
notes = t.repo(DayNotes) as DayNotesRepository;
|
||||
});
|
||||
beforeEach(() => { resetTestDb(testDb); t.clear(); });
|
||||
afterAll(async () => { await t.close(); testDb.close(); });
|
||||
|
||||
describe('DayNotesRepository timestamps', () => {
|
||||
it('NOTEREPO-001: an ORM insert leaves created_at to the column default, as text', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day = createDay(testDb, trip.id);
|
||||
const row = await notes.createNote({ day_id: day.id, trip_id: trip.id, text: 'Lunch' });
|
||||
const stored = testDb.prepare('SELECT created_at, typeof(created_at) AS kind FROM day_notes WHERE id = ?').get(row.id) as { created_at: string; kind: string };
|
||||
expect(stored.kind).toBe('text');
|
||||
expect(stored.created_at).toMatch(DB_TIMESTAMP_RE);
|
||||
expect(row.created_at).toBe(stored.created_at);
|
||||
});
|
||||
|
||||
it('NOTEREPO-002: the returned row is the SELECT * row and the defaults are the legacy ones', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day = createDay(testDb, trip.id);
|
||||
const row = await notes.createNote({ day_id: day.id, trip_id: trip.id, text: 'Lunch' });
|
||||
expect(row).toEqual(testDb.prepare('SELECT * FROM day_notes WHERE id = ?').get(row.id));
|
||||
expect(row.icon).toBe('📝');
|
||||
expect(row.sort_order).toBe(0);
|
||||
});
|
||||
|
||||
it('NOTEREPO-003: date() still works on a row the ORM wrote', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day = createDay(testDb, trip.id);
|
||||
const row = await notes.createNote({ day_id: day.id, trip_id: trip.id, text: 'Lunch' });
|
||||
const d = testDb.prepare('SELECT date(created_at) AS d FROM day_notes WHERE id = ?').get(row.id) as { d: string };
|
||||
expect(d.d).toMatch(/^\d{4}-\d{2}-\d{2}$/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,75 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { createTestDb, resetTestDb } from '../../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
|
||||
import { createDay, createTrip, createUser } from '../../../helpers/factories';
|
||||
import { Days } from '../../../../src/db/entities/Days.entity';
|
||||
import type { DaysRepository } from '../../../../src/db/repositories/Days.repository';
|
||||
|
||||
const testDb = createTestDb();
|
||||
let t: TestOrm;
|
||||
let days: DaysRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await createTestOrm(testDb);
|
||||
days = t.repo(Days) as DaysRepository;
|
||||
});
|
||||
beforeEach(() => { resetTestDb(testDb); t.clear(); });
|
||||
afterAll(async () => { await t.close(); testDb.close(); });
|
||||
|
||||
function rawDay(id: number): unknown {
|
||||
return testDb.prepare('SELECT * FROM days WHERE id = ?').get(id);
|
||||
}
|
||||
|
||||
describe('DaysRepository', () => {
|
||||
it('DAYREPO-001: a row read through the ORM is the SELECT * row, key for key', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const day = createDay(testDb, trip.id, { date: '2026-07-01', title: 'Arrival' });
|
||||
const [row] = await days.listByTrip(trip.id);
|
||||
expect(row).toEqual(rawDay(day.id));
|
||||
});
|
||||
|
||||
it('DAYREPO-002: the scalar FK hydrates without loading the trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
createDay(testDb, trip.id);
|
||||
const entity = await t.repo(Days).findOne({ trip: trip.id });
|
||||
expect(entity?.trip_id).toBe(trip.id);
|
||||
expect(entity?.trip.isInitialized()).toBe(false);
|
||||
});
|
||||
|
||||
it('DAYREPO-003: listByTrip orders by day_number', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
createDay(testDb, trip.id, { day_number: 2 });
|
||||
createDay(testDb, trip.id, { day_number: 1 });
|
||||
const rows = await days.listByTrip(trip.id);
|
||||
expect(rows.map((r) => r.day_number)).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
it('DAYREPO-004: findInTrip refuses a day from another trip', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const a = createTrip(testDb, user.id);
|
||||
const b = createTrip(testDb, user.id);
|
||||
const day = createDay(testDb, a.id);
|
||||
expect(await days.findInTrip(day.id, b.id)).toBeUndefined();
|
||||
expect((await days.findInTrip(day.id, a.id))?.id).toBe(day.id);
|
||||
});
|
||||
|
||||
it('DAYREPO-005: maxDayNumber is 0 for an empty trip and the max otherwise', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
expect(await days.maxDayNumber(trip.id)).toBe(0);
|
||||
createDay(testDb, trip.id, { day_number: 3 });
|
||||
expect(await days.maxDayNumber(trip.id)).toBe(3);
|
||||
});
|
||||
|
||||
it('DAYREPO-006: createDay writes the legacy column set and returns the re-read row', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const row = await days.createDay({ trip_id: trip.id, day_number: 1, date: '2026-07-01' });
|
||||
expect(row).toEqual(rawDay(row.id));
|
||||
expect(row.notes).toBeNull();
|
||||
expect(row.trip_id).toBe(trip.id);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
import { CAN_ACCESS_TRIP_SQL, createTestDb, resetTestDb } from '../../../helpers/test-db';
|
||||
import { createTestOrm, type TestOrm } from '../../../helpers/test-orm';
|
||||
import { addTripMember, createTrip, createUser } from '../../../helpers/factories';
|
||||
import { Trips } from '../../../../src/db/entities/Trips.entity';
|
||||
import type { TripsRepository } from '../../../../src/db/repositories/Trips.repository';
|
||||
|
||||
const testDb = createTestDb();
|
||||
let t: TestOrm;
|
||||
let trips: TripsRepository;
|
||||
|
||||
beforeAll(async () => {
|
||||
t = await createTestOrm(testDb);
|
||||
trips = t.repo(Trips) as TripsRepository;
|
||||
});
|
||||
beforeEach(() => { resetTestDb(testDb); t.clear(); });
|
||||
afterAll(async () => { await t.close(); testDb.close(); });
|
||||
|
||||
const legacy = (tripId: number, userId: number) => testDb.prepare(CAN_ACCESS_TRIP_SQL).get(userId, tripId, userId);
|
||||
|
||||
describe('TripsRepository.findAccessible — parity with canAccessTrip', () => {
|
||||
it('TRIPREPO-001: the owner sees the trip, with the same three columns', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
expect(await trips.findAccessible(trip.id, user.id)).toEqual(legacy(trip.id, user.id));
|
||||
expect(await trips.findAccessible(trip.id, user.id)).toEqual({ id: trip.id, user_id: user.id, currency: 'EUR' });
|
||||
});
|
||||
|
||||
it('TRIPREPO-002: a member sees it, a stranger does not', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const { user: member } = createUser(testDb);
|
||||
const { user: stranger } = createUser(testDb);
|
||||
const trip = createTrip(testDb, owner.id);
|
||||
addTripMember(testDb, trip.id, member.id);
|
||||
expect(await trips.findAccessible(trip.id, member.id)).toEqual(legacy(trip.id, member.id));
|
||||
expect(await trips.findAccessible(trip.id, stranger.id)).toBeUndefined();
|
||||
expect(legacy(trip.id, stranger.id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('TRIPREPO-003: a member of another trip is still a stranger here', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const { user: member } = createUser(testDb);
|
||||
const a = createTrip(testDb, owner.id);
|
||||
const b = createTrip(testDb, owner.id);
|
||||
addTripMember(testDb, a.id, member.id);
|
||||
expect(await trips.findAccessible(b.id, member.id)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('TRIPREPO-004: a null currency comes back null, not undefined', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
testDb.prepare('UPDATE trips SET currency = NULL WHERE id = ?').run(trip.id);
|
||||
expect(await trips.findAccessible(trip.id, user.id)).toEqual({ id: trip.id, user_id: user.id, currency: null });
|
||||
});
|
||||
|
||||
it('TRIPREPO-005: isOwner is true for the owner only', async () => {
|
||||
const { user: owner } = createUser(testDb);
|
||||
const { user: member } = createUser(testDb);
|
||||
const trip = createTrip(testDb, owner.id);
|
||||
addTripMember(testDb, trip.id, member.id);
|
||||
expect(await trips.isOwner(trip.id, owner.id)).toBe(true);
|
||||
expect(await trips.isOwner(trip.id, member.id)).toBe(false);
|
||||
});
|
||||
|
||||
it('TRIPREPO-006: toObject on a trip omits the hidden relation and every unloaded collection', async () => {
|
||||
const { user } = createUser(testDb);
|
||||
const trip = createTrip(testDb, user.id);
|
||||
const entity = await t.repo(Trips).findOneOrFail({ id: trip.id });
|
||||
const raw = testDb.prepare('SELECT * FROM trips WHERE id = ?').get(trip.id) as Record<string, unknown>;
|
||||
const { wrap } = await import('@mikro-orm/core');
|
||||
expect(wrap(entity).toObject()).toEqual(raw);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user