refactor(addons): the transit-provider setting and photo-provider routes move into the shared kernel

AddonsService read the transit backend through transit/transit-provider.ts
and built photo-provider endpoint paths with memories.helpers, so the addons
domain, which nearly every domain imports for its gates, depended on
transit and memories. Both are free functions over a repository or plain
strings; they move unchanged to common/, where the transit and admin sides
read them too.
This commit is contained in:
Maurice
2026-10-10 12:36:06 +02:00
parent 846d8d4952
commit 806f3c8e62
10 changed files with 71 additions and 58 deletions
@@ -3,44 +3,21 @@
"domainCycles": [
"accommodations -> assignments",
"accommodations -> places",
"addons -> memories",
"addons -> transit",
"assignments -> days",
"assignments -> journey",
"atlas -> addons",
"auth -> plugins",
"budget -> addons",
"days -> accommodations",
"days -> assignments",
"days -> places",
"geo -> maps",
"integrations -> addons",
"integrations -> assignments",
"integrations -> atlas",
"integrations -> journey",
"integrations -> places",
"integrations -> reservations",
"journey -> addons",
"journey -> atlas",
"journey -> memories",
"journey -> photos",
"maps -> geo",
"memories -> addons",
"memories -> photos",
"photos -> memories",
"places -> accommodations",
"places -> assignments",
"places -> atlas",
"places -> journey",
"plugins -> trips",
"plugins -> vacay",
"reservations -> accommodations",
"reservations -> assignments",
"reservations -> budget",
"reservations -> days",
"reservations -> integrations",
"transit -> days",
"transit -> reservations",
"trip-members -> auth",
"trip-members -> trips",
"trip-read-model -> trip-members",
@@ -57,11 +34,8 @@
"nest/common/validate-route-guards.ts -> nest/public-api/api-token.guard.ts"
],
"domainInternals": [
"addons -> memories/memories.helpers.ts",
"addons -> settings/instance-api-keys.ts",
"addons -> transit/transit-provider.ts",
"admin -> llm-parse/llm-config.ts",
"admin -> memories/memories.helpers.ts",
"atlas -> geo/nominatim.client.ts",
"atlas -> public-api/public-api-request.ts",
"auth -> maps/maps.helpers.ts",
+3 -3
View File
@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@mikro-orm/nestjs';
import { ADDON_IDS } from '../../addons';
import { getPhotoProviderConfig } from '../memories/memories.helpers';
import { readTransitProvider, writeTransitProvider } from '../transit/transit-provider';
import { getPhotoProviderConfig } from '../common/photo-provider-config';
import { readTransitProvider, writeTransitProvider } from '../common/transit-provider';
import { resolveApiKey, type ApiKeySource } from '../settings/instance-api-keys';
import { readEnv } from '../../app-config';
import type { TransitProvider } from '@trek/shared';
@@ -233,7 +233,7 @@ export class AddonsService {
// ── Transit backend (#1699) ────────────────────────────────────────────────
// Not a flag: two named backends, so it stores the name rather than a
// boolean. The read/write pair lives in transit/transit-provider.ts because
// boolean. The read/write pair lives in common/transit-provider.ts because
// TransitService reads the same row on every request — one key, one reader,
// one writer. Neither that module nor instance-api-keys.ts (below) is one of
// this plan's six domains, so they stay on DatabaseService until their own
+1 -1
View File
@@ -18,7 +18,7 @@ import { maybe_encrypt_api_key, decrypt_api_key } from '../common/crypto/apiKeyC
import { avatarUrl } from '../common/avatarUrl';
import { DATABASE_BACKUP, type DatabaseBackupStrategy } from '../database/database-backup.interface';
import { prepareLlmAddonConfigForWrite, maskLlmAddonConfig } from '../llm-parse/llm-config';
import { getPhotoProviderConfig } from '../memories/memories.helpers';
import { getPhotoProviderConfig } from '../common/photo-provider-config';
import { validatePassword } from '../common/passwordPolicy';
import { UserCleanupService } from '../auth/user-cleanup.service';
import { UnitOfWork } from '../database/unit-of-work';
@@ -0,0 +1,21 @@
/**
* Where the client finds a provider's settings/status/test endpoints. Pure
* string building in the shared kernel: the admin and addons surfaces both
* read it, and neither should have to import the memories domain to do so.
*/
export interface PhotoProviderConfig {
settings_get: string;
settings_put: string;
status_get: string;
test_post: string;
}
export function getPhotoProviderConfig(providerId: string): PhotoProviderConfig {
const prefix = `/integrations/memories/${providerId}`;
return {
settings_get: `${prefix}/settings`,
settings_put: `${prefix}/settings`,
status_get: `${prefix}/status`,
test_post: `${prefix}/test`,
};
}
@@ -360,28 +360,3 @@ export async function pipeAsset(url: string, response: Response, headers?: Recor
}
}
}
// ── Route shape for the settings page ─────────────────────────────────────
/**
* Where the client finds a provider's settings/status/test endpoints. Pure
* string building, so it stays here rather than on a service — the admin and
* addons surfaces both read it and neither should have to import the memories
* domain to do so.
*/
export interface PhotoProviderConfig {
settings_get: string;
settings_put: string;
status_get: string;
test_post: string;
}
export function getPhotoProviderConfig(providerId: string): PhotoProviderConfig {
const prefix = `/integrations/memories/${providerId}`;
return {
settings_get: `${prefix}/settings`,
settings_put: `${prefix}/settings`,
status_get: `${prefix}/status`,
test_post: `${prefix}/test`,
};
}
@@ -8,7 +8,7 @@ import type { AppSettingsRepository } from '../../db/repositories/AppSettings.re
import { Users } from '../../db/entities/Users.entity';
import type { UsersRepository } from '../../db/repositories/Users.repository';
import { GoogleQuotaService } from '../google-quota/google-quota.service';
import { readTransitProvider } from './transit-provider';
import { readTransitProvider } from '../common/transit-provider';
import {
decodePolyline,
deriveTransitStats,
+1 -1
View File
@@ -35,7 +35,7 @@ db.exec('DELETE FROM app_settings');
const { getPhotoProviderConfig } = vi.hoisted(() => ({
getPhotoProviderConfig: vi.fn(() => ({ url: 'https://immich.example' })),
}));
vi.mock('../../src/nest/memories/memories.helpers', () => ({ getPhotoProviderConfig }));
vi.mock('../../src/nest/common/photo-provider-config', () => ({ getPhotoProviderConfig }));
import { AddonsModule } from '../../src/nest/addons/addons.module';
import { TrekExceptionFilter } from '../../src/nest/common/trek-exception.filter';
@@ -43,7 +43,7 @@ import { PlaceShadowPicks } from '../../../src/db/entities/PlaceShadowPicks.enti
import type { UsersRepository } from '../../../src/db/repositories/Users.repository';
const { getPhotoProviderConfig } = vi.hoisted(() => ({ getPhotoProviderConfig: vi.fn(() => ({})) }));
vi.mock('../../../src/nest/memories/memories.helpers', () => ({ getPhotoProviderConfig }));
vi.mock('../../../src/nest/common/photo-provider-config', () => ({ getPhotoProviderConfig }));
const testDb = createSnapshotTestDb();
let t: TestOrm;
@@ -0,0 +1,43 @@
import { describe, expect, it, vi } from 'vitest';
import type { AppSettingsRepository } from '../../../../src/db/repositories/AppSettings.repository';
import {
DEFAULT_TRANSIT_PROVIDER,
readTransitProvider,
TRANSIT_PROVIDER_SETTING,
writeTransitProvider,
} from '../../../../src/nest/common/transit-provider';
import { getPhotoProviderConfig } from '../../../../src/nest/common/photo-provider-config';
function settings(value: string | null) {
return { getValue: vi.fn(async () => value), setValue: vi.fn(async () => undefined) };
}
describe('transit provider setting', () => {
it('reads a known backend as stored', async () => {
const repo = settings('google');
expect(await readTransitProvider(repo as unknown as AppSettingsRepository)).toBe('google');
expect(repo.getValue).toHaveBeenCalledWith(TRANSIT_PROVIDER_SETTING);
});
it('falls back to Transitous for a missing row or a name this version does not know', async () => {
expect(await readTransitProvider(settings(null) as unknown as AppSettingsRepository)).toBe(DEFAULT_TRANSIT_PROVIDER);
expect(await readTransitProvider(settings('rail-x') as unknown as AppSettingsRepository)).toBe(DEFAULT_TRANSIT_PROVIDER);
});
it('writes the name under its one key and hands it back', async () => {
const repo = settings(null);
expect(await writeTransitProvider(repo as unknown as AppSettingsRepository, 'google')).toBe('google');
expect(repo.setValue).toHaveBeenCalledWith(TRANSIT_PROVIDER_SETTING, 'google');
});
});
describe('photo provider config', () => {
it('points every endpoint under the provider prefix', () => {
expect(getPhotoProviderConfig('immich')).toEqual({
settings_get: '/integrations/memories/immich/settings',
settings_put: '/integrations/memories/immich/settings',
status_get: '/integrations/memories/immich/status',
test_post: '/integrations/memories/immich/test',
});
});
});