From 9ca9ca96442ef0204772419b9987f1695501a999 Mon Sep 17 00:00:00 2001 From: jubnl Date: Mon, 21 Sep 2026 08:50:56 +0200 Subject: [PATCH] feat(db): discover entities from an explicit list instead of a glob --- server/src/db/entities/index.ts | 250 ++++++++++++++++++++ server/src/mikro-orm.config.ts | 5 +- server/tests/unit/db/entities-index.test.ts | 23 ++ 3 files changed, 276 insertions(+), 2 deletions(-) create mode 100644 server/src/db/entities/index.ts create mode 100644 server/tests/unit/db/entities-index.test.ts diff --git a/server/src/db/entities/index.ts b/server/src/db/entities/index.ts new file mode 100644 index 000000000..8381179cc --- /dev/null +++ b/server/src/db/entities/index.ts @@ -0,0 +1,250 @@ +import type { EntitySchema } from '@mikro-orm/core'; +import { AddonsSchema } from './Addons.entity'; +import { AppSettingsSchema } from './AppSettings.entity'; +import { AssignmentParticipantsSchema } from './AssignmentParticipants.entity'; +import { AuditLogSchema } from './AuditLog.entity'; +import { BucketListSchema } from './BucketList.entity'; +import { BudgetCategoryOrderSchema } from './BudgetCategoryOrder.entity'; +import { BudgetItemMembersSchema } from './BudgetItemMembers.entity'; +import { BudgetItemPayersSchema } from './BudgetItemPayers.entity'; +import { BudgetItemsSchema } from './BudgetItems.entity'; +import { BudgetSettlementsSchema } from './BudgetSettlements.entity'; +import { CategoriesSchema } from './Categories.entity'; +import { CollabLinksSchema } from './CollabLinks.entity'; +import { CollabMessageReactionsSchema } from './CollabMessageReactions.entity'; +import { CollabMessagesSchema } from './CollabMessages.entity'; +import { CollabNotesSchema } from './CollabNotes.entity'; +import { CollabPollsSchema } from './CollabPolls.entity'; +import { CollabPollVotesSchema } from './CollabPollVotes.entity'; +import { CollectionLabelsSchema } from './CollectionLabels.entity'; +import { CollectionMembersSchema } from './CollectionMembers.entity'; +import { CollectionPlaceRatingsSchema } from './CollectionPlaceRatings.entity'; +import { CollectionPlacesSchema } from './CollectionPlaces.entity'; +import { CollectionsSchema } from './Collections.entity'; +import { DawarichConnectionsSchema } from './DawarichConnections.entity'; +import { DawarichVisitSuggestionsSchema } from './DawarichVisitSuggestions.entity'; +import { DayAccommodationsSchema } from './DayAccommodations.entity'; +import { DayAssignmentsSchema } from './DayAssignments.entity'; +import { DayNotesSchema } from './DayNotes.entity'; +import { DaysSchema } from './Days.entity'; +import { FileLinksSchema } from './FileLinks.entity'; +import { GooglePlacePhotoMetaSchema } from './GooglePlacePhotoMeta.entity'; +import { HiddenCountriesSchema } from './HiddenCountries.entity'; +import { HiddenRegionsSchema } from './HiddenRegions.entity'; +import { IdempotencyKeysSchema } from './IdempotencyKeys.entity'; +import { InviteTokensSchema } from './InviteTokens.entity'; +import { JourneyBooksSchema } from './JourneyBooks.entity'; +import { JourneyContributorsSchema } from './JourneyContributors.entity'; +import { JourneyEntriesSchema } from './JourneyEntries.entity'; +import { JourneyEntryPhotosSchema } from './JourneyEntryPhotos.entity'; +import { JourneyPhotosSchema } from './JourneyPhotos.entity'; +import { JourneysSchema } from './Journeys.entity'; +import { JourneyShareTokensSchema } from './JourneyShareTokens.entity'; +import { JourneyTripsSchema } from './JourneyTrips.entity'; +import { McpTokensSchema } from './McpTokens.entity'; +import { MigrationsSchema } from './Migrations.entity'; +import { NotificationChannelPreferencesSchema } from './NotificationChannelPreferences.entity'; +import { NotificationsSchema } from './Notifications.entity'; +import { OauthClientsSchema } from './OauthClients.entity'; +import { OauthConsentsSchema } from './OauthConsents.entity'; +import { OauthTokensSchema } from './OauthTokens.entity'; +import { PackingBagsSchema } from './PackingBags.entity'; +import { PackingCategoryAssigneesSchema } from './PackingCategoryAssignees.entity'; +import { PackingItemContributorsSchema } from './PackingItemContributors.entity'; +import { PackingItemsSchema } from './PackingItems.entity'; +import { PackingTemplateCategoriesSchema } from './PackingTemplateCategories.entity'; +import { PackingTemplateItemsSchema } from './PackingTemplateItems.entity'; +import { PackingTemplatesSchema } from './PackingTemplates.entity'; +import { PasswordResetTokensSchema } from './PasswordResetTokens.entity'; +import { PhotoProviderFieldsSchema } from './PhotoProviderFields.entity'; +import { PhotoProvidersSchema } from './PhotoProviders.entity'; +import { PhotosSchema } from './Photos.entity'; +import { PlaceDetailsCacheSchema } from './PlaceDetailsCache.entity'; +import { PlaceRatingsSchema } from './PlaceRatings.entity'; +import { PlaceRegionsSchema } from './PlaceRegions.entity'; +import { PlacesSchema } from './Places.entity'; +import { PlaceShadowPicksSchema } from './PlaceShadowPicks.entity'; +import { PluginActionsSchema } from './PluginActions.entity'; +import { PluginCapabilityAuditSchema } from './PluginCapabilityAudit.entity'; +import { PluginEgressHostsSchema } from './PluginEgressHosts.entity'; +import { PluginEntityMetadataSchema } from './PluginEntityMetadata.entity'; +import { PluginErrorLogSchema } from './PluginErrorLog.entity'; +import { PluginMetaMigrationsSchema } from './PluginMetaMigrations.entity'; +import { PluginOauthStateSchema } from './PluginOauthState.entity'; +import { PluginOauthTokensSchema } from './PluginOauthTokens.entity'; +import { PluginScheduledTasksSchema } from './PluginScheduledTasks.entity'; +import { PluginsSchema } from './Plugins.entity'; +import { PluginSettingsFieldsSchema } from './PluginSettingsFields.entity'; +import { PluginUserConfigSchema } from './PluginUserConfig.entity'; +import { PluginUserErasureQueueSchema } from './PluginUserErasureQueue.entity'; +import { ReservationDayPositionsSchema } from './ReservationDayPositions.entity'; +import { ReservationEndpointsSchema } from './ReservationEndpoints.entity'; +import { ReservationsSchema } from './Reservations.entity'; +import { ReservationTravelersSchema } from './ReservationTravelers.entity'; +import { RoadtripDayBoundariesSchema } from './RoadtripDayBoundaries.entity'; +import { RoadtripDayTracksSchema } from './RoadtripDayTracks.entity'; +import { RoadtripPreferencesSchema } from './RoadtripPreferences.entity'; +import { RoadtripViasSchema } from './RoadtripVias.entity'; +import { RouteUsageDailySchema } from './RouteUsageDaily.entity'; +import { SchemaVersionSchema } from './SchemaVersion.entity'; +import { SchoolHolidayCountriesSchema } from './SchoolHolidayCountries.entity'; +import { SchoolHolidayPeriodsSchema } from './SchoolHolidayPeriods.entity'; +import { SchoolHolidayRegionsSchema } from './SchoolHolidayRegions.entity'; +import { SettingsSchema } from './Settings.entity'; +import { ShareTokensSchema } from './ShareTokens.entity'; +import { TagsSchema } from './Tags.entity'; +import { TodoCategoryAssigneesSchema } from './TodoCategoryAssignees.entity'; +import { TodoItemsSchema } from './TodoItems.entity'; +import { TrekPhotoCacheMetaSchema } from './TrekPhotoCacheMeta.entity'; +import { TrekPhotosSchema } from './TrekPhotos.entity'; +import { TripAlbumLinksSchema } from './TripAlbumLinks.entity'; +import { TripFilesSchema } from './TripFiles.entity'; +import { TripInviteTokensSchema } from './TripInviteTokens.entity'; +import { TripMembersSchema } from './TripMembers.entity'; +import { TripPhotosSchema } from './TripPhotos.entity'; +import { TripsSchema } from './Trips.entity'; +import { UserNoticeDismissalsSchema } from './UserNoticeDismissals.entity'; +import { UsersSchema } from './Users.entity'; +import { VacayCompanyHolidaysSchema } from './VacayCompanyHolidays.entity'; +import { VacayEntriesSchema } from './VacayEntries.entity'; +import { VacayHolidayCalendarsSchema } from './VacayHolidayCalendars.entity'; +import { VacayPlanMembersSchema } from './VacayPlanMembers.entity'; +import { VacayPlansSchema } from './VacayPlans.entity'; +import { VacaySharesSchema } from './VacayShares.entity'; +import { VacayUserColorsSchema } from './VacayUserColors.entity'; +import { VacayUserSettingsSchema } from './VacayUserSettings.entity'; +import { VacayUserYearsSchema } from './VacayUserYears.entity'; +import { VacayYearsSchema } from './VacayYears.entity'; +import { VisitedCountriesSchema } from './VisitedCountries.entity'; +import { VisitedRegionsSchema } from './VisitedRegions.entity'; +import { WebauthnChallengesSchema } from './WebauthnChallenges.entity'; +import { WebauthnCredentialsSchema } from './WebauthnCredentials.entity'; + +/** + * Every entity the ORM knows, listed explicitly so discovery never depends on a + * filesystem glob (which fails under Node's ESM resolver in the vitest global + * setup and would silently drop an entity a build forgot to emit). + * `tests/unit/db/entities-index.test.ts` refuses a file this list misses. + */ +export const ALL_ENTITIES: readonly EntitySchema[] = [ + AddonsSchema, + AppSettingsSchema, + AssignmentParticipantsSchema, + AuditLogSchema, + BucketListSchema, + BudgetCategoryOrderSchema, + BudgetItemMembersSchema, + BudgetItemPayersSchema, + BudgetItemsSchema, + BudgetSettlementsSchema, + CategoriesSchema, + CollabLinksSchema, + CollabMessageReactionsSchema, + CollabMessagesSchema, + CollabNotesSchema, + CollabPollsSchema, + CollabPollVotesSchema, + CollectionLabelsSchema, + CollectionMembersSchema, + CollectionPlaceRatingsSchema, + CollectionPlacesSchema, + CollectionsSchema, + DawarichConnectionsSchema, + DawarichVisitSuggestionsSchema, + DayAccommodationsSchema, + DayAssignmentsSchema, + DayNotesSchema, + DaysSchema, + FileLinksSchema, + GooglePlacePhotoMetaSchema, + HiddenCountriesSchema, + HiddenRegionsSchema, + IdempotencyKeysSchema, + InviteTokensSchema, + JourneyBooksSchema, + JourneyContributorsSchema, + JourneyEntriesSchema, + JourneyEntryPhotosSchema, + JourneyPhotosSchema, + JourneysSchema, + JourneyShareTokensSchema, + JourneyTripsSchema, + McpTokensSchema, + MigrationsSchema, + NotificationChannelPreferencesSchema, + NotificationsSchema, + OauthClientsSchema, + OauthConsentsSchema, + OauthTokensSchema, + PackingBagsSchema, + PackingCategoryAssigneesSchema, + PackingItemContributorsSchema, + PackingItemsSchema, + PackingTemplateCategoriesSchema, + PackingTemplateItemsSchema, + PackingTemplatesSchema, + PasswordResetTokensSchema, + PhotoProviderFieldsSchema, + PhotoProvidersSchema, + PhotosSchema, + PlaceDetailsCacheSchema, + PlaceRatingsSchema, + PlaceRegionsSchema, + PlacesSchema, + PlaceShadowPicksSchema, + PluginActionsSchema, + PluginCapabilityAuditSchema, + PluginEgressHostsSchema, + PluginEntityMetadataSchema, + PluginErrorLogSchema, + PluginMetaMigrationsSchema, + PluginOauthStateSchema, + PluginOauthTokensSchema, + PluginScheduledTasksSchema, + PluginsSchema, + PluginSettingsFieldsSchema, + PluginUserConfigSchema, + PluginUserErasureQueueSchema, + ReservationDayPositionsSchema, + ReservationEndpointsSchema, + ReservationsSchema, + ReservationTravelersSchema, + RoadtripDayBoundariesSchema, + RoadtripDayTracksSchema, + RoadtripPreferencesSchema, + RoadtripViasSchema, + RouteUsageDailySchema, + SchemaVersionSchema, + SchoolHolidayCountriesSchema, + SchoolHolidayPeriodsSchema, + SchoolHolidayRegionsSchema, + SettingsSchema, + ShareTokensSchema, + TagsSchema, + TodoCategoryAssigneesSchema, + TodoItemsSchema, + TrekPhotoCacheMetaSchema, + TrekPhotosSchema, + TripAlbumLinksSchema, + TripFilesSchema, + TripInviteTokensSchema, + TripMembersSchema, + TripPhotosSchema, + TripsSchema, + UserNoticeDismissalsSchema, + UsersSchema, + VacayCompanyHolidaysSchema, + VacayEntriesSchema, + VacayHolidayCalendarsSchema, + VacayPlanMembersSchema, + VacayPlansSchema, + VacaySharesSchema, + VacayUserColorsSchema, + VacayUserSettingsSchema, + VacayUserYearsSchema, + VacayYearsSchema, + VisitedCountriesSchema, + VisitedRegionsSchema, + WebauthnChallengesSchema, + WebauthnCredentialsSchema, +]; diff --git a/server/src/mikro-orm.config.ts b/server/src/mikro-orm.config.ts index 4eb34e833..6f3220b6e 100644 --- a/server/src/mikro-orm.config.ts +++ b/server/src/mikro-orm.config.ts @@ -3,10 +3,10 @@ import { SharedSqliteDriver } from './db/orm-driver'; import { Migrator } from '@mikro-orm/migrations'; import { SeedManager } from '@mikro-orm/seeder'; import { defineConfig } from '@mikro-orm/sqlite'; +import { ALL_ENTITIES } from './db/entities'; export default defineConfig({ - entities: ['dist/**/*.entity.js'], - entitiesTs: ['src/**/*.entity.ts'], + entities: ALL_ENTITIES, extensions: [Migrator, SeedManager], // Reuses the connection db/database.ts owns rather than opening a second one. // See orm-driver.ts — with `:memory:` in tests, a second connection would be a @@ -15,6 +15,7 @@ export default defineConfig({ migrations: { path: 'dist/db/migrations', pathTs: 'src/db/migrations', + snapshot: false, }, seeder: { path: 'dist/db/seeders', diff --git a/server/tests/unit/db/entities-index.test.ts b/server/tests/unit/db/entities-index.test.ts new file mode 100644 index 000000000..7c6e1e093 --- /dev/null +++ b/server/tests/unit/db/entities-index.test.ts @@ -0,0 +1,23 @@ +import { EntitySchema } from '@mikro-orm/core'; +import fs from 'node:fs'; +import path from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { ALL_ENTITIES } from '../../../src/db/entities'; + +const ENTITIES_DIR = path.join(__dirname, '../../../src/db/entities'); + +describe('ALL_ENTITIES', () => { + it('ENT-001: lists exactly one schema per *.entity.ts file', () => { + const files = fs.readdirSync(ENTITIES_DIR).filter((f) => f.endsWith('.entity.ts')).sort(); + const names = ALL_ENTITIES.map((s) => s.name).sort(); + const expected = files.map((f) => f.replace(/\.entity\.ts$/, '')).sort(); + expect(names).toEqual(expected); + }); + + it('ENT-002: every entry is an EntitySchema with a class', () => { + for (const schema of ALL_ENTITIES) { + expect(schema).toBeInstanceOf(EntitySchema); + expect(typeof schema.meta.class).toBe('function'); + } + }); +});