From 724c88c201b2c16d31317eaea5f59f43d314b833 Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 10 Oct 2026 21:23:37 +0200 Subject: [PATCH] style: format server and shared with prettier A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them. --- server/scripts/build-atlas-geo.mjs | 539 +++- server/scripts/build-legacy-fixture-v244.mjs | 298 +- server/scripts/coverage-thresholds.mjs | 5 +- server/scripts/dev.mjs | 9 +- server/scripts/dialect-lint.mjs | 26 +- server/scripts/eslint-ratchet.mjs | 5 +- server/scripts/extract-templates.cjs | 56 +- server/scripts/format-baseline.json | 1571 +--------- server/scripts/format-lint.mjs | 5 +- server/scripts/gen-plugin-facts.ts | 76 +- server/scripts/generate-db-types.ts | 13 +- server/scripts/import-boundaries.mjs | 36 +- server/scripts/migrate-encryption.ts | 77 +- server/scripts/pg-probe.ts | 72 +- server/scripts/pg-probe/arg-samples.ts | 27 +- server/scripts/pg-probe/baseline.ts | 7 +- server/scripts/pg-probe/engines.ts | 80 +- server/scripts/pg-probe/helper-cases.ts | 251 +- server/scripts/pg-probe/recorder.ts | 12 +- server/scripts/pg-probe/report.ts | 85 +- server/scripts/pg-probe/schema.ts | 9 +- server/scripts/query-api-ratchet.mjs | 8 +- server/scripts/size-lint.mjs | 8 +- server/scripts/strict-lint.mjs | 25 +- server/scripts/test-new-service-ratchet.mjs | 3 +- server/scripts/test-sql-ratchet.mjs | 4 +- server/scripts/tx-writes.mjs | 48 +- server/src/addons.ts | 2 +- server/src/app-config/data-paths.ts | 3 +- server/src/app-config/derive.ts | 6 +- server/src/app-config/env.schema.ts | 10 +- server/src/app-config/parsers.ts | 5 +- server/src/bootstrap.ts | 6 +- server/src/config.ts | 17 +- server/src/db/dialect/kysely-functions.ts | 35 +- server/src/db/dialect/sql-functions.ts | 53 +- server/src/db/document-provider-seed.ts | 1 - server/src/db/durability.ts | 5 +- server/src/db/email-case-index.ts | 11 +- server/src/db/legacy-baseline.ts | 6 +- ...rek_photo_cache_meta_cache_key_not_null.ts | 4 +- ...ooked_night_finally_took_the_seat_a_new.ts | 2 +- .../Migration20200101042000_tours.ts | 2 +- ...101042200_an_index_on_every_foreign_key.ts | 44 +- ...2300_every_tour_type_the_contract_names.ts | 2 +- server/src/db/nul-safe-sqlite-platform.ts | 2 +- server/src/db/orm-driver.ts | 2 +- server/src/db/orm.ts | 10 +- .../AssignmentParticipants.repository.ts | 2 +- .../db/repositories/AuditLog.repository.ts | 14 +- .../db/repositories/BucketList.repository.ts | 45 +- .../BudgetCategoryOrder.repository.ts | 2 +- .../BudgetItemMembers.repository.ts | 84 +- .../BudgetItemPayers.repository.ts | 34 +- .../db/repositories/BudgetItems.repository.ts | 292 +- .../BudgetSettlements.repository.ts | 89 +- .../db/repositories/Categories.repository.ts | 4 +- .../db/repositories/CollabLinks.repository.ts | 38 +- .../repositories/CollabMessages.repository.ts | 47 +- .../db/repositories/CollabNotes.repository.ts | 91 +- .../db/repositories/CollabPolls.repository.ts | 34 +- .../CollectionLabels.repository.ts | 21 +- .../CollectionMembers.repository.ts | 7 +- .../CollectionPlaceRatings.repository.ts | 2 +- .../CollectionPlaces.repository.ts | 50 +- .../db/repositories/Collections.repository.ts | 125 +- .../DawarichConnections.repository.ts | 50 +- .../DawarichVisitSuggestions.repository.ts | 2 +- .../DayAccommodations.repository.ts | 120 +- .../db/repositories/DayNotes.repository.ts | 48 +- server/src/db/repositories/Days.repository.ts | 14 +- server/src/db/repositories/DemoRepository.ts | 113 +- .../DocumentConnections.repository.ts | 25 +- .../DocumentProviders.repository.ts | 7 +- .../DocumentSyncItems.repository.ts | 124 +- .../db/repositories/FileLinks.repository.ts | 16 +- .../repositories/GoogleApiUsage.repository.ts | 4 +- .../HiddenCountries.repository.ts | 14 +- .../repositories/HiddenRegions.repository.ts | 14 +- .../IdempotencyKeys.repository.ts | 12 +- .../repositories/InviteTokens.repository.ts | 2 +- .../repositories/JourneyBooks.repository.ts | 6 +- .../JourneyContributors.repository.ts | 2 +- .../repositories/JourneyEntries.repository.ts | 64 +- .../JourneyEntryPhotos.repository.ts | 24 +- .../repositories/JourneyPhotos.repository.ts | 68 +- .../JourneyShareTokens.repository.ts | 10 +- .../repositories/JourneyTrips.repository.ts | 26 +- .../db/repositories/Journeys.repository.ts | 19 +- .../db/repositories/MaintenanceRepository.ts | 4 +- .../db/repositories/McpTokens.repository.ts | 12 +- .../repositories/Notifications.repository.ts | 15 +- .../repositories/OauthClients.repository.ts | 48 +- .../repositories/OauthConsents.repository.ts | 7 +- .../db/repositories/OauthTokens.repository.ts | 39 +- .../db/repositories/PackingBags.repository.ts | 105 +- .../PackingCategoryAssignees.repository.ts | 14 +- .../PackingItemContributors.repository.ts | 9 +- .../repositories/PackingItems.repository.ts | 117 +- .../PackingTemplateCategories.repository.ts | 46 +- .../PackingTemplateItems.repository.ts | 20 +- .../PackingTemplates.repository.ts | 25 +- .../PasswordResetTokens.repository.ts | 8 +- .../PhotoProviderFields.repository.ts | 13 +- .../src/db/repositories/Photos.repository.ts | 2 +- .../PlaceDetailsCache.repository.ts | 14 +- .../repositories/PlaceRatings.repository.ts | 7 +- .../repositories/PlaceRegions.repository.ts | 36 +- .../PlaceShadowPicks.repository.ts | 8 +- .../repositories/PluginActions.repository.ts | 20 +- .../PluginCapabilityAudit.repository.ts | 26 +- .../PluginEgressHosts.repository.ts | 5 +- .../PluginEntityMetadata.repository.ts | 31 +- .../repositories/PluginErrorLog.repository.ts | 9 +- .../PluginOauthState.repository.ts | 10 +- .../PluginOauthTokens.repository.ts | 25 +- .../PluginScheduledTasks.repository.ts | 22 +- .../PluginSettingsFields.repository.ts | 22 +- .../PluginUserConfig.repository.ts | 8 +- .../PluginUserErasureQueue.repository.ts | 7 +- .../src/db/repositories/Plugins.repository.ts | 69 +- .../PushSubscriptions.repository.ts | 6 +- .../ReservationDayPositions.repository.ts | 17 +- .../ReservationEndpoints.repository.ts | 72 +- .../ReservationTravelers.repository.ts | 17 +- .../RoadtripDayBoundaries.repository.ts | 17 +- .../RoadtripDayTracks.repository.ts | 4 +- .../RoadtripPreferences.repository.ts | 5 +- .../repositories/RoadtripVias.repository.ts | 29 +- .../RouteUsageDaily.repository.ts | 4 +- .../SchedulerLeases.repository.ts | 6 +- .../SchoolHolidayCountries.repository.ts | 2 +- .../SchoolHolidayPeriods.repository.ts | 11 +- .../SchoolHolidayRegions.repository.ts | 5 +- .../db/repositories/ShareTokens.repository.ts | 43 +- server/src/db/repositories/Tags.repository.ts | 7 +- .../TodoCategoryAssignees.repository.ts | 2 +- .../db/repositories/TodoItems.repository.ts | 105 +- .../repositories/TourWaypoints.repository.ts | 2 +- .../src/db/repositories/Tours.repository.ts | 22 +- .../TrekPhotoCacheMeta.repository.ts | 5 +- .../db/repositories/TrekPhotos.repository.ts | 35 +- .../repositories/TripAlbumLinks.repository.ts | 22 +- .../TripDocumentLinks.repository.ts | 33 +- .../db/repositories/TripFiles.repository.ts | 28 +- .../TripInviteTokens.repository.ts | 28 +- .../db/repositories/TripMembers.repository.ts | 12 +- .../db/repositories/TripPhotos.repository.ts | 32 +- .../UserNoticeDismissals.repository.ts | 7 +- .../repositories/UserSessions.repository.ts | 18 +- .../VacayCompanyHolidays.repository.ts | 24 +- .../repositories/VacayEntries.repository.ts | 50 +- .../VacayHolidayCalendars.repository.ts | 14 +- .../VacayPlanMembers.repository.ts | 14 +- .../db/repositories/VacayPlans.repository.ts | 8 +- .../db/repositories/VacayShares.repository.ts | 10 +- .../VacayUserColors.repository.ts | 5 +- .../VacayUserSettings.repository.ts | 16 +- .../repositories/VacayUserYears.repository.ts | 20 +- .../db/repositories/VacayYears.repository.ts | 5 +- .../VisitedCountries.repository.ts | 14 +- .../repositories/VisitedRegions.repository.ts | 14 +- .../WebauthnChallenges.repository.ts | 8 +- .../WebauthnCredentials.repository.ts | 11 +- .../db/repositories/_shared/owned-lookup.ts | 2 +- .../_shared/packing-visibility.ts | 8 +- .../db/repositories/_shared/presence-set.ts | 6 +- .../_shared/reservation-travelers-owns.ts | 8 +- .../_shared/reservation-visibility.ts | 5 +- .../repositories/_shared/trek-repository.ts | 45 +- server/src/db/reseat-booked-nights.ts | 22 +- server/src/db/seeders/AddonSeeder.ts | 6 +- server/src/db/seeders/DatabaseSeeder.ts | 11 +- .../src/db/seeders/DocumentProviderSeeder.ts | 15 +- server/src/db/seeders/PhotoProviderSeeder.ts | 15 +- server/src/db/seeders/SchemaVersionSeeder.ts | 6 +- server/src/demo/demo-reset.ts | 17 +- server/src/demo/demo-seed.ts | 679 ++++- server/src/index.ts | 44 +- server/src/mcp/index.ts | 24 +- server/src/mcp/scopes.ts | 279 +- server/src/mcp/sessionManager.ts | 12 +- server/src/mcp/tools.ts | 21 +- server/src/middleware/globalMiddleware.ts | 304 +- server/src/mikro-orm.config.ts | 2 +- server/src/nest-mcp/registry.ts | 16 +- .../accommodations-domain.module.ts | 38 +- .../accommodations.controller.ts | 35 +- .../nest/accommodations/accommodations.dto.ts | 3 +- .../nest/accommodations/accommodations.mcp.ts | 229 +- .../accommodations/accommodations.module.ts | 25 +- .../nest/accommodations/accommodations.rpc.ts | 30 +- .../accommodations/accommodations.service.ts | 235 +- server/src/nest/accommodations/night-seat.ts | 53 +- server/src/nest/addons/addon-gate.ts | 2 +- server/src/nest/addons/addon.guard.ts | 4 +- server/src/nest/addons/addons.controller.ts | 4 +- server/src/nest/addons/addons.module.ts | 16 +- server/src/nest/addons/addons.service.ts | 100 +- server/src/nest/addons/mcp-addon-gate.ts | 5 +- server/src/nest/admin/admin.controller.ts | 299 +- server/src/nest/admin/admin.dto.ts | 3 +- server/src/nest/admin/admin.module.ts | 104 +- server/src/nest/admin/admin.service.ts | 171 +- server/src/nest/admin/demo-reset.job.ts | 10 +- server/src/nest/admin/version-check.job.ts | 8 +- .../src/nest/airports/airports.controller.ts | 4 +- server/src/nest/airports/airports.data.ts | 4 +- server/src/nest/airports/airports.mcp.ts | 12 +- server/src/nest/airports/airports.module.ts | 14 +- server/src/nest/airports/airports.service.ts | 61 +- .../src/nest/app-config/app-config.module.ts | 6 +- .../src/nest/app-config/data-paths.service.ts | 2 +- .../nest/app-config/runtime-env.service.ts | 2 +- server/src/nest/app-config/tokens.ts | 2 +- server/src/nest/app.module.ts | 24 +- .../assignments/assignments-domain.module.ts | 36 +- .../assignments/assignments.controller.ts | 80 +- .../src/nest/assignments/assignments.dto.ts | 3 +- .../src/nest/assignments/assignments.mcp.ts | 145 +- .../nest/assignments/assignments.module.ts | 25 +- .../nest/assignments/assignments.service.ts | 105 +- server/src/nest/assignments/itinerary.rpc.ts | 10 +- server/src/nest/atlas/atlas.controller.ts | 30 +- server/src/nest/atlas/atlas.dto.ts | 7 +- server/src/nest/atlas/atlas.mcp.ts | 101 +- server/src/nest/atlas/atlas.module.ts | 48 +- server/src/nest/atlas/atlas.rpc.ts | 13 +- server/src/nest/atlas/city-from-address.ts | 5 +- server/src/nest/atlas/known-countries.ts | 200 +- .../nest/atlas/place-regions-repair.job.ts | 6 +- .../src/nest/atlas/public-stats.controller.ts | 11 +- .../src/nest/atlas/travel-stats.controller.ts | 8 +- server/src/nest/audit/audit-log.logger.ts | 12 +- server/src/nest/audit/audit.module.ts | 4 +- server/src/nest/audit/audit.service.ts | 8 +- server/src/nest/audit/entry-trace.logger.ts | 2 +- .../src/nest/auth/auth-public.controller.ts | 94 +- server/src/nest/auth/auth.controller.ts | 93 +- server/src/nest/auth/auth.dto.ts | 3 +- server/src/nest/auth/auth.helpers.ts | 20 +- server/src/nest/auth/auth.mcp.ts | 14 +- server/src/nest/auth/auth.module.ts | 115 +- server/src/nest/auth/passkey-enabled.guard.ts | 2 +- server/src/nest/auth/passkey.controller.ts | 75 +- server/src/nest/auth/passkey.service.ts | 64 +- .../nest/auth/registration-invites.service.ts | 19 +- .../nest/auth/session-renewal.interceptor.ts | 9 +- server/src/nest/auth/sessions.controller.ts | 39 +- server/src/nest/auth/user-cleanup.service.ts | 67 +- server/src/nest/auth/user-profile.service.ts | 150 +- .../src/nest/auth/webauthn-config.service.ts | 4 +- server/src/nest/backup/auto-backup.job.ts | 10 +- .../src/nest/backup/auto-backup.settings.ts | 28 +- server/src/nest/backup/backup-archive.ts | 22 +- server/src/nest/backup/backup.controller.ts | 74 +- server/src/nest/backup/backup.dto.ts | 3 +- server/src/nest/backup/backup.impl.ts | 64 +- server/src/nest/backup/backup.module.ts | 20 +- server/src/nest/backup/backup.service.ts | 47 +- server/src/nest/backup/boot-restore.ts | 9 +- .../src/nest/backup/database-backup.module.ts | 2 +- .../src/nest/backup/sqlite-database-backup.ts | 11 +- .../booking-import/booking-import.module.ts | 22 +- .../booking-import/booking-import.service.ts | 122 +- .../booking-import/import-jobs.service.ts | 19 +- .../kitinerary-extractor.module.ts | 2 +- .../kitinerary-extractor.service.ts | 30 +- .../nest/booking-import/kitinerary-mapper.ts | 351 ++- .../nest/booking-import/kitinerary.types.ts | 4 +- server/src/nest/budget/budget.controller.ts | 77 +- server/src/nest/budget/budget.dto.ts | 3 +- server/src/nest/budget/budget.mcp.ts | 559 +++- server/src/nest/budget/budget.module.ts | 56 +- server/src/nest/budget/costs.rpc.ts | 17 +- .../src/nest/budget/exchange-rates.service.ts | 5 +- server/src/nest/calendar/calendar.module.ts | 12 +- server/src/nest/calendar/calendar.service.ts | 164 +- .../nest/categories/categories.controller.ts | 10 +- server/src/nest/categories/categories.dto.ts | 3 +- server/src/nest/categories/categories.mcp.ts | 47 +- .../src/nest/categories/categories.module.ts | 12 +- .../src/nest/categories/categories.service.ts | 6 +- server/src/nest/collab/collab.controller.ts | 242 +- server/src/nest/collab/collab.dto.ts | 3 +- server/src/nest/collab/collab.mcp.ts | 144 +- server/src/nest/collab/collab.module.ts | 57 +- server/src/nest/collab/collab.rpc.ts | 19 +- server/src/nest/collab/collab.service.ts | 219 +- .../collections/collections.controller.ts | 149 +- .../src/nest/collections/collections.dto.ts | 3 +- .../src/nest/collections/collections.mcp.ts | 306 +- .../nest/collections/collections.module.ts | 64 +- .../src/nest/collections/collections.rpc.ts | 18 +- server/src/nest/common/content-disposition.ts | 5 +- server/src/nest/common/cookie.ts | 3 +- server/src/nest/common/crypto/apiKeyCrypto.ts | 4 +- server/src/nest/common/crypto/mfaCrypto.ts | 3 +- server/src/nest/common/demo.module.ts | 2 +- server/src/nest/common/demo.service.ts | 6 +- server/src/nest/common/demo.ts | 5 +- server/src/nest/common/geo.ts | 4 +- .../nest/common/idempotency-cleanup.job.ts | 6 +- .../nest/common/idempotency.interceptor.ts | 29 +- server/src/nest/common/managed.guard.ts | 12 +- server/src/nest/common/managed.ts | 2 +- server/src/nest/common/place-image-upload.ts | 2 +- server/src/nest/common/rate-limit.module.ts | 2 +- server/src/nest/common/rate-limit.service.ts | 2 +- server/src/nest/common/rowShape.ts | 24 +- .../nest/common/spool-cleanup.interceptor.ts | 5 +- server/src/nest/common/timezoneService.ts | 4 +- .../src/nest/common/trek-exception.filter.ts | 7 +- .../nest/common/validate-body-contracts.ts | 10 +- .../nest/common/validate-managed-routes.ts | 19 +- .../src/nest/common/validate-route-guards.ts | 20 +- server/src/nest/common/zod-validation.pipe.ts | 10 +- server/src/nest/config/config.controller.ts | 4 +- server/src/nest/config/config.module.ts | 2 +- .../database/database-lifecycle.module.ts | 2 +- .../database/database-lifecycle.service.ts | 4 +- .../src/nest/database/maintenance.module.ts | 4 +- server/src/nest/database/orm.module.ts | 2 +- server/src/nest/database/unit-of-work.ts | 2 +- .../nest/day-notes/day-notes.controller.ts | 29 +- server/src/nest/day-notes/day-notes.dto.ts | 3 +- server/src/nest/day-notes/day-notes.mcp.ts | 124 +- server/src/nest/day-notes/day-notes.module.ts | 29 +- server/src/nest/day-notes/day-notes.rpc.ts | 13 +- .../src/nest/day-notes/day-notes.service.ts | 39 +- server/src/nest/days/day-removal.service.ts | 58 +- server/src/nest/days/days.controller.ts | 26 +- server/src/nest/days/days.dto.ts | 3 +- server/src/nest/days/days.mcp.ts | 116 +- server/src/nest/days/days.module.ts | 53 +- server/src/nest/days/days.rpc.ts | 12 +- server/src/nest/days/days.service.ts | 179 +- .../nest/doc-sync/doc-sync-config.service.ts | 82 +- server/src/nest/doc-sync/doc-sync-secrets.ts | 8 +- .../doc-sync/doc-sync-webhook.controller.ts | 28 +- .../src/nest/doc-sync/doc-sync.controller.ts | 69 +- server/src/nest/doc-sync/doc-sync.dto.ts | 3 +- server/src/nest/doc-sync/doc-sync.helpers.ts | 58 +- server/src/nest/doc-sync/doc-sync.job.ts | 10 +- server/src/nest/doc-sync/doc-sync.mcp.ts | 25 +- server/src/nest/doc-sync/doc-sync.module.ts | 38 +- .../doc-sync/document-provider.registry.ts | 2 +- server/src/nest/doc-sync/document-provider.ts | 52 +- .../doc-sync/providers/paperless.client.ts | 43 +- .../doc-sync/providers/paperless.provider.ts | 88 +- .../nest/doc-sync/providers/papra.client.ts | 2 +- .../nest/doc-sync/providers/provider-http.ts | 7 +- .../providers/synology-drive.provider.ts | 50 +- .../nest/doc-sync/providers/webdav.client.ts | 60 +- .../doc-sync/providers/webdav.provider.ts | 100 +- server/src/nest/feeds/feeds.controller.ts | 27 +- server/src/nest/feeds/feeds.mcp.ts | 45 +- server/src/nest/feeds/feeds.module.ts | 14 +- server/src/nest/feeds/feeds.service.ts | 7 +- .../nest/files/allowed-file-types.module.ts | 6 +- .../nest/files/allowed-file-types.service.ts | 6 +- .../nest/files/files-download.controller.ts | 14 +- server/src/nest/files/files.constants.ts | 38 +- server/src/nest/files/files.controller.ts | 155 +- server/src/nest/files/files.dto.ts | 7 +- server/src/nest/files/files.mcp.ts | 93 +- server/src/nest/files/files.module.ts | 61 +- server/src/nest/files/files.rpc.ts | 44 +- server/src/nest/files/files.service.ts | 159 +- server/src/nest/geo/geo.module.ts | 2 +- server/src/nest/geo/geocoding.service.ts | 2 +- server/src/nest/geo/nominatim.client.ts | 2 +- .../google-quota/google-quota.controller.ts | 28 +- .../src/nest/google-quota/google-quota.dto.ts | 3 +- .../nest/google-quota/google-quota.module.ts | 4 +- .../nest/google-quota/google-quota.service.ts | 6 +- server/src/nest/health/features.controller.ts | 11 +- server/src/nest/health/health.module.ts | 6 +- server/src/nest/help/help.controller.ts | 5 +- server/src/nest/help/help.mcp.ts | 5 +- server/src/nest/help/help.module.ts | 2 +- server/src/nest/help/wiki.ts | 10 +- .../nest/integrations/airtrail-core.module.ts | 16 +- .../integrations/airtrail-import.service.ts | 105 +- .../integrations/airtrail-link.service.ts | 32 +- .../integrations/airtrail-sync.helpers.ts | 11 +- .../nest/integrations/airtrail-sync.job.ts | 8 +- .../integrations/airtrail-sync.service.ts | 10 +- .../src/nest/integrations/airtrail.client.ts | 8 +- .../nest/integrations/airtrail.controller.ts | 31 +- server/src/nest/integrations/airtrail.dto.ts | 3 +- server/src/nest/integrations/airtrail.mcp.ts | 53 +- .../src/nest/integrations/airtrail.module.ts | 28 +- .../src/nest/integrations/airtrail.service.ts | 25 +- .../dawarich-suggestions.service.ts | 82 +- .../nest/integrations/dawarich-sync.job.ts | 8 +- .../integrations/dawarich-sync.service.ts | 48 +- .../integrations/dawarich-tracks.service.ts | 13 +- .../src/nest/integrations/dawarich.client.ts | 18 +- .../nest/integrations/dawarich.controller.ts | 68 +- server/src/nest/integrations/dawarich.dto.ts | 3 +- .../src/nest/integrations/dawarich.helpers.ts | 39 +- server/src/nest/integrations/dawarich.mcp.ts | 72 +- .../src/nest/integrations/dawarich.module.ts | 48 +- .../src/nest/integrations/dawarich.service.ts | 28 +- server/src/nest/journey/journal-rpc.module.ts | 17 +- server/src/nest/journey/journal.rpc.ts | 27 +- .../src/nest/journey/journey-book.service.ts | 14 +- .../src/nest/journey/journey-domain.module.ts | 36 +- server/src/nest/journey/journey-entry-row.ts | 13 +- .../journey/journey-photo-capture.module.ts | 4 +- .../journey/journey-photo-capture.service.ts | 41 +- .../nest/journey/journey-public.controller.ts | 27 +- .../src/nest/journey/journey-share.service.ts | 65 +- server/src/nest/journey/journey-stats.ts | 37 +- server/src/nest/journey/journey.dto.ts | 3 +- server/src/nest/journey/journey.mcp.ts | 348 ++- server/src/nest/journey/journey.module.ts | 35 +- server/src/nest/journey/journey.service.ts | 204 +- .../llm-parse/clients/anthropic.client.ts | 6 +- .../src/nest/llm-parse/clients/nuextract.ts | 21 +- .../clients/openai-compatible.client.ts | 16 +- server/src/nest/llm-parse/image-input.ts | 13 +- server/src/nest/llm-parse/lenient-json.ts | 6 +- .../llm-parse/llm-capabilities.controller.ts | 8 +- .../src/nest/llm-parse/llm-client.factory.ts | 6 +- .../src/nest/llm-parse/llm-config.resolver.ts | 8 +- server/src/nest/llm-parse/llm-config.ts | 2 +- .../nest/llm-parse/llm-local.controller.ts | 11 +- .../src/nest/llm-parse/llm-local.service.ts | 11 +- server/src/nest/llm-parse/llm-parse.module.ts | 18 +- .../src/nest/llm-parse/llm-parse.service.ts | 65 +- server/src/nest/llm-parse/mime-email.ts | 7 +- server/src/nest/llm-parse/receipt-read.ts | 11 +- .../llm-parse/router/extraction-router.ts | 85 +- .../src/nest/llm-parse/router/flat-schemas.ts | 117 +- .../llm-parse/router/ollama-format.client.ts | 5 +- server/src/nest/llm-parse/text-extract.ts | 23 +- server/src/nest/maps/maps-nearby.helpers.ts | 9 +- server/src/nest/maps/maps.controller.ts | 57 +- server/src/nest/maps/maps.dto.ts | 3 +- server/src/nest/maps/maps.helpers.ts | 73 +- server/src/nest/maps/maps.mcp.ts | 51 +- server/src/nest/maps/maps.module.ts | 35 +- server/src/nest/maps/maps.service.ts | 107 +- .../src/nest/maps/providers/amap.provider.ts | 47 +- .../nest/maps/providers/places-provider.ts | 7 +- server/src/nest/maps/trek-places.client.ts | 9 +- .../src/nest/mcp-shared/mcp-shared.module.ts | 10 +- .../mcp-shared/mcp-tool-guards.service.ts | 14 +- .../mcp-transport/mcp-transport.constants.ts | 8 +- .../mcp-transport/mcp-transport.controller.ts | 5 +- .../mcp-transport/mcp-transport.module.ts | 12 +- .../mcp-transport/mcp-transport.service.ts | 114 +- server/src/nest/memories/immich.controller.ts | 20 +- server/src/nest/memories/immich.service.ts | 340 ++- .../src/nest/memories/journey-thumbs.job.ts | 2 +- .../nest/memories/memories-access.service.ts | 53 +- server/src/nest/memories/memories.dto.ts | 3 +- server/src/nest/memories/memories.helpers.ts | 369 +-- server/src/nest/memories/memories.mcp.ts | 97 +- server/src/nest/memories/memories.module.ts | 104 +- server/src/nest/memories/memories.service.ts | 72 +- .../photo-capture-backfill.service.ts | 16 +- .../nest/memories/photo-provider.registry.ts | 2 +- server/src/nest/memories/photo-provider.ts | 3 +- .../nest/memories/photo-resolver.service.ts | 25 +- .../memories/providers/immich.provider.ts | 9 +- .../memories/providers/synology.provider.ts | 21 +- .../src/nest/memories/synology.controller.ts | 61 +- server/src/nest/memories/synology.service.ts | 1221 ++++---- server/src/nest/memories/thumbnail.service.ts | 85 +- .../src/nest/memories/trek-photo-cache.job.ts | 17 +- .../nest/memories/trek-photo-cache.service.ts | 19 +- .../nest/memories/unified-memories.service.ts | 112 +- .../src/nest/memories/unified.controller.ts | 33 +- .../nest/notifications/channel-registry.ts | 3 +- .../nest/notifications/channels/builtins.ts | 27 +- .../nest/notifications/mailer/email-html.ts | 4 +- .../notifications/mailer/mailer.module.ts | 10 +- .../notifications/mailer/mailer.service.ts | 40 +- .../notifications/mailer/smtp-diagnostics.ts | 25 +- .../notification-preferences.service.ts | 66 +- .../notifications/notifications.controller.ts | 37 +- .../nest/notifications/notifications.dto.ts | 3 +- .../nest/notifications/notifications.mcp.ts | 35 +- .../notifications/notifications.module.ts | 36 +- .../notifications/notifications.service.ts | 328 +- .../push/push-subscription.helpers.ts | 2 +- .../push/push-subscriptions.service.ts | 4 +- .../notifications/push/push.controller.ts | 4 +- .../notifications/push/vapid-keys.service.ts | 10 +- .../notifications/push/web-push-crypto.ts | 5 +- .../notifications/reminder-jobs.service.ts | 59 +- .../storage-health-notifier.service.ts | 4 +- .../notifications/transports/ntfy.service.ts | 14 +- .../transports/web-push.service.ts | 4 +- .../transports/webhook.service.ts | 12 +- server/src/nest/oauth/oauth-api.controller.ts | 64 +- .../src/nest/oauth/oauth-public.controller.ts | 145 +- server/src/nest/oauth/oauth-sdk.provider.ts | 320 +- .../nest/oauth/oauth-token-retention.job.ts | 6 +- server/src/nest/oauth/oauth.dto.ts | 3 +- server/src/nest/oauth/oauth.helpers.ts | 62 +- server/src/nest/oauth/oauth.module.ts | 50 +- server/src/nest/oauth/oauth.pending-codes.ts | 2 +- server/src/nest/oauth/oauth.service.ts | 226 +- server/src/nest/oidc/oidc.controller.ts | 35 +- server/src/nest/oidc/oidc.module.ts | 23 +- server/src/nest/oidc/oidc.service.ts | 154 +- .../admin-packing-templates.controller.ts | 35 +- server/src/nest/packing/packing.controller.ts | 103 +- server/src/nest/packing/packing.dto.ts | 3 +- server/src/nest/packing/packing.mcp.ts | 353 ++- server/src/nest/packing/packing.module.ts | 59 +- server/src/nest/packing/packing.rpc.ts | 29 +- .../nest/permissions/permissions.module.ts | 8 +- .../nest/permissions/permissions.service.ts | 17 +- .../src/nest/permissions/trip-access.guard.ts | 17 +- .../src/nest/permissions/trip-owner.guard.ts | 23 +- server/src/nest/permissions/trip.decorator.ts | 2 +- server/src/nest/photos/photos.controller.ts | 16 +- server/src/nest/photos/photos.module.ts | 4 +- server/src/nest/photos/photos.service.ts | 5 +- .../photos/trek-photo-registration.service.ts | 16 +- server/src/nest/photos/trek-photos.module.ts | 8 +- .../place-enrichment.controller.ts | 11 +- .../place-enrichment/place-enrichment.dto.ts | 3 +- .../place-enrichment.module.ts | 17 +- .../place-enrichment.service.ts | 104 +- .../place-photos/place-photo-cache.job.ts | 2 +- .../place-photos/place-photo-cache.service.ts | 17 +- .../nest/place-photos/place-photos.module.ts | 21 +- .../place-shadow/place-shadow.controller.ts | 8 +- .../src/nest/place-shadow/place-shadow.dto.ts | 3 +- .../src/nest/place-shadow/place-shadow.job.ts | 2 +- .../nest/place-shadow/place-shadow.module.ts | 8 +- .../nest/place-shadow/place-shadow.service.ts | 24 +- server/src/nest/places/places.controller.ts | 129 +- server/src/nest/places/places.dto.ts | 3 +- server/src/nest/places/places.helpers.ts | 35 +- server/src/nest/places/places.module.ts | 74 +- server/src/nest/places/places.rpc.ts | 23 +- server/src/nest/platform/api-docs.ts | 21 +- .../nest/platform/consent-coop.middleware.ts | 1 + .../platform/discovery-metadata.service.ts | 27 +- .../src/nest/platform/discovery.controller.ts | 19 +- .../nest/platform/mcp-metadata.middleware.ts | 9 +- server/src/nest/platform/platform.module.ts | 8 +- server/src/nest/platform/platform.routes.ts | 30 +- .../src/nest/platform/spa-fallback.filter.ts | 5 +- .../contributions/atlas-layers.controller.ts | 5 +- .../contributions/day-schedule.controller.ts | 15 +- .../contributions/day-tints.controller.ts | 22 +- .../journal-entry-rows.controller.ts | 15 +- .../contributions/map-layers.controller.ts | 14 +- .../contributions/map-markers.controller.ts | 17 +- .../contributions/pdf-sections.controller.ts | 14 +- .../contributions/place-details.controller.ts | 13 +- .../plugin-calendar.controller.ts | 20 +- .../plugin-contributions.module.ts | 59 +- .../contributions/plugin-mcp-tools.service.ts | 50 +- .../contributions/plugin-photos.controller.ts | 28 +- .../contributions/plugin-pois.controller.ts | 4 +- .../contributions/plugin-pois.helpers.ts | 6 +- .../plugins/contributions/plugin-pois.mcp.ts | 40 +- .../contributions/plugin-pois.service.ts | 21 +- .../contributions/plugin-route-normalize.ts | 2 +- .../contributions/plugin-routes.controller.ts | 13 +- .../contributions/plugin-search.controller.ts | 15 +- .../contributions/plugin-search.helpers.ts | 8 +- .../contributions/plugin-search.mcp.ts | 44 +- .../trip-card-contributions.controller.ts | 22 +- .../contributions/trip-warnings.controller.ts | 27 +- .../contributions/trip-warnings.mcp.ts | 16 +- .../view-contributions.controller.ts | 51 +- server/src/nest/plugins/dependencies.ts | 21 +- server/src/nest/plugins/dev-link.ts | 1 + server/src/nest/plugins/host/daily-budget.ts | 14 +- server/src/nest/plugins/host/plugin-audit.ts | 65 +- .../nest/plugins/host/plugin-data.service.ts | 41 +- .../nest/plugins/host/plugin-host-state.ts | 12 +- server/src/nest/plugins/host/plugin-jobs.ts | 3 +- .../plugins/host/plugin-rpc-host.factory.ts | 14 +- server/src/nest/plugins/host/rate-limit.ts | 5 +- server/src/nest/plugins/host/rpc-host.ts | 16 +- server/src/nest/plugins/host/rpc/db.rpc.ts | 2 +- .../nest/plugins/host/rpc/host-surface.rpc.ts | 48 +- server/src/nest/plugins/host/rpc/meta.rpc.ts | 30 +- server/src/nest/plugins/install/discovery.ts | 36 +- .../src/nest/plugins/install/host-compat.ts | 7 +- server/src/nest/plugins/install/manifest.ts | 63 +- .../src/nest/plugins/install/safe-extract.ts | 25 +- server/src/nest/plugins/install/safe-fetch.ts | 3 +- .../nest/plugins/install/verify-signature.ts | 17 +- server/src/nest/plugins/mcp-tool-schema.ts | 62 +- .../plugins/oauth/plugin-oauth.controller.ts | 29 +- .../nest/plugins/oauth/plugin-oauth.module.ts | 8 +- .../plugins/oauth/plugin-oauth.service.ts | 64 +- server/src/nest/plugins/paths.ts | 9 +- .../plugins/plugin-activity.controller.ts | 14 +- server/src/nest/plugins/plugin-backup.ts | 23 +- .../nest/plugins/plugin-frame.controller.ts | 14 +- .../plugin-user-settings.controller.ts | 23 +- .../plugins/plugin-user-settings.service.ts | 12 +- .../nest/plugins/plugins-feed.controller.ts | 31 +- .../nest/plugins/plugins-proxy.controller.ts | 42 +- .../nest/plugins/plugins-runtime.module.ts | 176 +- server/src/nest/plugins/plugins.controller.ts | 87 +- server/src/nest/plugins/plugins.dto.ts | 3 +- server/src/nest/plugins/plugins.module.ts | 24 +- server/src/nest/plugins/plugins.service.ts | 77 +- server/src/nest/plugins/poi-categories.ts | 8 +- .../nest/plugins/protocol/output-contract.ts | 329 +- .../nest/plugins/registry/registry.service.ts | 59 +- .../src/nest/plugins/runtime/egress-policy.ts | 3 +- .../nest/plugins/runtime/plugin-host-entry.ts | 102 +- server/src/nest/plugins/signature-status.ts | 13 +- .../plugins/supervisor/plugin-supervisor.ts | 121 +- server/src/nest/plugins/text-sanitize.ts | 5 +- .../src/nest/plugins/user-erasure-enqueue.ts | 2 +- server/src/nest/public-api/api-token.guard.ts | 15 +- .../src/nest/public-api/public-api-request.ts | 7 +- .../nest/public-api/public-api.controller.ts | 23 +- .../src/nest/public-api/public-api.module.ts | 31 +- .../src/nest/public-api/public-api.service.ts | 61 +- .../query-helpers/query-helpers.module.ts | 10 +- .../query-helpers/query-helpers.service.ts | 17 +- .../nest/realtime/realtime-gateway.module.ts | 12 +- server/src/nest/realtime/realtime.gateway.ts | 50 +- server/src/nest/realtime/realtime.module.ts | 2 +- server/src/nest/realtime/realtime.service.ts | 13 +- server/src/nest/realtime/trek-ws.adapter.ts | 19 +- server/src/nest/realtime/ws-state.ts | 15 +- .../receipt-scan/receipt-scan.controller.ts | 44 +- .../nest/receipt-scan/receipt-scan.module.ts | 17 +- .../reservation-import.controller.ts | 65 +- .../reservation-import.dto.ts | 8 +- .../reservation-import.mcp.ts | 40 +- .../reservation-import.module.ts | 24 +- .../reservations/reservations-read.module.ts | 8 +- .../reservations/reservations-read.service.ts | 33 +- .../reservations/reservations.controller.ts | 62 +- .../src/nest/reservations/reservations.dto.ts | 3 +- .../nest/reservations/reservations.module.ts | 75 +- .../src/nest/reservations/reservations.rpc.ts | 41 +- .../upcoming-reservations.controller.ts | 6 +- .../roadtrip/charging-lookup.controller.ts | 11 +- .../src/nest/roadtrip/charging.controller.ts | 6 +- server/src/nest/roadtrip/charging.helpers.ts | 162 +- server/src/nest/roadtrip/charging.mcp.ts | 35 +- server/src/nest/roadtrip/charging.service.ts | 86 +- .../roadtrip/day-boundaries.controller.ts | 20 +- .../src/nest/roadtrip/day-boundaries.mcp.ts | 47 +- .../nest/roadtrip/day-boundaries.service.ts | 13 +- .../nest/roadtrip/google-route.controller.ts | 21 +- server/src/nest/roadtrip/google-route.mcp.ts | 58 +- .../src/nest/roadtrip/google-route.service.ts | 73 +- .../roadtrip/roadtrip-hazards.controller.ts | 6 +- .../src/nest/roadtrip/roadtrip-hazards.mcp.ts | 17 +- .../nest/roadtrip/roadtrip-hazards.service.ts | 150 +- .../src/nest/roadtrip/roadtrip-mcp.helpers.ts | 6 +- .../nest/roadtrip/roadtrip-plan.service.ts | 89 +- .../nest/roadtrip/roadtrip-planning.mcp.ts | 18 +- .../roadtrip-preferences.controller.ts | 6 +- .../nest/roadtrip/roadtrip-preferences.mcp.ts | 11 +- .../roadtrip/roadtrip-preferences.service.ts | 8 +- .../nest/roadtrip/roadtrip-router.service.ts | 6 +- .../roadtrip/roadtrip-search.controller.ts | 9 +- .../nest/roadtrip/roadtrip-search.service.ts | 61 +- .../src/nest/roadtrip/roadtrip.controller.ts | 57 +- server/src/nest/roadtrip/roadtrip.dto.ts | 8 +- server/src/nest/roadtrip/roadtrip.mcp.ts | 156 +- server/src/nest/roadtrip/roadtrip.module.ts | 103 +- server/src/nest/roadtrip/roadtrip.service.ts | 42 +- .../route-usage/route-usage.controller.ts | 4 +- .../src/nest/route-usage/route-usage.dto.ts | 3 +- .../src/nest/route-usage/route-usage.job.ts | 2 +- .../nest/route-usage/route-usage.module.ts | 8 +- .../nest/route-usage/route-usage.service.ts | 14 +- .../nest/scheduling/cron-registrar.service.ts | 17 +- .../src/nest/scheduling/scheduling.module.ts | 4 +- .../school-holidays.controller.ts | 49 +- .../school-holidays/school-holidays.mcp.ts | 103 +- .../school-holidays/school-holidays.module.ts | 20 +- .../school-holidays.service.ts | 36 +- server/src/nest/sessions/session-purge.job.ts | 2 +- server/src/nest/sessions/sessions.module.ts | 4 +- server/src/nest/sessions/sessions.service.ts | 23 +- server/src/nest/settings/instance-api-keys.ts | 13 +- .../src/nest/settings/settings.controller.ts | 30 +- server/src/nest/settings/settings.dto.ts | 7 +- server/src/nest/settings/settings.mcp.ts | 54 +- server/src/nest/settings/settings.module.ts | 14 +- server/src/nest/settings/settings.service.ts | 31 +- server/src/nest/share/share-view.helpers.ts | 15 +- server/src/nest/share/share.controller.ts | 23 +- server/src/nest/share/share.dto.ts | 3 +- server/src/nest/share/share.mcp.ts | 66 +- server/src/nest/share/share.module.ts | 57 +- server/src/nest/share/share.service.ts | 176 +- server/src/nest/storage/content-type.ts | 38 +- .../src/nest/storage/drivers/local.driver.ts | 16 +- .../src/nest/storage/drivers/mirror.driver.ts | 13 +- server/src/nest/storage/drivers/s3.driver.ts | 18 +- server/src/nest/storage/http-serving.ts | 4 +- .../nest/storage/storage-admin.controller.ts | 53 +- server/src/nest/storage/storage-admin.dto.ts | 3 +- .../src/nest/storage/storage-admin.service.ts | 37 +- .../nest/storage/storage-events.service.ts | 2 +- .../src/nest/storage/storage-jobs.service.ts | 21 +- server/src/nest/storage/storage-paths.ts | 3 +- server/src/nest/storage/storage-probe.ts | 7 +- .../nest/storage/storage-registry.service.ts | 56 +- server/src/nest/storage/storage-secrets.ts | 4 +- .../src/nest/storage/storage-stats.service.ts | 6 +- .../nest/storage/storage-upload.factory.ts | 16 +- .../nest/storage/storage-usage-scan.job.ts | 2 +- server/src/nest/storage/storage.module.ts | 12 +- server/src/nest/storage/storage.service.ts | 15 +- server/src/nest/storage/storage.types.ts | 8 +- .../system-notices.controller.ts | 12 +- .../system-notices/system-notices.module.ts | 22 +- .../system-notices/system-notices.service.ts | 73 +- server/src/nest/tags/tags.controller.ts | 16 +- server/src/nest/tags/tags.mcp.ts | 18 +- server/src/nest/tags/tags.module.ts | 6 +- server/src/nest/tags/tags.rpc.ts | 4 +- server/src/nest/tags/tags.service.ts | 6 +- server/src/nest/todo/todo.controller.ts | 50 +- server/src/nest/todo/todo.dto.ts | 3 +- server/src/nest/todo/todo.mcp.ts | 143 +- server/src/nest/todo/todo.module.ts | 34 +- server/src/nest/todo/todo.rpc.ts | 4 +- server/src/nest/todo/todo.service.ts | 74 +- server/src/nest/tokens/token.service.ts | 41 +- server/src/nest/tokens/tokens.module.ts | 6 +- server/src/nest/tours/dto/tour-create.dto.ts | 5 +- .../src/nest/tours/tours-import.controller.ts | 18 +- server/src/nest/tours/tours.controller.ts | 10 +- server/src/nest/tours/tours.module.ts | 20 +- server/src/nest/tours/tours.service.ts | 90 +- .../nest/transit/google-transit.provider.ts | 62 +- .../nest/transit/transit-itinerary.helpers.ts | 2 +- server/src/nest/transit/transit.controller.ts | 35 +- server/src/nest/transit/transit.mcp.ts | 64 +- server/src/nest/transit/transit.module.ts | 35 +- server/src/nest/transit/transit.service.ts | 13 +- .../trip-invite/trip-invite.controller.ts | 54 +- .../src/nest/trip-invite/trip-invite.dto.ts | 3 +- .../src/nest/trip-invite/trip-invite.mcp.ts | 49 +- .../nest/trip-invite/trip-invite.module.ts | 32 +- .../nest/trip-invite/trip-invite.service.ts | 36 +- .../trip-members/trip-members.controller.ts | 45 +- .../nest/trip-members/trip-members.module.ts | 25 +- .../nest/trip-members/trip-members.service.ts | 73 +- .../trip-membership/trip-membership.module.ts | 10 +- .../trip-membership.service.ts | 8 +- .../trip-read-model/trip-read-model.module.ts | 29 +- .../trip-read-model.service.ts | 22 +- server/src/nest/trips/trip-prompts.mcp.ts | 54 +- server/src/nest/trips/trips.controller.ts | 148 +- server/src/nest/trips/trips.dto.ts | 3 +- server/src/nest/trips/trips.mcp.ts | 339 ++- server/src/nest/trips/trips.module.ts | 77 +- server/src/nest/trips/trips.rpc.ts | 45 +- server/src/nest/unsplash/unsplash.module.ts | 10 +- server/src/nest/unsplash/unsplash.service.ts | 219 +- server/src/nest/vacay/vacay.controller.ts | 107 +- server/src/nest/vacay/vacay.dto.ts | 3 +- server/src/nest/vacay/vacay.mcp.ts | 207 +- server/src/nest/vacay/vacay.module.ts | 52 +- server/src/nest/vacay/vacay.rpc.ts | 21 +- server/src/nest/weather/weather.controller.ts | 6 +- server/src/nest/weather/weather.impl.ts | 172 +- server/src/nest/weather/weather.mcp.ts | 23 +- server/src/nest/weather/weather.module.ts | 6 +- server/src/nest/weather/weather.service.ts | 2 +- server/src/plugin-event-sink.ts | 93 +- server/src/plugin-mcp-tools.ts | 1 - server/src/shutdown.ts | 15 +- server/src/systemNotices/conditions.ts | 10 +- server/src/systemNotices/registry.ts | 4 +- server/src/systemNotices/service.ts | 13 +- server/src/systemNotices/types.ts | 13 +- server/src/types.ts | 4 +- server/src/typings/express.d.ts | 3 +- server/src/utils/cappedFetch.ts | 12 +- server/src/utils/ipv6.ts | 3 +- server/src/utils/ssrfGuard.ts | 49 +- server/tests/e2e/addons.e2e.test.ts | 77 +- server/tests/e2e/admin.e2e.test.ts | 148 +- server/tests/e2e/airports.e2e.test.ts | 28 +- server/tests/e2e/assignments.e2e.test.ts | 247 +- server/tests/e2e/atlas.e2e.test.ts | 137 +- server/tests/e2e/auth.e2e.test.ts | 90 +- server/tests/e2e/backup.e2e.test.ts | 46 +- server/tests/e2e/booking-import.e2e.test.ts | 69 +- server/tests/e2e/budget.e2e.test.ts | 206 +- .../e2e/calendar-feed-visibility.e2e.test.ts | 28 +- server/tests/e2e/categories.e2e.test.ts | 60 +- server/tests/e2e/collab.e2e.test.ts | 152 +- server/tests/e2e/collections.e2e.test.ts | 521 +++- server/tests/e2e/config.e2e.test.ts | 13 +- server/tests/e2e/dawarich.e2e.test.ts | 96 +- server/tests/e2e/days.e2e.test.ts | 194 +- server/tests/e2e/doc-sync.e2e.test.ts | 114 +- server/tests/e2e/feeds.e2e.test.ts | 47 +- server/tests/e2e/files.e2e.test.ts | 64 +- server/tests/e2e/google-quota.e2e.test.ts | 58 +- server/tests/e2e/harness.ts | 13 +- server/tests/e2e/journey.e2e.test.ts | 134 +- server/tests/e2e/mailer.e2e.test.ts | 42 +- server/tests/e2e/maps.e2e.test.ts | 55 +- server/tests/e2e/memories.e2e.test.ts | 294 +- server/tests/e2e/notifications.e2e.test.ts | 78 +- server/tests/e2e/oauth.e2e.test.ts | 72 +- server/tests/e2e/oidc.e2e.test.ts | 50 +- server/tests/e2e/packing.e2e.test.ts | 278 +- server/tests/e2e/place-enrichment.e2e.test.ts | 52 +- server/tests/e2e/place-shadow.e2e.test.ts | 41 +- server/tests/e2e/places.e2e.test.ts | 209 +- server/tests/e2e/plugin-pois.e2e.test.ts | 62 +- server/tests/e2e/plugin-search.e2e.test.ts | 65 +- server/tests/e2e/public-api.e2e.test.ts | 138 +- server/tests/e2e/push.e2e.test.ts | 55 +- server/tests/e2e/receipt-scan.e2e.test.ts | 140 +- server/tests/e2e/reservations.e2e.test.ts | 116 +- server/tests/e2e/roadtrip.e2e.test.ts | 158 +- server/tests/e2e/school-holidays.e2e.test.ts | 355 ++- .../tests/e2e/session-revocation.e2e.test.ts | 39 +- server/tests/e2e/sessions.e2e.test.ts | 160 +- server/tests/e2e/settings.e2e.test.ts | 153 +- server/tests/e2e/share.e2e.test.ts | 98 +- server/tests/e2e/spa-fallback.e2e.test.ts | 15 +- server/tests/e2e/storage-admin.e2e.test.ts | 84 +- server/tests/e2e/system-notices.e2e.test.ts | 39 +- server/tests/e2e/tags.e2e.test.ts | 37 +- server/tests/e2e/todo.e2e.test.ts | 102 +- server/tests/e2e/tours.e2e.test.ts | 241 +- server/tests/e2e/transit-provider.e2e.test.ts | 46 +- server/tests/e2e/transit.e2e.test.ts | 56 +- server/tests/e2e/trip-invite.e2e.test.ts | 58 +- server/tests/e2e/trips.e2e.test.ts | 158 +- server/tests/e2e/vacay.e2e.test.ts | 57 +- server/tests/e2e/weather.e2e.test.ts | 33 +- .../tests/helpers/accommodations-service.ts | 71 +- server/tests/helpers/atlas-repos.ts | 19 +- server/tests/helpers/auth.ts | 8 +- server/tests/helpers/budget-repos.ts | 48 +- server/tests/helpers/collab-repos.ts | 23 +- server/tests/helpers/dawarich-repos.ts | 9 +- server/tests/helpers/db-mock.ts | 32 +- server/tests/helpers/doc-sync-repos.ts | 23 +- server/tests/helpers/exif-jpeg.ts | 13 +- server/tests/helpers/factories.ts | 439 +-- server/tests/helpers/factories/atlas.ts | 2 +- server/tests/helpers/factories/budget.ts | 2 +- server/tests/helpers/factories/collab.ts | 2 +- server/tests/helpers/factories/collections.ts | 2 +- server/tests/helpers/factories/context.ts | 2 +- server/tests/helpers/factories/files.ts | 2 +- server/tests/helpers/factories/itinerary.ts | 2 +- server/tests/helpers/factories/journeys.ts | 9 +- .../tests/helpers/factories/notifications.ts | 2 +- server/tests/helpers/factories/packing.ts | 2 +- server/tests/helpers/factories/photos.ts | 2 +- server/tests/helpers/factories/places.ts | 8 +- server/tests/helpers/factories/plugins.ts | 9 +- .../tests/helpers/factories/reservations.ts | 2 +- server/tests/helpers/factories/rows.ts | 4 +- server/tests/helpers/factories/todos.ts | 2 +- server/tests/helpers/factories/tokens.ts | 5 +- server/tests/helpers/factories/tours.ts | 10 +- server/tests/helpers/factories/trips.ts | 2 +- server/tests/helpers/factories/users.ts | 12 +- server/tests/helpers/factories/vacay.ts | 2 +- server/tests/helpers/files-repos.ts | 13 +- server/tests/helpers/journey-repos.ts | 21 +- server/tests/helpers/journey-share-repos.ts | 9 +- server/tests/helpers/mcp-harness.ts | 48 +- server/tests/helpers/migration-step.ts | 10 +- server/tests/helpers/notifications-repos.ts | 15 +- server/tests/helpers/notifications.ts | 35 +- server/tests/helpers/packing-repos.ts | 35 +- server/tests/helpers/plugin-host.ts | 632 ++-- server/tests/helpers/rpc-host-deps.ts | 67 +- server/tests/helpers/school-holidays-repos.ts | 13 +- server/tests/helpers/sessions.ts | 14 +- server/tests/helpers/share-repos.ts | 69 +- server/tests/helpers/storage-fixture.ts | 7 +- server/tests/helpers/test-addons.ts | 13 +- server/tests/helpers/test-db.ts | 109 +- server/tests/helpers/test-orm.ts | 11 +- server/tests/helpers/test-uow.ts | 166 +- server/tests/helpers/todo-repos.ts | 9 +- server/tests/helpers/tours-repos.ts | 18 +- server/tests/helpers/vacay-repos.ts | 33 +- server/tests/helpers/ws-client.ts | 31 +- server/tests/integration/admin.test.ts | 244 +- .../airtrail-sync-request-context.test.ts | 43 +- server/tests/integration/api-docs.test.ts | 14 +- server/tests/integration/app-config.test.ts | 14 +- server/tests/integration/assignments.test.ts | 52 +- .../async-closure-truthiness-guards.test.ts | 20 +- .../atlas-geocode-request-context.test.ts | 33 +- server/tests/integration/atlas.test.ts | 138 +- server/tests/integration/auth.test.ts | 226 +- server/tests/integration/backup.test.ts | 109 +- .../tests/integration/book-body-size.test.ts | 11 +- .../boot-sweeps-request-context.test.ts | 28 +- server/tests/integration/bootstrap.test.ts | 34 +- server/tests/integration/budget.test.ts | 75 +- server/tests/integration/categories.test.ts | 53 +- server/tests/integration/collab.test.ts | 62 +- server/tests/integration/collections.test.ts | 40 +- .../integration/database-lifecycle.test.ts | 11 +- server/tests/integration/dayNotes.test.ts | 22 +- server/tests/integration/dayReorder.test.ts | 79 +- server/tests/integration/days.test.ts | 153 +- .../demo-reset-connection-swap.spike.test.ts | 15 +- .../demo-seed-request-context.test.ts | 12 +- .../doc-sync-webhook-request-context.test.ts | 76 +- .../integration/feeds-request-context.test.ts | 35 +- server/tests/integration/files.test.ts | 82 +- server/tests/integration/help.test.ts | 17 +- server/tests/integration/immich.test.ts | 40 +- .../integration/journey-place-order.test.ts | 65 +- server/tests/integration/journey.test.ts | 233 +- .../integration/leg-mode-incoming.test.ts | 34 +- server/tests/integration/maps.test.ts | 89 +- server/tests/integration/mcp.test.ts | 177 +- .../tests/integration/memories-immich.test.ts | 394 ++- .../integration/memories-synology.test.ts | 644 ++-- .../integration/memories-unified.test.ts | 61 +- server/tests/integration/misc.test.ts | 26 +- .../tests/integration/notifications.test.ts | 156 +- .../tests/integration/oauth-authorize.test.ts | 449 +-- server/tests/integration/oauth.test.ts | 2686 ++++++++--------- .../integration/oidc-local-provider.test.ts | 40 +- server/tests/integration/oidc.test.ts | 108 +- .../orm-request-context-seams.test.ts | 34 +- server/tests/integration/packing.test.ts | 122 +- server/tests/integration/passkey.test.ts | 47 +- server/tests/integration/places.test.ts | 193 +- .../plugins/boot-registry-order.test.ts | 114 +- .../integration/plugins/dev-link.test.ts | 47 +- .../plugin-runtime.task2-conversion.test.ts | 280 +- .../plugins/plugin-runtime.test.ts | 325 +- .../plugins/plugin-scheduler-sweep.test.ts | 129 +- .../integration/plugins/registry.test.ts | 710 ++++- .../integration/plugins/supervisor.test.ts | 46 +- server/tests/integration/profile.test.ts | 82 +- server/tests/integration/reservations.test.ts | 80 +- .../tests/integration/scheduler-gate.test.ts | 12 +- server/tests/integration/security.test.ts | 58 +- server/tests/integration/settings.test.ts | 44 +- server/tests/integration/share.test.ts | 220 +- ...ge-health-notifier-request-context.test.ts | 10 +- .../storage-restore-reload.test.ts | 16 +- .../integration/storage-stream-abort.test.ts | 22 +- .../tests/integration/systemNotices.test.ts | 147 +- server/tests/integration/tags.test.ts | 62 +- server/tests/integration/todo.test.ts | 63 +- .../trip-access-primitives.test.ts | 62 +- server/tests/integration/trips.test.ts | 249 +- .../uploads-photo-request-context.test.ts | 38 +- .../tests/integration/uploads-static.test.ts | 19 +- server/tests/integration/vacay.test.ts | 125 +- server/tests/setup.console-noise.ts | 3 +- server/tests/setup.ts | 3 +- server/tests/unit/app-config/app-url.test.ts | 4 +- .../unit/app-config/config-ownership.test.ts | 9 +- .../unit/app-config/config-templates.test.ts | 20 +- .../tests/unit/app-config/data-paths.test.ts | 12 +- server/tests/unit/app-config/derive.test.ts | 31 +- .../unit/app-config/env-reference.test.ts | 5 +- .../unit/app-config/image-version.test.ts | 5 +- server/tests/unit/app-config/parsers.test.ts | 35 +- server/tests/unit/app-config/validate.test.ts | 30 +- server/tests/unit/auto-backup.test.ts | 89 +- server/tests/unit/budget/costs.rpc.test.ts | 192 +- server/tests/unit/config.test.ts | 8 +- server/tests/unit/days/day-notes.rpc.test.ts | 69 +- .../unit/days/days-itinerary.rpc.test.ts | 154 +- .../unit/db/atlas-region-crosswalk.test.ts | 40 +- .../tests/unit/db/booked-night-reseat.test.ts | 108 +- .../db/budget-ticket-json-migration.test.ts | 40 +- server/tests/unit/db/connection.test.ts | 20 +- .../tests/unit/db/database-lazy-open.test.ts | 11 +- .../db/dialect/sql-functions.postgres.test.ts | 116 +- .../unit/db/dialect/sql-functions.test.ts | 430 ++- server/tests/unit/db/durability.test.ts | 24 +- server/tests/unit/db/email-case-index.test.ts | 10 +- server/tests/unit/db/entities-index.test.ts | 18 +- .../entities/primary-relations.spike.test.ts | 15 +- .../tests/unit/db/entity-class-fields.test.ts | 13 +- .../unit/db/entity-schema-parity.test.ts | 48 +- .../tests/unit/db/generate-entities.test.ts | 658 ++-- .../unit/db/instance-api-key-backfill.test.ts | 5 +- server/tests/unit/db/known-migrations.test.ts | 12 +- server/tests/unit/db/legacy-baseline.test.ts | 28 +- .../unit/db/migrate-encryption-parity.test.ts | 110 +- server/tests/unit/db/migration.test.ts | 82 +- .../unit/db/nul-safe-sqlite-platform.test.ts | 31 +- .../db/repositories/Addons.repository.test.ts | 46 +- .../AppSettings.repository.test.ts | 48 +- .../AssignmentParticipants.repository.test.ts | 67 +- .../repositories/AuditLog.repository.test.ts | 54 +- .../BucketList.repository.test.ts | 70 +- .../BudgetItems.repository.test.ts | 62 +- .../Categories.repository.test.ts | 42 +- .../CollabMessages.repository.test.ts | 37 +- .../DawarichConnections.repository.test.ts | 138 +- ...awarichVisitSuggestions.repository.test.ts | 49 +- .../DayAccommodations.repository.test.ts | 84 +- .../DayAssignments.repository.test.ts | 487 ++- .../repositories/DayNotes.repository.test.ts | 136 +- .../db/repositories/Days.repository.test.ts | 85 +- .../db/repositories/DemoRepository.test.ts | 196 +- .../DocumentProviders.repository.test.ts | 43 +- .../DocumentSyncItems.repository.test.ts | 251 +- .../GooglePlacePhotoMeta.repository.test.ts | 40 +- .../HiddenRegions.repository.test.ts | 21 +- .../IdempotencyKeys.repository.test.ts | 113 +- .../InviteTokens.repository.test.ts | 41 +- .../MaintenanceRepository.test.ts | 18 +- .../repositories/McpTokens.repository.test.ts | 17 +- ...ationChannelPreferences.repository.test.ts | 106 +- .../Notifications.repository.test.ts | 156 +- .../OauthClients.repository.test.ts | 71 +- .../OauthConsents.repository.test.ts | 35 +- .../OauthTokens.repository.test.ts | 195 +- .../PackingItems.repository.test.ts | 71 +- .../PasswordResetTokens.repository.test.ts | 52 +- .../PhotoProviderFields.repository.test.ts | 89 +- .../PhotoProviders.repository.test.ts | 47 +- .../PlaceDetailsCache.repository.test.ts | 60 +- .../PlaceRatings.repository.test.ts | 44 +- .../PlaceRegions.repository.test.ts | 80 +- .../PlaceShadowPicks.repository.test.ts | 39 +- .../PluginOauthTokens.repository.test.ts | 98 +- ...ReservationDayPositions.repository.test.ts | 34 +- .../ReservationEndpoints.repository.test.ts | 63 +- .../ReservationTravelers.repository.test.ts | 43 +- .../Reservations.repository.test.ts | 306 +- .../RoadtripDayBoundaries.repository.test.ts | 124 +- .../RoadtripDayTracks.repository.test.ts | 48 +- .../RoadtripPreferences.repository.test.ts | 21 +- .../RoadtripVias.repository.test.ts | 31 +- .../SchedulerLeases.repository.test.ts | 9 +- .../repositories/Settings.repository.test.ts | 76 +- .../ShareTokens.repository.test.ts | 110 +- .../db/repositories/Tags.repository.test.ts | 70 +- .../repositories/TodoItems.repository.test.ts | 13 +- .../TourWaypoints.repository.test.ts | 28 +- .../db/repositories/Tours.repository.test.ts | 91 +- .../TripInviteTokens.repository.test.ts | 60 +- .../TripMembers.repository.test.ts | 107 +- .../db/repositories/Trips.repository.test.ts | 283 +- .../UserNoticeDismissals.repository.test.ts | 103 +- .../UserSessions.repository.test.ts | 73 +- .../db/repositories/Users.repository.test.ts | 324 +- .../VacayCompanyHolidays.repository.test.ts | 25 +- .../VacayHolidayCalendars.repository.test.ts | 30 +- .../VacayPlanMembers.repository.test.ts | 55 +- .../VacayPlans.repository.test.ts | 35 +- .../VacayShares.repository.test.ts | 21 +- .../VacayUserSettings.repository.test.ts | 29 +- .../VacayUserYears.repository.test.ts | 39 +- .../VacayYears.repository.test.ts | 23 +- .../VisitedCountries.repository.test.ts | 29 +- .../VisitedRegions.repository.test.ts | 82 +- .../WebauthnChallenges.repository.test.ts | 46 +- .../WebauthnCredentials.repository.test.ts | 62 +- .../atlas-travowns-consumers.parity.test.ts | 47 +- .../_shared/collection-role.parity.test.ts | 41 +- .../repositories/_shared/owned-lookup.test.ts | 23 +- .../_shared/packing-visibility.parity.test.ts | 53 +- .../reservation-travelers-owns.parity.test.ts | 29 +- .../_shared/reservation-visibility.test.ts | 102 +- .../addons-config-encryption.test.ts | 13 +- .../atlas-branch-coverage.test.ts | 24 +- .../bucket-list-duplicate.parity.test.ts | 169 +- .../identity-map-writeback.test.ts | 23 +- .../db/repositories/trek-repository.test.ts | 86 +- .../vacay-branch-coverage.test.ts | 31 +- ...reservation-ingest-state-migration.test.ts | 19 +- .../unit/db/reset-admin-sessions.test.ts | 8 +- .../db/roadtrip-preferences-migration.test.ts | 29 +- .../unit/db/seeders-insert-ignore.test.ts | 4 +- server/tests/unit/db/seeds-admin.test.ts | 4 +- server/tests/unit/db/test-orm.test.ts | 21 +- .../unit/db/tour-types-migration.test.ts | 26 +- server/tests/unit/db/tours-migration.test.ts | 130 +- .../tests/unit/db/transaction-yield.test.ts | 28 +- .../tests/unit/db/trimUserWhitespace.test.ts | 11 +- server/tests/unit/db/trip-access.test.ts | 21 +- .../db/trip-days-backfill-migration.test.ts | 28 +- .../db/trip-files-prefix-migration.test.ts | 8 +- .../unit/db/types/db-timestamp.type.test.ts | 17 +- .../unit/db/user-sessions-migration.test.ts | 21 +- .../tests/unit/demo/demo-reset-path.test.ts | 44 +- .../unit/demo/demo-reset-sessions.test.ts | 27 +- server/tests/unit/demo/demo-seed.test.ts | 65 +- server/tests/unit/docker-entrypoint.test.ts | 2 +- .../restricted-syntax-selectors.test.ts | 50 +- server/tests/unit/files/files.rpc.test.ts | 281 +- .../tests/unit/helpers/orm-factories.test.ts | 91 +- server/tests/unit/helpers/test-uow.test.ts | 11 +- server/tests/unit/idempotency-cleanup.test.ts | 30 +- server/tests/unit/mcp/nest-mcp-policy.test.ts | 9 +- server/tests/unit/mcp/scopes.test.ts | 3 +- server/tests/unit/mcp/sessionManager.test.ts | 36 +- .../tests/unit/mcp/tools-addon-gating.test.ts | 116 +- server/tests/unit/mcp/tools-airtrail.test.ts | 181 +- ...ols-assignments-reservations-extra.test.ts | 46 +- .../tests/unit/mcp/tools-assignments.test.ts | 257 +- .../unit/mcp/tools-atlas-expanded.test.ts | 102 +- server/tests/unit/mcp/tools-atlas.test.ts | 36 +- .../unit/mcp/tools-budget-advanced.test.ts | 143 +- server/tests/unit/mcp/tools-budget.test.ts | 371 ++- .../tests/unit/mcp/tools-categories.test.ts | 147 +- .../unit/mcp/tools-collab-polls-chat.test.ts | 89 +- .../tests/unit/mcp/tools-collections.test.ts | 372 ++- .../mcp/tools-days-accommodations.test.ts | 185 +- server/tests/unit/mcp/tools-days.test.ts | 91 +- .../tests/unit/mcp/tools-feeds-invite.test.ts | 145 +- server/tests/unit/mcp/tools-files.test.ts | 299 +- .../tests/unit/mcp/tools-help-addons.test.ts | 92 +- server/tests/unit/mcp/tools-journey.test.ts | 651 ++-- .../unit/mcp/tools-leg-transport-mode.test.ts | 45 +- server/tests/unit/mcp/tools-memories.test.ts | 194 +- server/tests/unit/mcp/tools-notes.test.ts | 144 +- .../unit/mcp/tools-notifications.test.ts | 30 +- .../unit/mcp/tools-packing-advanced.test.ts | 104 +- server/tests/unit/mcp/tools-packing.test.ts | 162 +- server/tests/unit/mcp/tools-places.test.ts | 381 ++- .../tests/unit/mcp/tools-plugin-pois.test.ts | 85 +- .../unit/mcp/tools-plugin-search.test.ts | 22 +- server/tests/unit/mcp/tools-prompts.test.ts | 218 +- .../tests/unit/mcp/tools-reservations.test.ts | 240 +- server/tests/unit/mcp/tools-roadtrip.test.ts | 146 +- server/tests/unit/mcp/tools-settings.test.ts | 66 +- server/tests/unit/mcp/tools-share.test.ts | 75 +- .../unit/mcp/tools-tags-maps-weather.test.ts | 71 +- server/tests/unit/mcp/tools-todos.test.ts | 66 +- server/tests/unit/mcp/tools-transit.test.ts | 26 +- .../tests/unit/mcp/tools-transports.test.ts | 571 ++-- .../tests/unit/mcp/tools-trip-members.test.ts | 120 +- .../unit/mcp/tools-trip-warnings.test.ts | 54 +- server/tests/unit/mcp/tools-trips.test.ts | 222 +- server/tests/unit/mcp/tools-vacay.test.ts | 160 +- .../globalMiddleware.correlation.test.ts | 20 +- .../middleware/globalMiddleware.cors.test.ts | 9 +- .../middleware/globalMiddleware.csp.test.ts | 33 +- .../globalMiddleware.redaction.test.ts | 10 +- server/tests/unit/nest-mcp/attach.test.ts | 21 +- .../nest-mcp/create-test-registry.test.ts | 5 +- .../tests/unit/nest-mcp/dynamic-tools.test.ts | 208 +- .../nest/accommodations.controller.test.ts | 127 +- .../unit/nest/accommodations.service.test.ts | 812 +++-- server/tests/unit/nest/addon-guard.test.ts | 36 +- .../tests/unit/nest/addons.controller.test.ts | 3 +- server/tests/unit/nest/addons.service.test.ts | 96 +- ...n-default-user-settings.controller.test.ts | 23 +- server/tests/unit/nest/admin-jobs.test.ts | 58 +- ...otification-preferences.controller.test.ts | 13 +- ...admin-packing-templates.controller.test.ts | 24 +- .../tests/unit/nest/admin.controller.test.ts | 450 ++- server/tests/unit/nest/admin.service.test.ts | 464 +-- .../nest/admin.version-notification.test.ts | 234 +- .../unit/nest/airports.controller.test.ts | 15 +- .../unit/nest/airports/airports.data.test.ts | 50 +- .../nest/airports/airports.service.test.ts | 63 +- .../tests/unit/nest/airtrail-sync.job.test.ts | 26 +- .../tests/unit/nest/airtrail.service.test.ts | 35 +- server/tests/unit/nest/app-clock.test.ts | 3 +- server/tests/unit/nest/app-config.test.ts | 35 +- .../unit/nest/assignments.controller.test.ts | 307 +- .../unit/nest/assignments.service.test.ts | 294 +- .../nest/atlas-transfer-endpoints.test.ts | 9 +- .../tests/unit/nest/atlas-visit-dates.test.ts | 4 +- .../tests/unit/nest/atlas.controller.test.ts | 97 +- server/tests/unit/nest/atlasBundle.test.ts | 29 +- .../tests/unit/nest/audit-log.logger.test.ts | 45 +- server/tests/unit/nest/audit.service.test.ts | 68 +- .../tests/unit/nest/auth.controller.test.ts | 651 +++- server/tests/unit/nest/auth.helpers.test.ts | 16 +- server/tests/unit/nest/auth.service.test.ts | 565 +++- .../nest/auth/sessions.controller.test.ts | 47 +- .../unit/nest/auto-backup-settings.test.ts | 59 +- .../unit/nest/backup-archive-version.test.ts | 9 +- .../tests/unit/nest/backup.controller.test.ts | 259 +- server/tests/unit/nest/backup.impl.test.ts | 396 ++- .../booking-import.service.test.ts | 219 +- .../import-jobs.service.test.ts | 31 +- .../kitinerary-extractor.service.test.ts | 68 +- .../booking-import/kitineraryMapper.test.ts | 143 +- .../reservation-import.controller.test.ts | 34 +- server/tests/unit/nest/boot-restore.test.ts | 46 +- .../tests/unit/nest/budget.controller.test.ts | 300 +- .../unit/nest/budget.service.calc.test.ts | 541 ++-- server/tests/unit/nest/budget.service.test.ts | 89 +- .../tests/unit/nest/cache-sweep-jobs.test.ts | 73 +- .../unit/nest/categories.controller.test.ts | 19 +- .../unit/nest/categories.service.test.ts | 13 +- .../tests/unit/nest/channel-override.test.ts | 143 +- .../tests/unit/nest/channel-registry.test.ts | 24 +- server/tests/unit/nest/charging.test.ts | 158 +- .../tests/unit/nest/city-from-address.test.ts | 11 +- .../tests/unit/nest/collab.controller.test.ts | 435 ++- server/tests/unit/nest/collab.service.test.ts | 347 ++- .../unit/nest/collections.controller.test.ts | 226 +- .../tests/unit/nest/common/avatarUrl.test.ts | 7 +- .../unit/nest/common/conflictUpdate.test.ts | 196 +- server/tests/unit/nest/common/cookie.test.ts | 6 +- .../nest/common/crypto/apiKeyCrypto.test.ts | 4 +- .../unit/nest/common/crypto/mfaCrypto.test.ts | 4 +- .../unit/nest/common/passwordPolicy.test.ts | 3 +- .../nest/common/request-correlation.test.ts | 10 +- server/tests/unit/nest/common/row-id.test.ts | 8 +- .../tests/unit/nest/common/rowShape.test.ts | 19 +- server/tests/unit/nest/compare.test.ts | 13 +- .../tests/unit/nest/config.controller.test.ts | 5 +- .../unit/nest/content-disposition.test.ts | 5 +- .../unit/nest/cron-registrar.service.test.ts | 119 +- .../database-lifecycle.service.test.ts | 6 +- .../nest/database/request-context.test.ts | 20 +- .../nest/database/unit-of-work.d5.test.ts | 23 +- .../unit/nest/database/unit-of-work.test.ts | 23 +- .../nest/dawarich-suggestions.service.test.ts | 106 +- .../tests/unit/nest/dawarich-sync.job.test.ts | 26 +- .../unit/nest/dawarich-sync.service.test.ts | 215 +- .../unit/nest/dawarich-tracks.service.test.ts | 39 +- .../tests/unit/nest/dawarich.client.test.ts | 41 +- .../unit/nest/dawarich.controller.test.ts | 291 +- .../tests/unit/nest/dawarich.helpers.test.ts | 407 ++- server/tests/unit/nest/dawarich.mcp.test.ts | 117 +- .../tests/unit/nest/dawarich.service.test.ts | 67 +- server/tests/unit/nest/day-boundaries.test.ts | 52 +- .../tests/unit/nest/day-notes.service.test.ts | 105 +- .../unit/nest/day-removal.service.test.ts | 251 +- .../tests/unit/nest/days.controller.test.ts | 246 +- server/tests/unit/nest/days.service.test.ts | 397 ++- .../nest/demo-service-survivor-sites.test.ts | 32 +- server/tests/unit/nest/demo-write.test.ts | 8 +- server/tests/unit/nest/demo.service.test.ts | 11 +- .../unit/nest/discovery.controller.test.ts | 89 +- .../doc-sync/doc-sync-config.service.test.ts | 171 +- .../doc-sync-webhook.controller.test.ts | 51 +- .../nest/doc-sync/doc-sync.controller.test.ts | 468 ++- .../nest/doc-sync/doc-sync.helpers.test.ts | 246 +- .../unit/nest/doc-sync/doc-sync.job.test.ts | 97 +- .../unit/nest/doc-sync/doc-sync.mcp.test.ts | 30 +- .../nest/doc-sync/paperless.provider.test.ts | 384 ++- .../unit/nest/doc-sync/papra.provider.test.ts | 12 +- .../unit/nest/doc-sync/provider-http.test.ts | 33 +- .../doc-sync/synology-drive.provider.test.ts | 135 +- .../nest/doc-sync/webdav.provider.test.ts | 118 +- .../unit/nest/entry-trace.logger.test.ts | 6 +- .../tests/unit/nest/exception-filter.test.ts | 15 +- .../unit/nest/exchange-rates.service.test.ts | 25 +- .../unit/nest/features.controller.test.ts | 32 +- server/tests/unit/nest/feeds.service.test.ts | 29 +- .../tests/unit/nest/files.controller.test.ts | 338 ++- server/tests/unit/nest/files.service.test.ts | 242 +- .../unit/nest/files/upload-filters.test.ts | 15 +- server/tests/unit/nest/geo.nominatim.test.ts | 10 +- .../unit/nest/google-quota.service.test.ts | 20 +- server/tests/unit/nest/google-route.test.ts | 85 +- .../unit/nest/google-transit.provider.test.ts | 60 +- .../unit/nest/idempotency.interceptor.test.ts | 168 +- server/tests/unit/nest/immich.service.test.ts | 431 ++- .../nest/integrations/airtrailImport.test.ts | 141 +- .../nest/integrations/airtrailMapper.test.ts | 147 +- .../nest/integrations/airtrailSync.test.ts | 60 +- .../airtrailTestConnection.test.ts | 40 +- .../integrations/airtrailWriteGate.test.ts | 51 +- .../integrations/airtrailWriteback.test.ts | 35 +- .../unit/nest/journey-book.service.test.ts | 105 +- .../journey-photo-capture.service.test.ts | 95 +- .../unit/nest/journey-share.service.test.ts | 306 +- .../unit/nest/journey-stats-trips.test.ts | 25 +- .../unit/nest/journey-stats.service.test.ts | 198 +- server/tests/unit/nest/journey-stats.test.ts | 58 +- .../unit/nest/journey.controller.test.ts | 974 +++++- .../tests/unit/nest/llm-parse/clients.test.ts | 280 +- .../unit/nest/llm-parse/currency-code.test.ts | 3 +- .../nest/llm-parse/extraction-router.test.ts | 102 +- .../unit/nest/llm-parse/image-input.test.ts | 47 +- .../unit/nest/llm-parse/lenient-json.test.ts | 9 +- .../llm-capabilities.controller.test.ts | 7 +- .../nest/llm-parse/llm-client.factory.test.ts | 9 +- .../llm-parse/llm-config.resolver.test.ts | 36 +- .../unit/nest/llm-parse/llm-config.test.ts | 3 +- .../nest/llm-parse/llm-local.service.test.ts | 35 +- .../nest/llm-parse/llm-parse.service.test.ts | 129 +- .../unit/nest/llm-parse/llm-prompt.test.ts | 10 +- .../unit/nest/llm-parse/mime-email.test.ts | 4 +- .../unit/nest/llm-parse/nuextract.test.ts | 44 +- .../llm-parse/ollama-format.client.test.ts | 4 +- .../unit/nest/llm-parse/receipt-read.test.ts | 73 +- .../unit/nest/llm-parse/text-extract.test.ts | 4 +- server/tests/unit/nest/log-file.test.ts | 9 +- server/tests/unit/nest/mailer.service.test.ts | 39 +- server/tests/unit/nest/managed-ext.test.ts | 5 +- server/tests/unit/nest/managed-inert.test.ts | 11 +- server/tests/unit/nest/managed-keys.test.ts | 18 +- server/tests/unit/nest/managed.guard.test.ts | 18 +- server/tests/unit/nest/managed.test.ts | 12 +- server/tests/unit/nest/maps.amap.test.ts | 229 +- server/tests/unit/nest/maps.area.test.ts | 21 +- .../tests/unit/nest/maps.autocomplete.test.ts | 42 +- .../tests/unit/nest/maps.controller.test.ts | 146 +- .../unit/nest/maps.curated-images.test.ts | 8 +- server/tests/unit/nest/maps.geocode.test.ts | 21 +- .../tests/unit/nest/maps.gers-details.test.ts | 59 +- .../tests/unit/nest/maps.google-quota.test.ts | 33 +- server/tests/unit/nest/maps.helpers.test.ts | 21 +- server/tests/unit/nest/maps.identity.test.ts | 8 +- .../tests/unit/nest/maps.merge-search.test.ts | 7 +- server/tests/unit/nest/maps.nearby.test.ts | 150 +- server/tests/unit/nest/maps.pois.test.ts | 64 +- .../tests/unit/nest/maps.search-merge.test.ts | 36 +- server/tests/unit/nest/mcp-addon-gate.test.ts | 15 +- .../unit/nest/mcp-gated-addons.parity.test.ts | 13 +- .../unit/nest/mcp-shared.service.test.ts | 37 +- .../unit/nest/mcp-transport.service.test.ts | 89 +- .../unit/nest/memories-access.service.test.ts | 140 +- .../unit/nest/memories.controller.test.ts | 179 +- .../tests/unit/nest/memories.helpers.test.ts | 53 +- .../tests/unit/nest/memories.service.test.ts | 19 +- .../nest/memories/journey-thumbs.job.test.ts | 59 +- .../memories/trek-photo-cache.job.test.ts | 57 +- .../notification-preferences.service.test.ts | 68 +- .../unit/nest/notification-transports.test.ts | 120 +- .../nest/notifications.controller.test.ts | 89 +- .../nest/notifications.inapp-prefs.test.ts | 60 +- .../unit/nest/notifications.service.test.ts | 364 ++- .../nest/notifications/in-app-actions.test.ts | 4 +- .../push-subscriptions.service.test.ts | 44 +- .../notifications/push.controller.test.ts | 20 +- .../replica-failure-debouncer.test.ts | 3 +- .../storage-health-notifier.service.test.ts | 11 +- .../notifications/vapid-keys.service.test.ts | 38 +- .../notifications/web-push-crypto.test.ts | 7 +- .../notifications/web-push-dispatch.test.ts | 77 +- .../notifications/web-push.service.test.ts | 67 +- .../unit/nest/oauth-redirect-uri.test.ts | 14 +- .../unit/nest/oauth-sdk.provider.test.ts | 229 +- .../nest/oauth-token-retention.job.test.ts | 13 +- .../tests/unit/nest/oauth.controller.test.ts | 689 ++++- server/tests/unit/nest/oauth.service.test.ts | 289 +- server/tests/unit/nest/oidc-ssrf.test.ts | 7 +- .../tests/unit/nest/oidc.controller.test.ts | 412 ++- server/tests/unit/nest/oidc.service.test.ts | 498 +-- .../unit/nest/packing.controller.test.ts | 262 +- .../tests/unit/nest/packing.service.test.ts | 619 ++-- .../tests/unit/nest/passkey.service.test.ts | 598 ++-- .../unit/nest/permissions.service.test.ts | 40 +- .../nest/photo-capture-backfill.exif.test.ts | 32 +- .../photo-capture-backfill.service.test.ts | 194 +- server/tests/unit/nest/photo-provider.test.ts | 26 +- .../unit/nest/photo-resolver.service.test.ts | 91 +- .../unit/nest/photos/trekPhotoMedia.test.ts | 35 +- .../unit/nest/place-enrichment.brand.test.ts | 54 +- .../nest/place-enrichment.controller.test.ts | 15 +- .../nest/place-enrichment.description.test.ts | 108 +- .../nest/place-enrichment.service.test.ts | 124 +- .../place-photo-cache.job.test.ts | 43 +- .../place-photo-cache.service.test.ts | 51 +- .../unit/nest/place-regions-repair.test.ts | 86 +- .../unit/nest/place-shadow.controller.test.ts | 5 +- .../tests/unit/nest/place-shadow.job.test.ts | 22 +- .../unit/nest/place-shadow.service.test.ts | 44 +- .../tests/unit/nest/places.controller.test.ts | 689 ++++- .../tests/unit/nest/places.directions.test.ts | 202 +- .../tests/unit/nest/places.enrichment.test.ts | 4 +- server/tests/unit/nest/places.helpers.test.ts | 22 +- .../unit/nest/platform.controller.test.ts | 135 +- .../unit/nest/process-state-ports.test.ts | 21 +- .../tests/unit/nest/public-api-scopes.test.ts | 161 +- .../unit/nest/public-api.controller.test.ts | 21 +- .../tests/unit/nest/public-api.guard.test.ts | 9 +- .../unit/nest/public-api.service.test.ts | 43 +- .../unit/nest/public-stats.controller.test.ts | 41 +- .../unit/nest/query-helpers.service.test.ts | 51 +- .../unit/nest/rate-limit.service.test.ts | 3 +- .../tests/unit/nest/realtime.gateway.test.ts | 93 +- .../tests/unit/nest/realtime.service.test.ts | 9 +- .../unit/nest/receipt-scan.controller.test.ts | 41 +- .../nest/registration-invites.service.test.ts | 56 +- .../unit/nest/reminder-jobs.service.test.ts | 100 +- .../unit/nest/reservations.controller.test.ts | 178 +- .../tests/unit/nest/roadtrip-hazards.test.ts | 81 +- .../unit/nest/roadtrip-mcp.helpers.test.ts | 19 +- .../unit/nest/roadtrip-plan.service.test.ts | 209 +- .../tests/unit/nest/roadtrip-planning.test.ts | 190 +- .../unit/nest/roadtrip-read-tools.test.ts | 80 +- .../tests/unit/nest/roadtrip-router.test.ts | 44 +- .../tests/unit/nest/roadtrip-search.test.ts | 58 +- .../unit/nest/roadtrip.controller.test.ts | 59 +- .../tests/unit/nest/roadtrip.service.test.ts | 127 +- .../tests/unit/nest/route-inventory.test.ts | 39 +- .../unit/nest/route-usage.service.test.ts | 37 +- .../unit/nest/school-holidays.service.test.ts | 55 +- .../nest/session-renewal.interceptor.test.ts | 80 +- .../nest/sessions/session-purge.job.test.ts | 13 +- .../nest/sessions/sessions.service.test.ts | 92 +- .../unit/nest/settings.controller.test.ts | 66 +- .../tests/unit/nest/settings.service.test.ts | 32 +- .../nest/settings/instance-api-keys.test.ts | 26 +- .../unit/nest/share-view.helpers.test.ts | 25 +- .../tests/unit/nest/share.controller.test.ts | 123 +- server/tests/unit/nest/share.service.test.ts | 421 ++- .../tests/unit/nest/smtp-diagnostics.test.ts | 9 +- .../nest/spool-cleanup.interceptor.test.ts | 36 +- .../unit/nest/sqlite-database-backup.test.ts | 44 +- .../unit/nest/storage/content-type.test.ts | 3 +- .../unit/nest/storage/http-serving.test.ts | 8 +- .../unit/nest/storage/local.driver.test.ts | 9 +- .../unit/nest/storage/mirror-backfill.test.ts | 16 +- .../unit/nest/storage/mirror.driver.test.ts | 11 +- .../nest/storage/s3.driver.contract.test.ts | 39 +- .../tests/unit/nest/storage/s3.driver.test.ts | 65 +- .../storage/storage-admin.controller.test.ts | 30 +- .../storage/storage-admin.service.test.ts | 248 +- .../nest/storage/storage-driver.contract.ts | 9 +- .../nest/storage/storage-jobs.service.test.ts | 65 +- .../unit/nest/storage/storage-keys.test.ts | 7 +- .../unit/nest/storage/storage-probe.test.ts | 9 +- .../storage/storage-registry.service.test.ts | 131 +- .../unit/nest/storage/storage-secrets.test.ts | 15 +- .../storage/storage-stats.service.test.ts | 33 +- .../storage/storage-upload.factory.test.ts | 17 +- .../storage/storage-usage-scan.job.test.ts | 18 +- .../unit/nest/storage/storage.service.test.ts | 42 +- .../tests/unit/nest/strip-html-tags.test.ts | 3 +- .../tests/unit/nest/synology.service.test.ts | 170 +- .../system-notices.branch-coverage.test.ts | 191 +- .../nest/system-notices.controller.test.ts | 13 +- .../unit/nest/system-notices.service.test.ts | 118 +- .../tests/unit/nest/tags.controller.test.ts | 30 +- server/tests/unit/nest/tags.service.test.ts | 13 +- .../tests/unit/nest/thumbnail.service.test.ts | 24 +- .../unit/nest/to-client-user.parity.test.ts | 2 +- .../tests/unit/nest/todo.controller.test.ts | 28 +- server/tests/unit/nest/todo.service.test.ts | 91 +- server/tests/unit/nest/token.service.test.ts | 92 +- .../tests/unit/nest/tours.controller.test.ts | 72 +- .../tests/unit/nest/tours.gpx.atomic.test.ts | 258 +- ...s.permissions.boundaries.mock-only.test.ts | 156 +- .../tests/unit/nest/tours.permissions.test.ts | 91 +- .../nest/tours.service.permissions.test.ts | 127 +- server/tests/unit/nest/tours.service.test.ts | 151 +- .../nest/transit-itinerary.helpers.test.ts | 122 +- .../tests/unit/nest/transit.service.test.ts | 10 +- .../unit/nest/travel-stats.service.test.ts | 125 +- .../nest/trek-photo-cache.service.test.ts | 29 +- .../trek-photo-registration.service.test.ts | 67 +- .../unit/nest/trek-places.client.test.ts | 156 +- .../tests/unit/nest/trek-ws-adapter.test.ts | 94 +- .../tests/unit/nest/trip-access.guard.test.ts | 37 +- .../unit/nest/trip-invite.service.test.ts | 44 +- .../unit/nest/trip-members.controller.test.ts | 336 ++- .../unit/nest/trip-members.service.test.ts | 146 +- .../trip-membership.service.test.ts | 31 +- .../tests/unit/nest/trip-owner.guard.test.ts | 23 +- .../unit/nest/trip-read-model.service.test.ts | 400 ++- .../tests/unit/nest/trips.controller.test.ts | 377 ++- .../nest/unified-memories.service.test.ts | 203 +- .../nest/unsplash/unsplash.service.test.ts | 87 +- .../unit/nest/user-cleanup.service.test.ts | 208 +- .../unit/nest/user-profile.service.test.ts | 81 +- .../tests/unit/nest/vacay.controller.test.ts | 133 +- .../unit/nest/validate-body-contracts.test.ts | 15 +- .../unit/nest/validate-managed-routes.test.ts | 35 +- .../unit/nest/weather.controller.test.ts | 5 +- .../weather/weather-cache-lifecycle.test.ts | 8 +- .../unit/nest/weather/weather.impl.test.ts | 114 +- .../unit/nest/webauthn-config.service.test.ts | 9 +- server/tests/unit/nest/wiki.test.ts | 16 +- server/tests/unit/nest/wiring.test.ts | 28 +- .../tests/unit/nest/ws-bind-failure.test.ts | 6 +- server/tests/unit/nest/zod-pipe.test.ts | 5 +- server/tests/unit/packing/packing.rpc.test.ts | 192 +- server/tests/unit/places/places.rpc.test.ts | 123 +- .../plugins/addon-domains-errors.rpc.test.ts | 369 ++- .../unit/plugins/addon-gates.rpc.test.ts | 100 +- .../plugins/atlas-layers.controller.test.ts | 69 +- .../tests/unit/plugins/daily-budget.test.ts | 21 +- .../plugins/day-schedule.controller.test.ts | 53 +- .../unit/plugins/day-tints.controller.test.ts | 74 +- .../tests/unit/plugins/dependencies.test.ts | 40 +- server/tests/unit/plugins/discovery.test.ts | 87 +- .../tests/unit/plugins/egress-policy.test.ts | 65 +- server/tests/unit/plugins/event-facts.test.ts | 5 +- .../unit/plugins/event-subscriptions.test.ts | 37 +- server/tests/unit/plugins/host-compat.test.ts | 3 +- server/tests/unit/plugins/host-rpc.test.ts | 421 ++- .../journal-entry-rows.controller.test.ts | 72 +- .../unit/plugins/journal-photo.rpc.test.ts | 69 +- server/tests/unit/plugins/manifest.test.ts | 278 +- .../plugins/map-layers.controller.test.ts | 121 +- .../plugins/map-markers.controller.test.ts | 71 +- .../plugins/mcp-session-invalidation.test.ts | 23 +- .../unit/plugins/mcp-tool-annotations.test.ts | 7 +- .../unit/plugins/mcp-tool-enforcement.test.ts | 11 +- .../unit/plugins/mcp-tool-schema.test.ts | 46 +- .../tests/unit/plugins/mcp-tool-sink.test.ts | 15 +- server/tests/unit/plugins/native-scan.test.ts | 13 +- .../unit/plugins/operator-egress.test.ts | 101 +- .../plugins/output-contract.schema.test.ts | 82 +- .../unit/plugins/output-contract.test.ts | 27 +- .../plugins/pdf-sections.controller.test.ts | 68 +- .../plugins/place-details.controller.test.ts | 47 +- .../tests/unit/plugins/plugin-audit.test.ts | 184 +- .../tests/unit/plugins/plugin-backup.test.ts | 60 +- server/tests/unit/plugins/plugin-data.test.ts | 54 +- .../unit/plugins/plugin-event-meta.test.ts | 53 +- .../tests/unit/plugins/plugin-frame.test.ts | 39 +- .../tests/unit/plugins/plugin-hooks.test.ts | 27 +- .../unit/plugins/plugin-host-state.test.ts | 43 +- .../plugins/plugin-instance-settings.test.ts | 228 +- .../plugins/plugin-jobs.cron-compat.test.ts | 28 +- server/tests/unit/plugins/plugin-jobs.test.ts | 31 +- .../unit/plugins/plugin-mcp-tools.test.ts | 19 +- .../plugins/plugin-oauth.controller.test.ts | 27 +- .../unit/plugins/plugin-oauth.service.test.ts | 104 +- .../plugin-photos-calendar.controller.test.ts | 53 +- server/tests/unit/plugins/plugin-pois.test.ts | 277 +- .../plugin-repository-sql-parity.test.ts | 38 +- .../plugins/plugin-retrust.controller.test.ts | 119 +- .../plugins/plugin-route-normalize.test.ts | 25 +- .../plugins/plugin-routes.controller.test.ts | 84 +- .../plugin-runtime.boot-no-orm.test.ts | 65 +- server/tests/unit/plugins/plugin-sdk.test.ts | 16 +- .../plugins/plugin-search.controller.test.ts | 56 +- .../plugin-user-settings.controller.test.ts | 35 +- .../unit/plugins/plugin-user-settings.test.ts | 110 +- .../plugins-controller-devlink.test.ts | 43 +- .../plugins/plugins-feed.controller.test.ts | 120 +- .../tests/unit/plugins/plugins-proxy.test.ts | 142 +- .../unit/plugins/plugins-service.test.ts | 231 +- .../tests/unit/plugins/poi-categories.test.ts | 59 +- .../tests/unit/plugins/protocol-paths.test.ts | 35 +- .../unit/plugins/provider-hook-grant.test.ts | 56 +- server/tests/unit/plugins/rate-limit.test.ts | 10 +- .../unit/plugins/registry-preview.test.ts | 5 +- .../unit/plugins/request-context.test.ts | 34 +- .../tests/unit/plugins/rpc-coverage.test.ts | 21 +- .../tests/unit/plugins/rpc-fallbacks.test.ts | 280 +- .../unit/plugins/rpc-host-inv-strip.test.ts | 34 +- server/tests/unit/plugins/rpc-host.test.ts | 51 +- server/tests/unit/plugins/rpc-trace.test.ts | 23 +- .../tests/unit/plugins/safe-extract.test.ts | 34 +- server/tests/unit/plugins/safe-fetch.test.ts | 21 +- .../unit/plugins/settings-isolation.test.ts | 95 +- server/tests/unit/plugins/sql-script.test.ts | 7 +- .../unit/plugins/supervisor-lifecycle.test.ts | 126 +- .../unit/plugins/tenant-free.rpc.test.ts | 50 +- .../tests/unit/plugins/text-sanitize.test.ts | 13 +- ...trip-card-contributions.controller.test.ts | 51 +- .../plugins/trip-warnings.controller.test.ts | 52 +- .../unit/plugins/verify-signature.test.ts | 5 +- .../view-contributions.controller.test.ts | 56 +- .../unit/reservations/bookings.rpc.test.ts | 299 +- .../tests/unit/scripts/db-call-graph.test.ts | 11 +- .../tests/unit/scripts/dialect-lint.test.ts | 19 +- .../scripts/error-envelope-ratchets.test.ts | 6 +- .../unit/scripts/pg-probe/arg-samples.test.ts | 14 +- .../unit/scripts/pg-probe/baseline.test.ts | 52 +- .../scripts/pg-probe/helper-cases.test.ts | 24 +- .../unit/scripts/pg-probe/recorder.test.ts | 27 +- .../unit/scripts/pg-probe/report.test.ts | 108 +- .../unit/scripts/pg-probe/schema.test.ts | 22 +- .../unit/scripts/test-sql-ratchet.test.ts | 19 +- server/tests/unit/scripts/tx-writes.test.ts | 18 +- server/tests/unit/shared-contract.test.ts | 3 +- server/tests/unit/shutdown.test.ts | 65 +- .../unit/systemNotices/conditions.test.ts | 8 +- .../tests/unit/systemNotices/registry.test.ts | 37 +- .../unit/systemNotices/versionRange.test.ts | 3 +- server/tests/unit/tags/tags.rpc.test.ts | 46 +- server/tests/unit/todo/todo.rpc.test.ts | 59 +- server/tests/unit/trips/trips.rpc.test.ts | 158 +- server/tests/unit/uploads-dirs.test.ts | 14 +- server/tests/unit/utils/cappedFetch.test.ts | 37 +- server/tests/unit/utils/ipv6.test.ts | 3 +- .../tests/unit/utils/ssrf-dual-stack.test.ts | 20 +- .../tests/unit/utils/ssrf-llm-timeout.test.ts | 39 +- server/tests/unit/utils/ssrf-proxy.test.ts | 12 +- .../unit/utils/ssrf-response-limits.test.ts | 6 +- server/tests/unit/utils/ssrfGuard.test.ts | 399 ++- server/tests/websocket/connection.test.ts | 133 +- server/tests/websocket/wire-envelope.test.ts | 48 +- shared/scripts/format-baseline.json | 383 +-- shared/src/book/book-store.schema.ts | 3 +- shared/src/book/book.schema.spec.ts | 11 +- shared/src/book/book.schema.ts | 215 +- shared/src/budget/budget.schema.ts | 14 +- shared/src/category/category.schema.ts | 3 +- shared/src/collection/collection.schema.ts | 27 +- .../src/datetime/datetime-normalize.spec.ts | 3 +- shared/src/datetime/datetime-normalize.ts | 5 +- shared/src/dawarich/dawarich.schema.ts | 18 +- shared/src/i18n/ar/admin.ts | 78 +- shared/src/i18n/ar/collection.ts | 3 +- shared/src/i18n/ar/dawarich.ts | 10 +- shared/src/i18n/ar/docsync.ts | 3 +- shared/src/i18n/ar/index.ts | 6 +- shared/src/i18n/ar/journey.ts | 18 +- shared/src/i18n/ar/map.ts | 3 +- shared/src/i18n/ar/notif.ts | 3 +- shared/src/i18n/ar/oauth.ts | 9 +- shared/src/i18n/ar/roadtrip.ts | 3 +- shared/src/i18n/ar/settings.ts | 15 +- shared/src/i18n/ar/system_notice.ts | 15 +- shared/src/i18n/az/admin.ts | 125 +- shared/src/i18n/az/airport.ts | 5 +- shared/src/i18n/az/atlas.ts | 32 +- shared/src/i18n/az/backup.ts | 5 +- shared/src/i18n/az/categories.ts | 8 +- shared/src/i18n/az/collab.ts | 14 +- shared/src/i18n/az/collection.ts | 14 +- shared/src/i18n/az/dashboard.ts | 11 +- shared/src/i18n/az/dawarich.ts | 14 +- shared/src/i18n/az/day.ts | 11 +- shared/src/i18n/az/dayplan.ts | 50 +- shared/src/i18n/az/docsync.ts | 44 +- shared/src/i18n/az/files.ts | 23 +- shared/src/i18n/az/index.ts | 6 +- shared/src/i18n/az/inspector.ts | 2 +- shared/src/i18n/az/journey.ts | 152 +- shared/src/i18n/az/login.ts | 6 +- shared/src/i18n/az/map.ts | 17 +- shared/src/i18n/az/members.ts | 20 +- shared/src/i18n/az/memories.ts | 65 +- shared/src/i18n/az/mobileAdmin.ts | 2 +- shared/src/i18n/az/mobileAtlas.ts | 2 +- shared/src/i18n/az/mobileCollections.ts | 2 +- shared/src/i18n/az/mobileJourney.ts | 2 +- shared/src/i18n/az/mobileNav.ts | 2 +- shared/src/i18n/az/mobileSettings.ts | 2 +- shared/src/i18n/az/mobileTrip.ts | 11 +- shared/src/i18n/az/mobileVacay.ts | 2 +- shared/src/i18n/az/notif.ts | 5 +- shared/src/i18n/az/notifications.ts | 26 +- shared/src/i18n/az/oauth.ts | 62 +- shared/src/i18n/az/packing.ts | 27 +- shared/src/i18n/az/pdf.ts | 2 +- shared/src/i18n/az/perm.ts | 41 +- shared/src/i18n/az/planner.ts | 20 +- shared/src/i18n/az/roadtrip.ts | 26 +- shared/src/i18n/az/settings.ts | 218 +- shared/src/i18n/az/share.ts | 3 +- shared/src/i18n/az/shared.ts | 3 +- shared/src/i18n/az/storage.ts | 23 +- shared/src/i18n/az/system_notice.ts | 26 +- shared/src/i18n/az/todo.ts | 2 +- shared/src/i18n/az/trip.ts | 11 +- shared/src/i18n/az/trips.ts | 3 +- shared/src/i18n/az/undo.ts | 2 +- shared/src/i18n/az/vacay.ts | 17 +- shared/src/i18n/br/admin.ts | 99 +- shared/src/i18n/br/collection.ts | 3 +- shared/src/i18n/br/dawarich.ts | 3 +- shared/src/i18n/br/docsync.ts | 3 +- shared/src/i18n/br/index.ts | 6 +- shared/src/i18n/br/journey.ts | 27 +- shared/src/i18n/br/map.ts | 3 +- shared/src/i18n/br/notif.ts | 3 +- shared/src/i18n/br/oauth.ts | 15 +- shared/src/i18n/br/roadtrip.ts | 3 +- shared/src/i18n/br/settings.ts | 15 +- shared/src/i18n/br/storage.ts | 3 +- shared/src/i18n/br/system_notice.ts | 15 +- shared/src/i18n/ca/admin.ts | 75 +- shared/src/i18n/ca/dawarich.ts | 4 +- shared/src/i18n/ca/docsync.ts | 3 +- shared/src/i18n/ca/index.ts | 6 +- shared/src/i18n/ca/journey.ts | 27 +- shared/src/i18n/ca/map.ts | 3 +- shared/src/i18n/ca/notif.ts | 3 +- shared/src/i18n/ca/oauth.ts | 12 +- shared/src/i18n/ca/roadtrip.ts | 3 +- shared/src/i18n/ca/settings.ts | 12 +- shared/src/i18n/ca/storage.ts | 3 +- shared/src/i18n/ca/system_notice.ts | 15 +- shared/src/i18n/cs/admin.ts | 81 +- shared/src/i18n/cs/collection.ts | 3 +- shared/src/i18n/cs/dawarich.ts | 4 +- shared/src/i18n/cs/docsync.ts | 3 +- shared/src/i18n/cs/index.ts | 6 +- shared/src/i18n/cs/journey.ts | 27 +- shared/src/i18n/cs/map.ts | 3 +- shared/src/i18n/cs/notif.ts | 3 +- shared/src/i18n/cs/oauth.ts | 12 +- shared/src/i18n/cs/roadtrip.ts | 3 +- shared/src/i18n/cs/settings.ts | 24 +- shared/src/i18n/cs/system_notice.ts | 15 +- shared/src/i18n/de/admin.ts | 99 +- shared/src/i18n/de/collection.ts | 3 +- shared/src/i18n/de/common.ts | 3 +- shared/src/i18n/de/dawarich.ts | 10 +- shared/src/i18n/de/docsync.ts | 3 +- shared/src/i18n/de/index.ts | 6 +- shared/src/i18n/de/journey.ts | 33 +- shared/src/i18n/de/map.ts | 6 +- shared/src/i18n/de/notif.ts | 3 +- shared/src/i18n/de/oauth.ts | 15 +- shared/src/i18n/de/roadtrip.ts | 3 +- shared/src/i18n/de/settings.ts | 27 +- shared/src/i18n/de/storage.ts | 3 +- shared/src/i18n/de/system_notice.ts | 15 +- shared/src/i18n/en/admin.ts | 75 +- shared/src/i18n/en/collection.ts | 3 +- shared/src/i18n/en/dawarich.ts | 7 +- shared/src/i18n/en/docsync.ts | 3 +- shared/src/i18n/en/index.ts | 6 +- shared/src/i18n/en/journey.ts | 24 +- shared/src/i18n/en/map.ts | 3 +- shared/src/i18n/en/notif.ts | 3 +- shared/src/i18n/en/oauth.ts | 15 +- shared/src/i18n/en/roadtrip.ts | 6 +- shared/src/i18n/en/settings.ts | 15 +- shared/src/i18n/en/system_notice.ts | 15 +- shared/src/i18n/es/admin.ts | 102 +- shared/src/i18n/es/collection.ts | 3 +- shared/src/i18n/es/common.ts | 3 +- shared/src/i18n/es/dawarich.ts | 4 +- shared/src/i18n/es/docsync.ts | 3 +- shared/src/i18n/es/index.ts | 6 +- shared/src/i18n/es/journey.ts | 27 +- shared/src/i18n/es/map.ts | 3 +- shared/src/i18n/es/notif.ts | 3 +- shared/src/i18n/es/oauth.ts | 18 +- shared/src/i18n/es/roadtrip.ts | 3 +- shared/src/i18n/es/settings.ts | 21 +- shared/src/i18n/es/system_notice.ts | 15 +- shared/src/i18n/et/admin.ts | 9 +- shared/src/i18n/et/dawarich.ts | 1 - shared/src/i18n/et/index.ts | 6 +- shared/src/i18n/et/journey.ts | 6 +- shared/src/i18n/et/settings.ts | 3 +- shared/src/i18n/fr/admin.ts | 81 +- shared/src/i18n/fr/collection.ts | 3 +- shared/src/i18n/fr/common.ts | 3 +- shared/src/i18n/fr/dawarich.ts | 10 +- shared/src/i18n/fr/docsync.ts | 3 +- shared/src/i18n/fr/index.ts | 6 +- shared/src/i18n/fr/journey.ts | 21 +- shared/src/i18n/fr/map.ts | 3 +- shared/src/i18n/fr/notif.ts | 3 +- shared/src/i18n/fr/oauth.ts | 18 +- shared/src/i18n/fr/roadtrip.ts | 3 +- shared/src/i18n/fr/settings.ts | 12 +- shared/src/i18n/fr/storage.ts | 3 +- shared/src/i18n/fr/system_notice.ts | 15 +- shared/src/i18n/gr/admin.ts | 99 +- shared/src/i18n/gr/collection.ts | 3 +- shared/src/i18n/gr/common.ts | 3 +- shared/src/i18n/gr/dawarich.ts | 13 +- shared/src/i18n/gr/docsync.ts | 3 +- shared/src/i18n/gr/index.ts | 6 +- shared/src/i18n/gr/journey.ts | 51 +- shared/src/i18n/gr/map.ts | 3 +- shared/src/i18n/gr/notif.ts | 3 +- shared/src/i18n/gr/oauth.ts | 18 +- shared/src/i18n/gr/roadtrip.ts | 3 +- shared/src/i18n/gr/settings.ts | 39 +- shared/src/i18n/gr/storage.ts | 3 +- shared/src/i18n/gr/system_notice.ts | 15 +- shared/src/i18n/hu/admin.ts | 99 +- shared/src/i18n/hu/collection.ts | 3 +- shared/src/i18n/hu/dawarich.ts | 4 +- shared/src/i18n/hu/docsync.ts | 3 +- shared/src/i18n/hu/index.ts | 6 +- shared/src/i18n/hu/journey.ts | 36 +- shared/src/i18n/hu/map.ts | 3 +- shared/src/i18n/hu/notif.ts | 3 +- shared/src/i18n/hu/oauth.ts | 18 +- shared/src/i18n/hu/roadtrip.ts | 6 +- shared/src/i18n/hu/settings.ts | 27 +- shared/src/i18n/hu/storage.ts | 3 +- shared/src/i18n/hu/system_notice.ts | 15 +- shared/src/i18n/hu/trip.ts | 3 +- .../src/i18n/i18n-permission-wording.spec.ts | 4 +- shared/src/i18n/id/admin.ts | 93 +- shared/src/i18n/id/collection.ts | 3 +- shared/src/i18n/id/dawarich.ts | 7 +- shared/src/i18n/id/docsync.ts | 3 +- shared/src/i18n/id/index.ts | 6 +- shared/src/i18n/id/journey.ts | 36 +- shared/src/i18n/id/map.ts | 6 +- shared/src/i18n/id/notif.ts | 3 +- shared/src/i18n/id/oauth.ts | 12 +- shared/src/i18n/id/roadtrip.ts | 3 +- shared/src/i18n/id/settings.ts | 27 +- shared/src/i18n/id/storage.ts | 3 +- shared/src/i18n/id/system_notice.ts | 15 +- shared/src/i18n/it/admin.ts | 90 +- shared/src/i18n/it/collection.ts | 3 +- shared/src/i18n/it/common.ts | 3 +- shared/src/i18n/it/dawarich.ts | 13 +- shared/src/i18n/it/docsync.ts | 3 +- shared/src/i18n/it/index.ts | 6 +- shared/src/i18n/it/journey.ts | 24 +- shared/src/i18n/it/map.ts | 3 +- shared/src/i18n/it/notif.ts | 3 +- shared/src/i18n/it/oauth.ts | 18 +- shared/src/i18n/it/roadtrip.ts | 3 +- shared/src/i18n/it/settings.ts | 3 +- shared/src/i18n/it/storage.ts | 3 +- shared/src/i18n/it/system_notice.ts | 15 +- shared/src/i18n/ja/admin.ts | 81 +- shared/src/i18n/ja/collection.ts | 6 +- shared/src/i18n/ja/common.ts | 3 +- shared/src/i18n/ja/dawarich.ts | 22 +- shared/src/i18n/ja/docsync.ts | 3 +- shared/src/i18n/ja/index.ts | 6 +- shared/src/i18n/ja/journey.ts | 30 +- shared/src/i18n/ja/map.ts | 3 +- shared/src/i18n/ja/mobileTrip.ts | 3 +- shared/src/i18n/ja/notif.ts | 3 +- shared/src/i18n/ja/oauth.ts | 6 +- shared/src/i18n/ja/roadtrip.ts | 6 +- shared/src/i18n/ja/settings.ts | 33 +- shared/src/i18n/ja/system_notice.ts | 15 +- shared/src/i18n/ko/admin.ts | 78 +- shared/src/i18n/ko/collection.ts | 3 +- shared/src/i18n/ko/dawarich.ts | 4 +- shared/src/i18n/ko/index.ts | 6 +- shared/src/i18n/ko/journey.ts | 30 +- shared/src/i18n/ko/map.ts | 3 +- shared/src/i18n/ko/memories.ts | 3 +- shared/src/i18n/ko/mobileTrip.ts | 3 +- shared/src/i18n/ko/notif.ts | 3 +- shared/src/i18n/ko/oauth.ts | 3 +- shared/src/i18n/ko/roadtrip.ts | 9 +- shared/src/i18n/ko/settings.ts | 18 +- shared/src/i18n/ko/system_notice.ts | 15 +- shared/src/i18n/nl/admin.ts | 90 +- shared/src/i18n/nl/collection.ts | 3 +- shared/src/i18n/nl/common.ts | 3 +- shared/src/i18n/nl/dawarich.ts | 4 +- shared/src/i18n/nl/docsync.ts | 3 +- shared/src/i18n/nl/index.ts | 6 +- shared/src/i18n/nl/journey.ts | 24 +- shared/src/i18n/nl/map.ts | 3 +- shared/src/i18n/nl/notif.ts | 3 +- shared/src/i18n/nl/oauth.ts | 15 +- shared/src/i18n/nl/roadtrip.ts | 3 +- shared/src/i18n/nl/settings.ts | 24 +- shared/src/i18n/nl/storage.ts | 3 +- shared/src/i18n/nl/system_notice.ts | 15 +- shared/src/i18n/pl/admin.ts | 93 +- shared/src/i18n/pl/collection.ts | 3 +- shared/src/i18n/pl/dawarich.ts | 10 +- shared/src/i18n/pl/docsync.ts | 3 +- shared/src/i18n/pl/index.ts | 6 +- shared/src/i18n/pl/journey.ts | 33 +- shared/src/i18n/pl/map.ts | 3 +- shared/src/i18n/pl/notif.ts | 3 +- shared/src/i18n/pl/oauth.ts | 18 +- shared/src/i18n/pl/roadtrip.ts | 3 +- shared/src/i18n/pl/settings.ts | 27 +- shared/src/i18n/pl/storage.ts | 3 +- shared/src/i18n/pl/system_notice.ts | 15 +- shared/src/i18n/ru/admin.ts | 90 +- shared/src/i18n/ru/collection.ts | 3 +- shared/src/i18n/ru/common.ts | 3 +- shared/src/i18n/ru/dawarich.ts | 10 +- shared/src/i18n/ru/docsync.ts | 3 +- shared/src/i18n/ru/index.ts | 6 +- shared/src/i18n/ru/journey.ts | 30 +- shared/src/i18n/ru/map.ts | 6 +- shared/src/i18n/ru/notif.ts | 3 +- shared/src/i18n/ru/oauth.ts | 18 +- shared/src/i18n/ru/roadtrip.ts | 3 +- shared/src/i18n/ru/settings.ts | 15 +- shared/src/i18n/ru/system_notice.ts | 15 +- shared/src/i18n/sk/admin.ts | 9 +- shared/src/i18n/sk/dawarich.ts | 1 - shared/src/i18n/sk/index.ts | 6 +- shared/src/i18n/sk/journey.ts | 6 +- shared/src/i18n/sk/settings.ts | 3 +- shared/src/i18n/sv/admin.ts | 84 +- shared/src/i18n/sv/collection.ts | 3 +- shared/src/i18n/sv/common.ts | 3 +- shared/src/i18n/sv/dawarich.ts | 7 +- shared/src/i18n/sv/docsync.ts | 3 +- shared/src/i18n/sv/index.ts | 6 +- shared/src/i18n/sv/journey.ts | 36 +- shared/src/i18n/sv/map.ts | 3 +- shared/src/i18n/sv/notif.ts | 3 +- shared/src/i18n/sv/oauth.ts | 15 +- shared/src/i18n/sv/roadtrip.ts | 3 +- shared/src/i18n/sv/settings.ts | 18 +- shared/src/i18n/sv/system_notice.ts | 15 +- shared/src/i18n/th/admin.ts | 6 +- shared/src/i18n/th/collection.ts | 3 +- shared/src/i18n/th/dashboard.ts | 8 +- shared/src/i18n/th/dayplan.ts | 9 +- shared/src/i18n/th/docsync.ts | 15 +- shared/src/i18n/th/index.ts | 6 +- shared/src/i18n/th/journey.ts | 9 +- shared/src/i18n/th/notif.ts | 3 +- shared/src/i18n/th/roadtrip.ts | 6 +- shared/src/i18n/th/settings.ts | 6 +- shared/src/i18n/th/system_notice.ts | 150 +- shared/src/i18n/th/trip.ts | 8 +- shared/src/i18n/tr/admin.ts | 78 +- shared/src/i18n/tr/collection.ts | 3 +- shared/src/i18n/tr/dawarich.ts | 13 +- shared/src/i18n/tr/docsync.ts | 3 +- shared/src/i18n/tr/index.ts | 6 +- shared/src/i18n/tr/journey.ts | 24 +- shared/src/i18n/tr/map.ts | 3 +- shared/src/i18n/tr/notif.ts | 3 +- shared/src/i18n/tr/oauth.ts | 18 +- shared/src/i18n/tr/roadtrip.ts | 3 +- shared/src/i18n/tr/settings.ts | 18 +- shared/src/i18n/tr/system_notice.ts | 15 +- shared/src/i18n/uk/admin.ts | 90 +- shared/src/i18n/uk/collection.ts | 3 +- shared/src/i18n/uk/dawarich.ts | 10 +- shared/src/i18n/uk/docsync.ts | 3 +- shared/src/i18n/uk/index.ts | 6 +- shared/src/i18n/uk/journey.ts | 33 +- shared/src/i18n/uk/map.ts | 3 +- shared/src/i18n/uk/notif.ts | 3 +- shared/src/i18n/uk/oauth.ts | 18 +- shared/src/i18n/uk/roadtrip.ts | 3 +- shared/src/i18n/uk/settings.ts | 21 +- shared/src/i18n/uk/system_notice.ts | 15 +- shared/src/i18n/vi/admin.ts | 87 +- shared/src/i18n/vi/collection.ts | 3 +- shared/src/i18n/vi/dawarich.ts | 4 +- shared/src/i18n/vi/docsync.ts | 3 +- shared/src/i18n/vi/index.ts | 6 +- shared/src/i18n/vi/journey.ts | 33 +- shared/src/i18n/vi/map.ts | 6 +- shared/src/i18n/vi/notif.ts | 3 +- shared/src/i18n/vi/oauth.ts | 12 +- shared/src/i18n/vi/roadtrip.ts | 3 +- shared/src/i18n/vi/settings.ts | 27 +- shared/src/i18n/vi/system_notice.ts | 15 +- shared/src/i18n/zh-TW/admin.ts | 78 +- shared/src/i18n/zh-TW/dawarich.ts | 7 +- shared/src/i18n/zh-TW/index.ts | 6 +- shared/src/i18n/zh-TW/journey.ts | 9 +- shared/src/i18n/zh-TW/notif.ts | 3 +- shared/src/i18n/zh-TW/oauth.ts | 3 +- shared/src/i18n/zh-TW/roadtrip.ts | 6 +- shared/src/i18n/zh-TW/settings.ts | 6 +- shared/src/i18n/zh-TW/system_notice.ts | 12 +- shared/src/i18n/zh/admin.ts | 78 +- shared/src/i18n/zh/dawarich.ts | 10 +- shared/src/i18n/zh/index.ts | 6 +- shared/src/i18n/zh/journey.ts | 9 +- shared/src/i18n/zh/notif.ts | 3 +- shared/src/i18n/zh/oauth.ts | 3 +- shared/src/i18n/zh/roadtrip.ts | 3 +- shared/src/i18n/zh/settings.ts | 6 +- shared/src/i18n/zh/system_notice.ts | 12 +- shared/src/journey/journey.schema.spec.ts | 23 +- shared/src/plugins/plugins.schema.spec.ts | 8 +- shared/src/public-api/public-api.schema.ts | 7 +- .../reservation/reservation.schema.spec.ts | 40 +- shared/src/reservation/reservation.schema.ts | 28 +- shared/src/roadtrip/assemble.spec.ts | 8 +- shared/src/roadtrip/dayWindow.spec.ts | 18 +- shared/src/roadtrip/roadtripModel.spec.ts | 22 +- shared/src/roadtrip/roadtripModel.ts | 6 +- shared/src/tours/tours.metrics.test.ts | 5 +- shared/src/vacay/vacay.schema.spec.ts | 3 +- 1978 files changed, 78448 insertions(+), 39347 deletions(-) diff --git a/server/scripts/build-atlas-geo.mjs b/server/scripts/build-atlas-geo.mjs index 19a9c91f6..ff856d309 100644 --- a/server/scripts/build-atlas-geo.mjs +++ b/server/scripts/build-atlas-geo.mjs @@ -13,21 +13,20 @@ // FeatureCollections that the server serves at runtime (no network at boot). // // geoBoundaries: CC BY 4.0 — https://www.geoboundaries.org/ (attribution required). +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import zlib from 'node:zlib'; -import fs from 'node:fs' -import path from 'node:path' -import zlib from 'node:zlib' -import { fileURLToPath } from 'node:url' - -const __dirname = path.dirname(fileURLToPath(import.meta.url)) -const OUT_DIR = path.join(__dirname, '..', 'assets', 'atlas') +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const OUT_DIR = path.join(__dirname, '..', 'assets', 'atlas'); // Pinned geoBoundaries revision (override with GB_REF=). The LFS media // endpoint resolves a commit SHA, branch, or tag in the path segment. -const GB_REF = process.env.GB_REF || '5c25134028196d43ce97b5071934fd0cfc92f09f' +const GB_REF = process.env.GB_REF || '5c25134028196d43ce97b5071934fd0cfc92f09f'; const MEDIA = (a3, level) => `https://media.githubusercontent.com/media/wmgeolab/geoBoundaries/${GB_REF}` + - `/releaseData/gbOpen/${a3}/${level}/geoBoundaries-${a3}-${level}_simplified.geojson` + `/releaseData/gbOpen/${a3}/${level}/geoBoundaries-${a3}-${level}_simplified.geojson`; // Country borders come from CGAZ (the Comprehensive Global Administrative Zones composite) // rather than per-country gbOpen ADM0: CGAZ is gap-filled, so it includes territories @@ -36,10 +35,10 @@ const MEDIA = (a3, level) => // irrelevant. (gbOpen ADM0 maxes Norway at 71°N and has no Svalbard at all.) const CGAZ_ADM0 = `https://media.githubusercontent.com/media/wmgeolab/geoBoundaries/${GB_REF}` + - `/releaseData/CGAZ/geoBoundariesCGAZ_ADM0.geojson` + `/releaseData/CGAZ/geoBoundariesCGAZ_ADM0.geojson`; -const CONCURRENCY = 8 -const RETRIES = 3 +const CONCURRENCY = 8; +const RETRIES = 3; // Complete ISO-3166-1 alpha-3 → alpha-2 map (source: lukes/ISO-3166-Countries-with- // Regional-Codes, plus user-assigned XKX/XK for Kosovo, which geoBoundaries ships but @@ -47,7 +46,258 @@ const RETRIES = 3 // code; missing ones 404 and are skipped) and stamps `iso_a2`/`ISO_A2` (geoBoundaries // keys by alpha-3 `shapeGroup`). A complete map — not the client's curated ~180 — is // what restores the dropped territories (Greenland, Falklands, French Guiana, …). -const A3_TO_A2 = {"ABW":"AW", "AFG":"AF", "AGO":"AO", "AIA":"AI", "ALA":"AX", "ALB":"AL", "AND":"AD", "ARE":"AE", "ARG":"AR", "ARM":"AM", "ASM":"AS", "ATA":"AQ", "ATF":"TF", "ATG":"AG", "AUS":"AU", "AUT":"AT", "AZE":"AZ", "BDI":"BI", "BEL":"BE", "BEN":"BJ", "BES":"BQ", "BFA":"BF", "BGD":"BD", "BGR":"BG", "BHR":"BH", "BHS":"BS", "BIH":"BA", "BLM":"BL", "BLR":"BY", "BLZ":"BZ", "BMU":"BM", "BOL":"BO", "BRA":"BR", "BRB":"BB", "BRN":"BN", "BTN":"BT", "BVT":"BV", "BWA":"BW", "CAF":"CF", "CAN":"CA", "CCK":"CC", "CHE":"CH", "CHL":"CL", "CHN":"CN", "CIV":"CI", "CMR":"CM", "COD":"CD", "COG":"CG", "COK":"CK", "COL":"CO", "COM":"KM", "CPV":"CV", "CRI":"CR", "CUB":"CU", "CUW":"CW", "CXR":"CX", "CYM":"KY", "CYP":"CY", "CZE":"CZ", "DEU":"DE", "DJI":"DJ", "DMA":"DM", "DNK":"DK", "DOM":"DO", "DZA":"DZ", "ECU":"EC", "EGY":"EG", "ERI":"ER", "ESH":"EH", "ESP":"ES", "EST":"EE", "ETH":"ET", "FIN":"FI", "FJI":"FJ", "FLK":"FK", "FRA":"FR", "FRO":"FO", "FSM":"FM", "GAB":"GA", "GBR":"GB", "GEO":"GE", "GGY":"GG", "GHA":"GH", "GIB":"GI", "GIN":"GN", "GLP":"GP", "GMB":"GM", "GNB":"GW", "GNQ":"GQ", "GRC":"GR", "GRD":"GD", "GRL":"GL", "GTM":"GT", "GUF":"GF", "GUM":"GU", "GUY":"GY", "HKG":"HK", "HMD":"HM", "HND":"HN", "HRV":"HR", "HTI":"HT", "HUN":"HU", "IDN":"ID", "IMN":"IM", "IND":"IN", "IOT":"IO", "IRL":"IE", "IRN":"IR", "IRQ":"IQ", "ISL":"IS", "ISR":"IL", "ITA":"IT", "JAM":"JM", "JEY":"JE", "JOR":"JO", "JPN":"JP", "KAZ":"KZ", "KEN":"KE", "KGZ":"KG", "KHM":"KH", "KIR":"KI", "KNA":"KN", "KOR":"KR", "KWT":"KW", "LAO":"LA", "LBN":"LB", "LBR":"LR", "LBY":"LY", "LCA":"LC", "LIE":"LI", "LKA":"LK", "LSO":"LS", "LTU":"LT", "LUX":"LU", "LVA":"LV", "MAC":"MO", "MAF":"MF", "MAR":"MA", "MCO":"MC", "MDA":"MD", "MDG":"MG", "MDV":"MV", "MEX":"MX", "MHL":"MH", "MKD":"MK", "MLI":"ML", "MLT":"MT", "MMR":"MM", "MNE":"ME", "MNG":"MN", "MNP":"MP", "MOZ":"MZ", "MRT":"MR", "MSR":"MS", "MTQ":"MQ", "MUS":"MU", "MWI":"MW", "MYS":"MY", "MYT":"YT", "NAM":"NA", "NCL":"NC", "NER":"NE", "NFK":"NF", "NGA":"NG", "NIC":"NI", "NIU":"NU", "NLD":"NL", "NOR":"NO", "NPL":"NP", "NRU":"NR", "NZL":"NZ", "OMN":"OM", "PAK":"PK", "PAN":"PA", "PCN":"PN", "PER":"PE", "PHL":"PH", "PLW":"PW", "PNG":"PG", "POL":"PL", "PRI":"PR", "PRK":"KP", "PRT":"PT", "PRY":"PY", "PSE":"PS", "PYF":"PF", "QAT":"QA", "REU":"RE", "ROU":"RO", "RUS":"RU", "RWA":"RW", "SAU":"SA", "SDN":"SD", "SEN":"SN", "SGP":"SG", "SGS":"GS", "SHN":"SH", "SJM":"SJ", "SLB":"SB", "SLE":"SL", "SLV":"SV", "SMR":"SM", "SOM":"SO", "SPM":"PM", "SRB":"RS", "SSD":"SS", "STP":"ST", "SUR":"SR", "SVK":"SK", "SVN":"SI", "SWE":"SE", "SWZ":"SZ", "SXM":"SX", "SYC":"SC", "SYR":"SY", "TCA":"TC", "TCD":"TD", "TGO":"TG", "THA":"TH", "TJK":"TJ", "TKL":"TK", "TKM":"TM", "TLS":"TL", "TON":"TO", "TTO":"TT", "TUN":"TN", "TUR":"TR", "TUV":"TV", "TWN":"TW", "TZA":"TZ", "UGA":"UG", "UKR":"UA", "UMI":"UM", "URY":"UY", "USA":"US", "UZB":"UZ", "VAT":"VA", "VCT":"VC", "VEN":"VE", "VGB":"VG", "VIR":"VI", "VNM":"VN", "VUT":"VU", "WLF":"WF", "WSM":"WS", "XKX":"XK", "YEM":"YE", "ZAF":"ZA", "ZMB":"ZM", "ZWE":"ZW"} +const A3_TO_A2 = { + ABW: 'AW', + AFG: 'AF', + AGO: 'AO', + AIA: 'AI', + ALA: 'AX', + ALB: 'AL', + AND: 'AD', + ARE: 'AE', + ARG: 'AR', + ARM: 'AM', + ASM: 'AS', + ATA: 'AQ', + ATF: 'TF', + ATG: 'AG', + AUS: 'AU', + AUT: 'AT', + AZE: 'AZ', + BDI: 'BI', + BEL: 'BE', + BEN: 'BJ', + BES: 'BQ', + BFA: 'BF', + BGD: 'BD', + BGR: 'BG', + BHR: 'BH', + BHS: 'BS', + BIH: 'BA', + BLM: 'BL', + BLR: 'BY', + BLZ: 'BZ', + BMU: 'BM', + BOL: 'BO', + BRA: 'BR', + BRB: 'BB', + BRN: 'BN', + BTN: 'BT', + BVT: 'BV', + BWA: 'BW', + CAF: 'CF', + CAN: 'CA', + CCK: 'CC', + CHE: 'CH', + CHL: 'CL', + CHN: 'CN', + CIV: 'CI', + CMR: 'CM', + COD: 'CD', + COG: 'CG', + COK: 'CK', + COL: 'CO', + COM: 'KM', + CPV: 'CV', + CRI: 'CR', + CUB: 'CU', + CUW: 'CW', + CXR: 'CX', + CYM: 'KY', + CYP: 'CY', + CZE: 'CZ', + DEU: 'DE', + DJI: 'DJ', + DMA: 'DM', + DNK: 'DK', + DOM: 'DO', + DZA: 'DZ', + ECU: 'EC', + EGY: 'EG', + ERI: 'ER', + ESH: 'EH', + ESP: 'ES', + EST: 'EE', + ETH: 'ET', + FIN: 'FI', + FJI: 'FJ', + FLK: 'FK', + FRA: 'FR', + FRO: 'FO', + FSM: 'FM', + GAB: 'GA', + GBR: 'GB', + GEO: 'GE', + GGY: 'GG', + GHA: 'GH', + GIB: 'GI', + GIN: 'GN', + GLP: 'GP', + GMB: 'GM', + GNB: 'GW', + GNQ: 'GQ', + GRC: 'GR', + GRD: 'GD', + GRL: 'GL', + GTM: 'GT', + GUF: 'GF', + GUM: 'GU', + GUY: 'GY', + HKG: 'HK', + HMD: 'HM', + HND: 'HN', + HRV: 'HR', + HTI: 'HT', + HUN: 'HU', + IDN: 'ID', + IMN: 'IM', + IND: 'IN', + IOT: 'IO', + IRL: 'IE', + IRN: 'IR', + IRQ: 'IQ', + ISL: 'IS', + ISR: 'IL', + ITA: 'IT', + JAM: 'JM', + JEY: 'JE', + JOR: 'JO', + JPN: 'JP', + KAZ: 'KZ', + KEN: 'KE', + KGZ: 'KG', + KHM: 'KH', + KIR: 'KI', + KNA: 'KN', + KOR: 'KR', + KWT: 'KW', + LAO: 'LA', + LBN: 'LB', + LBR: 'LR', + LBY: 'LY', + LCA: 'LC', + LIE: 'LI', + LKA: 'LK', + LSO: 'LS', + LTU: 'LT', + LUX: 'LU', + LVA: 'LV', + MAC: 'MO', + MAF: 'MF', + MAR: 'MA', + MCO: 'MC', + MDA: 'MD', + MDG: 'MG', + MDV: 'MV', + MEX: 'MX', + MHL: 'MH', + MKD: 'MK', + MLI: 'ML', + MLT: 'MT', + MMR: 'MM', + MNE: 'ME', + MNG: 'MN', + MNP: 'MP', + MOZ: 'MZ', + MRT: 'MR', + MSR: 'MS', + MTQ: 'MQ', + MUS: 'MU', + MWI: 'MW', + MYS: 'MY', + MYT: 'YT', + NAM: 'NA', + NCL: 'NC', + NER: 'NE', + NFK: 'NF', + NGA: 'NG', + NIC: 'NI', + NIU: 'NU', + NLD: 'NL', + NOR: 'NO', + NPL: 'NP', + NRU: 'NR', + NZL: 'NZ', + OMN: 'OM', + PAK: 'PK', + PAN: 'PA', + PCN: 'PN', + PER: 'PE', + PHL: 'PH', + PLW: 'PW', + PNG: 'PG', + POL: 'PL', + PRI: 'PR', + PRK: 'KP', + PRT: 'PT', + PRY: 'PY', + PSE: 'PS', + PYF: 'PF', + QAT: 'QA', + REU: 'RE', + ROU: 'RO', + RUS: 'RU', + RWA: 'RW', + SAU: 'SA', + SDN: 'SD', + SEN: 'SN', + SGP: 'SG', + SGS: 'GS', + SHN: 'SH', + SJM: 'SJ', + SLB: 'SB', + SLE: 'SL', + SLV: 'SV', + SMR: 'SM', + SOM: 'SO', + SPM: 'PM', + SRB: 'RS', + SSD: 'SS', + STP: 'ST', + SUR: 'SR', + SVK: 'SK', + SVN: 'SI', + SWE: 'SE', + SWZ: 'SZ', + SXM: 'SX', + SYC: 'SC', + SYR: 'SY', + TCA: 'TC', + TCD: 'TD', + TGO: 'TG', + THA: 'TH', + TJK: 'TJ', + TKL: 'TK', + TKM: 'TM', + TLS: 'TL', + TON: 'TO', + TTO: 'TT', + TUN: 'TN', + TUR: 'TR', + TUV: 'TV', + TWN: 'TW', + TZA: 'TZ', + UGA: 'UG', + UKR: 'UA', + UMI: 'UM', + URY: 'UY', + USA: 'US', + UZB: 'UZ', + VAT: 'VA', + VCT: 'VC', + VEN: 'VE', + VGB: 'VG', + VIR: 'VI', + VNM: 'VN', + VUT: 'VU', + WLF: 'WF', + WSM: 'WS', + XKX: 'XK', + YEM: 'YE', + ZAF: 'ZA', + ZMB: 'ZM', + ZWE: 'ZW', +}; /** * Countries whose sub-national level is not the one everybody else's ADM1 is. @@ -63,52 +313,55 @@ const A3_TO_A2 = {"ABW":"AW", "AFG":"AF", "AGO":"AO", "AIA":"AI", "ALA":"AX", "A * bundle several times over for no gain: for most countries ADM1 already is the * level people think in. */ -const ADM_LEVEL = { GBR: 'ADM2' } +const ADM_LEVEL = { GBR: 'ADM2' }; -const COUNTRIES = Object.keys(A3_TO_A2) // every ISO alpha-3 code (ADM1 fetch list) +const COUNTRIES = Object.keys(A3_TO_A2); // every ISO alpha-3 code (ADM1 fetch list) // Cache raw downloads so re-runs (e.g. to tune simplification) don't re-fetch ~360 files. -const CACHE_DIR = path.join(__dirname, '..', '.atlas-geo-cache', GB_REF) +const CACHE_DIR = path.join(__dirname, '..', '.atlas-geo-cache', GB_REF); async function fetchGeo(url) { - const cacheFile = path.join(CACHE_DIR, url.split('/').slice(-1)[0]) + const cacheFile = path.join(CACHE_DIR, url.split('/').slice(-1)[0]); if (fs.existsSync(cacheFile)) { - const cached = fs.readFileSync(cacheFile, 'utf8') - return cached === '' ? null : JSON.parse(cached) + const cached = fs.readFileSync(cacheFile, 'utf8'); + return cached === '' ? null : JSON.parse(cached); } for (let attempt = 1; attempt <= RETRIES; attempt++) { try { - const res = await fetch(url, { headers: { 'User-Agent': 'TREK atlas builder' } }) - if (res.status === 404) { fs.writeFileSync(cacheFile, ''); return null } // no file — skip - if (!res.ok) throw new Error(`HTTP ${res.status}`) - const text = await res.text() - if (text.startsWith('version https://git-lfs')) throw new Error('got LFS pointer, not content') - const parsed = JSON.parse(text) - fs.writeFileSync(cacheFile, text) - return parsed + const res = await fetch(url, { headers: { 'User-Agent': 'TREK atlas builder' } }); + if (res.status === 404) { + fs.writeFileSync(cacheFile, ''); + return null; + } // no file — skip + if (!res.ok) throw new Error(`HTTP ${res.status}`); + const text = await res.text(); + if (text.startsWith('version https://git-lfs')) throw new Error('got LFS pointer, not content'); + const parsed = JSON.parse(text); + fs.writeFileSync(cacheFile, text); + return parsed; } catch (err) { if (attempt === RETRIES) { - console.warn(` ! ${url.split('/').slice(-1)[0]}: ${err.message}`) - return null + console.warn(` ! ${url.split('/').slice(-1)[0]}: ${err.message}`); + return null; } - await new Promise(r => setTimeout(r, 500 * attempt)) + await new Promise((r) => setTimeout(r, 500 * attempt)); } } - return null + return null; } // Run async tasks with a fixed concurrency cap. async function pool(items, worker) { - const results = [] - let i = 0 + const results = []; + let i = 0; const runners = Array.from({ length: CONCURRENCY }, async () => { while (i < items.length) { - const idx = i++ - results[idx] = await worker(items[idx], idx) + const idx = i++; + results[idx] = await worker(items[idx], idx); } - }) - await Promise.all(runners) - return results + }); + await Promise.all(runners); + return results; } // Geometry size control. geoBoundaries' "_simplified" files still carry ~12-decimal @@ -117,149 +370,161 @@ async function pool(items, worker) { // the now-redundant consecutive duplicate points shrinks the bundles ~5-8x with no // visible effect at the atlas' zoom range (3-10). ADM0 fills are viewed zoomed out, so // they tolerate a coarser grid than ADM1 region borders. -const ADM0_DECIMALS = 2 // ~1.1 km -const ADM1_DECIMALS = 3 // ~110 m +const ADM0_DECIMALS = 2; // ~1.1 km +const ADM1_DECIMALS = 3; // ~110 m function quantizeRing(ring, decimals) { - const m = 10 ** decimals - const out = [] - let prevX, prevY + const m = 10 ** decimals; + const out = []; + let prevX, prevY; for (const pt of ring) { - const x = Math.round(pt[0] * m) / m - const y = Math.round(pt[1] * m) / m - if (x === prevX && y === prevY) continue - out.push([x, y]) - prevX = x; prevY = y + const x = Math.round(pt[0] * m) / m; + const y = Math.round(pt[1] * m) / m; + if (x === prevX && y === prevY) continue; + out.push([x, y]); + prevX = x; + prevY = y; } - return out + return out; } // Quantize a (Multi)Polygon, dropping rings that collapse below a valid ring (<4 pts). function quantizeGeometry(geom, decimals) { - if (!geom) return null + if (!geom) return null; if (geom.type === 'Polygon') { - const rings = geom.coordinates.map(r => quantizeRing(r, decimals)).filter(r => r.length >= 4) - return rings.length ? { type: 'Polygon', coordinates: rings } : null + const rings = geom.coordinates.map((r) => quantizeRing(r, decimals)).filter((r) => r.length >= 4); + return rings.length ? { type: 'Polygon', coordinates: rings } : null; } if (geom.type === 'MultiPolygon') { const polys = geom.coordinates - .map(poly => poly.map(r => quantizeRing(r, decimals)).filter(r => r.length >= 4)) - .filter(poly => poly.length) - return polys.length ? { type: 'MultiPolygon', coordinates: polys } : null + .map((poly) => poly.map((r) => quantizeRing(r, decimals)).filter((r) => r.length >= 4)) + .filter((poly) => poly.length); + return polys.length ? { type: 'MultiPolygon', coordinates: polys } : null; } - return geom + return geom; } // Normalize one CGAZ ADM0 feature (keyed by alpha-3 `shapeGroup`) to the property names // the client country layer reads (ISO_A2/ADM0_A3/NAME/ADMIN). Returns null for the CRS // pseudo-entry or anything without a group/geometry. function normalizeAdm0Feature(f) { - const a3 = f.properties?.shapeGroup - if (!a3) return null - const name = f.properties?.shapeName || a3 - const geometry = quantizeGeometry(f.geometry, ADM0_DECIMALS) - if (!geometry) return null + const a3 = f.properties?.shapeGroup; + if (!a3) return null; + const name = f.properties?.shapeName || a3; + const geometry = quantizeGeometry(f.geometry, ADM0_DECIMALS); + if (!geometry) return null; return { type: 'Feature', properties: { ISO_A2: A3_TO_A2[a3] || null, ADM0_A3: a3, NAME: name, ADMIN: name }, geometry, - } + }; } function normalizeAdm1(geo, a3, countryName) { - if (!geo?.features) return [] - const a2 = A3_TO_A2[a3] || null + if (!geo?.features) return []; + const a2 = A3_TO_A2[a3] || null; // Ensure every region in a country ends up with a distinct iso_3166_2 — the Atlas // marks/unmarks regions by this code, so duplicates make one mark light up the whole // country. - const used = new Set() + const used = new Set(); const uniq = (base) => { - let code = base, n = 2 - while (used.has(code)) code = `${base}-${n++}` - used.add(code) - return code - } - return geo.features.map(f => { - let name = f.properties?.shapeName || '' - // geoBoundaries erroneously names Guangdong Province as "Guangzhou Province" (Guangzhou is the capital city). - if (a3 === 'CHN' && name === 'Guangzhou Province') { - name = 'Guangdong Province' - } - const geometry = quantizeGeometry(f.geometry, ADM1_DECIMALS) - if (!geometry) return null - // shapeISO is a real ISO 3166-2 code for most features, but geoBoundaries sometimes - // fills it with the bare country code instead of a subdivision code — e.g. every - // Spanish region gets "ESP", every Chinese "CHN" (also CL/OM). Keep it only when it - // is a real `XX-…` subdivision code and not already taken; otherwise synthesize a - // stable, unique-per-country id from the region name so each region is independently - // markable. - const raw = f.properties?.shapeISO || '' - let code - if (/^[A-Za-z]{2}-[A-Za-z0-9]+$/.test(raw) && !used.has(raw)) { - code = raw - used.add(code) - } else if (a2) { - code = uniq(`${a2}-${name.replace(/[^A-Za-z0-9]/g, '').toUpperCase() || 'RGN'}`) - } else { - code = raw - } - return { - type: 'Feature', - // Property names the Atlas region layer + server getRegionGeo already read. - properties: { - iso_a2: a2, - iso_3166_2: code, - name, - name_en: name, - admin: countryName, - }, - geometry, - } - }).filter(Boolean) + let code = base, + n = 2; + while (used.has(code)) code = `${base}-${n++}`; + used.add(code); + return code; + }; + return geo.features + .map((f) => { + let name = f.properties?.shapeName || ''; + // geoBoundaries erroneously names Guangdong Province as "Guangzhou Province" (Guangzhou is the capital city). + if (a3 === 'CHN' && name === 'Guangzhou Province') { + name = 'Guangdong Province'; + } + const geometry = quantizeGeometry(f.geometry, ADM1_DECIMALS); + if (!geometry) return null; + // shapeISO is a real ISO 3166-2 code for most features, but geoBoundaries sometimes + // fills it with the bare country code instead of a subdivision code — e.g. every + // Spanish region gets "ESP", every Chinese "CHN" (also CL/OM). Keep it only when it + // is a real `XX-…` subdivision code and not already taken; otherwise synthesize a + // stable, unique-per-country id from the region name so each region is independently + // markable. + const raw = f.properties?.shapeISO || ''; + let code; + if (/^[A-Za-z]{2}-[A-Za-z0-9]+$/.test(raw) && !used.has(raw)) { + code = raw; + used.add(code); + } else if (a2) { + code = uniq(`${a2}-${name.replace(/[^A-Za-z0-9]/g, '').toUpperCase() || 'RGN'}`); + } else { + code = raw; + } + return { + type: 'Feature', + // Property names the Atlas region layer + server getRegionGeo already read. + properties: { + iso_a2: a2, + iso_3166_2: code, + name, + name_en: name, + admin: countryName, + }, + geometry, + }; + }) + .filter(Boolean); } async function main() { - console.log(`[atlas-geo] geoBoundaries ref ${GB_REF}; ${COUNTRIES.length} countries`) - fs.mkdirSync(OUT_DIR, { recursive: true }) - fs.mkdirSync(CACHE_DIR, { recursive: true }) + console.log(`[atlas-geo] geoBoundaries ref ${GB_REF}; ${COUNTRIES.length} countries`); + fs.mkdirSync(OUT_DIR, { recursive: true }); + fs.mkdirSync(CACHE_DIR, { recursive: true }); // ADM0 (countries) — one comprehensive CGAZ file (large; cached). Also yields the // English country name (shapeGroup → shapeName) used for the ADM1 `admin` field. - console.log('[atlas-geo] downloading CGAZ ADM0 (countries)…') - const cgaz = await fetchGeo(CGAZ_ADM0) - const adm0Features = [] - const a3ToName = {} + console.log('[atlas-geo] downloading CGAZ ADM0 (countries)…'); + const cgaz = await fetchGeo(CGAZ_ADM0); + const adm0Features = []; + const a3ToName = {}; for (const f of cgaz?.features || []) { - const nf = normalizeAdm0Feature(f) - if (nf) { a3ToName[nf.properties.ADM0_A3] = nf.properties.NAME; adm0Features.push(nf) } + const nf = normalizeAdm0Feature(f); + if (nf) { + a3ToName[nf.properties.ADM0_A3] = nf.properties.NAME; + adm0Features.push(nf); + } } // ADM1 (sub-national regions) — per-country gbOpen (carries ISO 3166-2 `shapeISO`). - console.log('[atlas-geo] downloading ADM1 (regions)…') - const adm1Raw = await pool(COUNTRIES, a3 => fetchGeo(MEDIA(a3, ADM_LEVEL[a3] || 'ADM1'))) - const adm1Features = [] - let withCodes = 0 + console.log('[atlas-geo] downloading ADM1 (regions)…'); + const adm1Raw = await pool(COUNTRIES, (a3) => fetchGeo(MEDIA(a3, ADM_LEVEL[a3] || 'ADM1'))); + const adm1Features = []; + let withCodes = 0; COUNTRIES.forEach((a3, idx) => { - const feats = normalizeAdm1(adm1Raw[idx], a3, a3ToName[a3] || a3) - for (const f of feats) if (f.properties.iso_3166_2) withCodes++ - adm1Features.push(...feats) - }) + const feats = normalizeAdm1(adm1Raw[idx], a3, a3ToName[a3] || a3); + for (const f of feats) if (f.properties.iso_3166_2) withCodes++; + adm1Features.push(...feats); + }); const write = (name, features) => { - const fc = { type: 'FeatureCollection', features } - const gz = zlib.gzipSync(Buffer.from(JSON.stringify(fc)), { level: 9 }) - const file = path.join(OUT_DIR, `${name}.geojson.gz`) - fs.writeFileSync(file, gz) - console.log(`[atlas-geo] wrote ${path.relative(path.join(__dirname, '..'), file)} — ${features.length} features, ${(gz.length / 1e6).toFixed(1)} MB gz`) - } + const fc = { type: 'FeatureCollection', features }; + const gz = zlib.gzipSync(Buffer.from(JSON.stringify(fc)), { level: 9 }); + const file = path.join(OUT_DIR, `${name}.geojson.gz`); + fs.writeFileSync(file, gz); + console.log( + `[atlas-geo] wrote ${path.relative(path.join(__dirname, '..'), file)} — ${features.length} features, ${(gz.length / 1e6).toFixed(1)} MB gz`, + ); + }; - write('admin0', adm0Features) - write('admin1', adm1Features) + write('admin0', adm0Features); + write('admin1', adm1Features); - const missing1 = COUNTRIES.filter((a3, i) => !normalizeAdm1(adm1Raw[i], a3, '').length) - console.log(`[atlas-geo] ADM0 country features: ${adm0Features.length}`) - console.log(`[atlas-geo] ADM1 countries without regions (skipped/404): ${missing1.length}`) - console.log(`[atlas-geo] ADM1 features with ISO 3166-2 code: ${withCodes}/${adm1Features.length}`) + const missing1 = COUNTRIES.filter((a3, i) => !normalizeAdm1(adm1Raw[i], a3, '').length); + console.log(`[atlas-geo] ADM0 country features: ${adm0Features.length}`); + console.log(`[atlas-geo] ADM1 countries without regions (skipped/404): ${missing1.length}`); + console.log(`[atlas-geo] ADM1 features with ISO 3166-2 code: ${withCodes}/${adm1Features.length}`); } -main().catch(err => { console.error(err); process.exit(1) }) +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/server/scripts/build-legacy-fixture-v244.mjs b/server/scripts/build-legacy-fixture-v244.mjs index d8b759486..003e29884 100644 --- a/server/scripts/build-legacy-fixture-v244.mjs +++ b/server/scripts/build-legacy-fixture-v244.mjs @@ -37,7 +37,17 @@ const HEADER = `-- Legacy-runner install at schema_version 244: the ${TAG} relea function bootRelease(tmp) { const archive = path.join(tmp, 'src.tar'); - execFileSync('git', ['-C', REPO, 'archive', '-o', archive, TAG, 'server/src', 'server/package.json', 'server/tsconfig.json']); + execFileSync('git', [ + '-C', + REPO, + 'archive', + '-o', + archive, + TAG, + 'server/src', + 'server/package.json', + 'server/tsconfig.json', + ]); // Relative names: GNU tar reads a drive letter as a remote host. execFileSync('tar', ['-xf', 'src.tar'], { cwd: tmp }); fs.rmSync(archive); @@ -70,14 +80,38 @@ function seed(db) { db.prepare(`UPDATE users SET password_hash = 'fixture-login-disabled' WHERE id = 1`).run(); // Mixed case on purpose: the case-insensitive email index must accept it. - ins('users', { id: 2, username: 'alice', email: 'Alice.Traveller@Example.test', password_hash: 'fixture-login-disabled', display_name: 'Alice' }); + ins('users', { + id: 2, + username: 'alice', + email: 'Alice.Traveller@Example.test', + password_hash: 'fixture-login-disabled', + display_name: 'Alice', + }); ins('users', { id: 3, username: 'bob', email: 'bob@example.test', password_hash: 'fixture-login-disabled' }); ins('settings', { user_id: 2, key: 'temperature_unit', value: 'celsius' }); ins('settings', { user_id: 3, key: 'language', value: 'de' }); // Trip 1 is upcoming and shared; trip 2 lies in the past with a reminder long due. - ins('trips', { id: 1, user_id: 2, title: 'Lisbon long weekend', description: 'Synthetic upgrade fixture trip', start_date: '2027-05-10', end_date: '2027-05-12', currency: 'EUR', reminder_days: 3 }); - ins('trips', { id: 2, user_id: 3, title: 'Alps hiking', start_date: '2026-03-01', end_date: '2026-03-02', currency: 'CHF', reminder_days: 1, is_archived: 1 }); + ins('trips', { + id: 1, + user_id: 2, + title: 'Lisbon long weekend', + description: 'Synthetic upgrade fixture trip', + start_date: '2027-05-10', + end_date: '2027-05-12', + currency: 'EUR', + reminder_days: 3, + }); + ins('trips', { + id: 2, + user_id: 3, + title: 'Alps hiking', + start_date: '2026-03-01', + end_date: '2026-03-02', + currency: 'CHF', + reminder_days: 1, + is_archived: 1, + }); ins('trip_members', { trip_id: 1, user_id: 3, invited_by: 2 }); for (const [id, trip_id, day_number, date, title] of [ @@ -90,16 +124,59 @@ function seed(db) { ins('days', { id, trip_id, day_number, date, title }); } - ins('places', { id: 1, trip_id: 1, name: 'Hotel Alfama', lat: 38.7115, lng: -9.13, address: 'Rua dos Remedios 1, Lisboa', category_id: 1 }); - ins('places', { id: 2, trip_id: 1, name: 'Torre de Belem', lat: 38.6916, lng: -9.216, address: 'Av. Brasilia, Lisboa', category_id: 3, price: 12.5, currency: 'EUR', notes: 'Book online' }); - ins('places', { id: 3, trip_id: 1, name: 'Time Out Market', lat: 38.7069, lng: -9.1459, category_id: 2, place_time: '19:30' }); + ins('places', { + id: 1, + trip_id: 1, + name: 'Hotel Alfama', + lat: 38.7115, + lng: -9.13, + address: 'Rua dos Remedios 1, Lisboa', + category_id: 1, + }); + ins('places', { + id: 2, + trip_id: 1, + name: 'Torre de Belem', + lat: 38.6916, + lng: -9.216, + address: 'Av. Brasilia, Lisboa', + category_id: 3, + price: 12.5, + currency: 'EUR', + notes: 'Book online', + }); + ins('places', { + id: 3, + trip_id: 1, + name: 'Time Out Market', + lat: 38.7069, + lng: -9.1459, + category_id: 2, + place_time: '19:30', + }); ins('places', { id: 4, trip_id: 2, name: 'Gornergrat', lat: 45.9833, lng: 7.7847, category_id: 3 }); - ins('places', { id: 5, trip_id: 1, name: 'Fado bar', lat: 38.7110, lng: -9.1290, category_id: 2 }); + ins('places', { id: 5, trip_id: 1, name: 'Fado bar', lat: 38.711, lng: -9.129, category_id: 2 }); // The canaries tests/helpers/legacy-upgrade-suite.ts asserts on, as in legacy-v242: // user-set times on assignments 1 to 3 of day 2, and the booked night after them. - ins('day_accommodations', { id: 1, trip_id: 1, place_id: 1, start_day_id: 1, end_day_id: 3, check_in: '15:00', check_out: '11:00', confirmation: 'HTL-4242' }); - ins('day_assignments', { id: 1, day_id: 2, place_id: 2, order_index: 0, assignment_time: '09:00', assignment_end_time: '12:00' }); + ins('day_accommodations', { + id: 1, + trip_id: 1, + place_id: 1, + start_day_id: 1, + end_day_id: 3, + check_in: '15:00', + check_out: '11:00', + confirmation: 'HTL-4242', + }); + ins('day_assignments', { + id: 1, + day_id: 2, + place_id: 2, + order_index: 0, + assignment_time: '09:00', + assignment_end_time: '12:00', + }); ins('day_assignments', { id: 2, day_id: 2, place_id: 3, order_index: 1, assignment_time: '18:00' }); ins('day_assignments', { id: 3, day_id: 2, place_id: 5, order_index: 2, assignment_time: '20:00' }); ins('day_assignments', { id: 4, day_id: 2, place_id: 1, order_index: 3, accommodation_id: 1 }); @@ -107,36 +184,203 @@ function seed(db) { ins('day_assignments', { id: 6, day_id: 4, place_id: 4, order_index: 0 }); ins('day_notes', { day_id: 2, trip_id: 1, text: 'Tram 15 to Belem', time: '09:30', sort_order: 0 }); - ins('reservations', { id: 1, trip_id: 1, day_id: 1, title: 'TP 1351 Frankfurt to Lisbon', reservation_time: '2027-05-10T08:15', reservation_end_time: '2027-05-10T10:05', confirmation_number: 'ABC123', status: 'confirmed', type: 'flight' }); - ins('reservation_endpoints', { reservation_id: 1, role: 'from', sequence: 0, name: 'Frankfurt', code: 'FRA', lat: 50.0379, lng: 8.5622, timezone: 'Europe/Berlin', local_time: '08:15', local_date: '2027-05-10' }); - ins('reservation_endpoints', { reservation_id: 1, role: 'to', sequence: 1, name: 'Lisbon', code: 'LIS', lat: 38.7742, lng: -9.1342, timezone: 'Europe/Lisbon', local_time: '10:05', local_date: '2027-05-10' }); - ins('reservations', { id: 2, trip_id: 1, day_id: 1, end_day_id: 3, place_id: 1, accommodation_id: 1, title: 'Hotel Alfama', confirmation_number: 'HTL-4242', status: 'confirmed', type: 'hotel' }); + ins('reservations', { + id: 1, + trip_id: 1, + day_id: 1, + title: 'TP 1351 Frankfurt to Lisbon', + reservation_time: '2027-05-10T08:15', + reservation_end_time: '2027-05-10T10:05', + confirmation_number: 'ABC123', + status: 'confirmed', + type: 'flight', + }); + ins('reservation_endpoints', { + reservation_id: 1, + role: 'from', + sequence: 0, + name: 'Frankfurt', + code: 'FRA', + lat: 50.0379, + lng: 8.5622, + timezone: 'Europe/Berlin', + local_time: '08:15', + local_date: '2027-05-10', + }); + ins('reservation_endpoints', { + reservation_id: 1, + role: 'to', + sequence: 1, + name: 'Lisbon', + code: 'LIS', + lat: 38.7742, + lng: -9.1342, + timezone: 'Europe/Lisbon', + local_time: '10:05', + local_date: '2027-05-10', + }); + ins('reservations', { + id: 2, + trip_id: 1, + day_id: 1, + end_day_id: 3, + place_id: 1, + accommodation_id: 1, + title: 'Hotel Alfama', + confirmation_number: 'HTL-4242', + status: 'confirmed', + type: 'hotel', + }); - ins('budget_items', { id: 1, trip_id: 1, category: 'Accommodation', name: 'Hotel Alfama, two nights', total_price: 312.4, persons: 2, days: 2, paid_by_user_id: 2, expense_date: '2027-05-10', reservation_id: 2, currency: 'EUR' }); + ins('budget_items', { + id: 1, + trip_id: 1, + category: 'Accommodation', + name: 'Hotel Alfama, two nights', + total_price: 312.4, + persons: 2, + days: 2, + paid_by_user_id: 2, + expense_date: '2027-05-10', + reservation_id: 2, + currency: 'EUR', + }); ins('budget_item_members', { budget_item_id: 1, user_id: 2, paid: 1, amount: 156.2 }); ins('budget_item_members', { budget_item_id: 1, user_id: 3, paid: 0, amount: 156.2 }); - ins('budget_items', { id: 2, trip_id: 1, category: 'Food', name: 'Dinner at the market', total_price: 64, persons: 2, paid_by_user_id: 3, currency: 'EUR' }); + ins('budget_items', { + id: 2, + trip_id: 1, + category: 'Food', + name: 'Dinner at the market', + total_price: 64, + persons: 2, + paid_by_user_id: 3, + currency: 'EUR', + }); - ins('packing_bags', { id: 1, trip_id: 1, name: 'Carry-on', color: '#3b82f6', weight_limit_grams: 8000, sort_order: 0, user_id: 2 }); - ins('packing_items', { trip_id: 1, name: 'Passport', checked: 1, category: 'Documents', sort_order: 0, bag_id: 1, quantity: 1 }); - ins('packing_items', { trip_id: 1, name: 'Sunscreen', checked: 0, category: 'Toiletries', sort_order: 1, weight_grams: 150, quantity: 1 }); - ins('packing_items', { trip_id: 1, name: 'Private charger', checked: 0, category: 'Tech', sort_order: 2, is_private: 1, owner_id: 3, quantity: 1 }); + ins('packing_bags', { + id: 1, + trip_id: 1, + name: 'Carry-on', + color: '#3b82f6', + weight_limit_grams: 8000, + sort_order: 0, + user_id: 2, + }); + ins('packing_items', { + trip_id: 1, + name: 'Passport', + checked: 1, + category: 'Documents', + sort_order: 0, + bag_id: 1, + quantity: 1, + }); + ins('packing_items', { + trip_id: 1, + name: 'Sunscreen', + checked: 0, + category: 'Toiletries', + sort_order: 1, + weight_grams: 150, + quantity: 1, + }); + ins('packing_items', { + trip_id: 1, + name: 'Private charger', + checked: 0, + category: 'Tech', + sort_order: 2, + is_private: 1, + owner_id: 3, + quantity: 1, + }); - ins('todo_items', { trip_id: 1, name: 'Buy Lisboa Card', checked: 0, category: 'Tickets', sort_order: 0, due_date: '2027-05-01', assigned_user_id: 3, priority: 1 }); - ins('collab_notes', { trip_id: 1, user_id: 2, category: 'Ideas', title: 'Fado evening', content: 'Ask the hotel for a tip' }); + ins('todo_items', { + trip_id: 1, + name: 'Buy Lisboa Card', + checked: 0, + category: 'Tickets', + sort_order: 0, + due_date: '2027-05-01', + assigned_user_id: 3, + priority: 1, + }); + ins('collab_notes', { + trip_id: 1, + user_id: 2, + category: 'Ideas', + title: 'Fado evening', + content: 'Ask the hotel for a tip', + }); - ins('trip_files', { id: 1, trip_id: 1, reservation_id: 1, filename: 'fixture-boarding-pass.pdf', original_name: 'Boarding pass.pdf', file_size: 48213, mime_type: 'application/pdf', uploaded_by: 2 }); - ins('trip_files', { id: 2, trip_id: 1, place_id: 2, filename: 'fixture-tickets.pdf', original_name: 'Tickets.pdf', file_size: 1024, mime_type: 'application/pdf', uploaded_by: 3, starred: 1 }); + ins('trip_files', { + id: 1, + trip_id: 1, + reservation_id: 1, + filename: 'fixture-boarding-pass.pdf', + original_name: 'Boarding pass.pdf', + file_size: 48213, + mime_type: 'application/pdf', + uploaded_by: 2, + }); + ins('trip_files', { + id: 2, + trip_id: 1, + place_id: 2, + filename: 'fixture-tickets.pdf', + original_name: 'Tickets.pdf', + file_size: 1024, + mime_type: 'application/pdf', + uploaded_by: 3, + starred: 1, + }); ins('tags', { id: 1, user_id: 2, name: 'Must see', color: '#ef4444' }); ins('place_tags', { place_id: 2, tag_id: 1 }); - ins('journeys', { id: 1, user_id: 2, title: 'Portugal 2027', subtitle: 'Three days by the river', status: 'draft', created_at: T, updated_at: T }); + ins('journeys', { + id: 1, + user_id: 2, + title: 'Portugal 2027', + subtitle: 'Three days by the river', + status: 'draft', + created_at: T, + updated_at: T, + }); ins('journey_trips', { journey_id: 1, trip_id: 1, added_at: T }); ins('journey_contributors', { journey_id: 1, user_id: 3, role: 'editor', added_at: T }); - ins('journey_entries', { id: 1, journey_id: 1, source_trip_id: 1, source_place_id: 2, author_id: 2, type: 'entry', title: 'Belem in the sun', story: 'Pasteis first, tower second.', entry_date: '2027-05-11', entry_time: '11:00', location_name: 'Belem', location_lat: 38.6916, location_lng: -9.216, mood: 'amazing', visibility: 'shared', sort_order: 0, created_at: T, updated_at: T }); + ins('journey_entries', { + id: 1, + journey_id: 1, + source_trip_id: 1, + source_place_id: 2, + author_id: 2, + type: 'entry', + title: 'Belem in the sun', + story: 'Pasteis first, tower second.', + entry_date: '2027-05-11', + entry_time: '11:00', + location_name: 'Belem', + location_lat: 38.6916, + location_lng: -9.216, + mood: 'amazing', + visibility: 'shared', + sort_order: 0, + created_at: T, + updated_at: T, + }); - ins('notifications', { type: 'simple', scope: 'trip', target: 1, sender_id: 2, recipient_id: 3, title_key: 'notif.test.title', text_key: 'notif.test.text', is_read: 0 }); + ins('notifications', { + type: 'simple', + scope: 'trip', + target: 1, + sender_id: 2, + recipient_id: 3, + title_key: 'notif.test.title', + text_key: 'notif.test.text', + is_read: 0, + }); } /** Every timestamp the runner or a column default wrote is "now"; pin them so a rebuild is byte-stable. */ diff --git a/server/scripts/coverage-thresholds.mjs b/server/scripts/coverage-thresholds.mjs index cd5953fbb..bef01fbc9 100644 --- a/server/scripts/coverage-thresholds.mjs +++ b/server/scripts/coverage-thresholds.mjs @@ -35,7 +35,10 @@ const summary = JSON.parse(fs.readFileSync(summaryPath, 'utf8')); const domains = new Map(); for (const [file, entry] of Object.entries(summary)) { if (file === 'total') continue; - const match = file.split('\\').join('/').match(/\/src\/nest\/([a-z0-9-]+)\//); + const match = file + .split('\\') + .join('/') + .match(/\/src\/nest\/([a-z0-9-]+)\//); if (!match) continue; const totals = domains.get(match[1]) ?? new Map(METRICS.map(([k]) => [k, { covered: 0, total: 0 }])); for (const [key, metric] of METRICS) { diff --git a/server/scripts/dev.mjs b/server/scripts/dev.mjs index 8063174f2..6ce5a32bc 100644 --- a/server/scripts/dev.mjs +++ b/server/scripts/dev.mjs @@ -4,7 +4,14 @@ console.log('[dev] initial build...'); execSync('node scripts/build.mjs', { stdio: 'inherit' }); const children = []; -const stop = () => { children.forEach((c) => { try { c.kill(); } catch {} }); process.exit(0); }; +const stop = () => { + children.forEach((c) => { + try { + c.kill(); + } catch {} + }); + process.exit(0); +}; process.on('SIGINT', stop); process.on('SIGTERM', stop); diff --git a/server/scripts/dialect-lint.mjs b/server/scripts/dialect-lint.mjs index c8933d735..72681c47b 100644 --- a/server/scripts/dialect-lint.mjs +++ b/server/scripts/dialect-lint.mjs @@ -29,8 +29,8 @@ * * --dir= points the check at another server root (the unit tests use it). */ -import { createRequire } from 'node:module'; import { existsSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; import { join, relative, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -225,9 +225,12 @@ export function readBaseline(path) { throw new Error(`scripts/dialect-baseline.json: ${file} must map rule names to counts`); } for (const [rule, n] of Object.entries(rules)) { - if (!RULE_NAMES.has(rule)) throw new Error(`scripts/dialect-baseline.json: ${file} names an unknown rule "${rule}"`); + if (!RULE_NAMES.has(rule)) + throw new Error(`scripts/dialect-baseline.json: ${file} names an unknown rule "${rule}"`); if (!Number.isInteger(n) || n <= 0) { - throw new Error(`scripts/dialect-baseline.json: ${file} ${rule} holds ${JSON.stringify(n)}, expected a positive integer`); + throw new Error( + `scripts/dialect-baseline.json: ${file} ${rule} holds ${JSON.stringify(n)}, expected a positive integer`, + ); } } } @@ -291,18 +294,27 @@ function main(argv) { console.error(`FAIL ${file}: ${n} SQLite-only ${rule} hit(s), ${allowed} allowed. ${adviceFor(rule)}.`); } for (const { file, rule, allowed, now } of stale) { - console.error(`FAIL ${file} is held at ${allowed} ${rule} hit(s) in scripts/dialect-baseline.json, but has ${now} now.`); + console.error( + `FAIL ${file} is held at ${allowed} ${rule} hit(s) in scripts/dialect-baseline.json, but has ${now} now.`, + ); } if (stale.length) { console.error('Run npm run lint:dialect -- --update to lower the baseline with the change that removed them.'); } - const total = Object.values(baseline).reduce((sum, rules) => sum + Object.values(rules).reduce((a, b) => a + b, 0), 0); - console.log(`dialect: ${Object.keys(baseline).length} file(s) hold ${total} SQLite-only spelling(s) at their baseline`); + const total = Object.values(baseline).reduce( + (sum, rules) => sum + Object.values(rules).reduce((a, b) => a + b, 0), + 0, + ); + console.log( + `dialect: ${Object.keys(baseline).length} file(s) hold ${total} SQLite-only spelling(s) at their baseline`, + ); return grown.length || stale.length ? 1 : 0; } const isCli = - Boolean(process.argv[1]) && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); + Boolean(process.argv[1]) && + existsSync(process.argv[1]) && + realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); if (isCli) { try { process.exitCode = main(process.argv.slice(2)); diff --git a/server/scripts/eslint-ratchet.mjs b/server/scripts/eslint-ratchet.mjs index 0542fc28e..d3b74c85f 100644 --- a/server/scripts/eslint-ratchet.mjs +++ b/server/scripts/eslint-ratchet.mjs @@ -23,10 +23,11 @@ * ESLint comes from this workspace, so the server lints with the major its * config was written for. */ -import { ESLint } from 'eslint'; -import { fileURLToPath } from 'node:url'; import { check, lintWith } from '../../scripts/lib/eslint-warnings.mjs'; import { runCli } from '../../scripts/lib/ratchet.mjs'; +import { ESLint } from 'eslint'; +import { fileURLToPath } from 'node:url'; + const root = fileURLToPath(new URL('..', import.meta.url)); await runCli('lint:warnings', (args) => check({ root, update: args.includes('--update'), lint: lintWith(ESLint) })); diff --git a/server/scripts/extract-templates.cjs b/server/scripts/extract-templates.cjs index dfa256944..f8a2f862e 100644 --- a/server/scripts/extract-templates.cjs +++ b/server/scripts/extract-templates.cjs @@ -6,16 +6,16 @@ * out on an A4 landscape book as it does on the 210mm square it was designed * for. Run from server/, output goes to the client. */ -const Database = require('better-sqlite3') -const fs = require('fs') +const Database = require('better-sqlite3'); +const fs = require('fs'); -const JOURNEY = Number(process.argv[2] || 7) -const OUT = process.argv[3] || '../client/src/components/Studio/bookTemplates.data.ts' +const JOURNEY = Number(process.argv[2] || 7); +const OUT = process.argv[3] || '../client/src/components/Studio/bookTemplates.data.ts'; -const db = new Database('data/travel.db', { readonly: true }) -const row = db.prepare('SELECT document FROM journey_books WHERE journey_id = ?').get(JOURNEY) -db.close() -if (!row) throw new Error(`no book on journey ${JOURNEY}`) +const db = new Database('data/travel.db', { readonly: true }); +const row = db.prepare('SELECT document FROM journey_books WHERE journey_id = ?').get(JOURNEY); +db.close(); +if (!row) throw new Error(`no book on journey ${JOURNEY}`); /* * Through the contract on the way in, so a template carries every field the @@ -23,40 +23,44 @@ if (!row) throw new Error(`no book on journey ${JOURNEY}`) * Without it a template written today is missing tomorrow's defaults and does * not typecheck as a BookElement. */ -const { normalizeBookDocument } = require('@trek/shared') -const doc = normalizeBookDocument(JSON.parse(row.document)) -const PW = doc.page.pageWidth -const PH = doc.page.pageHeight +const { normalizeBookDocument } = require('@trek/shared'); +const doc = normalizeBookDocument(JSON.parse(row.document)); +const PW = doc.page.pageWidth; +const PH = doc.page.pageHeight; -const r4 = n => Math.round(n * 10000) / 10000 +const r4 = (n) => Math.round(n * 10000) / 10000; /** A frame as fractions of one page's width and of the page height. */ function relFrame(f) { - return { x: r4(f.x / PW), y: r4(f.y / PH), w: r4(f.w / PW), h: r4(f.h / PH) } + return { x: r4(f.x / PW), y: r4(f.y / PH), w: r4(f.w / PW), h: r4(f.h / PH) }; } /** Type size as a fraction of the page height, so it scales with the sheet. */ -const relSize = n => r4(n / PH) +const relSize = (n) => r4(n / PH); function element(e) { // The id travels with the template and is replaced when it is applied — // dropping it here would leave the element off the BookElement union. - const out = { ...e, frame: relFrame(e.frame) } - if (typeof out.size === 'number') out.size = relSize(out.size) - if (typeof out.radius === 'number') out.radius = r4(out.radius / PW) - if (typeof out.strokeWidth === 'number') out.strokeWidth = r4(out.strokeWidth / PW) - return out + const out = { ...e, frame: relFrame(e.frame) }; + if (typeof out.size === 'number') out.size = relSize(out.size); + if (typeof out.radius === 'number') out.radius = r4(out.radius / PW); + if (typeof out.strokeWidth === 'number') out.strokeWidth = r4(out.strokeWidth / PW); + return out; } const spreads = doc.spreads .map((s, i) => ({ index: i, role: s.role, background: s.background, elements: s.elements.map(element) })) - .filter(s => s.role === 'inner') + .filter((s) => s.role === 'inner'); -const body = spreads.map((s, i) => ` { +const body = spreads + .map( + (s, i) => ` { id: 'ref-${i + 1}', background: ${JSON.stringify(s.background)}, elements: ${JSON.stringify(s.elements, null, 6).replace(/\n/g, '\n ')}, - },`).join('\n') + },`, + ) + .join('\n'); const file = `import type { BookElement } from '@trek/shared' @@ -103,7 +107,7 @@ export interface SpreadTemplate { export const SPREAD_TEMPLATES: SpreadTemplate[] = [ ${body} ] -` +`; -fs.writeFileSync(OUT, file) -console.log(`wrote ${spreads.length} templates to ${OUT}`) +fs.writeFileSync(OUT, file); +console.log(`wrote ${spreads.length} templates to ${OUT}`); diff --git a/server/scripts/format-baseline.json b/server/scripts/format-baseline.json index 05b3e73ef..e1ce926b3 100644 --- a/server/scripts/format-baseline.json +++ b/server/scripts/format-baseline.json @@ -1,1606 +1,37 @@ [ - "src/addons.ts", - "src/app-config/data-paths.ts", - "src/app-config/derive.ts", - "src/app-config/env.schema.ts", - "src/app-config/parsers.ts", - "src/bootstrap.ts", - "src/config.ts", - "src/db/dialect/kysely-functions.ts", - "src/db/dialect/sql-functions.ts", - "src/db/document-provider-seed.ts", - "src/db/durability.ts", - "src/db/email-case-index.ts", - "src/db/legacy-baseline.ts", - "src/db/migrations/Migration20200101040200_trek_photo_cache_meta_cache_key_not_null.ts", - "src/db/migrations/Migration20200101040300_a_booked_night_finally_took_the_seat_a_new.ts", - "src/db/migrations/Migration20200101042000_tours.ts", - "src/db/migrations/Migration20200101042200_an_index_on_every_foreign_key.ts", - "src/db/migrations/Migration20200101042300_every_tour_type_the_contract_names.ts", - "src/db/nul-safe-sqlite-platform.ts", - "src/db/orm-driver.ts", - "src/db/orm.ts", - "src/db/repositories/AssignmentParticipants.repository.ts", - "src/db/repositories/AuditLog.repository.ts", - "src/db/repositories/BucketList.repository.ts", - "src/db/repositories/BudgetCategoryOrder.repository.ts", - "src/db/repositories/BudgetItemMembers.repository.ts", - "src/db/repositories/BudgetItemPayers.repository.ts", - "src/db/repositories/BudgetItems.repository.ts", - "src/db/repositories/BudgetSettlements.repository.ts", - "src/db/repositories/Categories.repository.ts", - "src/db/repositories/CollabLinks.repository.ts", - "src/db/repositories/CollabMessages.repository.ts", - "src/db/repositories/CollabNotes.repository.ts", - "src/db/repositories/CollabPolls.repository.ts", - "src/db/repositories/CollectionLabels.repository.ts", - "src/db/repositories/CollectionMembers.repository.ts", - "src/db/repositories/CollectionPlaceRatings.repository.ts", - "src/db/repositories/CollectionPlaces.repository.ts", - "src/db/repositories/Collections.repository.ts", - "src/db/repositories/DawarichConnections.repository.ts", - "src/db/repositories/DawarichVisitSuggestions.repository.ts", - "src/db/repositories/DayAccommodations.repository.ts", "src/db/repositories/DayAssignments.repository.ts", - "src/db/repositories/DayNotes.repository.ts", - "src/db/repositories/Days.repository.ts", - "src/db/repositories/DemoRepository.ts", - "src/db/repositories/DocumentConnections.repository.ts", - "src/db/repositories/DocumentProviders.repository.ts", - "src/db/repositories/DocumentSyncItems.repository.ts", - "src/db/repositories/FileLinks.repository.ts", - "src/db/repositories/GoogleApiUsage.repository.ts", - "src/db/repositories/HiddenCountries.repository.ts", - "src/db/repositories/HiddenRegions.repository.ts", - "src/db/repositories/IdempotencyKeys.repository.ts", - "src/db/repositories/InviteTokens.repository.ts", - "src/db/repositories/JourneyBooks.repository.ts", - "src/db/repositories/JourneyContributors.repository.ts", - "src/db/repositories/JourneyEntries.repository.ts", - "src/db/repositories/JourneyEntryPhotos.repository.ts", - "src/db/repositories/JourneyPhotos.repository.ts", - "src/db/repositories/JourneyShareTokens.repository.ts", - "src/db/repositories/JourneyTrips.repository.ts", - "src/db/repositories/Journeys.repository.ts", - "src/db/repositories/MaintenanceRepository.ts", - "src/db/repositories/McpTokens.repository.ts", - "src/db/repositories/Notifications.repository.ts", - "src/db/repositories/OauthClients.repository.ts", - "src/db/repositories/OauthConsents.repository.ts", - "src/db/repositories/OauthTokens.repository.ts", - "src/db/repositories/PackingBags.repository.ts", - "src/db/repositories/PackingCategoryAssignees.repository.ts", - "src/db/repositories/PackingItemContributors.repository.ts", - "src/db/repositories/PackingItems.repository.ts", - "src/db/repositories/PackingTemplateCategories.repository.ts", - "src/db/repositories/PackingTemplateItems.repository.ts", - "src/db/repositories/PackingTemplates.repository.ts", - "src/db/repositories/PasswordResetTokens.repository.ts", - "src/db/repositories/PhotoProviderFields.repository.ts", - "src/db/repositories/Photos.repository.ts", - "src/db/repositories/PlaceDetailsCache.repository.ts", - "src/db/repositories/PlaceRatings.repository.ts", - "src/db/repositories/PlaceRegions.repository.ts", - "src/db/repositories/PlaceShadowPicks.repository.ts", "src/db/repositories/Places.repository.ts", - "src/db/repositories/PluginActions.repository.ts", - "src/db/repositories/PluginCapabilityAudit.repository.ts", - "src/db/repositories/PluginEgressHosts.repository.ts", - "src/db/repositories/PluginEntityMetadata.repository.ts", - "src/db/repositories/PluginErrorLog.repository.ts", - "src/db/repositories/PluginOauthState.repository.ts", - "src/db/repositories/PluginOauthTokens.repository.ts", - "src/db/repositories/PluginScheduledTasks.repository.ts", - "src/db/repositories/PluginSettingsFields.repository.ts", - "src/db/repositories/PluginUserConfig.repository.ts", - "src/db/repositories/PluginUserErasureQueue.repository.ts", - "src/db/repositories/Plugins.repository.ts", - "src/db/repositories/PushSubscriptions.repository.ts", - "src/db/repositories/ReservationDayPositions.repository.ts", - "src/db/repositories/ReservationEndpoints.repository.ts", - "src/db/repositories/ReservationTravelers.repository.ts", "src/db/repositories/Reservations.repository.ts", - "src/db/repositories/RoadtripDayBoundaries.repository.ts", - "src/db/repositories/RoadtripDayTracks.repository.ts", - "src/db/repositories/RoadtripPreferences.repository.ts", - "src/db/repositories/RoadtripVias.repository.ts", - "src/db/repositories/RouteUsageDaily.repository.ts", - "src/db/repositories/SchedulerLeases.repository.ts", - "src/db/repositories/SchoolHolidayCountries.repository.ts", - "src/db/repositories/SchoolHolidayPeriods.repository.ts", - "src/db/repositories/SchoolHolidayRegions.repository.ts", - "src/db/repositories/ShareTokens.repository.ts", - "src/db/repositories/Tags.repository.ts", - "src/db/repositories/TodoCategoryAssignees.repository.ts", - "src/db/repositories/TodoItems.repository.ts", - "src/db/repositories/TourWaypoints.repository.ts", - "src/db/repositories/Tours.repository.ts", - "src/db/repositories/TrekPhotoCacheMeta.repository.ts", - "src/db/repositories/TrekPhotos.repository.ts", - "src/db/repositories/TripAlbumLinks.repository.ts", - "src/db/repositories/TripDocumentLinks.repository.ts", - "src/db/repositories/TripFiles.repository.ts", - "src/db/repositories/TripInviteTokens.repository.ts", - "src/db/repositories/TripMembers.repository.ts", - "src/db/repositories/TripPhotos.repository.ts", "src/db/repositories/Trips.repository.ts", - "src/db/repositories/UserNoticeDismissals.repository.ts", - "src/db/repositories/UserSessions.repository.ts", "src/db/repositories/Users.repository.ts", - "src/db/repositories/VacayCompanyHolidays.repository.ts", - "src/db/repositories/VacayEntries.repository.ts", - "src/db/repositories/VacayHolidayCalendars.repository.ts", - "src/db/repositories/VacayPlanMembers.repository.ts", - "src/db/repositories/VacayPlans.repository.ts", - "src/db/repositories/VacayShares.repository.ts", - "src/db/repositories/VacayUserColors.repository.ts", - "src/db/repositories/VacayUserSettings.repository.ts", - "src/db/repositories/VacayUserYears.repository.ts", - "src/db/repositories/VacayYears.repository.ts", - "src/db/repositories/VisitedCountries.repository.ts", - "src/db/repositories/VisitedRegions.repository.ts", - "src/db/repositories/WebauthnChallenges.repository.ts", - "src/db/repositories/WebauthnCredentials.repository.ts", - "src/db/repositories/_shared/owned-lookup.ts", - "src/db/repositories/_shared/packing-visibility.ts", - "src/db/repositories/_shared/presence-set.ts", - "src/db/repositories/_shared/reservation-travelers-owns.ts", - "src/db/repositories/_shared/reservation-visibility.ts", - "src/db/repositories/_shared/trek-repository.ts", - "src/db/reseat-booked-nights.ts", - "src/db/seeders/AddonSeeder.ts", - "src/db/seeders/DatabaseSeeder.ts", - "src/db/seeders/DocumentProviderSeeder.ts", - "src/db/seeders/PhotoProviderSeeder.ts", - "src/db/seeders/SchemaVersionSeeder.ts", - "src/demo/demo-reset.ts", - "src/demo/demo-seed.ts", - "src/index.ts", - "src/mcp/index.ts", - "src/mcp/scopes.ts", - "src/mcp/sessionManager.ts", - "src/mcp/tools.ts", - "src/middleware/globalMiddleware.ts", - "src/mikro-orm.config.ts", - "src/nest-mcp/registry.ts", - "src/nest/accommodations/accommodations-domain.module.ts", - "src/nest/accommodations/accommodations.controller.ts", - "src/nest/accommodations/accommodations.dto.ts", - "src/nest/accommodations/accommodations.mcp.ts", - "src/nest/accommodations/accommodations.module.ts", - "src/nest/accommodations/accommodations.rpc.ts", - "src/nest/accommodations/accommodations.service.ts", - "src/nest/accommodations/night-seat.ts", - "src/nest/addons/addon-gate.ts", - "src/nest/addons/addon.guard.ts", - "src/nest/addons/addons.controller.ts", - "src/nest/addons/addons.module.ts", - "src/nest/addons/addons.service.ts", - "src/nest/addons/mcp-addon-gate.ts", - "src/nest/admin/admin.controller.ts", - "src/nest/admin/admin.dto.ts", - "src/nest/admin/admin.module.ts", - "src/nest/admin/admin.service.ts", - "src/nest/admin/demo-reset.job.ts", - "src/nest/admin/version-check.job.ts", - "src/nest/airports/airports.controller.ts", - "src/nest/airports/airports.data.ts", - "src/nest/airports/airports.mcp.ts", - "src/nest/airports/airports.module.ts", - "src/nest/airports/airports.service.ts", - "src/nest/app-config/app-config.module.ts", - "src/nest/app-config/data-paths.service.ts", - "src/nest/app-config/runtime-env.service.ts", - "src/nest/app-config/tokens.ts", - "src/nest/app.module.ts", - "src/nest/assignments/assignments-domain.module.ts", - "src/nest/assignments/assignments.controller.ts", - "src/nest/assignments/assignments.dto.ts", - "src/nest/assignments/assignments.mcp.ts", - "src/nest/assignments/assignments.module.ts", - "src/nest/assignments/assignments.service.ts", - "src/nest/assignments/itinerary.rpc.ts", "src/nest/atlas/atlas-geo.ts", - "src/nest/atlas/atlas.controller.ts", - "src/nest/atlas/atlas.dto.ts", - "src/nest/atlas/atlas.mcp.ts", - "src/nest/atlas/atlas.module.ts", - "src/nest/atlas/atlas.rpc.ts", "src/nest/atlas/atlas.service.ts", - "src/nest/atlas/city-from-address.ts", - "src/nest/atlas/known-countries.ts", - "src/nest/atlas/place-regions-repair.job.ts", - "src/nest/atlas/public-stats.controller.ts", - "src/nest/atlas/travel-stats.controller.ts", - "src/nest/audit/audit-log.logger.ts", - "src/nest/audit/audit.module.ts", - "src/nest/audit/audit.service.ts", - "src/nest/audit/entry-trace.logger.ts", - "src/nest/auth/auth-public.controller.ts", - "src/nest/auth/auth.controller.ts", - "src/nest/auth/auth.dto.ts", - "src/nest/auth/auth.helpers.ts", - "src/nest/auth/auth.mcp.ts", - "src/nest/auth/auth.module.ts", "src/nest/auth/auth.service.ts", - "src/nest/auth/passkey-enabled.guard.ts", - "src/nest/auth/passkey.controller.ts", - "src/nest/auth/passkey.service.ts", - "src/nest/auth/registration-invites.service.ts", - "src/nest/auth/session-renewal.interceptor.ts", - "src/nest/auth/sessions.controller.ts", - "src/nest/auth/user-cleanup.service.ts", - "src/nest/auth/user-profile.service.ts", - "src/nest/auth/webauthn-config.service.ts", - "src/nest/backup/auto-backup.job.ts", - "src/nest/backup/auto-backup.settings.ts", - "src/nest/backup/backup-archive.ts", - "src/nest/backup/backup.controller.ts", - "src/nest/backup/backup.dto.ts", - "src/nest/backup/backup.impl.ts", - "src/nest/backup/backup.module.ts", - "src/nest/backup/backup.service.ts", - "src/nest/backup/boot-restore.ts", - "src/nest/backup/database-backup.module.ts", - "src/nest/backup/sqlite-database-backup.ts", - "src/nest/booking-import/booking-import.module.ts", - "src/nest/booking-import/booking-import.service.ts", - "src/nest/booking-import/import-jobs.service.ts", - "src/nest/booking-import/kitinerary-extractor.module.ts", - "src/nest/booking-import/kitinerary-extractor.service.ts", - "src/nest/booking-import/kitinerary-mapper.ts", - "src/nest/booking-import/kitinerary.types.ts", - "src/nest/budget/budget.controller.ts", - "src/nest/budget/budget.dto.ts", - "src/nest/budget/budget.mcp.ts", - "src/nest/budget/budget.module.ts", "src/nest/budget/budget.service.ts", - "src/nest/budget/costs.rpc.ts", - "src/nest/budget/exchange-rates.service.ts", - "src/nest/calendar/calendar.module.ts", - "src/nest/calendar/calendar.service.ts", - "src/nest/categories/categories.controller.ts", - "src/nest/categories/categories.dto.ts", - "src/nest/categories/categories.mcp.ts", - "src/nest/categories/categories.module.ts", - "src/nest/categories/categories.service.ts", - "src/nest/collab/collab.controller.ts", - "src/nest/collab/collab.dto.ts", - "src/nest/collab/collab.mcp.ts", - "src/nest/collab/collab.module.ts", - "src/nest/collab/collab.rpc.ts", - "src/nest/collab/collab.service.ts", - "src/nest/collections/collections.controller.ts", - "src/nest/collections/collections.dto.ts", - "src/nest/collections/collections.mcp.ts", - "src/nest/collections/collections.module.ts", - "src/nest/collections/collections.rpc.ts", "src/nest/collections/collections.service.ts", - "src/nest/common/content-disposition.ts", - "src/nest/common/cookie.ts", - "src/nest/common/crypto/apiKeyCrypto.ts", - "src/nest/common/crypto/mfaCrypto.ts", - "src/nest/common/demo.module.ts", - "src/nest/common/demo.service.ts", - "src/nest/common/demo.ts", - "src/nest/common/geo.ts", - "src/nest/common/idempotency-cleanup.job.ts", - "src/nest/common/idempotency.interceptor.ts", - "src/nest/common/managed.guard.ts", - "src/nest/common/managed.ts", - "src/nest/common/place-image-upload.ts", - "src/nest/common/rate-limit.module.ts", - "src/nest/common/rate-limit.service.ts", - "src/nest/common/rowShape.ts", - "src/nest/common/spool-cleanup.interceptor.ts", - "src/nest/common/timezoneService.ts", - "src/nest/common/trek-exception.filter.ts", - "src/nest/common/validate-body-contracts.ts", - "src/nest/common/validate-managed-routes.ts", - "src/nest/common/validate-route-guards.ts", - "src/nest/common/zod-validation.pipe.ts", - "src/nest/config/config.controller.ts", - "src/nest/config/config.module.ts", - "src/nest/database/database-lifecycle.module.ts", - "src/nest/database/database-lifecycle.service.ts", - "src/nest/database/maintenance.module.ts", - "src/nest/database/orm.module.ts", - "src/nest/database/unit-of-work.ts", - "src/nest/day-notes/day-notes.controller.ts", - "src/nest/day-notes/day-notes.dto.ts", - "src/nest/day-notes/day-notes.mcp.ts", - "src/nest/day-notes/day-notes.module.ts", - "src/nest/day-notes/day-notes.rpc.ts", - "src/nest/day-notes/day-notes.service.ts", - "src/nest/days/day-removal.service.ts", - "src/nest/days/days.controller.ts", - "src/nest/days/days.dto.ts", - "src/nest/days/days.mcp.ts", - "src/nest/days/days.module.ts", - "src/nest/days/days.rpc.ts", - "src/nest/days/days.service.ts", - "src/nest/doc-sync/doc-sync-config.service.ts", - "src/nest/doc-sync/doc-sync-secrets.ts", - "src/nest/doc-sync/doc-sync-webhook.controller.ts", - "src/nest/doc-sync/doc-sync.controller.ts", - "src/nest/doc-sync/doc-sync.dto.ts", - "src/nest/doc-sync/doc-sync.helpers.ts", - "src/nest/doc-sync/doc-sync.job.ts", - "src/nest/doc-sync/doc-sync.mcp.ts", - "src/nest/doc-sync/doc-sync.module.ts", "src/nest/doc-sync/doc-sync.service.ts", - "src/nest/doc-sync/document-provider.registry.ts", - "src/nest/doc-sync/document-provider.ts", - "src/nest/doc-sync/providers/paperless.client.ts", - "src/nest/doc-sync/providers/paperless.provider.ts", - "src/nest/doc-sync/providers/papra.client.ts", - "src/nest/doc-sync/providers/provider-http.ts", "src/nest/doc-sync/providers/synology-drive.client.ts", - "src/nest/doc-sync/providers/synology-drive.provider.ts", - "src/nest/doc-sync/providers/webdav.client.ts", - "src/nest/doc-sync/providers/webdav.provider.ts", - "src/nest/feeds/feeds.controller.ts", - "src/nest/feeds/feeds.mcp.ts", - "src/nest/feeds/feeds.module.ts", - "src/nest/feeds/feeds.service.ts", - "src/nest/files/allowed-file-types.module.ts", - "src/nest/files/allowed-file-types.service.ts", - "src/nest/files/files-download.controller.ts", - "src/nest/files/files.constants.ts", - "src/nest/files/files.controller.ts", - "src/nest/files/files.dto.ts", - "src/nest/files/files.mcp.ts", - "src/nest/files/files.module.ts", - "src/nest/files/files.rpc.ts", - "src/nest/files/files.service.ts", - "src/nest/geo/geo.module.ts", - "src/nest/geo/geocoding.service.ts", - "src/nest/geo/nominatim.client.ts", - "src/nest/google-quota/google-quota.controller.ts", - "src/nest/google-quota/google-quota.dto.ts", - "src/nest/google-quota/google-quota.module.ts", - "src/nest/google-quota/google-quota.service.ts", - "src/nest/health/features.controller.ts", - "src/nest/health/health.module.ts", - "src/nest/help/help.controller.ts", - "src/nest/help/help.mcp.ts", - "src/nest/help/help.module.ts", - "src/nest/help/wiki.ts", - "src/nest/integrations/airtrail-core.module.ts", - "src/nest/integrations/airtrail-import.service.ts", - "src/nest/integrations/airtrail-link.service.ts", - "src/nest/integrations/airtrail-sync.helpers.ts", - "src/nest/integrations/airtrail-sync.job.ts", - "src/nest/integrations/airtrail-sync.service.ts", - "src/nest/integrations/airtrail.client.ts", - "src/nest/integrations/airtrail.controller.ts", - "src/nest/integrations/airtrail.dto.ts", - "src/nest/integrations/airtrail.mcp.ts", - "src/nest/integrations/airtrail.module.ts", - "src/nest/integrations/airtrail.service.ts", - "src/nest/integrations/dawarich-suggestions.service.ts", - "src/nest/integrations/dawarich-sync.job.ts", - "src/nest/integrations/dawarich-sync.service.ts", - "src/nest/integrations/dawarich-tracks.service.ts", - "src/nest/integrations/dawarich.client.ts", - "src/nest/integrations/dawarich.controller.ts", - "src/nest/integrations/dawarich.dto.ts", - "src/nest/integrations/dawarich.helpers.ts", - "src/nest/integrations/dawarich.mcp.ts", - "src/nest/integrations/dawarich.module.ts", - "src/nest/integrations/dawarich.service.ts", - "src/nest/journey/journal-rpc.module.ts", - "src/nest/journey/journal.rpc.ts", - "src/nest/journey/journey-book.service.ts", - "src/nest/journey/journey-domain.module.ts", "src/nest/journey/journey-domain.service.ts", - "src/nest/journey/journey-entry-row.ts", - "src/nest/journey/journey-photo-capture.module.ts", - "src/nest/journey/journey-photo-capture.service.ts", - "src/nest/journey/journey-public.controller.ts", - "src/nest/journey/journey-share.service.ts", - "src/nest/journey/journey-stats.ts", "src/nest/journey/journey.controller.ts", - "src/nest/journey/journey.dto.ts", - "src/nest/journey/journey.mcp.ts", - "src/nest/journey/journey.module.ts", - "src/nest/journey/journey.service.ts", - "src/nest/llm-parse/clients/anthropic.client.ts", - "src/nest/llm-parse/clients/nuextract.ts", - "src/nest/llm-parse/clients/openai-compatible.client.ts", - "src/nest/llm-parse/image-input.ts", - "src/nest/llm-parse/lenient-json.ts", - "src/nest/llm-parse/llm-capabilities.controller.ts", - "src/nest/llm-parse/llm-client.factory.ts", - "src/nest/llm-parse/llm-config.resolver.ts", - "src/nest/llm-parse/llm-config.ts", - "src/nest/llm-parse/llm-local.controller.ts", - "src/nest/llm-parse/llm-local.service.ts", - "src/nest/llm-parse/llm-parse.module.ts", - "src/nest/llm-parse/llm-parse.service.ts", - "src/nest/llm-parse/mime-email.ts", - "src/nest/llm-parse/receipt-read.ts", - "src/nest/llm-parse/router/extraction-router.ts", - "src/nest/llm-parse/router/flat-schemas.ts", - "src/nest/llm-parse/router/ollama-format.client.ts", - "src/nest/llm-parse/text-extract.ts", - "src/nest/maps/maps-nearby.helpers.ts", - "src/nest/maps/maps.controller.ts", - "src/nest/maps/maps.dto.ts", - "src/nest/maps/maps.helpers.ts", - "src/nest/maps/maps.mcp.ts", - "src/nest/maps/maps.module.ts", - "src/nest/maps/maps.service.ts", - "src/nest/maps/providers/amap.provider.ts", - "src/nest/maps/providers/places-provider.ts", - "src/nest/maps/trek-places.client.ts", - "src/nest/mcp-shared/mcp-shared.module.ts", - "src/nest/mcp-shared/mcp-tool-guards.service.ts", - "src/nest/mcp-transport/mcp-transport.constants.ts", - "src/nest/mcp-transport/mcp-transport.controller.ts", - "src/nest/mcp-transport/mcp-transport.module.ts", - "src/nest/mcp-transport/mcp-transport.service.ts", - "src/nest/memories/immich.controller.ts", - "src/nest/memories/immich.service.ts", - "src/nest/memories/journey-thumbs.job.ts", - "src/nest/memories/memories-access.service.ts", - "src/nest/memories/memories.dto.ts", - "src/nest/memories/memories.helpers.ts", - "src/nest/memories/memories.mcp.ts", - "src/nest/memories/memories.module.ts", - "src/nest/memories/memories.service.ts", - "src/nest/memories/photo-capture-backfill.service.ts", - "src/nest/memories/photo-provider.registry.ts", - "src/nest/memories/photo-provider.ts", - "src/nest/memories/photo-resolver.service.ts", - "src/nest/memories/providers/immich.provider.ts", - "src/nest/memories/providers/synology.provider.ts", - "src/nest/memories/synology.controller.ts", - "src/nest/memories/synology.service.ts", - "src/nest/memories/thumbnail.service.ts", - "src/nest/memories/trek-photo-cache.job.ts", - "src/nest/memories/trek-photo-cache.service.ts", - "src/nest/memories/unified-memories.service.ts", - "src/nest/memories/unified.controller.ts", - "src/nest/notifications/channel-registry.ts", - "src/nest/notifications/channels/builtins.ts", - "src/nest/notifications/mailer/email-html.ts", - "src/nest/notifications/mailer/mailer.module.ts", - "src/nest/notifications/mailer/mailer.service.ts", - "src/nest/notifications/mailer/smtp-diagnostics.ts", - "src/nest/notifications/notification-preferences.service.ts", - "src/nest/notifications/notifications.controller.ts", - "src/nest/notifications/notifications.dto.ts", - "src/nest/notifications/notifications.mcp.ts", - "src/nest/notifications/notifications.module.ts", - "src/nest/notifications/notifications.service.ts", - "src/nest/notifications/push/push-subscription.helpers.ts", - "src/nest/notifications/push/push-subscriptions.service.ts", - "src/nest/notifications/push/push.controller.ts", - "src/nest/notifications/push/vapid-keys.service.ts", - "src/nest/notifications/push/web-push-crypto.ts", - "src/nest/notifications/reminder-jobs.service.ts", - "src/nest/notifications/storage-health-notifier.service.ts", - "src/nest/notifications/transports/ntfy.service.ts", - "src/nest/notifications/transports/web-push.service.ts", - "src/nest/notifications/transports/webhook.service.ts", - "src/nest/oauth/oauth-api.controller.ts", - "src/nest/oauth/oauth-public.controller.ts", - "src/nest/oauth/oauth-sdk.provider.ts", - "src/nest/oauth/oauth-token-retention.job.ts", - "src/nest/oauth/oauth.dto.ts", - "src/nest/oauth/oauth.helpers.ts", - "src/nest/oauth/oauth.module.ts", - "src/nest/oauth/oauth.pending-codes.ts", - "src/nest/oauth/oauth.service.ts", - "src/nest/oidc/oidc.controller.ts", - "src/nest/oidc/oidc.module.ts", - "src/nest/oidc/oidc.service.ts", - "src/nest/packing/admin-packing-templates.controller.ts", - "src/nest/packing/packing.controller.ts", - "src/nest/packing/packing.dto.ts", - "src/nest/packing/packing.mcp.ts", - "src/nest/packing/packing.module.ts", - "src/nest/packing/packing.rpc.ts", "src/nest/packing/packing.service.ts", - "src/nest/permissions/permissions.module.ts", - "src/nest/permissions/permissions.service.ts", - "src/nest/permissions/trip-access.guard.ts", - "src/nest/permissions/trip-owner.guard.ts", - "src/nest/permissions/trip.decorator.ts", - "src/nest/photos/photos.controller.ts", - "src/nest/photos/photos.module.ts", - "src/nest/photos/photos.service.ts", - "src/nest/photos/trek-photo-registration.service.ts", - "src/nest/photos/trek-photos.module.ts", - "src/nest/place-enrichment/place-enrichment.controller.ts", - "src/nest/place-enrichment/place-enrichment.dto.ts", - "src/nest/place-enrichment/place-enrichment.module.ts", - "src/nest/place-enrichment/place-enrichment.service.ts", - "src/nest/place-photos/place-photo-cache.job.ts", - "src/nest/place-photos/place-photo-cache.service.ts", - "src/nest/place-photos/place-photos.module.ts", - "src/nest/place-shadow/place-shadow.controller.ts", - "src/nest/place-shadow/place-shadow.dto.ts", - "src/nest/place-shadow/place-shadow.job.ts", - "src/nest/place-shadow/place-shadow.module.ts", - "src/nest/place-shadow/place-shadow.service.ts", - "src/nest/places/places.controller.ts", - "src/nest/places/places.dto.ts", - "src/nest/places/places.helpers.ts", "src/nest/places/places.mcp.ts", - "src/nest/places/places.module.ts", - "src/nest/places/places.rpc.ts", "src/nest/places/places.service.ts", - "src/nest/platform/api-docs.ts", - "src/nest/platform/consent-coop.middleware.ts", - "src/nest/platform/discovery-metadata.service.ts", - "src/nest/platform/discovery.controller.ts", - "src/nest/platform/mcp-metadata.middleware.ts", - "src/nest/platform/platform.module.ts", - "src/nest/platform/platform.routes.ts", - "src/nest/platform/spa-fallback.filter.ts", - "src/nest/plugins/contributions/atlas-layers.controller.ts", - "src/nest/plugins/contributions/day-schedule.controller.ts", - "src/nest/plugins/contributions/day-tints.controller.ts", - "src/nest/plugins/contributions/journal-entry-rows.controller.ts", - "src/nest/plugins/contributions/map-layers.controller.ts", - "src/nest/plugins/contributions/map-markers.controller.ts", - "src/nest/plugins/contributions/pdf-sections.controller.ts", - "src/nest/plugins/contributions/place-details.controller.ts", - "src/nest/plugins/contributions/plugin-calendar.controller.ts", - "src/nest/plugins/contributions/plugin-contributions.module.ts", - "src/nest/plugins/contributions/plugin-mcp-tools.service.ts", - "src/nest/plugins/contributions/plugin-photos.controller.ts", - "src/nest/plugins/contributions/plugin-pois.controller.ts", - "src/nest/plugins/contributions/plugin-pois.helpers.ts", - "src/nest/plugins/contributions/plugin-pois.mcp.ts", - "src/nest/plugins/contributions/plugin-pois.service.ts", - "src/nest/plugins/contributions/plugin-route-normalize.ts", - "src/nest/plugins/contributions/plugin-routes.controller.ts", - "src/nest/plugins/contributions/plugin-search.controller.ts", - "src/nest/plugins/contributions/plugin-search.helpers.ts", - "src/nest/plugins/contributions/plugin-search.mcp.ts", - "src/nest/plugins/contributions/trip-card-contributions.controller.ts", - "src/nest/plugins/contributions/trip-warnings.controller.ts", - "src/nest/plugins/contributions/trip-warnings.mcp.ts", - "src/nest/plugins/contributions/view-contributions.controller.ts", - "src/nest/plugins/dependencies.ts", - "src/nest/plugins/dev-link.ts", - "src/nest/plugins/host/daily-budget.ts", - "src/nest/plugins/host/plugin-audit.ts", - "src/nest/plugins/host/plugin-data.service.ts", - "src/nest/plugins/host/plugin-host-state.ts", - "src/nest/plugins/host/plugin-jobs.ts", - "src/nest/plugins/host/plugin-rpc-host.factory.ts", - "src/nest/plugins/host/rate-limit.ts", - "src/nest/plugins/host/rpc-host.ts", - "src/nest/plugins/host/rpc/db.rpc.ts", - "src/nest/plugins/host/rpc/host-surface.rpc.ts", - "src/nest/plugins/host/rpc/meta.rpc.ts", - "src/nest/plugins/install/discovery.ts", - "src/nest/plugins/install/host-compat.ts", - "src/nest/plugins/install/manifest.ts", - "src/nest/plugins/install/safe-extract.ts", - "src/nest/plugins/install/safe-fetch.ts", - "src/nest/plugins/install/verify-signature.ts", - "src/nest/plugins/mcp-tool-schema.ts", - "src/nest/plugins/oauth/plugin-oauth.controller.ts", - "src/nest/plugins/oauth/plugin-oauth.module.ts", - "src/nest/plugins/oauth/plugin-oauth.service.ts", - "src/nest/plugins/paths.ts", - "src/nest/plugins/plugin-activity.controller.ts", - "src/nest/plugins/plugin-backup.ts", - "src/nest/plugins/plugin-frame.controller.ts", "src/nest/plugins/plugin-runtime.service.ts", - "src/nest/plugins/plugin-user-settings.controller.ts", - "src/nest/plugins/plugin-user-settings.service.ts", - "src/nest/plugins/plugins-feed.controller.ts", - "src/nest/plugins/plugins-proxy.controller.ts", - "src/nest/plugins/plugins-runtime.module.ts", - "src/nest/plugins/plugins.controller.ts", - "src/nest/plugins/plugins.dto.ts", - "src/nest/plugins/plugins.module.ts", - "src/nest/plugins/plugins.service.ts", - "src/nest/plugins/poi-categories.ts", - "src/nest/plugins/protocol/output-contract.ts", - "src/nest/plugins/registry/registry.service.ts", - "src/nest/plugins/runtime/egress-policy.ts", - "src/nest/plugins/runtime/plugin-host-entry.ts", "src/nest/plugins/runtime/plugin-sdk.ts", - "src/nest/plugins/signature-status.ts", - "src/nest/plugins/supervisor/plugin-supervisor.ts", - "src/nest/plugins/text-sanitize.ts", - "src/nest/plugins/user-erasure-enqueue.ts", - "src/nest/public-api/api-token.guard.ts", - "src/nest/public-api/public-api-request.ts", - "src/nest/public-api/public-api.controller.ts", - "src/nest/public-api/public-api.module.ts", - "src/nest/public-api/public-api.service.ts", - "src/nest/query-helpers/query-helpers.module.ts", - "src/nest/query-helpers/query-helpers.service.ts", - "src/nest/realtime/realtime-gateway.module.ts", - "src/nest/realtime/realtime.gateway.ts", - "src/nest/realtime/realtime.module.ts", - "src/nest/realtime/realtime.service.ts", - "src/nest/realtime/trek-ws.adapter.ts", - "src/nest/realtime/ws-state.ts", - "src/nest/receipt-scan/receipt-scan.controller.ts", - "src/nest/receipt-scan/receipt-scan.module.ts", - "src/nest/reservation-import/reservation-import.controller.ts", - "src/nest/reservation-import/reservation-import.dto.ts", - "src/nest/reservation-import/reservation-import.mcp.ts", - "src/nest/reservation-import/reservation-import.module.ts", - "src/nest/reservations/reservations-read.module.ts", - "src/nest/reservations/reservations-read.service.ts", - "src/nest/reservations/reservations.controller.ts", - "src/nest/reservations/reservations.dto.ts", "src/nest/reservations/reservations.mcp.ts", - "src/nest/reservations/reservations.module.ts", - "src/nest/reservations/reservations.rpc.ts", "src/nest/reservations/reservations.service.ts", - "src/nest/reservations/upcoming-reservations.controller.ts", - "src/nest/roadtrip/charging-lookup.controller.ts", - "src/nest/roadtrip/charging.controller.ts", - "src/nest/roadtrip/charging.helpers.ts", - "src/nest/roadtrip/charging.mcp.ts", - "src/nest/roadtrip/charging.service.ts", - "src/nest/roadtrip/day-boundaries.controller.ts", - "src/nest/roadtrip/day-boundaries.mcp.ts", - "src/nest/roadtrip/day-boundaries.service.ts", - "src/nest/roadtrip/google-route.controller.ts", - "src/nest/roadtrip/google-route.mcp.ts", - "src/nest/roadtrip/google-route.service.ts", - "src/nest/roadtrip/roadtrip-hazards.controller.ts", - "src/nest/roadtrip/roadtrip-hazards.mcp.ts", - "src/nest/roadtrip/roadtrip-hazards.service.ts", - "src/nest/roadtrip/roadtrip-mcp.helpers.ts", - "src/nest/roadtrip/roadtrip-plan.service.ts", - "src/nest/roadtrip/roadtrip-planning.mcp.ts", - "src/nest/roadtrip/roadtrip-preferences.controller.ts", - "src/nest/roadtrip/roadtrip-preferences.mcp.ts", - "src/nest/roadtrip/roadtrip-preferences.service.ts", - "src/nest/roadtrip/roadtrip-router.service.ts", - "src/nest/roadtrip/roadtrip-search.controller.ts", - "src/nest/roadtrip/roadtrip-search.service.ts", - "src/nest/roadtrip/roadtrip.controller.ts", - "src/nest/roadtrip/roadtrip.dto.ts", - "src/nest/roadtrip/roadtrip.mcp.ts", - "src/nest/roadtrip/roadtrip.module.ts", - "src/nest/roadtrip/roadtrip.service.ts", - "src/nest/route-usage/route-usage.controller.ts", - "src/nest/route-usage/route-usage.dto.ts", - "src/nest/route-usage/route-usage.job.ts", - "src/nest/route-usage/route-usage.module.ts", - "src/nest/route-usage/route-usage.service.ts", - "src/nest/scheduling/cron-registrar.service.ts", - "src/nest/scheduling/scheduling.module.ts", - "src/nest/school-holidays/school-holidays.controller.ts", - "src/nest/school-holidays/school-holidays.mcp.ts", - "src/nest/school-holidays/school-holidays.module.ts", - "src/nest/school-holidays/school-holidays.service.ts", - "src/nest/sessions/session-purge.job.ts", - "src/nest/sessions/sessions.module.ts", - "src/nest/sessions/sessions.service.ts", - "src/nest/settings/instance-api-keys.ts", - "src/nest/settings/settings.controller.ts", - "src/nest/settings/settings.dto.ts", - "src/nest/settings/settings.mcp.ts", - "src/nest/settings/settings.module.ts", - "src/nest/settings/settings.service.ts", - "src/nest/share/share-view.helpers.ts", - "src/nest/share/share.controller.ts", - "src/nest/share/share.dto.ts", - "src/nest/share/share.mcp.ts", - "src/nest/share/share.module.ts", - "src/nest/share/share.service.ts", - "src/nest/storage/content-type.ts", - "src/nest/storage/drivers/local.driver.ts", - "src/nest/storage/drivers/mirror.driver.ts", - "src/nest/storage/drivers/s3.driver.ts", - "src/nest/storage/http-serving.ts", - "src/nest/storage/storage-admin.controller.ts", - "src/nest/storage/storage-admin.dto.ts", - "src/nest/storage/storage-admin.service.ts", - "src/nest/storage/storage-events.service.ts", - "src/nest/storage/storage-jobs.service.ts", - "src/nest/storage/storage-paths.ts", - "src/nest/storage/storage-probe.ts", - "src/nest/storage/storage-registry.service.ts", - "src/nest/storage/storage-secrets.ts", - "src/nest/storage/storage-stats.service.ts", - "src/nest/storage/storage-upload.factory.ts", - "src/nest/storage/storage-usage-scan.job.ts", - "src/nest/storage/storage.module.ts", - "src/nest/storage/storage.service.ts", - "src/nest/storage/storage.types.ts", - "src/nest/system-notices/system-notices.controller.ts", - "src/nest/system-notices/system-notices.module.ts", - "src/nest/system-notices/system-notices.service.ts", - "src/nest/tags/tags.controller.ts", - "src/nest/tags/tags.mcp.ts", - "src/nest/tags/tags.module.ts", - "src/nest/tags/tags.rpc.ts", - "src/nest/tags/tags.service.ts", - "src/nest/todo/todo.controller.ts", - "src/nest/todo/todo.dto.ts", - "src/nest/todo/todo.mcp.ts", - "src/nest/todo/todo.module.ts", - "src/nest/todo/todo.rpc.ts", - "src/nest/todo/todo.service.ts", - "src/nest/tokens/token.service.ts", - "src/nest/tokens/tokens.module.ts", - "src/nest/tours/dto/tour-create.dto.ts", - "src/nest/tours/tours-import.controller.ts", - "src/nest/tours/tours.controller.ts", - "src/nest/tours/tours.module.ts", - "src/nest/tours/tours.service.ts", - "src/nest/transit/google-transit.provider.ts", - "src/nest/transit/transit-itinerary.helpers.ts", - "src/nest/transit/transit.controller.ts", - "src/nest/transit/transit.mcp.ts", - "src/nest/transit/transit.module.ts", - "src/nest/transit/transit.service.ts", - "src/nest/trip-invite/trip-invite.controller.ts", - "src/nest/trip-invite/trip-invite.dto.ts", - "src/nest/trip-invite/trip-invite.mcp.ts", - "src/nest/trip-invite/trip-invite.module.ts", - "src/nest/trip-invite/trip-invite.service.ts", - "src/nest/trip-members/trip-members.controller.ts", - "src/nest/trip-members/trip-members.module.ts", - "src/nest/trip-members/trip-members.service.ts", - "src/nest/trip-membership/trip-membership.module.ts", - "src/nest/trip-membership/trip-membership.service.ts", - "src/nest/trip-read-model/trip-read-model.module.ts", - "src/nest/trip-read-model/trip-read-model.service.ts", - "src/nest/trips/trip-prompts.mcp.ts", - "src/nest/trips/trips.controller.ts", - "src/nest/trips/trips.dto.ts", - "src/nest/trips/trips.mcp.ts", - "src/nest/trips/trips.module.ts", - "src/nest/trips/trips.rpc.ts", "src/nest/trips/trips.service.ts", - "src/nest/unsplash/unsplash.module.ts", - "src/nest/unsplash/unsplash.service.ts", - "src/nest/vacay/vacay.controller.ts", - "src/nest/vacay/vacay.dto.ts", - "src/nest/vacay/vacay.mcp.ts", - "src/nest/vacay/vacay.module.ts", - "src/nest/vacay/vacay.rpc.ts", "src/nest/vacay/vacay.service.ts", - "src/nest/weather/weather.controller.ts", - "src/nest/weather/weather.impl.ts", - "src/nest/weather/weather.mcp.ts", - "src/nest/weather/weather.module.ts", - "src/nest/weather/weather.service.ts", - "src/plugin-event-sink.ts", - "src/plugin-mcp-tools.ts", - "src/shutdown.ts", - "src/systemNotices/conditions.ts", - "src/systemNotices/registry.ts", - "src/systemNotices/service.ts", - "src/systemNotices/types.ts", - "src/types.ts", - "src/typings/express.d.ts", - "src/utils/cappedFetch.ts", - "src/utils/ipv6.ts", - "src/utils/ssrfGuard.ts", - "tests/e2e/addons.e2e.test.ts", - "tests/e2e/admin.e2e.test.ts", - "tests/e2e/airports.e2e.test.ts", - "tests/e2e/assignments.e2e.test.ts", - "tests/e2e/atlas.e2e.test.ts", - "tests/e2e/auth.e2e.test.ts", - "tests/e2e/backup.e2e.test.ts", - "tests/e2e/booking-import.e2e.test.ts", - "tests/e2e/budget.e2e.test.ts", - "tests/e2e/calendar-feed-visibility.e2e.test.ts", - "tests/e2e/categories.e2e.test.ts", - "tests/e2e/collab.e2e.test.ts", - "tests/e2e/collections.e2e.test.ts", - "tests/e2e/config.e2e.test.ts", - "tests/e2e/dawarich.e2e.test.ts", - "tests/e2e/days.e2e.test.ts", - "tests/e2e/doc-sync.e2e.test.ts", - "tests/e2e/feeds.e2e.test.ts", - "tests/e2e/files.e2e.test.ts", - "tests/e2e/google-quota.e2e.test.ts", - "tests/e2e/harness.ts", - "tests/e2e/journey.e2e.test.ts", - "tests/e2e/mailer.e2e.test.ts", - "tests/e2e/maps.e2e.test.ts", - "tests/e2e/memories.e2e.test.ts", - "tests/e2e/notifications.e2e.test.ts", - "tests/e2e/oauth.e2e.test.ts", - "tests/e2e/oidc.e2e.test.ts", - "tests/e2e/packing.e2e.test.ts", - "tests/e2e/place-enrichment.e2e.test.ts", - "tests/e2e/place-shadow.e2e.test.ts", - "tests/e2e/places.e2e.test.ts", - "tests/e2e/plugin-pois.e2e.test.ts", - "tests/e2e/plugin-search.e2e.test.ts", - "tests/e2e/public-api.e2e.test.ts", - "tests/e2e/push.e2e.test.ts", - "tests/e2e/receipt-scan.e2e.test.ts", - "tests/e2e/reservations.e2e.test.ts", - "tests/e2e/roadtrip.e2e.test.ts", - "tests/e2e/school-holidays.e2e.test.ts", - "tests/e2e/session-revocation.e2e.test.ts", - "tests/e2e/sessions.e2e.test.ts", - "tests/e2e/settings.e2e.test.ts", - "tests/e2e/share.e2e.test.ts", - "tests/e2e/spa-fallback.e2e.test.ts", - "tests/e2e/storage-admin.e2e.test.ts", - "tests/e2e/system-notices.e2e.test.ts", - "tests/e2e/tags.e2e.test.ts", - "tests/e2e/todo.e2e.test.ts", - "tests/e2e/tours.e2e.test.ts", - "tests/e2e/transit-provider.e2e.test.ts", - "tests/e2e/transit.e2e.test.ts", - "tests/e2e/trip-invite.e2e.test.ts", - "tests/e2e/trips.e2e.test.ts", - "tests/e2e/vacay.e2e.test.ts", - "tests/e2e/weather.e2e.test.ts", - "tests/helpers/accommodations-service.ts", - "tests/helpers/atlas-repos.ts", - "tests/helpers/auth.ts", - "tests/helpers/budget-repos.ts", - "tests/helpers/collab-repos.ts", - "tests/helpers/dawarich-repos.ts", - "tests/helpers/db-mock.ts", - "tests/helpers/doc-sync-repos.ts", - "tests/helpers/exif-jpeg.ts", - "tests/helpers/factories.ts", - "tests/helpers/factories/atlas.ts", - "tests/helpers/factories/budget.ts", - "tests/helpers/factories/collab.ts", - "tests/helpers/factories/collections.ts", - "tests/helpers/factories/context.ts", - "tests/helpers/factories/files.ts", - "tests/helpers/factories/itinerary.ts", - "tests/helpers/factories/journeys.ts", - "tests/helpers/factories/notifications.ts", - "tests/helpers/factories/packing.ts", - "tests/helpers/factories/photos.ts", - "tests/helpers/factories/places.ts", - "tests/helpers/factories/plugins.ts", - "tests/helpers/factories/reservations.ts", - "tests/helpers/factories/rows.ts", - "tests/helpers/factories/todos.ts", - "tests/helpers/factories/tokens.ts", - "tests/helpers/factories/tours.ts", - "tests/helpers/factories/trips.ts", - "tests/helpers/factories/users.ts", - "tests/helpers/factories/vacay.ts", - "tests/helpers/files-repos.ts", - "tests/helpers/journey-repos.ts", - "tests/helpers/journey-share-repos.ts", - "tests/helpers/mcp-harness.ts", - "tests/helpers/migration-step.ts", - "tests/helpers/notifications-repos.ts", - "tests/helpers/notifications.ts", - "tests/helpers/packing-repos.ts", - "tests/helpers/plugin-host.ts", - "tests/helpers/rpc-host-deps.ts", - "tests/helpers/school-holidays-repos.ts", - "tests/helpers/sessions.ts", - "tests/helpers/share-repos.ts", - "tests/helpers/storage-fixture.ts", - "tests/helpers/test-addons.ts", - "tests/helpers/test-db.ts", - "tests/helpers/test-orm.ts", - "tests/helpers/test-uow.ts", - "tests/helpers/todo-repos.ts", - "tests/helpers/tours-repos.ts", - "tests/helpers/vacay-repos.ts", - "tests/helpers/ws-client.ts", - "tests/integration/admin.test.ts", - "tests/integration/airtrail-sync-request-context.test.ts", - "tests/integration/api-docs.test.ts", - "tests/integration/app-config.test.ts", - "tests/integration/assignments.test.ts", - "tests/integration/async-closure-truthiness-guards.test.ts", - "tests/integration/atlas-geocode-request-context.test.ts", - "tests/integration/atlas.test.ts", - "tests/integration/auth.test.ts", - "tests/integration/backup.test.ts", - "tests/integration/book-body-size.test.ts", - "tests/integration/boot-sweeps-request-context.test.ts", - "tests/integration/bootstrap.test.ts", - "tests/integration/budget.test.ts", - "tests/integration/categories.test.ts", - "tests/integration/collab.test.ts", - "tests/integration/collections.test.ts", - "tests/integration/database-lifecycle.test.ts", - "tests/integration/dayNotes.test.ts", - "tests/integration/dayReorder.test.ts", - "tests/integration/days.test.ts", - "tests/integration/demo-reset-connection-swap.spike.test.ts", - "tests/integration/demo-seed-request-context.test.ts", - "tests/integration/doc-sync-webhook-request-context.test.ts", - "tests/integration/feeds-request-context.test.ts", - "tests/integration/files.test.ts", - "tests/integration/help.test.ts", - "tests/integration/immich.test.ts", - "tests/integration/journey-place-order.test.ts", - "tests/integration/journey.test.ts", - "tests/integration/leg-mode-incoming.test.ts", - "tests/integration/maps.test.ts", - "tests/integration/mcp.test.ts", - "tests/integration/memories-immich.test.ts", - "tests/integration/memories-synology.test.ts", - "tests/integration/memories-unified.test.ts", - "tests/integration/misc.test.ts", - "tests/integration/notifications.test.ts", - "tests/integration/oauth-authorize.test.ts", - "tests/integration/oauth.test.ts", - "tests/integration/oidc-local-provider.test.ts", - "tests/integration/oidc.test.ts", - "tests/integration/orm-request-context-seams.test.ts", - "tests/integration/packing.test.ts", - "tests/integration/passkey.test.ts", - "tests/integration/places.test.ts", - "tests/integration/plugins/boot-registry-order.test.ts", - "tests/integration/plugins/dev-link.test.ts", - "tests/integration/plugins/plugin-runtime.task2-conversion.test.ts", - "tests/integration/plugins/plugin-runtime.test.ts", - "tests/integration/plugins/plugin-scheduler-sweep.test.ts", - "tests/integration/plugins/registry.test.ts", - "tests/integration/plugins/supervisor.test.ts", - "tests/integration/profile.test.ts", - "tests/integration/reservations.test.ts", - "tests/integration/scheduler-gate.test.ts", - "tests/integration/security.test.ts", - "tests/integration/settings.test.ts", - "tests/integration/share.test.ts", - "tests/integration/storage-health-notifier-request-context.test.ts", - "tests/integration/storage-restore-reload.test.ts", - "tests/integration/storage-stream-abort.test.ts", - "tests/integration/systemNotices.test.ts", - "tests/integration/tags.test.ts", - "tests/integration/todo.test.ts", - "tests/integration/trip-access-primitives.test.ts", - "tests/integration/trips.test.ts", - "tests/integration/uploads-photo-request-context.test.ts", - "tests/integration/uploads-static.test.ts", - "tests/integration/vacay.test.ts", - "tests/setup.console-noise.ts", - "tests/setup.ts", - "tests/unit/app-config/app-url.test.ts", - "tests/unit/app-config/config-ownership.test.ts", - "tests/unit/app-config/config-templates.test.ts", - "tests/unit/app-config/data-paths.test.ts", - "tests/unit/app-config/derive.test.ts", - "tests/unit/app-config/env-reference.test.ts", - "tests/unit/app-config/image-version.test.ts", - "tests/unit/app-config/parsers.test.ts", - "tests/unit/app-config/validate.test.ts", - "tests/unit/auto-backup.test.ts", - "tests/unit/budget/costs.rpc.test.ts", - "tests/unit/config.test.ts", - "tests/unit/days/day-notes.rpc.test.ts", - "tests/unit/days/days-itinerary.rpc.test.ts", - "tests/unit/db/atlas-region-crosswalk.test.ts", - "tests/unit/db/booked-night-reseat.test.ts", - "tests/unit/db/budget-ticket-json-migration.test.ts", - "tests/unit/db/connection.test.ts", - "tests/unit/db/database-lazy-open.test.ts", - "tests/unit/db/dialect/sql-functions.postgres.test.ts", - "tests/unit/db/dialect/sql-functions.test.ts", - "tests/unit/db/durability.test.ts", - "tests/unit/db/email-case-index.test.ts", - "tests/unit/db/entities-index.test.ts", - "tests/unit/db/entities/primary-relations.spike.test.ts", - "tests/unit/db/entity-class-fields.test.ts", - "tests/unit/db/entity-schema-parity.test.ts", - "tests/unit/db/generate-entities.test.ts", - "tests/unit/db/instance-api-key-backfill.test.ts", - "tests/unit/db/known-migrations.test.ts", - "tests/unit/db/legacy-baseline.test.ts", - "tests/unit/db/migrate-encryption-parity.test.ts", - "tests/unit/db/migration.test.ts", - "tests/unit/db/nul-safe-sqlite-platform.test.ts", - "tests/unit/db/repositories/Addons.repository.test.ts", - "tests/unit/db/repositories/AppSettings.repository.test.ts", - "tests/unit/db/repositories/AssignmentParticipants.repository.test.ts", - "tests/unit/db/repositories/AuditLog.repository.test.ts", - "tests/unit/db/repositories/BucketList.repository.test.ts", - "tests/unit/db/repositories/BudgetItems.repository.test.ts", - "tests/unit/db/repositories/Categories.repository.test.ts", - "tests/unit/db/repositories/CollabMessages.repository.test.ts", - "tests/unit/db/repositories/DawarichConnections.repository.test.ts", - "tests/unit/db/repositories/DawarichVisitSuggestions.repository.test.ts", - "tests/unit/db/repositories/DayAccommodations.repository.test.ts", - "tests/unit/db/repositories/DayAssignments.repository.test.ts", - "tests/unit/db/repositories/DayNotes.repository.test.ts", - "tests/unit/db/repositories/Days.repository.test.ts", - "tests/unit/db/repositories/DemoRepository.test.ts", - "tests/unit/db/repositories/DocumentProviders.repository.test.ts", - "tests/unit/db/repositories/DocumentSyncItems.repository.test.ts", - "tests/unit/db/repositories/GooglePlacePhotoMeta.repository.test.ts", - "tests/unit/db/repositories/HiddenRegions.repository.test.ts", - "tests/unit/db/repositories/IdempotencyKeys.repository.test.ts", - "tests/unit/db/repositories/InviteTokens.repository.test.ts", - "tests/unit/db/repositories/MaintenanceRepository.test.ts", - "tests/unit/db/repositories/McpTokens.repository.test.ts", - "tests/unit/db/repositories/NotificationChannelPreferences.repository.test.ts", - "tests/unit/db/repositories/Notifications.repository.test.ts", - "tests/unit/db/repositories/OauthClients.repository.test.ts", - "tests/unit/db/repositories/OauthConsents.repository.test.ts", - "tests/unit/db/repositories/OauthTokens.repository.test.ts", - "tests/unit/db/repositories/PackingItems.repository.test.ts", - "tests/unit/db/repositories/PasswordResetTokens.repository.test.ts", - "tests/unit/db/repositories/PhotoProviderFields.repository.test.ts", - "tests/unit/db/repositories/PhotoProviders.repository.test.ts", - "tests/unit/db/repositories/PlaceDetailsCache.repository.test.ts", - "tests/unit/db/repositories/PlaceRatings.repository.test.ts", - "tests/unit/db/repositories/PlaceRegions.repository.test.ts", - "tests/unit/db/repositories/PlaceShadowPicks.repository.test.ts", "tests/unit/db/repositories/Places.repository.test.ts", - "tests/unit/db/repositories/PluginOauthTokens.repository.test.ts", - "tests/unit/db/repositories/ReservationDayPositions.repository.test.ts", - "tests/unit/db/repositories/ReservationEndpoints.repository.test.ts", - "tests/unit/db/repositories/ReservationTravelers.repository.test.ts", - "tests/unit/db/repositories/Reservations.repository.test.ts", - "tests/unit/db/repositories/RoadtripDayBoundaries.repository.test.ts", - "tests/unit/db/repositories/RoadtripDayTracks.repository.test.ts", - "tests/unit/db/repositories/RoadtripPreferences.repository.test.ts", - "tests/unit/db/repositories/RoadtripVias.repository.test.ts", - "tests/unit/db/repositories/SchedulerLeases.repository.test.ts", - "tests/unit/db/repositories/Settings.repository.test.ts", - "tests/unit/db/repositories/ShareTokens.repository.test.ts", - "tests/unit/db/repositories/Tags.repository.test.ts", - "tests/unit/db/repositories/TodoItems.repository.test.ts", - "tests/unit/db/repositories/TourWaypoints.repository.test.ts", - "tests/unit/db/repositories/Tours.repository.test.ts", - "tests/unit/db/repositories/TripInviteTokens.repository.test.ts", - "tests/unit/db/repositories/TripMembers.repository.test.ts", - "tests/unit/db/repositories/Trips.repository.test.ts", - "tests/unit/db/repositories/UserNoticeDismissals.repository.test.ts", - "tests/unit/db/repositories/UserSessions.repository.test.ts", - "tests/unit/db/repositories/Users.repository.test.ts", - "tests/unit/db/repositories/VacayCompanyHolidays.repository.test.ts", - "tests/unit/db/repositories/VacayHolidayCalendars.repository.test.ts", - "tests/unit/db/repositories/VacayPlanMembers.repository.test.ts", - "tests/unit/db/repositories/VacayPlans.repository.test.ts", - "tests/unit/db/repositories/VacayShares.repository.test.ts", - "tests/unit/db/repositories/VacayUserSettings.repository.test.ts", - "tests/unit/db/repositories/VacayUserYears.repository.test.ts", - "tests/unit/db/repositories/VacayYears.repository.test.ts", - "tests/unit/db/repositories/VisitedCountries.repository.test.ts", - "tests/unit/db/repositories/VisitedRegions.repository.test.ts", - "tests/unit/db/repositories/WebauthnChallenges.repository.test.ts", - "tests/unit/db/repositories/WebauthnCredentials.repository.test.ts", - "tests/unit/db/repositories/_shared/atlas-travowns-consumers.parity.test.ts", - "tests/unit/db/repositories/_shared/collection-role.parity.test.ts", - "tests/unit/db/repositories/_shared/owned-lookup.test.ts", - "tests/unit/db/repositories/_shared/packing-visibility.parity.test.ts", - "tests/unit/db/repositories/_shared/reservation-travelers-owns.parity.test.ts", - "tests/unit/db/repositories/_shared/reservation-visibility.test.ts", - "tests/unit/db/repositories/addons-config-encryption.test.ts", - "tests/unit/db/repositories/atlas-branch-coverage.test.ts", - "tests/unit/db/repositories/bucket-list-duplicate.parity.test.ts", - "tests/unit/db/repositories/identity-map-writeback.test.ts", - "tests/unit/db/repositories/trek-repository.test.ts", - "tests/unit/db/repositories/vacay-branch-coverage.test.ts", - "tests/unit/db/reservation-ingest-state-migration.test.ts", - "tests/unit/db/reset-admin-sessions.test.ts", - "tests/unit/db/roadtrip-preferences-migration.test.ts", - "tests/unit/db/seeders-insert-ignore.test.ts", - "tests/unit/db/seeds-admin.test.ts", - "tests/unit/db/test-orm.test.ts", - "tests/unit/db/tour-types-migration.test.ts", - "tests/unit/db/tours-migration.test.ts", - "tests/unit/db/transaction-yield.test.ts", - "tests/unit/db/trimUserWhitespace.test.ts", - "tests/unit/db/trip-access.test.ts", - "tests/unit/db/trip-days-backfill-migration.test.ts", - "tests/unit/db/trip-files-prefix-migration.test.ts", - "tests/unit/db/types/db-timestamp.type.test.ts", - "tests/unit/db/user-sessions-migration.test.ts", - "tests/unit/demo/demo-reset-path.test.ts", - "tests/unit/demo/demo-reset-sessions.test.ts", - "tests/unit/demo/demo-seed.test.ts", - "tests/unit/docker-entrypoint.test.ts", - "tests/unit/eslint-rules/restricted-syntax-selectors.test.ts", - "tests/unit/files/files.rpc.test.ts", - "tests/unit/helpers/orm-factories.test.ts", - "tests/unit/helpers/test-uow.test.ts", - "tests/unit/idempotency-cleanup.test.ts", - "tests/unit/mcp/nest-mcp-policy.test.ts", - "tests/unit/mcp/scopes.test.ts", - "tests/unit/mcp/sessionManager.test.ts", - "tests/unit/mcp/tools-addon-gating.test.ts", - "tests/unit/mcp/tools-airtrail.test.ts", - "tests/unit/mcp/tools-assignments-reservations-extra.test.ts", - "tests/unit/mcp/tools-assignments.test.ts", - "tests/unit/mcp/tools-atlas-expanded.test.ts", - "tests/unit/mcp/tools-atlas.test.ts", - "tests/unit/mcp/tools-budget-advanced.test.ts", - "tests/unit/mcp/tools-budget.test.ts", - "tests/unit/mcp/tools-categories.test.ts", - "tests/unit/mcp/tools-collab-polls-chat.test.ts", - "tests/unit/mcp/tools-collections.test.ts", - "tests/unit/mcp/tools-days-accommodations.test.ts", - "tests/unit/mcp/tools-days.test.ts", - "tests/unit/mcp/tools-feeds-invite.test.ts", - "tests/unit/mcp/tools-files.test.ts", - "tests/unit/mcp/tools-help-addons.test.ts", - "tests/unit/mcp/tools-journey.test.ts", - "tests/unit/mcp/tools-leg-transport-mode.test.ts", - "tests/unit/mcp/tools-memories.test.ts", - "tests/unit/mcp/tools-notes.test.ts", - "tests/unit/mcp/tools-notifications.test.ts", - "tests/unit/mcp/tools-packing-advanced.test.ts", - "tests/unit/mcp/tools-packing.test.ts", - "tests/unit/mcp/tools-places.test.ts", - "tests/unit/mcp/tools-plugin-pois.test.ts", - "tests/unit/mcp/tools-plugin-search.test.ts", - "tests/unit/mcp/tools-prompts.test.ts", - "tests/unit/mcp/tools-reservations.test.ts", - "tests/unit/mcp/tools-roadtrip.test.ts", - "tests/unit/mcp/tools-settings.test.ts", - "tests/unit/mcp/tools-share.test.ts", - "tests/unit/mcp/tools-tags-maps-weather.test.ts", - "tests/unit/mcp/tools-todos.test.ts", - "tests/unit/mcp/tools-transit.test.ts", - "tests/unit/mcp/tools-transports.test.ts", - "tests/unit/mcp/tools-trip-members.test.ts", - "tests/unit/mcp/tools-trip-warnings.test.ts", - "tests/unit/mcp/tools-trips.test.ts", - "tests/unit/mcp/tools-vacay.test.ts", - "tests/unit/middleware/globalMiddleware.correlation.test.ts", - "tests/unit/middleware/globalMiddleware.cors.test.ts", - "tests/unit/middleware/globalMiddleware.csp.test.ts", - "tests/unit/middleware/globalMiddleware.redaction.test.ts", - "tests/unit/nest-mcp/attach.test.ts", - "tests/unit/nest-mcp/create-test-registry.test.ts", - "tests/unit/nest-mcp/dynamic-tools.test.ts", - "tests/unit/nest/accommodations.controller.test.ts", - "tests/unit/nest/accommodations.service.test.ts", - "tests/unit/nest/addon-guard.test.ts", - "tests/unit/nest/addons.controller.test.ts", - "tests/unit/nest/addons.service.test.ts", - "tests/unit/nest/admin-default-user-settings.controller.test.ts", - "tests/unit/nest/admin-jobs.test.ts", - "tests/unit/nest/admin-notification-preferences.controller.test.ts", - "tests/unit/nest/admin-packing-templates.controller.test.ts", - "tests/unit/nest/admin.controller.test.ts", - "tests/unit/nest/admin.service.test.ts", - "tests/unit/nest/admin.version-notification.test.ts", - "tests/unit/nest/airports.controller.test.ts", - "tests/unit/nest/airports/airports.data.test.ts", - "tests/unit/nest/airports/airports.service.test.ts", - "tests/unit/nest/airtrail-sync.job.test.ts", - "tests/unit/nest/airtrail.service.test.ts", - "tests/unit/nest/app-clock.test.ts", - "tests/unit/nest/app-config.test.ts", - "tests/unit/nest/assignments.controller.test.ts", - "tests/unit/nest/assignments.service.test.ts", - "tests/unit/nest/atlas-transfer-endpoints.test.ts", - "tests/unit/nest/atlas-visit-dates.test.ts", - "tests/unit/nest/atlas.controller.test.ts", "tests/unit/nest/atlas.service.test.ts", - "tests/unit/nest/atlasBundle.test.ts", - "tests/unit/nest/audit-log.logger.test.ts", - "tests/unit/nest/audit.service.test.ts", - "tests/unit/nest/auth.controller.test.ts", - "tests/unit/nest/auth.helpers.test.ts", - "tests/unit/nest/auth.service.test.ts", - "tests/unit/nest/auth/sessions.controller.test.ts", - "tests/unit/nest/auto-backup-settings.test.ts", - "tests/unit/nest/backup-archive-version.test.ts", - "tests/unit/nest/backup.controller.test.ts", - "tests/unit/nest/backup.impl.test.ts", - "tests/unit/nest/booking-import/booking-import.service.test.ts", - "tests/unit/nest/booking-import/import-jobs.service.test.ts", - "tests/unit/nest/booking-import/kitinerary-extractor.service.test.ts", - "tests/unit/nest/booking-import/kitineraryMapper.test.ts", - "tests/unit/nest/booking-import/reservation-import.controller.test.ts", - "tests/unit/nest/boot-restore.test.ts", - "tests/unit/nest/budget.controller.test.ts", - "tests/unit/nest/budget.service.calc.test.ts", "tests/unit/nest/budget.service.db.test.ts", - "tests/unit/nest/budget.service.test.ts", - "tests/unit/nest/cache-sweep-jobs.test.ts", "tests/unit/nest/calendar.service.test.ts", - "tests/unit/nest/categories.controller.test.ts", - "tests/unit/nest/categories.service.test.ts", - "tests/unit/nest/channel-override.test.ts", - "tests/unit/nest/channel-registry.test.ts", - "tests/unit/nest/charging.test.ts", - "tests/unit/nest/city-from-address.test.ts", - "tests/unit/nest/collab.controller.test.ts", - "tests/unit/nest/collab.service.test.ts", - "tests/unit/nest/collections.controller.test.ts", "tests/unit/nest/collections.service.test.ts", - "tests/unit/nest/common/avatarUrl.test.ts", - "tests/unit/nest/common/conflictUpdate.test.ts", - "tests/unit/nest/common/cookie.test.ts", - "tests/unit/nest/common/crypto/apiKeyCrypto.test.ts", - "tests/unit/nest/common/crypto/mfaCrypto.test.ts", - "tests/unit/nest/common/passwordPolicy.test.ts", - "tests/unit/nest/common/request-correlation.test.ts", - "tests/unit/nest/common/row-id.test.ts", - "tests/unit/nest/common/rowShape.test.ts", - "tests/unit/nest/compare.test.ts", - "tests/unit/nest/config.controller.test.ts", - "tests/unit/nest/content-disposition.test.ts", - "tests/unit/nest/cron-registrar.service.test.ts", - "tests/unit/nest/database/database-lifecycle.service.test.ts", - "tests/unit/nest/database/request-context.test.ts", - "tests/unit/nest/database/unit-of-work.d5.test.ts", - "tests/unit/nest/database/unit-of-work.test.ts", - "tests/unit/nest/dawarich-suggestions.service.test.ts", - "tests/unit/nest/dawarich-sync.job.test.ts", - "tests/unit/nest/dawarich-sync.service.test.ts", - "tests/unit/nest/dawarich-tracks.service.test.ts", - "tests/unit/nest/dawarich.client.test.ts", - "tests/unit/nest/dawarich.controller.test.ts", - "tests/unit/nest/dawarich.helpers.test.ts", - "tests/unit/nest/dawarich.mcp.test.ts", - "tests/unit/nest/dawarich.service.test.ts", - "tests/unit/nest/day-boundaries.test.ts", - "tests/unit/nest/day-notes.service.test.ts", - "tests/unit/nest/day-removal.service.test.ts", - "tests/unit/nest/days.controller.test.ts", - "tests/unit/nest/days.service.test.ts", - "tests/unit/nest/demo-service-survivor-sites.test.ts", - "tests/unit/nest/demo-write.test.ts", - "tests/unit/nest/demo.service.test.ts", - "tests/unit/nest/discovery.controller.test.ts", - "tests/unit/nest/doc-sync/doc-sync-config.service.test.ts", - "tests/unit/nest/doc-sync/doc-sync-webhook.controller.test.ts", - "tests/unit/nest/doc-sync/doc-sync.controller.test.ts", - "tests/unit/nest/doc-sync/doc-sync.helpers.test.ts", - "tests/unit/nest/doc-sync/doc-sync.job.test.ts", - "tests/unit/nest/doc-sync/doc-sync.mcp.test.ts", "tests/unit/nest/doc-sync/doc-sync.service.test.ts", - "tests/unit/nest/doc-sync/paperless.provider.test.ts", - "tests/unit/nest/doc-sync/papra.provider.test.ts", - "tests/unit/nest/doc-sync/provider-http.test.ts", - "tests/unit/nest/doc-sync/synology-drive.provider.test.ts", - "tests/unit/nest/doc-sync/webdav.provider.test.ts", - "tests/unit/nest/entry-trace.logger.test.ts", - "tests/unit/nest/exception-filter.test.ts", - "tests/unit/nest/exchange-rates.service.test.ts", - "tests/unit/nest/features.controller.test.ts", - "tests/unit/nest/feeds.service.test.ts", - "tests/unit/nest/files.controller.test.ts", - "tests/unit/nest/files.service.test.ts", - "tests/unit/nest/files/upload-filters.test.ts", - "tests/unit/nest/geo.nominatim.test.ts", - "tests/unit/nest/google-quota.service.test.ts", - "tests/unit/nest/google-route.test.ts", - "tests/unit/nest/google-transit.provider.test.ts", - "tests/unit/nest/idempotency.interceptor.test.ts", - "tests/unit/nest/immich.service.test.ts", - "tests/unit/nest/integrations/airtrailImport.test.ts", - "tests/unit/nest/integrations/airtrailMapper.test.ts", - "tests/unit/nest/integrations/airtrailSync.test.ts", - "tests/unit/nest/integrations/airtrailTestConnection.test.ts", - "tests/unit/nest/integrations/airtrailWriteGate.test.ts", - "tests/unit/nest/integrations/airtrailWriteback.test.ts", - "tests/unit/nest/journey-book.service.test.ts", "tests/unit/nest/journey-domain.service.test.ts", - "tests/unit/nest/journey-photo-capture.service.test.ts", - "tests/unit/nest/journey-share.service.test.ts", - "tests/unit/nest/journey-stats-trips.test.ts", - "tests/unit/nest/journey-stats.service.test.ts", - "tests/unit/nest/journey-stats.test.ts", - "tests/unit/nest/journey.controller.test.ts", - "tests/unit/nest/llm-parse/clients.test.ts", - "tests/unit/nest/llm-parse/currency-code.test.ts", - "tests/unit/nest/llm-parse/extraction-router.test.ts", - "tests/unit/nest/llm-parse/image-input.test.ts", - "tests/unit/nest/llm-parse/lenient-json.test.ts", - "tests/unit/nest/llm-parse/llm-capabilities.controller.test.ts", - "tests/unit/nest/llm-parse/llm-client.factory.test.ts", - "tests/unit/nest/llm-parse/llm-config.resolver.test.ts", - "tests/unit/nest/llm-parse/llm-config.test.ts", - "tests/unit/nest/llm-parse/llm-local.service.test.ts", - "tests/unit/nest/llm-parse/llm-parse.service.test.ts", - "tests/unit/nest/llm-parse/llm-prompt.test.ts", - "tests/unit/nest/llm-parse/mime-email.test.ts", - "tests/unit/nest/llm-parse/nuextract.test.ts", - "tests/unit/nest/llm-parse/ollama-format.client.test.ts", - "tests/unit/nest/llm-parse/receipt-read.test.ts", - "tests/unit/nest/llm-parse/text-extract.test.ts", - "tests/unit/nest/log-file.test.ts", - "tests/unit/nest/mailer.service.test.ts", - "tests/unit/nest/managed-ext.test.ts", - "tests/unit/nest/managed-inert.test.ts", - "tests/unit/nest/managed-keys.test.ts", - "tests/unit/nest/managed.guard.test.ts", - "tests/unit/nest/managed.test.ts", - "tests/unit/nest/maps.amap.test.ts", - "tests/unit/nest/maps.area.test.ts", - "tests/unit/nest/maps.autocomplete.test.ts", - "tests/unit/nest/maps.controller.test.ts", - "tests/unit/nest/maps.curated-images.test.ts", - "tests/unit/nest/maps.geocode.test.ts", - "tests/unit/nest/maps.gers-details.test.ts", - "tests/unit/nest/maps.google-quota.test.ts", - "tests/unit/nest/maps.helpers.test.ts", - "tests/unit/nest/maps.identity.test.ts", - "tests/unit/nest/maps.merge-search.test.ts", - "tests/unit/nest/maps.nearby.test.ts", - "tests/unit/nest/maps.pois.test.ts", - "tests/unit/nest/maps.search-merge.test.ts", "tests/unit/nest/maps.service.test.ts", - "tests/unit/nest/mcp-addon-gate.test.ts", - "tests/unit/nest/mcp-gated-addons.parity.test.ts", - "tests/unit/nest/mcp-shared.service.test.ts", - "tests/unit/nest/mcp-transport.service.test.ts", - "tests/unit/nest/memories-access.service.test.ts", - "tests/unit/nest/memories.controller.test.ts", - "tests/unit/nest/memories.helpers.test.ts", - "tests/unit/nest/memories.service.test.ts", - "tests/unit/nest/memories/journey-thumbs.job.test.ts", - "tests/unit/nest/memories/trek-photo-cache.job.test.ts", - "tests/unit/nest/notification-preferences.service.test.ts", - "tests/unit/nest/notification-transports.test.ts", - "tests/unit/nest/notifications.controller.test.ts", - "tests/unit/nest/notifications.inapp-prefs.test.ts", - "tests/unit/nest/notifications.service.test.ts", - "tests/unit/nest/notifications/in-app-actions.test.ts", - "tests/unit/nest/notifications/push-subscriptions.service.test.ts", - "tests/unit/nest/notifications/push.controller.test.ts", - "tests/unit/nest/notifications/replica-failure-debouncer.test.ts", - "tests/unit/nest/notifications/storage-health-notifier.service.test.ts", - "tests/unit/nest/notifications/vapid-keys.service.test.ts", - "tests/unit/nest/notifications/web-push-crypto.test.ts", - "tests/unit/nest/notifications/web-push-dispatch.test.ts", - "tests/unit/nest/notifications/web-push.service.test.ts", - "tests/unit/nest/oauth-redirect-uri.test.ts", - "tests/unit/nest/oauth-sdk.provider.test.ts", - "tests/unit/nest/oauth-token-retention.job.test.ts", - "tests/unit/nest/oauth.controller.test.ts", - "tests/unit/nest/oauth.service.test.ts", - "tests/unit/nest/oidc-ssrf.test.ts", - "tests/unit/nest/oidc.controller.test.ts", - "tests/unit/nest/oidc.service.test.ts", - "tests/unit/nest/packing.controller.test.ts", - "tests/unit/nest/packing.service.test.ts", - "tests/unit/nest/passkey.service.test.ts", - "tests/unit/nest/permissions.service.test.ts", - "tests/unit/nest/photo-capture-backfill.exif.test.ts", - "tests/unit/nest/photo-capture-backfill.service.test.ts", - "tests/unit/nest/photo-provider.test.ts", - "tests/unit/nest/photo-resolver.service.test.ts", - "tests/unit/nest/photos/trekPhotoMedia.test.ts", - "tests/unit/nest/place-enrichment.brand.test.ts", - "tests/unit/nest/place-enrichment.controller.test.ts", - "tests/unit/nest/place-enrichment.description.test.ts", - "tests/unit/nest/place-enrichment.service.test.ts", - "tests/unit/nest/place-photos/place-photo-cache.job.test.ts", - "tests/unit/nest/place-photos/place-photo-cache.service.test.ts", - "tests/unit/nest/place-regions-repair.test.ts", - "tests/unit/nest/place-shadow.controller.test.ts", - "tests/unit/nest/place-shadow.job.test.ts", - "tests/unit/nest/place-shadow.service.test.ts", - "tests/unit/nest/places.controller.test.ts", - "tests/unit/nest/places.directions.test.ts", - "tests/unit/nest/places.enrichment.test.ts", - "tests/unit/nest/places.helpers.test.ts", "tests/unit/nest/places.service.test.ts", - "tests/unit/nest/platform.controller.test.ts", - "tests/unit/nest/process-state-ports.test.ts", - "tests/unit/nest/public-api-scopes.test.ts", - "tests/unit/nest/public-api.controller.test.ts", - "tests/unit/nest/public-api.guard.test.ts", - "tests/unit/nest/public-api.service.test.ts", - "tests/unit/nest/public-stats.controller.test.ts", - "tests/unit/nest/query-helpers.service.test.ts", - "tests/unit/nest/rate-limit.service.test.ts", - "tests/unit/nest/realtime.gateway.test.ts", - "tests/unit/nest/realtime.service.test.ts", - "tests/unit/nest/receipt-scan.controller.test.ts", - "tests/unit/nest/registration-invites.service.test.ts", - "tests/unit/nest/reminder-jobs.service.test.ts", - "tests/unit/nest/reservations.controller.test.ts", "tests/unit/nest/reservations.service.test.ts", - "tests/unit/nest/roadtrip-hazards.test.ts", - "tests/unit/nest/roadtrip-mcp.helpers.test.ts", - "tests/unit/nest/roadtrip-plan.service.test.ts", - "tests/unit/nest/roadtrip-planning.test.ts", - "tests/unit/nest/roadtrip-read-tools.test.ts", - "tests/unit/nest/roadtrip-router.test.ts", - "tests/unit/nest/roadtrip-search.test.ts", - "tests/unit/nest/roadtrip.controller.test.ts", - "tests/unit/nest/roadtrip.service.test.ts", - "tests/unit/nest/route-inventory.test.ts", - "tests/unit/nest/route-usage.service.test.ts", - "tests/unit/nest/school-holidays.service.test.ts", - "tests/unit/nest/session-renewal.interceptor.test.ts", - "tests/unit/nest/sessions/session-purge.job.test.ts", - "tests/unit/nest/sessions/sessions.service.test.ts", - "tests/unit/nest/settings.controller.test.ts", - "tests/unit/nest/settings.service.test.ts", - "tests/unit/nest/settings/instance-api-keys.test.ts", - "tests/unit/nest/share-view.helpers.test.ts", - "tests/unit/nest/share.controller.test.ts", - "tests/unit/nest/share.service.test.ts", - "tests/unit/nest/smtp-diagnostics.test.ts", - "tests/unit/nest/spool-cleanup.interceptor.test.ts", - "tests/unit/nest/sqlite-database-backup.test.ts", - "tests/unit/nest/storage/content-type.test.ts", - "tests/unit/nest/storage/http-serving.test.ts", - "tests/unit/nest/storage/local.driver.test.ts", - "tests/unit/nest/storage/mirror-backfill.test.ts", - "tests/unit/nest/storage/mirror.driver.test.ts", - "tests/unit/nest/storage/s3.driver.contract.test.ts", - "tests/unit/nest/storage/s3.driver.test.ts", - "tests/unit/nest/storage/storage-admin.controller.test.ts", - "tests/unit/nest/storage/storage-admin.service.test.ts", - "tests/unit/nest/storage/storage-driver.contract.ts", - "tests/unit/nest/storage/storage-jobs.service.test.ts", - "tests/unit/nest/storage/storage-keys.test.ts", - "tests/unit/nest/storage/storage-probe.test.ts", - "tests/unit/nest/storage/storage-registry.service.test.ts", - "tests/unit/nest/storage/storage-secrets.test.ts", - "tests/unit/nest/storage/storage-stats.service.test.ts", - "tests/unit/nest/storage/storage-upload.factory.test.ts", - "tests/unit/nest/storage/storage-usage-scan.job.test.ts", - "tests/unit/nest/storage/storage.service.test.ts", - "tests/unit/nest/strip-html-tags.test.ts", - "tests/unit/nest/synology.service.test.ts", - "tests/unit/nest/system-notices.branch-coverage.test.ts", - "tests/unit/nest/system-notices.controller.test.ts", - "tests/unit/nest/system-notices.service.test.ts", - "tests/unit/nest/tags.controller.test.ts", - "tests/unit/nest/tags.service.test.ts", - "tests/unit/nest/thumbnail.service.test.ts", - "tests/unit/nest/to-client-user.parity.test.ts", - "tests/unit/nest/todo.controller.test.ts", - "tests/unit/nest/todo.service.test.ts", - "tests/unit/nest/token.service.test.ts", - "tests/unit/nest/tours.controller.test.ts", - "tests/unit/nest/tours.gpx.atomic.test.ts", - "tests/unit/nest/tours.permissions.boundaries.mock-only.test.ts", - "tests/unit/nest/tours.permissions.test.ts", - "tests/unit/nest/tours.service.permissions.test.ts", - "tests/unit/nest/tours.service.test.ts", - "tests/unit/nest/transit-itinerary.helpers.test.ts", - "tests/unit/nest/transit.service.test.ts", - "tests/unit/nest/travel-stats.service.test.ts", - "tests/unit/nest/trek-photo-cache.service.test.ts", - "tests/unit/nest/trek-photo-registration.service.test.ts", - "tests/unit/nest/trek-places.client.test.ts", - "tests/unit/nest/trek-ws-adapter.test.ts", - "tests/unit/nest/trip-access.guard.test.ts", - "tests/unit/nest/trip-invite.service.test.ts", - "tests/unit/nest/trip-members.controller.test.ts", - "tests/unit/nest/trip-members.service.test.ts", - "tests/unit/nest/trip-membership/trip-membership.service.test.ts", - "tests/unit/nest/trip-owner.guard.test.ts", - "tests/unit/nest/trip-read-model.service.test.ts", - "tests/unit/nest/trips.controller.test.ts", "tests/unit/nest/trips.service.test.ts", - "tests/unit/nest/unified-memories.service.test.ts", - "tests/unit/nest/unsplash/unsplash.service.test.ts", - "tests/unit/nest/user-cleanup.service.test.ts", - "tests/unit/nest/user-profile.service.test.ts", - "tests/unit/nest/vacay.controller.test.ts", - "tests/unit/nest/vacay.service.test.ts", - "tests/unit/nest/validate-body-contracts.test.ts", - "tests/unit/nest/validate-managed-routes.test.ts", - "tests/unit/nest/weather.controller.test.ts", - "tests/unit/nest/weather/weather-cache-lifecycle.test.ts", - "tests/unit/nest/weather/weather.impl.test.ts", - "tests/unit/nest/webauthn-config.service.test.ts", - "tests/unit/nest/wiki.test.ts", - "tests/unit/nest/wiring.test.ts", - "tests/unit/nest/ws-bind-failure.test.ts", - "tests/unit/nest/zod-pipe.test.ts", - "tests/unit/packing/packing.rpc.test.ts", - "tests/unit/places/places.rpc.test.ts", - "tests/unit/plugins/addon-domains-errors.rpc.test.ts", - "tests/unit/plugins/addon-gates.rpc.test.ts", - "tests/unit/plugins/atlas-layers.controller.test.ts", - "tests/unit/plugins/daily-budget.test.ts", - "tests/unit/plugins/day-schedule.controller.test.ts", - "tests/unit/plugins/day-tints.controller.test.ts", - "tests/unit/plugins/dependencies.test.ts", - "tests/unit/plugins/discovery.test.ts", - "tests/unit/plugins/egress-policy.test.ts", - "tests/unit/plugins/event-facts.test.ts", - "tests/unit/plugins/event-subscriptions.test.ts", - "tests/unit/plugins/host-compat.test.ts", - "tests/unit/plugins/host-rpc.test.ts", - "tests/unit/plugins/journal-entry-rows.controller.test.ts", - "tests/unit/plugins/journal-photo.rpc.test.ts", - "tests/unit/plugins/manifest.test.ts", - "tests/unit/plugins/map-layers.controller.test.ts", - "tests/unit/plugins/map-markers.controller.test.ts", - "tests/unit/plugins/mcp-session-invalidation.test.ts", - "tests/unit/plugins/mcp-tool-annotations.test.ts", - "tests/unit/plugins/mcp-tool-enforcement.test.ts", - "tests/unit/plugins/mcp-tool-schema.test.ts", - "tests/unit/plugins/mcp-tool-sink.test.ts", - "tests/unit/plugins/native-scan.test.ts", - "tests/unit/plugins/operator-egress.test.ts", - "tests/unit/plugins/output-contract.schema.test.ts", - "tests/unit/plugins/output-contract.test.ts", - "tests/unit/plugins/pdf-sections.controller.test.ts", - "tests/unit/plugins/place-details.controller.test.ts", - "tests/unit/plugins/plugin-audit.test.ts", - "tests/unit/plugins/plugin-backup.test.ts", - "tests/unit/plugins/plugin-data.test.ts", - "tests/unit/plugins/plugin-event-meta.test.ts", - "tests/unit/plugins/plugin-frame.test.ts", - "tests/unit/plugins/plugin-hooks.test.ts", - "tests/unit/plugins/plugin-host-state.test.ts", - "tests/unit/plugins/plugin-instance-settings.test.ts", - "tests/unit/plugins/plugin-jobs.cron-compat.test.ts", - "tests/unit/plugins/plugin-jobs.test.ts", - "tests/unit/plugins/plugin-mcp-tools.test.ts", - "tests/unit/plugins/plugin-oauth.controller.test.ts", - "tests/unit/plugins/plugin-oauth.service.test.ts", - "tests/unit/plugins/plugin-photos-calendar.controller.test.ts", - "tests/unit/plugins/plugin-pois.test.ts", - "tests/unit/plugins/plugin-repository-sql-parity.test.ts", - "tests/unit/plugins/plugin-retrust.controller.test.ts", - "tests/unit/plugins/plugin-route-normalize.test.ts", - "tests/unit/plugins/plugin-routes.controller.test.ts", - "tests/unit/plugins/plugin-runtime.boot-no-orm.test.ts", - "tests/unit/plugins/plugin-sdk.test.ts", - "tests/unit/plugins/plugin-search.controller.test.ts", - "tests/unit/plugins/plugin-user-settings.controller.test.ts", - "tests/unit/plugins/plugin-user-settings.test.ts", - "tests/unit/plugins/plugins-controller-devlink.test.ts", - "tests/unit/plugins/plugins-feed.controller.test.ts", - "tests/unit/plugins/plugins-proxy.test.ts", - "tests/unit/plugins/plugins-service.test.ts", - "tests/unit/plugins/poi-categories.test.ts", - "tests/unit/plugins/protocol-paths.test.ts", - "tests/unit/plugins/provider-hook-grant.test.ts", - "tests/unit/plugins/rate-limit.test.ts", - "tests/unit/plugins/registry-preview.test.ts", - "tests/unit/plugins/request-context.test.ts", - "tests/unit/plugins/rpc-coverage.test.ts", - "tests/unit/plugins/rpc-fallbacks.test.ts", - "tests/unit/plugins/rpc-host-inv-strip.test.ts", - "tests/unit/plugins/rpc-host.test.ts", - "tests/unit/plugins/rpc-trace.test.ts", - "tests/unit/plugins/safe-extract.test.ts", - "tests/unit/plugins/safe-fetch.test.ts", - "tests/unit/plugins/settings-isolation.test.ts", - "tests/unit/plugins/sql-script.test.ts", - "tests/unit/plugins/supervisor-lifecycle.test.ts", - "tests/unit/plugins/tenant-free.rpc.test.ts", - "tests/unit/plugins/text-sanitize.test.ts", - "tests/unit/plugins/trip-card-contributions.controller.test.ts", - "tests/unit/plugins/trip-warnings.controller.test.ts", - "tests/unit/plugins/verify-signature.test.ts", - "tests/unit/plugins/view-contributions.controller.test.ts", - "tests/unit/reservations/bookings.rpc.test.ts", - "tests/unit/scripts/db-call-graph.test.ts", - "tests/unit/scripts/dialect-lint.test.ts", - "tests/unit/scripts/pg-probe/arg-samples.test.ts", - "tests/unit/scripts/pg-probe/baseline.test.ts", - "tests/unit/scripts/pg-probe/helper-cases.test.ts", - "tests/unit/scripts/pg-probe/recorder.test.ts", - "tests/unit/scripts/pg-probe/report.test.ts", - "tests/unit/scripts/pg-probe/schema.test.ts", - "tests/unit/scripts/test-sql-ratchet.test.ts", - "tests/unit/scripts/tx-writes.test.ts", - "tests/unit/shared-contract.test.ts", - "tests/unit/shutdown.test.ts", - "tests/unit/systemNotices/conditions.test.ts", - "tests/unit/systemNotices/registry.test.ts", - "tests/unit/systemNotices/versionRange.test.ts", - "tests/unit/tags/tags.rpc.test.ts", - "tests/unit/todo/todo.rpc.test.ts", - "tests/unit/trips/trips.rpc.test.ts", - "tests/unit/uploads-dirs.test.ts", - "tests/unit/utils/cappedFetch.test.ts", - "tests/unit/utils/ipv6.test.ts", - "tests/unit/utils/ssrf-dual-stack.test.ts", - "tests/unit/utils/ssrf-llm-timeout.test.ts", - "tests/unit/utils/ssrf-proxy.test.ts", - "tests/unit/utils/ssrf-response-limits.test.ts", - "tests/unit/utils/ssrfGuard.test.ts", - "tests/websocket/connection.test.ts", - "tests/websocket/wire-envelope.test.ts" + "tests/unit/nest/vacay.service.test.ts" ] diff --git a/server/scripts/format-lint.mjs b/server/scripts/format-lint.mjs index f371ababb..ddf9aebc0 100644 --- a/server/scripts/format-lint.mjs +++ b/server/scripts/format-lint.mjs @@ -22,11 +22,12 @@ * scripts/lib/format.mjs at the repository root; Prettier and its config come * from this workspace. */ -import { fileURLToPath } from 'node:url'; -import * as prettier from 'prettier'; import { check } from '../../scripts/lib/format.mjs'; import { runCli } from '../../scripts/lib/ratchet.mjs'; +import { fileURLToPath } from 'node:url'; +import * as prettier from 'prettier'; + const root = fileURLToPath(new URL('..', import.meta.url)); // The entity files are written by scripts/generate-entities.ts and held byte for // byte to its output by check:entities, so their layout is the generator's, not diff --git a/server/scripts/gen-plugin-facts.ts b/server/scripts/gen-plugin-facts.ts index 6be512c26..9af28aab6 100644 --- a/server/scripts/gen-plugin-facts.ts +++ b/server/scripts/gen-plugin-facts.ts @@ -15,30 +15,60 @@ * plugin-sdk/ checkout with no server present, so a build-time artefact would ship an SDK * with an empty permission list. */ -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; -import { - HOOK_PERMISSION, KNOWN_METHODS, KNOWN_PERMISSIONS, METHOD_PERMISSION, - EVENTS_PERMISSION, JOBS_PERMISSION, USER_DATA_PERMISSION, HTTP_OUTBOUND_PREFIX, PLUGIN_API_VERSION, -} from '../src/nest/plugins/protocol/envelope'; -import { - ACTIONS_MAX, ADDON_ID_RE, CAPABILITY_NAME_RE, EGRESS_HOST_RE, MCP_TOOLS_MAX, PLUGIN_CHANNEL_EVENTS, PLUGIN_ID_RE, - PLUGIN_SEMVER_RE, PLUGIN_TYPES, REPLACEABLE_TABS, RESERVED_PLUGIN_IDS, RESERVED_SETTING_KEYS, ROUTE_PROFILE_ID_RE, - ROUTE_PROFILES_MAX, SETTING_FIELD_KEYS, SETTING_KEY_RE, TOOL_NAME_RE, TRIP_PAGE_POSITION_MAX, WIDGET_SLOTS, -} from '../src/nest/plugins/protocol/manifest-rules'; -import { SNAPSHOT_GRANT, ENTITY_ID_KEYS } from '../src/plugin-event-sink'; -import { - PLUGIN_ENTITY_CONTRACT, PLUGIN_ENTITY_NESTED, PLUGIN_METHOD_OUTPUT, pluginEntityFields, type PluginEntityName, - type PluginMethodOutput, -} from '../src/nest/plugins/protocol/output-contract'; -import { ADDON_IDS } from '../src/addons'; // A relative import of the shared SOURCE, not @trek/shared: the plugin-facts CI job // installs only the server, so shared's dist does not exist there. The file has no // imports of its own for exactly this reason. import { - PLUGIN_POI_ICONS, PLUGIN_POI_LABEL_MAX, PLUGIN_POI_MAX_CATEGORIES, + PLUGIN_POI_ICONS, + PLUGIN_POI_LABEL_MAX, + PLUGIN_POI_MAX_CATEGORIES, } from '../../shared/src/plugins/plugin-poi-facts'; +import { ADDON_IDS } from '../src/addons'; +import { + HOOK_PERMISSION, + KNOWN_METHODS, + KNOWN_PERMISSIONS, + METHOD_PERMISSION, + EVENTS_PERMISSION, + JOBS_PERMISSION, + USER_DATA_PERMISSION, + HTTP_OUTBOUND_PREFIX, + PLUGIN_API_VERSION, +} from '../src/nest/plugins/protocol/envelope'; +import { + ACTIONS_MAX, + ADDON_ID_RE, + CAPABILITY_NAME_RE, + EGRESS_HOST_RE, + MCP_TOOLS_MAX, + PLUGIN_CHANNEL_EVENTS, + PLUGIN_ID_RE, + PLUGIN_SEMVER_RE, + PLUGIN_TYPES, + REPLACEABLE_TABS, + RESERVED_PLUGIN_IDS, + RESERVED_SETTING_KEYS, + ROUTE_PROFILE_ID_RE, + ROUTE_PROFILES_MAX, + SETTING_FIELD_KEYS, + SETTING_KEY_RE, + TOOL_NAME_RE, + TRIP_PAGE_POSITION_MAX, + WIDGET_SLOTS, +} from '../src/nest/plugins/protocol/manifest-rules'; +import { + PLUGIN_ENTITY_CONTRACT, + PLUGIN_ENTITY_NESTED, + PLUGIN_METHOD_OUTPUT, + pluginEntityFields, + type PluginEntityName, + type PluginMethodOutput, +} from '../src/nest/plugins/protocol/output-contract'; +import { SNAPSHOT_GRANT, ENTITY_ID_KEYS } from '../src/plugin-event-sink'; + +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; const REPO = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..'); @@ -54,7 +84,9 @@ const HEADER = [ const list = (xs: readonly string[]) => xs.map((x) => ` '${x}',`).join('\n'); const pairs = (o: Readonly>) => - Object.entries(o).map(([k, v]) => ` ${/^[A-Za-z_$][\w$]*$/.test(k) ? k : `'${k}'`}: '${v}',`).join('\n'); + Object.entries(o) + .map(([k, v]) => ` ${/^[A-Za-z_$][\w$]*$/.test(k) ? k : `'${k}'`}: '${v}',`) + .join('\n'); /** * Types here are deliberately WIDE — Readonly> and string[] — @@ -261,9 +293,7 @@ function coverageProblems(): string[] { const ui = fs.readFileSync(path.join(REPO, 'plugin-sdk/src/cli/ui.ts'), 'utf8'); const missingHint = KNOWN_PERMISSIONS.filter((p) => !ui.includes(`'${p}'`)); if (missingHint.length) { - problems.push( - `plugin-sdk/src/cli/ui.ts PERMISSION_FAMILIES does not cover: ${missingHint.join(', ')}`, - ); + problems.push(`plugin-sdk/src/cli/ui.ts PERMISSION_FAMILIES does not cover: ${missingHint.join(', ')}`); } return problems; diff --git a/server/scripts/generate-db-types.ts b/server/scripts/generate-db-types.ts index d88bcfe92..b5a9d2f58 100644 --- a/server/scripts/generate-db-types.ts +++ b/server/scripts/generate-db-types.ts @@ -25,13 +25,11 @@ * node --import tsx scripts/generate-db-types.ts # rewrite src/db/kysely/ * node --import tsx scripts/generate-db-types.ts --check # exit 1 when src/db/kysely/ drifted (CI) */ - -import fs from 'node:fs'; -import path from 'node:path'; +import { migrateTempDb, tempDbPath } from './generate-entities'; import Database from 'better-sqlite3'; - -import { migrateTempDb, tempDbPath } from './generate-entities'; +import fs from 'node:fs'; +import path from 'node:path'; const SERVER_ROOT = path.join(__dirname, '..'); export const KYSELY_DIR = path.join(SERVER_ROOT, 'src/db/kysely'); @@ -39,7 +37,8 @@ export const KYSELY_DIR = path.join(SERVER_ROOT, 'src/db/kysely'); /** Tables the application never queries: the migrator's own bookkeeping. */ const SKIPPED_TABLES: ReadonlySet = new Set(['mikro_orm_migrations']); -const HEADER = '// Generated by scripts/generate-db-types.ts from the migrated schema. Do not edit by hand:\n' + +const HEADER = + '// Generated by scripts/generate-db-types.ts from the migrated schema. Do not edit by hand:\n' + '// change the migration, then run `npm run gen:db-types`.\n'; export interface ColumnInfo { @@ -151,7 +150,7 @@ export function renderDb(tables: readonly TableInfo[]): string { const body = tables.map((t) => ` ${propertyKey(t.name)}: ${interfaceName(t.name)};`).join('\n'); return ( `${HEADER}${imports}\n\n` + - '/** Every table of the migrated schema. A repository narrows it: `this.kysely>()`. */\n' + + "/** Every table of the migrated schema. A repository narrows it: `this.kysely>()`. */\n" + `export interface DB {\n${body}\n}\n` ); } diff --git a/server/scripts/import-boundaries.mjs b/server/scripts/import-boundaries.mjs index 06eaf764c..e20dc1f42 100644 --- a/server/scripts/import-boundaries.mjs +++ b/server/scripts/import-boundaries.mjs @@ -68,15 +68,23 @@ * * --dir= points the check at another server root (the unit tests use it). */ -import { createRequire } from 'node:module'; import { existsSync, readdirSync, readFileSync, realpathSync, statSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; import { dirname, join, relative, resolve, sep } from 'node:path'; import { fileURLToPath } from 'node:url'; const require = createRequire(import.meta.url); const ts = require('typescript'); -export const RULES = ['fileCycles', 'domainCycles', 'sharedImportsDomain', 'domainInternals', 'dbImportsNest', 'foreignRepositories', 'providersImportOrchestrator']; +export const RULES = [ + 'fileCycles', + 'domainCycles', + 'sharedImportsDomain', + 'domainInternals', + 'dbImportsNest', + 'foreignRepositories', + 'providersImportOrchestrator', +]; /** * Folders under src/nest whose every file is shared infrastructure, open to all domains. @@ -148,7 +156,9 @@ function isTypeOnlyImport(node) { function isTypeOnlyExport(node) { if (node.isTypeOnly) return true; const clause = node.exportClause; - return Boolean(clause && ts.isNamedExports(clause) && clause.elements.length > 0 && clause.elements.every((el) => el.isTypeOnly)); + return Boolean( + clause && ts.isNamedExports(clause) && clause.elements.length > 0 && clause.elements.every((el) => el.isTypeOnly), + ); } /** Every relative import of a file: { spec, runtime } where runtime means eager and not erased. */ @@ -180,7 +190,8 @@ export function importsOf(text, fileName = 'file.ts') { ts.forEachChild(node, (child) => visit(child, inner)); }; for (const stmt of sf.statements) { - if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt) && !ts.isImportEqualsDeclaration(stmt)) visit(stmt, 0); + if (!ts.isImportDeclaration(stmt) && !ts.isExportDeclaration(stmt) && !ts.isImportEqualsDeclaration(stmt)) + visit(stmt, 0); } return found; } @@ -412,7 +423,8 @@ export function analyse(edges) { if (a && b && SHARED_DOMAINS.has(a) && !SHARED_DOMAINS.has(b)) sharedImportsDomain.add(`${e.from} -> ${e.to}`); if (a && b && a !== b && !isPublic(e.to)) domainInternals.add(`${a} -> ${e.to.slice('nest/'.length)}`); if (e.from.startsWith('db/') && e.to.startsWith('nest/')) dbImportsNest.add(`${e.from} -> ${e.to}`); - if (isProviderFile(e.from) && isOrchestratorOf(e.from, e.to)) providersImportOrchestrator.add(`${e.from} -> ${e.to}`); + if (isProviderFile(e.from) && isOrchestratorOf(e.from, e.to)) + providersImportOrchestrator.add(`${e.from} -> ${e.to}`); } const sorted = (set) => [...set].sort(); return { @@ -494,14 +506,17 @@ function main(argv) { ); if (unowned.length) { // An entity without an owner is an injection the rule cannot judge. - for (const u of unowned) console.error(`FAIL no owner for injected entity ${u}: add it to scripts/repository-owners.json`); + for (const u of unowned) + console.error(`FAIL no owner for injected entity ${u}: add it to scripts/repository-owners.json`); return 1; } found.foreignRepositories = foreignRepositories; let baseline = readBaseline(baselinePath); if (update) { const now = Object.fromEntries(RULES.map((rule) => [rule, new Set(found[rule])])); - baseline = Object.fromEntries(RULES.map((rule) => [rule, baseline[rule].filter((key) => now[rule].has(key)).sort()])); + baseline = Object.fromEntries( + RULES.map((rule) => [rule, baseline[rule].filter((key) => now[rule].has(key)).sort()]), + ); writeFileSync(baselinePath, JSON.stringify(baseline, null, 2) + '\n'); } @@ -526,14 +541,17 @@ function main(argv) { ); } console.log( - `boundaries: ${edges.length} import(s); held at baseline: ` + RULES.map((rule) => `${rule} ${baseline[rule].length}`).join(', '), + `boundaries: ${edges.length} import(s); held at baseline: ` + + RULES.map((rule) => `${rule} ${baseline[rule].length}`).join(', '), ); return failed ? 1 : 0; } // Compared by real path, so the check still runs when the script is started through a symlink. const isCli = - Boolean(process.argv[1]) && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); + Boolean(process.argv[1]) && + existsSync(process.argv[1]) && + realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); if (isCli) { try { process.exitCode = main(process.argv.slice(2)); diff --git a/server/scripts/migrate-encryption.ts b/server/scripts/migrate-encryption.ts index 38d2e6829..d6ffe1323 100644 --- a/server/scripts/migrate-encryption.ts +++ b/server/scripts/migrate-encryption.ts @@ -14,12 +14,11 @@ * The script will prompt for the old and new keys interactively so they never * appear in shell history, process arguments, or log output. */ - +import Database from 'better-sqlite3'; import crypto from 'crypto'; import fs from 'fs'; import path from 'path'; import readline from 'readline'; -import Database from 'better-sqlite3'; // --------------------------------------------------------------------------- // Crypto helpers — mirrors apiKeyCrypto.ts and mfaCrypto.ts but with @@ -143,9 +142,7 @@ async function main() { console.log('A backup of the database will be created before any changes are made.\n'); // Resolve DB path - const dbPath = path.resolve( - process.env.DB_PATH ?? path.join(__dirname, '../data/travel.db') - ); + const dbPath = path.resolve(process.env.DB_PATH ?? path.join(__dirname, '../data/travel.db')); if (!fs.existsSync(dbPath)) { console.error(`Database not found at: ${dbPath}`); @@ -213,14 +210,18 @@ async function main() { const wantedJournal = (process.env.TREK_DB_JOURNAL_MODE ?? '').trim().toUpperCase(); const journalMode = journalModes.includes(wantedJournal) ? wantedJournal : 'WAL'; if (wantedJournal && journalMode !== wantedJournal) { - console.warn(`TREK_DB_JOURNAL_MODE="${process.env.TREK_DB_JOURNAL_MODE}" is not a SQLite journal mode — using ${journalMode}.`); + console.warn( + `TREK_DB_JOURNAL_MODE="${process.env.TREK_DB_JOURNAL_MODE}" is not a SQLite journal mode — using ${journalMode}.`, + ); } const wantedSync = (process.env.TREK_DB_SYNCHRONOUS ?? '').trim().toUpperCase(); const defaultSync = journalMode === 'WAL' ? 'NORMAL' : 'FULL'; const synchronous = syncLevels.includes(wantedSync) ? wantedSync : defaultSync; if (wantedSync && synchronous !== wantedSync) { - console.warn(`TREK_DB_SYNCHRONOUS="${process.env.TREK_DB_SYNCHRONOUS}" is not a SQLite synchronous level — using ${synchronous}.`); + console.warn( + `TREK_DB_SYNCHRONOUS="${process.env.TREK_DB_SYNCHRONOUS}" is not a SQLite synchronous level — using ${synchronous}.`, + ); } db.pragma(`journal_mode = ${journalMode}`); @@ -257,7 +258,10 @@ async function main() { // Helper: migrate a single MFA value (no prefix, raw base64) function migrateMfaValue(raw: string, label: string): string | null { - if (!raw) { result.skipped++; return null; } + if (!raw) { + result.skipped++; + return null; + } const plain = decryptMfa(raw, oldKey); if (plain !== null) { @@ -335,7 +339,10 @@ async function main() { const users = db.prepare('SELECT id FROM users').all() as { id: number }[]; for (const user of users) { - const row = db.prepare(`SELECT ${apiKeyColumns.join(', ')} FROM users WHERE id = ?`).get(user.id) as Record; + const row = db.prepare(`SELECT ${apiKeyColumns.join(', ')} FROM users WHERE id = ?`).get(user.id) as Record< + string, + string + >; for (const col of apiKeyColumns) { if (!row[col]) continue; @@ -346,7 +353,8 @@ async function main() { } // mfa_secret (mfa crypto) - const mfaRow = db.prepare('SELECT mfa_secret FROM users WHERE id = ? AND mfa_secret IS NOT NULL').get(user.id) as { mfa_secret: string } | undefined; + const mfaRow = db.prepare('SELECT mfa_secret FROM users WHERE id = ? AND mfa_secret IS NOT NULL').get(user.id) as + { mfa_secret: string } | undefined; if (mfaRow?.mfa_secret) { const newVal = migrateMfaValue(mfaRow.mfa_secret, `users[${user.id}].mfa_secret`); if (newVal !== null) { @@ -373,8 +381,7 @@ async function main() { }; { const row = db.prepare("SELECT value FROM app_settings WHERE key = 'storage.backends'").get() as - | { value: string } - | undefined; + { value: string } | undefined; if (row?.value) { let backends: unknown; try { @@ -418,9 +425,11 @@ async function main() { // --- settings: per-user encrypted keys --- const encryptedSettingKeys = ['webhook_url', 'ntfy_token', 'mapbox_access_token', 'carto_api_key', 'llm_api_key']; - const settingRows = db.prepare( - `SELECT user_id, key, value FROM settings WHERE key IN (${encryptedSettingKeys.map(() => '?').join(', ')})` - ).all(...encryptedSettingKeys) as { user_id: number; key: string; value: string }[]; + const settingRows = db + .prepare( + `SELECT user_id, key, value FROM settings WHERE key IN (${encryptedSettingKeys.map(() => '?').join(', ')})`, + ) + .all(...encryptedSettingKeys) as { user_id: number; key: string; value: string }[]; for (const row of settingRows) { if (!row.value) continue; @@ -465,7 +474,7 @@ async function main() { // Which keys are secret is per plugin and per scope, and it lives in the DB // rather than in this script, so nothing has to be pinned by hand here. const secretFields = db - .prepare("SELECT plugin_id, field_key, scope FROM plugin_settings_fields WHERE secret = 1") + .prepare('SELECT plugin_id, field_key, scope FROM plugin_settings_fields WHERE secret = 1') .all() as { plugin_id: string; field_key: string; scope: string | null }[]; const secretKeysFor = (pluginId: string, scope: string): string[] => secretFields @@ -500,7 +509,10 @@ async function main() { }; if (tableExists('plugins')) { - const pluginRows = db.prepare('SELECT id, config FROM plugins').all() as { id: string; config: string | null }[]; + const pluginRows = db.prepare('SELECT id, config FROM plugins').all() as { + id: string; + config: string | null; + }[]; for (const row of pluginRows) { const next = migrateConfigJson(row.config, secretKeysFor(row.id, 'instance'), `plugins[${row.id}].config`); if (next !== null) { @@ -510,9 +522,11 @@ async function main() { } if (tableExists('plugin_user_config')) { - const userConfigRows = db - .prepare('SELECT plugin_id, user_id, config FROM plugin_user_config') - .all() as { plugin_id: string; user_id: number; config: string | null }[]; + const userConfigRows = db.prepare('SELECT plugin_id, user_id, config FROM plugin_user_config').all() as { + plugin_id: string; + user_id: number; + config: string | null; + }[]; for (const row of userConfigRows) { const next = migrateConfigJson( row.config, @@ -535,9 +549,10 @@ async function main() { // (llm-parse/llm-config.ts). Left out, LLM parsing quietly runs without a // key after the rotation, since a failed decrypt reads as "no key". if (tableExists('addons')) { - const addonRows = db - .prepare('SELECT id, config FROM addons WHERE config IS NOT NULL') - .all() as { id: string; config: string }[]; + const addonRows = db.prepare('SELECT id, config FROM addons WHERE config IS NOT NULL').all() as { + id: string; + config: string; + }[]; for (const row of addonRows) { let config: unknown; try { @@ -559,7 +574,10 @@ async function main() { } // --- trip_album_links: passphrase --- - const albumLinks = db.prepare('SELECT id, passphrase FROM trip_album_links WHERE passphrase IS NOT NULL').all() as { id: number; passphrase: string }[]; + const albumLinks = db.prepare('SELECT id, passphrase FROM trip_album_links WHERE passphrase IS NOT NULL').all() as { + id: number; + passphrase: string; + }[]; for (const row of albumLinks) { const newVal = migrateApiKeyValue(row.passphrase, `trip_album_links[${row.id}].passphrase`); if (newVal !== null) { @@ -584,7 +602,10 @@ async function main() { } // --- trek_photos: passphrase --- - const photos = db.prepare('SELECT id, passphrase FROM trek_photos WHERE passphrase IS NOT NULL').all() as { id: number; passphrase: string }[]; + const photos = db.prepare('SELECT id, passphrase FROM trek_photos WHERE passphrase IS NOT NULL').all() as { + id: number; + passphrase: string; + }[]; for (const row of photos) { const newVal = migrateApiKeyValue(row.passphrase, `trek_photos[${row.id}].passphrase`); if (newVal !== null) { @@ -640,7 +661,7 @@ async function main() { console.log(` Skipped (empty): ${result.skipped}`); if (result.errors.length > 0) { console.warn(` Errors: ${result.errors.length}`); - result.errors.forEach(e => console.warn(` - ${e}`)); + result.errors.forEach((e) => console.warn(` - ${e}`)); console.warn('\nSome secrets could not be migrated. Check the errors above.'); console.warn(`Your original database is backed up at: ${backupPath}`); process.exit(1); @@ -654,7 +675,9 @@ async function main() { fs.writeFileSync(keyFile, newKey, { mode: 0o600 }); console.log(`Key file updated: ${keyFile}`); } else { - console.log('No key file next to the database. Store the new key wherever TREK reads it from (ENCRYPTION_KEY or data/.encryption_key).'); + console.log( + 'No key file next to the database. Store the new key wherever TREK reads it from (ENCRYPTION_KEY or data/.encryption_key).', + ); } console.log(`Backup retained at: ${backupPath}`); } diff --git a/server/scripts/pg-probe.ts b/server/scripts/pg-probe.ts index 4d0975670..3d9cb2baf 100644 --- a/server/scripts/pg-probe.ts +++ b/server/scripts/pg-probe.ts @@ -50,17 +50,31 @@ * Exit codes: 0 passed, 1 a helper case or the ratchet failed, 2 the probe * could not run (no database, a database that is not empty, a crash). */ +import { planRepositories, readCompilerOptions, type MethodPlan } from './pg-probe/arg-samples'; +import { compareWithBaseline, formatBaseline, lowerBaseline, readBaseline } from './pg-probe/baseline'; +import { + connectProbeOrm, + connectSetupOrm, + postgresHelperEngine, + prepareDatabase, + probeRepositories, + sqliteHelperEngine, +} from './pg-probe/engines'; +import { runHelperCases, type HelperResult } from './pg-probe/helper-cases'; +import { StatementRecorder } from './pg-probe/recorder'; +import { + formatConsole, + formatMarkdown, + passed, + probedNothing, + summarize, + type MethodResult, + type ReportInput, +} from './pg-probe/report'; import { appendFileSync, readdirSync, writeFileSync } from 'node:fs'; import path from 'node:path'; -import { planRepositories, readCompilerOptions, type MethodPlan } from './pg-probe/arg-samples'; -import { compareWithBaseline, formatBaseline, lowerBaseline, readBaseline } from './pg-probe/baseline'; -import { connectProbeOrm, connectSetupOrm, postgresHelperEngine, prepareDatabase, probeRepositories, sqliteHelperEngine } from './pg-probe/engines'; -import { runHelperCases, type HelperResult } from './pg-probe/helper-cases'; -import { StatementRecorder } from './pg-probe/recorder'; -import { formatConsole, formatMarkdown, passed, probedNothing, summarize, type MethodResult, type ReportInput } from './pg-probe/report'; - const SERVER_ROOT = path.join(__dirname, '..'); export const BASELINE_PATH = path.join(SERVER_ROOT, 'scripts', 'pg-probe-baseline.json'); const REPOSITORIES_DIR = path.join(SERVER_ROOT, 'src', 'db', 'repositories'); @@ -87,7 +101,9 @@ export function parseArgs(argv: readonly string[], env: Readonly { try { const schema = await timed('derive the schema from the entities', () => prepareDatabase(setup)); schemaFailures = schema.failures; - console.log(` ${schema.statements} DDL statements, ${schema.adjusted} columns adjusted, ${schema.failures.length} refused`); + console.log( + ` ${schema.statements} DDL statements, ${schema.adjusted} columns adjusted, ${schema.failures.length} refused`, + ); helpers = await timed('dialect helpers on SQLite and Postgres', async () => { const sqlite = sqliteHelperEngine(); @@ -159,14 +179,20 @@ async function main(): Promise { }); const plans: MethodPlan[] = await timed('plan the repository calls', async () => - planRepositories(repositoryFiles(REPOSITORIES_DIR), readCompilerOptions(path.join(SERVER_ROOT, 'tsconfig.json')), SERVER_ROOT), + planRepositories( + repositoryFiles(REPOSITORIES_DIR), + readCompilerOptions(path.join(SERVER_ROOT, 'tsconfig.json')), + SERVER_ROOT, + ), ); console.log(` ${plans.length} public repository methods`); const recorder = new StatementRecorder(); const probe = await connectProbeOrm(args.url, recorder, STATEMENT_TIMEOUT_MS); try { - results = await timed('call every repository method', () => probeRepositories(probe, recorder, plans, SERVER_ROOT, METHOD_TIMEOUT_MS)); + results = await timed('call every repository method', () => + probeRepositories(probe, recorder, plans, SERVER_ROOT, METHOD_TIMEOUT_MS), + ); } finally { await probe.close(true); } @@ -201,7 +227,10 @@ async function main(): Promise { annotate('error', `pg-probe helper case [${failure.engine}] ${failure.name}: ${failure.failure}`); } for (const entry of verdict.grown) { - annotate('error', `pg-probe: ${entry.method} sends ${entry.now} statement(s) Postgres refuses, ${entry.allowed} allowed.`); + annotate( + 'error', + `pg-probe: ${entry.method} sends ${entry.now} statement(s) Postgres refuses, ${entry.allowed} allowed.`, + ); } for (const entry of verdict.stale) { annotate( @@ -218,9 +247,13 @@ async function main(): Promise { ); } for (const method of verdict.nowCovered) { - annotate('error', `pg-probe: ${method} is listed as uncovered but is measured now (or gone). Drop it (--update, or the pg-probe-baseline artifact).`); + annotate( + 'error', + `pg-probe: ${method} is listed as uncovered but is measured now (or gone). Drop it (--update, or the pg-probe-baseline artifact).`, + ); } - if (probedNothing(summary)) annotate('error', `pg-probe: no repository method was called (${summary.methods} planned).`); + if (probedNothing(summary)) + annotate('error', `pg-probe: no repository method was called (${summary.methods} planned).`); if (verdict.unseeded) { annotate( 'error', @@ -236,12 +269,17 @@ if (require.main === module) { // A method abandoned after its timeout may still reject later; that is // already counted as a timeout and must not take the whole run down. process.on('unhandledRejection', (reason: unknown) => { - annotate('warning', `pg-probe: a rejection nobody awaited: ${reason instanceof Error ? reason.message.split('\n')[0] : String(reason)}`); + annotate( + 'warning', + `pg-probe: a rejection nobody awaited: ${reason instanceof Error ? reason.message.split('\n')[0] : String(reason)}`, + ); }); main().then( (code) => process.exit(code), (error: unknown) => { - console.error(`pg-probe could not run: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`); + console.error( + `pg-probe could not run: ${error instanceof Error ? (error.stack ?? error.message) : String(error)}`, + ); process.exit(2); }, ); diff --git a/server/scripts/pg-probe/arg-samples.ts b/server/scripts/pg-probe/arg-samples.ts index 56b73dda5..e6273f1f4 100644 --- a/server/scripts/pg-probe/arg-samples.ts +++ b/server/scripts/pg-probe/arg-samples.ts @@ -14,9 +14,7 @@ * method unprobeable: the report lists it instead of guessing, and the * baseline's `uncovered` list must name it, so a new one fails the run. */ - import path from 'node:path'; - import ts from 'typescript'; export type Sample = { ok: true; value: unknown } | { ok: false; reason: string }; @@ -118,7 +116,13 @@ function sampleObjectType(checker: ts.TypeChecker, type: ts.ObjectType, name: st return sampleObject(checker, type, name, depth); } -function sampleCollection(checker: ts.TypeChecker, type: ts.ObjectType, kind: string, name: string, depth: number): Sample { +function sampleCollection( + checker: ts.TypeChecker, + type: ts.ObjectType, + kind: string, + name: string, + depth: number, +): Sample { const args = checker.getTypeArguments(type as ts.TypeReference); const samples = args.map((arg, i) => sampleFor(checker, arg, i === 0 ? singular(name) : name, depth + 1)); const failed = samples.find((sample) => !sample.ok); @@ -152,7 +156,10 @@ function isPublicMethod(member: ts.ClassElement): member is ts.MethodDeclaration } /** The sample argument list for one method, or why there is none. */ -export function planMethod(checker: ts.TypeChecker, method: ts.MethodDeclaration): { args: unknown[] } | { unprobeable: string } { +export function planMethod( + checker: ts.TypeChecker, + method: ts.MethodDeclaration, +): { args: unknown[] } | { unprobeable: string } { const args: unknown[] = []; for (const parameter of method.parameters) { const name = ts.isIdentifier(parameter.name) ? parameter.name.text : 'options'; @@ -169,7 +176,11 @@ export function planMethod(checker: ts.TypeChecker, method: ts.MethodDeclaration } /** Every public method of every exported class declared in `files`, with its sample arguments. */ -export function planRepositories(files: readonly string[], compilerOptions: ts.CompilerOptions, root: string): MethodPlan[] { +export function planRepositories( + files: readonly string[], + compilerOptions: ts.CompilerOptions, + root: string, +): MethodPlan[] { const program = ts.createProgram({ rootNames: [...files], options: { ...compilerOptions, noEmit: true } }); const checker = program.getTypeChecker(); const wanted = new Set(files.map((file) => path.resolve(file))); @@ -186,7 +197,11 @@ export function planRepositories(files: readonly string[], compilerOptions: ts.C if (!isPublicMethod(member)) continue; const method = (member.name as ts.Identifier).text; const plan = planMethod(checker, member); - plans.push('args' in plan ? { className, file, method, args: plan.args } : { className, file, method, unprobeable: plan.unprobeable }); + plans.push( + 'args' in plan + ? { className, file, method, args: plan.args } + : { className, file, method, unprobeable: plan.unprobeable }, + ); } } } diff --git a/server/scripts/pg-probe/baseline.ts b/server/scripts/pg-probe/baseline.ts index fe51e04c7..879604355 100644 --- a/server/scripts/pg-probe/baseline.ts +++ b/server/scripts/pg-probe/baseline.ts @@ -31,7 +31,6 @@ * measurement, written by `--update` or taken from the CI artifact, is * committed. */ - import { readFileSync } from 'node:fs'; export interface ProbeBaseline { @@ -104,7 +103,8 @@ function parseFailing(failing: unknown, source: string): Record /** A missing `uncovered` key reads as unmeasured, so a baseline written before the list existed gets it seeded. */ function parseUncovered(uncovered: unknown, source: string): string[] | null { if (uncovered === undefined || uncovered === null) return null; - if (!Array.isArray(uncovered)) throw new Error(`${source}: "uncovered" must be null or an array of Class.method names`); + if (!Array.isArray(uncovered)) + throw new Error(`${source}: "uncovered" must be null or an array of Class.method names`); const seen = new Set(); for (const method of uncovered as unknown[]) { if (typeof method !== 'string' || !METHOD_KEY.test(method)) { @@ -169,7 +169,8 @@ export function lowerBaseline( if (now > 0) next[method] = now; } } - const uncovered = baseline.uncovered === null ? [...uncoveredNow] : baseline.uncovered.filter((method) => uncoveredNow.has(method)); + const uncovered = + baseline.uncovered === null ? [...uncoveredNow] : baseline.uncovered.filter((method) => uncoveredNow.has(method)); return { failing: Object.fromEntries(Object.entries(next).sort(([a], [b]) => a.localeCompare(b))), uncovered: [...new Set(uncovered)].sort((a, b) => a.localeCompare(b)), diff --git a/server/scripts/pg-probe/engines.ts b/server/scripts/pg-probe/engines.ts index c7a30b939..0903d4ec4 100644 --- a/server/scripts/pg-probe/engines.ts +++ b/server/scripts/pg-probe/engines.ts @@ -5,21 +5,34 @@ * SQLite database the helper cases are compared on, and the loop that calls * every repository method. */ - -import path from 'node:path'; - -import { RequestContext, type Configuration, type EntityManager, type EntityProperty } from '@mikro-orm/core'; -import { MikroORM, PostgreSqlConnection, PostgreSqlDriver, type Options } from '@mikro-orm/postgresql'; -import { SqlitePlatform } from '@mikro-orm/sql'; -import Database from 'better-sqlite3'; -import { DummyDriver, Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, type PostgresDialect } from 'kysely'; - import { ALL_ENTITIES } from '../../src/db/entities'; import type { MethodPlan } from './arg-samples'; -import { VALUES_TABLE, VALUES_TABLE_DDL, type HelperEngine, type KyselySelect, type RawSelect, type ValuesDB, type ValuesRow } from './helper-cases'; +import { + VALUES_TABLE, + VALUES_TABLE_DDL, + type HelperEngine, + type KyselySelect, + type RawSelect, + type ValuesDB, + type ValuesRow, +} from './helper-cases'; import { ProbePostgresDialect, type StatementRecorder } from './recorder'; import type { MethodResult } from './report'; import { adjustColumns, NOCASE_COLLATION_SQL, splitStatements, type ColumnMeta } from './schema'; +import { RequestContext, type Configuration, type EntityManager, type EntityProperty } from '@mikro-orm/core'; +import { MikroORM, PostgreSqlConnection, PostgreSqlDriver, type Options } from '@mikro-orm/postgresql'; +import { SqlitePlatform } from '@mikro-orm/sql'; + +import Database from 'better-sqlite3'; +import { + DummyDriver, + Kysely, + SqliteAdapter, + SqliteIntrospector, + SqliteQueryCompiler, + type PostgresDialect, +} from 'kysely'; +import path from 'node:path'; /** Marks a database the probe built, so a rerun may drop what it finds there and nothing else. */ const MARKER_TABLE = 'trek_pg_probe_marker'; @@ -42,9 +55,14 @@ export function connectSetupOrm(url: string): Promise { } /** A Postgres driver whose Kysely dialect records every statement (see recorder.ts). */ -export function createProbeDriverClass(recorder: StatementRecorder, statementTimeoutMs: number): typeof PostgreSqlDriver { +export function createProbeDriverClass( + recorder: StatementRecorder, + statementTimeoutMs: number, +): typeof PostgreSqlDriver { class ProbeConnection extends PostgreSqlConnection { - override createKyselyDialect(overrides: Parameters[0]): PostgresDialect { + override createKyselyDialect( + overrides: Parameters[0], + ): PostgresDialect { return new ProbePostgresDialect(super.createKyselyDialect(overrides), recorder, statementTimeoutMs); } } @@ -58,7 +76,11 @@ export function createProbeDriverClass(recorder: StatementRecorder, statementTim }; } -export function connectProbeOrm(url: string, recorder: StatementRecorder, statementTimeoutMs: number): Promise { +export function connectProbeOrm( + url: string, + recorder: StatementRecorder, + statementTimeoutMs: number, +): Promise { return MikroORM.init({ ...baseOptions(url), driver: createProbeDriverClass(recorder, statementTimeoutMs) }); } @@ -74,7 +96,9 @@ async function resetDatabase(orm: MikroORM): Promise { ); const names = tables.map((row) => row.table_name); if (names.length > 0 && !names.includes(MARKER_TABLE)) { - throw new Error(`refusing to run: the database holds ${names.length} table(s) the probe did not create. Point it at an empty database.`); + throw new Error( + `refusing to run: the database holds ${names.length} table(s) the probe did not create. Point it at an empty database.`, + ); } if (names.length > 0) { await connection.execute('drop schema public cascade'); @@ -89,7 +113,9 @@ export interface SchemaFailure { } /** Resets the database and builds the probe schema; returns the DDL statements Postgres refused. */ -export async function prepareDatabase(orm: MikroORM): Promise<{ failures: SchemaFailure[]; adjusted: number; statements: number }> { +export async function prepareDatabase( + orm: MikroORM, +): Promise<{ failures: SchemaFailure[]; adjusted: number; statements: number }> { await resetDatabase(orm); const connection = orm.em.getConnection(); const failures: SchemaFailure[] = []; @@ -128,7 +154,10 @@ export function postgresHelperEngine(orm: MikroORM): HelperEngine { }, async selectKysely(select: KyselySelect) { const platform = orm.em.getPlatform(); - const row = await kysely.selectFrom(VALUES_TABLE).select((eb) => select(platform, eb).as('v')).executeTakeFirst(); + const row = await kysely + .selectFrom(VALUES_TABLE) + .select((eb) => select(platform, eb).as('v')) + .executeTakeFirst(); return row?.v; }, }; @@ -153,7 +182,13 @@ export function sqliteHelperEngine(): HelperEngine & { close(): void } { platform, async writeRow(row: ValuesRow) { db.prepare(`delete from ${VALUES_TABLE}`).run(); - db.prepare(`insert into ${VALUES_TABLE} (id, a, b, n, x) values (?, ?, ?, ?, ?)`).run(row.id, row.a, row.b, row.n, row.x); + db.prepare(`insert into ${VALUES_TABLE} (id, a, b, n, x) values (?, ?, ?, ?, ?)`).run( + row.id, + row.a, + row.b, + row.n, + row.x, + ); }, async selectRaw(select: RawSelect) { const sql = `select ${select.sql}${select.column ? '' : ' as v'} from ${VALUES_TABLE}`; @@ -161,7 +196,10 @@ export function sqliteHelperEngine(): HelperEngine & { close(): void } { return row?.[select.column ?? 'v']; }, async selectKysely(select: KyselySelect) { - const compiled = compiler.selectFrom(VALUES_TABLE).select((eb) => select(platform, eb).as('v')).compile(); + const compiled = compiler + .selectFrom(VALUES_TABLE) + .select((eb) => select(platform, eb).as('v')) + .compile(); const row = db.prepare(compiled.sql).get(...compiled.parameters.map(bind)) as Record | undefined; return row?.v; }, @@ -172,7 +210,11 @@ export function sqliteHelperEngine(): HelperEngine & { close(): void } { type RepositoryFactory = (em: EntityManager) => object; /** How to reach each repository class: through its entity, or `new X(em)` for the two that have none. */ -function repositoryFactories(orm: MikroORM, plans: readonly MethodPlan[], serverRoot: string): Map { +function repositoryFactories( + orm: MikroORM, + plans: readonly MethodPlan[], + serverRoot: string, +): Map { const byEntity = new Map(); for (const meta of orm.getMetadata().getAll().values()) { const repository = meta.repository?.(); diff --git a/server/scripts/pg-probe/helper-cases.ts b/server/scripts/pg-probe/helper-cases.ts index bf9e0342d..3974ec3d7 100644 --- a/server/scripts/pg-probe/helper-cases.ts +++ b/server/scripts/pg-probe/helper-cases.ts @@ -15,10 +15,15 @@ * Unlike the repository statements, nothing here is baselined: every case * must pass on both engines. */ - -import type { Platform } from '@mikro-orm/core'; -import type { AliasableExpression, ExpressionBuilder } from 'kysely'; - +import { + castIntegerKysely, + concatKysely, + currentTimestampKysely, + nowPlusSecondsKysely, + startsWithIsoDateKysely, + substringKysely, + unixEpochToIsoKysely, +} from '../../src/db/dialect/kysely-functions'; import { absDifference, caseWhenEquals, @@ -53,15 +58,9 @@ import { substring, trim, } from '../../src/db/dialect/sql-functions'; -import { - castIntegerKysely, - concatKysely, - currentTimestampKysely, - nowPlusSecondsKysely, - startsWithIsoDateKysely, - substringKysely, - unixEpochToIsoKysely, -} from '../../src/db/dialect/kysely-functions'; +import type { Platform } from '@mikro-orm/core'; + +import type { AliasableExpression, ExpressionBuilder } from 'kysely'; export type Engine = 'sqlite' | 'postgres'; @@ -92,7 +91,10 @@ export interface RawSelect { column?: string; } -export type KyselySelect = (platform: Platform, eb: ExpressionBuilder) => AliasableExpression; +export type KyselySelect = ( + platform: Platform, + eb: ExpressionBuilder, +) => AliasableExpression; export type Expectation = | { equals: string | number | boolean | null } @@ -118,7 +120,10 @@ const rawOf = (fragment: { sql: string; params: readonly unknown[] }, column?: s }); /** `( = )` for the helpers used as a filter key. */ -const comparison = (key: { sql: string; params: readonly unknown[] }, value: { sql: string; params: readonly unknown[] }): RawSelect => ({ +const comparison = ( + key: { sql: string; params: readonly unknown[] }, + value: { sql: string; params: readonly unknown[] }, +): RawSelect => ({ sql: `(${key.sql} = ${value.sql})`, params: [...key.params, ...value.params], }); @@ -133,9 +138,24 @@ export function isoSeconds(ms: number): string { const EPOCH_MS = 1_758_459_909_123; export const HELPER_CASES: readonly HelperCase[] = [ - { name: 'dateOf', row: { a: '2026-09-21 13:05:09' }, raw: (p) => rawOf(dateOf(p, 'a')), expect: { equals: '2026-09-21' } }, - { name: 'dateAdd forward', row: { a: '2026-09-21 13:05:09' }, raw: (p) => rawOf(dateAdd(p, 'a', 10)), expect: { equals: '2026-10-01' } }, - { name: 'dateAdd back', row: { a: '2026-09-21 13:05:09' }, raw: (p) => rawOf(dateAdd(p, 'a', -10)), expect: { equals: '2026-09-11' } }, + { + name: 'dateOf', + row: { a: '2026-09-21 13:05:09' }, + raw: (p) => rawOf(dateOf(p, 'a')), + expect: { equals: '2026-09-21' }, + }, + { + name: 'dateAdd forward', + row: { a: '2026-09-21 13:05:09' }, + raw: (p) => rawOf(dateAdd(p, 'a', 10)), + expect: { equals: '2026-10-01' }, + }, + { + name: 'dateAdd back', + row: { a: '2026-09-21 13:05:09' }, + raw: (p) => rawOf(dateAdd(p, 'a', -10)), + expect: { equals: '2026-09-11' }, + }, { name: 'currentTimestamp', row: {}, raw: (p) => rawOf(currentTimestamp(p)), expect: { timestamp: 0 } }, { name: 'nowMinusDays', row: {}, raw: (p) => rawOf(nowMinusDays(p, 30)), expect: { timestamp: -30 * DAY_MS } }, { name: 'nowMinusHours', row: {}, raw: (p) => rawOf(nowMinusHours(p, 20)), expect: { timestamp: -20 * 3_600_000 } }, @@ -143,28 +163,98 @@ export const HELPER_CASES: readonly HelperCase[] = [ { name: 'nowDateOffset back', row: {}, raw: (p) => rawOf(nowDateOffset(p, -400)), expect: { date: -400 } }, { name: 'nowDateOffset forward', row: {}, raw: (p) => rawOf(nowDateOffset(p, 1)), expect: { date: 1 } }, { name: 'columnRef', row: { n: 7 }, raw: (p) => rawOf(columnRef(p, 'n')), expect: { equals: 7 } }, - { name: 'columnIncrementedBy', row: { n: 7 }, raw: (p) => rawOf(columnIncrementedBy(p, 'n', -2)), expect: { equals: 5 } }, + { + name: 'columnIncrementedBy', + row: { n: 7 }, + raw: (p) => rawOf(columnIncrementedBy(p, 'n', -2)), + expect: { equals: 5 }, + }, { name: 'lower', row: { a: 'Probe@X.com' }, raw: (p) => rawOf(lower(p, 'a')), expect: { equals: 'probe@x.com' } }, - { name: 'lower = lowerParam', row: { a: 'Probe@X.com' }, raw: (p) => comparison(lower(p, 'a'), lowerParam(p, 'PROBE@x.COM')), expect: { equals: true } }, + { + name: 'lower = lowerParam', + row: { a: 'Probe@X.com' }, + raw: (p) => comparison(lower(p, 'a'), lowerParam(p, 'PROBE@x.COM')), + expect: { equals: true }, + }, { name: 'trim', row: { a: ' probe ' }, raw: (p) => rawOf(trim(p, 'a')), expect: { equals: 'probe' } }, { name: 'lowerTrim', row: { a: ' AbC ' }, raw: (p) => rawOf(lowerTrim(p, 'a')), expect: { equals: 'abc' } }, - { name: 'lowerTrim = lowerTrimParam', row: { a: ' AbC ' }, raw: (p) => comparison(lowerTrim(p, 'a'), lowerTrimParam(p, ' aBc')), expect: { equals: true } }, - { name: 'coalesce', row: { a: null, b: 'fallback' }, raw: (p) => rawOf(coalesce(p, 'a', 'b')), expect: { equals: 'fallback' } }, - { name: 'coalesceParam keeps the column', row: { a: 'stored' }, raw: (p) => rawOf(coalesceParam(p, 'a', 'new')), expect: { equals: 'stored' } }, - { name: 'coalesceParam fills a null', row: { a: null }, raw: (p) => rawOf(coalesceParam(p, 'a', 'new')), expect: { equals: 'new' } }, - { name: 'coalesceOverride prefers the value', row: { a: 'stored' }, raw: (p) => rawOf(coalesceOverride(p, 'new', 'a')), expect: { equals: 'new' } }, - { name: 'coalesceOverride keeps on null', row: { a: 'stored' }, raw: (p) => rawOf(coalesceOverride(p, null, 'a')), expect: { equals: 'stored' } }, + { + name: 'lowerTrim = lowerTrimParam', + row: { a: ' AbC ' }, + raw: (p) => comparison(lowerTrim(p, 'a'), lowerTrimParam(p, ' aBc')), + expect: { equals: true }, + }, + { + name: 'coalesce', + row: { a: null, b: 'fallback' }, + raw: (p) => rawOf(coalesce(p, 'a', 'b')), + expect: { equals: 'fallback' }, + }, + { + name: 'coalesceParam keeps the column', + row: { a: 'stored' }, + raw: (p) => rawOf(coalesceParam(p, 'a', 'new')), + expect: { equals: 'stored' }, + }, + { + name: 'coalesceParam fills a null', + row: { a: null }, + raw: (p) => rawOf(coalesceParam(p, 'a', 'new')), + expect: { equals: 'new' }, + }, + { + name: 'coalesceOverride prefers the value', + row: { a: 'stored' }, + raw: (p) => rawOf(coalesceOverride(p, 'new', 'a')), + expect: { equals: 'new' }, + }, + { + name: 'coalesceOverride keeps on null', + row: { a: 'stored' }, + raw: (p) => rawOf(coalesceOverride(p, null, 'a')), + expect: { equals: 'stored' }, + }, { name: 'absDifference', row: { x: 4 }, raw: (p) => rawOf(absDifference(p, 'x', 6.5)), expect: { equals: 2.5 } }, { name: 'countAll', row: {}, raw: (p) => rawOf(countAll(p, 'c'), 'c'), expect: { equals: 1 } }, { name: 'countAllRef', row: {}, raw: (p) => rawOf(countAllRef(p)), expect: { equals: 1 } }, { name: 'minOf', row: { n: 3 }, raw: (p) => rawOf(minOf(p, 'n', 'lo'), 'lo'), expect: { equals: 3 } }, { name: 'maxOf', row: { n: 3 }, raw: (p) => rawOf(maxOf(p, 'n', 'hi'), 'hi'), expect: { equals: 3 } }, - { name: 'caseWhenEquals', row: { n: 7 }, raw: (p) => rawOf(caseWhenEquals(p, 'n', 7, 'owner', 'member')), expect: { equals: 'owner' } }, - { name: 'caseWhenEquals else', row: { n: 8 }, raw: (p) => rawOf(caseWhenEquals(p, 'n', 7, 'owner', 'member')), expect: { equals: 'member' } }, - { name: 'startsWithIsoDate yes', row: { a: '2026-09-21T10:00' }, raw: (p) => rawOf(startsWithIsoDate(p, 'a')), expect: { equals: true } }, - { name: 'startsWithIsoDate no', row: { a: 'abcd-ef-gh' }, raw: (p) => rawOf(startsWithIsoDate(p, 'a')), expect: { equals: false } }, - { name: 'substring with length', row: { a: '2026-09-21T10:00' }, raw: (p) => rawOf(substring(p, 'a', 1, 10)), expect: { equals: '2026-09-21' } }, - { name: 'substring to the end', row: { a: '2026-09-21T10:00' }, raw: (p) => rawOf(substring(p, 'a', 12)), expect: { equals: '10:00' } }, + { + name: 'caseWhenEquals', + row: { n: 7 }, + raw: (p) => rawOf(caseWhenEquals(p, 'n', 7, 'owner', 'member')), + expect: { equals: 'owner' }, + }, + { + name: 'caseWhenEquals else', + row: { n: 8 }, + raw: (p) => rawOf(caseWhenEquals(p, 'n', 7, 'owner', 'member')), + expect: { equals: 'member' }, + }, + { + name: 'startsWithIsoDate yes', + row: { a: '2026-09-21T10:00' }, + raw: (p) => rawOf(startsWithIsoDate(p, 'a')), + expect: { equals: true }, + }, + { + name: 'startsWithIsoDate no', + row: { a: 'abcd-ef-gh' }, + raw: (p) => rawOf(startsWithIsoDate(p, 'a')), + expect: { equals: false }, + }, + { + name: 'substring with length', + row: { a: '2026-09-21T10:00' }, + raw: (p) => rawOf(substring(p, 'a', 1, 10)), + expect: { equals: '2026-09-21' }, + }, + { + name: 'substring to the end', + row: { a: '2026-09-21T10:00' }, + raw: (p) => rawOf(substring(p, 'a', 12)), + expect: { equals: '10:00' }, + }, { name: 'concat', row: { a: '2026-09-21', b: '10:00' }, @@ -173,15 +263,30 @@ export const HELPER_CASES: readonly HelperCase[] = [ }, { name: 'castInteger of 14.0', row: { a: '14.0' }, raw: (p) => rawOf(castInteger(p, 'a')), expect: { equals: 14 } }, { name: 'castInteger of 14', row: { a: '14' }, raw: (p) => rawOf(castInteger(p, 'a')), expect: { equals: 14 } }, - { name: 'dayDistance', row: { a: '2026-09-21' }, raw: (p) => rawOf(dayDistance(p, 'a', '2026-09-24')), expect: { equals: 3 } }, - { name: 'collateNoCase', row: { a: 'Probe' }, raw: (p) => comparison(collateNoCase(p, 'a'), bound('pROBE')), expect: { equals: true } }, + { + name: 'dayDistance', + row: { a: '2026-09-21' }, + raw: (p) => rawOf(dayDistance(p, 'a', '2026-09-24')), + expect: { equals: 3 }, + }, + { + name: 'collateNoCase', + row: { a: 'Probe' }, + raw: (p) => comparison(collateNoCase(p, 'a'), bound('pROBE')), + expect: { equals: true }, + }, { name: 'foundAgainState recovers', row: { a: 'remote_missing', b: 'file-1' }, raw: (p) => rawOf(foundAgainState(p, 'a', 'b')), expect: { equals: 'synced' }, }, - { name: 'foundAgainState passes through', row: { a: 'error', b: 'file-1' }, raw: (p) => rawOf(foundAgainState(p, 'a', 'b')), expect: { equals: 'error' } }, + { + name: 'foundAgainState passes through', + row: { a: 'error', b: 'file-1' }, + raw: (p) => rawOf(foundAgainState(p, 'a', 'b')), + expect: { equals: 'error' }, + }, { name: 'coalesceOverrideWhileSame keeps for the same key', row: { a: 'https://a', n: 1 }, @@ -194,24 +299,61 @@ export const HELPER_CASES: readonly HelperCase[] = [ raw: (p) => rawOf(coalesceOverrideWhileSame(p, null, 'n', 'a', 'https://b')), expect: { equals: 0 }, }, - { name: 'caseWhenNotNull', row: { a: 'x', b: 'route' }, raw: (p) => rawOf(caseWhenNotNull(p, 'a', 'b')), expect: { equals: 'route' } }, - { name: 'caseWhenNotNull null', row: { a: null, b: 'route' }, raw: (p) => rawOf(caseWhenNotNull(p, 'a', 'b')), expect: { equals: null } }, + { + name: 'caseWhenNotNull', + row: { a: 'x', b: 'route' }, + raw: (p) => rawOf(caseWhenNotNull(p, 'a', 'b')), + expect: { equals: 'route' }, + }, + { + name: 'caseWhenNotNull null', + row: { a: null, b: 'route' }, + raw: (p) => rawOf(caseWhenNotNull(p, 'a', 'b')), + expect: { equals: null }, + }, { name: 'startsWithIsoDateKysely', row: { a: '2026-09-21 10:00:00' }, - kysely: (p, eb) => eb.case().when(startsWithIsoDateKysely(p, eb, 'a')).then('yes').else('no').end(), + kysely: (p, eb) => + eb + .case() + .when(startsWithIsoDateKysely(p, eb, 'a')) + .then('yes') + .else('no') + .end(), expect: { equals: 'yes' }, }, - { name: 'substringKysely', row: { a: '2026-09-21T10:00' }, kysely: (p, eb) => substringKysely(p, eb, 'a', 1, 10), expect: { equals: '2026-09-21' } }, + { + name: 'substringKysely', + row: { a: '2026-09-21T10:00' }, + kysely: (p, eb) => substringKysely(p, eb, 'a', 1, 10), + expect: { equals: '2026-09-21' }, + }, { name: 'concatKysely', row: { a: '2026-09-21', b: 'xx10:00' }, - kysely: (p, eb) => concatKysely(p, eb, { column: 'a' }, { value: 'T' }, { expression: substringKysely(p, eb, 'b', 3) }), + kysely: (p, eb) => + concatKysely(p, eb, { column: 'a' }, { value: 'T' }, { expression: substringKysely(p, eb, 'b', 3) }), expect: { equals: '2026-09-21T10:00' }, }, - { name: 'castIntegerKysely', row: { a: '14.0' }, kysely: (p, eb) => castIntegerKysely(p, eb, 'a'), expect: { equals: 14 } }, - { name: 'unixEpochToIsoKysely', row: { n: EPOCH_MS }, kysely: (p, eb) => unixEpochToIsoKysely(p, eb, 'n'), expect: { equals: isoSeconds(EPOCH_MS) } }, - { name: 'nowPlusSecondsKysely', row: {}, kysely: (p, eb) => nowPlusSecondsKysely(p, eb, 45), expect: { timestamp: 45_000 } }, + { + name: 'castIntegerKysely', + row: { a: '14.0' }, + kysely: (p, eb) => castIntegerKysely(p, eb, 'a'), + expect: { equals: 14 }, + }, + { + name: 'unixEpochToIsoKysely', + row: { n: EPOCH_MS }, + kysely: (p, eb) => unixEpochToIsoKysely(p, eb, 'n'), + expect: { equals: isoSeconds(EPOCH_MS) }, + }, + { + name: 'nowPlusSecondsKysely', + row: {}, + kysely: (p, eb) => nowPlusSecondsKysely(p, eb, 45), + expect: { timestamp: 45_000 }, + }, { name: 'currentTimestampKysely', row: {}, kysely: (p) => currentTimestampKysely(p), expect: { timestamp: 0 } }, ]; @@ -222,14 +364,17 @@ const DATE_TEXT = /^\d{4}-\d{2}-\d{2}$/; export function checkValue(expectation: Expectation, value: unknown, engine: Engine, now: number): string | null { if ('perEngine' in expectation) return checkValue(expectation.perEngine[engine], value, engine, now); if ('timestamp' in expectation) { - if (typeof value !== 'string' || !TIMESTAMP_TEXT.test(value)) return `expected timestamp text, got ${JSON.stringify(value)}`; + if (typeof value !== 'string' || !TIMESTAMP_TEXT.test(value)) + return `expected timestamp text, got ${JSON.stringify(value)}`; const off = Date.parse(`${value.replace(' ', 'T')}Z`) - (now + expectation.timestamp); return Math.abs(off) <= CLOCK_TOLERANCE_MS ? null : `${value} is ${Math.round(off / 1000)}s off`; } if ('date' in expectation) { if (typeof value !== 'string' || !DATE_TEXT.test(value)) return `expected date text, got ${JSON.stringify(value)}`; const wanted = Date.parse(new Date(now + expectation.date * DAY_MS).toISOString().slice(0, 10)); - return Math.abs(Date.parse(value) - wanted) <= DAY_MS ? null : `${value} is not ${expectation.date} day(s) from today`; + return Math.abs(Date.parse(value) - wanted) <= DAY_MS + ? null + : `${value} is not ${expectation.date} day(s) from today`; } const expected = expectation.equals; if (expected === null) return value === null ? null : `expected null, got ${JSON.stringify(value)}`; @@ -239,8 +384,15 @@ export function checkValue(expectation: Expectation, value: unknown, engine: Eng return (expected ? truthy : falsy) ? null : `expected ${expected}, got ${JSON.stringify(value)}`; } if (typeof expected === 'number') { - const n = typeof value === 'bigint' ? Number(value) : typeof value === 'string' || typeof value === 'number' ? Number(value) : Number.NaN; - return Math.abs(n - expected) < 1e-9 ? null : `expected ${expected}, got ${JSON.stringify(typeof value === 'bigint' ? String(value) : value)}`; + const n = + typeof value === 'bigint' + ? Number(value) + : typeof value === 'string' || typeof value === 'number' + ? Number(value) + : Number.NaN; + return Math.abs(n - expected) < 1e-9 + ? null + : `expected ${expected}, got ${JSON.stringify(typeof value === 'bigint' ? String(value) : value)}`; } return value === expected ? null : `expected ${JSON.stringify(expected)}, got ${JSON.stringify(value)}`; } @@ -260,7 +412,10 @@ export interface HelperResult { failure: string | null; } -export async function runHelperCases(engine: HelperEngine, cases: readonly HelperCase[] = HELPER_CASES): Promise { +export async function runHelperCases( + engine: HelperEngine, + cases: readonly HelperCase[] = HELPER_CASES, +): Promise { const results: HelperResult[] = []; for (const helperCase of cases) { let failure: string | null; diff --git a/server/scripts/pg-probe/recorder.ts b/server/scripts/pg-probe/recorder.ts index cb58da7c0..373f9dc98 100644 --- a/server/scripts/pg-probe/recorder.ts +++ b/server/scripts/pg-probe/recorder.ts @@ -21,9 +21,6 @@ * AsyncLocalStorage, so a straggler from a timed-out method can never be * counted against the next one. */ - -import { AsyncLocalStorage } from 'node:async_hooks'; - import { CompiledQuery, type DatabaseConnection, @@ -35,6 +32,7 @@ import { type QueryCompiler, type QueryResult, } from 'kysely'; +import { AsyncLocalStorage } from 'node:async_hooks'; export type StatementOutcome = { ok: true } | { ok: false; code: string; message: string }; @@ -44,7 +42,8 @@ export interface StatementRecord { } /** Transaction control a probed method sends; each statement already runs in its own rolled-back transaction. */ -const TRANSACTION_CONTROL = /^\s*(begin|commit|rollback|end|abort|start\s+transaction|savepoint|release(\s+savepoint)?)\b/i; +const TRANSACTION_CONTROL = + /^\s*(begin|commit|rollback|end|abort|start\s+transaction|savepoint|release(\s+savepoint)?)\b/i; export function isTransactionControl(sql: string): boolean { return TRANSACTION_CONTROL.test(sql); @@ -114,7 +113,10 @@ export class ProbeConnection implements DatabaseConnection { this.recorder.record(key, { sql: compiled.sql, outcome: { ok: true } }); return result; } catch (error) { - this.recorder.record(key, { sql: compiled.sql, outcome: { ok: false, code: sqlState(error), message: errorLine(error) } }); + this.recorder.record(key, { + sql: compiled.sql, + outcome: { ok: false, code: sqlState(error), message: errorLine(error) }, + }); return { rows: [] }; } finally { await this.inner.executeQuery(CompiledQuery.raw('rollback')); diff --git a/server/scripts/pg-probe/report.ts b/server/scripts/pg-probe/report.ts index 2abfcc4ea..10feb0ea7 100644 --- a/server/scripts/pg-probe/report.ts +++ b/server/scripts/pg-probe/report.ts @@ -2,7 +2,6 @@ * Turns what the Postgres probe saw into counts, a console log and the * Markdown the CI job writes to its step summary. */ - import { countsTowardRatchet, type BaselineVerdict } from './baseline'; import type { HelperResult } from './helper-cases'; import type { StatementRecord } from './recorder'; @@ -77,7 +76,10 @@ export function summarize(results: readonly MethodResult[]): ProbeSummary { summary.timedOut += 1; summary.uncovered.set(result.key, 'timed out'); } else if (result.statements.length === 0) { - summary.uncovered.set(result.key, result.threw === undefined ? 'sent no SQL' : `threw before any statement: ${result.threw}`); + summary.uncovered.set( + result.key, + result.threw === undefined ? 'sent no SQL' : `threw before any statement: ${result.threw}`, + ); } if (result.statements.length === 0) summary.withoutSql += 1; const seen = new Set(); @@ -93,7 +95,13 @@ export function summarize(results: readonly MethodResult[]): ProbeSummary { if (!('code' in outcome)) continue; const dialect = countsTowardRatchet(outcome.code); if (dialect) counted += 1; - summary.failedStatements.push({ method: result.key, code: outcome.code, message: outcome.message, sql: statement.sql, counted: dialect }); + summary.failedStatements.push({ + method: result.key, + code: outcome.code, + message: outcome.message, + sql: statement.sql, + counted: dialect, + }); summary.failuresByCode.set(outcome.code, (summary.failuresByCode.get(outcome.code) ?? 0) + 1); } summary.failingByMethod.set(result.key, counted); @@ -102,7 +110,10 @@ export function summarize(results: readonly MethodResult[]): ProbeSummary { } /** Refused statements the ratchet holds (dialect errors) and the ones it only reports. */ -export function splitRefusals(summary: Pick): { counted: FailedStatement[]; reported: FailedStatement[] } { +export function splitRefusals(summary: Pick): { + counted: FailedStatement[]; + reported: FailedStatement[]; +} { return { counted: summary.failedStatements.filter((failed) => failed.counted), reported: summary.failedStatements.filter((failed) => !failed.counted), @@ -163,11 +174,16 @@ export function formatConsole(input: ReportInput): string { const { summary, verdict } = input; const lines: string[] = []; const failures = helperFailures(input.helpers); - lines.push(`Dialect helpers: ${input.helpers.length - failures.length}/${input.helpers.length} cases pass on SQLite and Postgres.`); + lines.push( + `Dialect helpers: ${input.helpers.length - failures.length}/${input.helpers.length} cases pass on SQLite and Postgres.`, + ); for (const failure of failures) lines.push(` FAIL [${failure.engine}] ${failure.name}: ${failure.failure}`); if (input.schemaFailures.length > 0) { - lines.push(`Schema: ${input.schemaFailures.length} DDL statement(s) Postgres refused (their tables are missing below):`); - for (const failure of input.schemaFailures) lines.push(` ${failure.message} <- ${oneLine(failure.statement, 120)}`); + lines.push( + `Schema: ${input.schemaFailures.length} DDL statement(s) Postgres refused (their tables are missing below):`, + ); + for (const failure of input.schemaFailures) + lines.push(` ${failure.message} <- ${oneLine(failure.statement, 120)}`); } lines.push( `Repositories: ${summary.methods} methods, ${summary.called} called, ${summary.unprobeable} unprobeable, ` + @@ -187,10 +203,13 @@ export function formatConsole(input: ReportInput): string { } if (summary.uncovered.size > 0) { lines.push(`Unmeasured: ${summary.uncovered.size} method(s) whose SQL the probe did not see:`); - for (const [method, why] of [...summary.uncovered.entries()].sort(([a], [b]) => a.localeCompare(b))) lines.push(` ${method}: ${why}`); + for (const [method, why] of [...summary.uncovered.entries()].sort(([a], [b]) => a.localeCompare(b))) + lines.push(` ${method}: ${why}`); } if (probedNothing(summary)) { - lines.push(`FAIL No repository method was called (${summary.methods} planned); there is nothing to hold to the baseline.`); + lines.push( + `FAIL No repository method was called (${summary.methods} planned); there is nothing to hold to the baseline.`, + ); } if (verdict.unseeded) { lines.push( @@ -202,7 +221,9 @@ export function formatConsole(input: ReportInput): string { lines.push(`FAIL ${entry.method} sends ${entry.now} statement(s) Postgres refuses, ${entry.allowed} allowed.`); } for (const entry of verdict.stale) { - lines.push(`FAIL ${entry.method} is held at ${entry.allowed} failing statement(s) but fails ${entry.now} now; lower the baseline.`); + lines.push( + `FAIL ${entry.method} is held at ${entry.allowed} failing statement(s) but fails ${entry.now} now; lower the baseline.`, + ); } for (const method of verdict.newlyUncovered) { lines.push( @@ -223,16 +244,21 @@ export function formatMarkdown(input: ReportInput): string { const out: string[] = ['## Postgres probe', '']; out.push(passed(input) ? 'Result: **passed**' : 'Result: **failed**', ''); out.push('| | |', '|---|---|'); - out.push(`| Dialect helper cases (SQLite and Postgres) | ${input.helpers.length - failures.length} of ${input.helpers.length} pass |`); + out.push( + `| Dialect helper cases (SQLite and Postgres) | ${input.helpers.length - failures.length} of ${input.helpers.length} pass |`, + ); out.push(`| Schema statements refused | ${input.schemaFailures.length} |`); const refusals = splitRefusals(summary); - out.push(`| Repository methods | ${summary.methods} (${summary.called} called, ${summary.unprobeable} unprobeable) |`); + out.push( + `| Repository methods | ${summary.methods} (${summary.called} called, ${summary.unprobeable} unprobeable) |`, + ); out.push(`| Methods whose SQL the probe did not see | ${summary.uncovered.size} |`); out.push(`| Distinct statements | ${summary.statements} |`); out.push(`| Refused by Postgres as SQL (held) | ${refusals.counted.length} |`); out.push(`| Refused over a sample value or the empty tables (reported only) | ${refusals.reported.length} |`); out.push(''); - if (probedNothing(summary)) out.push('No repository method was called, so there is nothing to hold to the baseline.', ''); + if (probedNothing(summary)) + out.push('No repository method was called, so there is nothing to hold to the baseline.', ''); if (verdict.unseeded) { out.push( 'The baseline has not been measured yet, which fails the run. Commit `pg-probe-baseline.json` from the `pg-probe-baseline` artifact as `server/scripts/pg-probe-baseline.json`.', @@ -241,33 +267,50 @@ export function formatMarkdown(input: ReportInput): string { } if (verdict.grown.length > 0 || verdict.stale.length > 0) { out.push('### Ratchet', '', '| Method | Allowed | Now |', '|---|---|---|'); - for (const entry of [...verdict.grown, ...verdict.stale]) out.push(`| ${cell(entry.method)} | ${entry.allowed} | ${entry.now} |`); + for (const entry of [...verdict.grown, ...verdict.stale]) + out.push(`| ${cell(entry.method)} | ${entry.allowed} | ${entry.now} |`); out.push(''); } if (verdict.newlyUncovered.length > 0 || verdict.nowCovered.length > 0) { out.push('### Coverage ratchet', '', '| Method | Baseline | Now |', '|---|---|---|'); - for (const method of verdict.newlyUncovered) out.push(`| ${cell(method)} | measured | ${cell(summary.uncovered.get(method) ?? 'unmeasured')} |`); + for (const method of verdict.newlyUncovered) + out.push(`| ${cell(method)} | measured | ${cell(summary.uncovered.get(method) ?? 'unmeasured')} |`); for (const method of verdict.nowCovered) out.push(`| ${cell(method)} | uncovered | measured or gone |`); out.push(''); } if (failures.length > 0) { out.push('### Helper cases that failed', '', '| Engine | Case | Why |', '|---|---|---|'); - for (const failure of failures) out.push(`| ${failure.engine} | ${cell(failure.name)} | ${cell(failure.failure ?? '')} |`); + for (const failure of failures) + out.push(`| ${failure.engine} | ${cell(failure.name)} | ${cell(failure.failure ?? '')} |`); out.push(''); } if (summary.failuresByCode.size > 0) { out.push('### Refused statements by SQLSTATE', '', '| SQLSTATE | Statements |', '|---|---|'); - for (const [code, count] of [...summary.failuresByCode.entries()].sort((a, b) => b[1] - a[1])) out.push(`| ${code} | ${count} |`); + for (const [code, count] of [...summary.failuresByCode.entries()].sort((a, b) => b[1] - a[1])) + out.push(`| ${code} | ${count} |`); out.push(''); - out.push('
Every refused statement', '', '| Method | SQLSTATE | Held | Message |', '|---|---|---|---|'); + out.push( + '
Every refused statement', + '', + '| Method | SQLSTATE | Held | Message |', + '|---|---|---|---|', + ); for (const failed of summary.failedStatements) { - out.push(`| ${cell(failed.method)} | ${failed.code} | ${failed.counted ? 'yes' : 'no'} | ${cell(failed.message)} |`); + out.push( + `| ${cell(failed.method)} | ${failed.code} | ${failed.counted ? 'yes' : 'no'} | ${cell(failed.message)} |`, + ); } out.push('', '
', ''); } if (summary.uncovered.size > 0) { - out.push('
Methods whose SQL the probe did not see', '', '| Method | Why |', '|---|---|'); - for (const [method, why] of [...summary.uncovered.entries()].sort(([a], [b]) => a.localeCompare(b))) out.push(`| ${cell(method)} | ${cell(why)} |`); + out.push( + '
Methods whose SQL the probe did not see', + '', + '| Method | Why |', + '|---|---|', + ); + for (const [method, why] of [...summary.uncovered.entries()].sort(([a], [b]) => a.localeCompare(b))) + out.push(`| ${cell(method)} | ${cell(why)} |`); out.push('', '
', ''); } return `${out.join('\n')}\n`; diff --git a/server/scripts/pg-probe/schema.ts b/server/scripts/pg-probe/schema.ts index 1f092a3ee..f2b7042f6 100644 --- a/server/scripts/pg-probe/schema.ts +++ b/server/scripts/pg-probe/schema.ts @@ -19,7 +19,6 @@ * * Unique and partial unique indexes stay, because `ON CONFLICT` needs them. */ - import { pgTimestampText, PG_UTC_NOW } from '../../src/db/dialect/platform'; /** SQLite's timestamp text on Postgres, for a column default. */ @@ -31,9 +30,13 @@ export const NOCASE_COLLATION_SQL = /** The Postgres spelling of a column default the entities carry in SQLite's. */ export function portableDefault(raw: string): string { - const text = raw.trim().replace(/^\((.*)\)$/s, '$1').trim(); + const text = raw + .trim() + .replace(/^\((.*)\)$/s, '$1') + .trim(); if (/^current_timestamp$/i.test(text) || /^datetime\(\s*'now'\s*\)$/i.test(text)) return PG_NOW_TEXT_DEFAULT; - if (/^strftime\(\s*'%s'\s*,\s*'now'\s*\)$/i.test(text)) return 'CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) AS bigint)'; + if (/^strftime\(\s*'%s'\s*,\s*'now'\s*\)$/i.test(text)) + return 'CAST(EXTRACT(EPOCH FROM CURRENT_TIMESTAMP) AS bigint)'; return raw; } diff --git a/server/scripts/query-api-ratchet.mjs b/server/scripts/query-api-ratchet.mjs index 2f01c837b..e2f708346 100644 --- a/server/scripts/query-api-ratchet.mjs +++ b/server/scripts/query-api-ratchet.mjs @@ -138,13 +138,17 @@ function main(argv) { console.error('Run npm run lint:query-api -- --update to lower the baseline with the change that made it smaller.'); } const total = Object.values(counts).reduce((sum, n) => sum + n, 0); - console.log(`query-api: ${total} QueryBuilder call(s) in ${Object.keys(counts).length} file(s) held at their baseline`); + console.log( + `query-api: ${total} QueryBuilder call(s) in ${Object.keys(counts).length} file(s) held at their baseline`, + ); return grown.length || stale.length ? 1 : 0; } // Compared by real path, so the check still runs when the script is started through a symlink. const isCli = - Boolean(process.argv[1]) && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); + Boolean(process.argv[1]) && + existsSync(process.argv[1]) && + realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); if (isCli) { try { process.exitCode = main(process.argv.slice(2)); diff --git a/server/scripts/size-lint.mjs b/server/scripts/size-lint.mjs index 90775c65a..52bb94de6 100644 --- a/server/scripts/size-lint.mjs +++ b/server/scripts/size-lint.mjs @@ -33,8 +33,6 @@ * Walking the tree and handling the baseline live in scripts/lib/ratchet.mjs * at the repository root. */ -import { join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; import { listFiles, lowerCounts, @@ -47,6 +45,9 @@ import { writeBaseline, } from '../../scripts/lib/ratchet.mjs'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + /** The most lines a source file without a baseline entry may hold. */ const LIMIT = 1000; @@ -112,7 +113,8 @@ const sizeEntries = (limit) => (value) => { return 'it must be an object of file paths to line counts'; } for (const [file, n] of Object.entries(value)) { - if (!Number.isInteger(n) || n <= limit) return `${file} holds ${JSON.stringify(n)}, expected an integer above ${limit}`; + if (!Number.isInteger(n) || n <= limit) + return `${file} holds ${JSON.stringify(n)}, expected an integer above ${limit}`; } return null; }; diff --git a/server/scripts/strict-lint.mjs b/server/scripts/strict-lint.mjs index ec9b13abc..9f1414f26 100644 --- a/server/scripts/strict-lint.mjs +++ b/server/scripts/strict-lint.mjs @@ -24,11 +24,6 @@ * use both). The counting lives in scripts/lib/tsc-errors.mjs, the baseline * handling in scripts/lib/ratchet.mjs at the repository root. */ -import { execFileSync } from 'node:child_process'; -import { readFileSync } from 'node:fs'; -import { createRequire } from 'node:module'; -import { join, resolve } from 'node:path'; -import { fileURLToPath } from 'node:url'; import { countMap, lowerCounts, @@ -40,15 +35,25 @@ import { } from '../../scripts/lib/ratchet.mjs'; import { countErrors } from './lib/tsc-errors.mjs'; +import { execFileSync } from 'node:child_process'; +import { readFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; +import { join, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + /** tsc over tsconfig.strict.json, as its output and exit status. */ function runTsc(serverDir) { const tsc = createRequire(join(serverDir, 'package.json')).resolve('typescript/bin/tsc'); try { - const output = execFileSync(process.execPath, [tsc, '-p', 'tsconfig.strict.json', '--noEmit', '--pretty', 'false'], { - cwd: serverDir, - encoding: 'utf8', - maxBuffer: 256 * 1024 * 1024, - }); + const output = execFileSync( + process.execPath, + [tsc, '-p', 'tsconfig.strict.json', '--noEmit', '--pretty', 'false'], + { + cwd: serverDir, + encoding: 'utf8', + maxBuffer: 256 * 1024 * 1024, + }, + ); return { output, status: 0 }; } catch (err) { if (typeof err.status !== 'number') throw err; diff --git a/server/scripts/test-new-service-ratchet.mjs b/server/scripts/test-new-service-ratchet.mjs index c75c3cdaa..e4bfcd8e4 100644 --- a/server/scripts/test-new-service-ratchet.mjs +++ b/server/scripts/test-new-service-ratchet.mjs @@ -24,9 +24,10 @@ * npm run lint:test-new-service check against the baseline (CI) * npm run lint:test-new-service -- --update lower the counts; never raises or adds an entry */ +import { cliMain, stripComments } from './lib/count-ratchet.mjs'; + import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'; import { join } from 'node:path'; -import { cliMain, stripComments } from './lib/count-ratchet.mjs'; const CLASS_DECL = /\bclass\s+([A-Za-z_$][\w$]*)(?:\s*<[^{]*?>)?(?:\s+extends\s+([A-Za-z_$][\w$.]*))?[^{]*\{/g; diff --git a/server/scripts/test-sql-ratchet.mjs b/server/scripts/test-sql-ratchet.mjs index 732de681d..fa7489307 100644 --- a/server/scripts/test-sql-ratchet.mjs +++ b/server/scripts/test-sql-ratchet.mjs @@ -225,7 +225,9 @@ function main(argv) { // Compared by real path, so the check still runs when the script is started through a symlink. const isCli = - Boolean(process.argv[1]) && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); + Boolean(process.argv[1]) && + existsSync(process.argv[1]) && + realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); if (isCli) { try { process.exitCode = main(process.argv.slice(2)); diff --git a/server/scripts/tx-writes.mjs b/server/scripts/tx-writes.mjs index f6cbad4c7..6fa61f65a 100644 --- a/server/scripts/tx-writes.mjs +++ b/server/scripts/tx-writes.mjs @@ -266,26 +266,41 @@ export function analyse(classes) { return true; } const first = call.arguments[0]; - return Boolean(first && (ts.isStringLiteral(first) || ts.isNoSubstitutionTemplateLiteral(first)) && RAW_WRITE.test(first.text)); + return Boolean( + first && (ts.isStringLiteral(first) || ts.isNoSubstitutionTemplateLiteral(first)) && RAW_WRITE.test(first.text), + ); } /** Write units of `node`, read inside `cls`. `covered` is true inside a transactional callback. */ function count(cls, node, covered) { if (!node) return 0; if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node)) return 0; - if (ts.isBlock(node) || ts.isSourceFile(node) || ts.isModuleBlock(node)) return statements(cls, node.statements, 0, covered); + if (ts.isBlock(node) || ts.isSourceFile(node) || ts.isModuleBlock(node)) + return statements(cls, node.statements, 0, covered); if (ts.isIfStatement(node)) { - return count(cls, node.expression, covered) + Math.max(count(cls, node.thenStatement, covered), count(cls, node.elseStatement, covered)); + return ( + count(cls, node.expression, covered) + + Math.max(count(cls, node.thenStatement, covered), count(cls, node.elseStatement, covered)) + ); } if (ts.isConditionalExpression(node)) { - return count(cls, node.condition, covered) + Math.max(count(cls, node.whenTrue, covered), count(cls, node.whenFalse, covered)); + return ( + count(cls, node.condition, covered) + + Math.max(count(cls, node.whenTrue, covered), count(cls, node.whenFalse, covered)) + ); } if (ts.isSwitchStatement(node)) { let max = 0; for (const clause of node.caseBlock.clauses) max = Math.max(max, statements(cls, clause.statements, 0, covered)); return count(cls, node.expression, covered) + max; } - if (ts.isForStatement(node) || ts.isForOfStatement(node) || ts.isForInStatement(node) || ts.isWhileStatement(node) || ts.isDoStatement(node)) { + if ( + ts.isForStatement(node) || + ts.isForOfStatement(node) || + ts.isForInStatement(node) || + ts.isWhileStatement(node) || + ts.isDoStatement(node) + ) { let head = 0; if (ts.isForStatement(node)) head = count(cls, node.initializer, covered); else if (!ts.isDoStatement(node) && !ts.isWhileStatement(node)) head = count(cls, node.expression, covered); @@ -325,7 +340,9 @@ export function analyse(classes) { function call(cls, node, covered) { const callee = node.expression; const method = ts.isPropertyAccessExpression(callee) ? callee.name.text : null; - const receiver = ts.isPropertyAccessExpression(callee) ? count(cls, callee.expression, covered) : count(cls, callee, covered); + const receiver = ts.isPropertyAccessExpression(callee) + ? count(cls, callee.expression, covered) + : count(cls, callee, covered); if (method === 'transactional') { let inner = 0; frames.push([]); // what runs inside is one unit, not a list of its own @@ -381,7 +398,7 @@ export function analyse(classes) { // transactional callback or comes from a method that is bound itself. Grown from // nothing to a fixed point, so a cycle of methods calling each other binds none. const bound = new Set(); - for (let grew = true; grew; ) { + for (let grew = true; grew;) { grew = false; for (const [key, list] of callSites) { if (bound.has(key) || list.length === 0) continue; @@ -404,7 +421,10 @@ export function analyse(classes) { explained[`${cls.file}#${key}`] = sites.get(key); } } - return { found: Object.fromEntries(Object.entries(reported).sort(([a], [b]) => a.localeCompare(b))), sites: explained }; + return { + found: Object.fromEntries(Object.entries(reported).sort(([a], [b]) => a.localeCompare(b))), + sites: explained, + }; } /** The baseline as committed. Missing or malformed stops the run instead of reading as empty. */ @@ -420,7 +440,9 @@ export function readBaseline(path) { } for (const [key, n] of Object.entries(parsed)) { if (!Number.isInteger(n) || n < 2) { - throw new Error(`scripts/tx-writes-baseline.json: ${key} holds ${JSON.stringify(n)}, expected an integer of at least 2`); + throw new Error( + `scripts/tx-writes-baseline.json: ${key} holds ${JSON.stringify(n)}, expected an integer of at least 2`, + ); } } return parsed; @@ -479,12 +501,16 @@ function main(argv) { if (stale.length) { console.error('Run npm run lint:tx -- --update to lower the baseline with the change that fixed it.'); } - console.log(`tx: ${Object.keys(found).length} method(s) with writes outside one transaction, ${Object.keys(baseline).length} held at their baseline`); + console.log( + `tx: ${Object.keys(found).length} method(s) with writes outside one transaction, ${Object.keys(baseline).length} held at their baseline`, + ); return grown.length || stale.length ? 1 : 0; } const isCli = - Boolean(process.argv[1]) && existsSync(process.argv[1]) && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); + Boolean(process.argv[1]) && + existsSync(process.argv[1]) && + realpathSync(process.argv[1]) === fileURLToPath(import.meta.url); if (isCli) { try { process.exitCode = main(process.argv.slice(2)); diff --git a/server/src/addons.ts b/server/src/addons.ts index c1a12cc39..dd9e19fe8 100644 --- a/server/src/addons.ts +++ b/server/src/addons.ts @@ -15,7 +15,7 @@ export const ADDON_IDS = { TOURS: 'tours', } as const; -export type AddonId = typeof ADDON_IDS[keyof typeof ADDON_IDS]; +export type AddonId = (typeof ADDON_IDS)[keyof typeof ADDON_IDS]; /** * The addons that gate an MCP surface, which is what decides whether switching diff --git a/server/src/app-config/data-paths.ts b/server/src/app-config/data-paths.ts index 229759c81..6193cb868 100644 --- a/server/src/app-config/data-paths.ts +++ b/server/src/app-config/data-paths.ts @@ -1,6 +1,7 @@ -import path from 'node:path'; import { readEnv, type AppEnv } from './env'; +import path from 'node:path'; + /** * `/`, the directory holding package.json. `src/app-config` and * `dist/app-config` both sit two levels below it, so the anchor is the same diff --git a/server/src/app-config/derive.ts b/server/src/app-config/derive.ts index 200bb0614..a96f9b3ac 100644 --- a/server/src/app-config/derive.ts +++ b/server/src/app-config/derive.ts @@ -14,7 +14,7 @@ * call, which is what keeps the runtime env-mutation semantics the test suite * depends on. Zod validation runs once at boot (env.schema.ts), never here. */ -import { SUPPORTED_LANGUAGE_CODES } from '@trek/shared'; +import { imageVersion } from './image-version'; import { csvList, csvListFiltered, @@ -29,7 +29,7 @@ import { resolveSessionTtlMs, stripTrailingSlashes, } from './parsers'; -import { imageVersion } from './image-version'; +import { SUPPORTED_LANGUAGE_CODES } from '@trek/shared'; export type RawEnv = Record; @@ -358,7 +358,7 @@ export function deriveNet(raw: RawEnv) { https: (raw.HTTPS_PROXY ?? raw.https_proxy)?.trim() || undefined, noProxy: ((raw.NO_PROXY ?? raw.no_proxy) || '') .split(',') - .map(entry => entry.trim().toLowerCase()) + .map((entry) => entry.trim().toLowerCase()) .filter(Boolean), }, }; diff --git a/server/src/app-config/env.schema.ts b/server/src/app-config/env.schema.ts index 2631ed336..f49250feb 100644 --- a/server/src/app-config/env.schema.ts +++ b/server/src/app-config/env.schema.ts @@ -9,8 +9,6 @@ * listed here pass through untouched (Zod strips unknown keys, it does not * reject them). */ -import { z } from 'zod'; -import { SUPPORTED_LANGUAGE_CODES } from '@trek/shared'; import { isP256PrivateKey, isUncompressedP256Key, @@ -18,6 +16,9 @@ import { parseDurationMs, parseLinkLocalAllowList, } from './parsers'; +import { SUPPORTED_LANGUAGE_CODES } from '@trek/shared'; + +import { z } from 'zod'; /** Present-but-malformed fails; unset/blank always passes (defaults apply). */ function optionalWith(test: (v: string) => boolean, message: string) { @@ -49,10 +50,7 @@ const url = optionalWith((v) => { return false; } }, 'must be a valid URL (with protocol)'); -const duration = optionalWith( - (v) => parseDurationMs(v) != null, - 'must be a duration like "1h", "7d" or "30d"', -); +const duration = optionalWith((v) => parseDurationMs(v) != null, 'must be a duration like "1h", "7d" or "30d"'); const oneOf = (values: string[]) => optionalWith((v) => values.includes(v.toLowerCase()), `must be one of: ${values.join(', ')}`); diff --git a/server/src/app-config/parsers.ts b/server/src/app-config/parsers.ts index 009a929d6..d651e6383 100644 --- a/server/src/app-config/parsers.ts +++ b/server/src/app-config/parsers.ts @@ -199,7 +199,10 @@ export function synchronousName(level: unknown): string { export function parseLinkLocalAllowList(raw: string | undefined): { ips: string[]; invalid: string[] } { const ips: string[] = []; const invalid: string[] = []; - for (const entry of (raw ?? '').split(',').map((e) => e.trim()).filter(Boolean)) { + for (const entry of (raw ?? '') + .split(',') + .map((e) => e.trim()) + .filter(Boolean)) { const m = /^169\.254\.(\d{1,3})\.(\d{1,3})$/.exec(entry); const canonical = m !== null && [m[1], m[2]].every((o) => String(Number(o)) === o && Number(o) <= 255); if (canonical && m[1] !== '169' && m[1] !== '170') ips.push(entry); diff --git a/server/src/bootstrap.ts b/server/src/bootstrap.ts index 026214b8d..d84ddf90b 100644 --- a/server/src/bootstrap.ts +++ b/server/src/bootstrap.ts @@ -1,12 +1,14 @@ -import { applyGlobalMiddleware, routingCspOrigins } from './middleware/globalMiddleware'; import { readEnv } from './app-config'; import { nestLogLevels } from './app-config/nest-log-levels'; +import { applyGlobalMiddleware, routingCspOrigins } from './middleware/globalMiddleware'; import { httpConfig } from './nest/app-config'; import { AppModule } from './nest/app.module'; import { apiDocsEnabled } from './nest/common/api-docs.kill-switch'; import { validateBodyContracts } from './nest/common/validate-body-contracts'; import { validateManagedRoutes } from './nest/common/validate-managed-routes'; import { validateRouteGuards } from './nest/common/validate-route-guards'; +import { DatabaseLifecycle } from './nest/database/database-lifecycle.service'; +import { withRequestContext } from './nest/database/request-context'; import { setupApiDocs } from './nest/platform/api-docs'; import { MCP_METADATA_MIDDLEWARE } from './nest/platform/mcp-metadata.middleware'; import { applyPlatformUploads, applyPlatformStatic } from './nest/platform/platform.routes'; @@ -14,8 +16,6 @@ import { TrekWsAdapter } from './nest/realtime/trek-ws.adapter'; import { SettingsService } from './nest/settings/settings.service'; import { StorageService } from './nest/storage/storage.service'; import { MikroORM } from '@mikro-orm/core'; -import { withRequestContext } from './nest/database/request-context'; -import { DatabaseLifecycle } from './nest/database/database-lifecycle.service'; import type { INestApplication } from '@nestjs/common'; import type { ConfigType } from '@nestjs/config'; import { NestFactory } from '@nestjs/core'; diff --git a/server/src/config.ts b/server/src/config.ts index 16375e41a..8e433969a 100644 --- a/server/src/config.ts +++ b/server/src/config.ts @@ -1,8 +1,9 @@ -import crypto from 'node:crypto'; -import fs from 'node:fs'; import { readEnv } from './app-config'; import { resolveDataPaths } from './app-config/data-paths'; +import crypto from 'node:crypto'; +import fs from 'node:fs'; + const { dataDir, jwtSecretFile, encryptionKeyFile: encKeyFile } = resolveDataPaths(); // ENCRYPTION_KEY is used to derive at-rest encryption keys for stored secrets @@ -45,8 +46,12 @@ function persistEnvKey(envKey: string): void { } if (storedKey && storedKey !== envKey.trim()) { console.error(`FATAL: ENCRYPTION_KEY does not match ${encKeyFile}.`); - console.error('The stored secrets are encrypted with the key in that file, so TREK will not start with a different one.'); - console.error('To change the key, run scripts/migrate-encryption.ts first; it re-encrypts the secrets and updates the file.'); + console.error( + 'The stored secrets are encrypted with the key in that file, so TREK will not start with a different one.', + ); + console.error( + 'To change the key, run scripts/migrate-encryption.ts first; it re-encrypts the secrets and updates the file.', + ); console.error('Otherwise set ENCRYPTION_KEY back to the value in the file, or remove the variable.'); process.exit(1); } @@ -79,7 +84,9 @@ if (_encryptionKey) { if (code === 'EISDIR') { // Compose creating a missing bind-mount source as a directory is by far // the most common way this file stops being a file. - console.error('EISDIR: the path is a directory. A bind mount pointed at data/.encryption_key created it as one — remove it and mount the data directory instead.'); + console.error( + 'EISDIR: the path is a directory. A bind mount pointed at data/.encryption_key created it as one — remove it and mount the data directory instead.', + ); } console.error('Fix the file permissions or set ENCRYPTION_KEY explicitly.'); process.exit(1); diff --git a/server/src/db/dialect/kysely-functions.ts b/server/src/db/dialect/kysely-functions.ts index 8b5339e90..1ad527a63 100644 --- a/server/src/db/dialect/kysely-functions.ts +++ b/server/src/db/dialect/kysely-functions.ts @@ -1,6 +1,25 @@ +import { + isKnownPlatform, + isPostgres, + isSqlite, + PG_ISO_DATE_PREFIX, + PG_TIMESTAMP_FORMAT, + PG_UTC_NOW, + pgTimestampText, + unsupported, +} from './platform'; import type { Platform } from '@mikro-orm/core'; -import { sql, type Expression, type ExpressionBuilder, type ExpressionWrapper, type RawBuilder, type ReferenceExpression, type SqlBool, type StringReference } from 'kysely'; -import { isKnownPlatform, isPostgres, isSqlite, PG_ISO_DATE_PREFIX, PG_TIMESTAMP_FORMAT, PG_UTC_NOW, pgTimestampText, unsupported } from './platform'; + +import { + sql, + type Expression, + type ExpressionBuilder, + type ExpressionWrapper, + type RawBuilder, + type ReferenceExpression, + type SqlBool, + type StringReference, +} from 'kysely'; /** * The Kysely-expression half of the dialect layer: the twins of the MikroORM @@ -83,9 +102,8 @@ export function substringKysely( throw new Error(`sql-functions: substringKysely needs a non-negative integer length, got ${length}`); } if (isKnownPlatform(platform)) { - const args: ReferenceExpression[] = length === undefined - ? [ref, eb.val(start)] - : [ref, eb.val(start), eb.val(length)]; + const args: ReferenceExpression[] = + length === undefined ? [ref, eb.val(start)] : [ref, eb.val(start), eb.val(length)]; return eb.fn('substr', args); } return unsupported(platform); @@ -93,9 +111,7 @@ export function substringKysely( /** A part of a `concatKysely()` expression — a column reference, a bound value, or a nested Kysely `Expression` (composes freely, unlike `concat()`'s MikroORM `RawQueryFragment` form). */ export type KyselyConcatPart = - | { column: StringReference } - | { value: string } - | { expression: Expression }; + { column: StringReference } | { value: string } | { expression: Expression }; /** * The Kysely-expression twin of {@link concat}: ` || || …`, @@ -116,7 +132,8 @@ export function concatKysely( throw new Error(`sql-functions: concatKysely needs at least two parts, got ${parts.length}`); } if (!isKnownPlatform(platform)) return unsupported(platform); - const value = (text: string): Expression => (isPostgres(platform) ? eb.cast(eb.val(text), 'text') : eb.val(text)); + const value = (text: string): Expression => + isPostgres(platform) ? eb.cast(eb.val(text), 'text') : eb.val(text); const operand = (part: KyselyConcatPart): Expression => 'column' in part ? eb.ref(part.column).$castTo() : 'value' in part ? value(part.value) : part.expression; let acc: Expression = operand(parts[0]!); diff --git a/server/src/db/dialect/sql-functions.ts b/server/src/db/dialect/sql-functions.ts index 778b1d45e..a511960f3 100644 --- a/server/src/db/dialect/sql-functions.ts +++ b/server/src/db/dialect/sql-functions.ts @@ -1,5 +1,14 @@ +import { + isKnownPlatform, + isPostgres, + isSqlite, + PG_ISO_DATE_PREFIX, + PG_UTC_NOW, + pgDateText, + pgTimestampText, + unsupported, +} from './platform'; import { raw, type Platform, type RawQueryFragment } from '@mikro-orm/core'; -import { isKnownPlatform, isPostgres, isSqlite, PG_ISO_DATE_PREFIX, PG_UTC_NOW, pgDateText, pgTimestampText, unsupported } from './platform'; /** * The only place a repository may spell a database function. @@ -232,7 +241,11 @@ export function coalesce(platform: Platform, ref: string, fallbackRef: string): * anywhere a plain `RawQueryFragment` is expected, a strict widening with no * behaviour change (the function body is untouched). */ -export function coalesceParam(platform: Platform, ref: string, value: string | number | null): RawQueryFragment & symbol { +export function coalesceParam( + platform: Platform, + ref: string, + value: string | number | null, +): RawQueryFragment & symbol { if (isKnownPlatform(platform)) return raw(`COALESCE(${column(ref)}, ?)`, [value]); return unsupported(platform); } @@ -256,7 +269,11 @@ export function coalesceParam(platform: Platform, ref: string, value: string | n * columns exactly as they were before, since `coalesceParam`'s `COALESCE(col, * ?)` always preferred the non-null existing value). */ -export function coalesceOverride(platform: Platform, value: string | number | null, ref: string): RawQueryFragment & symbol { +export function coalesceOverride( + platform: Platform, + value: string | number | null, + ref: string, +): RawQueryFragment & symbol { if (isKnownPlatform(platform)) return raw(`COALESCE(?, ${column(ref)})`, [value]); return unsupported(platform); } @@ -334,8 +351,15 @@ export function maxOf(platform: Platform, ref: string, aliasName: string): RawQu * time regardless of this function's own escaping; callers must guard a * non-finite `value` before calling.) */ -export function caseWhenEquals(platform: Platform, ref: string, value: number, whenTrue: string, whenFalse: string): RawQueryFragment { - if (isKnownPlatform(platform)) return raw(`CASE WHEN ${column(ref)} = ? THEN ? ELSE ? END`, [value, whenTrue, whenFalse]); +export function caseWhenEquals( + platform: Platform, + ref: string, + value: number, + whenTrue: string, + whenFalse: string, +): RawQueryFragment { + if (isKnownPlatform(platform)) + return raw(`CASE WHEN ${column(ref)} = ? THEN ? ELSE ? END`, [value, whenTrue, whenFalse]); return unsupported(platform); } @@ -537,7 +561,9 @@ export function castInteger(platform: Platform, ref: string): RawQueryFragment & export function dayDistance(platform: Platform, ref: string, isoDate: string): RawQueryFragment & symbol { if (isSqlite(platform)) return raw(`ABS(JULIANDAY(${column(ref)}) - JULIANDAY(?))`, [isoDate]); if (isPostgres(platform)) { - return raw(`ABS(EXTRACT(EPOCH FROM (CAST(${column(ref)} AS timestamp) - CAST(? AS timestamp))) / 86400)`, [isoDate]); + return raw(`ABS(EXTRACT(EPOCH FROM (CAST(${column(ref)} AS timestamp) - CAST(? AS timestamp))) / 86400)`, [ + isoDate, + ]); } return unsupported(platform); } @@ -779,9 +805,19 @@ export function foundAgainState(platform: Platform, stateRef: string, fileIdRef: * the fragment has two value slots and `raw()` binds positionally. SQLite's * null-safe `IS` is `IS NOT DISTINCT FROM` on Postgres. */ -export function coalesceOverrideWhileSame(platform: Platform, value: number | null, ref: string, keyRef: string, keyValue: string): RawQueryFragment { +export function coalesceOverrideWhileSame( + platform: Platform, + value: number | null, + ref: string, + keyRef: string, + keyValue: string, +): RawQueryFragment { if (isSqlite(platform)) { - return raw(`CASE WHEN ${column(keyRef)} IS ? THEN COALESCE(?, ${column(ref)}) ELSE COALESCE(?, 0) END`, [keyValue, value, value]); + return raw(`CASE WHEN ${column(keyRef)} IS ? THEN COALESCE(?, ${column(ref)}) ELSE COALESCE(?, 0) END`, [ + keyValue, + value, + value, + ]); } if (isPostgres(platform)) { const sql = `CASE WHEN ${column(keyRef)} IS NOT DISTINCT FROM ? THEN COALESCE(?, ${column(ref)}) ELSE COALESCE(?, 0) END`; @@ -790,7 +826,6 @@ export function coalesceOverrideWhileSame(platform: Platform, value: number | nu return unsupported(platform); } - // --------------------------------------------------------------------------- // Tours (#2586): `DayAssignmentsRepository`'s assignment projection carries a // place's route geometry only when the place is a Tour, so the day list never diff --git a/server/src/db/document-provider-seed.ts b/server/src/db/document-provider-seed.ts index 52b2b171a..42ff05fd9 100644 --- a/server/src/db/document-provider-seed.ts +++ b/server/src/db/document-provider-seed.ts @@ -1,4 +1,3 @@ - /** * The five document providers and the fields their connection form asks for. * diff --git a/server/src/db/durability.ts b/server/src/db/durability.ts index 3e3457508..5a8148cf6 100644 --- a/server/src/db/durability.ts +++ b/server/src/db/durability.ts @@ -1,7 +1,8 @@ -import type Database from 'better-sqlite3'; import { readEnv } from '../app-config'; import { synchronousName } from '../app-config/parsers'; +import type Database from 'better-sqlite3'; + export interface ActiveDurability { journalMode: string; synchronous: string; @@ -24,7 +25,7 @@ export interface ActiveDurability { */ export function applyDurabilityPragmas(db: Database.Database): ActiveDurability { const { journalMode, synchronous, durabilityWarnings } = readEnv().db; - durabilityWarnings.forEach(warning => console.warn(`[DB] ${warning}`)); + durabilityWarnings.forEach((warning) => console.warn(`[DB] ${warning}`)); db.exec(`PRAGMA journal_mode = ${journalMode}`); db.exec(`PRAGMA synchronous = ${synchronous}`); diff --git a/server/src/db/email-case-index.ts b/server/src/db/email-case-index.ts index 0b2330717..c6b80e3c3 100644 --- a/server/src/db/email-case-index.ts +++ b/server/src/db/email-case-index.ts @@ -31,8 +31,15 @@ export async function ensureEmailCaseIndex(connection: Connection): Promise { - await connection.execute('CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_lower ON users (lower(email))', [], 'run', trx); + await connection.execute( + 'CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_lower ON users (lower(email))', + [], + 'run', + trx, + ); await connection.execute('DELETE FROM app_settings WHERE key = ?', [PENDING_KEY], 'run', trx); }); - console.log('[DB] Every account has its own email address now; created the case-insensitive unique index on users.email'); + console.log( + '[DB] Every account has its own email address now; created the case-insensitive unique index on users.email', + ); } diff --git a/server/src/db/legacy-baseline.ts b/server/src/db/legacy-baseline.ts index a60d6df15..e5265d7f4 100644 --- a/server/src/db/legacy-baseline.ts +++ b/server/src/db/legacy-baseline.ts @@ -1,8 +1,8 @@ +import { knownMigrationNames, unknownMigrations } from './known-migrations'; import type { Connection, MigrationInfo } from '@mikro-orm/core'; import type { Migrator } from '@mikro-orm/migrations'; import fs from 'node:fs'; -import { knownMigrationNames, unknownMigrations } from './known-migrations'; /** * Upgrading an install the retired hand-written runner migrated. @@ -175,7 +175,9 @@ async function withoutToursSteps(connection: Connection, version: number): Promi `SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'tour_types'`, ); if (table.length === 0) return version; - console.log(`[DB] schema_version ${version} was written by the pre-ORM Tours branch; baselining to upstream step ${toursLine}`); + console.log( + `[DB] schema_version ${version} was written by the pre-ORM Tours branch; baselining to upstream step ${toursLine}`, + ); return toursLine; } diff --git a/server/src/db/migrations/Migration20200101040200_trek_photo_cache_meta_cache_key_not_null.ts b/server/src/db/migrations/Migration20200101040200_trek_photo_cache_meta_cache_key_not_null.ts index 57d11840b..2c67abb5d 100644 --- a/server/src/db/migrations/Migration20200101040200_trek_photo_cache_meta_cache_key_not_null.ts +++ b/server/src/db/migrations/Migration20200101040200_trek_photo_cache_meta_cache_key_not_null.ts @@ -36,6 +36,8 @@ export class Migration20200101040200_trek_photo_cache_meta_cache_key_not_null ex this.addSql(`ALTER TABLE trek_photo_cache_meta_new RENAME TO trek_photo_cache_meta`); - this.addSql(`CREATE INDEX IF NOT EXISTS idx_trek_photo_cache_meta_fetched_at ON trek_photo_cache_meta (fetched_at)`); + this.addSql( + `CREATE INDEX IF NOT EXISTS idx_trek_photo_cache_meta_fetched_at ON trek_photo_cache_meta (fetched_at)`, + ); } } diff --git a/server/src/db/migrations/Migration20200101040300_a_booked_night_finally_took_the_seat_a_new.ts b/server/src/db/migrations/Migration20200101040300_a_booked_night_finally_took_the_seat_a_new.ts index bac62e0f7..016f79991 100644 --- a/server/src/db/migrations/Migration20200101040300_a_booked_night_finally_took_the_seat_a_new.ts +++ b/server/src/db/migrations/Migration20200101040300_a_booked_night_finally_took_the_seat_a_new.ts @@ -1,6 +1,6 @@ +import { reseatBookedNights } from '../reseat-booked-nights'; import { Migration } from '@mikro-orm/migrations'; import type { SqliteConnection } from '@mikro-orm/sqlite'; -import { reseatBookedNights } from '../reseat-booked-nights'; /** * Legacy migration step 242 (`db/migrations.ts`). diff --git a/server/src/db/migrations/Migration20200101042000_tours.ts b/server/src/db/migrations/Migration20200101042000_tours.ts index c9edf9ff1..c3a5c910b 100644 --- a/server/src/db/migrations/Migration20200101042000_tours.ts +++ b/server/src/db/migrations/Migration20200101042000_tours.ts @@ -1,5 +1,5 @@ -import { Migration } from '@mikro-orm/migrations'; import { addColumnIfMissing, tableExists } from '../migration-utils'; +import { Migration } from '@mikro-orm/migrations'; /** * Tours: a facet on `places` for hikes and other single-day routes. diff --git a/server/src/db/migrations/Migration20200101042200_an_index_on_every_foreign_key.ts b/server/src/db/migrations/Migration20200101042200_an_index_on_every_foreign_key.ts index ad2ede96e..3507215b1 100644 --- a/server/src/db/migrations/Migration20200101042200_an_index_on_every_foreign_key.ts +++ b/server/src/db/migrations/Migration20200101042200_an_index_on_every_foreign_key.ts @@ -54,10 +54,16 @@ export class Migration20200101042200_an_index_on_every_foreign_key extends Migra this.addSql('CREATE INDEX IF NOT EXISTS idx_collab_message_reactions_user_id ON collab_message_reactions(user_id)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_invite_tokens_trip_id ON invite_tokens(trip_id)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_invite_tokens_created_by ON invite_tokens(created_by)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_packing_category_assignees_user_id ON packing_category_assignees(user_id)'); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_packing_category_assignees_user_id ON packing_category_assignees(user_id)', + ); this.addSql('CREATE INDEX IF NOT EXISTS idx_packing_templates_created_by ON packing_templates(created_by)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_packing_template_categories_template_id ON packing_template_categories(template_id)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_packing_template_items_category_id ON packing_template_items(category_id)'); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_packing_template_categories_template_id ON packing_template_categories(template_id)', + ); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_packing_template_items_category_id ON packing_template_items(category_id)', + ); this.addSql('CREATE INDEX IF NOT EXISTS idx_packing_bags_user_id ON packing_bags(user_id)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_packing_bags_trip_id ON packing_bags(trip_id)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_bucket_list_user_id ON bucket_list(user_id)'); @@ -84,22 +90,38 @@ export class Migration20200101042200_an_index_on_every_foreign_key extends Migra this.addSql('CREATE INDEX IF NOT EXISTS idx_day_accommodations_place_id ON day_accommodations(place_id)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_journey_photos_photo_id ON journey_photos(photo_id)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_budget_item_payers_user_id ON budget_item_payers(user_id)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_budget_settlements_created_by_user_id ON budget_settlements(created_by_user_id)'); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_budget_settlements_created_by_user_id ON budget_settlements(created_by_user_id)', + ); this.addSql('CREATE INDEX IF NOT EXISTS idx_budget_settlements_to_user_id ON budget_settlements(to_user_id)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_budget_settlements_from_user_id ON budget_settlements(from_user_id)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_packing_item_contributors_user_id ON packing_item_contributors(user_id)'); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_packing_item_contributors_user_id ON packing_item_contributors(user_id)', + ); this.addSql('CREATE INDEX IF NOT EXISTS idx_trip_invite_tokens_created_by ON trip_invite_tokens(created_by)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_journey_books_updated_by ON journey_books(updated_by)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_journey_books_created_by ON journey_books(created_by)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_collab_links_user_id ON collab_links(user_id)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_dawarich_visit_suggestions_matched_bucket_list_item_id ON dawarich_visit_suggestions(matched_bucket_list_item_id)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_dawarich_visit_suggestions_accepted_bucket_list_item_id ON dawarich_visit_suggestions(accepted_bucket_list_item_id)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_dawarich_visit_suggestions_accepted_place_id ON dawarich_visit_suggestions(accepted_place_id)'); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_dawarich_visit_suggestions_matched_bucket_list_item_id ON dawarich_visit_suggestions(matched_bucket_list_item_id)', + ); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_dawarich_visit_suggestions_accepted_bucket_list_item_id ON dawarich_visit_suggestions(accepted_bucket_list_item_id)', + ); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_dawarich_visit_suggestions_accepted_place_id ON dawarich_visit_suggestions(accepted_place_id)', + ); this.addSql('CREATE INDEX IF NOT EXISTS idx_document_connections_provider_id ON document_connections(provider_id)'); this.addSql('CREATE INDEX IF NOT EXISTS idx_trip_document_links_created_by ON trip_document_links(created_by)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_trip_document_links_connection_id ON trip_document_links(connection_id)'); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_trip_document_links_connection_id ON trip_document_links(connection_id)', + ); this.addSql('CREATE INDEX IF NOT EXISTS idx_document_sync_items_file_id ON document_sync_items(file_id)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_roadtrip_day_boundaries_to_assignment_id ON roadtrip_day_boundaries(to_assignment_id)'); - this.addSql('CREATE INDEX IF NOT EXISTS idx_roadtrip_day_boundaries_from_assignment_id ON roadtrip_day_boundaries(from_assignment_id)'); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_roadtrip_day_boundaries_to_assignment_id ON roadtrip_day_boundaries(to_assignment_id)', + ); + this.addSql( + 'CREATE INDEX IF NOT EXISTS idx_roadtrip_day_boundaries_from_assignment_id ON roadtrip_day_boundaries(from_assignment_id)', + ); } } diff --git a/server/src/db/migrations/Migration20200101042300_every_tour_type_the_contract_names.ts b/server/src/db/migrations/Migration20200101042300_every_tour_type_the_contract_names.ts index 7b59fddae..56faddad0 100644 --- a/server/src/db/migrations/Migration20200101042300_every_tour_type_the_contract_names.ts +++ b/server/src/db/migrations/Migration20200101042300_every_tour_type_the_contract_names.ts @@ -1,5 +1,5 @@ -import { Migration } from '@mikro-orm/migrations'; import { columnNames } from '../migration-utils'; +import { Migration } from '@mikro-orm/migrations'; /** * `tour_types` holds every key the shared `tourTypeKeySchema` accepts. Only diff --git a/server/src/db/nul-safe-sqlite-platform.ts b/server/src/db/nul-safe-sqlite-platform.ts index bdc080ab4..f7398ad02 100644 --- a/server/src/db/nul-safe-sqlite-platform.ts +++ b/server/src/db/nul-safe-sqlite-platform.ts @@ -97,7 +97,7 @@ import { SqlitePlatform } from '@mikro-orm/sql'; export class NulSafeSqlitePlatform extends SqlitePlatform { override escape(value: unknown): string { if (typeof value === 'string' && value.includes('\u0000')) { - const literals = value.split('\u0000').map(part => super.escape(part)); + const literals = value.split('\u0000').map((part) => super.escape(part)); return `(${literals.join(' || char(0) || ')})`; } if (typeof value === 'number' && !Number.isFinite(value)) { diff --git a/server/src/db/orm-driver.ts b/server/src/db/orm-driver.ts index 7dcda9f7c..02fc6743f 100644 --- a/server/src/db/orm-driver.ts +++ b/server/src/db/orm-driver.ts @@ -1,8 +1,8 @@ import { getRawConnection } from './database'; +import { NulSafeSqlitePlatform } from './nul-safe-sqlite-platform'; import type { Configuration } from '@mikro-orm/core'; import type { SqlitePlatform } from '@mikro-orm/sql'; import { SqliteConnection, SqliteDriver } from '@mikro-orm/sqlite'; -import { NulSafeSqlitePlatform } from './nul-safe-sqlite-platform'; import type Database from 'better-sqlite3'; import { SqliteDialect, SqliteDriver as KyselySqliteDriver, type Dialect, type SqliteDialectConfig } from 'kysely'; diff --git a/server/src/db/orm.ts b/server/src/db/orm.ts index 9ae93ed7d..0364b8484 100644 --- a/server/src/db/orm.ts +++ b/server/src/db/orm.ts @@ -1,9 +1,9 @@ import mikroOrmConfig from '../mikro-orm.config'; +import { withRequestContext } from '../nest/database/request-context'; import { runDemoSeed } from './database'; import { ensureEmailCaseIndex } from './email-case-index'; import { migrateToHead, NO_SAFETY_NET } from './legacy-baseline'; import { preMigrateSnapshot } from './pre-migrate-snapshot'; -import { withRequestContext } from '../nest/database/request-context'; import type { AnyEntity, EntityClass, EntityManager, EntitySchema, IDatabaseDriver, MikroORM } from '@mikro-orm/core'; import type { Migrator } from '@mikro-orm/migrations'; import { MikroORM as SqliteMikroORM } from '@mikro-orm/sqlite'; @@ -45,7 +45,13 @@ export async function runSchemaBootstrap(orm: AnyOrm, { snapshot = true }: { sna // what it already has is recorded in the same transaction as the run, or the // run would replay the whole history over it (see legacy-baseline.ts). Every // other database just gets its pending migrations. - await migrateToHead(connection, migrator, undefined, undefined, snapshot ? preMigrateSnapshot(connection) : NO_SAFETY_NET); + await migrateToHead( + connection, + migrator, + undefined, + undefined, + snapshot ? preMigrateSnapshot(connection) : NO_SAFETY_NET, + ); // The one index a migration may have had to defer (see email-case-index.ts). await ensureEmailCaseIndex(connection); diff --git a/server/src/db/repositories/AssignmentParticipants.repository.ts b/server/src/db/repositories/AssignmentParticipants.repository.ts index 983cdd695..c86bf7750 100644 --- a/server/src/db/repositories/AssignmentParticipants.repository.ts +++ b/server/src/db/repositories/AssignmentParticipants.repository.ts @@ -1,5 +1,5 @@ -import type { AssignmentParticipants } from '../entities/AssignmentParticipants.entity'; import { coalesce, columnRef } from '../dialect/sql-functions'; +import type { AssignmentParticipants } from '../entities/AssignmentParticipants.entity'; import { TrekRepository } from './_shared/trek-repository'; /** One assignment's participant, from `QueryHelpersService.loadParticipantsByAssignmentIds` (QH3). */ diff --git a/server/src/db/repositories/AuditLog.repository.ts b/server/src/db/repositories/AuditLog.repository.ts index 66fe1180f..595e67128 100644 --- a/server/src/db/repositories/AuditLog.repository.ts +++ b/server/src/db/repositories/AuditLog.repository.ts @@ -1,7 +1,7 @@ import { AuditLog } from '../entities/AuditLog.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** * An `audit_log` row as the API emits it (Plan 3i's `admin.service.ts` @@ -105,7 +105,17 @@ export class AuditLogRepository extends TrekRepository { return await this.kysely() .selectFrom('audit_log as a') .leftJoin('users as u', 'u.id', 'a.user_id') - .select(['a.id', 'a.created_at', 'a.user_id', 'u.username', 'u.email as user_email', 'a.action', 'a.resource', 'a.details', 'a.ip']) + .select([ + 'a.id', + 'a.created_at', + 'a.user_id', + 'u.username', + 'u.email as user_email', + 'a.action', + 'a.resource', + 'a.details', + 'a.ip', + ]) .orderBy('a.id', 'desc') .limit(limit) .offset(offset) diff --git a/server/src/db/repositories/BucketList.repository.ts b/server/src/db/repositories/BucketList.repository.ts index de111b92c..f727514f7 100644 --- a/server/src/db/repositories/BucketList.repository.ts +++ b/server/src/db/repositories/BucketList.repository.ts @@ -1,8 +1,8 @@ import { lowerTrim, lowerTrimParam } from '../dialect/sql-functions'; import type { BucketList } from '../entities/BucketList.entity'; +import type { DB } from '../kysely/db'; import { presenceSet } from './_shared/presence-set'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** `bucket_list` — every scalar column, `AT30`/`AT33`/`AT34`'s `SELECT *` shape. */ export interface BucketListRow { @@ -67,7 +67,12 @@ export class BucketListRepository extends TrekRepository { /** AT30 (`bucketList`) — `SELECT * FROM bucket_list WHERE user_id = ? ORDER BY created_at DESC`. */ async listForUser(userId: number): Promise { - return await this.readDb().selectFrom('bucket_list').selectAll().where('user_id', '=', userId).orderBy('created_at', 'desc').execute(); + return await this.readDb() + .selectFrom('bucket_list') + .selectAll() + .where('user_id', '=', userId) + .orderBy('created_at', 'desc') + .execute(); } /** @@ -128,18 +133,31 @@ export class BucketListRepository extends TrekRepository { target_date: string | null; region_code: string | null; }): Promise { - const inserted = await this.insertDb().insertInto('bucket_list').values(row).returning('id').executeTakeFirstOrThrow(); + const inserted = await this.insertDb() + .insertInto('bucket_list') + .values(row) + .returning('id') + .executeTakeFirstOrThrow(); return inserted.id; } /** AT33 (`createBucketItem`'s post-insert re-select, NOT user-scoped — the legacy statement's own shape) — `SELECT * FROM bucket_list WHERE id = ?`. */ async findById(id: number | string): Promise { - return await this.readDb().selectFrom('bucket_list').selectAll().where('id', '=', id as number).executeTakeFirst(); + return await this.readDb() + .selectFrom('bucket_list') + .selectAll() + .where('id', '=', id as number) + .executeTakeFirst(); } /** AT34/AT36/AT37 (`updateBucketItem`'s guard, its post-update re-select, `deleteBucketItem`'s guard — identical text, three call sites) — `SELECT * FROM bucket_list WHERE id = ? AND user_id = ?`. */ async findForUser(id: number | string, userId: number): Promise { - return await this.readDb().selectFrom('bucket_list').selectAll().where('id', '=', id as number).where('user_id', '=', userId).executeTakeFirst(); + return await this.readDb() + .selectFrom('bucket_list') + .selectAll() + .where('id', '=', id as number) + .where('user_id', '=', userId) + .executeTakeFirst(); } /** @@ -181,12 +199,21 @@ export class BucketListRepository extends TrekRepository { target_date: string | null; }>(write); if (Object.keys(data).length === 0) return; - await this.writeDb().updateTable('bucket_list').set(data).where('id', '=', id as number).where('user_id', '=', userId).execute(); + await this.writeDb() + .updateTable('bucket_list') + .set(data) + .where('id', '=', id as number) + .where('user_id', '=', userId) + .execute(); } /** AT38 (`deleteBucketItem`) — `DELETE FROM bucket_list WHERE id = ? AND user_id = ?`. */ async deleteForUser(id: number | string, userId: number): Promise { - await this.writeDb().deleteFrom('bucket_list').where('id', '=', id as number).where('user_id', '=', userId).execute(); + await this.writeDb() + .deleteFrom('bucket_list') + .where('id', '=', id as number) + .where('user_id', '=', userId) + .execute(); } // --------------------------------------------------------------------------- @@ -216,7 +243,9 @@ export class BucketListRepository extends TrekRepository { * created_at DESC, id DESC`. The boolean-expression `ORDER BY` key is * portable SQL — no dialect helper needed. */ - async listForScan(userId: number): Promise<{ id: number; name: string; lat: number | null; lng: number | null; visited_at: string | null }[]> { + async listForScan( + userId: number, + ): Promise<{ id: number; name: string; lat: number | null; lng: number | null; visited_at: string | null }[]> { return await this.readDb() .selectFrom('bucket_list') .select(['id', 'name', 'lat', 'lng', 'visited_at']) diff --git a/server/src/db/repositories/BudgetCategoryOrder.repository.ts b/server/src/db/repositories/BudgetCategoryOrder.repository.ts index 859e6b240..3ef621976 100644 --- a/server/src/db/repositories/BudgetCategoryOrder.repository.ts +++ b/server/src/db/repositories/BudgetCategoryOrder.repository.ts @@ -1,6 +1,6 @@ import type { BudgetCategoryOrder } from '../entities/BudgetCategoryOrder.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; type BudgetCategoryOrderKyselyDB = Pick; diff --git a/server/src/db/repositories/BudgetItemMembers.repository.ts b/server/src/db/repositories/BudgetItemMembers.repository.ts index 7047d6fc0..9aac404c6 100644 --- a/server/src/db/repositories/BudgetItemMembers.repository.ts +++ b/server/src/db/repositories/BudgetItemMembers.repository.ts @@ -1,7 +1,7 @@ import type { BudgetItemMembers } from '../entities/BudgetItemMembers.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A bare `budget_item_members` row — every scalar column, incl. the two `persist(false)` relation mirrors. */ export interface BudgetItemMemberRow { @@ -43,7 +43,13 @@ export class BudgetItemMembersRepository extends TrekRepository() .selectFrom('budget_item_members as bm') .innerJoin('users as u', 'u.id', 'bm.user_id') - .select(['bm.user_id', 'bm.paid', 'bm.amount', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), 'u.avatar']) + .select([ + 'bm.user_id', + 'bm.paid', + 'bm.amount', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + 'u.avatar', + ]) .where('bm.budget_item_id', '=', budget_item_id) .execute(); } @@ -54,7 +60,14 @@ export class BudgetItemMembersRepository extends TrekRepository() .selectFrom('budget_item_members as bm') .innerJoin('users as u', 'u.id', 'bm.user_id') - .select(['bm.budget_item_id', 'bm.user_id', 'bm.paid', 'bm.amount', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), 'u.avatar']) + .select([ + 'bm.budget_item_id', + 'bm.user_id', + 'bm.paid', + 'bm.amount', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + 'u.avatar', + ]) .where('bm.budget_item_id', 'in', budget_item_ids) .execute(); } @@ -79,12 +92,22 @@ export class BudgetItemMembersRepository extends TrekRepository { + async listForTripWithUsers( + trip_id: number | string, + ): Promise< + { budget_item_id: number; user_id: number; amount: number | null; username: string; avatar: string | null }[] + > { return await this.kysely>() .selectFrom('budget_item_members as bm') .innerJoin('budget_items as b', 'b.id', 'bm.budget_item_id') .innerJoin('users as u', 'u.id', 'bm.user_id') - .select(['bm.budget_item_id', 'bm.user_id', 'bm.amount', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), 'u.avatar']) + .select([ + 'bm.budget_item_id', + 'bm.user_id', + 'bm.amount', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + 'u.avatar', + ]) .where('b.trip_id', '=', trip_id as number) .execute(); } @@ -96,12 +119,21 @@ export class BudgetItemMembersRepository extends TrekRepository { + async listForTripWithUsersAndPaid( + trip_id: number | string, + ): Promise<(BudgetItemMemberWithUserRow & { budget_item_id: number })[]> { return await this.kysely>() .selectFrom('budget_item_members as bm') .innerJoin('budget_items as bi', 'bi.id', 'bm.budget_item_id') .innerJoin('users as u', 'u.id', 'bm.user_id') - .select(['bm.budget_item_id', 'bm.user_id', 'bm.amount', 'bm.paid', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), 'u.avatar']) + .select([ + 'bm.budget_item_id', + 'bm.user_id', + 'bm.amount', + 'bm.paid', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + 'u.avatar', + ]) .where('bi.trip_id', '=', trip_id as number) .execute(); } @@ -114,12 +146,15 @@ export class BudgetItemMembersRepository extends TrekRepository { + async findMemberWithUser( + budget_item_id: number, + user_id: number, + ): Promise<{ user_id: number; paid: number; username: string; avatar: string | null } | undefined> { return await this.kysely() .selectFrom('budget_item_members as bm') .innerJoin('users as u', 'u.id', 'bm.user_id') - .select(['bm.user_id', 'bm.paid', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), 'u.avatar']) + .select([ + 'bm.user_id', + 'bm.paid', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + 'u.avatar', + ]) .where('bm.budget_item_id', '=', budget_item_id) .where('bm.user_id', '=', user_id) .executeTakeFirst(); @@ -155,7 +198,10 @@ export class BudgetItemMembersRepository extends TrekRepository { - await this.kysely().deleteFrom('budget_item_members').where('budget_item_id', '=', budget_item_id).execute(); + await this.kysely() + .deleteFrom('budget_item_members') + .where('budget_item_id', '=', budget_item_id) + .execute(); } /** @@ -168,10 +214,20 @@ export class BudgetItemMembersRepository extends TrekRepository { + async insertIgnore(row: { + budget_item_id: number; + user_id: number; + paid?: number; + amount?: number | null; + }): Promise { await this.kysely() .insertInto('budget_item_members') - .values({ budget_item_id: row.budget_item_id, user_id: row.user_id, paid: row.paid ?? 0, amount: row.amount ?? null }) + .values({ + budget_item_id: row.budget_item_id, + user_id: row.user_id, + paid: row.paid ?? 0, + amount: row.amount ?? null, + }) .onConflict((oc) => oc.doNothing()) .execute(); } diff --git a/server/src/db/repositories/BudgetItemPayers.repository.ts b/server/src/db/repositories/BudgetItemPayers.repository.ts index b6c322f6a..b83e16a50 100644 --- a/server/src/db/repositories/BudgetItemPayers.repository.ts +++ b/server/src/db/repositories/BudgetItemPayers.repository.ts @@ -1,7 +1,7 @@ import type { BudgetItemPayers } from '../entities/BudgetItemPayers.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A bare `budget_item_payers` row — every scalar column, incl. the two `persist(false)` relation mirrors. */ export interface BudgetItemPayerRow { @@ -38,7 +38,12 @@ export class BudgetItemPayersRepository extends TrekRepository return await this.kysely() .selectFrom('budget_item_payers as bp') .innerJoin('users as u', 'u.id', 'bp.user_id') - .select(['bp.user_id', 'bp.amount', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), 'u.avatar']) + .select([ + 'bp.user_id', + 'bp.amount', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + 'u.avatar', + ]) .where('bp.budget_item_id', '=', budget_item_id) .execute(); } @@ -49,7 +54,13 @@ export class BudgetItemPayersRepository extends TrekRepository return await this.kysely() .selectFrom('budget_item_payers as bp') .innerJoin('users as u', 'u.id', 'bp.user_id') - .select(['bp.budget_item_id', 'bp.user_id', 'bp.amount', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), 'u.avatar']) + .select([ + 'bp.budget_item_id', + 'bp.user_id', + 'bp.amount', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + 'u.avatar', + ]) .where('bp.budget_item_id', 'in', budget_item_ids) .execute(); } @@ -65,19 +76,30 @@ export class BudgetItemPayersRepository extends TrekRepository } /** BG74 (`calculateSettlement`'s payer read) — the `budget_item_id IN (SELECT id FROM budget_items WHERE trip_id = ?)` subquery, re-expressed as a join (rule 23) — same result set. */ - async listForTripWithUsers(trip_id: number | string): Promise<(BudgetItemPayerWithUserRow & { budget_item_id: number })[]> { + async listForTripWithUsers( + trip_id: number | string, + ): Promise<(BudgetItemPayerWithUserRow & { budget_item_id: number })[]> { return await this.kysely>() .selectFrom('budget_item_payers as bp') .innerJoin('budget_items as b', 'b.id', 'bp.budget_item_id') .innerJoin('users as u', 'u.id', 'bp.user_id') - .select(['bp.budget_item_id', 'bp.user_id', 'bp.amount', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), 'u.avatar']) + .select([ + 'bp.budget_item_id', + 'bp.user_id', + 'bp.amount', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + 'u.avatar', + ]) .where('b.trip_id', '=', trip_id as number) .execute(); } /** BG4 — `DELETE FROM budget_item_payers WHERE budget_item_id = ?` (replace-all before a re-insert). */ async deleteForItem(budget_item_id: number): Promise { - await this.kysely().deleteFrom('budget_item_payers').where('budget_item_id', '=', budget_item_id).execute(); + await this.kysely() + .deleteFrom('budget_item_payers') + .where('budget_item_id', '=', budget_item_id) + .execute(); } /** BG5/TP65 — `INSERT OR IGNORE INTO budget_item_payers (budget_item_id, user_id, amount) VALUES (?,?,?)`, one row per call (the legacy `prepare(...).run(...)` loop). */ diff --git a/server/src/db/repositories/BudgetItems.repository.ts b/server/src/db/repositories/BudgetItems.repository.ts index 4bcdfae59..cc4cb9463 100644 --- a/server/src/db/repositories/BudgetItems.repository.ts +++ b/server/src/db/repositories/BudgetItems.repository.ts @@ -1,9 +1,9 @@ -import type { BudgetItems } from '../entities/BudgetItems.entity'; import { columnRef } from '../dialect/sql-functions'; +import type { BudgetItems } from '../entities/BudgetItems.entity'; +import type { DB } from '../kysely/db'; +import { presenceSet } from './_shared/presence-set'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import { presenceSet } from './_shared/presence-set'; -import type { DB } from '../kysely/db'; /** A bare `budget_items` row — every scalar column, incl. the four `persist(false)` relation mirrors (the program-wide trap this class goes through Kysely to avoid). */ export interface BudgetItemRow { @@ -56,7 +56,10 @@ export interface BudgetItemMoneyRow { exchange_rate: number; } -type BudgetKyselyDB = Pick; +type BudgetKyselyDB = Pick< + DB, + 'budget_items' | 'budget_item_members' | 'budget_category_order' | 'users' | 'trip_files' | 'file_links' +>; /** * The table `create` (BG25) inserts into. It binds the legacy column list: @@ -101,7 +104,9 @@ export class BudgetItemsRepository extends TrekRepository { async listWithCategoryOrder(trip_id: number | string): Promise { return await this.kysely() .selectFrom('budget_items as bi') - .leftJoin('budget_category_order as bco', (join) => join.onRef('bco.trip_id', '=', 'bi.trip_id').onRef('bco.category', '=', 'bi.category')) + .leftJoin('budget_category_order as bco', (join) => + join.onRef('bco.trip_id', '=', 'bi.trip_id').onRef('bco.category', '=', 'bi.category'), + ) .selectAll('bi') .where('bi.trip_id', '=', trip_id as number) .orderBy((eb) => eb.fn.coalesce('bco.sort_order', eb.val(999999)), 'asc') @@ -111,33 +116,64 @@ export class BudgetItemsRepository extends TrekRepository { /** BG14 — `SELECT currency FROM budget_items WHERE id = ? AND trip_id = ?`. `id: number` (Plan 4 Task 8b, U6 — the program's gate-level id parsing carry: its one caller, `freezeForeignRate`, is only reached with `BudgetController.update`'s `toRowId`-parsed id). */ async getCurrency(id: number, trip_id: number | string): Promise { - const row = await this.kysely().selectFrom('budget_items').select('currency').where('id', '=', id).where('trip_id', '=', trip_id as number).executeTakeFirst(); + const row = await this.kysely() + .selectFrom('budget_items') + .select('currency') + .where('id', '=', id) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); return row?.currency; } /** BG31/BG32/BG57 — `SELECT * FROM budget_items WHERE id = ? AND trip_id = ?`, the full-row trip-scoping guard. `id: number`, same Plan 4 Task 8b narrowing as {@link getCurrency}; `trip_id` stays `number | string`, a separate, still-accepted carry. */ async findInTrip(id: number, trip_id: number | string): Promise { - return await this.kysely().selectFrom('budget_items').selectAll().where('id', '=', id).where('trip_id', '=', trip_id as number).executeTakeFirst(); + return await this.kysely() + .selectFrom('budget_items') + .selectAll() + .where('id', '=', id) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); } /** BG50/BG68 — `SELECT id FROM budget_items WHERE id = ? AND trip_id = ?`, the id-only trip-scoping guard. `id: number`, same Plan 4 Task 8b narrowing as {@link getCurrency}. */ async existsInTrip(id: number, trip_id: number | string): Promise<{ id: number } | undefined> { - return await this.kysely().selectFrom('budget_items').select('id').where('id', '=', id).where('trip_id', '=', trip_id as number).executeTakeFirst(); + return await this.kysely() + .selectFrom('budget_items') + .select('id') + .where('id', '=', id) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); } /** BG52 (`deleteBudgetItem`'s pre-image) — `SELECT id, reservation_id FROM budget_items WHERE id = ? AND trip_id = ?`. `id: number`, same Plan 4 Task 8b narrowing as {@link getCurrency}. */ - async findForDelete(id: number, trip_id: number | string): Promise<{ id: number; reservation_id: number | null } | undefined> { - return await this.kysely().selectFrom('budget_items').select(['id', 'reservation_id']).where('id', '=', id).where('trip_id', '=', trip_id as number).executeTakeFirst(); + async findForDelete( + id: number, + trip_id: number | string, + ): Promise<{ id: number; reservation_id: number | null } | undefined> { + return await this.kysely() + .selectFrom('budget_items') + .select(['id', 'reservation_id']) + .where('id', '=', id) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); } /** BG30/BG49/BG51/BG63 — `SELECT * FROM budget_items WHERE id = ?`, the post-write re-select (no trip filter — the caller already knows `id` is in-scope, having just written it). `id: number`, same Plan 4 Task 8b narrowing as {@link getCurrency}. */ async findById(id: number): Promise { - return await this.kysely().selectFrom('budget_items').selectAll().where('id', '=', id).executeTakeFirst(); + return await this.kysely() + .selectFrom('budget_items') + .selectAll() + .where('id', '=', id) + .executeTakeFirst(); } /** BG21 — `SELECT MAX(sort_order) as max FROM budget_items WHERE trip_id = ?`. */ async maxSortOrder(trip_id: number | string): Promise { - const row = await this.kysely().selectFrom('budget_items').select((eb) => eb.fn.max('sort_order').as('max')).where('trip_id', '=', trip_id as number).executeTakeFirst(); + const row = await this.kysely() + .selectFrom('budget_items') + .select((eb) => eb.fn.max('sort_order').as('max')) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); return row?.max ?? null; } @@ -149,11 +185,26 @@ export class BudgetItemsRepository extends TrekRepository { * different signature: an unpersisted-entity builder, not an INSERT). */ async insertItem(row: { - trip_id: number | string; category: string; name: string; total_price: number; currency: string | null; - exchange_rate: number; persons: number | null; days: number | null; note: string | null; ticket_json: string | null; - sort_order: number; expense_date: string | null; reservation_id: number | null; place_id: number | null; + trip_id: number | string; + category: string; + name: string; + total_price: number; + currency: string | null; + exchange_rate: number; + persons: number | null; + days: number | null; + note: string | null; + ticket_json: string | null; + sort_order: number; + expense_date: string | null; + reservation_id: number | null; + place_id: number | null; }): Promise { - const inserted = await this.kysely().insertInto('budget_items').values({ ...row, trip_id: row.trip_id as number }).returning('id').executeTakeFirstOrThrow(); + const inserted = await this.kysely() + .insertInto('budget_items') + .values({ ...row, trip_id: row.trip_id as number }) + .returning('id') + .executeTakeFirstOrThrow(); return inserted.id; } @@ -166,11 +217,26 @@ export class BudgetItemsRepository extends TrekRepository { * column survives a copy unexamined, matching legacy exactly). */ async insertCopy(row: { - trip_id: number | string; category: string; name: string; total_price: number; persons: number | null; days: number | null; - note: string | null; sort_order: number | null; reservation_id: number | null; currency: string | null; - exchange_rate: number; expense_date: string | null; ticket_json: string | null; paid_by_user_id: number | null; + trip_id: number | string; + category: string; + name: string; + total_price: number; + persons: number | null; + days: number | null; + note: string | null; + sort_order: number | null; + reservation_id: number | null; + currency: string | null; + exchange_rate: number; + expense_date: string | null; + ticket_json: string | null; + paid_by_user_id: number | null; }): Promise { - const inserted = await this.kysely().insertInto('budget_items').values({ ...row, trip_id: row.trip_id as number }).returning('id').executeTakeFirstOrThrow(); + const inserted = await this.kysely() + .insertInto('budget_items') + .values({ ...row, trip_id: row.trip_id as number }) + .returning('id') + .executeTakeFirstOrThrow(); return inserted.id; } @@ -185,25 +251,38 @@ export class BudgetItemsRepository extends TrekRepository { * column is a true presence sentinel (`present = data.field !== undefined`). */ /** `id: number` — same Plan 4 Task 8b (U6) narrowing as {@link getCurrency} (its one caller, `updateBudgetItem`, is only reached with a `toRowId`-parsed id). */ - async update(id: number, write: { - category?: readonly [present: boolean, value: string]; - name?: readonly [present: boolean, value: string]; - total_price?: readonly [present: boolean, value: number]; - currency?: readonly [present: boolean, value: string | null]; - exchange_rate?: readonly [present: boolean, value: number]; - persons?: readonly [present: boolean, value: number | null]; - days?: readonly [present: boolean, value: number | null]; - note?: readonly [present: boolean, value: string | null]; - ticket_json?: readonly [present: boolean, value: string | null]; - sort_order?: readonly [present: boolean, value: number]; - expense_date?: readonly [present: boolean, value: string | null]; - reservation_id?: readonly [present: boolean, value: number | null]; - place_id?: readonly [present: boolean, value: number | null]; - }): Promise { + async update( + id: number, + write: { + category?: readonly [present: boolean, value: string]; + name?: readonly [present: boolean, value: string]; + total_price?: readonly [present: boolean, value: number]; + currency?: readonly [present: boolean, value: string | null]; + exchange_rate?: readonly [present: boolean, value: number]; + persons?: readonly [present: boolean, value: number | null]; + days?: readonly [present: boolean, value: number | null]; + note?: readonly [present: boolean, value: string | null]; + ticket_json?: readonly [present: boolean, value: string | null]; + sort_order?: readonly [present: boolean, value: number]; + expense_date?: readonly [present: boolean, value: string | null]; + reservation_id?: readonly [present: boolean, value: number | null]; + place_id?: readonly [present: boolean, value: number | null]; + }, + ): Promise { const data = presenceSet<{ - category: string; name: string; total_price: number; currency: string | null; exchange_rate: number; - persons: number | null; days: number | null; note: string | null; ticket_json: string | null; - sort_order: number; expense_date: string | null; reservation_id: number | null; place_id: number | null; + category: string; + name: string; + total_price: number; + currency: string | null; + exchange_rate: number; + persons: number | null; + days: number | null; + note: string | null; + ticket_json: string | null; + sort_order: number; + expense_date: string | null; + reservation_id: number | null; + place_id: number | null; }>(write); if (Object.keys(data).length === 0) return; await this.kysely().updateTable('budget_items').set(data).where('id', '=', id).execute(); @@ -211,17 +290,30 @@ export class BudgetItemsRepository extends TrekRepository { /** BG6/BG34 — `UPDATE budget_items SET total_price = ? WHERE id = ?`. */ async setTotalPrice(id: number | string, total_price: number): Promise { - await this.kysely().updateTable('budget_items').set({ total_price }).where('id', '=', id as number).execute(); + await this.kysely() + .updateTable('budget_items') + .set({ total_price }) + .where('id', '=', id as number) + .execute(); } /** BG37/BG40/BG61/BG62 — `UPDATE budget_items SET persons = ? WHERE id = ?` (`persons: null` is BG62's clear branch). */ async setPersons(id: number | string, persons: number | null): Promise { - await this.kysely().updateTable('budget_items').set({ persons }).where('id', '=', id as number).execute(); + await this.kysely() + .updateTable('budget_items') + .set({ persons }) + .where('id', '=', id as number) + .execute(); } /** BG83 (`reorderItems`, looped) — `UPDATE budget_items SET sort_order = ? WHERE id = ? AND trip_id = ?`. */ async setSortOrder(id: number | string, trip_id: number | string, sort_order: number): Promise { - await this.kysely().updateTable('budget_items').set({ sort_order }).where('id', '=', id as number).where('trip_id', '=', trip_id as number).execute(); + await this.kysely() + .updateTable('budget_items') + .set({ sort_order }) + .where('id', '=', id as number) + .where('trip_id', '=', trip_id as number) + .execute(); } /** BG53 — `DELETE FROM budget_items WHERE id = ?` (no trip scoping in the statement itself — relies on the caller's prior {@link findForDelete} gate, matching legacy). `id: number`, same Plan 4 Task 8b narrowing as {@link getCurrency}. */ @@ -237,7 +329,11 @@ export class BudgetItemsRepository extends TrekRepository { /** BG72 (`calculateSettlement`) — `SELECT * FROM budget_items WHERE trip_id = ?`. */ async listAllForTrip(trip_id: number | string): Promise { - return await this.kysely().selectFrom('budget_items').selectAll().where('trip_id', '=', trip_id as number).execute(); + return await this.kysely() + .selectFrom('budget_items') + .selectAll() + .where('trip_id', '=', trip_id as number) + .execute(); } /** @@ -246,7 +342,12 @@ export class BudgetItemsRepository extends TrekRepository { * (unlike BG72's `listAllForTrip` above, which has no `ORDER BY`). */ async listPublicForShare(trip_id: number | string): Promise { - return await this.kysely().selectFrom('budget_items').selectAll().where('trip_id', '=', trip_id as number).orderBy('category', 'asc').execute(); + return await this.kysely() + .selectFrom('budget_items') + .selectAll() + .where('trip_id', '=', trip_id as number) + .orderBy('category', 'asc') + .execute(); } /** @@ -277,7 +378,9 @@ export class BudgetItemsRepository extends TrekRepository { .where('currency', 'is not', null) .where('currency', '!=', '') .where((eb) => eb(eb.fn('upper', ['currency']), '!=', trip_currency)) - .where((eb) => eb.or([eb('exchange_rate', 'is', null), eb('exchange_rate', '<=', 0), eb('exchange_rate', '=', 1)])) + .where((eb) => + eb.or([eb('exchange_rate', 'is', null), eb('exchange_rate', '<=', 0), eb('exchange_rate', '=', 1)]), + ) .limit(1) .executeTakeFirst(); return row !== undefined; @@ -300,9 +403,11 @@ export class BudgetItemsRepository extends TrekRepository { .where('currency', 'is not', null) .where('currency', '!=', '') .where((eb) => eb(eb.fn('upper', ['currency']), '!=', trip_currency)) - .where((eb) => eb.or([eb('exchange_rate', 'is', null), eb('exchange_rate', '<=', 0), eb('exchange_rate', '=', 1)])) + .where((eb) => + eb.or([eb('exchange_rate', 'is', null), eb('exchange_rate', '<=', 0), eb('exchange_rate', '=', 1)]), + ) .execute(); - return rows.map(r => r.cur); + return rows.map((r) => r.cur); } /** BG91's `budget_items` half (`freezeMissingRates`) — `SELECT id FROM budget_items WHERE trip_id = ? AND UPPER(currency) = ? AND (exchange_rate IS NULL OR exchange_rate <= 0 OR exchange_rate = 1)`. */ @@ -312,9 +417,11 @@ export class BudgetItemsRepository extends TrekRepository { .select('id') .where('trip_id', '=', trip_id as number) .where((eb) => eb(eb.fn('upper', ['currency']), '=', currency)) - .where((eb) => eb.or([eb('exchange_rate', 'is', null), eb('exchange_rate', '<=', 0), eb('exchange_rate', '=', 1)])) + .where((eb) => + eb.or([eb('exchange_rate', 'is', null), eb('exchange_rate', '<=', 0), eb('exchange_rate', '=', 1)]), + ) .execute(); - return rows.map(r => r.id); + return rows.map((r) => r.id); } /** BG92's `budget_items` half (`freezeMissingRates`) — `UPDATE budget_items SET exchange_rate = ? WHERE trip_id = ? AND UPPER(currency) = ? AND (exchange_rate IS NULL OR exchange_rate <= 0 OR exchange_rate = 1)`, the compare-and-set write. */ @@ -324,12 +431,17 @@ export class BudgetItemsRepository extends TrekRepository { .set({ exchange_rate }) .where('trip_id', '=', trip_id as number) .where((eb) => eb(eb.fn('upper', ['currency']), '=', currency)) - .where((eb) => eb.or([eb('exchange_rate', 'is', null), eb('exchange_rate', '<=', 0), eb('exchange_rate', '=', 1)])) + .where((eb) => + eb.or([eb('exchange_rate', 'is', null), eb('exchange_rate', '<=', 0), eb('exchange_rate', '=', 1)]), + ) .execute(); } /** BG93 (`resyncReservationPrice`) — `SELECT total_price, currency FROM budget_items WHERE trip_id = ? AND reservation_id = ? ORDER BY id`, every expense linked to one booking (#2084). */ - async listLinkedToReservation(trip_id: number | string, reservation_id: number): Promise<{ total_price: number | null; currency: string | null }[]> { + async listLinkedToReservation( + trip_id: number | string, + reservation_id: number, + ): Promise<{ total_price: number | null; currency: string | null }[]> { return await this.kysely() .selectFrom('budget_items') .select(['total_price', 'currency']) @@ -351,13 +463,24 @@ export class BudgetItemsRepository extends TrekRepository { /** BG18's `budget_items` half — `SELECT DISTINCT currency AS cur FROM budget_items WHERE trip_id = ? AND currency IS NOT NULL`. */ async listDistinctCurrencies(trip_id: number | string): Promise { - const rows = await this.kysely().selectFrom('budget_items').select('currency').distinct().where('trip_id', '=', trip_id as number).where('currency', 'is not', null).execute(); - return rows.map(r => r.currency as string); + const rows = await this.kysely() + .selectFrom('budget_items') + .select('currency') + .distinct() + .where('trip_id', '=', trip_id as number) + .where('currency', 'is not', null) + .execute(); + return rows.map((r) => r.currency as string); } /** BG19's `budget_items` half — `UPDATE budget_items SET exchange_rate = ? WHERE trip_id = ? AND currency = ?`. */ async setExchangeRateForCurrency(trip_id: number | string, currency: string, exchange_rate: number): Promise { - await this.kysely().updateTable('budget_items').set({ exchange_rate }).where('trip_id', '=', trip_id as number).where('currency', '=', currency).execute(); + await this.kysely() + .updateTable('budget_items') + .set({ exchange_rate }) + .where('trip_id', '=', trip_id as number) + .where('currency', '=', currency) + .execute(); } // --------------------------------------------------------------------------- @@ -408,7 +531,11 @@ export class BudgetItemsRepository extends TrekRepository { /** BG8 (`unlinkReceipts`) — `UPDATE file_links SET budget_item_id = NULL WHERE id = ?`. */ async clearLinkBudgetRef(link_id: number): Promise { - await this.kysely().updateTable('file_links').set({ budget_item_id: null }).where('id', '=', link_id).execute(); + await this.kysely() + .updateTable('file_links') + .set({ budget_item_id: null }) + .where('id', '=', link_id) + .execute(); } /** BG9 (`unlinkReceipts`) — `DELETE FROM file_links WHERE id = ?`. */ @@ -438,7 +565,11 @@ export class BudgetItemsRepository extends TrekRepository { /** BG46 (`updateBudgetItem`'s `adopt`) — `UPDATE file_links SET budget_item_id = ? WHERE id = ?`, re-parenting a spare link row instead of inserting a duplicate. */ async adoptSpareLink(link_id: number, budget_item_id: number | string): Promise { - await this.kysely().updateTable('file_links').set({ budget_item_id: budget_item_id as number }).where('id', '=', link_id).execute(); + await this.kysely() + .updateTable('file_links') + .set({ budget_item_id: budget_item_id as number }) + .where('id', '=', link_id) + .execute(); } /** BG47 — `SELECT 1 FROM file_links WHERE file_id = ? AND budget_item_id = ?`, the "already this item's receipt" short-circuit. */ @@ -454,7 +585,13 @@ export class BudgetItemsRepository extends TrekRepository { /** BG48 — `SELECT id FROM file_links WHERE file_id = ? AND budget_item_id IS NULL LIMIT 1` (`spare`), drives {@link adoptSpareLink}'s adopt-vs-insert branch. */ async findSpareLink(file_id: number): Promise<{ id: number } | undefined> { - return await this.kysely().selectFrom('file_links').select('id').where('file_id', '=', file_id).where('budget_item_id', 'is', null).limit(1).executeTakeFirst(); + return await this.kysely() + .selectFrom('file_links') + .select('id') + .where('file_id', '=', file_id) + .where('budget_item_id', 'is', null) + .limit(1) + .executeTakeFirst(); } // --------------------------------------------------------------------------- @@ -470,7 +607,11 @@ export class BudgetItemsRepository extends TrekRepository { * them all. */ async listIdsByReservation(reservation_id: number): Promise<{ id: number }[]> { - return await this.kysely().selectFrom('budget_items').select('id').where('reservation_id', '=', reservation_id).execute(); + return await this.kysely() + .selectFrom('budget_items') + .select('id') + .where('reservation_id', '=', reservation_id) + .execute(); } /** PL15 (`linkedExpenseIds`) — `SELECT id FROM budget_items WHERE trip_id = ? AND place_id IN (dynamic)`. */ @@ -482,17 +623,29 @@ export class BudgetItemsRepository extends TrekRepository { .where('trip_id', '=', trip_id as number) .where('place_id', 'in', place_ids as number[]) .execute(); - return rows.map(r => r.id); + return rows.map((r) => r.id); } /** PL18/PL22 — `DELETE FROM budget_items WHERE trip_id = ? AND place_id = ?`. */ async deleteForPlace(trip_id: number | string, place_id: number | string): Promise { - await this.kysely().deleteFrom('budget_items').where('trip_id', '=', trip_id as number).where('place_id', '=', place_id as number).execute(); + await this.kysely() + .deleteFrom('budget_items') + .where('trip_id', '=', trip_id as number) + .where('place_id', '=', place_id as number) + .execute(); } /** RS52/RS53 — `SELECT id FROM budget_items WHERE trip_id = ? AND reservation_id = ?`. */ - async findIdByReservationInTrip(trip_id: number | string, reservation_id: number | string): Promise<{ id: number } | undefined> { - return await this.kysely().selectFrom('budget_items').select('id').where('trip_id', '=', trip_id as number).where('reservation_id', '=', reservation_id as number).executeTakeFirst(); + async findIdByReservationInTrip( + trip_id: number | string, + reservation_id: number | string, + ): Promise<{ id: number } | undefined> { + return await this.kysely() + .selectFrom('budget_items') + .select('id') + .where('trip_id', '=', trip_id as number) + .where('reservation_id', '=', reservation_id as number) + .executeTakeFirst(); } /** @@ -502,7 +655,10 @@ export class BudgetItemsRepository extends TrekRepository { * all, the type change re-files each one still on the derived category, and * the price field is left alone once more than one is linked. */ - async listIdAndCategoryByReservation(trip_id: number | string, reservation_id: number | string): Promise<{ id: number; category: string }[]> { + async listIdAndCategoryByReservation( + trip_id: number | string, + reservation_id: number | string, + ): Promise<{ id: number; category: string }[]> { return await this.kysely() .selectFrom('budget_items') .select(['id', 'category']) @@ -514,11 +670,19 @@ export class BudgetItemsRepository extends TrekRepository { /** RS54 — `UPDATE budget_items SET reservation_id = ? WHERE id = ?` (binds the reservation id verbatim — a string in the legacy call site, matching parity). */ async setReservationId(id: number | string, reservation_id: number | string): Promise { - await this.kysely().updateTable('budget_items').set({ reservation_id: reservation_id as number }).where('id', '=', id as number).execute(); + await this.kysely() + .updateTable('budget_items') + .set({ reservation_id: reservation_id as number }) + .where('id', '=', id as number) + .execute(); } /** UC5 (`UserCleanupService.cleanupUserReferences`) — `UPDATE budget_items SET paid_by_user_id = NULL WHERE paid_by_user_id = ?`. */ async clearPaidByUser(user_id: number): Promise { - await this.kysely().updateTable('budget_items').set({ paid_by_user_id: null }).where('paid_by_user_id', '=', user_id).execute(); + await this.kysely() + .updateTable('budget_items') + .set({ paid_by_user_id: null }) + .where('paid_by_user_id', '=', user_id) + .execute(); } } diff --git a/server/src/db/repositories/BudgetSettlements.repository.ts b/server/src/db/repositories/BudgetSettlements.repository.ts index b84cfa8fb..7d0739ec9 100644 --- a/server/src/db/repositories/BudgetSettlements.repository.ts +++ b/server/src/db/repositories/BudgetSettlements.repository.ts @@ -1,8 +1,8 @@ import type { BudgetSettlements } from '../entities/BudgetSettlements.entity'; +import type { DB } from '../kysely/db'; +import { presenceSet } from './_shared/presence-set'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import { presenceSet } from './_shared/presence-set'; -import type { DB } from '../kysely/db'; /** A bare `budget_settlements` row — every scalar column, incl. the `persist(false)` relation mirrors. */ export interface BudgetSettlementRow { @@ -50,10 +50,21 @@ export class BudgetSettlementsRepository extends TrekRepository eb.fn.coalesce('fu.display_name', 'fu.username').as('from_username'), 'fu.avatar as from_avatar', - (eb) => eb.fn.coalesce('tu.display_name', 'tu.username').as('to_username'), 'tu.avatar as to_avatar', + 's.id', + 's.trip_id', + 's.from_user_id', + 's.to_user_id', + 's.amount', + 's.currency', + 's.exchange_rate', + 's.created_at', + 's.settled_at', + 's.note', + 's.created_by_user_id', + (eb) => eb.fn.coalesce('fu.display_name', 'fu.username').as('from_username'), + 'fu.avatar as from_avatar', + (eb) => eb.fn.coalesce('tu.display_name', 'tu.username').as('to_username'), + 'tu.avatar as to_avatar', ]); } @@ -98,14 +109,28 @@ export class BudgetSettlementsRepository extends TrekRepository { const inserted = await this.kysely() .insertInto('budget_settlements') .values({ - trip_id: row.trip_id as number, from_user_id: row.from_user_id, to_user_id: row.to_user_id, amount: row.amount, - currency: row.currency, exchange_rate: row.exchange_rate, settled_at: row.settled_at, note: row.note, created_by_user_id: row.created_by_user_id, + trip_id: row.trip_id as number, + from_user_id: row.from_user_id, + to_user_id: row.to_user_id, + amount: row.amount, + currency: row.currency, + exchange_rate: row.exchange_rate, + settled_at: row.settled_at, + note: row.note, + created_by_user_id: row.created_by_user_id, }) .returning('id') .executeTakeFirstOrThrow(); @@ -119,14 +144,24 @@ export class BudgetSettlementsRepository extends TrekRepository { - const data = presenceSet<{ currency: string | null; exchange_rate: number; settled_at: string | null; note: string | null }>({ + async update( + id: number, + write: { + from_user_id: number; + to_user_id: number; + amount: number; + currency?: [present: boolean, value: string | null]; + exchange_rate?: [present: boolean, value: number]; + settled_at?: [present: boolean, value: string | null]; + note?: [present: boolean, value: string | null]; + }, + ): Promise { + const data = presenceSet<{ + currency: string | null; + exchange_rate: number; + settled_at: string | null; + note: string | null; + }>({ currency: write.currency, exchange_rate: write.exchange_rate, settled_at: write.settled_at, @@ -177,7 +212,7 @@ export class BudgetSettlementsRepository extends TrekRepository r.currency as string); + return rows.map((r) => r.currency as string); } /** @@ -196,9 +231,11 @@ export class BudgetSettlementsRepository extends TrekRepository eb(eb.fn('upper', ['currency']), '!=', trip_currency)) - .where((eb) => eb.or([eb('exchange_rate', 'is', null), eb('exchange_rate', '<=', 0), eb('exchange_rate', '=', 1)])) + .where((eb) => + eb.or([eb('exchange_rate', 'is', null), eb('exchange_rate', '<=', 0), eb('exchange_rate', '=', 1)]), + ) .execute(); - return rows.map(r => r.cur); + return rows.map((r) => r.cur); } /** BG91's `budget_settlements` half (`freezeMissingRates`) — `SELECT id FROM budget_settlements WHERE trip_id = ? AND UPPER(currency) = ? AND (exchange_rate IS NULL OR exchange_rate <= 0 OR exchange_rate = 1)`. */ @@ -208,9 +245,11 @@ export class BudgetSettlementsRepository extends TrekRepository('all', false); + return this.qb('c').select(['c.id', 'c.name']).execute<{ id: number; name: string }[]>('all', false); } /** diff --git a/server/src/db/repositories/CollabLinks.repository.ts b/server/src/db/repositories/CollabLinks.repository.ts index b7a4f640b..989ae394f 100644 --- a/server/src/db/repositories/CollabLinks.repository.ts +++ b/server/src/db/repositories/CollabLinks.repository.ts @@ -1,8 +1,8 @@ -import type { CollabLinks } from '../entities/CollabLinks.entity'; import { currentTimestamp } from '../dialect/sql-functions'; +import type { CollabLinks } from '../entities/CollabLinks.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A bare `collab_links` row — every scalar column of the entity, incl. the two `persist(false)` relation mirrors (`trip_id`, `user_id`). */ export interface CollabLinkRow { @@ -47,17 +47,38 @@ export class CollabLinksRepository extends TrekRepository { * `string | number` id via `toRowId` before calling (rule 15). */ async listForTrip(trip_id: number): Promise { - return await this.joinedQuery().where('l.trip_id', '=', trip_id).orderBy('l.pinned', 'desc').orderBy('l.created_at', 'desc').execute(); + return await this.joinedQuery() + .where('l.trip_id', '=', trip_id) + .orderBy('l.pinned', 'desc') + .orderBy('l.created_at', 'desc') + .execute(); } /** CB39 (`updateLink`'s trip-scoping guard) — `SELECT * FROM collab_links WHERE id = ? AND trip_id = ?`. Full row: the falsy-coercion update (CB40) needs the pre-image to fall back onto. */ async findInTrip(id: number, trip_id: number): Promise { - return await this.kysely().selectFrom('collab_links').selectAll().where('id', '=', id).where('trip_id', '=', trip_id).executeTakeFirst(); + return await this.kysely() + .selectFrom('collab_links') + .selectAll() + .where('id', '=', id) + .where('trip_id', '=', trip_id) + .executeTakeFirst(); } /** CB37 (`createLink`) — `INSERT INTO collab_links (trip_id, user_id, title, url, pinned) VALUES (?×5)`. The service trims `title`/`url` before calling (matching the legacy inline `.trim()`). Returns the new row's id. */ - async insertLink(row: { trip_id: number | string; user_id: number; title: string; url: string; pinned: number }): Promise { - return await this.insert({ trip: row.trip_id, user: row.user_id, title: row.title, url: row.url, pinned: row.pinned }); + async insertLink(row: { + trip_id: number | string; + user_id: number; + title: string; + url: string; + pinned: number; + }): Promise { + return await this.insert({ + trip: row.trip_id, + user: row.user_id, + title: row.title, + url: row.url, + pinned: row.pinned, + }); } /** @@ -75,7 +96,10 @@ export class CollabLinksRepository extends TrekRepository { */ async update(id: number, trip_id: number, write: { title: string; url: string; pinned: number }): Promise { const platform = this.getEntityManager().getPlatform(); - await this.nativeUpdate({ id, trip: trip_id }, { title: write.title, url: write.url, pinned: write.pinned, updated_at: currentTimestamp(platform) }); + await this.nativeUpdate( + { id, trip: trip_id }, + { title: write.title, url: write.url, pinned: write.pinned, updated_at: currentTimestamp(platform) }, + ); } /** CB38/CB41 (`createLink`'s post-insert re-select, `updateLink`'s post-write re-select) — `SELECT l.*, u.username FROM collab_links l JOIN users u ON u.id = l.user_id WHERE l.id = ?`, same text at both call sites. */ diff --git a/server/src/db/repositories/CollabMessages.repository.ts b/server/src/db/repositories/CollabMessages.repository.ts index b26eabf06..fcc99e587 100644 --- a/server/src/db/repositories/CollabMessages.repository.ts +++ b/server/src/db/repositories/CollabMessages.repository.ts @@ -1,8 +1,8 @@ import type { CollabMessages } from '../entities/CollabMessages.entity'; +import type { DB } from '../kysely/db'; import type { TripFileRow } from './TripFiles.repository'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A bare `collab_messages` row — every scalar column of the entity, incl. the three `persist(false)` mirrors (`trip_id`, `user_id`, `reply_to`). */ export interface CollabMessageRow { @@ -142,12 +142,23 @@ export class CollabMessagesRepository extends TrekRepository { * .deleteMessage` docstring for how that pre-existing gap is preserved. */ async findInTrip(id: number, trip_id: number): Promise { - return await this.kysely().selectFrom('collab_messages').selectAll().where('id', '=', id).where('trip_id', '=', trip_id).executeTakeFirst(); + return await this.kysely() + .selectFrom('collab_messages') + .selectAll() + .where('id', '=', id) + .where('trip_id', '=', trip_id) + .executeTakeFirst(); } /** CB47 (`createMessage`'s reply-target validation) — `SELECT id FROM collab_messages WHERE id = ? AND trip_id = ? AND deleted = 0`. A soft-deleted message is gone as far as anyone replying is concerned. */ async findActiveInTrip(id: number, trip_id: number): Promise<{ id: number } | undefined> { - return await this.kysely().selectFrom('collab_messages').select(['id']).where('id', '=', id).where('trip_id', '=', trip_id).where('deleted', '=', 0).executeTakeFirst(); + return await this.kysely() + .selectFrom('collab_messages') + .select(['id']) + .where('id', '=', id) + .where('trip_id', '=', trip_id) + .where('deleted', '=', 0) + .executeTakeFirst(); } /** CB44 (`countMessages`) — `SELECT COUNT(*) as cnt FROM collab_messages WHERE trip_id = ?`. */ @@ -156,7 +167,12 @@ export class CollabMessagesRepository extends TrekRepository { } /** CB48 (`createMessage`, inside its transaction) — `INSERT INTO collab_messages (trip_id, user_id, text, reply_to) VALUES (?×4)`. Returns the new row's id. */ - async insertMessage(trip_id: number | string, user_id: number, text: string, reply_to: number | null): Promise { + async insertMessage( + trip_id: number | string, + user_id: number, + text: string, + reply_to: number | null, + ): Promise { return await this.insert({ trip: trip_id, user: user_id, text, reply_to }); } @@ -188,7 +204,15 @@ export class CollabMessagesRepository extends TrekRepository { * `INSERT INTO trip_files (trip_id, message_id, filename, original_name, * file_size, mime_type, uploaded_by) VALUES (?×7)`. */ - async insertAttachmentForMessage(row: { trip_id: number | string; message_id: number | string; filename: string; original_name: string; file_size: number; mime_type: string; uploaded_by: number }): Promise { + async insertAttachmentForMessage(row: { + trip_id: number | string; + message_id: number | string; + filename: string; + original_name: string; + file_size: number; + mime_type: string; + uploaded_by: number; + }): Promise { await this.kysely() .insertInto('trip_files') .values({ @@ -205,11 +229,20 @@ export class CollabMessagesRepository extends TrekRepository { /** CB52 (`deleteMessage`'s pre-tx storage-cleanup list) — `SELECT filename FROM trip_files WHERE message_id = ? AND trip_id = ?`, run BEFORE the transaction (the legacy ordering). */ async listFilenamesForMessage(message_id: number, trip_id: number): Promise<{ filename: string }[]> { - return await this.kysely().selectFrom('trip_files').select(['filename']).where('message_id', '=', message_id).where('trip_id', '=', trip_id).execute(); + return await this.kysely() + .selectFrom('trip_files') + .select(['filename']) + .where('message_id', '=', message_id) + .where('trip_id', '=', trip_id) + .execute(); } /** CB53 (`deleteMessage`, inside its transaction, before CB54) — `DELETE FROM trip_files WHERE message_id = ? AND trip_id = ?`. */ async deleteAttachmentsForMessage(message_id: number, trip_id: number): Promise { - await this.kysely().deleteFrom('trip_files').where('message_id', '=', message_id).where('trip_id', '=', trip_id).execute(); + await this.kysely() + .deleteFrom('trip_files') + .where('message_id', '=', message_id) + .where('trip_id', '=', trip_id) + .execute(); } } diff --git a/server/src/db/repositories/CollabNotes.repository.ts b/server/src/db/repositories/CollabNotes.repository.ts index 2de93fda2..be4f2d25f 100644 --- a/server/src/db/repositories/CollabNotes.repository.ts +++ b/server/src/db/repositories/CollabNotes.repository.ts @@ -1,9 +1,9 @@ -import type { CollabNotes } from '../entities/CollabNotes.entity'; import { currentTimestamp } from '../dialect/sql-functions'; +import type { CollabNotes } from '../entities/CollabNotes.entity'; +import type { DB } from '../kysely/db'; import type { TripFileRow } from './TripFiles.repository'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A bare `collab_notes` row — every scalar column of the entity, incl. the two `persist(false)` relation mirrors (`trip_id`, `user_id`). */ export interface CollabNoteRow { @@ -98,12 +98,21 @@ export class CollabNotesRepository extends TrekRepository { * this query at all. */ async listForTrip(trip_id: number): Promise { - return await this.joinedQuery().where('n.trip_id', '=', trip_id).orderBy('n.pinned', 'desc').orderBy('n.updated_at', 'desc').execute(); + return await this.joinedQuery() + .where('n.trip_id', '=', trip_id) + .orderBy('n.pinned', 'desc') + .orderBy('n.updated_at', 'desc') + .execute(); } /** CB10/CB13/CB17 (`updateNote`/`deleteNote`/`addNoteFile`'s shared trip-scoping guard) — `SELECT * FROM collab_notes WHERE id = ? AND trip_id = ?`, same text at all three call sites. */ async findInTrip(id: number, trip_id: number): Promise { - return await this.kysely().selectFrom('collab_notes').selectAll().where('id', '=', id).where('trip_id', '=', trip_id).executeTakeFirst(); + return await this.kysely() + .selectFrom('collab_notes') + .selectAll() + .where('id', '=', id) + .where('trip_id', '=', trip_id) + .executeTakeFirst(); } /** CB9/CB12 (`createNote`'s post-insert re-select, `updateNote`'s post-write re-select) — `SELECT n.*, u.username, u.avatar FROM collab_notes n JOIN users u ON n.user_id = u.id WHERE n.id = ?`, no trip filter (the caller already knows `id` is in-scope, having just written it). */ @@ -117,7 +126,16 @@ export class CollabNotesRepository extends TrekRepository { } /** CB8 (`createNote`) — `INSERT INTO collab_notes (trip_id, user_id, title, content, category, color, website, pinned) VALUES (?×8)`. The service resolves the `|| null`/`|| 'General'`/`|| '#6366f1'` defaults before calling (D2's split). Returns the new row's id. */ - async insertNote(row: { trip_id: number | string; user_id: number; title: string; content: string | null; category: string; color: string; website: string | null; pinned: number }): Promise { + async insertNote(row: { + trip_id: number | string; + user_id: number; + title: string; + content: string | null; + category: string; + color: string; + website: string | null; + pinned: number; + }): Promise { return await this.insert({ trip: row.trip_id, user: row.user_id, @@ -150,17 +168,30 @@ export class CollabNotesRepository extends TrekRepository { * `CASE` helper, and this class docstring records that choice for * whichever task reads it next. */ - async update(id: number, write: { title: string; content: string | null; category: string | null; color: string | null; pinned: number | null; website: string | null }): Promise { + async update( + id: number, + write: { + title: string; + content: string | null; + category: string | null; + color: string | null; + pinned: number | null; + website: string | null; + }, + ): Promise { const platform = this.getEntityManager().getPlatform(); - await this.nativeUpdate({ id }, { - title: write.title, - content: write.content, - category: write.category, - color: write.color, - pinned: write.pinned, - website: write.website, - updated_at: currentTimestamp(platform), - }); + await this.nativeUpdate( + { id }, + { + title: write.title, + content: write.content, + category: write.category, + color: write.color, + pinned: write.pinned, + website: write.website, + updated_at: currentTimestamp(platform), + }, + ); } /** CB16 (`deleteNote`, inside its transaction, after CB15's attachment sweep) — `DELETE FROM collab_notes WHERE id = ?`. */ @@ -191,7 +222,11 @@ export class CollabNotesRepository extends TrekRepository { /** CB14 (`deleteNote`'s pre-tx storage-cleanup list) — `SELECT id, filename FROM trip_files WHERE note_id = ?`, run BEFORE the transaction (the legacy ordering: the storage deletes happen outside/before the DB transaction). */ async listFilenamesForNote(note_id: number): Promise<{ id: number; filename: string }[]> { - return await this.kysely().selectFrom('trip_files').select(['id', 'filename']).where('note_id', '=', note_id).execute(); + return await this.kysely() + .selectFrom('trip_files') + .select(['id', 'filename']) + .where('note_id', '=', note_id) + .execute(); } /** CB15 (`deleteNote`, inside its transaction, before CB16) — `DELETE FROM trip_files WHERE note_id = ?`. */ @@ -206,10 +241,24 @@ export class CollabNotesRepository extends TrekRepository { * supported engine answers, unlike `InsertResult.insertId`) for CB19's * re-select. */ - async insertAttachmentForNote(row: { trip_id: number | string; note_id: number | string; filename: string; original_name: string; file_size: number; mime_type: string }): Promise { + async insertAttachmentForNote(row: { + trip_id: number | string; + note_id: number | string; + filename: string; + original_name: string; + file_size: number; + mime_type: string; + }): Promise { const inserted = await this.kysely() .insertInto('trip_files') - .values({ trip_id: row.trip_id as number, note_id: row.note_id as number, filename: row.filename, original_name: row.original_name, file_size: row.file_size, mime_type: row.mime_type }) + .values({ + trip_id: row.trip_id as number, + note_id: row.note_id as number, + filename: row.filename, + original_name: row.original_name, + file_size: row.file_size, + mime_type: row.mime_type, + }) .returning('id') .executeTakeFirstOrThrow(); return inserted.id; @@ -217,7 +266,11 @@ export class CollabNotesRepository extends TrekRepository { /** CB19 (`addNoteFile`'s re-select, immediately after {@link insertAttachmentForNote}) — `SELECT * FROM trip_files WHERE id = ?`, unscoped (the caller just wrote this exact id). */ async findAttachmentById(id: number): Promise { - return await this.kysely().selectFrom('trip_files').selectAll().where('id', '=', id).executeTakeFirst(); + return await this.kysely() + .selectFrom('trip_files') + .selectAll() + .where('id', '=', id) + .executeTakeFirst(); } /** diff --git a/server/src/db/repositories/CollabPolls.repository.ts b/server/src/db/repositories/CollabPolls.repository.ts index 2046e87b1..b1b23990a 100644 --- a/server/src/db/repositories/CollabPolls.repository.ts +++ b/server/src/db/repositories/CollabPolls.repository.ts @@ -1,7 +1,7 @@ import type { CollabPolls } from '../entities/CollabPolls.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A bare `collab_polls` row — every scalar column of the entity, incl. the two `persist(false)` relation mirrors (`trip_id`, `user_id`). */ export interface CollabPollRow { @@ -45,7 +45,12 @@ export class CollabPollsRepository extends TrekRepository { /** CB25 (`listPolls`) — `SELECT id FROM collab_polls WHERE trip_id = ? ORDER BY created_at DESC`. */ async listIdsForTrip(trip_id: number): Promise { - const rows = await this.kysely().selectFrom('collab_polls').select(['id']).where('trip_id', '=', trip_id).orderBy('created_at', 'desc').execute(); + const rows = await this.kysely() + .selectFrom('collab_polls') + .select(['id']) + .where('trip_id', '=', trip_id) + .orderBy('created_at', 'desc') + .execute(); return rows.map((r) => r.id); } @@ -55,12 +60,31 @@ export class CollabPollsRepository extends TrekRepository { * text at all three call sites. */ async findInTrip(id: number, trip_id: number): Promise { - return await this.kysely().selectFrom('collab_polls').selectAll().where('id', '=', id).where('trip_id', '=', trip_id).executeTakeFirst(); + return await this.kysely() + .selectFrom('collab_polls') + .selectAll() + .where('id', '=', id) + .where('trip_id', '=', trip_id) + .executeTakeFirst(); } /** CB26 (`createPoll`) — `INSERT INTO collab_polls (trip_id, user_id, question, options, multiple, deadline) VALUES (?×6)`. Returns the new row's id. */ - async insertPoll(row: { trip_id: number | string; user_id: number; question: string; options: string; multiple: number; deadline: string | null }): Promise { - return await this.insert({ trip: row.trip_id, user: row.user_id, question: row.question, options: row.options, multiple: row.multiple, deadline: row.deadline }); + async insertPoll(row: { + trip_id: number | string; + user_id: number; + question: string; + options: string; + multiple: number; + deadline: string | null; + }): Promise { + return await this.insert({ + trip: row.trip_id, + user: row.user_id, + question: row.question, + options: row.options, + multiple: row.multiple, + deadline: row.deadline, + }); } /** CB33 (`closePoll`) — `UPDATE collab_polls SET closed = 1 WHERE id = ?`. */ diff --git a/server/src/db/repositories/CollectionLabels.repository.ts b/server/src/db/repositories/CollectionLabels.repository.ts index 8a89ed5d5..a5e5158f1 100644 --- a/server/src/db/repositories/CollectionLabels.repository.ts +++ b/server/src/db/repositories/CollectionLabels.repository.ts @@ -1,7 +1,7 @@ -import type { CollectionLabels } from '../entities/CollectionLabels.entity'; import { lower, lowerParam } from '../dialect/sql-functions'; -import { TrekRepository } from './_shared/trek-repository'; +import type { CollectionLabels } from '../entities/CollectionLabels.entity'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** `collection_labels` — `loadLabelsByCollection`'s (CL7) shape. */ export interface CollectionLabelRow { @@ -61,7 +61,11 @@ export class CollectionLabelsRepository extends TrekRepository /** CL20 (`insertImportedLabel`) — `INSERT INTO collection_labels (collection_id, name, color, sort_order) VALUES (?,?,?,?)`. Returns the new row's id. Dup text also `createLabel` (Task 2's own call site). */ async insertLabel(row: { collection_id: number; name: string; color: string; sort_order: number }): Promise { - const inserted = await this.kysely().insertInto('collection_labels').values(row).returning('id').executeTakeFirstOrThrow(); + const inserted = await this.kysely() + .insertInto('collection_labels') + .values(row) + .returning('id') + .executeTakeFirstOrThrow(); return inserted.id; } @@ -78,7 +82,11 @@ export class CollectionLabelsRepository extends TrekRepository * applies here exactly as it does to a bare `.select([...])`. */ async collectionIdOf(labelId: number): Promise { - const row = await this.db_().selectFrom('collection_labels').select('collection_id').where('id', '=', labelId).executeTakeFirst(); + const row = await this.db_() + .selectFrom('collection_labels') + .select('collection_id') + .where('id', '=', labelId) + .executeTakeFirst(); return row?.collection_id; } @@ -122,7 +130,10 @@ export class CollectionLabelsRepository extends TrekRepository } /** CL76 (`updateLabel`) — the allow-listed dynamic `UPDATE collection_labels SET ... WHERE id=?`. No `updated_at` stamp — the table has no such column, and the legacy statement never wrote one. */ - async updateFields(labelId: number, write: Partial<{ name: string; color: string; sort_order: number }>): Promise { + async updateFields( + labelId: number, + write: Partial<{ name: string; color: string; sort_order: number }>, + ): Promise { if (Object.keys(write).length === 0) return; await this.nativeUpdate({ id: labelId }, write); } diff --git a/server/src/db/repositories/CollectionMembers.repository.ts b/server/src/db/repositories/CollectionMembers.repository.ts index 6de24c255..71d759668 100644 --- a/server/src/db/repositories/CollectionMembers.repository.ts +++ b/server/src/db/repositories/CollectionMembers.repository.ts @@ -1,6 +1,6 @@ import type { CollectionMembers } from '../entities/CollectionMembers.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** `buildMembers`'s owner row — `collections` joined to `users` (CL12). */ export interface CollectionOwnerMemberRow { @@ -108,7 +108,10 @@ export class CollectionMembersRepository extends TrekRepository { + async findByCollectionAndUser( + collectionId: number, + userId: number, + ): Promise<{ id: number; status: string } | undefined> { const row = await this.findOne({ collection: collectionId, user: userId }, { fields: ['id', 'status'] }); return row ?? undefined; } diff --git a/server/src/db/repositories/CollectionPlaceRatings.repository.ts b/server/src/db/repositories/CollectionPlaceRatings.repository.ts index 2850563a0..05c1ad973 100644 --- a/server/src/db/repositories/CollectionPlaceRatings.repository.ts +++ b/server/src/db/repositories/CollectionPlaceRatings.repository.ts @@ -1,6 +1,6 @@ import type { CollectionPlaceRatings } from '../entities/CollectionPlaceRatings.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** * `collection_place_ratings` (Plan 3h Task 2, part B — #1435's per-voter diff --git a/server/src/db/repositories/CollectionPlaces.repository.ts b/server/src/db/repositories/CollectionPlaces.repository.ts index e4380ef33..d9d8e977e 100644 --- a/server/src/db/repositories/CollectionPlaces.repository.ts +++ b/server/src/db/repositories/CollectionPlaces.repository.ts @@ -1,8 +1,8 @@ -import type { CollectionPlaces } from '../entities/CollectionPlaces.entity'; import { currentTimestamp } from '../dialect/sql-functions'; +import type { CollectionPlaces } from '../entities/CollectionPlaces.entity'; +import type { DB } from '../kysely/db'; import type { CollectionPlaceRow } from './Collections.repository'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** * `collection_places` (Plan 3h Task 2, part B — saved places CRUD, dedup, @@ -196,7 +196,10 @@ export class CollectionPlacesRepository extends TrekRepository /** CL50 (`updatePlace`'s move branch, TX, dup text also `setPlaceLabels`) — `DELETE FROM collection_place_labels WHERE collection_place_id=?`. Pivot table, Kysely. */ async deleteLabelAssignments(collectionPlaceId: number): Promise { - await this.db_().deleteFrom('collection_place_labels').where('collection_place_id', '=', collectionPlaceId).execute(); + await this.db_() + .deleteFrom('collection_place_labels') + .where('collection_place_id', '=', collectionPlaceId) + .execute(); } // --------------------------------------------------------------------- @@ -256,7 +259,14 @@ export class CollectionPlacesRepository extends TrekRepository async matchingByTripSource( collectionIds: number[], tripId: number, - place: { id: number; lat: number | null; lng: number | null; google_place_id: string | null; google_ftid: string | null; osm_id: string | null }, + place: { + id: number; + lat: number | null; + lng: number | null; + google_place_id: string | null; + google_ftid: string | null; + osm_id: string | null; + }, tolerance: number, ): Promise<{ id: number }[]> { if (collectionIds.length === 0) return []; @@ -309,8 +319,23 @@ export class CollectionPlacesRepository extends TrekRepository return await this.db_() .selectFrom('collection_places') .select([ - 'id', 'collection_id', 'name', 'description', 'lat', 'lng', 'address', 'category_id', 'price', 'currency', - 'notes', 'image_url', 'google_place_id', 'google_ftid', 'osm_id', 'website', 'phone', + 'id', + 'collection_id', + 'name', + 'description', + 'lat', + 'lng', + 'address', + 'category_id', + 'price', + 'currency', + 'notes', + 'image_url', + 'google_place_id', + 'google_ftid', + 'osm_id', + 'website', + 'phone', ]) .where('id', '=', id) .executeTakeFirst(); @@ -322,7 +347,11 @@ export class CollectionPlacesRepository extends TrekRepository /** CL67 (`copyToTrip`, loop, TX) — `SELECT tag_id FROM collection_place_tags WHERE collection_place_id=?`. Pivot table, Kysely. */ async tagIdsFor(collectionPlaceId: number): Promise { - const rows = await this.db_().selectFrom('collection_place_tags').select('tag_id').where('collection_place_id', '=', collectionPlaceId).execute(); + const rows = await this.db_() + .selectFrom('collection_place_tags') + .select('tag_id') + .where('collection_place_id', '=', collectionPlaceId) + .execute(); return rows.map((r) => r.tag_id); } @@ -433,7 +462,12 @@ export class CollectionPlacesRepository extends TrekRepository * `PlacesRepository.existsByImageUrl` for its own table. */ async existsByImageUrl(imageUrl: string): Promise { - const row = await this.db_().selectFrom('collection_places').select('id').where('image_url', '=', imageUrl).limit(1).executeTakeFirst(); + const row = await this.db_() + .selectFrom('collection_places') + .select('id') + .where('image_url', '=', imageUrl) + .limit(1) + .executeTakeFirst(); return !!row; } diff --git a/server/src/db/repositories/Collections.repository.ts b/server/src/db/repositories/Collections.repository.ts index 2384b7ab8..78c498a1e 100644 --- a/server/src/db/repositories/Collections.repository.ts +++ b/server/src/db/repositories/Collections.repository.ts @@ -1,7 +1,7 @@ import type { Collections } from '../entities/Collections.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; import { dbNow } from '../types'; +import { TrekRepository } from './_shared/trek-repository'; /** `collections` — every scalar column, `getCollectionRow`'s (CL14) `SELECT *` shape. */ export interface CollectionRow { @@ -145,7 +145,11 @@ export class CollectionsRepository extends TrekRepository { /** CL5 (`ownerOf`) — `SELECT owner_id FROM collections WHERE id=?`. */ async ownerId(id: number): Promise { - const row = await this.readDb().selectFrom('collections').select('owner_id').where('id', '=', id).executeTakeFirst(); + const row = await this.readDb() + .selectFrom('collections') + .select('owner_id') + .where('id', '=', id) + .executeTakeFirst(); return row?.owner_id; } @@ -189,7 +193,11 @@ export class CollectionsRepository extends TrekRepository { links: string | null; sort_order: number; }): Promise { - const inserted = await this.insertDb().insertInto('collections').values(row).returning('id').executeTakeFirstOrThrow(); + const inserted = await this.insertDb() + .insertInto('collections') + .values(row) + .returning('id') + .executeTakeFirstOrThrow(); return inserted.id; } @@ -215,18 +223,30 @@ export class CollectionsRepository extends TrekRepository { }>, ): Promise { if (Object.keys(write).length === 0) return; - await this.readDb().updateTable('collections').set({ ...write, updated_at: dbNow() }).where('id', '=', id).execute(); + await this.readDb() + .updateTable('collections') + .set({ ...write, updated_at: dbNow() }) + .where('id', '=', id) + .execute(); } /** CL31 (`setCollectionCover`) — `SELECT cover_image FROM collections WHERE id=?`. */ async coverImage(id: number): Promise { - const row = await this.readDb().selectFrom('collections').select('cover_image').where('id', '=', id).executeTakeFirst(); + const row = await this.readDb() + .selectFrom('collections') + .select('cover_image') + .where('id', '=', id) + .executeTakeFirst(); return row?.cover_image; } /** CL32 (`setCollectionCover`) — `UPDATE collections SET cover_image=?, updated_at= WHERE id=?`. */ async setCoverImage(id: number, coverUrl: string | null): Promise { - await this.readDb().updateTable('collections').set({ cover_image: coverUrl, updated_at: dbNow() }).where('id', '=', id).execute(); + await this.readDb() + .updateTable('collections') + .set({ cover_image: coverUrl, updated_at: dbNow() }) + .where('id', '=', id) + .execute(); } /** CL35 (`deleteCollection`) — `DELETE FROM collections WHERE id=?` (CASCADE drops members + places + tags). */ @@ -269,11 +289,35 @@ export class CollectionsRepository extends TrekRepository { .selectFrom('collection_places as cp') .leftJoin('categories as c', 'c.id', 'cp.category_id') .select([ - 'cp.id', 'cp.collection_id', 'cp.owner_id', 'cp.saved_by', 'cp.name', 'cp.description', 'cp.lat', 'cp.lng', - 'cp.address', 'cp.category_id', 'cp.price', 'cp.currency', 'cp.notes', 'cp.image_url', 'cp.google_place_id', - 'cp.google_ftid', 'cp.osm_id', 'cp.website', 'cp.phone', 'cp.status', 'cp.source_trip_id', 'cp.source_place_id', - 'cp.links', 'cp.sort_order', 'cp.created_at', 'cp.updated_at', - 'c.name as category_name', 'c.color as category_color', 'c.icon as category_icon', + 'cp.id', + 'cp.collection_id', + 'cp.owner_id', + 'cp.saved_by', + 'cp.name', + 'cp.description', + 'cp.lat', + 'cp.lng', + 'cp.address', + 'cp.category_id', + 'cp.price', + 'cp.currency', + 'cp.notes', + 'cp.image_url', + 'cp.google_place_id', + 'cp.google_ftid', + 'cp.osm_id', + 'cp.website', + 'cp.phone', + 'cp.status', + 'cp.source_trip_id', + 'cp.source_place_id', + 'cp.links', + 'cp.sort_order', + 'cp.created_at', + 'cp.updated_at', + 'c.name as category_name', + 'c.color as category_color', + 'c.icon as category_icon', ]) .where('cp.id', '=', id) .executeTakeFirst(); @@ -290,11 +334,35 @@ export class CollectionsRepository extends TrekRepository { .selectFrom('collection_places as cp') .leftJoin('categories as c', 'c.id', 'cp.category_id') .select([ - 'cp.id', 'cp.collection_id', 'cp.owner_id', 'cp.saved_by', 'cp.name', 'cp.description', 'cp.lat', 'cp.lng', - 'cp.address', 'cp.category_id', 'cp.price', 'cp.currency', 'cp.notes', 'cp.image_url', 'cp.google_place_id', - 'cp.google_ftid', 'cp.osm_id', 'cp.website', 'cp.phone', 'cp.status', 'cp.source_trip_id', 'cp.source_place_id', - 'cp.links', 'cp.sort_order', 'cp.created_at', 'cp.updated_at', - 'c.name as category_name', 'c.color as category_color', 'c.icon as category_icon', + 'cp.id', + 'cp.collection_id', + 'cp.owner_id', + 'cp.saved_by', + 'cp.name', + 'cp.description', + 'cp.lat', + 'cp.lng', + 'cp.address', + 'cp.category_id', + 'cp.price', + 'cp.currency', + 'cp.notes', + 'cp.image_url', + 'cp.google_place_id', + 'cp.google_ftid', + 'cp.osm_id', + 'cp.website', + 'cp.phone', + 'cp.status', + 'cp.source_trip_id', + 'cp.source_place_id', + 'cp.links', + 'cp.sort_order', + 'cp.created_at', + 'cp.updated_at', + 'c.name as category_name', + 'c.color as category_color', + 'c.icon as category_icon', ]) .where('cp.collection_id', '=', collectionId) .orderBy('cp.sort_order') @@ -304,7 +372,11 @@ export class CollectionsRepository extends TrekRepository { /** CL11 (`collectionIdOfPlace`) — `SELECT collection_id FROM collection_places WHERE id=?`. */ async collectionIdOfPlace(placeId: number): Promise { - const row = await this.placesReadDb().selectFrom('collection_places').select('collection_id').where('id', '=', placeId).executeTakeFirst(); + const row = await this.placesReadDb() + .selectFrom('collection_places') + .select('collection_id') + .where('id', '=', placeId) + .executeTakeFirst(); return row?.collection_id; } @@ -396,7 +468,11 @@ export class CollectionsRepository extends TrekRepository { links: string | null; sort_order: number; }): Promise { - const inserted = await this.placesWriteDb().insertInto('collection_places').values(row).returning('id').executeTakeFirstOrThrow(); + const inserted = await this.placesWriteDb() + .insertInto('collection_places') + .values(row) + .returning('id') + .executeTakeFirstOrThrow(); return inserted.id; } @@ -418,14 +494,23 @@ export class CollectionsRepository extends TrekRepository { .selectFrom('collection_places') .select(['id', 'name']) .where('collection_id', '=', collectionId) - .where((eb) => eb.or([eb('google_place_id', '=', externalId), eb('google_ftid', '=', externalId), eb('osm_id', '=', externalId)])) + .where((eb) => + eb.or([ + eb('google_place_id', '=', externalId), + eb('google_ftid', '=', externalId), + eb('osm_id', '=', externalId), + ]), + ) .orderBy('id', 'asc') .limit(1) .executeTakeFirst(); } /** CL26 (`findDuplicateCollectionPlace`, name branch) — `SELECT id,name FROM collection_places WHERE collection_id=? AND lower(trim(name))=? ORDER BY id ASC LIMIT 1`. `normalizedName` is already `lower(trim(...))`-shaped by the caller (`_shared/../dialect/sql-functions.ts`'s `lowerTrim`/`lowerTrimParam` precedent). */ - async findDuplicateByName(collectionId: number, normalizedName: string): Promise<{ id: number; name: string } | undefined> { + async findDuplicateByName( + collectionId: number, + normalizedName: string, + ): Promise<{ id: number; name: string } | undefined> { return await this.placesReadDb() .selectFrom('collection_places') .select(['id', 'name']) diff --git a/server/src/db/repositories/DawarichConnections.repository.ts b/server/src/db/repositories/DawarichConnections.repository.ts index 2dbb381c0..f0ede73e2 100644 --- a/server/src/db/repositories/DawarichConnections.repository.ts +++ b/server/src/db/repositories/DawarichConnections.repository.ts @@ -60,7 +60,17 @@ export class DawarichConnectionsRepository extends TrekRepository { const row = await this.qb('c') - .select(['c.user', 'c.url', 'c.api_key', 'c.allow_insecure_tls', 'c.sync_enabled', 'c.last_sync_at', 'c.last_sync_state', 'c.last_sync_error', 'c.capabilities']) + .select([ + 'c.user', + 'c.url', + 'c.api_key', + 'c.allow_insecure_tls', + 'c.sync_enabled', + 'c.last_sync_at', + 'c.last_sync_state', + 'c.last_sync_error', + 'c.capabilities', + ]) .where({ user: userId }) .execute('get', false); return row ?? null; @@ -109,7 +119,10 @@ export class DawarichConnectionsRepository extends TrekRepository { + async upsertConnection( + userId: number, + data: { url: string | null; allowInsecureTls: boolean; syncEnabled: boolean }, + ): Promise { const platform = this.getEntityManager().getPlatform(); await this.upsert( { @@ -119,7 +132,11 @@ export class DawarichConnectionsRepository extends TrekRepository { - await this.nativeUpdate({ user: userId }, { capabilities: null, last_sync_state: 'never', last_sync_error: null, last_sync_at: null }); + await this.nativeUpdate( + { user: userId }, + { capabilities: null, last_sync_state: 'never', last_sync_error: null, last_sync_at: null }, + ); } /** @@ -151,7 +171,10 @@ export class DawarichConnectionsRepository extends TrekRepository { - await this.nativeUpdate({ user: userId }, { api_key: null, capabilities: null, last_sync_state: 'never', last_sync_error: null, last_sync_at: null }); + await this.nativeUpdate( + { user: userId }, + { api_key: null, capabilities: null, last_sync_state: 'never', last_sync_error: null, last_sync_at: null }, + ); } /** @@ -168,7 +191,10 @@ export class DawarichConnectionsRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); - await this.nativeUpdate({ user: userId }, { capabilities: capabilitiesJson, updated_at: currentTimestamp(platform) }); + await this.nativeUpdate( + { user: userId }, + { capabilities: capabilitiesJson, updated_at: currentTimestamp(platform) }, + ); } /** @@ -177,11 +203,19 @@ export class DawarichConnectionsRepository extends TrekRepository { + async recordSyncResult( + userId: number, + data: { lastSyncAt: string; state: string; error: string | null }, + ): Promise { const platform = this.getEntityManager().getPlatform(); await this.nativeUpdate( { user: userId }, - { last_sync_at: data.lastSyncAt, last_sync_state: data.state, last_sync_error: data.error, updated_at: currentTimestamp(platform) }, + { + last_sync_at: data.lastSyncAt, + last_sync_state: data.state, + last_sync_error: data.error, + updated_at: currentTimestamp(platform), + }, ); } diff --git a/server/src/db/repositories/DawarichVisitSuggestions.repository.ts b/server/src/db/repositories/DawarichVisitSuggestions.repository.ts index b5ea286fa..6acfb7443 100644 --- a/server/src/db/repositories/DawarichVisitSuggestions.repository.ts +++ b/server/src/db/repositories/DawarichVisitSuggestions.repository.ts @@ -1,8 +1,8 @@ import { coalesceParam, columnRef } from '../dialect/sql-functions'; import type { DawarichVisitSuggestions } from '../entities/DawarichVisitSuggestions.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** `dawarich_visit_suggestions` — every scalar column, DWS6/DSY3's `SELECT *` shape. */ export interface SuggestionRow { diff --git a/server/src/db/repositories/DayAccommodations.repository.ts b/server/src/db/repositories/DayAccommodations.repository.ts index df8c38226..403d6cd1b 100644 --- a/server/src/db/repositories/DayAccommodations.repository.ts +++ b/server/src/db/repositories/DayAccommodations.repository.ts @@ -1,8 +1,8 @@ -import type { DayAccommodations } from '../entities/DayAccommodations.entity'; import { coalesceOverride, columnRef } from '../dialect/sql-functions'; +import type { DayAccommodations } from '../entities/DayAccommodations.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** * A `day_accommodations` row exactly as AC34 (`AccommodationsService @@ -103,7 +103,12 @@ export class DayAccommodationsRepository extends TrekRepository { + async patchTimes( + id: number, + check_in: string | null, + check_in_end: string | null, + check_out: string | null, + ): Promise { const platform = this.getEntityManager().getPlatform(); await this.qb() .update({ @@ -156,22 +161,28 @@ export class DayAccommodationsRepository extends TrekRepository { - await this.nativeUpdate({ id }, { - place: write.place_id, - startDay: write.start_day_id, - endDay: write.end_day_id, - check_in: write.check_in, - check_out: write.check_out, - confirmation: write.confirmation, - }); + async updateFromBooking( + id: number, + write: { + place_id: number | null; + start_day_id: number; + end_day_id: number; + check_in: string | null; + check_out: string | null; + confirmation: string | null; + }, + ): Promise { + await this.nativeUpdate( + { id }, + { + place: write.place_id, + startDay: write.start_day_id, + endDay: write.end_day_id, + check_in: write.check_in, + check_out: write.check_out, + confirmation: write.confirmation, + }, + ); } /** RS47 — `DELETE FROM day_accommodations WHERE id = ? AND trip_id = ?`, run AFTER `accommodations.dropStayStops` (ordering load-bearing, kept in the service). `id`/`trip_id: number` (typed filter, program rule 23; rule 21 — the SAME `toRowId`-parsed trip id `existsInTrip`'s gate read used). */ @@ -216,7 +227,14 @@ export class DayAccommodationsRepository extends TrekRepository('get', false); } @@ -307,26 +325,32 @@ export class DayAccommodationsRepository extends TrekRepository { - await this.nativeUpdate({ id }, { - place: write.place_id, - startDay: write.start_day_id, - endDay: write.end_day_id, - check_in: write.check_in, - check_in_end: write.check_in_end, - check_out: write.check_out, - confirmation: write.confirmation, - notes: write.notes, - }); + async updateStay( + id: number, + write: { + place_id: number | null; + start_day_id: number; + end_day_id: number; + check_in: string | null; + check_in_end: string | null; + check_out: string | null; + confirmation: string | null; + notes: string | null; + }, + ): Promise { + await this.nativeUpdate( + { id }, + { + place: write.place_id, + startDay: write.start_day_id, + endDay: write.end_day_id, + check_in: write.check_in, + check_in_end: write.check_in_end, + check_out: write.check_out, + confirmation: write.confirmation, + notes: write.notes, + }, + ); } /** @@ -364,10 +388,17 @@ export class DayAccommodationsRepository extends TrekRepository { + async listForResync( + trip_id: number, + ): Promise<{ id: number; start_day_id: number; end_day_id: number; check_in: string | null }[]> { const platform = this.getEntityManager().getPlatform(); return await this.qb('a') - .select(['a.id', columnRef(platform, 'a.start_day_id').as('start_day_id'), columnRef(platform, 'a.end_day_id').as('end_day_id'), 'a.check_in']) + .select([ + 'a.id', + columnRef(platform, 'a.start_day_id').as('start_day_id'), + columnRef(platform, 'a.end_day_id').as('end_day_id'), + 'a.check_in', + ]) .where({ trip: trip_id }) .execute<{ id: number; start_day_id: number; end_day_id: number; check_in: string | null }[]>('all', false); } @@ -419,10 +450,7 @@ export class DayAccommodationsRepository extends TrekRepository { - return await this.qb('a') - .select(['a.*']) - .where({ trip: trip_id }) - .execute('all', false); + return await this.qb('a').select(['a.*']).where({ trip: trip_id }).execute('all', false); } // --------------------------------------------------------------------------- diff --git a/server/src/db/repositories/DayNotes.repository.ts b/server/src/db/repositories/DayNotes.repository.ts index eeaa5560a..1019bdf88 100644 --- a/server/src/db/repositories/DayNotes.repository.ts +++ b/server/src/db/repositories/DayNotes.repository.ts @@ -1,7 +1,7 @@ import type { DayNotes } from '../entities/DayNotes.entity'; +import type { DB } from '../kysely/db'; import { toRow, type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; export interface DayNoteRow { id: number; @@ -87,10 +87,7 @@ export class DayNotesRepository extends TrekRepository { * relies on read order). */ async listByTrip(trip_id: number | string): Promise { - return await this.qb('n') - .select(['n.*']) - .where('n.trip_id = ?', [trip_id]) - .execute('all', false); + return await this.qb('n').select(['n.*']).where('n.trip_id = ?', [trip_id]).execute('all', false); } /** @@ -145,7 +142,11 @@ export class DayNotesRepository extends TrekRepository { * AND day_id = ? AND trip_id = ?`. Same raw-bind seam as * {@link listByDayAndTrip}. */ - async findByIdDayTrip(id: number | string, day_id: number | string, trip_id: number | string): Promise { + async findByIdDayTrip( + id: number | string, + day_id: number | string, + trip_id: number | string, + ): Promise { return await this.qb('n') .select(['n.*']) .where('n.id = ? AND n.day_id = ? AND n.trip_id = ?', [id, day_id, trip_id]) @@ -160,29 +161,32 @@ export class DayNotesRepository extends TrekRepository { * already resolves each one's `fields.x !== undefined ? fields.x : * current.x` fallback, same shape as `createNote`'s docstring). */ - async updateNote(id: number | string, fields: { - text: string; - time: string | null; - icon: string | null; - sort_order: number | null; - color: string | null; - }): Promise { + async updateNote( + id: number | string, + fields: { + text: string; + time: string | null; + icon: string | null; + sort_order: number | null; + color: string | null; + }, + ): Promise { await this.qb('n') - .update({ text: fields.text, time: fields.time, icon: fields.icon, sort_order: fields.sort_order, color: fields.color }) + .update({ + text: fields.text, + time: fields.time, + icon: fields.icon, + sort_order: fields.sort_order, + color: fields.color, + }) .where('n.id = ?', [id]) .execute('run'); - return await this.qb('n') - .select(['n.*']) - .where('n.id = ?', [id]) - .execute('get', false); + return await this.qb('n').select(['n.*']).where('n.id = ?', [id]).execute('get', false); } /** `day-notes.service.ts::remove` — `DELETE FROM day_notes WHERE id = ?`. */ async deleteById(id: number | string): Promise { - await this.qb('n') - .delete() - .where('n.id = ?', [id]) - .execute('run'); + await this.qb('n').delete().where('n.id = ?', [id]).execute('run'); } /** diff --git a/server/src/db/repositories/Days.repository.ts b/server/src/db/repositories/Days.repository.ts index b8c840dd4..1827ac614 100644 --- a/server/src/db/repositories/Days.repository.ts +++ b/server/src/db/repositories/Days.repository.ts @@ -1,8 +1,8 @@ -import type { Days } from '../entities/Days.entity'; import { columnRef } from '../dialect/sql-functions'; +import type { Days } from '../entities/Days.entity'; +import type { DB } from '../kysely/db'; import { toRow, type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A `days` row as the API emits it. */ export interface DayRow { @@ -317,10 +317,12 @@ export class DaysRepository extends TrekRepository { .selectFrom('days as d') .select(['d.id', 'd.day_number', 'd.date']) .select((eb) => - eb.or([ - eb.exists(eb.selectFrom('day_assignments as da').select('da.id').whereRef('da.day_id', '=', 'd.id')), - eb.exists(eb.selectFrom('day_notes as dn').select('dn.id').whereRef('dn.day_id', '=', 'd.id')), - ]).as('has_plan_items'), + eb + .or([ + eb.exists(eb.selectFrom('day_assignments as da').select('da.id').whereRef('da.day_id', '=', 'd.id')), + eb.exists(eb.selectFrom('day_notes as dn').select('dn.id').whereRef('dn.day_id', '=', 'd.id')), + ]) + .as('has_plan_items'), ) .where('d.trip_id', '=', trip_id) .orderBy('d.day_number', 'asc') diff --git a/server/src/db/repositories/DemoRepository.ts b/server/src/db/repositories/DemoRepository.ts index c36a7360e..de24a6046 100644 --- a/server/src/db/repositories/DemoRepository.ts +++ b/server/src/db/repositories/DemoRepository.ts @@ -1,16 +1,24 @@ -import type { EntityManager } from '@mikro-orm/core'; -import type { SqlEntityManager } from '@mikro-orm/sql'; -import { Users } from '../entities/Users.entity'; -import type { NewAdminUserRow } from './Users.repository'; +import { currentTimestampKysely } from '../dialect/kysely-functions'; import { AppSettings } from '../entities/AppSettings.entity'; import { Trips } from '../entities/Trips.entity'; -import { currentTimestampKysely } from '../dialect/kysely-functions'; +import { Users } from '../entities/Users.entity'; import type { DB } from '../kysely/db'; +import type { NewAdminUserRow } from './Users.repository'; +import type { EntityManager } from '@mikro-orm/core'; +import type { SqlEntityManager } from '@mikro-orm/sql'; /** The tables the example-trip seed writes through Kysely. */ type DemoSeedKyselyDB = Pick< DB, - 'trip_members' | 'trips' | 'days' | 'places' | 'day_assignments' | 'packing_items' | 'budget_items' | 'reservations' | 'day_notes' + | 'trip_members' + | 'trips' + | 'days' + | 'places' + | 'day_assignments' + | 'packing_items' + | 'budget_items' + | 'reservations' + | 'day_notes' >; /** DMR1's projection (`resetDemoUser`'s pre-close credential read). */ @@ -165,7 +173,14 @@ export class DemoRepository { } /** DMS7 — `INSERT INTO trips (user_id, title, description, start_date, end_date, currency) VALUES (?, ?, ?, ?, ?, ?)`. Returns the generated trip id. */ - async insertTrip(userId: number, title: string, description: string, startDate: string, endDate: string, currency: string): Promise { + async insertTrip( + userId: number, + title: string, + description: string, + startDate: string, + endDate: string, + currency: string, + ): Promise { const inserted = await this.kysely() .insertInto('trips') .values({ user_id: userId, title, description, start_date: startDate, end_date: endDate, currency }) @@ -186,11 +201,37 @@ export class DemoRepository { /** DMS9 — the 13-column `places` insert (see {@link NewDemoPlaceRow}). Returns the generated place id. */ async insertPlace(row: NewDemoPlaceRow): Promise { - const [trip_id, name, lat, lng, address, category_id, place_time, duration_minutes, notes, image_url, google_place_id, website, phone] = row; + const [ + trip_id, + name, + lat, + lng, + address, + category_id, + place_time, + duration_minutes, + notes, + image_url, + google_place_id, + website, + phone, + ] = row; const inserted = await this.kysely() .insertInto('places') .values({ - trip_id, name, lat, lng, address, category_id, place_time, duration_minutes, notes, image_url, google_place_id, website, phone, + trip_id, + name, + lat, + lng, + address, + category_id, + place_time, + duration_minutes, + notes, + image_url, + google_place_id, + website, + phone, }) .returning('id') .executeTakeFirstOrThrow(); @@ -199,20 +240,43 @@ export class DemoRepository { /** DMS10 — `INSERT INTO day_assignments (day_id, place_id, order_index) VALUES (?, ?, ?)`. */ async insertDayAssignment(dayId: number, placeId: number, orderIndex: number): Promise { - await this.kysely().insertInto('day_assignments').values({ day_id: dayId, place_id: placeId, order_index: orderIndex }).execute(); + await this.kysely() + .insertInto('day_assignments') + .values({ day_id: dayId, place_id: placeId, order_index: orderIndex }) + .execute(); } /** DMS11 — `INSERT INTO packing_items (trip_id, name, checked, category, sort_order, updated_at) VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)`. */ - async insertPackingItem(tripId: number, name: string, checked: number, category: string, sortOrder: number): Promise { + async insertPackingItem( + tripId: number, + name: string, + checked: number, + category: string, + sortOrder: number, + ): Promise { const platform = this.em.getPlatform(); await this.kysely() .insertInto('packing_items') - .values({ trip_id: tripId, name, checked, category, sort_order: sortOrder, updated_at: currentTimestampKysely(platform) }) + .values({ + trip_id: tripId, + name, + checked, + category, + sort_order: sortOrder, + updated_at: currentTimestampKysely(platform), + }) .execute(); } /** DMS12 — `INSERT INTO budget_items (trip_id, category, name, total_price, persons, note) VALUES (?, ?, ?, ?, ?, ?)`. */ - async insertBudgetItem(tripId: number, category: string, name: string, totalPrice: number, persons: number, note: string | null): Promise { + async insertBudgetItem( + tripId: number, + category: string, + name: string, + totalPrice: number, + persons: number, + note: string | null, + ): Promise { await this.kysely() .insertInto('budget_items') .values({ trip_id: tripId, category, name, total_price: totalPrice, persons, note }) @@ -238,13 +302,27 @@ export class DemoRepository { await this.kysely() .insertInto('reservations') .values({ - trip_id: tripId, day_id: dayId, title, reservation_time: reservationTime, confirmation_number: confirmationNumber, status, type, location, + trip_id: tripId, + day_id: dayId, + title, + reservation_time: reservationTime, + confirmation_number: confirmationNumber, + status, + type, + location, }) .execute(); } /** DMS14 — `INSERT INTO day_notes (day_id, trip_id, text, time, icon, sort_order) VALUES (?, ?, ?, ?, ?, ?)`. */ - async insertDayNote(dayId: number, tripId: number, text: string, time: string, icon: string, sortOrder: number): Promise { + async insertDayNote( + dayId: number, + tripId: number, + text: string, + time: string, + icon: string, + sortOrder: number, + ): Promise { await this.kysely() .insertInto('day_notes') .values({ day_id: dayId, trip_id: tripId, text, time, icon, sort_order: sortOrder }) @@ -265,7 +343,10 @@ export class DemoRepository { async getAdminCredentials(email: string): Promise { const row = await this.em .getRepository(Users) - .findOne({ email }, { fields: ['password_hash', 'maps_api_key', 'openweather_api_key', 'unsplash_api_key', 'avatar'] }); + .findOne( + { email }, + { fields: ['password_hash', 'maps_api_key', 'openweather_api_key', 'unsplash_api_key', 'avatar'] }, + ); return row ? { password_hash: row.password_hash, diff --git a/server/src/db/repositories/DocumentConnections.repository.ts b/server/src/db/repositories/DocumentConnections.repository.ts index dd8566b82..07a0302d4 100644 --- a/server/src/db/repositories/DocumentConnections.repository.ts +++ b/server/src/db/repositories/DocumentConnections.repository.ts @@ -1,8 +1,8 @@ import { currentTimestamp, coalesceOverride } from '../dialect/sql-functions'; import type { DocumentConnections } from '../entities/DocumentConnections.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** * A bare `document_connections` row. `secrets` is opaque encrypted TEXT here @@ -72,7 +72,13 @@ export class DocumentConnectionsRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); await this.nativeUpdate( @@ -156,15 +162,12 @@ export class DocumentConnectionsRepository extends TrekRepository - eb( - 'c.owner_user_id', - 'not in', - (qb) => - qb - .selectFrom('trip_members as tm') - .select('tm.user_id') - .whereRef('tm.trip_id', '=', 'c.trip_id') - .union(qb.selectFrom('trips as t').select('t.user_id').whereRef('t.id', '=', 'c.trip_id')), + eb('c.owner_user_id', 'not in', (qb) => + qb + .selectFrom('trip_members as tm') + .select('tm.user_id') + .whereRef('tm.trip_id', '=', 'c.trip_id') + .union(qb.selectFrom('trips as t').select('t.user_id').whereRef('t.id', '=', 'c.trip_id')), ), ) .execute(); diff --git a/server/src/db/repositories/DocumentProviders.repository.ts b/server/src/db/repositories/DocumentProviders.repository.ts index cfb419148..95aa389a6 100644 --- a/server/src/db/repositories/DocumentProviders.repository.ts +++ b/server/src/db/repositories/DocumentProviders.repository.ts @@ -67,7 +67,12 @@ export class DocumentProvidersRepository extends TrekRepository ({ id: r.id as string, name: r.name, description: r.description ?? null, icon: r.icon ?? null })); + return rows.map((r) => ({ + id: r.id as string, + name: r.name, + description: r.description ?? null, + icon: r.icon ?? null, + })); } // --------------------------------------------------------------------- diff --git a/server/src/db/repositories/DocumentSyncItems.repository.ts b/server/src/db/repositories/DocumentSyncItems.repository.ts index 703db5f45..f2122dfb4 100644 --- a/server/src/db/repositories/DocumentSyncItems.repository.ts +++ b/server/src/db/repositories/DocumentSyncItems.repository.ts @@ -1,10 +1,10 @@ -import type { EntityData } from '@mikro-orm/core'; -import { coalesceOverride, currentTimestamp, foundAgainState, nowPlusSeconds } from '../dialect/sql-functions'; import { currentTimestampKysely, nowPlusSecondsKysely } from '../dialect/kysely-functions'; +import { coalesceOverride, currentTimestamp, foundAgainState, nowPlusSeconds } from '../dialect/sql-functions'; import type { DocumentSyncItems } from '../entities/DocumentSyncItems.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; +import type { EntityData } from '@mikro-orm/core'; /** A bare `document_sync_items` row (every scalar column). */ export interface DocumentSyncItemRow { @@ -98,7 +98,10 @@ export class DocumentSyncItemsRepository extends TrekRepository { + async applyRelocate( + id: number, + data: { remote_id: string; remote_version: string; remote_size: number | null; remote_modified_at: string | null }, + ): Promise { const platform = this.getEntityManager().getPlatform(); await this.nativeUpdate( { id }, @@ -152,7 +155,14 @@ export class DocumentSyncItemsRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); - await this.nativeUpdate({ id }, { state: 'remote_missing', remote_missing_at: currentTimestamp(platform), last_seen_at: currentTimestamp(platform) }); + await this.nativeUpdate( + { id }, + { + state: 'remote_missing', + remote_missing_at: currentTimestamp(platform), + last_seen_at: currentTimestamp(platform), + }, + ); } /** @@ -201,7 +211,14 @@ export class DocumentSyncItemsRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); await this.nativeUpdate( @@ -235,13 +258,21 @@ export class DocumentSyncItemsRepository extends TrekRepository { + async findPairing( + id: number, + ): Promise<{ file_id: number | null; remote_name: string | null; content_sha256: string | null } | undefined> { const row = await this.kysely() .selectFrom('document_sync_items') .select(['file_id', 'remote_name', 'content_sha256']) .where('id', '=', id) .executeTakeFirst(); - return row ? { file_id: row.file_id ?? null, remote_name: row.remote_name ?? null, content_sha256: row.content_sha256 ?? null } : undefined; + return row + ? { + file_id: row.file_id ?? null, + remote_name: row.remote_name ?? null, + content_sha256: row.content_sha256 ?? null, + } + : undefined; } /** @@ -261,8 +292,21 @@ export class DocumentSyncItemsRepository extends TrekRepository() .selectFrom('document_sync_items') .select([ - 'id', 'file_id', 'trek_doc_uid', 'remote_id', 'remote_version', 'remote_name', 'remote_size', 'remote_modified_at', - 'content_sha256', 'pushed_sha256', 'state', 'attempts', 'next_attempt_at', 'remote_missing_at', 'remote_trashed_at', + 'id', + 'file_id', + 'trek_doc_uid', + 'remote_id', + 'remote_version', + 'remote_name', + 'remote_size', + 'remote_modified_at', + 'content_sha256', + 'pushed_sha256', + 'state', + 'attempts', + 'next_attempt_at', + 'remote_missing_at', + 'remote_trashed_at', ]) .where('link_id', '=', linkId) .execute(); @@ -309,7 +353,11 @@ export class DocumentSyncItemsRepository extends TrekRepository { - const row = await this.kysely().selectFrom('document_sync_items').select('attempts').where('id', '=', id).executeTakeFirst(); + const row = await this.kysely() + .selectFrom('document_sync_items') + .select('attempts') + .where('id', '=', id) + .executeTakeFirst(); return row?.attempts; } @@ -374,7 +422,8 @@ export class DocumentSyncItemsRepository extends TrekRepository eb.ref('excluded.state'), error_code: (eb) => eb.ref('excluded.error_code'), file_id: (eb) => eb.fn.coalesce(eb.ref('excluded.file_id'), eb.ref('document_sync_items.file_id')), - remote_version: (eb) => eb.fn.coalesce(eb.ref('excluded.remote_version'), eb.ref('document_sync_items.remote_version')), - content_sha256: (eb) => eb.fn.coalesce(eb.ref('excluded.content_sha256'), eb.ref('document_sync_items.content_sha256')), - pushed_sha256: (eb) => eb.fn.coalesce(eb.ref('excluded.pushed_sha256'), eb.ref('document_sync_items.pushed_sha256')), + remote_version: (eb) => + eb.fn.coalesce(eb.ref('excluded.remote_version'), eb.ref('document_sync_items.remote_version')), + content_sha256: (eb) => + eb.fn.coalesce(eb.ref('excluded.content_sha256'), eb.ref('document_sync_items.content_sha256')), + pushed_sha256: (eb) => + eb.fn.coalesce(eb.ref('excluded.pushed_sha256'), eb.ref('document_sync_items.pushed_sha256')), last_seen_at: () => currentTimestampKysely(platform), }), ) @@ -470,7 +524,10 @@ export class DocumentSyncItemsRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); - await this.nativeUpdate({ id }, { remote_name: data.remote_name, remote_version: data.remote_version, last_seen_at: currentTimestamp(platform) }); + await this.nativeUpdate( + { id }, + { remote_name: data.remote_name, remote_version: data.remote_version, last_seen_at: currentTimestamp(platform) }, + ); } /** DS28 (`resolveConflict`'s pre-image read) — `SELECT * FROM document_sync_items WHERE id=?`. */ @@ -511,7 +568,14 @@ export class DocumentSyncItemsRepository extends TrekRepository() .selectFrom('document_sync_items as i') .leftJoin('trip_files as f', 'f.id', 'i.file_id') - .select(['i.id', 'i.state', 'i.error_code', 'i.remote_name', 'i.remote_missing_at', 'f.original_name as file_name']) + .select([ + 'i.id', + 'i.state', + 'i.error_code', + 'i.remote_name', + 'i.remote_missing_at', + 'f.original_name as file_name', + ]) .where('i.trip_id', '=', tripId) .where('i.state', 'in', [...ISSUE_STATES]) .orderBy('i.id', 'desc') @@ -548,7 +612,9 @@ export class DocumentSyncItemsRepository extends TrekRepository( eb .case() - .when(eb.and([eb('file_id', 'is not', null), eb('remote_id', 'is not', null), eb('state', '=', 'synced')])) + .when( + eb.and([eb('file_id', 'is not', null), eb('remote_id', 'is not', null), eb('state', '=', 'synced')]), + ) .then(1) .else(0) .end(), @@ -571,7 +637,16 @@ export class DocumentSyncItemsRepository extends TrekRepository(eb.case().when(eb('state', '=', 'remote_missing')).then(1).else(0).end()).as('missing'), + eb.fn + .sum( + eb + .case() + .when(eb('state', '=', 'remote_missing')) + .then(1) + .else(0) + .end(), + ) + .as('missing'), ]) .where('trip_id', '=', tripId) .groupBy('link_id') @@ -590,7 +665,12 @@ export class DocumentSyncItemsRepository extends TrekRepository { - return await this.itemsWithFileQuery().where('i.trip_id', '=', tripId).where('i.state', '=', state).orderBy('i.id', 'desc').limit(500).execute(); + return await this.itemsWithFileQuery() + .where('i.trip_id', '=', tripId) + .where('i.state', '=', state) + .orderBy('i.id', 'desc') + .limit(500) + .execute(); } /** DSCTRL5 (`DocSyncController.items`, unfiltered) — same as {@link listForTripByState} minus the state predicate. */ diff --git a/server/src/db/repositories/FileLinks.repository.ts b/server/src/db/repositories/FileLinks.repository.ts index 93fe5f5dd..2a2c99ee5 100644 --- a/server/src/db/repositories/FileLinks.repository.ts +++ b/server/src/db/repositories/FileLinks.repository.ts @@ -1,7 +1,7 @@ import type { FileLinks } from '../entities/FileLinks.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A bare `file_links` row — every scalar column of the entity, incl. its five `persist(false)` relation mirrors. */ export interface FileLinkRow { @@ -77,7 +77,13 @@ export class FileLinksRepository extends TrekRepository { * legacy sites (FL10/FL13 pass only `budget_item_id`; FL24 passes any * combination the caller supplies) — one method, not three. */ - async insertIgnore(row: { file_id: number; reservation_id?: number | null; assignment_id?: number | null; place_id?: number | null; budget_item_id?: number | null }): Promise { + async insertIgnore(row: { + file_id: number; + reservation_id?: number | null; + assignment_id?: number | null; + place_id?: number | null; + budget_item_id?: number | null; + }): Promise { await this.kysely() .insertInto('file_links') .values({ @@ -111,7 +117,11 @@ export class FileLinksRepository extends TrekRepository { /** FL25 (`createFileLink`'s bare re-select) — `SELECT * FROM file_links WHERE file_id = ?`. */ async listForFile(file_id: number): Promise { - return await this.kysely().selectFrom('file_links').selectAll().where('file_id', '=', file_id).execute(); + return await this.kysely() + .selectFrom('file_links') + .selectAll() + .where('file_id', '=', file_id) + .execute(); } /** FL27 (`FilesService.getFileLinks`) — `SELECT fl.*, r.title as reservation_title FROM file_links fl LEFT JOIN reservations r ON fl.reservation_id = r.id WHERE fl.file_id = ?`. */ diff --git a/server/src/db/repositories/GoogleApiUsage.repository.ts b/server/src/db/repositories/GoogleApiUsage.repository.ts index fe96ddf49..47932a7bc 100644 --- a/server/src/db/repositories/GoogleApiUsage.repository.ts +++ b/server/src/db/repositories/GoogleApiUsage.repository.ts @@ -1,8 +1,8 @@ -import type { GoogleApiUsage } from '../entities/GoogleApiUsage.entity'; import { nowDateOffset } from '../dialect/sql-functions'; +import type { GoogleApiUsage } from '../entities/GoogleApiUsage.entity'; +import type { DB } from '../kysely/db'; import type { AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A `google_api_usage` row: Google API calls counted per UTC day (#1582). */ export interface GoogleApiUsageRow { diff --git a/server/src/db/repositories/HiddenCountries.repository.ts b/server/src/db/repositories/HiddenCountries.repository.ts index 97e35cfc3..4d8777f76 100644 --- a/server/src/db/repositories/HiddenCountries.repository.ts +++ b/server/src/db/repositories/HiddenCountries.repository.ts @@ -1,6 +1,6 @@ import type { HiddenCountries } from '../entities/HiddenCountries.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; type HiddenCountriesKyselyDB = Pick; @@ -17,7 +17,11 @@ export class HiddenCountriesRepository extends TrekRepository { /** AT11 (`getHiddenCountries`) — `SELECT country_code FROM hidden_countries WHERE user_id = ?`. */ async listForUser(userId: number): Promise { - const rows = await this.db().selectFrom('hidden_countries').select('country_code').where('user_id', '=', userId).execute(); + const rows = await this.db() + .selectFrom('hidden_countries') + .select('country_code') + .where('user_id', '=', userId) + .execute(); return rows.map((r) => r.country_code); } @@ -32,6 +36,10 @@ export class HiddenCountriesRepository extends TrekRepository { /** AT13/AT20 (`markCountry`'s and `markRegion`'s "lift a prior removal" delete — identical text, two call sites) — `DELETE FROM hidden_countries WHERE user_id = ? AND country_code = ?`. */ async unhide(userId: number, countryCode: string): Promise { - await this.db().deleteFrom('hidden_countries').where('user_id', '=', userId).where('country_code', '=', countryCode).execute(); + await this.db() + .deleteFrom('hidden_countries') + .where('user_id', '=', userId) + .where('country_code', '=', countryCode) + .execute(); } } diff --git a/server/src/db/repositories/HiddenRegions.repository.ts b/server/src/db/repositories/HiddenRegions.repository.ts index 4c0535779..332a365b0 100644 --- a/server/src/db/repositories/HiddenRegions.repository.ts +++ b/server/src/db/repositories/HiddenRegions.repository.ts @@ -1,6 +1,6 @@ import type { HiddenRegions } from '../entities/HiddenRegions.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; type HiddenRegionsKyselyDB = Pick; @@ -18,7 +18,11 @@ export class HiddenRegionsRepository extends TrekRepository { /** AT22 (`getHiddenRegions`) — `SELECT region_code FROM hidden_regions WHERE user_id = ?`. */ async listForUser(userId: number): Promise { - const rows = await this.db().selectFrom('hidden_regions').select('region_code').where('user_id', '=', userId).execute(); + const rows = await this.db() + .selectFrom('hidden_regions') + .select('region_code') + .where('user_id', '=', userId) + .execute(); return rows.map((r) => r.region_code); } @@ -33,6 +37,10 @@ export class HiddenRegionsRepository extends TrekRepository { /** AT18 (`markRegion`, inside its transaction) — `DELETE FROM hidden_regions WHERE user_id = ? AND region_code = ?`. */ async unhide(userId: number, regionCode: string): Promise { - await this.db().deleteFrom('hidden_regions').where('user_id', '=', userId).where('region_code', '=', regionCode).execute(); + await this.db() + .deleteFrom('hidden_regions') + .where('user_id', '=', userId) + .where('region_code', '=', regionCode) + .execute(); } } diff --git a/server/src/db/repositories/IdempotencyKeys.repository.ts b/server/src/db/repositories/IdempotencyKeys.repository.ts index adf402b48..85e9205dd 100644 --- a/server/src/db/repositories/IdempotencyKeys.repository.ts +++ b/server/src/db/repositories/IdempotencyKeys.repository.ts @@ -19,11 +19,13 @@ export class IdempotencyKeysRepository extends TrekRepository { * response_body FROM idempotency_keys WHERE key = ? AND user_id = ? AND * method = ? AND path = ?`. */ - async findResponse(key: string, userId: number, method: string, path: string): Promise { - const row = await this.findOne( - { key, user: userId, method, path }, - { fields: ['status_code', 'response_body'] }, - ); + async findResponse( + key: string, + userId: number, + method: string, + path: string, + ): Promise { + const row = await this.findOne({ key, user: userId, method, path }, { fields: ['status_code', 'response_body'] }); return row ? { status_code: row.status_code, response_body: row.response_body } : null; } diff --git a/server/src/db/repositories/InviteTokens.repository.ts b/server/src/db/repositories/InviteTokens.repository.ts index bb104fa3d..4e0030dcb 100644 --- a/server/src/db/repositories/InviteTokens.repository.ts +++ b/server/src/db/repositories/InviteTokens.repository.ts @@ -1,6 +1,6 @@ +import { columnIncrementedBy, columnRef } from '../dialect/sql-functions'; import type { InviteTokens } from '../entities/InviteTokens.entity'; import { toRow, type AssertRowKeys } from './_shared/rows'; -import { columnIncrementedBy, columnRef } from '../dialect/sql-functions'; import { TrekRepository } from './_shared/trek-repository'; /** An `invite_tokens` row as the API emits it. */ diff --git a/server/src/db/repositories/JourneyBooks.repository.ts b/server/src/db/repositories/JourneyBooks.repository.ts index e7f3489aa..5b590359f 100644 --- a/server/src/db/repositories/JourneyBooks.repository.ts +++ b/server/src/db/repositories/JourneyBooks.repository.ts @@ -84,7 +84,11 @@ export class JourneyBooksRepository extends TrekRepository { * means a conflict — the service re-reads via {@link findById} for the * `{conflict}` response, it does not throw. */ - async casUpdate(id: number, baseVersion: number, data: { title: string; document: string; updatedBy: number }): Promise { + async casUpdate( + id: number, + baseVersion: number, + data: { title: string; document: string; updatedBy: number }, + ): Promise { const platform = this.getEntityManager().getPlatform(); return await this.nativeUpdate( { id, version: baseVersion }, diff --git a/server/src/db/repositories/JourneyContributors.repository.ts b/server/src/db/repositories/JourneyContributors.repository.ts index f3fb859ad..6f2ef1cf8 100644 --- a/server/src/db/repositories/JourneyContributors.repository.ts +++ b/server/src/db/repositories/JourneyContributors.repository.ts @@ -1,6 +1,6 @@ import type { JourneyContributors } from '../entities/JourneyContributors.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** JG18's contributor-list row (`getJourneyFull`) — the junction plus the joined username/avatar. */ export interface JourneyContributorListRow { diff --git a/server/src/db/repositories/JourneyEntries.repository.ts b/server/src/db/repositories/JourneyEntries.repository.ts index ed2ab956a..5c745aa47 100644 --- a/server/src/db/repositories/JourneyEntries.repository.ts +++ b/server/src/db/repositories/JourneyEntries.repository.ts @@ -1,7 +1,7 @@ import type { JourneyEntry } from '../../types'; import type { JourneyEntries } from '../entities/JourneyEntries.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** JG55's wider reconciliation projection (`reconcileTripSkeletons`) — a distinct column set from JG35's dedup-key-only read. */ export interface ReconcileEntryRow { @@ -42,7 +42,10 @@ export interface PhotoPlacementRow { } /** The `trek_photos`/`journey_photos`/`journey_entry_photos`/`journey_entries`/`journeys` tables {@link JourneyEntriesRepository.listPhotoPlacementCandidates} reads. */ -type PhotoPlacementKyselyDB = Pick; +type PhotoPlacementKyselyDB = Pick< + DB, + 'trek_photos' | 'journey_photos' | 'journey_entry_photos' | 'journey_entries' | 'journeys' +>; /** `journey_entry_photos`, read cross-table (no `JourneyEntryPhotosRepository` yet — Task 2's own; the two `hasPhotos` guards in Part A only ever check existence). */ type JourneyEntryPhotosExistsKyselyDB = Pick; @@ -200,7 +203,10 @@ export class JourneyEntriesRepository extends TrekRepository { * "fixed" into a redundant one. */ async detachAllFilledForTrip(tripId: number): Promise { - await this.nativeUpdate({ sourceTrip: tripId }, { sourcePlace: null, sourceTrip: null, source_assignment_id: null }); + await this.nativeUpdate( + { sourceTrip: tripId }, + { sourcePlace: null, sourceTrip: null, source_assignment_id: null }, + ); } /** @@ -214,7 +220,10 @@ export class JourneyEntriesRepository extends TrekRepository { } /** JG35 — `syncTripPlaces`'s existing-skeleton dedup-key read: `SELECT source_place_id, source_assignment_id FROM journey_entries WHERE journey_id = ? AND source_trip_id = ?`. */ - async listSourceKeysForTrip(journeyId: number, tripId: number): Promise<{ source_place_id: number; source_assignment_id: number | null }[]> { + async listSourceKeysForTrip( + journeyId: number, + tripId: number, + ): Promise<{ source_place_id: number; source_assignment_id: number | null }[]> { return await this.qb('je') .select(['je.sourcePlace', 'je.source_assignment_id']) .where({ journey: journeyId, sourceTrip: tripId }) @@ -323,7 +332,13 @@ export class JourneyEntriesRepository extends TrekRepository { /** JG47/JG59 — the location-only silent update (`onPlaceUpdated`'s filled-entry branch, `reconcileTripSkeletons`'s stale-filled-entry branch), one statement text. */ async updateLocationOnly( id: number, - patch: { location_name: string; location_lat: number | null; location_lng: number | null; country_code: string | null; updated_at: number }, + patch: { + location_name: string; + location_lat: number | null; + location_lng: number | null; + country_code: string | null; + updated_at: number; + }, ): Promise { await this.nativeUpdate({ id }, patch); } @@ -390,7 +405,17 @@ export class JourneyEntriesRepository extends TrekRepository { */ async listStatsRows(journeyId: number): Promise { 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']) + .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('all', false); @@ -431,7 +456,15 @@ export class JourneyEntriesRepository extends TrekRepository { .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('d.date').as('day'), eb.fn.min('da.order_index').as('ord')]) + .select((eb) => [ + 'p.id', + 'p.name', + 'p.lat', + 'p.lng', + 'p.trip_id as tripId', + eb.fn.min('d.date').as('day'), + eb.fn.min('da.order_index').as('ord'), + ]) .where('jt.journey_id', '=', journeyId) .groupBy('p.id') .orderBy((eb) => eb(eb.fn.min('d.date'), 'is', null), 'asc') @@ -466,7 +499,9 @@ export class JourneyEntriesRepository extends TrekRepository { * (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 }[]> { + 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) { // M4 (task-5-review.md) — `chunk` can never be empty here: the loop @@ -597,7 +632,18 @@ export class JourneyEntriesRepository extends TrekRepository { /** 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 { - await this.nativeUpdate({ id }, { type: 'skeleton', story: null, mood: null, weather: null, pros_cons: null, visibility: 'private', updated_at: updatedAt }); + 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). */ diff --git a/server/src/db/repositories/JourneyEntryPhotos.repository.ts b/server/src/db/repositories/JourneyEntryPhotos.repository.ts index c45ac11e5..fa7112aee 100644 --- a/server/src/db/repositories/JourneyEntryPhotos.repository.ts +++ b/server/src/db/repositories/JourneyEntryPhotos.repository.ts @@ -1,17 +1,33 @@ import type { JourneyPhoto } from '../../types'; import type { JourneyEntryPhotos } from '../entities/JourneyEntryPhotos.entity'; +import type { DB } from '../kysely/db'; import type { JourneyPublicGalleryRow } from './JourneyShareTokens.repository'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** The `journey_entry_photos`/`journey_photos`/`trek_photos`/`journey_entries` tables the `JP_SELECT`/`JP_JOIN`-shaped reads read. */ type JourneyPhotoJoinKyselyDB = Pick; /** `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', + '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; /** diff --git a/server/src/db/repositories/JourneyPhotos.repository.ts b/server/src/db/repositories/JourneyPhotos.repository.ts index baa31456b..d113e3c50 100644 --- a/server/src/db/repositories/JourneyPhotos.repository.ts +++ b/server/src/db/repositories/JourneyPhotos.repository.ts @@ -1,11 +1,12 @@ -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/kysely-functions'; +import type { JourneyPhotos } from '../entities/JourneyPhotos.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; +import type { Platform } from '@mikro-orm/core'; + +import type { ExpressionBuilder } from 'kysely'; /** `SELECT * FROM journey_photos WHERE id = ?`'s row (JG105) — every scalar column of the entity. */ export interface JourneyPhotoFullRow { @@ -24,7 +25,10 @@ export interface JourneyPhotoFullRow { const _journeyPhotoFullRowKeys: AssertRowKeys = true; /** The `journey_photos`/`trek_photos`/`journey_entry_photos`/`journey_entries` tables the gallery reads (JG19/JG101/JG102, `galleryChronologicalOrderExpr`) read. */ -type GalleryKyselyDB = Pick; +type GalleryKyselyDB = Pick< + DB, + 'journey_photos' | 'trek_photos' | 'journey_entry_photos' | 'journey_entries' | 'journeys' +>; /** JS7 (Plan 4 Task 8b relocation) — the public photo-validation join's row (`journey_photos` + `trek_photos`). */ export interface JourneyPublicPhotoValidationRow { @@ -41,9 +45,25 @@ export interface JourneyPublicAssetValidationRow { /** `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', + '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; /** @@ -63,7 +83,10 @@ const GALLERY_COLUMNS = [ */ function galleryChronologicalOrderExpr( platform: Platform, - eb: ExpressionBuilder, + eb: ExpressionBuilder< + GalleryKyselyDB & { gp: GalleryKyselyDB['journey_photos']; tp: GalleryKyselyDB['trek_photos'] }, + 'gp' | 'tp' + >, ) { return eb.fn.coalesce( eb.fn('nullif', [eb.ref('tp.taken_at'), eb.val('')]), @@ -78,7 +101,12 @@ function galleryChronologicalOrderExpr( eb2, { column: 'je.entry_date' }, { value: 'T' }, - { expression: eb2.fn.coalesce(eb2.fn('nullif', [eb2.ref('je.entry_time'), eb2.val('')]), eb2.val('00:00')) }, + { + expression: eb2.fn.coalesce( + eb2.fn('nullif', [eb2.ref('je.entry_time'), eb2.val('')]), + eb2.val('00:00'), + ), + }, ), ) .as('min_dt'), @@ -147,7 +175,10 @@ export class JourneyPhotosRepository extends TrekRepository { * already converted onto `TripPhotosRepository.existsForPhoto`). */ async existsForPhoto(photoId: number): Promise { - const row = await this.qb('gp').select(['gp.id']).where({ photo: photoId }).execute<{ id: number } | undefined>('get', false); + const row = await this.qb('gp') + .select(['gp.id']) + .where({ photo: photoId }) + .execute<{ id: number } | undefined>('get', false); return !!row; } @@ -205,7 +236,10 @@ export class JourneyPhotosRepository extends TrekRepository { * fallback stub from 3g Task 3 (this repository was still mid-flight when * that task landed) — relocated here now that it is stable. */ - async findGalleryPhotoForValidation(photoId: number, journeyId: number): Promise { + async findGalleryPhotoForValidation( + photoId: number, + journeyId: number, + ): Promise { return await this.kysely() .selectFrom('journey_photos as gp') .innerJoin('trek_photos as tkp', 'tkp.id', 'gp.photo_id') @@ -227,7 +261,10 @@ export class JourneyPhotosRepository extends TrekRepository { * resolves `ownerId`. Relocated the same way as {@link * findGalleryPhotoForValidation}/JS7. */ - async findAssetForValidation(assetId: string, journeyId: number): Promise { + async findAssetForValidation( + assetId: string, + journeyId: number, + ): Promise { return await this.kysely() .selectFrom('journey_photos as gp') .innerJoin('trek_photos as tkp', 'tkp.id', 'gp.photo_id') @@ -323,10 +360,7 @@ export class JourneyPhotosRepository extends TrekRepository { /** JG105 — `deleteGalleryPhoto`'s pre-delete read: `SELECT * FROM journey_photos WHERE id = ?`. */ async findFull(id: number): Promise { - return await this.qb('gp') - .select(['gp.*']) - .where({ id }) - .execute('get', false); + return await this.qb('gp').select(['gp.*']).where({ id }).execute('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. */ diff --git a/server/src/db/repositories/JourneyShareTokens.repository.ts b/server/src/db/repositories/JourneyShareTokens.repository.ts index 16304958a..42d3559ae 100644 --- a/server/src/db/repositories/JourneyShareTokens.repository.ts +++ b/server/src/db/repositories/JourneyShareTokens.repository.ts @@ -114,7 +114,10 @@ export class JourneyShareTokensRepository extends TrekRepository { - return await this.qb('jst').select(['jst.*']).where({ journey: journeyId }).execute('get', false); + return await this.qb('jst') + .select(['jst.*']) + .where({ journey: journeyId }) + .execute('get', false); } /** JS5 — `deleteJourneyShareLink`: `DELETE FROM journey_share_tokens WHERE journey_id = ?`. */ @@ -146,7 +149,10 @@ export class JourneyShareTokensRepository extends TrekRepository { - return await this.qb('jst').select(['jst.*']).where({ token }).execute('get', false); + return await this.qb('jst') + .select(['jst.*']) + .where({ token }) + .execute('get', false); } // JS7/JS10 — `validateShareTokenForPhoto`'s/`validateShareTokenForAsset`'s diff --git a/server/src/db/repositories/JourneyTrips.repository.ts b/server/src/db/repositories/JourneyTrips.repository.ts index 5240d3940..d8d377767 100644 --- a/server/src/db/repositories/JourneyTrips.repository.ts +++ b/server/src/db/repositories/JourneyTrips.repository.ts @@ -1,6 +1,6 @@ import type { JourneyTrips } from '../entities/JourneyTrips.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** JG17's trip-link row (`getJourneyFull`) — the junction plus the joined trip summary and its place count. */ export interface JourneyTripLinkRow { @@ -126,7 +126,17 @@ export class JourneyTripsRepository extends TrekRepository { .selectFrom('places as p') .innerJoin('day_assignments as da', 'da.place_id', 'p.id') .innerJoin('days as d', 'd.id', 'da.day_id') - .select(['p.id', 'p.name', 'p.address', 'p.lat', 'p.lng', 'p.place_time', 'da.id as assignment_id', 'd.date as day_date', 'da.assignment_time']) + .select([ + 'p.id', + 'p.name', + 'p.address', + 'p.lat', + 'p.lng', + 'p.place_time', + 'da.id as assignment_id', + 'd.date as day_date', + 'da.assignment_time', + ]) .where('p.trip_id', '=', tripId) .orderBy('d.day_number', 'asc') .orderBy('da.order_index', 'asc') @@ -140,7 +150,17 @@ export class JourneyTripsRepository extends TrekRepository { .selectFrom('places as p') .innerJoin('day_assignments as da', 'da.place_id', 'p.id') .innerJoin('days as d', 'd.id', 'da.day_id') - .select(['p.id', 'p.name', 'p.address', 'p.lat', 'p.lng', 'p.place_time', 'da.id as assignment_id', 'd.date as day_date', 'da.assignment_time']) + .select([ + 'p.id', + 'p.name', + 'p.address', + 'p.lat', + 'p.lng', + 'p.place_time', + 'da.id as assignment_id', + 'd.date as day_date', + 'da.assignment_time', + ]) .where('p.id', '=', placeId) .orderBy('d.day_number', 'asc') .orderBy('da.order_index', 'asc') diff --git a/server/src/db/repositories/Journeys.repository.ts b/server/src/db/repositories/Journeys.repository.ts index 7edf725cb..c1f504928 100644 --- a/server/src/db/repositories/Journeys.repository.ts +++ b/server/src/db/repositories/Journeys.repository.ts @@ -1,6 +1,6 @@ import type { Journeys } from '../entities/Journeys.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** * `SELECT * FROM journeys WHERE id = ?`'s row — every scalar column the @@ -41,7 +41,10 @@ export interface JourneyListRow extends JourneyRow { } /** The `journeys`/`journey_contributors`/`journey_entries`/`journey_photos`/`journey_trips`/`trips` tables {@link JourneysRepository.listForUser} reads. */ -type JourneyListKyselyDB = Pick; +type JourneyListKyselyDB = Pick< + DB, + 'journeys' | 'journey_contributors' | 'journey_entries' | 'journey_photos' | 'journey_trips' | 'trips' +>; /** The `journeys`/`journey_contributors` tables {@link JourneysRepository.listRecipientUserIds} reads. */ type JourneyRecipientsKyselyDB = Pick; @@ -130,7 +133,9 @@ export class JourneysRepository extends TrekRepository { async listForUser(user_id: number): Promise { const rows = await this.kysely() .selectFrom('journeys as j') - .leftJoin('journey_contributors as jc', (join) => join.onRef('jc.journey_id', '=', 'j.id').on('jc.user_id', '=', user_id)) + .leftJoin('journey_contributors as jc', (join) => + join.onRef('jc.journey_id', '=', 'j.id').on('jc.user_id', '=', user_id), + ) .selectAll('j') .select((eb) => [ eb @@ -171,7 +176,13 @@ export class JourneysRepository extends TrekRepository { } /** JG9 — `createJourney`'s INSERT: `(user_id, title, subtitle, status, created_at, updated_at)`, `status` hard-coded `'active'` like the legacy statement. */ - async insertJourney(data: { user_id: number; title: string; subtitle: string | null; created_at: number; updated_at: number }): Promise { + async insertJourney(data: { + user_id: number; + title: string; + subtitle: string | null; + created_at: number; + updated_at: number; + }): Promise { return await this.insert({ user: data.user_id, title: data.title, diff --git a/server/src/db/repositories/MaintenanceRepository.ts b/server/src/db/repositories/MaintenanceRepository.ts index 16a56d3a2..93a82b412 100644 --- a/server/src/db/repositories/MaintenanceRepository.ts +++ b/server/src/db/repositories/MaintenanceRepository.ts @@ -105,7 +105,9 @@ export class MaintenanceRepository { for (const table of ['plugin_user_config', 'plugin_oauth_tokens', 'plugin_oauth_state']) { try { await this.em.getConnection().execute(`DELETE FROM ${table} WHERE user_id = ?`, [userId], 'run', ctx); - } catch { /* table absent (slim schema) */ } + } catch { + /* table absent (slim schema) */ + } } } } diff --git a/server/src/db/repositories/McpTokens.repository.ts b/server/src/db/repositories/McpTokens.repository.ts index a23a0c227..a3a211406 100644 --- a/server/src/db/repositories/McpTokens.repository.ts +++ b/server/src/db/repositories/McpTokens.repository.ts @@ -1,6 +1,6 @@ +import { columnRef, currentTimestamp } from '../dialect/sql-functions'; import { McpTokens } from '../entities/McpTokens.entity'; import type { AssertRowKeys } from './_shared/rows'; -import { columnRef, currentTimestamp } from '../dialect/sql-functions'; import { TrekRepository } from './_shared/trek-repository'; /** @@ -312,7 +312,15 @@ export class McpTokensRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); const rows = await this.qb('t') .innerJoin('t.user', 'u') - .select(['t.id', 't.name', 't.token_prefix', 't.created_at', 't.last_used_at', columnRef(platform, 't.user_id'), 'u.username']) + .select([ + 't.id', + 't.name', + 't.token_prefix', + 't.created_at', + 't.last_used_at', + columnRef(platform, 't.user_id'), + 'u.username', + ]) .orderBy({ 't.created_at': 'desc' }) .execute('all', false); return rows.map((row) => ({ diff --git a/server/src/db/repositories/Notifications.repository.ts b/server/src/db/repositories/Notifications.repository.ts index 6ebfceae2..fe376d134 100644 --- a/server/src/db/repositories/Notifications.repository.ts +++ b/server/src/db/repositories/Notifications.repository.ts @@ -1,7 +1,7 @@ import type { Notifications } from '../entities/Notifications.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** * A bare `notifications` row — every scalar column of the entity, incl. the @@ -178,7 +178,11 @@ export class NotificationsRepository extends TrekRepository { /** NT7/NT9/NT19 — `SELECT * FROM notifications WHERE id = ?`, full row incl. the persist(false) mirrors. */ async findById(id: number): Promise { - return await this.kysely().selectFrom('notifications').selectAll().where('id', '=', id).executeTakeFirst(); + return await this.kysely() + .selectFrom('notifications') + .selectAll() + .where('id', '=', id) + .executeTakeFirst(); } /** NT19 — `respond`'s recipient-scoped initial read: `SELECT * FROM notifications WHERE id = ? AND recipient_id = ?`. */ @@ -192,7 +196,12 @@ export class NotificationsRepository extends TrekRepository { } /** NT10 — `listInApp`'s joined page read, `unreadOnly` toggling the extra `is_read = 0` predicate (the legacy statement's two dynamic-WHERE variants). */ - async listForRecipient(recipientId: number, limit: number, offset: number, unreadOnly: boolean): Promise { + async listForRecipient( + recipientId: number, + limit: number, + offset: number, + unreadOnly: boolean, + ): Promise { let q = this.joinedQuery().where('n.recipient_id', '=', recipientId); if (unreadOnly) q = q.where('n.is_read', '=', 0); return await q.orderBy('n.created_at', 'desc').limit(limit).offset(offset).execute(); diff --git a/server/src/db/repositories/OauthClients.repository.ts b/server/src/db/repositories/OauthClients.repository.ts index a90e39ed6..7d565d849 100644 --- a/server/src/db/repositories/OauthClients.repository.ts +++ b/server/src/db/repositories/OauthClients.repository.ts @@ -15,10 +15,10 @@ export interface OauthClientRow { name: string; client_id: string; client_secret_hash: string; - redirect_uris: string; // JSON array + redirect_uris: string; // JSON array allowed_scopes: string; // JSON array created_at: string | null; - is_public: number; // 0 | 1 + is_public: number; // 0 | 1 created_via: string; allows_client_credentials: number; // 0 | 1 } @@ -129,7 +129,18 @@ export class OauthClientsRepository extends TrekRepository { const rows = await this.find( { user: userId }, { - fields: ['id', 'user', 'name', 'client_id', 'redirect_uris', 'allowed_scopes', 'created_at', 'is_public', 'created_via', 'allows_client_credentials'], + fields: [ + 'id', + 'user', + 'name', + 'client_id', + 'redirect_uris', + 'allowed_scopes', + 'created_at', + 'is_public', + 'created_via', + 'allows_client_credentials', + ], orderBy: { created_at: 'desc' }, }, ); @@ -213,7 +224,18 @@ export class OauthClientsRepository extends TrekRepository { const row = await this.findOne( { id }, { - fields: ['id', 'user', 'name', 'client_id', 'redirect_uris', 'allowed_scopes', 'created_at', 'is_public', 'created_via', 'allows_client_credentials'], + fields: [ + 'id', + 'user', + 'name', + 'client_id', + 'redirect_uris', + 'allowed_scopes', + 'created_at', + 'is_public', + 'created_via', + 'allows_client_credentials', + ], }, ); return row @@ -247,10 +269,7 @@ export class OauthClientsRepository extends TrekRepository { * unscoped read. */ async findOwned(id: string, userId: number): Promise { - const row = await this.findOne( - { id, user: userId }, - { fields: ['id', 'client_id', 'is_public'] }, - ); + const row = await this.findOne({ id, user: userId }, { fields: ['id', 'client_id', 'is_public'] }); return row ? { id: row.id as string, client_id: row.client_id, is_public: row.is_public } : null; } @@ -288,7 +307,14 @@ export class OauthClientsRepository extends TrekRepository { { fields: ['client_id', 'name', 'redirect_uris', 'allowed_scopes', 'is_public', 'created_via'] }, ); return row - ? { client_id: row.client_id, name: row.name, redirect_uris: row.redirect_uris, allowed_scopes: row.allowed_scopes, is_public: row.is_public, created_via: row.created_via } + ? { + client_id: row.client_id, + name: row.name, + redirect_uris: row.redirect_uris, + allowed_scopes: row.allowed_scopes, + is_public: row.is_public, + created_via: row.created_via, + } : null; } @@ -309,7 +335,9 @@ export class OauthClientsRepository extends TrekRepository { { client_id: clientId }, { fields: ['client_id', 'client_secret_hash', 'is_public'] }, ); - return row ? { client_id: row.client_id, client_secret_hash: row.client_secret_hash, is_public: row.is_public } : null; + return row + ? { client_id: row.client_id, client_secret_hash: row.client_secret_hash, is_public: row.is_public } + : null; } // --------------------------------------------------------------------- diff --git a/server/src/db/repositories/OauthConsents.repository.ts b/server/src/db/repositories/OauthConsents.repository.ts index 9746eb5b0..fbc908c57 100644 --- a/server/src/db/repositories/OauthConsents.repository.ts +++ b/server/src/db/repositories/OauthConsents.repository.ts @@ -1,5 +1,5 @@ -import { OauthConsents } from '../entities/OauthConsents.entity'; import { currentTimestamp } from '../dialect/sql-functions'; +import { OauthConsents } from '../entities/OauthConsents.entity'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; @@ -52,10 +52,7 @@ export class OauthConsentsRepository extends TrekRepository { * method (same rule `upsertGrant` below follows for `#upsert`). */ async findScopes(clientId: string, userId: number): Promise<{ scopes: string } | null> { - const row = await this.findOne( - { client: clientId, user: userId }, - { fields: ['scopes'] }, - ); + const row = await this.findOne({ client: clientId, user: userId }, { fields: ['scopes'] }); return row ? { scopes: row.scopes } : null; } diff --git a/server/src/db/repositories/OauthTokens.repository.ts b/server/src/db/repositories/OauthTokens.repository.ts index 77103f93d..43a297b66 100644 --- a/server/src/db/repositories/OauthTokens.repository.ts +++ b/server/src/db/repositories/OauthTokens.repository.ts @@ -1,9 +1,9 @@ -import { OauthTokens } from '../entities/OauthTokens.entity'; -import { type AssertRowKeys } from './_shared/rows'; import { columnRef, currentTimestamp } from '../dialect/sql-functions'; -import { TrekRepository } from './_shared/trek-repository'; +import { OauthTokens } from '../entities/OauthTokens.entity'; import type { DB } from '../kysely/db'; import type { DbTimestamp } from '../types'; +import { type AssertRowKeys } from './_shared/rows'; +import { TrekRepository } from './_shared/trek-repository'; /** * The Kysely-side table shape `em.getKysely()` needs for `collectChainIds`'s @@ -259,9 +259,15 @@ export class OauthTokensRepository extends TrekRepository { const row = await this.qb('ot') .innerJoin('ot.user', 'u') .select([ - 'ot.scopes', 'ot.audience', 'ot.revoked_at', 'ot.access_token_expires_at', - columnRef(platform, 'ot.user_id'), columnRef(platform, 'ot.client_id'), - 'u.username', 'u.email', 'u.role', + 'ot.scopes', + 'ot.audience', + 'ot.revoked_at', + 'ot.access_token_expires_at', + columnRef(platform, 'ot.user_id'), + columnRef(platform, 'ot.client_id'), + 'u.username', + 'u.email', + 'u.role', ]) .where({ 'ot.access_token_hash': hash }) .execute('get', false); @@ -327,11 +333,13 @@ export class OauthTokensRepository extends TrekRepository { async collectChainIds(rootId: number): Promise { const rows = await this.kysely() .withRecursive('chain', (db) => - db.selectFrom('oauth_tokens') + db + .selectFrom('oauth_tokens') .select('id') .where('id', '=', rootId) .unionAll((eb) => - eb.selectFrom('oauth_tokens as ot2') + eb + .selectFrom('oauth_tokens as ot2') .innerJoin('chain as c', 'c.id', 'ot2.parent_token_id') .select('ot2.id'), ), @@ -364,7 +372,10 @@ export class OauthTokensRepository extends TrekRepository { .deleteFrom('oauth_tokens') .where('refresh_token_expires_at', '<', cutoff) .where('id', 'not in', (eb) => - eb.selectFrom('oauth_tokens as child').select('child.parent_token_id').where('child.parent_token_id', 'is not', null), + eb + .selectFrom('oauth_tokens as child') + .select('child.parent_token_id') + .where('child.parent_token_id', 'is not', null), ) .executeTakeFirst(); const deleted = Number(result.numDeletedRows ?? 0); @@ -433,7 +444,9 @@ export class OauthTokensRepository extends TrekRepository { async findByRefreshTokenHash(hash: string): Promise { const row = await this.findOne( { refresh_token_hash: hash }, - { fields: ['id', 'client', 'user', 'scopes', 'audience', 'refresh_token_expires_at', 'revoked_at', 'parentToken'] }, + { + fields: ['id', 'client', 'user', 'scopes', 'audience', 'refresh_token_expires_at', 'revoked_at', 'parentToken'], + }, ); return row ? { @@ -548,7 +561,11 @@ export class OauthTokensRepository extends TrekRepository { 'ot.refresh_token_expires_at', 'ot.created_at', ]) - .where({ 'ot.user_id': userId, 'ot.revoked_at': null, 'ot.refresh_token_expires_at': { $gt: currentTimestamp(platform) } }) + .where({ + 'ot.user_id': userId, + 'ot.revoked_at': null, + 'ot.refresh_token_expires_at': { $gt: currentTimestamp(platform) }, + }) .orderBy({ 'ot.created_at': 'desc' }) .execute('all', false); return rows.map((row) => ({ diff --git a/server/src/db/repositories/PackingBags.repository.ts b/server/src/db/repositories/PackingBags.repository.ts index f5d2844d4..1665f8a45 100644 --- a/server/src/db/repositories/PackingBags.repository.ts +++ b/server/src/db/repositories/PackingBags.repository.ts @@ -1,8 +1,8 @@ import type { PackingBags } from '../entities/PackingBags.entity'; +import type { DB } from '../kysely/db'; +import { presenceSet } from './_shared/presence-set'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import { presenceSet } from './_shared/presence-set'; -import type { DB } from '../kysely/db'; /** A bare `packing_bags` row — every scalar column, incl. the two `persist(false)` relation mirrors (`trip_id`, `user_id`). */ export interface PackingBagRow { @@ -80,23 +80,41 @@ export class PackingBagsRepository extends TrekRepository { * still-accepted carry. */ async findInTrip(id: number, trip_id: number | string): Promise { - return await this.db().selectFrom('packing_bags').selectAll().where('id', '=', id).where('trip_id', '=', trip_id as number).executeTakeFirst(); + return await this.db() + .selectFrom('packing_bags') + .selectAll() + .where('id', '=', id) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); } /** PK30 (`bulkImport`'s bag-by-name resolution) — `SELECT id FROM packing_bags WHERE trip_id = ? AND name = ?`. */ async byNameInTrip(trip_id: number | string, name: string): Promise<{ id: number } | undefined> { - return await this.db().selectFrom('packing_bags').select('id').where('trip_id', '=', trip_id as number).where('name', '=', name).executeTakeFirst(); + return await this.db() + .selectFrom('packing_bags') + .select('id') + .where('trip_id', '=', trip_id as number) + .where('name', '=', name) + .executeTakeFirst(); } /** PK31 (`bulkImport`, picks the next `BAG_COLORS` entry) — `SELECT COUNT(*) as c FROM packing_bags WHERE trip_id = ?`. */ async countForTrip(trip_id: number | string): Promise { - const row = await this.db().selectFrom('packing_bags').select((eb) => eb.fn.countAll().as('c')).where('trip_id', '=', trip_id as number).executeTakeFirst(); + const row = await this.db() + .selectFrom('packing_bags') + .select((eb) => eb.fn.countAll().as('c')) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); return row?.c ?? 0; } /** PK41 (`createBag`) — `SELECT MAX(sort_order) as max FROM packing_bags WHERE trip_id = ?`. */ async maxSortOrder(trip_id: number | string): Promise { - const row = await this.db().selectFrom('packing_bags').select((eb) => eb.fn.max('sort_order').as('max')).where('trip_id', '=', trip_id as number).executeTakeFirst(); + const row = await this.db() + .selectFrom('packing_bags') + .select((eb) => eb.fn.max('sort_order').as('max')) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); return row?.max ?? null; } @@ -112,7 +130,11 @@ export class PackingBagsRepository extends TrekRepository { * the new row's id. */ async insertMinimal(trip_id: number | string, name: string, color: string): Promise { - const inserted = await this.kysely().insertInto('packing_bags').values({ trip_id: trip_id as number, name, color }).returning('id').executeTakeFirstOrThrow(); + const inserted = await this.kysely() + .insertInto('packing_bags') + .values({ trip_id: trip_id as number, name, color }) + .returning('id') + .executeTakeFirstOrThrow(); return inserted.id; } @@ -121,14 +143,28 @@ export class PackingBagsRepository extends TrekRepository { * sort_order, weight_limit_grams) VALUES (?×5)`, a distinct, wider column * set from {@link insertMinimal}'s. Returns the new row's id. */ - async insertBag(row: { trip_id: number | string; name: string; color: string; sort_order: number | null; weight_limit_grams: number | null }): Promise { - const inserted = await this.kysely().insertInto('packing_bags').values({ ...row, trip_id: row.trip_id as number }).returning('id').executeTakeFirstOrThrow(); + async insertBag(row: { + trip_id: number | string; + name: string; + color: string; + sort_order: number | null; + weight_limit_grams: number | null; + }): Promise { + const inserted = await this.kysely() + .insertInto('packing_bags') + .values({ ...row, trip_id: row.trip_id as number }) + .returning('id') + .executeTakeFirstOrThrow(); return inserted.id; } /** PK43 (`createBag`'s post-insert re-select) — `SELECT * FROM packing_bags WHERE id = ?`. */ async findById(id: number | string): Promise { - return await this.db().selectFrom('packing_bags').selectAll().where('id', '=', id as number).executeTakeFirst(); + return await this.db() + .selectFrom('packing_bags') + .selectAll() + .where('id', '=', id as number) + .executeTakeFirst(); } /** @@ -148,13 +184,21 @@ export class PackingBagsRepository extends TrekRepository { * (`:222`) and `TodoItems` (`:125`) already carry. */ /** `id: number`, same Plan 4 Task 8b (U6) narrowing as {@link findInTrip} (its one caller, `updateBag`, is only reached with a `toRowId`-parsed/Zod-typed id). */ - async update(id: number, write: { - name?: readonly [present: boolean, value: string | null]; - color?: readonly [present: boolean, value: string | null]; - weight_limit_grams?: readonly [present: boolean, value: number | null]; - user_id?: readonly [present: boolean, value: number | null]; - }): Promise { - const data = presenceSet<{ name: string; color: string; weight_limit_grams: number | null; user_id: number | null }>(write); + async update( + id: number, + write: { + name?: readonly [present: boolean, value: string | null]; + color?: readonly [present: boolean, value: string | null]; + weight_limit_grams?: readonly [present: boolean, value: number | null]; + user_id?: readonly [present: boolean, value: number | null]; + }, + ): Promise { + const data = presenceSet<{ + name: string; + color: string; + weight_limit_grams: number | null; + user_id: number | null; + }>(write); if (Object.keys(data).length === 0) return; await this.db().updateTable('packing_bags').set(data).where('id', '=', id).execute(); } @@ -177,7 +221,13 @@ export class PackingBagsRepository extends TrekRepository { /** PK35 (`decorateBags`) — `SELECT * FROM packing_bags WHERE trip_id = ? ORDER BY sort_order, id`. */ async listForTrip(trip_id: number | string): Promise { - return await this.db().selectFrom('packing_bags').selectAll().where('trip_id', '=', trip_id as number).orderBy('sort_order', 'asc').orderBy('id', 'asc').execute(); + return await this.db() + .selectFrom('packing_bags') + .selectAll() + .where('trip_id', '=', trip_id as number) + .orderBy('sort_order', 'asc') + .orderBy('id', 'asc') + .execute(); } // --------------------------------------------------------------------------- @@ -194,14 +244,23 @@ export class PackingBagsRepository extends TrekRepository { .selectFrom('packing_bag_members as bm') .innerJoin('users as u', 'u.id', 'bm.user_id') .innerJoin('packing_bags as b', 'b.id', 'bm.bag_id') - .select(['bm.bag_id', 'bm.user_id', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), 'u.avatar']) + .select([ + 'bm.bag_id', + 'bm.user_id', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + 'u.avatar', + ]) .where('b.trip_id', '=', trip_id as number) .execute(); } /** PK26 (`bagForCloner`) — `SELECT user_id FROM packing_bag_members WHERE bag_id = ?`. */ async listMemberIdsForBag(bag_id: number): Promise { - const rows = await this.db().selectFrom('packing_bag_members').select('user_id').where('bag_id', '=', bag_id).execute(); + const rows = await this.db() + .selectFrom('packing_bag_members') + .select('user_id') + .where('bag_id', '=', bag_id) + .execute(); return rows.map((r) => r.user_id); } @@ -252,6 +311,10 @@ export class PackingBagsRepository extends TrekRepository { * directly — same column set, called once per row. */ async listAllForTrip(trip_id: number | string): Promise { - return await this.db().selectFrom('packing_bags').selectAll().where('trip_id', '=', trip_id as number).execute(); + return await this.db() + .selectFrom('packing_bags') + .selectAll() + .where('trip_id', '=', trip_id as number) + .execute(); } } diff --git a/server/src/db/repositories/PackingCategoryAssignees.repository.ts b/server/src/db/repositories/PackingCategoryAssignees.repository.ts index 5ae1b28a5..e3020a701 100644 --- a/server/src/db/repositories/PackingCategoryAssignees.repository.ts +++ b/server/src/db/repositories/PackingCategoryAssignees.repository.ts @@ -1,6 +1,6 @@ import type { PackingCategoryAssignees } from '../entities/PackingCategoryAssignees.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** PK58/PK61's joined projection. */ export interface PackingCategoryAssigneeRow { @@ -46,7 +46,12 @@ export class PackingCategoryAssigneesRepository extends TrekRepository() .selectFrom('packing_category_assignees as pca') .innerJoin('users as u', 'u.id', 'pca.user_id') - .select(['pca.category_name', 'pca.user_id', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), 'u.avatar']) + .select([ + 'pca.category_name', + 'pca.user_id', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + 'u.avatar', + ]) .where('pca.trip_id', '=', trip_id as number) .execute(); } @@ -78,7 +83,10 @@ export class PackingCategoryAssigneesRepository extends TrekRepository[]> { + async listForCategory( + trip_id: number | string, + category_name: string, + ): Promise[]> { return await this.kysely() .selectFrom('packing_category_assignees as pca') .innerJoin('users as u', 'u.id', 'pca.user_id') diff --git a/server/src/db/repositories/PackingItemContributors.repository.ts b/server/src/db/repositories/PackingItemContributors.repository.ts index ced603af2..ae47d011e 100644 --- a/server/src/db/repositories/PackingItemContributors.repository.ts +++ b/server/src/db/repositories/PackingItemContributors.repository.ts @@ -1,6 +1,6 @@ import type { PackingItemContributors } from '../entities/PackingItemContributors.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** PK3's contributor projection (`enrichItems`). */ export interface PackingItemContributorRow { @@ -42,7 +42,12 @@ export class PackingItemContributorsRepository extends TrekRepository() .selectFrom('packing_item_contributors as c') .innerJoin('users as u', 'u.id', 'c.user_id') - .select(['c.item_id', 'c.user_id', 'c.status', (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username')]) + .select([ + 'c.item_id', + 'c.user_id', + 'c.status', + (eb) => eb.fn.coalesce('u.display_name', 'u.username').as('username'), + ]) .where('c.item_id', 'in', item_ids) .execute(); } diff --git a/server/src/db/repositories/PackingItems.repository.ts b/server/src/db/repositories/PackingItems.repository.ts index e8ffd73d9..be9d799f7 100644 --- a/server/src/db/repositories/PackingItems.repository.ts +++ b/server/src/db/repositories/PackingItems.repository.ts @@ -1,10 +1,10 @@ -import type { PackingItems } from '../entities/PackingItems.entity'; import { currentTimestamp } from '../dialect/sql-functions'; +import type { PackingItems } from '../entities/PackingItems.entity'; +import type { DB } from '../kysely/db'; +import { packingVisibleToActorExpr, type PackingVisibilityKyselyDB } from './_shared/packing-visibility'; +import { presenceSet } from './_shared/presence-set'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import { presenceSet } from './_shared/presence-set'; -import { packingVisibleToActorExpr, type PackingVisibilityKyselyDB } from './_shared/packing-visibility'; -import type { DB } from '../kysely/db'; /** A bare `packing_items` row — every scalar column, incl. the three `persist(false)` relation mirrors (`trip_id`, `bag_id`, `owner_id`) — the program-wide trap this class goes through Kysely throughout to avoid. */ export interface PackingItemRow { @@ -49,7 +49,8 @@ export interface PackingItemExportRow { * `CalendarStayKyselyDB`/`ReservationVisibilityKyselyDB` precedent, * `Reservations.repository.ts`). */ -type PackingItemsKyselyDB = PackingVisibilityKyselyDB & Pick; +type PackingItemsKyselyDB = PackingVisibilityKyselyDB & + Pick; /** * `packing_items` — the checklist rows themselves, plus (this class's own @@ -114,7 +115,13 @@ export class PackingItemsRepository extends TrekRepository { /** PK4 (`listItems`'s no-viewer branch) — `SELECT * FROM packing_items WHERE trip_id = ? ORDER BY sort_order ASC, created_at ASC`, unfiltered — only for genuinely viewer-less internal reads. */ async listForTrip(trip_id: number | string): Promise { - return await this.db().selectFrom('packing_items').selectAll().where('trip_id', '=', trip_id as number).orderBy('sort_order', 'asc').orderBy('created_at', 'asc').execute(); + return await this.db() + .selectFrom('packing_items') + .selectAll() + .where('trip_id', '=', trip_id as number) + .orderBy('sort_order', 'asc') + .orderBy('created_at', 'asc') + .execute(); } /** @@ -156,13 +163,25 @@ export class PackingItemsRepository extends TrekRepository { } /** PK6 (`getItemPrivacy`) — `SELECT is_private, owner_id FROM packing_items WHERE id = ? AND trip_id = ?`. `id: number` (Plan 4 Task 8b, U6 — the program's gate-level id parsing carry: its one caller, `PackingService.getItemPrivacy`, is only reached with a `toRowId`-parsed id); `trip_id` stays `number | string`, a separate, still-accepted carry. */ - async getPrivacy(id: number, trip_id: number | string): Promise<{ is_private: number; owner_id: number | null } | undefined> { - return await this.db().selectFrom('packing_items').select(['is_private', 'owner_id']).where('id', '=', id).where('trip_id', '=', trip_id as number).executeTakeFirst(); + async getPrivacy( + id: number, + trip_id: number | string, + ): Promise<{ is_private: number; owner_id: number | null } | undefined> { + return await this.db() + .selectFrom('packing_items') + .select(['is_private', 'owner_id']) + .where('id', '=', id) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); } /** PK7/PK28/PK51 (`createItem`/`bulkImport`/`applyTemplate`) — `SELECT MAX(sort_order) as max FROM packing_items WHERE trip_id = ?`, one method, three call sites. */ async maxSortOrder(trip_id: number | string): Promise { - const row = await this.db().selectFrom('packing_items').select((eb) => eb.fn.max('sort_order').as('max')).where('trip_id', '=', trip_id as number).executeTakeFirst(); + const row = await this.db() + .selectFrom('packing_items') + .select((eb) => eb.fn.max('sort_order').as('max')) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); return row?.max ?? null; } @@ -190,8 +209,16 @@ export class PackingItemsRepository extends TrekRepository { * `day`/`place` precedent). */ async insertItem(row: { - trip_id: number | string; name: string; checked: number; category: string; sort_order: number; - quantity: number; weight_grams: number | null; bag_id: number | null; is_private: number; owner_id: number | null; + trip_id: number | string; + name: string; + checked: number; + category: string; + sort_order: number; + quantity: number; + weight_grams: number | null; + bag_id: number | null; + is_private: number; + owner_id: number | null; }): Promise { const platform = this.getEntityManager().getPlatform(); const id = await this.insert({ @@ -219,8 +246,15 @@ export class PackingItemsRepository extends TrekRepository { * same reason as {@link insertItem}'s docstring. Returns the new row's id. */ async insertFromTemplate(row: { - trip_id: number | string; name: string; category: string; sort_order: number; is_private: number; owner_id: number | null; - weight_grams: number | null; quantity: number; bag_id: number | null; + trip_id: number | string; + name: string; + category: string; + sort_order: number; + is_private: number; + owner_id: number | null; + weight_grams: number | null; + quantity: number; + bag_id: number | null; }): Promise { const platform = this.getEntityManager().getPlatform(); const id = await this.insert({ @@ -279,7 +313,11 @@ export class PackingItemsRepository extends TrekRepository { * sites now thread a `toRowId`-parsed/Zod-typed id); `trip_id` stays * `number | string`, a separate, still-accepted carry. */ - async findVisibleInTrip(id: number, trip_id: number | string, actorId: number | undefined): Promise { + async findVisibleInTrip( + id: number, + trip_id: number | string, + actorId: number | undefined, + ): Promise { if (actorId == null) return undefined; return await this.db() .selectFrom('packing_items') @@ -312,21 +350,31 @@ export class PackingItemsRepository extends TrekRepository { * no `updated_at` column at all). */ /** `id: number`, same Plan 4 Task 8b (U6) narrowing as {@link findById} (its one caller, `updateItem`, is only reached with a `toRowId`-parsed/Zod-typed id). */ - async update(id: number, write: { - name?: readonly [present: boolean, value: string | null]; - checked?: readonly [present: boolean, value: number]; - packed_quantity?: readonly [present: boolean, value: number | null]; - category?: readonly [present: boolean, value: string | null]; - weight_grams?: readonly [present: boolean, value: number | null]; - bag_id?: readonly [present: boolean, value: number | null]; - quantity?: readonly [present: boolean, value: number]; - is_private?: readonly [present: boolean, value: number]; - owner_id?: readonly [present: boolean, value: number | null]; - }): Promise { + async update( + id: number, + write: { + name?: readonly [present: boolean, value: string | null]; + checked?: readonly [present: boolean, value: number]; + packed_quantity?: readonly [present: boolean, value: number | null]; + category?: readonly [present: boolean, value: string | null]; + weight_grams?: readonly [present: boolean, value: number | null]; + bag_id?: readonly [present: boolean, value: number | null]; + quantity?: readonly [present: boolean, value: number]; + is_private?: readonly [present: boolean, value: number]; + owner_id?: readonly [present: boolean, value: number | null]; + }, + ): Promise { const platform = this.getEntityManager().getPlatform(); const data = presenceSet<{ - name: string | null; checked: number; packed_quantity: number | null; category: string | null; weight_grams: number | null; - bag: number | null; quantity: number; is_private: number; owner: number | null; + name: string | null; + checked: number; + packed_quantity: number | null; + category: string | null; + weight_grams: number | null; + bag: number | null; + quantity: number; + is_private: number; + owner: number | null; }>({ name: write.name, checked: write.checked, @@ -353,7 +401,9 @@ export class PackingItemsRepository extends TrekRepository { */ async updateSharing(id: number, is_private: number, claimOwnerId?: number): Promise { const platform = this.getEntityManager().getPlatform(); - const data = presenceSet<{ owner: number }>({ owner: claimOwnerId !== undefined ? [true, claimOwnerId] : undefined }); + const data = presenceSet<{ owner: number }>({ + owner: claimOwnerId !== undefined ? [true, claimOwnerId] : undefined, + }); await this.nativeUpdate({ id }, { is_private, ...data, updated_at: currentTimestamp(platform) }); } @@ -464,7 +514,16 @@ export class PackingItemsRepository extends TrekRepository { * stay restricted, owned by the copier; recipient rows are not carried * over (the caller's own note, unchanged). */ - async insertCopy(row: { trip_id: number | string; name: string; category: string | null; sort_order: number | null; weight_grams: number | null; bag_id: number | null; is_private: number; owner_id: number | null }): Promise { + async insertCopy(row: { + trip_id: number | string; + name: string; + category: string | null; + sort_order: number | null; + weight_grams: number | null; + bag_id: number | null; + is_private: number; + owner_id: number | null; + }): Promise { const platform = this.getEntityManager().getPlatform(); const id = await this.insert({ trip: row.trip_id, diff --git a/server/src/db/repositories/PackingTemplateCategories.repository.ts b/server/src/db/repositories/PackingTemplateCategories.repository.ts index 2377b979c..9e199479e 100644 --- a/server/src/db/repositories/PackingTemplateCategories.repository.ts +++ b/server/src/db/repositories/PackingTemplateCategories.repository.ts @@ -1,7 +1,7 @@ import type { PackingTemplateCategories } from '../entities/PackingTemplateCategories.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A bare `packing_template_categories` row — every scalar column, incl. the `persist(false)` `template_id` relation mirror. */ export interface PackingTemplateCategoryRow { @@ -26,7 +26,13 @@ export class PackingTemplateCategoriesRepository extends TrekRepository { - return await this.db().selectFrom('packing_template_categories').selectAll().where('template_id', '=', template_id as number).orderBy('sort_order', 'asc').orderBy('id', 'asc').execute(); + return await this.db() + .selectFrom('packing_template_categories') + .selectAll() + .where('template_id', '=', template_id as number) + .orderBy('sort_order', 'asc') + .orderBy('id', 'asc') + .execute(); } /** @@ -37,29 +43,53 @@ export class PackingTemplateCategoriesRepository extends TrekRepository { - const inserted = await this.kysely().insertInto('packing_template_categories').values({ template_id: template_id as number, name, sort_order }).returning('id').executeTakeFirstOrThrow(); + const inserted = await this.kysely() + .insertInto('packing_template_categories') + .values({ template_id: template_id as number, name, sort_order }) + .returning('id') + .executeTakeFirstOrThrow(); return inserted.id; } /** PK76 (`createTemplateCategory`) — `SELECT MAX(sort_order) as max FROM packing_template_categories WHERE template_id = ?`. */ async maxSortOrder(template_id: number | string): Promise { - const row = await this.db().selectFrom('packing_template_categories').select((eb) => eb.fn.max('sort_order').as('max')).where('template_id', '=', template_id as number).executeTakeFirst(); + const row = await this.db() + .selectFrom('packing_template_categories') + .select((eb) => eb.fn.max('sort_order').as('max')) + .where('template_id', '=', template_id as number) + .executeTakeFirst(); return row?.max ?? null; } /** PK78/PK81 — `SELECT * FROM packing_template_categories WHERE id = ?`, same text at two call sites (`createTemplateCategory`'s re-select, `updateTemplateCategory`'s re-select). */ async findById(id: number | string): Promise { - return await this.db().selectFrom('packing_template_categories').selectAll().where('id', '=', id as number).executeTakeFirst(); + return await this.db() + .selectFrom('packing_template_categories') + .selectAll() + .where('id', '=', id as number) + .executeTakeFirst(); } /** PK79/PK82 — `SELECT * FROM packing_template_categories WHERE id = ? AND template_id = ?`, the scope-check text at two call sites (`updateTemplateCategory`, `deleteTemplateCategory`). */ - async findInTemplate(id: number | string, template_id: number | string): Promise { - return await this.db().selectFrom('packing_template_categories').selectAll().where('id', '=', id as number).where('template_id', '=', template_id as number).executeTakeFirst(); + async findInTemplate( + id: number | string, + template_id: number | string, + ): Promise { + return await this.db() + .selectFrom('packing_template_categories') + .selectAll() + .where('id', '=', id as number) + .where('template_id', '=', template_id as number) + .executeTakeFirst(); } /** PK80 (`updateTemplateCategory`) — `UPDATE packing_template_categories SET name = ? WHERE id = ?`. */ async updateName(id: number | string, name: string): Promise { - await this.db().updateTable('packing_template_categories').set({ name }).where('id', '=', id as number).execute(); + await this.db() + .updateTable('packing_template_categories') + .set({ name }) + .where('id', '=', id as number) + .execute(); } /** PK83 (`deleteTemplateCategory`) — `DELETE FROM packing_template_categories WHERE id = ?`. */ diff --git a/server/src/db/repositories/PackingTemplateItems.repository.ts b/server/src/db/repositories/PackingTemplateItems.repository.ts index 39a90960c..81e746e30 100644 --- a/server/src/db/repositories/PackingTemplateItems.repository.ts +++ b/server/src/db/repositories/PackingTemplateItems.repository.ts @@ -1,7 +1,7 @@ import type { PackingTemplateItems } from '../entities/PackingTemplateItems.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A bare `packing_template_items` row — every scalar column, incl. the `persist(false)` `category_id` relation mirror. */ export interface PackingTemplateItemRow { @@ -109,13 +109,21 @@ export class PackingTemplateItemsRepository extends TrekRepository { - const row = await this.db().selectFrom('packing_template_items').select((eb) => eb.fn.max('sort_order').as('max')).where('category_id', '=', category_id as number).executeTakeFirst(); + const row = await this.db() + .selectFrom('packing_template_items') + .select((eb) => eb.fn.max('sort_order').as('max')) + .where('category_id', '=', category_id as number) + .executeTakeFirst(); return row?.max ?? null; } /** PK87/PK89 — `SELECT * FROM packing_template_items WHERE id = ?`, same text at two call sites (`createTemplateItem`'s re-select, `updateTemplateItem`'s re-select). */ async findById(id: number | string): Promise { - return await this.db().selectFrom('packing_template_items').selectAll().where('id', '=', id as number).executeTakeFirst(); + return await this.db() + .selectFrom('packing_template_items') + .selectAll() + .where('id', '=', id as number) + .executeTakeFirst(); } /** @@ -136,7 +144,11 @@ export class PackingTemplateItemsRepository extends TrekRepository { - await this.db().updateTable('packing_template_items').set({ name }).where('id', '=', id as number).execute(); + await this.db() + .updateTable('packing_template_items') + .set({ name }) + .where('id', '=', id as number) + .execute(); } /** PK90 (`deleteTemplateItem`) — `DELETE FROM packing_template_items WHERE id = ?`. */ diff --git a/server/src/db/repositories/PackingTemplates.repository.ts b/server/src/db/repositories/PackingTemplates.repository.ts index 9569e4745..ce44f5a93 100644 --- a/server/src/db/repositories/PackingTemplates.repository.ts +++ b/server/src/db/repositories/PackingTemplates.repository.ts @@ -1,7 +1,7 @@ import type { PackingTemplates } from '../entities/PackingTemplates.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A bare `packing_templates` row — every scalar column, incl. the `persist(false)` `created_by` relation mirror. */ export interface PackingTemplateRow { @@ -27,7 +27,10 @@ export interface PackingTemplateAdminRow extends PackingTemplateRow { category_count: number; } -type PackingTemplatesKyselyDB = Pick; +type PackingTemplatesKyselyDB = Pick< + DB, + 'packing_templates' | 'packing_template_categories' | 'packing_template_items' | 'users' +>; /** The table `insertTemplate` (PK55/PK68) inserts into; `id`/`created_at` are `InsertOptional` in the generated type. */ type PackingTemplatesInsertKyselyDB = Pick; @@ -106,7 +109,11 @@ export class PackingTemplatesRepository extends TrekRepository * `deletePackingTemplate`'s existence check). One method, five sites. */ async findById(id: number | string): Promise { - return await this.db().selectFrom('packing_templates').selectAll().where('id', '=', id as number).executeTakeFirst(); + return await this.db() + .selectFrom('packing_templates') + .selectAll() + .where('id', '=', id as number) + .executeTakeFirst(); } /** @@ -118,13 +125,21 @@ export class PackingTemplatesRepository extends TrekRepository * .insertItem`'s precedent). */ async insertTemplate(name: string, created_by: number): Promise { - const inserted = await this.kysely().insertInto('packing_templates').values({ name, created_by }).returning('id').executeTakeFirstOrThrow(); + const inserted = await this.kysely() + .insertInto('packing_templates') + .values({ name, created_by }) + .returning('id') + .executeTakeFirstOrThrow(); return inserted.id; } /** PK71 (`updatePackingTemplate`) — `UPDATE packing_templates SET name = ? WHERE id = ?`. */ async updateName(id: number | string, name: string): Promise { - await this.db().updateTable('packing_templates').set({ name }).where('id', '=', id as number).execute(); + await this.db() + .updateTable('packing_templates') + .set({ name }) + .where('id', '=', id as number) + .execute(); } /** PK74 (`deletePackingTemplate`) — `DELETE FROM packing_templates WHERE id = ?`. */ diff --git a/server/src/db/repositories/PasswordResetTokens.repository.ts b/server/src/db/repositories/PasswordResetTokens.repository.ts index ed2cf5076..493625186 100644 --- a/server/src/db/repositories/PasswordResetTokens.repository.ts +++ b/server/src/db/repositories/PasswordResetTokens.repository.ts @@ -1,7 +1,7 @@ -import type { PasswordResetTokens } from '../entities/PasswordResetTokens.entity'; -import type { AssertRowKeys } from './_shared/rows'; import { currentTimestamp } from '../dialect/sql-functions'; +import type { PasswordResetTokens } from '../entities/PasswordResetTokens.entity'; import type { DbTimestamp } from '../types'; +import type { AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; /** A `password_reset_tokens` row as the API emits it. */ @@ -102,7 +102,9 @@ export class PasswordResetTokensRepository extends TrekRepository { const row = await this.findOne({ token_hash: hash }, { fields: ['id', 'user', 'expires_at', 'consumed_at'] }); - return row ? { id: row.id, user_id: row.user.id, expires_at: row.expires_at, consumed_at: row.consumed_at ?? null } : null; + return row + ? { id: row.id, user_id: row.user.id, expires_at: row.expires_at, consumed_at: row.consumed_at ?? null } + : null; } /** diff --git a/server/src/db/repositories/PhotoProviderFields.repository.ts b/server/src/db/repositories/PhotoProviderFields.repository.ts index 3f3acba06..027b2a489 100644 --- a/server/src/db/repositories/PhotoProviderFields.repository.ts +++ b/server/src/db/repositories/PhotoProviderFields.repository.ts @@ -58,7 +58,18 @@ export class PhotoProviderFieldsRepository extends TrekRepository; diff --git a/server/src/db/repositories/PlaceDetailsCache.repository.ts b/server/src/db/repositories/PlaceDetailsCache.repository.ts index 0d7bf32c3..8d4c9aa03 100644 --- a/server/src/db/repositories/PlaceDetailsCache.repository.ts +++ b/server/src/db/repositories/PlaceDetailsCache.repository.ts @@ -9,7 +9,11 @@ export class PlaceDetailsCacheRepository extends TrekRepository { + async findEntry( + placeId: string, + lang: string, + kind: number, + ): Promise<{ payload_json: string; fetched_at: number } | null> { const row = await this.findOne( { place_id: placeId, lang, expanded: kind }, { fields: ['payload_json', 'fetched_at'] }, @@ -29,7 +33,13 @@ export class PlaceDetailsCacheRepository extends TrekRepository { + async upsertEntry(row: { + place_id: string; + lang: string; + expanded: number; + payload_json: string; + fetched_at: number; + }): Promise { await this.upsert(row, { onConflictFields: ['place_id', 'lang', 'expanded'], onConflictAction: 'merge' }); } } diff --git a/server/src/db/repositories/PlaceRatings.repository.ts b/server/src/db/repositories/PlaceRatings.repository.ts index 0be147462..14c2b006f 100644 --- a/server/src/db/repositories/PlaceRatings.repository.ts +++ b/server/src/db/repositories/PlaceRatings.repository.ts @@ -1,6 +1,6 @@ import type { PlaceRatings } from '../entities/PlaceRatings.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** One rated place, from `QueryHelpersService.loadRatingsByPlaceIds` (QH2). */ export interface PlaceRatingForPlaceRow { @@ -107,6 +107,9 @@ export class PlaceRatingsRepository extends TrekRepository { * unique {@link upsertRating} documents. */ async insertIgnore(place_id: number, user_id: number, rating: number): Promise { - await this.upsert({ place: place_id, user: user_id, rating }, { onConflictFields: ['place', 'user'], onConflictAction: 'ignore' }); + await this.upsert( + { place: place_id, user: user_id, rating }, + { onConflictFields: ['place', 'user'], onConflictAction: 'ignore' }, + ); } } diff --git a/server/src/db/repositories/PlaceRegions.repository.ts b/server/src/db/repositories/PlaceRegions.repository.ts index ab201a465..aad5bc9ba 100644 --- a/server/src/db/repositories/PlaceRegions.repository.ts +++ b/server/src/db/repositories/PlaceRegions.repository.ts @@ -1,6 +1,6 @@ import type { PlaceRegions } from '../entities/PlaceRegions.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** `place_regions` — the columns `AT28`'s `SELECT *` needs. */ export interface PlaceRegionRow { @@ -30,7 +30,11 @@ export class PlaceRegionsRepository extends TrekRepository { /** AT3 (`resolvePlaceCountries`'s batch cache probe) — `SELECT place_id, country_code FROM place_regions WHERE place_id IN (...)`. Empty-array short-circuit (matches the legacy's own dynamic-`IN` guard). */ async listCountryCodesForPlaceIds(placeIds: number[]): Promise<{ place_id: number; country_code: string }[]> { if (placeIds.length === 0) return []; - return await this.db().selectFrom('place_regions').select(['place_id', 'country_code']).where('place_id', 'in', placeIds).execute(); + return await this.db() + .selectFrom('place_regions') + .select(['place_id', 'country_code']) + .where('place_id', 'in', placeIds) + .execute(); } /** AT28 (`visitedRegions`'s cache probe) — `SELECT * FROM place_regions WHERE place_id IN (...)`. */ @@ -70,7 +74,9 @@ export class PlaceRegionsRepository extends TrekRepository { .where('places.id', '=', place.id) .where('places.lat', '=', place.lat) .where('places.lng', '=', place.lng) - .where((wb) => (place.address == null ? wb('places.address', 'is', null) : wb('places.address', '=', place.address))), + .where((wb) => + place.address == null ? wb('places.address', 'is', null) : wb('places.address', '=', place.address), + ), ) .onConflict((oc) => oc.column('place_id').doUpdateSet((ub) => ({ @@ -88,7 +94,16 @@ export class PlaceRegionsRepository extends TrekRepository { * pr.region_code, p.lat, p.lng, p.address FROM place_regions pr JOIN places p * ON p.id = pr.place_id ORDER BY pr.place_id`. */ - async listWithPlaceLocation(): Promise<{ id: number; country_code: string; region_code: string; lat: number | null; lng: number | null; address: string | null }[]> { + async listWithPlaceLocation(): Promise< + { + id: number; + country_code: string; + region_code: string; + lat: number | null; + lng: number | null; + address: string | null; + }[] + > { return await this.db() .selectFrom('place_regions as pr') .innerJoin('places as p', 'p.id', 'pr.place_id') @@ -103,7 +118,14 @@ export class PlaceRegionsRepository extends TrekRepository { * id = ? AND lat IS ? AND lng IS ? AND address IS ?)`. Only while the place * still holds the location the row was read with. Returns whether a row went. */ - async deleteRegionWhileUnmoved(row: { id: number; country_code: string; region_code: string; lat: number | null; lng: number | null; address: string | null }): Promise { + async deleteRegionWhileUnmoved(row: { + id: number; + country_code: string; + region_code: string; + lat: number | null; + lng: number | null; + address: string | null; + }): Promise { const result = await this.db() .deleteFrom('place_regions') .where('place_id', '=', row.id) @@ -117,7 +139,9 @@ export class PlaceRegionsRepository extends TrekRepository { .where('places.id', '=', row.id) .where((wb) => (row.lat == null ? wb('places.lat', 'is', null) : wb('places.lat', '=', row.lat))) .where((wb) => (row.lng == null ? wb('places.lng', 'is', null) : wb('places.lng', '=', row.lng))) - .where((wb) => (row.address == null ? wb('places.address', 'is', null) : wb('places.address', '=', row.address))), + .where((wb) => + row.address == null ? wb('places.address', 'is', null) : wb('places.address', '=', row.address), + ), ), ) .executeTakeFirst(); diff --git a/server/src/db/repositories/PlaceShadowPicks.repository.ts b/server/src/db/repositories/PlaceShadowPicks.repository.ts index 47bd15e66..121fced6d 100644 --- a/server/src/db/repositories/PlaceShadowPicks.repository.ts +++ b/server/src/db/repositories/PlaceShadowPicks.repository.ts @@ -1,5 +1,5 @@ -import type { PlaceShadowPicks } from '../entities/PlaceShadowPicks.entity'; import { countAll, countAllRef, maxOf, minOf, nowMinusDays } from '../dialect/sql-functions'; +import type { PlaceShadowPicks } from '../entities/PlaceShadowPicks.entity'; import { toRow, type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; @@ -80,7 +80,11 @@ export class PlaceShadowPicksRepository extends TrekRepository // coverage), unreachable, not a real "no rows" guard. Non-null // asserted, matching the docstring's own claim. const row = await this.qb('p') - .select([countAll(platform, 'total'), minOf(platform, 'p.created_at', 'oldest'), maxOf(platform, 'p.created_at', 'newest')]) + .select([ + countAll(platform, 'total'), + minOf(platform, 'p.created_at', 'oldest'), + maxOf(platform, 'p.created_at', 'newest'), + ]) .execute<{ total: number; oldest: string | null; newest: string | null } | undefined>('get', false); return { total: row!.total, oldest: row!.oldest, newest: row!.newest }; } diff --git a/server/src/db/repositories/PluginActions.repository.ts b/server/src/db/repositories/PluginActions.repository.ts index 226bc9b6d..82f3d4940 100644 --- a/server/src/db/repositories/PluginActions.repository.ts +++ b/server/src/db/repositories/PluginActions.repository.ts @@ -1,6 +1,6 @@ import type { PluginActions } from '../entities/PluginActions.entity'; -import type { PluginActionScope } from '@trek/shared'; import { TrekRepository } from './_shared/trek-repository'; +import type { PluginActionScope } from '@trek/shared'; /** `actionsOf`'s own projection — mapped to `PluginActionDescriptor` in the service. */ export interface PluginActionRow { @@ -23,7 +23,13 @@ export class PluginActionsRepository extends TrekRepository { { plugin_id: pluginId, scope }, { fields: ['action_key', 'label', 'hint', 'danger', 'scope'], orderBy: { sort_order: 'asc' } }, ); - return rows.map((r) => ({ action_key: r.action_key, label: r.label, hint: r.hint ?? null, danger: r.danger, scope: r.scope as PluginActionScope })); + return rows.map((r) => ({ + action_key: r.action_key, + label: r.label, + hint: r.hint ?? null, + danger: r.danger, + scope: r.scope as PluginActionScope, + })); } /** PR35 (uninstall cascade) — `DELETE FROM plugin_actions WHERE plugin_id = ?`. The caller wraps this in its own try/catch (table absent on a slimmed test schema is tolerated here). */ @@ -42,7 +48,15 @@ export class PluginActionsRepository extends TrekRepository { async insertActions(pluginId: string, actions: NewPluginActionRow[]): Promise { if (!actions.length) return; await this.insertMany( - actions.map((a) => ({ plugin_id: pluginId, action_key: a.action_key, label: a.label, hint: a.hint, danger: a.danger, scope: a.scope, sort_order: a.sort_order })), + actions.map((a) => ({ + plugin_id: pluginId, + action_key: a.action_key, + label: a.label, + hint: a.hint, + danger: a.danger, + scope: a.scope, + sort_order: a.sort_order, + })), ); } } diff --git a/server/src/db/repositories/PluginCapabilityAudit.repository.ts b/server/src/db/repositories/PluginCapabilityAudit.repository.ts index 33b0203c4..84826d007 100644 --- a/server/src/db/repositories/PluginCapabilityAudit.repository.ts +++ b/server/src/db/repositories/PluginCapabilityAudit.repository.ts @@ -1,6 +1,6 @@ import type { PluginCapabilityAudit } from '../entities/PluginCapabilityAudit.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** * Plan 3j Task 2 note: this repository's ONLY method today is the uninstall @@ -42,7 +42,12 @@ export class PluginCapabilityAuditRepository extends TrekRepository ({ ts: r.ts, plugin_id: r.plugin_id, plugin_name: r.plugin_name ?? null, method: r.method, resource: r.resource, code: r.code })); + return rows.map((r) => ({ + ts: r.ts, + plugin_id: r.plugin_id, + plugin_name: r.plugin_name ?? null, + method: r.method, + resource: r.resource, + code: r.code, + })); } /** @@ -111,7 +123,13 @@ export class PluginCapabilityAuditRepository extends TrekRepository ({ ts: r.ts, acting_user_id: r.acting_user_id ?? null, method: r.method, resource: r.resource ?? null, code: r.code })); + return rows.map((r) => ({ + ts: r.ts, + acting_user_id: r.acting_user_id ?? null, + method: r.method, + resource: r.resource ?? null, + code: r.code, + })); } /** diff --git a/server/src/db/repositories/PluginEgressHosts.repository.ts b/server/src/db/repositories/PluginEgressHosts.repository.ts index 582ae9ce0..e0c0226d1 100644 --- a/server/src/db/repositories/PluginEgressHosts.repository.ts +++ b/server/src/db/repositories/PluginEgressHosts.repository.ts @@ -28,7 +28,10 @@ export class PluginEgressHostsRepository extends TrekRepository { await this.nativeDelete({ plugin_id: pluginId }); for (const host of hosts) { - await this.upsert({ plugin_id: pluginId, host }, { onConflictFields: ['plugin_id', 'host'], onConflictAction: 'ignore' }); + await this.upsert( + { plugin_id: pluginId, host }, + { onConflictFields: ['plugin_id', 'host'], onConflictAction: 'ignore' }, + ); } } diff --git a/server/src/db/repositories/PluginEntityMetadata.repository.ts b/server/src/db/repositories/PluginEntityMetadata.repository.ts index bdfc9aa0d..0932170a2 100644 --- a/server/src/db/repositories/PluginEntityMetadata.repository.ts +++ b/server/src/db/repositories/PluginEntityMetadata.repository.ts @@ -1,5 +1,5 @@ -import type { PluginEntityMetadata } from '../entities/PluginEntityMetadata.entity'; import { currentTimestamp } from '../dialect/sql-functions'; +import type { PluginEntityMetadata } from '../entities/PluginEntityMetadata.entity'; import { TrekRepository } from './_shared/trek-repository'; export class PluginEntityMetadataRepository extends TrekRepository { @@ -22,7 +22,10 @@ export class PluginEntityMetadataRepository extends TrekRepository { - const row = await this.findOne({ plugin_id: pluginId, entity_type: entityType, entity_id: entityId, key }, { fields: ['value'] }); + const row = await this.findOne( + { plugin_id: pluginId, entity_type: entityType, entity_id: entityId, key }, + { fields: ['value'] }, + ); return row?.value ?? null; } @@ -45,7 +48,14 @@ export class PluginEntityMetadataRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); await this.upsert( - { plugin_id: pluginId, entity_type: entityType, entity_id: entityId, key, value, updated_at: currentTimestamp(platform) }, + { + plugin_id: pluginId, + entity_type: entityType, + entity_id: entityId, + key, + value, + updated_at: currentTimestamp(platform), + }, { onConflictFields: ['plugin_id', 'entity_type', 'entity_id', 'key'], onConflictAction: 'merge', @@ -55,7 +65,11 @@ export class PluginEntityMetadataRepository extends TrekRepository { + async listForEntity( + pluginId: string, + entityType: string, + entityId: number, + ): Promise<{ key: string; value: string }[]> { const rows = await this.find( { plugin_id: pluginId, entity_type: entityType, entity_id: entityId }, { fields: ['key', 'value'], orderBy: { key: 'asc' } }, @@ -87,7 +101,14 @@ export class PluginEntityMetadataRepository extends TrekRepository { + async upsertValueCapped( + pluginId: string, + entityType: string, + entityId: number, + key: string, + value: string, + maxKeys: number, + ): Promise { return this.getEntityManager().transactional(async () => { const exists = (await this.findValue(pluginId, entityType, entityId, key)) !== null; if (!exists) { diff --git a/server/src/db/repositories/PluginErrorLog.repository.ts b/server/src/db/repositories/PluginErrorLog.repository.ts index 46dee0959..e6ee019f0 100644 --- a/server/src/db/repositories/PluginErrorLog.repository.ts +++ b/server/src/db/repositories/PluginErrorLog.repository.ts @@ -1,6 +1,6 @@ import type { PluginErrorLog } from '../entities/PluginErrorLog.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** PR2 delivery row / plugins.service.ts `errors()`'s own projection. */ export interface PluginErrorLogRow { @@ -35,7 +35,12 @@ export class PluginErrorLogRepository extends TrekRepository { .where( 'id', 'not in', - db.selectFrom('plugin_error_log').select('id').where('plugin_id', '=', pluginId).orderBy('id', 'desc').limit(retention), + db + .selectFrom('plugin_error_log') + .select('id') + .where('plugin_id', '=', pluginId) + .orderBy('id', 'desc') + .limit(retention), ) .execute(); } diff --git a/server/src/db/repositories/PluginOauthState.repository.ts b/server/src/db/repositories/PluginOauthState.repository.ts index a9c0d083e..12a9aaf2d 100644 --- a/server/src/db/repositories/PluginOauthState.repository.ts +++ b/server/src/db/repositories/PluginOauthState.repository.ts @@ -1,6 +1,6 @@ import type { PluginOauthState } from '../entities/PluginOauthState.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** * Plan 3j Task 2 note: this repository's ONLY method today is the uninstall @@ -37,7 +37,13 @@ export class PluginOauthStateRepository extends TrekRepository } /** PO4 (`startConnect`) — `INSERT INTO plugin_oauth_state (state, plugin_id, user_id, verifier, created_at) VALUES (?, ?, ?, ?, ?)`. */ - async insertState(state: string, pluginId: string, userId: number, verifier: string, createdAt: number): Promise { + async insertState( + state: string, + pluginId: string, + userId: number, + verifier: string, + createdAt: number, + ): Promise { await this.insert({ state, plugin_id: pluginId, user_id: userId, verifier, created_at: createdAt }); } diff --git a/server/src/db/repositories/PluginOauthTokens.repository.ts b/server/src/db/repositories/PluginOauthTokens.repository.ts index 94498a7b5..4246ff31f 100644 --- a/server/src/db/repositories/PluginOauthTokens.repository.ts +++ b/server/src/db/repositories/PluginOauthTokens.repository.ts @@ -1,7 +1,7 @@ -import type { PluginOauthTokens } from '../entities/PluginOauthTokens.entity'; import { currentTimestampKysely } from '../dialect/kysely-functions'; -import { TrekRepository } from './_shared/trek-repository'; +import type { PluginOauthTokens } from '../entities/PluginOauthTokens.entity'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** * Plan 3j Task 2 note: this repository's ONLY method today is the uninstall @@ -38,14 +38,26 @@ export class PluginOauthTokensRepository extends TrekRepository { - const row = await this.findOne({ plugin_id: pluginId, user_id: userId, access_token: { $ne: null } }, { fields: ['plugin_id'] }); + const row = await this.findOne( + { plugin_id: pluginId, user_id: userId, access_token: { $ne: null } }, + { fields: ['plugin_id'] }, + ); return row !== null; } /** PO8 (`getAccessToken`) — `SELECT access_token, refresh_token, expires_at FROM plugin_oauth_tokens WHERE plugin_id = ? AND user_id = ?`. */ async findTokenRow(pluginId: string, userId: number): Promise { - const row = await this.findOne({ plugin_id: pluginId, user_id: userId }, { fields: ['access_token', 'refresh_token', 'expires_at'] }); - return row ? { access_token: row.access_token ?? null, refresh_token: row.refresh_token ?? null, expires_at: row.expires_at ?? null } : null; + const row = await this.findOne( + { plugin_id: pluginId, user_id: userId }, + { fields: ['access_token', 'refresh_token', 'expires_at'] }, + ); + return row + ? { + access_token: row.access_token ?? null, + refresh_token: row.refresh_token ?? null, + expires_at: row.expires_at ?? null, + } + : null; } /** PO9 (`disconnect`) — `DELETE FROM plugin_oauth_tokens WHERE plugin_id = ? AND user_id = ?`. */ @@ -104,7 +116,8 @@ export class PluginOauthTokensRepository extends TrekRepository oc.columns(['plugin_id', 'user_id']).doUpdateSet({ access_token: (eb) => eb.ref('excluded.access_token'), - refresh_token: (eb) => eb.fn.coalesce(eb.ref('excluded.refresh_token'), eb.ref('plugin_oauth_tokens.refresh_token')), + refresh_token: (eb) => + eb.fn.coalesce(eb.ref('excluded.refresh_token'), eb.ref('plugin_oauth_tokens.refresh_token')), expires_at: (eb) => eb.ref('excluded.expires_at'), scope: (eb) => eb.ref('excluded.scope'), updated_at: () => currentTimestampKysely(platform), diff --git a/server/src/db/repositories/PluginScheduledTasks.repository.ts b/server/src/db/repositories/PluginScheduledTasks.repository.ts index ea3312573..a0d4553c8 100644 --- a/server/src/db/repositories/PluginScheduledTasks.repository.ts +++ b/server/src/db/repositories/PluginScheduledTasks.repository.ts @@ -32,7 +32,13 @@ export class PluginScheduledTasksRepository extends TrekRepository ({ id: r.id, plugin_id: r.plugin_id, name: r.name, payload: r.payload, every_ms: r.every_ms ?? null })); + return rows.map((r) => ({ + id: r.id, + plugin_id: r.plugin_id, + name: r.name, + payload: r.payload, + every_ms: r.every_ms ?? null, + })); } /** @@ -103,8 +109,18 @@ export class PluginScheduledTasksRepository extends TrekRepository { await this.upsert( - { plugin_id: input.plugin_id, name: input.name, due_at: input.due_at, payload: input.payload, every_ms: input.every_ms }, - { onConflictFields: ['plugin_id', 'name'], onConflictAction: 'merge', onConflictMergeFields: ['due_at', 'payload', 'every_ms'] }, + { + plugin_id: input.plugin_id, + name: input.name, + due_at: input.due_at, + payload: input.payload, + every_ms: input.every_ms, + }, + { + onConflictFields: ['plugin_id', 'name'], + onConflictAction: 'merge', + onConflictMergeFields: ['due_at', 'payload', 'every_ms'], + }, ); } diff --git a/server/src/db/repositories/PluginSettingsFields.repository.ts b/server/src/db/repositories/PluginSettingsFields.repository.ts index f04a50889..cbde242b3 100644 --- a/server/src/db/repositories/PluginSettingsFields.repository.ts +++ b/server/src/db/repositories/PluginSettingsFields.repository.ts @@ -62,7 +62,17 @@ export class PluginSettingsFieldsRepository extends TrekRepository> { + async listFieldKeysWithSecretFlag( + pluginId: string, + scope: 'instance' | 'user', + ): Promise> { const rows = await this.find({ plugin_id: pluginId, scope }, { fields: ['field_key', 'secret'] }); return rows.map((r) => ({ field_key: r.field_key, secret: r.secret })); } @@ -153,7 +166,10 @@ export class PluginSettingsFieldsRepository extends TrekRepository> { + async listDefaults( + pluginId: string, + scope: 'instance' | 'user', + ): Promise> { const rows = await this.find( { plugin_id: pluginId, scope, secret: 0, default_value: { $ne: null } }, { fields: ['field_key', 'default_value'] }, diff --git a/server/src/db/repositories/PluginUserConfig.repository.ts b/server/src/db/repositories/PluginUserConfig.repository.ts index f80ac6a48..12736804b 100644 --- a/server/src/db/repositories/PluginUserConfig.repository.ts +++ b/server/src/db/repositories/PluginUserConfig.repository.ts @@ -1,5 +1,5 @@ -import type { PluginUserConfig } from '../entities/PluginUserConfig.entity'; import { currentTimestamp } from '../dialect/sql-functions'; +import type { PluginUserConfig } from '../entities/PluginUserConfig.entity'; import { TrekRepository } from './_shared/trek-repository'; /** `exportUserData`'s own per-user config scan. */ @@ -40,7 +40,11 @@ export class PluginUserConfigRepository extends TrekRepository const platform = this.getEntityManager().getPlatform(); await this.upsert( { plugin_id: pluginId, user_id: userId, config, updated_at: currentTimestamp(platform) }, - { onConflictFields: ['plugin_id', 'user_id'], onConflictAction: 'merge', onConflictMergeFields: ['config', 'updated_at'] }, + { + onConflictFields: ['plugin_id', 'user_id'], + onConflictAction: 'merge', + onConflictMergeFields: ['config', 'updated_at'], + }, ); } diff --git a/server/src/db/repositories/PluginUserErasureQueue.repository.ts b/server/src/db/repositories/PluginUserErasureQueue.repository.ts index a32eb3ce9..35e2cc82f 100644 --- a/server/src/db/repositories/PluginUserErasureQueue.repository.ts +++ b/server/src/db/repositories/PluginUserErasureQueue.repository.ts @@ -1,6 +1,6 @@ import type { PluginUserErasureQueue } from '../entities/PluginUserErasureQueue.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** The drain's own pending-row projection. */ export interface PendingErasureRow { @@ -15,7 +15,10 @@ type PluginUserErasureQueueKyselyDB = Pick { /** PR8 — `INSERT OR IGNORE INTO plugin_user_erasure_queue (plugin_id, user_id) VALUES (?, ?)` (idempotent — a duplicate enqueue for the same pair is a no-op). */ async insertIgnore(pluginId: string, userId: number): Promise { - await this.upsert({ plugin_id: pluginId, user_id: userId }, { onConflictFields: ['plugin_id', 'user_id'], onConflictAction: 'ignore' }); + await this.upsert( + { plugin_id: pluginId, user_id: userId }, + { onConflictFields: ['plugin_id', 'user_id'], onConflictAction: 'ignore' }, + ); } /** diff --git a/server/src/db/repositories/Plugins.repository.ts b/server/src/db/repositories/Plugins.repository.ts index ee416f905..bc7aa4caa 100644 --- a/server/src/db/repositories/Plugins.repository.ts +++ b/server/src/db/repositories/Plugins.repository.ts @@ -1,5 +1,5 @@ -import type { Plugins } from '../entities/Plugins.entity'; import { currentTimestamp } from '../dialect/sql-functions'; +import type { Plugins } from '../entities/Plugins.entity'; import { TrekRepository } from './_shared/trek-repository'; /** @@ -128,7 +128,12 @@ export class PluginsRepository extends TrekRepository { /** PR15/PR16/PR25 — `SELECT id, version, enabled, dependencies FROM plugins` (identical text, three call sites: `installedDepRows`, `deactivateForDisabledAddon`, `deactivateWithDependents`). */ async listDepRows(): Promise { const rows = await this.find({}, { fields: ['id', 'version', 'enabled', 'dependencies'] }); - return rows.map((r) => ({ id: r.id ?? '', version: r.version ?? null, enabled: r.enabled, dependencies: r.dependencies ?? null })); + return rows.map((r) => ({ + id: r.id ?? '', + version: r.version ?? null, + enabled: r.enabled, + dependencies: r.dependencies ?? null, + })); } // ----------------------------------------------------------------------- @@ -137,7 +142,10 @@ export class PluginsRepository extends TrekRepository { /** PR17 — `SELECT permissions, granted_permissions, dependencies, trek_range, api_version FROM plugins WHERE id = ?`. */ async findActivationGate(id: string): Promise { - const row = await this.findOne({ id }, { fields: ['permissions', 'granted_permissions', 'dependencies', 'trek_range', 'api_version'] }); + const row = await this.findOne( + { id }, + { fields: ['permissions', 'granted_permissions', 'dependencies', 'trek_range', 'api_version'] }, + ); return row ? { permissions: row.permissions ?? '[]', @@ -195,7 +203,9 @@ export class PluginsRepository extends TrekRepository { /** PR26 — `SELECT enabled, granted_permissions, version FROM plugins WHERE id = ?`. */ async findPreUpdateSnapshot(id: string): Promise { const row = await this.findOne({ id }, { fields: ['enabled', 'granted_permissions', 'version'] }); - return row ? { enabled: row.enabled, granted_permissions: row.granted_permissions ?? '', version: row.version ?? null } : null; + return row + ? { enabled: row.enabled, granted_permissions: row.granted_permissions ?? '', version: row.version ?? null } + : null; } // ----------------------------------------------------------------------- @@ -344,9 +354,27 @@ export class PluginsRepository extends TrekRepository { {}, { fields: [ - 'id', 'name', 'description', 'type', 'icon', 'version', 'status', 'enabled', 'last_error', 'reviewed_at', 'source_repo', - 'permissions', 'capabilities', 'dependencies', 'operator_egress', 'trek_range', 'author_pubkey', - 'update_block_code', 'update_block_detail', 'update_block_version', 'update_hold', + 'id', + 'name', + 'description', + 'type', + 'icon', + 'version', + 'status', + 'enabled', + 'last_error', + 'reviewed_at', + 'source_repo', + 'permissions', + 'capabilities', + 'dependencies', + 'operator_egress', + 'trek_range', + 'author_pubkey', + 'update_block_code', + 'update_block_detail', + 'update_block_version', + 'update_hold', ], orderBy: [{ sort_order: 'asc' }, { name: 'asc' }], }, @@ -470,7 +498,10 @@ export class PluginsRepository extends TrekRepository { /** SG1 — `UPDATE plugins SET update_block_code = ?, update_block_detail = ?, update_block_version = ? WHERE id = ?` (`setUpdateBlock`, a signature-verification failure). */ async setUpdateBlockColumns(id: string, code: string, detail: string, version: string | null): Promise { - await this.nativeUpdate({ id }, { update_block_code: code, update_block_detail: detail, update_block_version: version }); + await this.nativeUpdate( + { id }, + { update_block_code: code, update_block_detail: detail, update_block_version: version }, + ); } /** SG2 — `UPDATE plugins SET update_block_code = NULL, update_block_detail = NULL, update_block_version = NULL WHERE id = ?` (`clearUpdateBlock`, the inverse — a successful install/update). */ @@ -485,8 +516,17 @@ export class PluginsRepository extends TrekRepository { // ----------------------------------------------------------------------- /** RG2 (`install`'s post-discover provenance write) — `UPDATE plugins SET source_repo = ?, source_commit = ?, sha256 = ?, reviewed_at = ? WHERE id = ?`. */ - async setInstallProvenance(id: string, sourceRepo: string, sourceCommit: string, sha256: string, reviewedAt: string | null): Promise { - await this.nativeUpdate({ id }, { source_repo: sourceRepo, source_commit: sourceCommit, sha256, reviewed_at: reviewedAt }); + async setInstallProvenance( + id: string, + sourceRepo: string, + sourceCommit: string, + sha256: string, + reviewedAt: string | null, + ): Promise { + await this.nativeUpdate( + { id }, + { source_repo: sourceRepo, source_commit: sourceCommit, sha256, reviewed_at: reviewedAt }, + ); } /** RG3 (`install`'s TOFU pin) — `UPDATE plugins SET author_pubkey = ? WHERE id = ?`. Security-sensitive: only ever called with a key the artifact just verified under (see `registry.service.ts#install`'s own docstring) — never cleared to NULL by this method. */ @@ -534,7 +574,9 @@ export class PluginsRepository extends TrekRepository { } /** RG11 (`assertRetrustable`) — `SELECT source_repo, author_pubkey FROM plugins WHERE id = ?`. */ - async findSourceRepoAndAuthorPubkey(id: string): Promise<{ source_repo: string | null; author_pubkey: string | null } | null> { + async findSourceRepoAndAuthorPubkey( + id: string, + ): Promise<{ source_repo: string | null; author_pubkey: string | null } | null> { const row = await this.findOne({ id }, { fields: ['source_repo', 'author_pubkey'] }); return row ? { source_repo: row.source_repo ?? null, author_pubkey: row.author_pubkey ?? null } : null; } @@ -556,7 +598,10 @@ export class PluginsRepository extends TrekRepository { async findActiveFeedRows(): Promise { const rows = await this.find( { status: 'active' }, - { fields: ['id', 'name', 'type', 'icon', 'capabilities', 'granted_permissions'], orderBy: [{ sort_order: 'asc' }, { name: 'asc' }] }, + { + fields: ['id', 'name', 'type', 'icon', 'capabilities', 'granted_permissions'], + orderBy: [{ sort_order: 'asc' }, { name: 'asc' }], + }, ); return rows.map((r) => ({ id: r.id ?? '', diff --git a/server/src/db/repositories/PushSubscriptions.repository.ts b/server/src/db/repositories/PushSubscriptions.repository.ts index 1dcb83a78..4e3874c85 100644 --- a/server/src/db/repositories/PushSubscriptions.repository.ts +++ b/server/src/db/repositories/PushSubscriptions.repository.ts @@ -1,9 +1,9 @@ -import type { PushSubscriptions } from '../entities/PushSubscriptions.entity'; -import { columnIncrementedBy, currentTimestamp } from '../dialect/sql-functions'; import { currentTimestampKysely } from '../dialect/kysely-functions'; +import { columnIncrementedBy, currentTimestamp } from '../dialect/sql-functions'; +import type { PushSubscriptions } from '../entities/PushSubscriptions.entity'; +import type { DB } from '../kysely/db'; import type { AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A `push_subscriptions` row exactly as `SELECT *` reads it (Web Push, #894). */ export interface PushSubscriptionRow { diff --git a/server/src/db/repositories/ReservationDayPositions.repository.ts b/server/src/db/repositories/ReservationDayPositions.repository.ts index b27c487dd..14b7a18a2 100644 --- a/server/src/db/repositories/ReservationDayPositions.repository.ts +++ b/server/src/db/repositories/ReservationDayPositions.repository.ts @@ -1,7 +1,7 @@ -import type { ReservationDayPositions } from '../entities/ReservationDayPositions.entity'; import { columnRef } from '../dialect/sql-functions'; -import { TrekRepository } from './_shared/trek-repository'; +import type { ReservationDayPositions } from '../entities/ReservationDayPositions.entity'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** `reservation_day_positions`/`reservations`/`days`'s shape for RS31's Kysely upsert. */ type ReservationDayPositionsKyselyDB = Pick; @@ -23,7 +23,11 @@ export class ReservationDayPositionsRepository extends TrekRepository('all', false); } @@ -42,7 +46,12 @@ export class ReservationDayPositionsRepository extends TrekRepository { + async upsertScoped( + trip_id: number | string, + reservation_id: number, + day_id: number, + position: number, + ): Promise { await this.kysely() .insertInto('reservation_day_positions') .orReplace() diff --git a/server/src/db/repositories/ReservationEndpoints.repository.ts b/server/src/db/repositories/ReservationEndpoints.repository.ts index 2481bc450..693c1f027 100644 --- a/server/src/db/repositories/ReservationEndpoints.repository.ts +++ b/server/src/db/repositories/ReservationEndpoints.repository.ts @@ -1,9 +1,9 @@ -import type { ReservationEndpoints } from '../entities/ReservationEndpoints.entity'; -import { type AssertRowKeys } from './_shared/rows'; import { columnRef } from '../dialect/sql-functions'; -import { travelerOwnsExpr, type ReservationTravelersOwnsKyselyDB } from './_shared/reservation-travelers-owns'; -import { TrekRepository } from './_shared/trek-repository'; +import type { ReservationEndpoints } from '../entities/ReservationEndpoints.entity'; import type { DB } from '../kysely/db'; +import { travelerOwnsExpr, type ReservationTravelersOwnsKyselyDB } from './_shared/reservation-travelers-owns'; +import { type AssertRowKeys } from './_shared/rows'; +import { TrekRepository } from './_shared/trek-repository'; /** A `reservation_endpoints` row exactly as `r.*`/`SELECT *` read it (every scalar column). */ export interface ReservationEndpointRow { @@ -117,14 +117,19 @@ export class ReservationEndpointsRepository extends TrekRepository { + async listFlightEndpointsForTrip( + trip_id: number, + ): Promise<{ reservation_id: number; code: string | null; local_date: string | null; sequence: number }[]> { const platform = this.getEntityManager().getPlatform(); return this.qb('e') .join('e.reservation', 'r') .select([columnRef(platform, 'e.reservation_id').as('reservation_id'), 'e.code', 'e.local_date', 'e.sequence']) .where({ 'r.trip': trip_id, 'r.type': 'flight' }) .orderBy({ sequence: 'asc' }) - .execute<{ reservation_id: number; code: string | null; local_date: string | null; sequence: number }[]>('all', false); + .execute<{ reservation_id: number; code: string | null; local_date: string | null; sequence: number }[]>( + 'all', + false, + ); } // --------------------------------------------------------------------------- @@ -155,9 +160,24 @@ export class ReservationEndpointsRepository extends TrekRepository [ - 'e.id', 'e.reservation_id', 'r.trip_id', 'e.role', 'e.code', 'e.lat', 'e.lng', 'e.local_date', 'e.local_time', - 'r.type as reservation_type', 'r.status as reservation_status', - eb.case().when('e.role', '=', 'to').then(eb.fn.coalesce('r.reservation_end_time', 'r.reservation_time')).else(eb.fn.coalesce('r.reservation_time', 'r.reservation_end_time')).end().as('fallback_time'), + 'e.id', + 'e.reservation_id', + 'r.trip_id', + 'e.role', + 'e.code', + 'e.lat', + 'e.lng', + 'e.local_date', + 'e.local_time', + 'r.type as reservation_type', + 'r.status as reservation_status', + eb + .case() + .when('e.role', '=', 'to') + .then(eb.fn.coalesce('r.reservation_end_time', 'r.reservation_time')) + .else(eb.fn.coalesce('r.reservation_time', 'r.reservation_end_time')) + .end() + .as('fallback_time'), ]) .distinct() .where('r.trip_id', 'in', tripIds) @@ -189,9 +209,24 @@ export class ReservationEndpointsRepository extends TrekRepository [ - 'e.id', 'e.reservation_id', 'r.trip_id', 'e.role', 'e.code', 'e.lat', 'e.lng', 'e.local_date', 'e.local_time', - 'r.type as reservation_type', 'r.status as reservation_status', - eb.case().when('e.role', '=', 'to').then(eb.fn.coalesce('r.reservation_end_time', 'r.reservation_time')).else(eb.fn.coalesce('r.reservation_time', 'r.reservation_end_time')).end().as('fallback_time'), + 'e.id', + 'e.reservation_id', + 'r.trip_id', + 'e.role', + 'e.code', + 'e.lat', + 'e.lng', + 'e.local_date', + 'e.local_time', + 'r.type as reservation_type', + 'r.status as reservation_status', + eb + .case() + .when('e.role', '=', 'to') + .then(eb.fn.coalesce('r.reservation_end_time', 'r.reservation_time')) + .else(eb.fn.coalesce('r.reservation_time', 'r.reservation_end_time')) + .end() + .as('fallback_time'), ]) .distinct() .where((eb) => eb.or([eb('t.user_id', '=', userId), eb('tm.user_id', '=', userId)])) @@ -240,7 +275,15 @@ export class ReservationEndpointsRepository extends TrekRepository('all', false); @@ -275,7 +318,8 @@ export interface TravelerOwnedEndpointRow { } /** {@link ReservationEndpointsRepository.listOwnedEndpointsForTrips}'s `reservation_endpoints`/`reservations`/`reservation_travelers` tables (`r` aliased for `travelerOwnsExpr`, R7). */ -type TravelerOwnedEndpointsKyselyDB = ReservationTravelersOwnsKyselyDB & Pick; +type TravelerOwnedEndpointsKyselyDB = ReservationTravelersOwnsKyselyDB & + Pick; /** {@link ReservationEndpointsRepository.listOwnedEndpointsForUser}/{@link ReservationEndpointsRepository.listOwnedFlightLegsForUser}'s tables, adding `trips`/`trip_members` to {@link TravelerOwnedEndpointsKyselyDB}. */ type TravelerOwnedEndpointsWithTripsKyselyDB = TravelerOwnedEndpointsKyselyDB & Pick; diff --git a/server/src/db/repositories/ReservationTravelers.repository.ts b/server/src/db/repositories/ReservationTravelers.repository.ts index c72e39de7..16a89d20d 100644 --- a/server/src/db/repositories/ReservationTravelers.repository.ts +++ b/server/src/db/repositories/ReservationTravelers.repository.ts @@ -1,5 +1,5 @@ -import type { ReservationTravelers } from '../entities/ReservationTravelers.entity'; import { coalesce, columnRef } from '../dialect/sql-functions'; +import type { ReservationTravelers } from '../entities/ReservationTravelers.entity'; import { TrekRepository } from './_shared/trek-repository'; /** A traveler row as RS6/RR3 project it (before `toTraveler()`'s `avatar_url` add). */ @@ -30,7 +30,13 @@ export class ReservationTravelersRepository extends TrekRepository('all', false); @@ -47,7 +53,12 @@ export class ReservationTravelersRepository extends TrekRepository[]>('all', false); } diff --git a/server/src/db/repositories/RoadtripDayBoundaries.repository.ts b/server/src/db/repositories/RoadtripDayBoundaries.repository.ts index 81741644c..531e23ba5 100644 --- a/server/src/db/repositories/RoadtripDayBoundaries.repository.ts +++ b/server/src/db/repositories/RoadtripDayBoundaries.repository.ts @@ -1,7 +1,7 @@ -import type { RoadtripDayBoundary } from '@trek/shared'; -import type { RoadtripDayBoundaries } from '../entities/RoadtripDayBoundaries.entity'; import { columnRef } from '../dialect/sql-functions'; +import type { RoadtripDayBoundaries } from '../entities/RoadtripDayBoundaries.entity'; import { TrekRepository } from './_shared/trek-repository'; +import type { RoadtripDayBoundary } from '@trek/shared'; /** * `roadtrip_day_boundaries` — where a manual day ending sits (a stop, or a @@ -23,7 +23,12 @@ export class RoadtripDayBoundariesRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); return await this.qb('b') - .select(['b.day_number', columnRef(platform, 'b.from_assignment_id'), columnRef(platform, 'b.to_assignment_id'), 'b.fraction']) + .select([ + 'b.day_number', + columnRef(platform, 'b.from_assignment_id'), + columnRef(platform, 'b.to_assignment_id'), + 'b.fraction', + ]) .where({ trip: trip_id }) .orderBy({ day_number: 'asc' }) .execute('all', false); @@ -54,7 +59,11 @@ export class RoadtripDayBoundariesRepository extends TrekRepository { - await this.upsert({ trip: trip_id, key, value }, { onConflictFields: ['trip', 'key'], onConflictAction: 'merge', onConflictMergeFields: ['value'] }); + await this.upsert( + { trip: trip_id, key, value }, + { onConflictFields: ['trip', 'key'], onConflictAction: 'merge', onConflictMergeFields: ['value'] }, + ); } } diff --git a/server/src/db/repositories/RoadtripVias.repository.ts b/server/src/db/repositories/RoadtripVias.repository.ts index 0049ffb13..3da21626d 100644 --- a/server/src/db/repositories/RoadtripVias.repository.ts +++ b/server/src/db/repositories/RoadtripVias.repository.ts @@ -1,9 +1,9 @@ -import type { RoadtripVia } from '@trek/shared'; -import type { AnchoredVia } from '@trek/shared/roadtrip'; -import type { RoadtripVias } from '../entities/RoadtripVias.entity'; import { columnRef, maxOf } from '../dialect/sql-functions'; +import type { RoadtripVias } from '../entities/RoadtripVias.entity'; import { toRow, type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; +import type { RoadtripVia } from '@trek/shared'; +import type { AnchoredVia } from '@trek/shared/roadtrip'; const _roadtripViaRowKeys: AssertRowKeys = true; @@ -31,7 +31,10 @@ export interface RoadtripViaLegRow { export class RoadtripViasRepository extends TrekRepository { /** RT2/AC16/AS23 — `SELECT id, day_id, after_order_index, sequence, lat, lng, created_at FROM roadtrip_vias WHERE day_id = ? ORDER BY after_order_index, sequence, id`. */ async listForDay(day_id: number): Promise { - const vias = await this.find({ day: day_id }, { orderBy: { after_order_index: 'asc', sequence: 'asc', id: 'asc' } }); + const vias = await this.find( + { day: day_id }, + { orderBy: { after_order_index: 'asc', sequence: 'asc', id: 'asc' } }, + ); return vias.map((v) => toRow(v) as RoadtripVia); } @@ -53,7 +56,15 @@ export class RoadtripViasRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); return await this.qb('v') .join('v.day', 'd') - .select(['v.id', columnRef(platform, 'v.day_id'), 'v.after_order_index', 'v.sequence', 'v.lat', 'v.lng', 'v.created_at']) + .select([ + 'v.id', + columnRef(platform, 'v.day_id'), + 'v.after_order_index', + 'v.sequence', + 'v.lat', + 'v.lng', + 'v.created_at', + ]) .where({ 'd.trip': trip_id }) .orderBy({ 'd.id': 'asc', 'v.after_order_index': 'asc', 'v.sequence': 'asc', 'v.id': 'asc' }) .execute('all', false); @@ -78,7 +89,13 @@ export class RoadtripViasRepository extends TrekRepository { } /** RT5/RT11/TP43 — `INSERT INTO roadtrip_vias (day_id, after_order_index, sequence, lat, lng) VALUES (?×5)`, returning the new id. */ - async insertVia(input: { day_id: number; after_order_index: number; sequence: number; lat: number; lng: number }): Promise { + async insertVia(input: { + day_id: number; + after_order_index: number; + sequence: number; + lat: number; + lng: number; + }): Promise { return await this.insert({ day: input.day_id, after_order_index: input.after_order_index, diff --git a/server/src/db/repositories/RouteUsageDaily.repository.ts b/server/src/db/repositories/RouteUsageDaily.repository.ts index 2b41a8cc0..ce011685f 100644 --- a/server/src/db/repositories/RouteUsageDaily.repository.ts +++ b/server/src/db/repositories/RouteUsageDaily.repository.ts @@ -1,7 +1,7 @@ -import type { RouteUsageDaily } from '../entities/RouteUsageDaily.entity'; import { nowDateOffset } from '../dialect/sql-functions'; -import { TrekRepository } from './_shared/trek-repository'; +import type { RouteUsageDaily } from '../entities/RouteUsageDaily.entity'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** * A `route_usage_daily` row exactly as `SELECT *` reads it (RU3's shape, diff --git a/server/src/db/repositories/SchedulerLeases.repository.ts b/server/src/db/repositories/SchedulerLeases.repository.ts index 3822c0da2..862fdcded 100644 --- a/server/src/db/repositories/SchedulerLeases.repository.ts +++ b/server/src/db/repositories/SchedulerLeases.repository.ts @@ -1,6 +1,6 @@ import type { SchedulerLeases } from '../entities/SchedulerLeases.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** * `scheduler_leases`: which process may run a cron job's tick, and until when @@ -23,7 +23,9 @@ export class SchedulerLeasesRepository extends TrekRepository { oc .column('name') .doUpdateSet({ owner, expires_at: until }) - .where((eb) => eb.or([eb('scheduler_leases.expires_at', '<=', now), eb('scheduler_leases.owner', '=', owner)])), + .where((eb) => + eb.or([eb('scheduler_leases.expires_at', '<=', now), eb('scheduler_leases.owner', '=', owner)]), + ), ) .executeTakeFirst(); return (result.numInsertedOrUpdatedRows ?? 0n) > 0n; diff --git a/server/src/db/repositories/SchoolHolidayCountries.repository.ts b/server/src/db/repositories/SchoolHolidayCountries.repository.ts index 7ffdf69c0..86b0226eb 100644 --- a/server/src/db/repositories/SchoolHolidayCountries.repository.ts +++ b/server/src/db/repositories/SchoolHolidayCountries.repository.ts @@ -1,7 +1,7 @@ import type { SchoolHolidayCountries } from '../entities/SchoolHolidayCountries.entity'; +import type { DB } from '../kysely/db'; import { toRow, type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A `school_holiday_countries` row as the API emits it (SH1/SH3 — `SchoolHolidayCountryRequest`'s exact shape, `@trek/shared`). */ export interface SchoolHolidayCountryRow { diff --git a/server/src/db/repositories/SchoolHolidayPeriods.repository.ts b/server/src/db/repositories/SchoolHolidayPeriods.repository.ts index 0fe2b9838..9b0c2452c 100644 --- a/server/src/db/repositories/SchoolHolidayPeriods.repository.ts +++ b/server/src/db/repositories/SchoolHolidayPeriods.repository.ts @@ -1,6 +1,6 @@ -import type { SchoolHolidayPeriod } from '@trek/shared'; import type { SchoolHolidayPeriods } from '../entities/SchoolHolidayPeriods.entity'; import { TrekRepository } from './_shared/trek-repository'; +import type { SchoolHolidayPeriod } from '@trek/shared'; /** * `school_holiday_periods`'s single-table read/write shape (Plan 3f Task 2, @@ -57,6 +57,13 @@ export class SchoolHolidayPeriodsRepository extends TrekRepository { if (holidays.length === 0) return; - await this.insertMany(holidays.map((holiday) => ({ region: region_id, name: holiday.name, start_date: holiday.startDate, end_date: holiday.endDate }))); + await this.insertMany( + holidays.map((holiday) => ({ + region: region_id, + name: holiday.name, + start_date: holiday.startDate, + end_date: holiday.endDate, + })), + ); } } diff --git a/server/src/db/repositories/SchoolHolidayRegions.repository.ts b/server/src/db/repositories/SchoolHolidayRegions.repository.ts index 3d4233eb0..e2f35acd3 100644 --- a/server/src/db/repositories/SchoolHolidayRegions.repository.ts +++ b/server/src/db/repositories/SchoolHolidayRegions.repository.ts @@ -124,7 +124,10 @@ export class SchoolHolidayRegionsRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); - return this.nativeUpdate({ id, revision: expectedRevision }, { name, revision: columnIncrementedBy(platform, 'revision', 1) }); + return this.nativeUpdate( + { id, revision: expectedRevision }, + { name, revision: columnIncrementedBy(platform, 'revision', 1) }, + ); } /** diff --git a/server/src/db/repositories/ShareTokens.repository.ts b/server/src/db/repositories/ShareTokens.repository.ts index 9b37ed22d..b66fb81d2 100644 --- a/server/src/db/repositories/ShareTokens.repository.ts +++ b/server/src/db/repositories/ShareTokens.repository.ts @@ -1,7 +1,7 @@ -import type { ShareTokens } from '../entities/ShareTokens.entity'; import { currentTimestampKysely } from '../dialect/kysely-functions'; -import { TrekRepository } from './_shared/trek-repository'; +import type { ShareTokens } from '../entities/ShareTokens.entity'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** * `share_tokens` — public trip-share links (Plan 3h Task 6). `trip_id` and @@ -42,7 +42,11 @@ export class ShareTokensRepository extends TrekRepository { * trip_id = ?`. */ async findTokenByTrip(trip_id: number | string): Promise<{ token: string } | undefined> { - return await this.db_().selectFrom('share_tokens').select('token').where('trip_id', '=', trip_id as number).executeTakeFirst(); + return await this.db_() + .selectFrom('share_tokens') + .select('token') + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); } /** @@ -51,16 +55,19 @@ export class ShareTokensRepository extends TrekRepository { * share_collab=?, share_travel_only=?, share_hide_images=?, expires_at=? * WHERE trip_id=?`. */ - async updateFlagsByTrip(trip_id: number | string, data: { - share_map: number; - share_bookings: number; - share_packing: number; - share_budget: number; - share_collab: number; - share_travel_only: number; - share_hide_images: number; - expires_at: string; - }): Promise { + async updateFlagsByTrip( + trip_id: number | string, + data: { + share_map: number; + share_bookings: number; + share_packing: number; + share_budget: number; + share_collab: number; + share_travel_only: number; + share_hide_images: number; + expires_at: string; + }, + ): Promise { await this.nativeUpdate({ trip: trip_id as number }, data); } @@ -102,7 +109,11 @@ export class ShareTokensRepository extends TrekRepository { /** SH4 (`get`) — `SELECT * FROM share_tokens WHERE trip_id = ?`. */ async findRawByTrip(trip_id: number | string): Promise { - return await this.db_().selectFrom('share_tokens').selectAll().where('trip_id', '=', trip_id as number).executeTakeFirst(); + return await this.db_() + .selectFrom('share_tokens') + .selectAll() + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); } /** @@ -143,7 +154,9 @@ export class ShareTokensRepository extends TrekRepository { * SH17 (`getSharedPlacePhotoKey`) — `trip_id, share_map, * share_hide_images` only, SAME valid-token predicate as SH6/R1. */ - async findTripAndShareMapByToken(token: string): Promise<{ trip_id: number; share_map: number | null; share_hide_images: number } | undefined> { + async findTripAndShareMapByToken( + token: string, + ): Promise<{ trip_id: number; share_map: number | null; share_hide_images: number } | undefined> { return await this.validTokenQuery(token).select(['trip_id', 'share_map', 'share_hide_images']).executeTakeFirst(); } diff --git a/server/src/db/repositories/Tags.repository.ts b/server/src/db/repositories/Tags.repository.ts index 9281f0bb2..5484aab3d 100644 --- a/server/src/db/repositories/Tags.repository.ts +++ b/server/src/db/repositories/Tags.repository.ts @@ -1,8 +1,8 @@ import type { Tags } from '../entities/Tags.entity'; +import type { DB } from '../kysely/db'; import { findOwnedByUser, listForOwner } from './_shared/owned-lookup'; import { toRow, type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A `tags` row as the API emits it. */ export interface TagRow { @@ -241,10 +241,7 @@ export class TagsRepository extends TrekRepository { /** PL12 — `DELETE FROM place_tags WHERE place_id = ?` (the replace-all half of an update). */ async deleteForPlace(place_id: number): Promise { - await this.kysely() - .deleteFrom('place_tags') - .where('place_id', '=', place_id) - .execute(); + await this.kysely().deleteFrom('place_tags').where('place_id', '=', place_id).execute(); } // --------------------------------------------------------------------------- diff --git a/server/src/db/repositories/TodoCategoryAssignees.repository.ts b/server/src/db/repositories/TodoCategoryAssignees.repository.ts index 5fa17d1cb..358b3f100 100644 --- a/server/src/db/repositories/TodoCategoryAssignees.repository.ts +++ b/server/src/db/repositories/TodoCategoryAssignees.repository.ts @@ -1,6 +1,6 @@ import type { TodoCategoryAssignees } from '../entities/TodoCategoryAssignees.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** TD11/TD14's joined projection — the assignee row plus the display fields the picker needs. */ export interface TodoCategoryAssigneeRow { diff --git a/server/src/db/repositories/TodoItems.repository.ts b/server/src/db/repositories/TodoItems.repository.ts index bdfa5620d..fc20978cc 100644 --- a/server/src/db/repositories/TodoItems.repository.ts +++ b/server/src/db/repositories/TodoItems.repository.ts @@ -1,9 +1,9 @@ +import { columnRef, currentTimestamp, nowMinusHours } from '../dialect/sql-functions'; import type { TodoItems } from '../entities/TodoItems.entity'; +import type { DB } from '../kysely/db'; +import { presenceSet } from './_shared/presence-set'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import { presenceSet } from './_shared/presence-set'; -import { columnRef, currentTimestamp, nowMinusHours } from '../dialect/sql-functions'; -import type { DB } from '../kysely/db'; /** A bare `todo_items` row — every scalar column, incl. the two `persist(false)` relation mirrors (`trip_id`, `assigned_user_id`). */ export interface TodoItemRow { @@ -59,7 +59,11 @@ export class TodoItemsRepository extends TrekRepository { /** TD2 (`createItem`) — `SELECT MAX(sort_order) as max FROM todo_items WHERE trip_id = ?`. */ async maxSortOrder(trip_id: number | string): Promise { - const row = await this.kysely().selectFrom('todo_items').select((eb) => eb.fn.max('sort_order').as('max')).where('trip_id', '=', trip_id as number).executeTakeFirst(); + const row = await this.kysely() + .selectFrom('todo_items') + .select((eb) => eb.fn.max('sort_order').as('max')) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); return row?.max ?? null; } @@ -72,8 +76,14 @@ export class TodoItemsRepository extends TrekRepository { * different signature: an unpersisted-entity builder, not an INSERT). */ async insertItem(row: { - trip_id: number | string; name: string; category: string | null; sort_order: number; - due_date: string | null; description: string | null; assigned_user_id: number | null; priority: number; + trip_id: number | string; + name: string; + category: string | null; + sort_order: number; + due_date: string | null; + description: string | null; + assigned_user_id: number | null; + priority: number; }): Promise { const inserted = await this.kysely() .insertInto('todo_items') @@ -85,7 +95,11 @@ export class TodoItemsRepository extends TrekRepository { /** TD4/TD7 (`createItem`/`updateItem`'s post-write re-select) — `SELECT * FROM todo_items WHERE id = ?` (no trip filter — the caller already knows `id` is in-scope, having just written it). */ async findById(id: number | string): Promise { - return await this.kysely().selectFrom('todo_items').selectAll().where('id', '=', id as number).executeTakeFirst(); + return await this.kysely() + .selectFrom('todo_items') + .selectAll() + .where('id', '=', id as number) + .executeTakeFirst(); } /** @@ -97,12 +111,22 @@ export class TodoItemsRepository extends TrekRepository { * a separate, still-accepted carry — see `todo.service.ts`'s own note). */ async findInTrip(id: number, trip_id: number | string): Promise { - return await this.kysely().selectFrom('todo_items').selectAll().where('id', '=', id).where('trip_id', '=', trip_id as number).executeTakeFirst(); + return await this.kysely() + .selectFrom('todo_items') + .selectAll() + .where('id', '=', id) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); } /** TD8 (`deleteItem`'s trip-scoping guard) — `SELECT id FROM todo_items WHERE id = ? AND trip_id = ?`. `id: number`, same Plan 4 Task 8b narrowing as {@link findInTrip}. */ async existsInTrip(id: number, trip_id: number | string): Promise<{ id: number } | undefined> { - return await this.kysely().selectFrom('todo_items').select('id').where('id', '=', id).where('trip_id', '=', trip_id as number).executeTakeFirst(); + return await this.kysely() + .selectFrom('todo_items') + .select('id') + .where('id', '=', id) + .where('trip_id', '=', trip_id as number) + .executeTakeFirst(); } /** @@ -114,18 +138,26 @@ export class TodoItemsRepository extends TrekRepository { * exactly); every other column is a true presence sentinel bound off the * legacy `bodyKeys` array (`present = bodyKeys.includes('')`). */ - async update(id: number, write: { - name?: readonly [present: boolean, value: string]; - checked?: readonly [present: boolean, value: number]; - category?: readonly [present: boolean, value: string | null]; - due_date?: readonly [present: boolean, value: string | null]; - description?: readonly [present: boolean, value: string | null]; - assigned_user_id?: readonly [present: boolean, value: number | null]; - priority?: readonly [present: boolean, value: number]; - }): Promise { + async update( + id: number, + write: { + name?: readonly [present: boolean, value: string]; + checked?: readonly [present: boolean, value: number]; + category?: readonly [present: boolean, value: string | null]; + due_date?: readonly [present: boolean, value: string | null]; + description?: readonly [present: boolean, value: string | null]; + assigned_user_id?: readonly [present: boolean, value: number | null]; + priority?: readonly [present: boolean, value: number]; + }, + ): Promise { const data = presenceSet<{ - name: string; checked: number; category: string | null; due_date: string | null; - description: string | null; assigned_user_id: number | null; priority: number; + name: string; + checked: number; + category: string | null; + due_date: string | null; + description: string | null; + assigned_user_id: number | null; + priority: number; }>(write); if (Object.keys(data).length === 0) return; await this.kysely().updateTable('todo_items').set(data).where('id', '=', id).execute(); @@ -138,12 +170,21 @@ export class TodoItemsRepository extends TrekRepository { /** TD10 (`reorderItems`, looped) — `UPDATE todo_items SET sort_order = ? WHERE id = ? AND trip_id = ?`. */ async setSortOrder(id: number | string, trip_id: number | string, sort_order: number): Promise { - await this.kysely().updateTable('todo_items').set({ sort_order }).where('id', '=', id as number).where('trip_id', '=', trip_id as number).execute(); + await this.kysely() + .updateTable('todo_items') + .set({ sort_order }) + .where('id', '=', id as number) + .where('trip_id', '=', trip_id as number) + .execute(); } /** TP72 (`TripsService.copy`) — `SELECT * FROM todo_items WHERE trip_id = ?`. */ async listAllForTrip(trip_id: number | string): Promise { - return await this.kysely().selectFrom('todo_items').selectAll().where('trip_id', '=', trip_id as number).execute(); + return await this.kysely() + .selectFrom('todo_items') + .selectAll() + .where('trip_id', '=', trip_id as number) + .execute(); } /** @@ -155,8 +196,13 @@ export class TodoItemsRepository extends TrekRepository { * has no roster yet (`TripsService.copy`'s own comment, kept exactly). */ async insertCopy(row: { - trip_id: number | string; name: string; category: string | null; sort_order: number | null; - due_date: string | null; description: string | null; priority: number | null; + trip_id: number | string; + name: string; + category: string | null; + sort_order: number | null; + due_date: string | null; + description: string | null; + priority: number | null; }): Promise { const inserted = await this.kysely() .insertInto('todo_items') @@ -213,7 +259,16 @@ export class TodoItemsRepository extends TrekRepository { const platform = this.getEntityManager().getPlatform(); const rows = await this.qb('ti') .join('ti.trip', 't') - .select(['ti.id', columnRef(platform, 'ti.trip_id'), 'ti.name', 'ti.due_date', columnRef(platform, 'ti.assigned_user_id'), 'ti.reminded_at', 't.title', 't.user']) + .select([ + 'ti.id', + columnRef(platform, 'ti.trip_id'), + 'ti.name', + 'ti.due_date', + columnRef(platform, 'ti.assigned_user_id'), + 'ti.reminded_at', + 't.title', + 't.user', + ]) .andWhere({ checked: 0 }) .andWhere({ due_date: { $ne: null } }) .andWhere({ due_date: { $ne: '' } }) diff --git a/server/src/db/repositories/TourWaypoints.repository.ts b/server/src/db/repositories/TourWaypoints.repository.ts index fbc3be0d2..10e4bb7d7 100644 --- a/server/src/db/repositories/TourWaypoints.repository.ts +++ b/server/src/db/repositories/TourWaypoints.repository.ts @@ -1,6 +1,6 @@ import type { TourWaypoints } from '../entities/TourWaypoints.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** One saved routing control point of a tour, in route order. */ export interface TourWaypointRow { diff --git a/server/src/db/repositories/Tours.repository.ts b/server/src/db/repositories/Tours.repository.ts index dd147376c..18645d360 100644 --- a/server/src/db/repositories/Tours.repository.ts +++ b/server/src/db/repositories/Tours.repository.ts @@ -1,7 +1,7 @@ -import type { AssertRowKeys } from './_shared/rows'; import type { Tours } from '../entities/Tours.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import type { AssertRowKeys } from './_shared/rows'; +import { TrekRepository } from './_shared/trek-repository'; /** * A tour as the Tours list and detail read it: the `tours` facet joined with @@ -90,8 +90,12 @@ export class ToursRepository extends TrekRepository { 't.wanderer_ref as wanderer_ref', 't.match_confidence as match_confidence', 't.max_hiking_difficulty as max_hiking_difficulty', - eb.exists(eb.selectFrom('day_assignments as da').select('da.id').whereRef('da.place_id', '=', 'p.id')).as('planned'), - eb.exists(eb.selectFrom('tour_waypoints as tw').select('tw.id').whereRef('tw.place_id', '=', 'p.id')).as('has_waypoints'), + eb + .exists(eb.selectFrom('day_assignments as da').select('da.id').whereRef('da.place_id', '=', 'p.id')) + .as('planned'), + eb + .exists(eb.selectFrom('tour_waypoints as tw').select('tw.id').whereRef('tw.place_id', '=', 'p.id')) + .as('has_waypoints'), ]); } @@ -118,7 +122,11 @@ export class ToursRepository extends TrekRepository { /** TO3 — `SELECT 1 FROM tours WHERE place_id = ?`: whether the place is a tour. */ async existsForPlace(place_id: number): Promise { - const row = await this.db().selectFrom('tours').select('place_id').where('place_id', '=', place_id).executeTakeFirst(); + const row = await this.db() + .selectFrom('tours') + .select('place_id') + .where('place_id', '=', place_id) + .executeTakeFirst(); return !!row; } @@ -209,6 +217,8 @@ export class ToursRepository extends TrekRepository { } } -function toListRow & { planned: unknown; has_waypoints: unknown }>(r: R): TourListRow { +function toListRow< + R extends Omit & { planned: unknown; has_waypoints: unknown }, +>(r: R): TourListRow { return { ...r, planned: Number(r.planned), has_waypoints: Number(r.has_waypoints) }; } diff --git a/server/src/db/repositories/TrekPhotoCacheMeta.repository.ts b/server/src/db/repositories/TrekPhotoCacheMeta.repository.ts index 6dfbb54e7..6bfc04b75 100644 --- a/server/src/db/repositories/TrekPhotoCacheMeta.repository.ts +++ b/server/src/db/repositories/TrekPhotoCacheMeta.repository.ts @@ -33,7 +33,10 @@ export class TrekPhotoCacheMetaRepository extends TrekRepository { - await this.upsert({ cache_key, content_type, fetched_at }, { onConflictFields: ['cache_key'], onConflictAction: 'merge' }); + await this.upsert( + { cache_key, content_type, fetched_at }, + { onConflictFields: ['cache_key'], onConflictAction: 'merge' }, + ); } /** TC5 — `SELECT cache_key FROM trek_photo_cache_meta WHERE fetched_at < ?`, `sweepExpired`'s stale-row pass. */ diff --git a/server/src/db/repositories/TrekPhotos.repository.ts b/server/src/db/repositories/TrekPhotos.repository.ts index 4d6b12e79..c37e95681 100644 --- a/server/src/db/repositories/TrekPhotos.repository.ts +++ b/server/src/db/repositories/TrekPhotos.repository.ts @@ -1,6 +1,6 @@ import type { TrekPhoto } from '../../types'; -import type { TrekPhotos } from '../entities/TrekPhotos.entity'; import { coalesceParam } from '../dialect/sql-functions'; +import type { TrekPhotos } from '../entities/TrekPhotos.entity'; import { toRow, type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; @@ -93,11 +93,14 @@ export class TrekPhotosRepository extends TrekRepository { */ async patchThumbnail(id: number, thumbnail_path: string, width: number, height: number): Promise { const platform = this.getEntityManager().getPlatform(); - await this.nativeUpdate({ id }, { - thumbnail_path, - width: coalesceParam(platform, 'width', width), - height: coalesceParam(platform, 'height', height), - }); + await this.nativeUpdate( + { id }, + { + thumbnail_path, + width: coalesceParam(platform, 'width', width), + height: coalesceParam(platform, 'height', height), + }, + ); } /** @@ -116,18 +119,26 @@ export class TrekPhotosRepository extends TrekRepository { * built as an `$or` over just the columns that bring a value; with none of * them there is nothing to learn and no statement is issued. */ - async patchCaptureMetadata(id: number, taken_at: string | null, lat: number | null, lng: number | null): Promise { + async patchCaptureMetadata( + id: number, + taken_at: string | null, + lat: number | null, + lng: number | null, + ): Promise { const learns: Array<{ taken_at: null } | { lat: null } | { lng: null }> = []; if (taken_at != null) learns.push({ taken_at: null }); if (lat != null) learns.push({ lat: null }); if (lng != null) learns.push({ lng: null }); if (learns.length === 0) return false; const platform = this.getEntityManager().getPlatform(); - const changed = await this.nativeUpdate({ id, $or: learns }, { - taken_at: coalesceParam(platform, 'taken_at', taken_at), - lat: coalesceParam(platform, 'lat', lat), - lng: coalesceParam(platform, 'lng', lng), - }); + const changed = await this.nativeUpdate( + { id, $or: learns }, + { + taken_at: coalesceParam(platform, 'taken_at', taken_at), + lat: coalesceParam(platform, 'lat', lat), + lng: coalesceParam(platform, 'lng', lng), + }, + ); return changed > 0; } diff --git a/server/src/db/repositories/TripAlbumLinks.repository.ts b/server/src/db/repositories/TripAlbumLinks.repository.ts index 7691feb8d..a91e839b3 100644 --- a/server/src/db/repositories/TripAlbumLinks.repository.ts +++ b/server/src/db/repositories/TripAlbumLinks.repository.ts @@ -1,7 +1,7 @@ -import type { TripAlbumLinks } from '../entities/TripAlbumLinks.entity'; import { currentTimestamp } from '../dialect/sql-functions'; -import { TrekRepository } from './_shared/trek-repository'; +import type { TripAlbumLinks } from '../entities/TripAlbumLinks.entity'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** MA7/MA8's shared row shape — album_id always needed, passphrase only by the sync path (MA8). */ export interface TripAlbumLinkSyncRow { @@ -81,10 +81,24 @@ export class TripAlbumLinksRepository extends TrekRepository { * equivalent of the legacy `OR IGNORE`. Returns whether a row was actually * inserted (legacy `result.changes === 0` drove the "already linked" 409). */ - async insertIgnore(row: { trip_id: number | string; user_id: number; provider: string; album_id: string; album_name: string; passphrase: string | null }): Promise { + async insertIgnore(row: { + trip_id: number | string; + user_id: number; + provider: string; + album_id: string; + album_name: string; + passphrase: string | null; + }): Promise { const result = await this.kysely() .insertInto('trip_album_links') - .values({ trip_id: row.trip_id as number, user_id: row.user_id, provider: row.provider, album_id: row.album_id, album_name: row.album_name, passphrase: row.passphrase }) + .values({ + trip_id: row.trip_id as number, + user_id: row.user_id, + provider: row.provider, + album_id: row.album_id, + album_name: row.album_name, + passphrase: row.passphrase, + }) .onConflict((oc) => oc.columns(['trip_id', 'user_id', 'provider', 'album_id']).doNothing()) .executeTakeFirst(); return (result?.numInsertedOrUpdatedRows ?? 0n) > 0n; diff --git a/server/src/db/repositories/TripDocumentLinks.repository.ts b/server/src/db/repositories/TripDocumentLinks.repository.ts index 418f8ccbc..b271b1d4f 100644 --- a/server/src/db/repositories/TripDocumentLinks.repository.ts +++ b/server/src/db/repositories/TripDocumentLinks.repository.ts @@ -74,7 +74,11 @@ export class TripDocumentLinksRepository extends TrekRepository { + async dueLinks( + circuitOpenAfter: number, + enabledProviderIds: readonly string[], + limit: number, + ): Promise { if (enabledProviderIds.length === 0) return []; const platform = this.getEntityManager().getPlatform(); const rows = await this.qb('l') @@ -98,7 +102,11 @@ export class TripDocumentLinksRepository extends TrekRepository { - return await this.qb('l').select(['l.*']).where({ trip_id: tripId }).orderBy({ id: 'asc' }).execute('all', false); + return await this.qb('l') + .select(['l.*']) + .where({ trip_id: tripId }) + .orderBy({ id: 'asc' }) + .execute('all', false); } /** DSC13 (`getLink`) — `SELECT * FROM trip_document_links WHERE id = ?`. */ @@ -108,11 +116,18 @@ export class TripDocumentLinksRepository extends TrekRepository { - return await this.qb('l').select(['l.*']).where({ webhook_token: token }).execute('get', false); + return await this.qb('l') + .select(['l.*']) + .where({ webhook_token: token }) + .execute('get', false); } /** DSC15 (`createLink`'s pre-check) — `SELECT * FROM trip_document_links WHERE trip_id = ? AND connection_id = ? AND remote_scope_key = ?`. */ - async findByTripConnectionScope(tripId: number, connectionId: number, scopeKey: string): Promise { + async findByTripConnectionScope( + tripId: number, + connectionId: number, + scopeKey: string, + ): Promise { return await this.qb('l') .select(['l.*']) .where({ trip_id: tripId, connection_id: connectionId, remote_scope_key: scopeKey }) @@ -208,7 +223,10 @@ export class TripDocumentLinksRepository extends TrekRepository { + async recordSuccess( + id: number, + data: { remote_cursor: string | null; state: string; error_code: string | null }, + ): Promise { const platform = this.getEntityManager().getPlatform(); await this.nativeUpdate( { id }, @@ -235,7 +253,10 @@ export class TripDocumentLinksRepository extends TrekRepository { + async recordFailure( + id: number, + data: { state: string; error_code: string; failure_count: number; next_attempt_seconds: number }, + ): Promise { const platform = this.getEntityManager().getPlatform(); await this.nativeUpdate( { id }, diff --git a/server/src/db/repositories/TripFiles.repository.ts b/server/src/db/repositories/TripFiles.repository.ts index 4d5d90772..3fdfccb98 100644 --- a/server/src/db/repositories/TripFiles.repository.ts +++ b/server/src/db/repositories/TripFiles.repository.ts @@ -1,8 +1,8 @@ import { currentTimestamp } from '../dialect/sql-functions'; import type { TripFiles } from '../entities/TripFiles.entity'; +import type { DB } from '../kysely/db'; import { type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** * A bare `trip_files` row — every scalar column of the entity, incl. the SIX @@ -169,8 +169,14 @@ export class TripFilesRepository extends TrekRepository { * calling this (matching the legacy inline ternaries) — this method just * writes the three final values verbatim. */ - async updateFile(id: number, write: { description: string | null; place_id: number | null; reservation_id: number | null }): Promise { - await this.nativeUpdate({ id }, { description: write.description, place: write.place_id, reservation: write.reservation_id }); + async updateFile( + id: number, + write: { description: string | null; place_id: number | null; reservation_id: number | null }, + ): Promise { + await this.nativeUpdate( + { id }, + { description: write.description, place: write.place_id, reservation: write.reservation_id }, + ); } /** FL16 (`FilesService.toggleStarred`) — `UPDATE trip_files SET starred = ? WHERE id = ?`. */ @@ -219,7 +225,11 @@ export class TripFilesRepository extends TrekRepository { * Kysely for the same reason, so this one does too). */ async findOriginalName(id: number): Promise { - const row = await this.kysely().selectFrom('trip_files').select('original_name').where('id', '=', id).executeTakeFirst(); + const row = await this.kysely() + .selectFrom('trip_files') + .select('original_name') + .where('id', '=', id) + .executeTakeFirst(); return row?.original_name; } @@ -230,7 +240,15 @@ export class TripFilesRepository extends TrekRepository { * Excludes chat and note attachments — a binding mirrors the trip's * PAPERWORK, not its conversation. */ - async listSyncableForTrip(trip_id: number): Promise> { + async listSyncableForTrip(trip_id: number): Promise< + Array<{ + id: number; + original_name: string; + file_size: number | null; + mime_type: string | null; + deleted_at: string | null; + }> + > { return await this.kysely() .selectFrom('trip_files') .select(['id', 'original_name', 'file_size', 'mime_type', 'deleted_at']) diff --git a/server/src/db/repositories/TripInviteTokens.repository.ts b/server/src/db/repositories/TripInviteTokens.repository.ts index b304907b3..467b6ca32 100644 --- a/server/src/db/repositories/TripInviteTokens.repository.ts +++ b/server/src/db/repositories/TripInviteTokens.repository.ts @@ -1,7 +1,7 @@ -import type { TripInviteTokens } from '../entities/TripInviteTokens.entity'; import { currentTimestamp } from '../dialect/sql-functions'; -import { TrekRepository } from './_shared/trek-repository'; +import type { TripInviteTokens } from '../entities/TripInviteTokens.entity'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** `trip-invite.service.ts::get`'s projection. */ export interface TripInviteInfoRow { @@ -57,10 +57,18 @@ export class TripInviteTokensRepository extends TrekRepository * `CURRENT_TIMESTAMP` through the shared dialect helper (rule 5), matching * the column's own `defaultRaw` literal exactly. */ - async updateForTrip(trip_id: number | string, input: { token: string; expires_at: string | null; created_by: number }): Promise { + async updateForTrip( + trip_id: number | string, + input: { token: string; expires_at: string | null; created_by: number }, + ): Promise { const platform = this.getEntityManager().getPlatform(); await this.qb('t') - .update({ token: input.token, expires_at: input.expires_at, createdByRef: input.created_by, created_at: currentTimestamp(platform) }) + .update({ + token: input.token, + expires_at: input.expires_at, + createdByRef: input.created_by, + created_at: currentTimestamp(platform), + }) .where('t.trip_id = ?', [trip_id]) .execute('run'); } @@ -78,7 +86,12 @@ export class TripInviteTokensRepository extends TrekRepository * `Number(...)` here mirrors the same seam `DayNotesService.create`'s own * `Number(dayId)`/`Number(tripId)` conversion documents. */ - async insertForTrip(input: { trip_id: number | string; token: string; created_by: number; expires_at: string | null }): Promise { + async insertForTrip(input: { + trip_id: number | string; + token: string; + created_by: number; + expires_at: string | null; + }): Promise { await this.insert({ trip: Number(input.trip_id), token: input.token, @@ -89,10 +102,7 @@ export class TripInviteTokensRepository extends TrekRepository /** `remove()` — `DELETE FROM trip_invite_tokens WHERE trip_id = ?`. */ async deleteByTrip(trip_id: number | string): Promise { - await this.qb('t') - .delete() - .where('t.trip_id = ?', [trip_id]) - .execute('run'); + await this.qb('t').delete().where('t.trip_id = ?', [trip_id]).execute('run'); } /** diff --git a/server/src/db/repositories/TripMembers.repository.ts b/server/src/db/repositories/TripMembers.repository.ts index 6e94289b1..3aa843861 100644 --- a/server/src/db/repositories/TripMembers.repository.ts +++ b/server/src/db/repositories/TripMembers.repository.ts @@ -1,6 +1,6 @@ +import { caseWhenEquals, coalesce } from '../dialect/sql-functions'; import type { TripMembers } from '../entities/TripMembers.entity'; import { Trips } from '../entities/Trips.entity'; -import { caseWhenEquals, coalesce } from '../dialect/sql-functions'; import { TrekRepository } from './_shared/trek-repository'; /** @@ -263,11 +263,7 @@ export class TripMembersRepository extends TrekRepository { */ async remove(trip_id: number, user_id: number): Promise { if (!Number.isFinite(user_id)) return; - await this.qb('m') - .delete() - .where({ trip: trip_id }) - .andWhere('m.user_id = ?', [user_id]) - .execute('run'); + await this.qb('m').delete().where({ trip: trip_id }).andWhere('m.user_id = ?', [user_id]).execute('run'); } // --------------------------------------------------------------------------- @@ -287,7 +283,9 @@ export class TripMembersRepository extends TrekRepository { * `trips.members` and the REST/MCP roster are two different wire shapes on * purpose, inventory §18.10). */ - async listRawUsernameAndDisplayName(trip_id: number | string): Promise<{ id: number; username: string; display_name: string | null; avatar: string | null }[]> { + async listRawUsernameAndDisplayName( + trip_id: number | string, + ): Promise<{ id: number; username: string; display_name: string | null; avatar: string | null }[]> { return await this.qb('m') .join('m.user', 'u') .select(['u.id', 'u.username', 'u.display_name', 'u.avatar']) diff --git a/server/src/db/repositories/TripPhotos.repository.ts b/server/src/db/repositories/TripPhotos.repository.ts index 3ee3e7d22..3619578f2 100644 --- a/server/src/db/repositories/TripPhotos.repository.ts +++ b/server/src/db/repositories/TripPhotos.repository.ts @@ -1,6 +1,6 @@ import type { TripPhotos } from '../entities/TripPhotos.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** * `trip_photos`'s single-table writes and lookups (Plan 3e Task 7, @@ -58,7 +58,13 @@ export class TripPhotosRepository extends TrekRepository { const row = await this.qb('tp') .select(['tp.id']) .join('tp.photo', 'tkp') - .where({ 'tp.user': user_id, 'tp.trip': trip_id, 'tp.shared': 1, 'tkp.asset_id': asset_id, 'tkp.provider': provider }) + .where({ + 'tp.user': user_id, + 'tp.trip': trip_id, + 'tp.shared': 1, + 'tkp.asset_id': asset_id, + 'tkp.provider': provider, + }) .execute<{ id: number } | undefined>('get', false); return !!row; } @@ -98,10 +104,22 @@ export class TripPhotosRepository extends TrekRepository { * is Kysely's dialect-general affected-row count, 0 when the conflict * target already existed and the insert was ignored. */ - async insertIgnore(row: { trip_id: number | string; user_id: number; photo_id: number; shared: number; album_link_id: number | string | null }): Promise { + async insertIgnore(row: { + trip_id: number | string; + user_id: number; + photo_id: number; + shared: number; + album_link_id: number | string | null; + }): Promise { const result = await this.kysely() .insertInto('trip_photos') - .values({ trip_id: row.trip_id as number, user_id: row.user_id, photo_id: row.photo_id, shared: row.shared, album_link_id: row.album_link_id as number }) + .values({ + trip_id: row.trip_id as number, + user_id: row.user_id, + photo_id: row.photo_id, + shared: row.shared, + album_link_id: row.album_link_id as number, + }) .onConflict((oc) => oc.columns(['trip_id', 'user_id', 'photo_id']).doNothing()) .executeTakeFirst(); return (result?.numInsertedOrUpdatedRows ?? 0n) > 0n; @@ -158,7 +176,11 @@ export class TripPhotosRepository extends TrekRepository { * this is ever reached); the early return here is defensive, matching an * `IN ()` that can never match any row. */ - async listForTrip(trip_id: number | string, user_id: number, enabled_providers: string[]): Promise { + async listForTrip( + trip_id: number | string, + user_id: number, + enabled_providers: string[], + ): Promise { if (enabled_providers.length === 0) return []; const rows = await this.kysely() .selectFrom('trip_photos as tp') diff --git a/server/src/db/repositories/UserNoticeDismissals.repository.ts b/server/src/db/repositories/UserNoticeDismissals.repository.ts index b163da495..e6977189a 100644 --- a/server/src/db/repositories/UserNoticeDismissals.repository.ts +++ b/server/src/db/repositories/UserNoticeDismissals.repository.ts @@ -65,7 +65,12 @@ export class UserNoticeDismissalsRepository extends TrekRepository { + async upsertDismissal( + userId: number, + noticeId: string, + dismissedAt: number, + dismissedAppVersion: string, + ): Promise { await this.upsert( { user: userId, notice_id: noticeId, dismissed_at: dismissedAt, dismissed_app_version: dismissedAppVersion }, { onConflictFields: ['user', 'notice_id'], onConflictAction: 'merge' }, diff --git a/server/src/db/repositories/UserSessions.repository.ts b/server/src/db/repositories/UserSessions.repository.ts index f65ef386b..24b4fd1d6 100644 --- a/server/src/db/repositories/UserSessions.repository.ts +++ b/server/src/db/repositories/UserSessions.repository.ts @@ -63,14 +63,16 @@ export class UserSessionsRepository extends TrekRepository { */ async insertSessionIfAbsent(row: NewUserSessionRow): Promise { await this.upsertMany( - [{ - id: row.id, - user: row.user_id, - created_at: row.created_at, - last_seen_at: row.created_at, - expires_at: row.expires_at, - user_agent: row.user_agent, - }], + [ + { + id: row.id, + user: row.user_id, + created_at: row.created_at, + last_seen_at: row.created_at, + expires_at: row.expires_at, + user_agent: row.user_agent, + }, + ], { onConflictFields: ['id'], onConflictAction: 'ignore' }, ); } diff --git a/server/src/db/repositories/VacayCompanyHolidays.repository.ts b/server/src/db/repositories/VacayCompanyHolidays.repository.ts index 3b4f2e62f..7a4716606 100644 --- a/server/src/db/repositories/VacayCompanyHolidays.repository.ts +++ b/server/src/db/repositories/VacayCompanyHolidays.repository.ts @@ -17,14 +17,27 @@ export class VacayCompanyHolidaysRepository extends TrekRepository= ? AND date < ? ORDER BY date` (`getSharedCalendars`) and `SELECT * FROM vacay_company_holidays WHERE plan_id = ? AND date >= ? AND date < ?` (`getEntries`, unordered, full row). Two distinct statements — two methods. */ async listDatesForRange(planId: number, start: string, end: string): Promise<{ date: string; fraction: number }[]> { - const rows = await this.find({ plan: planId, date: { $gte: start, $lt: end } }, { fields: ['date', 'fraction'], orderBy: { date: 'asc' } }); + const rows = await this.find( + { plan: planId, date: { $gte: start, $lt: end } }, + { fields: ['date', 'fraction'], orderBy: { date: 'asc' } }, + ); return rows.map((row) => ({ date: row.date, fraction: row.fraction })); } /** VC112 — `SELECT * FROM vacay_company_holidays WHERE plan_id = ? AND date >= ? AND date < ?` (`getEntries`, full row, unordered). */ - async listForRange(planId: number, start: string, end: string): Promise<{ id: number; plan_id: number; date: string; note: string | null; fraction: number }[]> { + async listForRange( + planId: number, + start: string, + end: string, + ): Promise<{ id: number; plan_id: number; date: string; note: string | null; fraction: number }[]> { const rows = await this.find({ plan: planId, date: { $gte: start, $lt: end } }); - return rows.map((row) => ({ id: row.id, plan_id: row.plan_id, date: row.date, note: row.note ?? null, fraction: row.fraction })); + return rows.map((row) => ({ + id: row.id, + plan_id: row.plan_id, + date: row.date, + note: row.note ?? null, + fraction: row.fraction, + })); } /** VC24-sibling: VC25 — `DELETE FROM vacay_company_holidays WHERE plan_id = ? AND date = ?` (`applyHolidayCalendars`'s auto-clear). */ @@ -39,7 +52,10 @@ export class VacayCompanyHolidaysRepository extends TrekRepository { - await this.upsert({ plan: planId, date, note, fraction }, { onConflictFields: ['plan', 'date'], onConflictAction: 'ignore' }); + await this.upsert( + { plan: planId, date, note, fraction }, + { onConflictFields: ['plan', 'date'], onConflictAction: 'ignore' }, + ); } /** diff --git a/server/src/db/repositories/VacayEntries.repository.ts b/server/src/db/repositories/VacayEntries.repository.ts index 7ba83e121..ccc9574d8 100644 --- a/server/src/db/repositories/VacayEntries.repository.ts +++ b/server/src/db/repositories/VacayEntries.repository.ts @@ -1,7 +1,7 @@ import { dateAdd } from '../dialect/sql-functions'; import type { VacayEntries } from '../entities/VacayEntries.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; type VacayEntriesKyselyDB = Pick; @@ -39,7 +39,14 @@ export class VacayEntriesRepository extends TrekRepository { async sumFraction(userId: number, planId: number, start: string, end: string): Promise { const row = await this.kysely() .selectFrom('vacay_entries') - .select((eb) => eb.fn.coalesce(eb.fn.sum(eb.case().when('kind', '=', 'comp').then(0).else(eb.ref('fraction')).end()), eb.val(0)).as('used')) + .select((eb) => + eb.fn + .coalesce( + eb.fn.sum(eb.case().when('kind', '=', 'comp').then(0).else(eb.ref('fraction')).end()), + eb.val(0), + ) + .as('used'), + ) .where('user_id', '=', userId) .where('plan_id', '=', planId) .where('date', '>=', start) @@ -96,7 +103,10 @@ export class VacayEntriesRepository extends TrekRepository { // (user, plan, date) index order instead, so the per-row collision check // above skips different rows than legacy did on consecutive days inserted // out of id order. - const rows = await this.find({ plan: planId, user: userId, date: { $gte: start, $lte: end } }, { fields: ['id', 'date'], orderBy: { id: 'asc' } }); + const rows = await this.find( + { plan: planId, user: userId, date: { $gte: start, $lte: end } }, + { fields: ['id', 'date'], orderBy: { id: 'asc' } }, + ); for (const row of rows) { const shiftedDate = shiftIsoDate(row.date, offset); const collision = await this.findOne({ plan: planId, user: userId, date: shiftedDate }, { fields: ['id'] }); @@ -159,9 +169,17 @@ export class VacayEntriesRepository extends TrekRepository { return this.kysely() .selectFrom('vacay_entries as e') .innerJoin('users as u', 'u.id', 'e.user_id') - .leftJoin('vacay_user_colors as c', (join) => join.onRef('c.user_id', '=', 'e.user_id').onRef('c.plan_id', '=', 'e.plan_id')) + .leftJoin('vacay_user_colors as c', (join) => + join.onRef('c.user_id', '=', 'e.user_id').onRef('c.plan_id', '=', 'e.plan_id'), + ) .select((eb) => [ - 'e.id', 'e.plan_id', 'e.user_id', 'e.date', 'e.note', 'e.fraction', 'e.kind', + 'e.id', + 'e.plan_id', + 'e.user_id', + 'e.date', + 'e.note', + 'e.fraction', + 'e.kind', 'u.username as person_name', eb.fn.coalesce('c.color', eb.val('#6366f1')).as('person_color'), ]) @@ -172,7 +190,12 @@ export class VacayEntriesRepository extends TrekRepository { } /** VC92 (`getSharedCalendars`) — `SELECT date, fraction, kind FROM vacay_entries WHERE plan_id = ? AND user_id = ? AND date >= ? AND date < ? ORDER BY date`, over the OWNER's plan and the VIEWER's leave-year window. */ - async listForOwnerRange(planId: number, userId: number, start: string, end: string): Promise<{ date: string; fraction: number; kind: string | null }[]> { + async listForOwnerRange( + planId: number, + userId: number, + start: string, + end: string, + ): Promise<{ date: string; fraction: number; kind: string | null }[]> { const rows = await this.find( { plan: planId, user: userId, date: { $gte: start, $lt: end } }, { fields: ['date', 'fraction', 'kind'], orderBy: { date: 'asc' } }, @@ -186,7 +209,11 @@ export class VacayEntriesRepository extends TrekRepository { * Named `findByUserDatePlan`, not `find` — `EntityRepository#find` already * exists with an incompatible signature. */ - async findByUserDatePlan(userId: number, date: string, planId: number): Promise<{ id: number; fraction: number; kind: string | null } | null> { + async findByUserDatePlan( + userId: number, + date: string, + planId: number, + ): Promise<{ id: number; fraction: number; kind: string | null } | null> { const row = await this.findOne({ user: userId, date, plan: planId }, { fields: ['id', 'fraction', 'kind'] }); return row ? { id: row.id, fraction: row.fraction, kind: row.kind ?? null } : null; } @@ -202,7 +229,14 @@ export class VacayEntriesRepository extends TrekRepository { } /** VC117 — `INSERT INTO vacay_entries (plan_id, user_id, date, note, fraction, kind) VALUES (?, ?, ?, ?, ?, ?)` (`toggleEntry`'s new entry — `note` is always `''` from this path). */ - async insertEntry(planId: number, userId: number, date: string, note: string, fraction: number, kind: string): Promise { + async insertEntry( + planId: number, + userId: number, + date: string, + note: string, + fraction: number, + kind: string, + ): Promise { await this.insert({ plan: planId, user: userId, date, note, fraction, kind }); } } diff --git a/server/src/db/repositories/VacayHolidayCalendars.repository.ts b/server/src/db/repositories/VacayHolidayCalendars.repository.ts index 3ffe6d2e5..7a16f3e96 100644 --- a/server/src/db/repositories/VacayHolidayCalendars.repository.ts +++ b/server/src/db/repositories/VacayHolidayCalendars.repository.ts @@ -55,7 +55,10 @@ export class VacayHolidayCalendarsRepository extends TrekRepository { - const rows = await this.find({ plan: planId, type: 'public_holiday' }, { orderBy: { sort_order: 'asc', id: 'asc' } }); + const rows = await this.find( + { plan: planId, type: 'public_holiday' }, + { orderBy: { sort_order: 'asc', id: 'asc' } }, + ); return rows.map((row) => toRow(row) as VacayHolidayCalendarRow); } @@ -87,7 +90,14 @@ export class VacayHolidayCalendarsRepository extends TrekRepository { + async insertCalendar( + planId: number, + type: 'public_holiday' | 'school_holiday', + region: string, + label: string | null, + color: string, + sortOrder: number, + ): Promise { return this.insert({ plan: planId, type, region, label, color, sort_order: sortOrder }); } diff --git a/server/src/db/repositories/VacayPlanMembers.repository.ts b/server/src/db/repositories/VacayPlanMembers.repository.ts index d787b3ae0..4230678b2 100644 --- a/server/src/db/repositories/VacayPlanMembers.repository.ts +++ b/server/src/db/repositories/VacayPlanMembers.repository.ts @@ -1,7 +1,7 @@ import type { VacayPlanMembers } from '../entities/VacayPlanMembers.entity'; +import type { DB } from '../kysely/db'; import { toRow, type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A `vacay_plan_members` row as the legacy `SELECT *` returned it (VC51). */ export interface VacayPlanMemberRow { @@ -171,13 +171,19 @@ export class VacayPlanMembersRepository extends TrekRepository .select(['u.id', 'u.username', 'u.email']) .where('u.id', '!=', userId) .where((eb) => eb(eb.fn.coalesce('u.is_guest', eb.val(0)), '=', 0)) - .where('u.id', 'not in', (eb) => eb.selectFrom('vacay_plan_members').select('user_id').where('plan_id', '=', planId)) - .where('u.id', 'not in', (eb) => eb.selectFrom('vacay_plan_members').select('user_id').where('status', '=', 'accepted')) + .where('u.id', 'not in', (eb) => + eb.selectFrom('vacay_plan_members').select('user_id').where('plan_id', '=', planId), + ) + .where('u.id', 'not in', (eb) => + eb.selectFrom('vacay_plan_members').select('user_id').where('status', '=', 'accepted'), + ) .where('u.id', 'not in', (eb) => eb .selectFrom('vacay_plans') .select('owner_id') - .where('id', 'in', (eb2) => eb2.selectFrom('vacay_plan_members').select('plan_id').where('status', '=', 'accepted')), + .where('id', 'in', (eb2) => + eb2.selectFrom('vacay_plan_members').select('plan_id').where('status', '=', 'accepted'), + ), ) .orderBy('u.username', 'asc') .execute(); diff --git a/server/src/db/repositories/VacayPlans.repository.ts b/server/src/db/repositories/VacayPlans.repository.ts index b0830f384..a5cd320c7 100644 --- a/server/src/db/repositories/VacayPlans.repository.ts +++ b/server/src/db/repositories/VacayPlans.repository.ts @@ -1,7 +1,7 @@ import type { VacayPlans } from '../entities/VacayPlans.entity'; +import type { DB } from '../kysely/db'; import { toRow, type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** * A `vacay_plans` row as the legacy `SELECT *` returned it (Plan 3f Task 5, @@ -116,7 +116,11 @@ export class VacayPlansRepository extends TrekRepository { */ async findVacayUser(id: number): Promise<{ id: number; username: string; email: string } | null> { type UsersKyselyDB = Pick; - const row = await this.kysely().selectFrom('users').select(['id', 'username', 'email']).where('id', '=', id).executeTakeFirst(); + const row = await this.kysely() + .selectFrom('users') + .select(['id', 'username', 'email']) + .where('id', '=', id) + .executeTakeFirst(); return row ?? null; } } diff --git a/server/src/db/repositories/VacayShares.repository.ts b/server/src/db/repositories/VacayShares.repository.ts index d34d960c9..c39754ca6 100644 --- a/server/src/db/repositories/VacayShares.repository.ts +++ b/server/src/db/repositories/VacayShares.repository.ts @@ -1,7 +1,7 @@ import type { VacayShares } from '../entities/VacayShares.entity'; +import type { DB } from '../kysely/db'; import { toRow, type AssertRowKeys } from './_shared/rows'; import { TrekRepository } from './_shared/trek-repository'; -import type { DB } from '../kysely/db'; /** A `vacay_shares` row as the legacy `SELECT *` returned it. */ export interface VacayShareRow { @@ -164,7 +164,13 @@ export class VacaySharesRepository extends TrekRepository { .selectFrom('vacay_plans') .select('owner_id') .where('id', '=', planId) - .union(eb.selectFrom('vacay_plan_members').select('user_id as owner_id').where('plan_id', '=', planId).where('status', '=', 'accepted')), + .union( + eb + .selectFrom('vacay_plan_members') + .select('user_id as owner_id') + .where('plan_id', '=', planId) + .where('status', '=', 'accepted'), + ), ) .orderBy('u.username', 'asc') .execute(); diff --git a/server/src/db/repositories/VacayUserColors.repository.ts b/server/src/db/repositories/VacayUserColors.repository.ts index 4f97d1c00..6642d7e79 100644 --- a/server/src/db/repositories/VacayUserColors.repository.ts +++ b/server/src/db/repositories/VacayUserColors.repository.ts @@ -34,7 +34,10 @@ export class VacayUserColorsRepository extends TrekRepository { * three identical-text call sites, one method). */ async insertIgnore(userId: number, planId: number, color: string | null): Promise { - await this.upsert({ user: userId, plan: planId, color }, { onConflictFields: ['user', 'plan'], onConflictAction: 'ignore' }); + await this.upsert( + { user: userId, plan: planId, color }, + { onConflictFields: ['user', 'plan'], onConflictAction: 'ignore' }, + ); } /** diff --git a/server/src/db/repositories/VacayUserSettings.repository.ts b/server/src/db/repositories/VacayUserSettings.repository.ts index a2d880baa..4a3ebad9f 100644 --- a/server/src/db/repositories/VacayUserSettings.repository.ts +++ b/server/src/db/repositories/VacayUserSettings.repository.ts @@ -39,9 +39,21 @@ export class VacayUserSettingsRepository extends TrekRepository { + async upsertSettings( + userId: number, + yearType: string, + yearStartMonth: number, + yearStartDay: number, + hireDate: string | null, + ): Promise { await this.upsert( - { user: userId, year_type: yearType, year_start_month: yearStartMonth, year_start_day: yearStartDay, hire_date: hireDate }, + { + user: userId, + year_type: yearType, + year_start_month: yearStartMonth, + year_start_day: yearStartDay, + hire_date: hireDate, + }, { onConflictFields: ['user'], onConflictAction: 'merge' }, ); } diff --git a/server/src/db/repositories/VacayUserYears.repository.ts b/server/src/db/repositories/VacayUserYears.repository.ts index 590f260e1..3137ffe04 100644 --- a/server/src/db/repositories/VacayUserYears.repository.ts +++ b/server/src/db/repositories/VacayUserYears.repository.ts @@ -45,7 +45,13 @@ export class VacayUserYearsRepository extends TrekRepository { * legacy SQL text inserts the exact same row a bound `30` does), so a * single flexible method, not four near-duplicates. */ - async insertIgnore(userId: number, planId: number, year: number, vacationDays: number | null, carriedOver: number | null): Promise { + async insertIgnore( + userId: number, + planId: number, + year: number, + vacationDays: number | null, + carriedOver: number | null, + ): Promise { await this.upsert( { user: userId, plan: planId, year, vacation_days: vacationDays, carried_over: carriedOver }, { onConflictFields: ['user', 'plan', 'year'], onConflictAction: 'ignore' }, @@ -68,7 +74,11 @@ export class VacayUserYearsRepository extends TrekRepository { async upsertCarriedOver(userId: number, planId: number, year: number, carriedOver: number): Promise { await this.upsert( { user: userId, plan: planId, year, vacation_days: 30, carried_over: carriedOver }, - { onConflictFields: ['user', 'plan', 'year'], onConflictAction: 'merge', onConflictMergeFields: ['carried_over'] }, + { + onConflictFields: ['user', 'plan', 'year'], + onConflictAction: 'merge', + onConflictMergeFields: ['carried_over'], + }, ); } @@ -83,7 +93,11 @@ export class VacayUserYearsRepository extends TrekRepository { async upsertVacationDays(userId: number, planId: number, year: number, vacationDays: number): Promise { await this.upsert( { user: userId, plan: planId, year, vacation_days: vacationDays, carried_over: 0 }, - { onConflictFields: ['user', 'plan', 'year'], onConflictAction: 'merge', onConflictMergeFields: ['vacation_days'] }, + { + onConflictFields: ['user', 'plan', 'year'], + onConflictAction: 'merge', + onConflictMergeFields: ['vacation_days'], + }, ); } diff --git a/server/src/db/repositories/VacayYears.repository.ts b/server/src/db/repositories/VacayYears.repository.ts index 035420b85..731b43cc4 100644 --- a/server/src/db/repositories/VacayYears.repository.ts +++ b/server/src/db/repositories/VacayYears.repository.ts @@ -44,7 +44,10 @@ export class VacayYearsRepository extends TrekRepository { /** VC108 — `SELECT year FROM vacay_years WHERE plan_id = ? AND year < ? ORDER BY year DESC LIMIT 1` (`deleteYear`'s new-previous-year lookup). */ async previousYear(planId: number, beforeYear: number): Promise { - const row = await this.findOne({ plan: planId, year: { $lt: beforeYear } }, { fields: ['year'], orderBy: { year: 'desc' } }); + const row = await this.findOne( + { plan: planId, year: { $lt: beforeYear } }, + { fields: ['year'], orderBy: { year: 'desc' } }, + ); return row?.year ?? null; } diff --git a/server/src/db/repositories/VisitedCountries.repository.ts b/server/src/db/repositories/VisitedCountries.repository.ts index a23c7c3bc..2764d5de0 100644 --- a/server/src/db/repositories/VisitedCountries.repository.ts +++ b/server/src/db/repositories/VisitedCountries.repository.ts @@ -1,6 +1,6 @@ import type { VisitedCountries } from '../entities/VisitedCountries.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** `visited_countries` — every scalar column, `AT10`'s own read shape. */ export interface VisitedCountryRow { @@ -59,7 +59,11 @@ export class VisitedCountriesRepository extends TrekRepository * ever reads `.country_code` off the legacy row shape. */ async listCodesForUser(userId: number): Promise { - const rows = await this.writeDb().selectFrom('visited_countries').select('country_code').where('user_id', '=', userId).execute(); + const rows = await this.writeDb() + .selectFrom('visited_countries') + .select('country_code') + .where('user_id', '=', userId) + .execute(); return rows.map((r) => r.country_code); } @@ -90,7 +94,11 @@ export class VisitedCountriesRepository extends TrekRepository /** AT14 (`unmarkCountry`, inside its transaction) — `DELETE FROM visited_countries WHERE user_id = ? AND country_code = ?`. */ async unmark(userId: number, countryCode: string): Promise { - await this.writeDb().deleteFrom('visited_countries').where('user_id', '=', userId).where('country_code', '=', countryCode).execute(); + await this.writeDb() + .deleteFrom('visited_countries') + .where('user_id', '=', userId) + .where('country_code', '=', countryCode) + .execute(); } /** diff --git a/server/src/db/repositories/VisitedRegions.repository.ts b/server/src/db/repositories/VisitedRegions.repository.ts index e79f08f6d..c16339e22 100644 --- a/server/src/db/repositories/VisitedRegions.repository.ts +++ b/server/src/db/repositories/VisitedRegions.repository.ts @@ -1,6 +1,6 @@ import type { VisitedRegions } from '../entities/VisitedRegions.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** `visited_regions` — the columns `AT21`'s read needs. */ export interface VisitedRegionRow { @@ -79,11 +79,19 @@ export class VisitedRegionsRepository extends TrekRepository { /** AT15 (`unmarkCountry`, inside its transaction) — `DELETE FROM visited_regions WHERE user_id = ? AND country_code = ?` (every region in the country). */ async unmarkAllInCountry(userId: number, countryCode: string): Promise { - await this.writeDb().deleteFrom('visited_regions').where('user_id', '=', userId).where('country_code', '=', countryCode).execute(); + await this.writeDb() + .deleteFrom('visited_regions') + .where('user_id', '=', userId) + .where('country_code', '=', countryCode) + .execute(); } /** AT26 (`unmarkRegion`, inside its transaction) — `DELETE FROM visited_regions WHERE user_id = ? AND region_code = ?` (one region). */ async unmark(userId: number, regionCode: string): Promise { - await this.writeDb().deleteFrom('visited_regions').where('user_id', '=', userId).where('region_code', '=', regionCode).execute(); + await this.writeDb() + .deleteFrom('visited_regions') + .where('user_id', '=', userId) + .where('region_code', '=', regionCode) + .execute(); } } diff --git a/server/src/db/repositories/WebauthnChallenges.repository.ts b/server/src/db/repositories/WebauthnChallenges.repository.ts index e9ae29b06..4de686b2d 100644 --- a/server/src/db/repositories/WebauthnChallenges.repository.ts +++ b/server/src/db/repositories/WebauthnChallenges.repository.ts @@ -1,6 +1,6 @@ import type { WebauthnChallenges } from '../entities/WebauthnChallenges.entity'; -import { TrekRepository } from './_shared/trek-repository'; import type { DB } from '../kysely/db'; +import { TrekRepository } from './_shared/trek-repository'; /** `PasskeyService.storeChallenge` (PK2). */ export interface NewChallengeRow { @@ -83,7 +83,11 @@ export class WebauthnChallengesRepository extends TrekRepository { + async claimChallenge( + challenge: string, + type: 'registration' | 'authentication', + now: number, + ): Promise<{ user_id: number | null } | null> { const row = await this.kysely() .deleteFrom('webauthn_challenges') .where('challenge', '=', challenge) diff --git a/server/src/db/repositories/WebauthnCredentials.repository.ts b/server/src/db/repositories/WebauthnCredentials.repository.ts index 290144130..62649bef5 100644 --- a/server/src/db/repositories/WebauthnCredentials.repository.ts +++ b/server/src/db/repositories/WebauthnCredentials.repository.ts @@ -1,6 +1,6 @@ +import { currentTimestamp } from '../dialect/sql-functions'; import type { WebauthnCredentials } from '../entities/WebauthnCredentials.entity'; import { toRow, type AssertRowKeys } from './_shared/rows'; -import { currentTimestamp } from '../dialect/sql-functions'; import { TrekRepository } from './_shared/trek-repository'; /** @@ -58,7 +58,14 @@ export interface PasskeyPanelRow { const PANEL_FIELDS = ['id', 'name', 'device_type', 'backed_up', 'created_at', 'last_used_at'] as const; -function toPanelRow(row: { id: number; name?: string | null; device_type?: string | null; backed_up: number; created_at?: string | null; last_used_at?: string | null }): PasskeyPanelRow { +function toPanelRow(row: { + id: number; + name?: string | null; + device_type?: string | null; + backed_up: number; + created_at?: string | null; + last_used_at?: string | null; +}): PasskeyPanelRow { return { id: row.id, name: row.name ?? null, diff --git a/server/src/db/repositories/_shared/owned-lookup.ts b/server/src/db/repositories/_shared/owned-lookup.ts index bee215f0a..df48df679 100644 --- a/server/src/db/repositories/_shared/owned-lookup.ts +++ b/server/src/db/repositories/_shared/owned-lookup.ts @@ -1,5 +1,5 @@ -import type { FilterQuery, FilterValue, OrderDefinition } from '@mikro-orm/core'; import type { TrekRepository } from './trek-repository'; +import type { FilterQuery, FilterValue, OrderDefinition } from '@mikro-orm/core'; /** * Shared lookup shapes for entities scoped to a user. diff --git a/server/src/db/repositories/_shared/packing-visibility.ts b/server/src/db/repositories/_shared/packing-visibility.ts index eb3589cea..dbfa50ef6 100644 --- a/server/src/db/repositories/_shared/packing-visibility.ts +++ b/server/src/db/repositories/_shared/packing-visibility.ts @@ -1,6 +1,7 @@ -import type { Expression, ExpressionBuilder, ExpressionWrapper, SqlBool } from 'kysely'; import type { DB } from '../../kysely/db'; +import type { Expression, ExpressionBuilder, ExpressionWrapper, SqlBool } from 'kysely'; + /** * The per-actor visibility predicate packing's three-tier sharing model * (#858) runs every restricted read/write through (Plan 3e Task 0, R1) — ONE @@ -111,10 +112,7 @@ import type { DB } from '../../kysely/db'; export function packingVisibleToActorCondition(actorId: number | undefined) { return { - $or: [ - { is_private: 0 }, - ...(actorId != null ? [{ owner_id: actorId }, { packing_item_recipients: actorId }] : []), - ], + $or: [{ is_private: 0 }, ...(actorId != null ? [{ owner_id: actorId }, { packing_item_recipients: actorId }] : [])], }; } diff --git a/server/src/db/repositories/_shared/presence-set.ts b/server/src/db/repositories/_shared/presence-set.ts index cb9cab1d1..5c871c7c2 100644 --- a/server/src/db/repositories/_shared/presence-set.ts +++ b/server/src/db/repositories/_shared/presence-set.ts @@ -23,9 +23,9 @@ * Land here first (Task 2); Tasks 3 (packing), 4 (todo) and 5 (collab) copy * this shape rather than re-deriving their own CASE WHEN translation. */ -export function presenceSet>( - fields: { [K in keyof T]?: readonly [present: boolean, value: T[K]] }, -): Partial { +export function presenceSet>(fields: { + [K in keyof T]?: readonly [present: boolean, value: T[K]]; +}): Partial { const out: Partial = {}; for (const key of Object.keys(fields) as (keyof T)[]) { const entry = fields[key]; diff --git a/server/src/db/repositories/_shared/reservation-travelers-owns.ts b/server/src/db/repositories/_shared/reservation-travelers-owns.ts index ebbb7dca2..526400e0e 100644 --- a/server/src/db/repositories/_shared/reservation-travelers-owns.ts +++ b/server/src/db/repositories/_shared/reservation-travelers-owns.ts @@ -1,6 +1,7 @@ -import type { ExpressionBuilder, ExpressionWrapper, SqlBool } from 'kysely'; import type { DB } from '../../kysely/db'; +import type { ExpressionBuilder, ExpressionWrapper, SqlBool } from 'kysely'; + /** * The `TRAVELER_OWNS` predicate atlas's `AtlasService` inlines as a * `private static readonly` string fragment (`atlas.service.ts:1173-1175`, @@ -128,8 +129,5 @@ export function travelerOwnsExpr( .select('rt.reservation_id') .whereRef('rt.reservation_id', '=', 'r.id'); - return eb.or([ - eb.not(eb.exists(forThisReservation)), - eb.exists(forThisReservation.where('rt.user_id', '=', userId)), - ]); + return eb.or([eb.not(eb.exists(forThisReservation)), eb.exists(forThisReservation.where('rt.user_id', '=', userId))]); } diff --git a/server/src/db/repositories/_shared/reservation-visibility.ts b/server/src/db/repositories/_shared/reservation-visibility.ts index fa79e7abe..f57dc75c6 100644 --- a/server/src/db/repositories/_shared/reservation-visibility.ts +++ b/server/src/db/repositories/_shared/reservation-visibility.ts @@ -1,7 +1,8 @@ -import type { Platform } from '@mikro-orm/core'; -import type { ExpressionBuilder, ExpressionWrapper, ReferenceExpression, SqlBool } from 'kysely'; import { coalesceParam } from '../../dialect/sql-functions'; import type { DB } from '../../kysely/db'; +import type { Platform } from '@mikro-orm/core'; + +import type { ExpressionBuilder, ExpressionWrapper, ReferenceExpression, SqlBool } from 'kysely'; /** * The visibility predicate an anonymous viewer's reads are filtered diff --git a/server/src/db/repositories/_shared/trek-repository.ts b/server/src/db/repositories/_shared/trek-repository.ts index 6ced110ee..0811cf8fb 100644 --- a/server/src/db/repositories/_shared/trek-repository.ts +++ b/server/src/db/repositories/_shared/trek-repository.ts @@ -161,7 +161,10 @@ export abstract class TrekRepository extends EntityReposi return super.findAll({ ...options, disableIdentityMap }); } - override async count(where?: FilterQuery, options?: CountOptions): Promise { + override async count( + where?: FilterQuery, + options?: CountOptions, + ): Promise { this.validateRequestContext(); return super.count(where, options); } @@ -177,7 +180,11 @@ export abstract class TrekRepository extends EntityReposi * `findAll` above: `Using` dropped for the same override-compatibility * reason documented on `findOne`. */ - override async findOneOrFail( + override async findOneOrFail< + Hint extends string = never, + Fields extends string = never, + Excludes extends string = never, + >( where: FilterQuery, options?: FindOneOrFailOptions, ): Promise> { @@ -186,7 +193,11 @@ export abstract class TrekRepository extends EntityReposi return super.findOneOrFail(where, { ...options, disableIdentityMap }); } - override async findAndCount( + override async findAndCount< + Hint extends string = never, + Fields extends string = never, + Excludes extends string = never, + >( where: FilterQuery, options?: FindOptions, ): Promise<[Loaded[], number]> { @@ -210,12 +221,20 @@ export abstract class TrekRepository extends EntityReposi } /** Grouping counts don't hydrate an entity — no identity map to isolate — so this only needs the fail-closed guard, matching `count` above. */ - override async countBy(groupBy: EntityKey | readonly EntityKey[], options?: CountByOptions): Promise> { + override async countBy( + groupBy: EntityKey | readonly EntityKey[], + options?: CountByOptions, + ): Promise> { this.validateRequestContext(); return super.countBy(groupBy, options); } - override stream( + override stream< + Hint extends string = never, + Fields extends string = never, + Excludes extends string = never, + Using extends string = never, + >( options?: WithUsingOptions, Entity, Using>, ): AsyncIterableIterator> { this.validateRequestContext(); @@ -223,7 +242,10 @@ export abstract class TrekRepository extends EntityReposi return super.stream({ ...options, disableIdentityMap }); } - override async insertMany(data: Entity[] | RequiredEntityData[], options?: NativeInsertUpdateOptions): Promise[]> { + override async insertMany( + data: Entity[] | RequiredEntityData[], + options?: NativeInsertUpdateOptions, + ): Promise[]> { this.validateRequestContext(); return super.insertMany(data, options); } @@ -236,7 +258,11 @@ export abstract class TrekRepository extends EntityReposi return super.upsertMany(entitiesOrData, options); } - override async nativeUpdate(where: FilterQuery, data: EntityData, options?: UpdateOptions): Promise { + override async nativeUpdate( + where: FilterQuery, + data: EntityData, + options?: UpdateOptions, + ): Promise { this.validateRequestContext(); return super.nativeUpdate(where, data, options); } @@ -246,7 +272,10 @@ export abstract class TrekRepository extends EntityReposi return super.nativeDelete(where, options); } - override async insert(data: Entity | RequiredEntityData, options?: NativeInsertUpdateOptions): Promise> { + override async insert( + data: Entity | RequiredEntityData, + options?: NativeInsertUpdateOptions, + ): Promise> { this.validateRequestContext(); return super.insert(data, options); } diff --git a/server/src/db/reseat-booked-nights.ts b/server/src/db/reseat-booked-nights.ts index a5a29b7f9..0155d6a80 100644 --- a/server/src/db/reseat-booked-nights.ts +++ b/server/src/db/reseat-booked-nights.ts @@ -1,5 +1,13 @@ +import { + locatedIds, + planViaCarry, + sameOrder, + seatAmong, + type PinnedVia, + type SeatRow, +} from '../nest/accommodations/night-seat'; + import Database from 'better-sqlite3'; -import { locatedIds, planViaCarry, sameOrder, seatAmong, type PinnedVia, type SeatRow } from '../nest/accommodations/night-seat'; /** * Seat every booked night where its check-in says, the way a night booked today is @@ -27,12 +35,16 @@ import { locatedIds, planViaCarry, sameOrder, seatAmong, type PinnedVia, type Se * how many nights moved. */ export function reseatBookedNights(db: Database.Database): number { - const nights = db.prepare(` + const nights = db + .prepare( + ` SELECT da.id, da.day_id, a.id AS accommodation_id, a.check_in FROM day_assignments da JOIN day_accommodations a ON a.id = da.accommodation_id ORDER BY da.day_id, a.check_in IS NOT NULL, a.check_in, a.id - `).all() as Array<{ id: number; day_id: number; accommodation_id: number; check_in: string | null }>; + `, + ) + .all() as Array<{ id: number; day_id: number; accommodation_id: number; check_in: string | null }>; // A day's stops as the rule sees them (SeatRow): the same statement // DayAssignmentsRepository.listSeatRows (AC7) runs for the services. const dayStops = db.prepare(` @@ -76,7 +88,9 @@ export function reseatBookedNights(db: Database.Database): number { roads.removed += plan.remove.length; } if (roads.moved || roads.removed) { - console.log(`[DB] Re-pinned ${roads.moved} drawn road(s) behind the reseated nights, dropped ${roads.removed} left without a leg`); + console.log( + `[DB] Re-pinned ${roads.moved} drawn road(s) behind the reseated nights, dropped ${roads.removed} left without a leg`, + ); } return seated; } diff --git a/server/src/db/seeders/AddonSeeder.ts b/server/src/db/seeders/AddonSeeder.ts index 9d8a4e190..681854429 100644 --- a/server/src/db/seeders/AddonSeeder.ts +++ b/server/src/db/seeders/AddonSeeder.ts @@ -1,7 +1,7 @@ -import type { EntityManager } from '@mikro-orm/core'; -import type { SqlEntityManager } from '@mikro-orm/sql'; -import { Seeder } from '@mikro-orm/seeder'; import type { DB } from '../kysely/db'; +import type { EntityManager } from '@mikro-orm/core'; +import { Seeder } from '@mikro-orm/seeder'; +import type { SqlEntityManager } from '@mikro-orm/sql'; interface AddonRow { id: string; diff --git a/server/src/db/seeders/DatabaseSeeder.ts b/server/src/db/seeders/DatabaseSeeder.ts index b54d68ad2..75f07cd8b 100644 --- a/server/src/db/seeders/DatabaseSeeder.ts +++ b/server/src/db/seeders/DatabaseSeeder.ts @@ -3,9 +3,9 @@ import { AdminSeeder } from './AdminSeeder'; import { CategorySeeder } from './CategorySeeder'; import { DocumentProviderSeeder } from './DocumentProviderSeeder'; import { PhotoProviderSeeder } from './PhotoProviderSeeder'; +import { SchemaVersionSeeder } from './SchemaVersionSeeder'; import type { EntityManager } from '@mikro-orm/core'; import { Seeder } from '@mikro-orm/seeder'; -import { SchemaVersionSeeder } from './SchemaVersionSeeder'; /** * The default seeder, replacing `db/seeds.ts::runSeeds()`. @@ -16,6 +16,13 @@ import { SchemaVersionSeeder } from './SchemaVersionSeeder'; */ export class DatabaseSeeder extends Seeder { async run(em: EntityManager): Promise { - await this.call(em, [AdminSeeder, CategorySeeder, AddonSeeder, PhotoProviderSeeder, DocumentProviderSeeder, SchemaVersionSeeder]); + await this.call(em, [ + AdminSeeder, + CategorySeeder, + AddonSeeder, + PhotoProviderSeeder, + DocumentProviderSeeder, + SchemaVersionSeeder, + ]); } } diff --git a/server/src/db/seeders/DocumentProviderSeeder.ts b/server/src/db/seeders/DocumentProviderSeeder.ts index 38e3138bd..3cfeb0014 100644 --- a/server/src/db/seeders/DocumentProviderSeeder.ts +++ b/server/src/db/seeders/DocumentProviderSeeder.ts @@ -1,8 +1,8 @@ import { DOCUMENT_PROVIDERS, DOCUMENT_PROVIDER_FIELDS } from '../document-provider-seed'; -import type { EntityManager } from '@mikro-orm/core'; -import type { SqlEntityManager } from '@mikro-orm/sql'; -import { Seeder } from '@mikro-orm/seeder'; import type { DB } from '../kysely/db'; +import type { EntityManager } from '@mikro-orm/core'; +import { Seeder } from '@mikro-orm/seeder'; +import type { SqlEntityManager } from '@mikro-orm/sql'; /** * Seeds the document providers. @@ -19,7 +19,14 @@ export class DocumentProviderSeeder extends Seeder { for (const p of DOCUMENT_PROVIDERS) { await db .insertInto('document_providers') - .values({ id: p.id, name: p.name, description: p.description, icon: p.icon, enabled: 0, sort_order: p.sort_order }) + .values({ + id: p.id, + name: p.name, + description: p.description, + icon: p.icon, + enabled: 0, + sort_order: p.sort_order, + }) .onConflict((oc) => oc.doNothing()) .execute(); } diff --git a/server/src/db/seeders/PhotoProviderSeeder.ts b/server/src/db/seeders/PhotoProviderSeeder.ts index b61de0f23..f2d2424c3 100644 --- a/server/src/db/seeders/PhotoProviderSeeder.ts +++ b/server/src/db/seeders/PhotoProviderSeeder.ts @@ -1,7 +1,7 @@ -import type { EntityManager } from '@mikro-orm/core'; -import type { SqlEntityManager } from '@mikro-orm/sql'; -import { Seeder } from '@mikro-orm/seeder'; import type { DB } from '../kysely/db'; +import type { EntityManager } from '@mikro-orm/core'; +import { Seeder } from '@mikro-orm/seeder'; +import type { SqlEntityManager } from '@mikro-orm/sql'; interface PhotoProviderRow { id: string; @@ -167,7 +167,14 @@ export class PhotoProviderSeeder extends Seeder { for (const p of PROVIDERS) { await db .insertInto('photo_providers') - .values({ id: p.id, name: p.name, description: p.description, icon: p.icon, enabled: p.enabled, sort_order: p.sort_order }) + .values({ + id: p.id, + name: p.name, + description: p.description, + icon: p.icon, + enabled: p.enabled, + sort_order: p.sort_order, + }) .onConflict((oc) => oc.doNothing()) .execute(); } diff --git a/server/src/db/seeders/SchemaVersionSeeder.ts b/server/src/db/seeders/SchemaVersionSeeder.ts index af925ce74..f71445353 100644 --- a/server/src/db/seeders/SchemaVersionSeeder.ts +++ b/server/src/db/seeders/SchemaVersionSeeder.ts @@ -1,7 +1,7 @@ -import type { EntityManager } from '@mikro-orm/core'; -import type { SqlEntityManager } from '@mikro-orm/sql'; -import { Seeder } from '@mikro-orm/seeder'; import type { DB } from '../kysely/db'; +import type { EntityManager } from '@mikro-orm/core'; +import { Seeder } from '@mikro-orm/seeder'; +import type { SqlEntityManager } from '@mikro-orm/sql'; /** * The version the hand-written `db/migrations.ts` chain ended on — the highest diff --git a/server/src/demo/demo-reset.ts b/server/src/demo/demo-reset.ts index e05103d4e..1618aaad6 100644 --- a/server/src/demo/demo-reset.ts +++ b/server/src/demo/demo-reset.ts @@ -1,13 +1,18 @@ -import fs from 'fs'; -import path from 'path'; -import { RequestContext, type EntityManager } from '@mikro-orm/core'; import { readEnv } from '../app-config'; import { resolveDataPaths } from '../app-config/data-paths'; -import { DemoRepository, type DemoAdminCredentialsRow, type DemoInstanceKeyRow } from '../db/repositories/DemoRepository'; -import { DatabaseConnectionLostError, type DatabaseBackupStrategy } from '../nest/database/database-backup.interface'; -import type { CarriedUserSessionRow } from '../db/repositories/UserSessions.repository'; import { UserSessions } from '../db/entities/UserSessions.entity'; +import { + DemoRepository, + type DemoAdminCredentialsRow, + type DemoInstanceKeyRow, +} from '../db/repositories/DemoRepository'; +import type { CarriedUserSessionRow } from '../db/repositories/UserSessions.repository'; import { dbNow } from '../db/types'; +import { DatabaseConnectionLostError, type DatabaseBackupStrategy } from '../nest/database/database-backup.interface'; +import { RequestContext, type EntityManager } from '@mikro-orm/core'; + +import fs from 'fs'; +import path from 'path'; const baselinePath = path.join(resolveDataPaths().dataDir, 'travel-baseline.db'); diff --git a/server/src/demo/demo-seed.ts b/server/src/demo/demo-seed.ts index 7f58a07a3..9908ea621 100644 --- a/server/src/demo/demo-seed.ts +++ b/server/src/demo/demo-seed.ts @@ -1,11 +1,12 @@ -import bcrypt from 'bcryptjs'; import { readEnv } from '../app-config'; -import { DEMO_PASS } from '../nest/common/demo'; import { DemoRepository, type NewDemoPlaceRow } from '../db/repositories/DemoRepository'; +import { DEMO_PASS } from '../nest/common/demo'; // Static like in demo-reset.job.ts: the module top is inert, everything that // touches the database happens inside the functions. import { markExampleTripsSeeded, requireEntityManager } from './demo-reset'; +import bcrypt from 'bcryptjs'; + // D6 (task-2-review.md's controller ruling / non-HTTP caller table): this runs // from `runSchemaBootstrap` before `app.init()`, outside any request context, but // `runSchemaBootstrap` already wraps the whole call in `withRequestContext` @@ -81,7 +82,14 @@ async function seedExampleTrips(demo: DemoRepository, adminId: number, demoId: n // Category IDs: 1=Hotel, 2=Restaurant, 3=Attraction, 5=Transport, 7=Bar/Cafe, 8=Beach, 9=Nature, 6=Entertainment // --- Trip 1: Tokyo & Kyoto --- - const t1 = await demo.insertTrip(adminId, 'Tokyo & Kyoto', 'Two weeks in Japan — from the neon-lit streets of Tokyo to the serene temples of Kyoto.', '2026-04-15', '2026-04-21', 'JPY'); + const t1 = await demo.insertTrip( + adminId, + 'Tokyo & Kyoto', + 'Two weeks in Japan — from the neon-lit streets of Tokyo to the serene temples of Kyoto.', + '2026-04-15', + '2026-04-21', + 'JPY', + ); const t1days: number[] = []; for (let i = 0; i < 7; i++) { @@ -89,19 +97,201 @@ async function seedExampleTrips(demo: DemoRepository, adminId: number, demoId: n } const t1places: NewDemoPlaceRow[] = [ - [t1, 'Hotel Shinjuku Granbell', 35.6938, 139.7035, '2-14-5 Kabukicho, Shinjuku City, Tokyo 160-0021, Japan', 1, '15:00', 60, 'Check-in from 3 PM. Steps from Shinjuku Station.', null, 'ChIJdaGEJBeMGGARYgt8sLBv6lM', 'https://www.grfranbellhotel.jp/shinjuku/', '+81 3-5155-2666'], - [t1, 'Senso-ji Temple', 35.7148, 139.7967, '2 Chome-3-1 Asakusa, Taito City, Tokyo 111-0032, Japan', 3, '09:00', 90, 'Oldest temple in Tokyo. Fewer tourists in the early morning.', null, 'ChIJ8T1GpMGOGGARDYGSgpoOdfg', 'https://www.senso-ji.jp/', '+81 3-3842-0181'], - [t1, 'Shibuya Crossing', 35.6595, 139.7004, '2 Chome-2-1 Dogenzaka, Shibuya City, Tokyo 150-0043, Japan', 3, '18:00', 45, 'World\'s busiest pedestrian crossing. Most impressive at night.', null, 'ChIJLyzOhmyLGGARMKWbl5z6wGg', null, null], - [t1, 'Tsukiji Outer Market', 35.6654, 139.7707, '4 Chome-16-2 Tsukiji, Chuo City, Tokyo 104-0045, Japan', 2, '08:00', 120, 'Fresh sushi for breakfast! Explore the street food stalls.', null, 'ChIJq2i1dZCLGGAR1TfoBRo25VU', 'https://www.tsukiji.or.jp/', null], - [t1, 'Meiji Jingu Shrine', 35.6764, 139.6993, '1-1 Yoyogikamizonocho, Shibuya City, Tokyo 151-8557, Japan', 3, '10:00', 75, 'Peaceful oasis in the middle of the city. Walk through the forest to the shrine.', null, 'ChIJ5SuJSByMGGARMg9qOlTFgkc', 'https://www.meijijingu.or.jp/', '+81 3-3379-5511'], - [t1, 'Akihabara Electric Town', 35.7023, 139.7745, 'Sotokanda, Chiyoda City, Tokyo, Japan', 3, '14:00', 180, 'Electric Town — anime, manga, electronics. Retro gaming shops!', null, 'ChIJGz1usEyMGGAR1mYByqOOJao', null, null], - [t1, 'Shinkansen to Kyoto', 35.6812, 139.7671, '1 Chome Marunouchi, Chiyoda City, Tokyo 100-0005, Japan', 5, '08:30', 140, 'Nozomi Shinkansen, approx. 2h15. Window seat for Mt. Fuji views!', null, 'ChIJC3Cf2PuLGGAROO00ukl8JwA', null, null], - [t1, 'Hotel Granvia Kyoto', 34.9856, 135.7580, 'Karasuma-dori Shiokoji-sagaru, Shimogyo-ku, Kyoto 600-8216, Japan', 1, '14:00', 60, 'Right at Kyoto Station. Perfect base for day trips.', null, 'ChIJUf6MDFcIAWARLihjKC9FWDY', 'https://www.granvia-kyoto.co.jp/', '+81 75-344-8888'], - [t1, 'Fushimi Inari Taisha', 34.9671, 135.7727, '68 Fukakusa Yabunouchicho, Fushimi Ward, Kyoto 612-0882, Japan', 3, '07:00', 150, '10,000 vermillion torii gates. Start early for empty paths!', null, 'ChIJIW0JRbMIAWARPYEzP5LVHGE', 'http://inari.jp/', '+81 75-641-7331'], - [t1, 'Kinkaku-ji (Golden Pavilion)', 35.0394, 135.7292, '1 Kinkakujicho, Kita Ward, Kyoto 603-8361, Japan', 3, '10:00', 60, 'The golden temple reflected in the mirror pond. Iconic photo spot.', null, 'ChIJvUbrwCCoAWAR5-uyAXPzBHg', null, '+81 75-461-0013'], - [t1, 'Arashiyama Bamboo Grove', 35.0095, 135.6673, 'Sagatenryuji Susukinobabacho, Ukyo Ward, Kyoto 616-8385, Japan', 9, '09:00', 90, 'Magical bamboo forest. Best visited in the morning before the crowds.', null, 'ChIJFS4EvA6pAWARQsAPVijvW7I', null, null], - [t1, 'Nishiki Market', 35.0050, 135.7647, 'Nishiki-koji Dori, Nakagyo Ward, Kyoto 604-8054, Japan', 2, '12:00', 90, 'Kyoto\'s kitchen street. Try the matcha ice cream and fresh mochi!', null, 'ChIJ09zzUigJAWARXzIdh1NE3hQ', 'http://www.kyoto-nishiki.or.jp/', null], - [t1, 'Gion District', 35.0037, 135.7755, 'Gionmachi Minamigawa, Higashiyama Ward, Kyoto 605-0074, Japan', 3, '17:00', 120, 'Historic geisha district. Best chance of spotting a maiko in the evening.', null, 'ChIJ7WWWjfYJAWARGqEHAfXIzgQ', null, null], + [ + t1, + 'Hotel Shinjuku Granbell', + 35.6938, + 139.7035, + '2-14-5 Kabukicho, Shinjuku City, Tokyo 160-0021, Japan', + 1, + '15:00', + 60, + 'Check-in from 3 PM. Steps from Shinjuku Station.', + null, + 'ChIJdaGEJBeMGGARYgt8sLBv6lM', + 'https://www.grfranbellhotel.jp/shinjuku/', + '+81 3-5155-2666', + ], + [ + t1, + 'Senso-ji Temple', + 35.7148, + 139.7967, + '2 Chome-3-1 Asakusa, Taito City, Tokyo 111-0032, Japan', + 3, + '09:00', + 90, + 'Oldest temple in Tokyo. Fewer tourists in the early morning.', + null, + 'ChIJ8T1GpMGOGGARDYGSgpoOdfg', + 'https://www.senso-ji.jp/', + '+81 3-3842-0181', + ], + [ + t1, + 'Shibuya Crossing', + 35.6595, + 139.7004, + '2 Chome-2-1 Dogenzaka, Shibuya City, Tokyo 150-0043, Japan', + 3, + '18:00', + 45, + "World's busiest pedestrian crossing. Most impressive at night.", + null, + 'ChIJLyzOhmyLGGARMKWbl5z6wGg', + null, + null, + ], + [ + t1, + 'Tsukiji Outer Market', + 35.6654, + 139.7707, + '4 Chome-16-2 Tsukiji, Chuo City, Tokyo 104-0045, Japan', + 2, + '08:00', + 120, + 'Fresh sushi for breakfast! Explore the street food stalls.', + null, + 'ChIJq2i1dZCLGGAR1TfoBRo25VU', + 'https://www.tsukiji.or.jp/', + null, + ], + [ + t1, + 'Meiji Jingu Shrine', + 35.6764, + 139.6993, + '1-1 Yoyogikamizonocho, Shibuya City, Tokyo 151-8557, Japan', + 3, + '10:00', + 75, + 'Peaceful oasis in the middle of the city. Walk through the forest to the shrine.', + null, + 'ChIJ5SuJSByMGGARMg9qOlTFgkc', + 'https://www.meijijingu.or.jp/', + '+81 3-3379-5511', + ], + [ + t1, + 'Akihabara Electric Town', + 35.7023, + 139.7745, + 'Sotokanda, Chiyoda City, Tokyo, Japan', + 3, + '14:00', + 180, + 'Electric Town — anime, manga, electronics. Retro gaming shops!', + null, + 'ChIJGz1usEyMGGAR1mYByqOOJao', + null, + null, + ], + [ + t1, + 'Shinkansen to Kyoto', + 35.6812, + 139.7671, + '1 Chome Marunouchi, Chiyoda City, Tokyo 100-0005, Japan', + 5, + '08:30', + 140, + 'Nozomi Shinkansen, approx. 2h15. Window seat for Mt. Fuji views!', + null, + 'ChIJC3Cf2PuLGGAROO00ukl8JwA', + null, + null, + ], + [ + t1, + 'Hotel Granvia Kyoto', + 34.9856, + 135.758, + 'Karasuma-dori Shiokoji-sagaru, Shimogyo-ku, Kyoto 600-8216, Japan', + 1, + '14:00', + 60, + 'Right at Kyoto Station. Perfect base for day trips.', + null, + 'ChIJUf6MDFcIAWARLihjKC9FWDY', + 'https://www.granvia-kyoto.co.jp/', + '+81 75-344-8888', + ], + [ + t1, + 'Fushimi Inari Taisha', + 34.9671, + 135.7727, + '68 Fukakusa Yabunouchicho, Fushimi Ward, Kyoto 612-0882, Japan', + 3, + '07:00', + 150, + '10,000 vermillion torii gates. Start early for empty paths!', + null, + 'ChIJIW0JRbMIAWARPYEzP5LVHGE', + 'http://inari.jp/', + '+81 75-641-7331', + ], + [ + t1, + 'Kinkaku-ji (Golden Pavilion)', + 35.0394, + 135.7292, + '1 Kinkakujicho, Kita Ward, Kyoto 603-8361, Japan', + 3, + '10:00', + 60, + 'The golden temple reflected in the mirror pond. Iconic photo spot.', + null, + 'ChIJvUbrwCCoAWAR5-uyAXPzBHg', + null, + '+81 75-461-0013', + ], + [ + t1, + 'Arashiyama Bamboo Grove', + 35.0095, + 135.6673, + 'Sagatenryuji Susukinobabacho, Ukyo Ward, Kyoto 616-8385, Japan', + 9, + '09:00', + 90, + 'Magical bamboo forest. Best visited in the morning before the crowds.', + null, + 'ChIJFS4EvA6pAWARQsAPVijvW7I', + null, + null, + ], + [ + t1, + 'Nishiki Market', + 35.005, + 135.7647, + 'Nishiki-koji Dori, Nakagyo Ward, Kyoto 604-8054, Japan', + 2, + '12:00', + 90, + "Kyoto's kitchen street. Try the matcha ice cream and fresh mochi!", + null, + 'ChIJ09zzUigJAWARXzIdh1NE3hQ', + 'http://www.kyoto-nishiki.or.jp/', + null, + ], + [ + t1, + 'Gion District', + 35.0037, + 135.7755, + 'Gionmachi Minamigawa, Higashiyama Ward, Kyoto 605-0074, Japan', + 3, + '17:00', + 120, + 'Historic geisha district. Best chance of spotting a maiko in the evening.', + null, + 'ChIJ7WWWjfYJAWARGqEHAfXIzgQ', + null, + null, + ], ]; const t1pIds: number[] = []; @@ -134,13 +324,19 @@ async function seedExampleTrips(demo: DemoRepository, adminId: number, demoId: n // Packing const t1packing: [string, number, string, number][] = [ - ['Passport', 1, 'Documents', 0], ['Japan Rail Pass', 1, 'Documents', 1], - ['Power adapter Type A/B', 0, 'Electronics', 2], ['Camera + charger', 0, 'Electronics', 3], - ['Comfortable walking shoes', 0, 'Clothing', 4], ['Rain jacket', 0, 'Clothing', 5], - ['Sunscreen', 0, 'Toiletries', 6], ['Travel first aid kit', 0, 'Toiletries', 7], - ['Pocket WiFi confirmation', 1, 'Electronics', 8], ['Yen cash', 0, 'Documents', 9], + ['Passport', 1, 'Documents', 0], + ['Japan Rail Pass', 1, 'Documents', 1], + ['Power adapter Type A/B', 0, 'Electronics', 2], + ['Camera + charger', 0, 'Electronics', 3], + ['Comfortable walking shoes', 0, 'Clothing', 4], + ['Rain jacket', 0, 'Clothing', 5], + ['Sunscreen', 0, 'Toiletries', 6], + ['Travel first aid kit', 0, 'Toiletries', 7], + ['Pocket WiFi confirmation', 1, 'Electronics', 8], + ['Yen cash', 0, 'Documents', 9], ]; - for (const [name, checked, category, sortOrder] of t1packing) await demo.insertPackingItem(t1, name, checked, category, sortOrder); + for (const [name, checked, category, sortOrder] of t1packing) + await demo.insertPackingItem(t1, name, checked, category, sortOrder); // Budget await demo.insertBudgetItem(t1, 'Accommodation', 'Hotel Shinjuku (3 nights)', 67500, 2, 'Double room'); @@ -153,13 +349,38 @@ async function seedExampleTrips(demo: DemoRepository, adminId: number, demoId: n // Reservations. reservation_time carries the full date, the way the booking // form writes it: a bare clock time is a different shape, and readers that // compare the column against a timestamp cannot tell the two apart (#1934). - await demo.insertReservation(t1, t1days[0], 'Hotel Shinjuku Check-in', '2026-04-15T15:00', 'SG-2026-78432', 'confirmed', 'hotel', 'Shinjuku, Tokyo'); - await demo.insertReservation(t1, t1days[3], 'Shinkansen Tokyo → Kyoto', '2026-04-18T08:30', 'JR-NOZOMI-445', 'confirmed', 'transport', 'Tokyo Station'); + await demo.insertReservation( + t1, + t1days[0], + 'Hotel Shinjuku Check-in', + '2026-04-15T15:00', + 'SG-2026-78432', + 'confirmed', + 'hotel', + 'Shinjuku, Tokyo', + ); + await demo.insertReservation( + t1, + t1days[3], + 'Shinkansen Tokyo → Kyoto', + '2026-04-18T08:30', + 'JR-NOZOMI-445', + 'confirmed', + 'transport', + 'Tokyo Station', + ); await demo.addTripMember(t1, demoId, adminId); // --- Trip 2: Barcelona Long Weekend --- - const t2 = await demo.insertTrip(adminId, 'Barcelona Long Weekend', 'Gaudi, tapas, and Mediterranean vibes — a long weekend in the Catalan capital.', '2026-05-21', '2026-05-24', 'EUR'); + const t2 = await demo.insertTrip( + adminId, + 'Barcelona Long Weekend', + 'Gaudi, tapas, and Mediterranean vibes — a long weekend in the Catalan capital.', + '2026-05-21', + '2026-05-24', + 'EUR', + ); const t2days: number[] = []; for (let i = 0; i < 4; i++) { @@ -167,14 +388,126 @@ async function seedExampleTrips(demo: DemoRepository, adminId: number, demoId: n } const t2places: NewDemoPlaceRow[] = [ - [t2, 'W Barcelona', 41.3686, 2.1920, 'Placa de la Rosa dels Vents 1, 08039 Barcelona, Spain', 1, '14:00', 60, 'Right on the beach. Rooftop bar with panoramic views!', null, 'ChIJKfj5C8yjpBIRCPC3RPI0JO4', 'https://www.marriott.com/hotels/travel/bcnwh-w-barcelona/', '+34 932 95 28 00'], - [t2, 'Sagrada Familia', 41.4036, 2.1744, 'C/ de Mallorca, 401, 08013 Barcelona, Spain', 3, '10:00', 120, 'Gaudi\'s masterpiece. Book tickets online in advance — sells out fast!', null, 'ChIJk_s92NyipBIRUMnDG8Kq2Js', 'https://sagradafamilia.org/', '+34 932 08 04 14'], - [t2, 'Park Guell', 41.4145, 2.1527, '08024 Barcelona, Spain', 3, '09:00', 90, 'Mosaic terrace with city views. Book early for the Monumental Zone.', null, 'ChIJ4eQMeOmipBIRb65JRUzGE8k', 'https://parkguell.barcelona/', '+34 934 09 18 31'], - [t2, 'La Boqueria Market', 41.3816, 2.1717, 'La Rambla, 91, 08001 Barcelona, Spain', 2, '12:00', 75, 'Famous market on La Rambla. Fresh juice, jamon iberico, and seafood!', null, 'ChIJB_RfKcuipBIRkPKW7MzVGKg', 'http://www.boqueria.barcelona/', '+34 933 18 25 84'], - [t2, 'Barceloneta Beach', 41.3784, 2.1925, 'Passeig Maritim de la Barceloneta, 08003 Barcelona, Spain', 8, '16:00', 120, 'City beach to unwind after sightseeing. Great chiringuitos nearby.', null, 'ChIJAQCl79-ipBIRUKF3myrMYkM', null, null], - [t2, 'Gothic Quarter', 41.3834, 2.1762, 'Barri Gotic, 08002 Barcelona, Spain', 3, '15:00', 90, 'Medieval lanes, the cathedral, and Placa Reial. Get lost in the alleys!', null, 'ChIJ4_xkvv2ipBIRrK3bdd-lHgo', null, null], - [t2, 'Casa Batllo', 41.3916, 2.1650, 'Passeig de Gracia, 43, 08007 Barcelona, Spain', 3, '11:00', 75, 'Gaudi\'s dragon house. The facade alone is worth the visit.', null, 'ChIJ-2VKIcaipBIRKK63H5PYjqQ', 'https://www.casabatllo.es/', '+34 932 16 03 06'], - [t2, 'El Born & Tapas', 41.3856, 2.1825, 'El Born, 08003 Barcelona, Spain', 7, '20:00', 120, 'Trendy neighborhood with the best tapas bars. Try Cal Pep or El Xampanyet!', null, 'ChIJNY56dxuipBIRbqjSczmLvIA', null, null], + [ + t2, + 'W Barcelona', + 41.3686, + 2.192, + 'Placa de la Rosa dels Vents 1, 08039 Barcelona, Spain', + 1, + '14:00', + 60, + 'Right on the beach. Rooftop bar with panoramic views!', + null, + 'ChIJKfj5C8yjpBIRCPC3RPI0JO4', + 'https://www.marriott.com/hotels/travel/bcnwh-w-barcelona/', + '+34 932 95 28 00', + ], + [ + t2, + 'Sagrada Familia', + 41.4036, + 2.1744, + 'C/ de Mallorca, 401, 08013 Barcelona, Spain', + 3, + '10:00', + 120, + "Gaudi's masterpiece. Book tickets online in advance — sells out fast!", + null, + 'ChIJk_s92NyipBIRUMnDG8Kq2Js', + 'https://sagradafamilia.org/', + '+34 932 08 04 14', + ], + [ + t2, + 'Park Guell', + 41.4145, + 2.1527, + '08024 Barcelona, Spain', + 3, + '09:00', + 90, + 'Mosaic terrace with city views. Book early for the Monumental Zone.', + null, + 'ChIJ4eQMeOmipBIRb65JRUzGE8k', + 'https://parkguell.barcelona/', + '+34 934 09 18 31', + ], + [ + t2, + 'La Boqueria Market', + 41.3816, + 2.1717, + 'La Rambla, 91, 08001 Barcelona, Spain', + 2, + '12:00', + 75, + 'Famous market on La Rambla. Fresh juice, jamon iberico, and seafood!', + null, + 'ChIJB_RfKcuipBIRkPKW7MzVGKg', + 'http://www.boqueria.barcelona/', + '+34 933 18 25 84', + ], + [ + t2, + 'Barceloneta Beach', + 41.3784, + 2.1925, + 'Passeig Maritim de la Barceloneta, 08003 Barcelona, Spain', + 8, + '16:00', + 120, + 'City beach to unwind after sightseeing. Great chiringuitos nearby.', + null, + 'ChIJAQCl79-ipBIRUKF3myrMYkM', + null, + null, + ], + [ + t2, + 'Gothic Quarter', + 41.3834, + 2.1762, + 'Barri Gotic, 08002 Barcelona, Spain', + 3, + '15:00', + 90, + 'Medieval lanes, the cathedral, and Placa Reial. Get lost in the alleys!', + null, + 'ChIJ4_xkvv2ipBIRrK3bdd-lHgo', + null, + null, + ], + [ + t2, + 'Casa Batllo', + 41.3916, + 2.165, + 'Passeig de Gracia, 43, 08007 Barcelona, Spain', + 3, + '11:00', + 75, + "Gaudi's dragon house. The facade alone is worth the visit.", + null, + 'ChIJ-2VKIcaipBIRKK63H5PYjqQ', + 'https://www.casabatllo.es/', + '+34 932 16 03 06', + ], + [ + t2, + 'El Born & Tapas', + 41.3856, + 2.1825, + 'El Born, 08003 Barcelona, Spain', + 7, + '20:00', + 120, + 'Trendy neighborhood with the best tapas bars. Try Cal Pep or El Xampanyet!', + null, + 'ChIJNY56dxuipBIRbqjSczmLvIA', + null, + null, + ], ]; const t2pIds: number[] = []; @@ -197,7 +530,14 @@ async function seedExampleTrips(demo: DemoRepository, adminId: number, demoId: n await demo.insertDayNote(t2days[3], t2, 'Flight departs at 18:30 — leave hotel by 15:00', '14:00', 'Plane', 1); // Packing - const t2packingNames = ['Passport', 'Sunscreen SPF50', 'Swimwear', 'Sunglasses', 'Comfortable sandals', 'Beach towel']; + const t2packingNames = [ + 'Passport', + 'Sunscreen SPF50', + 'Swimwear', + 'Sunglasses', + 'Comfortable sandals', + 'Beach towel', + ]; for (let i = 0; i < t2packingNames.length; i++) { await demo.insertPackingItem(t2, t2packingNames[i], 0, i < 1 ? 'Documents' : 'Summer', i); } @@ -208,12 +548,28 @@ async function seedExampleTrips(demo: DemoRepository, adminId: number, demoId: n await demo.insertBudgetItem(t2, 'Food', 'Restaurants & tapas', 300, 2, 'Approx. 75 EUR/day'); await demo.insertBudgetItem(t2, 'Activities', 'Sagrada Familia + Park Guell + Casa Batllo', 95, 2, 'Online tickets'); - await demo.insertReservation(t2, t2days[1], 'Sagrada Familia Entry', '2026-05-22T10:00', 'SF-2026-11234', 'confirmed', 'activity', 'Eixample, Barcelona'); + await demo.insertReservation( + t2, + t2days[1], + 'Sagrada Familia Entry', + '2026-05-22T10:00', + 'SF-2026-11234', + 'confirmed', + 'activity', + 'Eixample, Barcelona', + ); await demo.addTripMember(t2, demoId, adminId); // --- Trip 3: New York City --- - const t3 = await demo.insertTrip(adminId, 'New York City', 'The city that never sleeps — iconic landmarks, world-class food, and Broadway lights.', '2026-09-18', '2026-09-22', 'USD'); + const t3 = await demo.insertTrip( + adminId, + 'New York City', + 'The city that never sleeps — iconic landmarks, world-class food, and Broadway lights.', + '2026-09-18', + '2026-09-22', + 'USD', + ); const t3days: number[] = []; for (let i = 0; i < 5; i++) { @@ -221,17 +577,171 @@ async function seedExampleTrips(demo: DemoRepository, adminId: number, demoId: n } const t3places: NewDemoPlaceRow[] = [ - [t3, 'The Plaza Hotel', 40.7645, -73.9744, '768 5th Ave, New York, NY 10019, USA', 1, '15:00', 60, 'Iconic luxury hotel on Central Park. The lobby alone is worth a visit.', null, 'ChIJYbISlAVYwokRn6ORbSPV0xk', 'https://www.theplazany.com/', '+1 212-759-3000'], - [t3, 'Statue of Liberty', 40.6892, -74.0445, 'Liberty Island, New York, NY 10004, USA', 3, '09:00', 180, 'Book crown access tickets months in advance. Ferry from Battery Park.', null, 'ChIJPTacEpBQwokRKwIlDXelxkA', 'https://www.nps.gov/stli/', '+1 212-363-3200'], - [t3, 'Central Park', 40.7829, -73.9654, 'Central Park, New York, NY 10024, USA', 9, '10:00', 120, 'Bethesda Fountain, Bow Bridge, and Strawberry Fields. Rent bikes!', null, 'ChIJ4zGFAZpYwokRGUGph3Mf37k', 'https://www.centralparknyc.org/', null], - [t3, 'Times Square', 40.7580, -73.9855, 'Manhattan, NY 10036, USA', 3, '19:00', 60, 'The crossroads of the world. Best experienced at night with all the lights.', null, 'ChIJmQJIxlVYwokRLgeuocVOGVU', 'https://www.timessquarenyc.org/', null], - [t3, 'Empire State Building', 40.7484, -73.9857, '350 5th Ave, New York, NY 10118, USA', 3, '11:00', 90, '86th floor observation deck. Go at sunset for the best views.', null, 'ChIJaXQRs6lZwokRY6EFpJnhNNE', 'https://www.esbnyc.com/', '+1 212-736-3100'], - [t3, 'Brooklyn Bridge', 40.7061, -73.9969, 'Brooklyn Bridge, New York, NY 10038, USA', 3, '16:00', 75, 'Walk from Manhattan to Brooklyn. DUMBO has great pizza and views.', null, 'ChIJK3vOQyNawokRXEYwET2GUtY', null, null], - [t3, 'The Metropolitan Museum of Art', 40.7794, -73.9632, '1000 5th Ave, New York, NY 10028, USA', 3, '10:00', 180, 'One of the world\'s greatest art museums. Could spend days here.', null, 'ChIJb8Jg766MwokR1YWG0nV7k-E', 'https://www.metmuseum.org/', '+1 212-535-7710'], - [t3, 'Joe\'s Pizza', 40.7309, -73.9969, '7 Carmine St, New York, NY 10014, USA', 2, '13:00', 30, 'New York\'s most famous pizza slice. Cash only, always a line, always worth it.', null, 'ChIJrfCL1IZZwokRwO3NKN22ZBc', 'http://www.joespizzanyc.com/', '+1 212-366-1182'], - [t3, 'Top of the Rock', 40.7593, -73.9794, '30 Rockefeller Plaza, New York, NY 10112, USA', 3, '17:30', 60, 'Better views than Empire State because you can SEE the Empire State.', null, 'ChIJ_y2Fb1JYwokRT_iGzhTLdBo', 'https://www.topoftherocknyc.com/', '+1 212-698-2000'], - [t3, 'Chelsea Market', 40.7424, -74.0061, '75 9th Ave, New York, NY 10011, USA', 2, '12:00', 90, 'Food hall in a converted factory. Lobster rolls, tacos, doughnuts, and more.', null, 'ChIJw2FNFyZZwokRcP9th_vIbkE', 'https://www.chelseamarket.com/', null], - [t3, 'Broadway Show', 40.7590, -73.9845, 'Broadway, Manhattan, NY 10019, USA', 6, '20:00', 150, 'Can\'t visit NYC without seeing a show. Book TKTS booth for discounts.', null, 'ChIJMYQhxFtYwokR7cJBcNqfKDY', null, null], + [ + t3, + 'The Plaza Hotel', + 40.7645, + -73.9744, + '768 5th Ave, New York, NY 10019, USA', + 1, + '15:00', + 60, + 'Iconic luxury hotel on Central Park. The lobby alone is worth a visit.', + null, + 'ChIJYbISlAVYwokRn6ORbSPV0xk', + 'https://www.theplazany.com/', + '+1 212-759-3000', + ], + [ + t3, + 'Statue of Liberty', + 40.6892, + -74.0445, + 'Liberty Island, New York, NY 10004, USA', + 3, + '09:00', + 180, + 'Book crown access tickets months in advance. Ferry from Battery Park.', + null, + 'ChIJPTacEpBQwokRKwIlDXelxkA', + 'https://www.nps.gov/stli/', + '+1 212-363-3200', + ], + [ + t3, + 'Central Park', + 40.7829, + -73.9654, + 'Central Park, New York, NY 10024, USA', + 9, + '10:00', + 120, + 'Bethesda Fountain, Bow Bridge, and Strawberry Fields. Rent bikes!', + null, + 'ChIJ4zGFAZpYwokRGUGph3Mf37k', + 'https://www.centralparknyc.org/', + null, + ], + [ + t3, + 'Times Square', + 40.758, + -73.9855, + 'Manhattan, NY 10036, USA', + 3, + '19:00', + 60, + 'The crossroads of the world. Best experienced at night with all the lights.', + null, + 'ChIJmQJIxlVYwokRLgeuocVOGVU', + 'https://www.timessquarenyc.org/', + null, + ], + [ + t3, + 'Empire State Building', + 40.7484, + -73.9857, + '350 5th Ave, New York, NY 10118, USA', + 3, + '11:00', + 90, + '86th floor observation deck. Go at sunset for the best views.', + null, + 'ChIJaXQRs6lZwokRY6EFpJnhNNE', + 'https://www.esbnyc.com/', + '+1 212-736-3100', + ], + [ + t3, + 'Brooklyn Bridge', + 40.7061, + -73.9969, + 'Brooklyn Bridge, New York, NY 10038, USA', + 3, + '16:00', + 75, + 'Walk from Manhattan to Brooklyn. DUMBO has great pizza and views.', + null, + 'ChIJK3vOQyNawokRXEYwET2GUtY', + null, + null, + ], + [ + t3, + 'The Metropolitan Museum of Art', + 40.7794, + -73.9632, + '1000 5th Ave, New York, NY 10028, USA', + 3, + '10:00', + 180, + "One of the world's greatest art museums. Could spend days here.", + null, + 'ChIJb8Jg766MwokR1YWG0nV7k-E', + 'https://www.metmuseum.org/', + '+1 212-535-7710', + ], + [ + t3, + "Joe's Pizza", + 40.7309, + -73.9969, + '7 Carmine St, New York, NY 10014, USA', + 2, + '13:00', + 30, + "New York's most famous pizza slice. Cash only, always a line, always worth it.", + null, + 'ChIJrfCL1IZZwokRwO3NKN22ZBc', + 'http://www.joespizzanyc.com/', + '+1 212-366-1182', + ], + [ + t3, + 'Top of the Rock', + 40.7593, + -73.9794, + '30 Rockefeller Plaza, New York, NY 10112, USA', + 3, + '17:30', + 60, + 'Better views than Empire State because you can SEE the Empire State.', + null, + 'ChIJ_y2Fb1JYwokRT_iGzhTLdBo', + 'https://www.topoftherocknyc.com/', + '+1 212-698-2000', + ], + [ + t3, + 'Chelsea Market', + 40.7424, + -74.0061, + '75 9th Ave, New York, NY 10011, USA', + 2, + '12:00', + 90, + 'Food hall in a converted factory. Lobster rolls, tacos, doughnuts, and more.', + null, + 'ChIJw2FNFyZZwokRcP9th_vIbkE', + 'https://www.chelseamarket.com/', + null, + ], + [ + t3, + 'Broadway Show', + 40.759, + -73.9845, + 'Broadway, Manhattan, NY 10019, USA', + 6, + '20:00', + 150, + "Can't visit NYC without seeing a show. Book TKTS booth for discounts.", + null, + 'ChIJMYQhxFtYwokR7cJBcNqfKDY', + null, + null, + ], ]; const t3pIds: number[] = []; @@ -245,7 +755,14 @@ async function seedExampleTrips(demo: DemoRepository, adminId: number, demoId: n await demo.insertDayAssignment(t3days[1], t3pIds[1], 0); await demo.insertDayAssignment(t3days[1], t3pIds[5], 1); await demo.insertDayAssignment(t3days[1], t3pIds[7], 2); - await demo.insertDayNote(t3days[1], t3, 'First ferry at 8:30 AM — arrive early at Battery Park', '08:00', 'Ship', 0.5); + await demo.insertDayNote( + t3days[1], + t3, + 'First ferry at 8:30 AM — arrive early at Battery Park', + '08:00', + 'Ship', + 0.5, + ); // Day 3: Central Park, Met Museum, Top of the Rock sunset await demo.insertDayAssignment(t3days[2], t3pIds[2], 0); await demo.insertDayAssignment(t3days[2], t3pIds[6], 1); @@ -255,27 +772,73 @@ async function seedExampleTrips(demo: DemoRepository, adminId: number, demoId: n await demo.insertDayAssignment(t3days[3], t3pIds[9], 1); await demo.insertDayNote(t3days[3], t3, 'SoHo and 5th Avenue shopping in the afternoon', '14:00', 'ShoppingBag', 1.5); // Day 5: Free morning, departure - await demo.insertDayNote(t3days[4], t3, 'Flight departs JFK at 17:00 — last bagel at Russ & Daughters!', '10:00', 'Plane', 0); + await demo.insertDayNote( + t3days[4], + t3, + 'Flight departs JFK at 17:00 — last bagel at Russ & Daughters!', + '10:00', + 'Plane', + 0, + ); // Packing const t3packing: [string, number, string, number][] = [ - ['Passport', 1, 'Documents', 0], ['ESTA confirmation', 1, 'Documents', 1], - ['Travel insurance', 0, 'Documents', 2], ['Comfortable sneakers', 0, 'Clothing', 3], - ['Light jacket', 0, 'Clothing', 4], ['Portable charger', 0, 'Electronics', 5], - ['Camera', 0, 'Electronics', 6], ['Subway card (OMNY)', 0, 'Transport', 7], + ['Passport', 1, 'Documents', 0], + ['ESTA confirmation', 1, 'Documents', 1], + ['Travel insurance', 0, 'Documents', 2], + ['Comfortable sneakers', 0, 'Clothing', 3], + ['Light jacket', 0, 'Clothing', 4], + ['Portable charger', 0, 'Electronics', 5], + ['Camera', 0, 'Electronics', 6], + ['Subway card (OMNY)', 0, 'Transport', 7], ]; - for (const [name, checked, category, sortOrder] of t3packing) await demo.insertPackingItem(t3, name, checked, category, sortOrder); + for (const [name, checked, category, sortOrder] of t3packing) + await demo.insertPackingItem(t3, name, checked, category, sortOrder); // Budget await demo.insertBudgetItem(t3, 'Accommodation', 'The Plaza Hotel (4 nights)', 2400, 2, 'Park View Room'); await demo.insertBudgetItem(t3, 'Transport', 'Flights FRA-JFK return', 850, 2, 'United Airlines'); await demo.insertBudgetItem(t3, 'Food', 'Daily food budget', 500, 2, 'Approx. 100 USD/day'); - await demo.insertBudgetItem(t3, 'Activities', 'Statue of Liberty + Empire State + Top of the Rock + Met', 180, 2, 'CityPASS'); + await demo.insertBudgetItem( + t3, + 'Activities', + 'Statue of Liberty + Empire State + Top of the Rock + Met', + 180, + 2, + 'CityPASS', + ); await demo.insertBudgetItem(t3, 'Entertainment', 'Broadway show tickets', 300, 2, 'Hamilton or Wicked'); - await demo.insertReservation(t3, t3days[0], 'The Plaza Hotel Check-in', '2026-09-18T15:00', 'PZ-2026-55891', 'confirmed', 'hotel', '768 5th Ave, New York'); - await demo.insertReservation(t3, t3days[0], 'Broadway Show', '2026-09-18T20:00', 'BW-HAM-2026-1192', 'pending', 'activity', 'Richard Rodgers Theatre'); - await demo.insertReservation(t3, t3days[1], 'Statue of Liberty Ferry', '2026-09-19T08:30', 'SOL-2026-3347', 'confirmed', 'transport', 'Battery Park'); + await demo.insertReservation( + t3, + t3days[0], + 'The Plaza Hotel Check-in', + '2026-09-18T15:00', + 'PZ-2026-55891', + 'confirmed', + 'hotel', + '768 5th Ave, New York', + ); + await demo.insertReservation( + t3, + t3days[0], + 'Broadway Show', + '2026-09-18T20:00', + 'BW-HAM-2026-1192', + 'pending', + 'activity', + 'Richard Rodgers Theatre', + ); + await demo.insertReservation( + t3, + t3days[1], + 'Statue of Liberty Ferry', + '2026-09-19T08:30', + 'SOL-2026-3347', + 'confirmed', + 'transport', + 'Battery Park', + ); await demo.addTripMember(t3, demoId, adminId); diff --git a/server/src/index.ts b/server/src/index.ts index a7794f961..ebad020c5 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -4,23 +4,24 @@ import 'dotenv/config'; // variable aborts before any other module runs its import-time side effects // (config.ts key resolution, ...). import './app-config/boot-validate'; -import fs from 'node:fs'; -import http from 'node:http'; -import type { INestApplication } from '@nestjs/common'; + +import { getAppUrl, getMcpSafeUrl, readEnv } from './app-config'; +import { resolveDataPaths } from './app-config/data-paths'; // bootstrap is required inside bootstrap() below, not imported here. Importing // it no longer opens the database (the ORM's first connect inside buildApp() // does, through DatabaseLifecycle), but a first-start restore (#1089) has to // put the backup's database in place before anything near it loads, and // keeping the require after the restore keeps that true by construction. import type * as Bootstrap from './bootstrap'; -import type { DatabaseLifecycle } from './nest/database/database-lifecycle.service'; - -import { getAppUrl, getMcpSafeUrl, readEnv } from './app-config'; -import { resolveDataPaths } from './app-config/data-paths'; import { resolveDbPath } from './db/db-path'; import { flushLogFileSync, logError } from './nest/audit/audit-log.logger'; +import type { DatabaseLifecycle } from './nest/database/database-lifecycle.service'; import { ReadinessService } from './nest/health/readiness.service'; import { createFatalHandler } from './shutdown'; +import type { INestApplication } from '@nestjs/common'; + +import fs from 'node:fs'; +import http from 'node:http'; // data/tmp is the driver-agnostic global scratch dir (restore-upload spool, // mirror stream staging) and stays boot-created here. Driver-owned roots — the @@ -61,21 +62,25 @@ const onListen = () => { ` User: uid=${process.getuid?.()} gid=${process.getgid?.()}`, '──────────────────────────────────────', ]; - banner.forEach(l => console.log(l)); + banner.forEach((l) => console.log(l)); sLogInfo('NestJS serving all routes (Express decommissioned)'); if (env.app.appUrl) { let parsedAppUrl: URL | null = null; - try { parsedAppUrl = new URL(env.app.appUrl); } catch { /* invalid */ } + try { + parsedAppUrl = new URL(env.app.appUrl); + } catch { + /* invalid */ + } if (!parsedAppUrl) { sLogWarn(`APP_URL: "${env.app.appUrl}" is not a valid URL — it will be ignored.`); } - const mcpSafe = parsedAppUrl !== null && ( - parsedAppUrl.protocol === 'https:' || - parsedAppUrl.hostname === 'localhost' || - parsedAppUrl.hostname === '127.0.0.1' - ); + const mcpSafe = + parsedAppUrl !== null && + (parsedAppUrl.protocol === 'https:' || + parsedAppUrl.hostname === 'localhost' || + parsedAppUrl.hostname === '127.0.0.1'); if (!mcpSafe) { sLogWarn(`APP_URL: not MCP-safe (requires https:// or http://localhost) — MCP will use ${resolvedAppUrl}.`); } @@ -150,14 +155,17 @@ async function finishFirstBootRestore( app: INestApplication, restore: { uploads: string | null; staging: string }, ): Promise { - const { StorageService } = require('./nest/storage/storage.service') as typeof import('./nest/storage/storage.service'); + const { StorageService } = + require('./nest/storage/storage.service') as typeof import('./nest/storage/storage.service'); const { rehydrateUploads } = require('./nest/backup/backup.impl') as typeof import('./nest/backup/backup.impl'); try { if (restore.uploads) await rehydrateUploads(app.get(StorageService), restore.uploads); fs.rmSync(restore.staging, { recursive: true, force: true }); console.log('[restore] Backup restored. Sign in with an account from the backup.'); } catch (err) { - console.error(`[restore] The database was restored, but the uploads could not be copied (${err instanceof Error ? err.message : String(err)}). They are still in ${restore.staging}.`); + console.error( + `[restore] The database was restored, but the uploads could not be copied (${err instanceof Error ? err.message : String(err)}). They are still in ${restore.staging}.`, + ); } } @@ -193,7 +201,9 @@ function shutdown(signal: string, code = 0): void { server, // nestApp.close() stops every cron via the scheduling registrar's shutdown // hook, and tears the plugin supervisor's forked children down. - closeNestApp: async () => { await nestApp?.close(); }, + closeNestApp: async () => { + await nestApp?.close(); + }, getWsClients: () => getServer()?.clients ?? null, closeMcpSessions, // Through the lifecycle provider once the app is up; before that (a signal diff --git a/server/src/mcp/index.ts b/server/src/mcp/index.ts index 87f4ad419..a0fb89228 100644 --- a/server/src/mcp/index.ts +++ b/server/src/mcp/index.ts @@ -46,8 +46,16 @@ const sessionSweepInterval = setInterval(() => { let cleaned = 0; for (const [sid, session] of sessions) { if (session.lastActivity < cutoff) { - try { session.server.close(); } catch { /* ignore */ } - try { session.transport.close(); } catch { /* ignore */ } + try { + session.server.close(); + } catch { + /* ignore */ + } + try { + session.transport.close(); + } catch { + /* ignore */ + } sessions.delete(sid); cleaned++; } @@ -70,8 +78,16 @@ sessionSweepInterval.unref(); export function closeMcpSessions(): void { clearInterval(sessionSweepInterval); for (const [, session] of sessions) { - try { session.server.close(); } catch { /* ignore */ } - try { session.transport.close(); } catch { /* ignore */ } + try { + session.server.close(); + } catch { + /* ignore */ + } + try { + session.transport.close(); + } catch { + /* ignore */ + } } sessions.clear(); rateLimitMap.clear(); diff --git a/server/src/mcp/scopes.ts b/server/src/mcp/scopes.ts index 14dce363c..8cda41bfd 100644 --- a/server/src/mcp/scopes.ts +++ b/server/src/mcp/scopes.ts @@ -3,45 +3,45 @@ // --------------------------------------------------------------------------- export const SCOPES = { - TRIPS_READ: 'trips:read', - TRIPS_WRITE: 'trips:write', - TRIPS_DELETE: 'trips:delete', - TRIPS_SHARE: 'trips:share', - PLACES_READ: 'places:read', - PLACES_WRITE: 'places:write', - COLLECTIONS_READ: 'collections:read', - COLLECTIONS_WRITE: 'collections:write', - ATLAS_READ: 'atlas:read', - ATLAS_WRITE: 'atlas:write', - PACKING_READ: 'packing:read', - PACKING_WRITE: 'packing:write', - TODOS_READ: 'todos:read', - TODOS_WRITE: 'todos:write', - BUDGET_READ: 'budget:read', - BUDGET_WRITE: 'budget:write', - RESERVATIONS_READ: 'reservations:read', - RESERVATIONS_WRITE: 'reservations:write', - COLLAB_READ: 'collab:read', - COLLAB_WRITE: 'collab:write', - NOTIFICATIONS_READ: 'notifications:read', + TRIPS_READ: 'trips:read', + TRIPS_WRITE: 'trips:write', + TRIPS_DELETE: 'trips:delete', + TRIPS_SHARE: 'trips:share', + PLACES_READ: 'places:read', + PLACES_WRITE: 'places:write', + COLLECTIONS_READ: 'collections:read', + COLLECTIONS_WRITE: 'collections:write', + ATLAS_READ: 'atlas:read', + ATLAS_WRITE: 'atlas:write', + PACKING_READ: 'packing:read', + PACKING_WRITE: 'packing:write', + TODOS_READ: 'todos:read', + TODOS_WRITE: 'todos:write', + BUDGET_READ: 'budget:read', + BUDGET_WRITE: 'budget:write', + RESERVATIONS_READ: 'reservations:read', + RESERVATIONS_WRITE: 'reservations:write', + COLLAB_READ: 'collab:read', + COLLAB_WRITE: 'collab:write', + NOTIFICATIONS_READ: 'notifications:read', NOTIFICATIONS_WRITE: 'notifications:write', - VACAY_READ: 'vacay:read', - VACAY_WRITE: 'vacay:write', - GEO_READ: 'geo:read', - WEATHER_READ: 'weather:read', - JOURNEY_READ: 'journey:read', - JOURNEY_WRITE: 'journey:write', - JOURNEY_SHARE: 'journey:share', - FILES_READ: 'files:read', - FILES_WRITE: 'files:write', + VACAY_READ: 'vacay:read', + VACAY_WRITE: 'vacay:write', + GEO_READ: 'geo:read', + WEATHER_READ: 'weather:read', + JOURNEY_READ: 'journey:read', + JOURNEY_WRITE: 'journey:write', + JOURNEY_SHARE: 'journey:share', + FILES_READ: 'files:read', + FILES_WRITE: 'files:write', // A third mode rather than a second group: listing what a trip carries and // reading the bytes of a booking PDF are different privileges, and the plugin // host already draws that line (db:read:files vs db:read:files:content). Not // implied by files:write, the way journey:share is not implied by // journey:write. The policy's fallback branch covers both. - FILES_CONTENT: 'files:content', - SETTINGS_READ: 'settings:read', - SETTINGS_WRITE: 'settings:write', + FILES_CONTENT: 'files:content', + SETTINGS_READ: 'settings:read', + SETTINGS_WRITE: 'settings:write', // One coarse scope, and a fourth mode rather than a per-plugin group. Which // plugins are installed is runtime data, so a per-plugin scope cannot be // statically enumerated: it would break the ${group}:${mode} derivation and @@ -52,10 +52,10 @@ export const SCOPES = { // Per-plugin granularity already exists one layer down, as the admin's // mcp:tools grant, and that is the real boundary: this scope only decides // whether plugin tools are advertised to a client at all. - PLUGINS_USE: 'plugins:use', + PLUGINS_USE: 'plugins:use', } as const; -export type Scope = typeof SCOPES[keyof typeof SCOPES]; +export type Scope = (typeof SCOPES)[keyof typeof SCOPES]; /** 'trips' | 'places' | ... — derived from SCOPES; adding a scope extends it. */ export type ScopeGroup = Scope extends `${infer G}:${string}` ? G : never; @@ -88,41 +88,174 @@ export interface ScopeInfo { } export const SCOPE_INFO: Record = { - 'trips:read': { label: 'View trips & itineraries', description: 'Read trips, days, day notes, and members', group: 'Trips' }, - 'trips:write': { label: 'Edit trips & itineraries', description: 'Create and update trips, days, notes, and manage members', group: 'Trips' }, - 'trips:delete': { label: 'Delete trips', description: 'Permanently delete entire trips — this action is irreversible', group: 'Trips' }, - 'trips:share': { label: 'Manage share links', description: 'Create, update, and revoke public share links for trips', group: 'Trips' }, - 'places:read': { label: 'View places & map data', description: 'Read places, day assignments, tags, and categories', group: 'Places' }, - 'places:write': { label: 'Manage places', description: 'Create, update, and delete places, assignments, and tags', group: 'Places' }, - 'collections:read': { label: 'View collections', description: 'Read saved-place collections, their places, ratings, labels, and members', group: 'Collections' }, - 'collections:write': { label: 'Manage collections', description: 'Create/edit collections, save, rate, label and copy places, and share lists', group: 'Collections' }, - 'atlas:read': { label: 'View Atlas', description: 'Read visited countries, regions, and bucket list', group: 'Atlas' }, - 'atlas:write': { label: 'Manage Atlas', description: 'Mark countries and regions visited, manage bucket list', group: 'Atlas' }, - 'packing:read': { label: 'View packing lists', description: 'Read packing items, bags, and category assignees', group: 'Packing' }, - 'packing:write': { label: 'Manage packing lists', description: 'Add, update, delete, toggle, and reorder packing items and bags', group: 'Packing' }, - 'todos:read': { label: 'View to-do lists', description: 'Read trip to-do items and category assignees', group: 'To-dos' }, - 'todos:write': { label: 'Manage to-do lists', description: 'Create, update, toggle, delete, and reorder to-do items', group: 'To-dos' }, - 'budget:read': { label: 'View budget', description: 'Read budget items and expense breakdown', group: 'Budget' }, - 'budget:write': { label: 'Manage budget', description: 'Create, update, and delete budget items', group: 'Budget' }, - 'reservations:read': { label: 'View reservations', description: 'Read reservations and accommodation details', group: 'Reservations' }, - 'reservations:write': { label: 'Manage reservations', description: 'Create, update, delete, and reorder reservations', group: 'Reservations' }, - 'collab:read': { label: 'View collaboration', description: 'Read collab notes, polls, and messages', group: 'Collaboration' }, - 'collab:write': { label: 'Manage collaboration', description: 'Create, update, and delete collab notes, polls, and messages', group: 'Collaboration' }, - 'notifications:read': { label: 'View notifications', description: 'Read in-app notifications and unread counts', group: 'Notifications' }, - 'notifications:write': { label: 'Manage notifications', description: 'Mark notifications as read and respond to them', group: 'Notifications' }, - 'vacay:read': { label: 'View vacation plans', description: 'Read vacation planning data, entries, and stats', group: 'Vacation' }, - 'vacay:write': { label: 'Manage vacation plans', description: 'Create and manage vacation entries, holidays, and team plans', group: 'Vacation' }, - 'geo:read': { label: 'Maps, geocoding & transit', description: 'Search locations and public transit routes, resolve map URLs, and reverse geocode coordinates', group: 'Geo' }, - 'weather:read': { label: 'Weather forecasts', description: 'Fetch weather forecasts for trip locations and dates', group: 'Weather' }, - 'journey:read': { label: 'View journeys', description: 'Read journeys, entries, and contributor list', group: 'Journey' }, - 'journey:write': { label: 'Manage journeys', description: 'Create, update, and delete journeys and their entries', group: 'Journey' }, - 'journey:share': { label: 'Manage journey links', description: 'Create, update, and revoke public share links for journeys', group: 'Journey' }, - 'files:read': { label: 'View trip files', description: 'List the documents on a trip: names, sizes, who uploaded them, what they link to', group: 'Files' }, - 'files:write': { label: 'Organise trip files', description: 'Rename and describe files, link them to bookings and places, star and trash them', group: 'Files' }, - 'files:content': { label: 'Read file contents', description: 'Read what is inside an uploaded document, such as a booking PDF or a ticket', group: 'Files' }, - 'settings:read': { label: 'View your preferences', description: 'Read units, time format, language, default currency, and start page', group: 'Settings' }, - 'settings:write': { label: 'Change your preferences', description: 'Change units, time format, language, default currency, and start page. Never stored API keys', group: 'Settings' }, - 'plugins:use': { label: 'Run plugin tools', description: 'Let this client call tools published by the plugins an administrator installed and approved. Each plugin acts with the access it was already granted, not with the scopes on this token', group: 'Plugins' }, + 'trips:read': { + label: 'View trips & itineraries', + description: 'Read trips, days, day notes, and members', + group: 'Trips', + }, + 'trips:write': { + label: 'Edit trips & itineraries', + description: 'Create and update trips, days, notes, and manage members', + group: 'Trips', + }, + 'trips:delete': { + label: 'Delete trips', + description: 'Permanently delete entire trips — this action is irreversible', + group: 'Trips', + }, + 'trips:share': { + label: 'Manage share links', + description: 'Create, update, and revoke public share links for trips', + group: 'Trips', + }, + 'places:read': { + label: 'View places & map data', + description: 'Read places, day assignments, tags, and categories', + group: 'Places', + }, + 'places:write': { + label: 'Manage places', + description: 'Create, update, and delete places, assignments, and tags', + group: 'Places', + }, + 'collections:read': { + label: 'View collections', + description: 'Read saved-place collections, their places, ratings, labels, and members', + group: 'Collections', + }, + 'collections:write': { + label: 'Manage collections', + description: 'Create/edit collections, save, rate, label and copy places, and share lists', + group: 'Collections', + }, + 'atlas:read': { + label: 'View Atlas', + description: 'Read visited countries, regions, and bucket list', + group: 'Atlas', + }, + 'atlas:write': { + label: 'Manage Atlas', + description: 'Mark countries and regions visited, manage bucket list', + group: 'Atlas', + }, + 'packing:read': { + label: 'View packing lists', + description: 'Read packing items, bags, and category assignees', + group: 'Packing', + }, + 'packing:write': { + label: 'Manage packing lists', + description: 'Add, update, delete, toggle, and reorder packing items and bags', + group: 'Packing', + }, + 'todos:read': { + label: 'View to-do lists', + description: 'Read trip to-do items and category assignees', + group: 'To-dos', + }, + 'todos:write': { + label: 'Manage to-do lists', + description: 'Create, update, toggle, delete, and reorder to-do items', + group: 'To-dos', + }, + 'budget:read': { label: 'View budget', description: 'Read budget items and expense breakdown', group: 'Budget' }, + 'budget:write': { label: 'Manage budget', description: 'Create, update, and delete budget items', group: 'Budget' }, + 'reservations:read': { + label: 'View reservations', + description: 'Read reservations and accommodation details', + group: 'Reservations', + }, + 'reservations:write': { + label: 'Manage reservations', + description: 'Create, update, delete, and reorder reservations', + group: 'Reservations', + }, + 'collab:read': { + label: 'View collaboration', + description: 'Read collab notes, polls, and messages', + group: 'Collaboration', + }, + 'collab:write': { + label: 'Manage collaboration', + description: 'Create, update, and delete collab notes, polls, and messages', + group: 'Collaboration', + }, + 'notifications:read': { + label: 'View notifications', + description: 'Read in-app notifications and unread counts', + group: 'Notifications', + }, + 'notifications:write': { + label: 'Manage notifications', + description: 'Mark notifications as read and respond to them', + group: 'Notifications', + }, + 'vacay:read': { + label: 'View vacation plans', + description: 'Read vacation planning data, entries, and stats', + group: 'Vacation', + }, + 'vacay:write': { + label: 'Manage vacation plans', + description: 'Create and manage vacation entries, holidays, and team plans', + group: 'Vacation', + }, + 'geo:read': { + label: 'Maps, geocoding & transit', + description: 'Search locations and public transit routes, resolve map URLs, and reverse geocode coordinates', + group: 'Geo', + }, + 'weather:read': { + label: 'Weather forecasts', + description: 'Fetch weather forecasts for trip locations and dates', + group: 'Weather', + }, + 'journey:read': { + label: 'View journeys', + description: 'Read journeys, entries, and contributor list', + group: 'Journey', + }, + 'journey:write': { + label: 'Manage journeys', + description: 'Create, update, and delete journeys and their entries', + group: 'Journey', + }, + 'journey:share': { + label: 'Manage journey links', + description: 'Create, update, and revoke public share links for journeys', + group: 'Journey', + }, + 'files:read': { + label: 'View trip files', + description: 'List the documents on a trip: names, sizes, who uploaded them, what they link to', + group: 'Files', + }, + 'files:write': { + label: 'Organise trip files', + description: 'Rename and describe files, link them to bookings and places, star and trash them', + group: 'Files', + }, + 'files:content': { + label: 'Read file contents', + description: 'Read what is inside an uploaded document, such as a booking PDF or a ticket', + group: 'Files', + }, + 'settings:read': { + label: 'View your preferences', + description: 'Read units, time format, language, default currency, and start page', + group: 'Settings', + }, + 'settings:write': { + label: 'Change your preferences', + description: 'Change units, time format, language, default currency, and start page. Never stored API keys', + group: 'Settings', + }, + 'plugins:use': { + label: 'Run plugin tools', + description: + 'Let this client call tools published by the plugins an administrator installed and approved. Each plugin acts with the access it was already granted, not with the scopes on this token', + group: 'Plugins', + }, }; // --------------------------------------------------------------------------- @@ -133,7 +266,7 @@ export const SCOPE_INFO: Record = { /** trips:read OR trips:write OR trips:delete OR trips:share all grant read access to trips */ export function canReadTrips(scopes: string[] | null): boolean { if (!scopes) return true; - return scopes.some(s => s === 'trips:read' || s === 'trips:write' || s === 'trips:delete' || s === 'trips:share'); + return scopes.some((s) => s === 'trips:read' || s === 'trips:write' || s === 'trips:delete' || s === 'trips:share'); } /** group:write grants write access; for trips canReadTrips handles read */ @@ -145,7 +278,7 @@ export function canWrite(scopes: string[] | null, group: ScopeGroup): boolean { /** group:read OR group:write grant read access */ export function canRead(scopes: string[] | null, group: ScopeGroup): boolean { if (!scopes) return true; - return scopes.some(s => s === `${group}:read` || s === `${group}:write`); + return scopes.some((s) => s === `${group}:read` || s === `${group}:write`); } /** trips:delete is a separate scope from trips:write */ @@ -176,6 +309,6 @@ export function canShareJourneys(scopes: string[] | null): boolean { } export function validateScopes(requestedScopes: string[]): { valid: boolean; invalid: string[] } { - const invalid = requestedScopes.filter(s => !ALL_SCOPES.includes(s as Scope)); + const invalid = requestedScopes.filter((s) => !ALL_SCOPES.includes(s as Scope)); return { valid: invalid.length === 0, invalid }; } diff --git a/server/src/mcp/sessionManager.ts b/server/src/mcp/sessionManager.ts index a0e260d83..0d6db866e 100644 --- a/server/src/mcp/sessionManager.ts +++ b/server/src/mcp/sessionManager.ts @@ -21,8 +21,16 @@ export const sessions = new Map(); /** Close both halves of a session and drop it from the map. Close errors are non-fatal: * a transport whose socket is already gone must not block eviction of the map entry. */ function closeSession(sid: string, session: McpSession): void { - try { session.server.close(); } catch { /* ignore */ } - try { session.transport.close(); } catch { /* ignore */ } + try { + session.server.close(); + } catch { + /* ignore */ + } + try { + session.transport.close(); + } catch { + /* ignore */ + } sessions.delete(sid); } diff --git a/server/src/mcp/tools.ts b/server/src/mcp/tools.ts index b340975da..b3955b28c 100644 --- a/server/src/mcp/tools.ts +++ b/server/src/mcp/tools.ts @@ -1,6 +1,6 @@ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; import type { McpAttachOptions, McpRegistry } from '../nest-mcp'; import { getPluginMcpToolSource } from '../plugin-mcp-tools'; +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp'; /** * Attaches the MCP surface to a session's server. @@ -18,6 +18,21 @@ import { getPluginMcpToolSource } from '../plugin-mcp-tools'; * exist and an app built without the plugins runtime resolves it to undefined. * Tests pass it explicitly instead of reaching for the global. */ -export async function registerTools(registry: McpRegistry | null, server: McpServer, userId: number, scopes: string[] | null, isStaticToken = false, getDeprecationNotice: () => string | null = () => null, onInvoke?: McpAttachOptions['onInvoke'], dynamicTools: McpAttachOptions['dynamicTools'] = getPluginMcpToolSource() ?? undefined, around?: McpAttachOptions['around']): Promise { - if (registry) await registry.attach(server, { userId, scopes, isStaticToken, getDeprecationNotice }, { onInvoke, dynamicTools, around }); +export async function registerTools( + registry: McpRegistry | null, + server: McpServer, + userId: number, + scopes: string[] | null, + isStaticToken = false, + getDeprecationNotice: () => string | null = () => null, + onInvoke?: McpAttachOptions['onInvoke'], + dynamicTools: McpAttachOptions['dynamicTools'] = getPluginMcpToolSource() ?? undefined, + around?: McpAttachOptions['around'], +): Promise { + if (registry) + await registry.attach( + server, + { userId, scopes, isStaticToken, getDeprecationNotice }, + { onInvoke, dynamicTools, around }, + ); } diff --git a/server/src/middleware/globalMiddleware.ts b/server/src/middleware/globalMiddleware.ts index d1f9a5afd..61e1186bb 100644 --- a/server/src/middleware/globalMiddleware.ts +++ b/server/src/middleware/globalMiddleware.ts @@ -1,14 +1,6 @@ -import express, { Request, Response, NextFunction } from 'express'; -import { AsyncResource } from 'node:async_hooks'; -import compression from 'compression'; -import cors from 'cors'; -import helmet from 'helmet'; -import cookieParser from 'cookie-parser'; -import type { ConfigType } from '@nestjs/config'; import { readEnv } from '../app-config'; import { httpConfig } from '../nest/app-config/tokens'; import { logDebug, logWarn, logError } from '../nest/audit/audit-log.logger'; -import { isSameHostOrigin } from '../nest/common/same-origin'; import { ACCESS_LOG_ATTACHED, REQUEST_ID_HEADER, @@ -17,6 +9,15 @@ import { newCorrelationId, runWithCorrelation, } from '../nest/common/request-correlation'; +import { isSameHostOrigin } from '../nest/common/same-origin'; +import type { ConfigType } from '@nestjs/config'; + +import compression from 'compression'; +import cookieParser from 'cookie-parser'; +import cors from 'cors'; +import express, { Request, Response, NextFunction } from 'express'; +import helmet from 'helmet'; +import { AsyncResource } from 'node:async_hooks'; /** * Field names redacted from request-log query/body dumps (case-insensitive — @@ -245,120 +246,135 @@ export function applyGlobalMiddleware( // session per tool call until the per-user cap wedges the connection. Same reasoning for // WWW-Authenticate, which carries the RFC 9728 resource-metadata challenge that drives // OAuth discovery. - app.use( - (req: Request, _res: Response, next: NextFunction) => { - if ( - req.path.startsWith('/.well-known/') || - req.path === '/oauth/register' || - req.path === '/oauth/authorize' || - req.path === '/oauth/userinfo' || - req.path === '/mcp' - ) { - cors({ - origin: '*', - credentials: false, - exposedHeaders: ['Mcp-Session-Id', 'MCP-Protocol-Version', 'WWW-Authenticate'], - })(req, _res, next); - } else { - next(); - } - }, - ); + app.use((req: Request, _res: Response, next: NextFunction) => { + if ( + req.path.startsWith('/.well-known/') || + req.path === '/oauth/register' || + req.path === '/oauth/authorize' || + req.path === '/oauth/userinfo' || + req.path === '/mcp' + ) { + cors({ + origin: '*', + credentials: false, + exposedHeaders: ['Mcp-Session-Id', 'MCP-Protocol-Version', 'WWW-Authenticate'], + })(req, _res, next); + } else { + next(); + } + }); app.use(cors(corsOptions)); - app.use(helmet({ - contentSecurityPolicy: { - directives: { - defaultSrc: ["'self'"], - // 'unsafe-eval' is load-bearing, not leftover: heic-to's libheif build - // initialises embind through new Function(), and that is what converts - // an iPhone .heic the moment somebody picks one. 'wasm-unsafe-eval' - // alone was tried first and was not enough (93b51a0b). The package - // ships a CSP-safe entry point at heic-to/csp; dropping this directive - // means switching client/src/utils/convertHeic.ts over to it and - // verifying a real .heic upload in a browser, not just deleting the - // string here. - scriptSrc: ["'self'", "'wasm-unsafe-eval'", "'unsafe-eval'"], - styleSrc: ["'self'", "'unsafe-inline'", "https://fonts.googleapis.com", "https://unpkg.com"], - imgSrc: ["'self'", "data:", "blob:", "https:"], - connectSrc: [ - "'self'", "ws:", "wss:", - "https://nominatim.openstreetmap.org", "https://overpass-api.de", - "https://places.googleapis.com", "https://api.openweathermap.org", - "https://en.wikipedia.org", "https://commons.wikimedia.org", - // Both forms here too: CARTO documents the apex host on its key page, - // so that is the template users paste in, and the {s} sharded form is - // what TREK ships (#2054). - "https://basemaps.cartocdn.com", "https://*.basemaps.cartocdn.com", - // Both forms: a CSP wildcard host never matches the apex, and OSM - // serves everything from the bare tile.openstreetmap.org since it - // retired the a/b/c/d shards (#1733). The sharded hosts stay listed - // for tile templates users saved before that. - "https://tile.openstreetmap.org", "https://*.tile.openstreetmap.org", - // The other two raster presets TREK ships. `mode: 'no-cors'` relaxes - // CORS, not CSP, so without these the tile prefetch is refused in the - // document and never reaches the Service Worker that would cache it - // (#2180). routing.openstreetmap.de below is a different host. - "https://tile.openstreetmap.de", "https://tiles.stadiamaps.com", - // OpenTopoMap is the key-free raster layer offered only by the Tours - // planner. Leaflet uses its a/b/c shards; the apex is named too so a - // future unsharded template does not repeat the OSM CSP gap above. - "https://tile.opentopomap.org", "https://*.tile.opentopomap.org", - // The imagery host, for the same reason and one more. Leaflet fetches a tile - // as an , which img-src's blanket `https:` waves through, so the satellite - // view worked on Leaflet with this host missing. A GL map reads the raster - // through fetch to hand it to WebGL, and the prefetch does too, so both were - // refused here while nothing in the app could see it: the switch flipped, the - // layer went on, and no tile ever arrived (#2307). - "https://server.arcgisonline.com", - // Amap's raster tiles, for an install whose users are in China, for the - // same reason as the hosts above (#2180). Road (webrd01..04) and - // satellite (webst01..04) are numbered shards of one domain, so the - // wildcard is the whole list. - "https://*.is.autonavi.com", - "https://unpkg.com", "https://open-meteo.com", "https://api.open-meteo.com", - "https://geocoding-api.open-meteo.com", "https://api.frankfurter.dev", - "https://router.project-osrm.org/route/v1/", "https://routing.openstreetmap.de/", - // The second routing engine, shipped as a default the same way the OSRM hosts - // above are. It is asked only when a leg should avoid tolls, motorways or a - // ferry — which the OSRM hosts answer with HTTP 400, because their car profile - // carries no excludable classes. Origin only, no path: unlike OSRM this one is - // a POST to /route and would grow more endpoints if isochrones ever land. - "https://valhalla1.openstreetmap.de", - "https://api.mapbox.com", "https://*.tiles.mapbox.com", "https://events.mapbox.com", - "https://tiles.openfreemap.org", - // A self-hosted routing engine, when the instance has one configured. Without - // this the browser blocks it silently: no error the app can catch, just legs - // that never route (#1797). - ...extraConnectSrc, - ], - workerSrc: ["'self'", "blob:"], - childSrc: ["'self'", "blob:"], - // blob: because a picked clip is previewed and its poster frame grabbed - // through a