refactor: repositories (#15561)

* refactor: version history repository

* refactor: oauth repository

* refactor: trash repository

* refactor: telemetry repository

* refactor: metadata repository

* refactor: cron repository

* refactor: map repository

* refactor: server-info repository

* refactor: album user repository

* refactor: notification repository
This commit is contained in:
Jason Rasmussen
2025-01-23 18:10:17 -05:00
committed by GitHub
parent 995314446b
commit 1869b1b41a
57 changed files with 372 additions and 469 deletions

View File

@@ -4,10 +4,14 @@ import { InjectKysely } from 'nestjs-kysely';
import { AlbumsSharedUsersUsers, DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AlbumUserRole } from 'src/enum';
import { AlbumPermissionId, IAlbumUserRepository } from 'src/interfaces/album-user.interface';
export type AlbumPermissionId = {
albumsId: string;
usersId: string;
};
@Injectable()
export class AlbumUserRepository implements IAlbumUserRepository {
export class AlbumUserRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }] })
@@ -16,10 +20,7 @@ export class AlbumUserRepository implements IAlbumUserRepository {
}
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] })
update(
{ usersId, albumsId }: AlbumPermissionId,
dto: Updateable<AlbumsSharedUsersUsers>,
): Promise<Selectable<AlbumsSharedUsersUsers>> {
update({ usersId, albumsId }: AlbumPermissionId, dto: Updateable<AlbumsSharedUsersUsers>) {
return this.db
.updateTable('albums_shared_users_users')
.set(dto)

View File

@@ -43,8 +43,8 @@ import {
WithProperty,
WithoutProperty,
} from 'src/interfaces/asset.interface';
import { MapMarker, MapMarkerSearchOptions } from 'src/interfaces/map.interface';
import { AssetSearchOptions, SearchExploreItem, SearchExploreItemSet } from 'src/interfaces/search.interface';
import { MapMarker, MapMarkerSearchOptions } from 'src/repositories/map.repository';
import { anyUuid, asUuid, mapUpsertColumns } from 'src/utils/database';
import { Paginated, PaginationOptions, paginationHelper } from 'src/utils/pagination';

View File

@@ -1,11 +1,24 @@
import { Injectable } from '@nestjs/common';
import { SchedulerRegistry } from '@nestjs/schedule';
import { CronJob, CronTime } from 'cron';
import { CronCreate, CronUpdate, ICronRepository } from 'src/interfaces/cron.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
type CronBase = {
name: string;
start?: boolean;
};
export type CronCreate = CronBase & {
expression: string;
onTick: () => void;
};
export type CronUpdate = CronBase & {
expression?: string;
};
@Injectable()
export class CronRepository implements ICronRepository {
export class CronRepository {
constructor(
private schedulerRegistry: SchedulerRegistry,
private logger: LoggingRepository,

View File

@@ -1,33 +1,23 @@
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
import { IAlbumRepository } from 'src/interfaces/album.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ICronRepository } from 'src/interfaces/cron.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IDatabaseRepository } from 'src/interfaces/database.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository } from 'src/interfaces/job.interface';
import { ILibraryRepository } from 'src/interfaces/library.interface';
import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
import { IMapRepository } from 'src/interfaces/map.interface';
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { IMoveRepository } from 'src/interfaces/move.interface';
import { INotificationRepository } from 'src/interfaces/notification.interface';
import { IOAuthRepository } from 'src/interfaces/oauth.interface';
import { IPartnerRepository } from 'src/interfaces/partner.interface';
import { IPersonRepository } from 'src/interfaces/person.interface';
import { IProcessRepository } from 'src/interfaces/process.interface';
import { ISearchRepository } from 'src/interfaces/search.interface';
import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
import { ISessionRepository } from 'src/interfaces/session.interface';
import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
import { IStackRepository } from 'src/interfaces/stack.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ITagRepository } from 'src/interfaces/tag.interface';
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
import { ITrashRepository } from 'src/interfaces/trash.interface';
import { IUserRepository } from 'src/interfaces/user.interface';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
import { AccessRepository } from 'src/repositories/access.repository';
import { ActivityRepository } from 'src/repositories/activity.repository';
import { AlbumUserRepository } from 'src/repositories/album-user.repository';
@@ -71,44 +61,44 @@ import { ViewRepository } from 'src/repositories/view-repository';
export const repositories = [
AccessRepository,
ActivityRepository,
AlbumUserRepository,
AuditRepository,
ApiKeyRepository,
ConfigRepository,
CronRepository,
LoggingRepository,
MapRepository,
MediaRepository,
MemoryRepository,
MetadataRepository,
NotificationRepository,
OAuthRepository,
ServerInfoRepository,
TelemetryRepository,
TrashRepository,
ViewRepository,
VersionHistoryRepository,
];
export const providers = [
{ provide: IAlbumRepository, useClass: AlbumRepository },
{ provide: IAlbumUserRepository, useClass: AlbumUserRepository },
{ provide: IAssetRepository, useClass: AssetRepository },
{ provide: ICronRepository, useClass: CronRepository },
{ provide: ICryptoRepository, useClass: CryptoRepository },
{ provide: IDatabaseRepository, useClass: DatabaseRepository },
{ provide: IEventRepository, useClass: EventRepository },
{ provide: IJobRepository, useClass: JobRepository },
{ provide: ILibraryRepository, useClass: LibraryRepository },
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
{ provide: IMapRepository, useClass: MapRepository },
{ provide: IMetadataRepository, useClass: MetadataRepository },
{ provide: IMoveRepository, useClass: MoveRepository },
{ provide: INotificationRepository, useClass: NotificationRepository },
{ provide: IOAuthRepository, useClass: OAuthRepository },
{ provide: IPartnerRepository, useClass: PartnerRepository },
{ provide: IPersonRepository, useClass: PersonRepository },
{ provide: IProcessRepository, useClass: ProcessRepository },
{ provide: ISearchRepository, useClass: SearchRepository },
{ provide: IServerInfoRepository, useClass: ServerInfoRepository },
{ provide: ISessionRepository, useClass: SessionRepository },
{ provide: ISharedLinkRepository, useClass: SharedLinkRepository },
{ provide: IStackRepository, useClass: StackRepository },
{ provide: IStorageRepository, useClass: StorageRepository },
{ provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
{ provide: ITagRepository, useClass: TagRepository },
{ provide: ITelemetryRepository, useClass: TelemetryRepository },
{ provide: ITrashRepository, useClass: TrashRepository },
{ provide: IUserRepository, useClass: UserRepository },
{ provide: IVersionHistoryRepository, useClass: VersionHistoryRepository },
];

View File

@@ -11,24 +11,41 @@ import { DB, GeodataPlaces, NaturalearthCountries } from 'src/db';
import { AssetEntity, withExif } from 'src/entities/asset.entity';
import { NaturalEarthCountriesTempEntity } from 'src/entities/natural-earth-countries.entity';
import { LogLevel, SystemMetadataKey } from 'src/enum';
import {
GeoPoint,
IMapRepository,
MapMarker,
MapMarkerSearchOptions,
ReverseGeocodeResult,
} from 'src/interfaces/map.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
export interface MapMarkerSearchOptions {
isArchived?: boolean;
isFavorite?: boolean;
fileCreatedBefore?: Date;
fileCreatedAfter?: Date;
}
export interface GeoPoint {
latitude: number;
longitude: number;
}
export interface ReverseGeocodeResult {
country: string | null;
state: string | null;
city: string | null;
}
export interface MapMarker extends ReverseGeocodeResult {
id: string;
lat: number;
lon: number;
}
interface MapDB extends DB {
geodata_places_tmp: GeodataPlaces;
naturalearth_countries_tmp: NaturalearthCountries;
}
@Injectable()
export class MapRepository implements IMapRepository {
export class MapRepository {
constructor(
private configRepository: ConfigRepository,
@Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,

View File

@@ -1,11 +1,72 @@
import { Injectable } from '@nestjs/common';
import { DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
import { BinaryField, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
import geotz from 'geo-tz';
import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
interface ExifDuration {
Value: number;
Scale?: number;
}
type StringOrNumber = string | number;
type TagsWithWrongTypes =
| 'FocalLength'
| 'Duration'
| 'Description'
| 'ImageDescription'
| 'RegionInfo'
| 'TagsList'
| 'Keywords'
| 'HierarchicalSubject'
| 'ISO';
export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
ContentIdentifier?: string;
MotionPhoto?: number;
MotionPhotoVersion?: number;
MotionPhotoPresentationTimestampUs?: number;
MediaGroupUUID?: string;
ImagePixelDepth?: string;
FocalLength?: number;
Duration?: number | string | ExifDuration;
EmbeddedVideoType?: string;
EmbeddedVideoFile?: BinaryField;
MotionPhotoVideo?: BinaryField;
TagsList?: StringOrNumber[];
HierarchicalSubject?: StringOrNumber[];
Keywords?: StringOrNumber | StringOrNumber[];
ISO?: number | number[];
// Type is wrong, can also be number.
Description?: StringOrNumber;
ImageDescription?: StringOrNumber;
// Extended properties for image regions, such as faces
RegionInfo?: {
AppliedToDimensions: {
W: number;
H: number;
Unit: string;
};
RegionList: {
Area: {
// (X,Y) // center of the rectangle
X: number;
Y: number;
W: number;
H: number;
Unit: string;
};
Rotation?: number;
Type?: string;
Name?: string;
}[];
};
}
@Injectable()
export class MetadataRepository implements IMetadataRepository {
export class MetadataRepository {
private exiftool = new ExifTool({
defaultVideosToUTC: true,
backfillTimezones: true,

View File

@@ -1,6 +1,5 @@
import { EmailRenderRequest, EmailTemplate } from 'src/interfaces/notification.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
import { NotificationRepository } from 'src/repositories/notification.repository';
import { EmailRenderRequest, EmailTemplate, NotificationRepository } from 'src/repositories/notification.repository';
import { ILoggingRepository } from 'src/types';
import { newLoggingRepositoryMock } from 'test/repositories/logger.repository.mock';
import { Mocked } from 'vitest';

View File

@@ -6,18 +6,104 @@ import { AlbumInviteEmail } from 'src/emails/album-invite.email';
import { AlbumUpdateEmail } from 'src/emails/album-update.email';
import { TestEmail } from 'src/emails/test.email';
import { WelcomeEmail } from 'src/emails/welcome.email';
import {
EmailRenderRequest,
EmailTemplate,
INotificationRepository,
SendEmailOptions,
SendEmailResponse,
SmtpOptions,
} from 'src/interfaces/notification.interface';
import { LoggingRepository } from 'src/repositories/logging.repository';
export type EmailImageAttachment = {
filename: string;
path: string;
cid: string;
};
export type SendEmailOptions = {
from: string;
to: string;
replyTo?: string;
subject: string;
html: string;
text: string;
imageAttachments?: EmailImageAttachment[];
smtp: SmtpOptions;
};
export type SmtpOptions = {
host: string;
port?: number;
username?: string;
password?: string;
ignoreCert?: boolean;
};
export enum EmailTemplate {
TEST_EMAIL = 'test',
// AUTH
WELCOME = 'welcome',
RESET_PASSWORD = 'reset-password',
// ALBUM
ALBUM_INVITE = 'album-invite',
ALBUM_UPDATE = 'album-update',
}
interface BaseEmailProps {
baseUrl: string;
customTemplate?: string;
}
export interface TestEmailProps extends BaseEmailProps {
displayName: string;
}
export interface WelcomeEmailProps extends BaseEmailProps {
displayName: string;
username: string;
password?: string;
}
export interface AlbumInviteEmailProps extends BaseEmailProps {
albumName: string;
albumId: string;
senderName: string;
recipientName: string;
cid?: string;
}
export interface AlbumUpdateEmailProps extends BaseEmailProps {
albumName: string;
albumId: string;
recipientName: string;
cid?: string;
}
export type EmailRenderRequest =
| {
template: EmailTemplate.TEST_EMAIL;
data: TestEmailProps;
customTemplate: string;
}
| {
template: EmailTemplate.WELCOME;
data: WelcomeEmailProps;
customTemplate: string;
}
| {
template: EmailTemplate.ALBUM_INVITE;
data: AlbumInviteEmailProps;
customTemplate: string;
}
| {
template: EmailTemplate.ALBUM_UPDATE;
data: AlbumUpdateEmailProps;
customTemplate: string;
};
export type SendEmailResponse = {
messageId: string;
response: any;
};
@Injectable()
export class NotificationRepository implements INotificationRepository {
export class NotificationRepository {
constructor(private logger: LoggingRepository) {
this.logger.setContext(NotificationRepository.name);
}

View File

@@ -1,10 +1,21 @@
import { Injectable, InternalServerErrorException } from '@nestjs/common';
import { custom, generators, Issuer } from 'openid-client';
import { IOAuthRepository, OAuthConfig, OAuthProfile } from 'src/interfaces/oauth.interface';
import { custom, generators, Issuer, UserinfoResponse } from 'openid-client';
import { LoggingRepository } from 'src/repositories/logging.repository';
export type OAuthConfig = {
clientId: string;
clientSecret: string;
issuerUrl: string;
mobileOverrideEnabled: boolean;
mobileRedirectUri: string;
profileSigningAlgorithm: string;
scope: string;
signingAlgorithm: string;
};
export type OAuthProfile = UserinfoResponse;
@Injectable()
export class OAuthRepository implements IOAuthRepository {
export class OAuthRepository {
constructor(private logger: LoggingRepository) {
this.logger.setContext(OAuthRepository.name);
}

View File

@@ -4,10 +4,27 @@ import { exec as execCallback } from 'node:child_process';
import { readFile } from 'node:fs/promises';
import { promisify } from 'node:util';
import sharp from 'sharp';
import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
export interface GitHubRelease {
id: number;
url: string;
tag_name: string;
name: string;
created_at: string;
published_at: string;
body: string;
}
export interface ServerBuildVersions {
nodejs: string;
ffmpeg: string;
libvips: string;
exiftool: string;
imagemagick: string;
}
const exec = promisify(execCallback);
const maybeFirstLine = async (command: string): Promise<string> => {
try {
@@ -34,7 +51,7 @@ const getLockfileVersion = (name: string, lockfile?: BuildLockfile) => {
};
@Injectable()
export class ServerInfoRepository implements IServerInfoRepository {
export class ServerInfoRepository {
constructor(
private configRepository: ConfigRepository,
private logger: LoggingRepository,

View File

@@ -15,11 +15,12 @@ import { MetricService } from 'nestjs-otel';
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
import { serverVersion } from 'src/constants';
import { ImmichTelemetry, MetadataKey } from 'src/enum';
import { IMetricGroupRepository, ITelemetryRepository, MetricGroupOptions } from 'src/interfaces/telemetry.interface';
import { ConfigRepository } from 'src/repositories/config.repository';
import { LoggingRepository } from 'src/repositories/logging.repository';
class MetricGroupRepository implements IMetricGroupRepository {
type MetricGroupOptions = { enabled: boolean };
export class MetricGroupRepository {
private enabled = false;
constructor(private metricService: MetricService) {}
@@ -86,7 +87,7 @@ export const teardownTelemetry = async () => {
};
@Injectable()
export class TelemetryRepository implements ITelemetryRepository {
export class TelemetryRepository {
api: MetricGroupRepository;
host: MetricGroupRepository;
jobs: MetricGroupRepository;

View File

@@ -3,9 +3,8 @@ import { InjectKysely } from 'nestjs-kysely';
import { DB } from 'src/db';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetStatus } from 'src/enum';
import { ITrashRepository } from 'src/interfaces/trash.interface';
export class TrashRepository implements ITrashRepository {
export class TrashRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
getDeletedIds(): AsyncIterableIterator<{ id: string }> {

View File

@@ -3,25 +3,23 @@ import { Insertable, Kysely } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { DB, VersionHistory } from 'src/db';
import { GenerateSql } from 'src/decorators';
import { VersionHistoryEntity } from 'src/entities/version-history.entity';
import { IVersionHistoryRepository } from 'src/interfaces/version-history.interface';
@Injectable()
export class VersionHistoryRepository implements IVersionHistoryRepository {
export class VersionHistoryRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
@GenerateSql()
getAll(): Promise<VersionHistoryEntity[]> {
getAll() {
return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').execute();
}
@GenerateSql()
getLatest(): Promise<VersionHistoryEntity | undefined> {
getLatest() {
return this.db.selectFrom('version_history').selectAll().orderBy('createdAt', 'desc').executeTakeFirst();
}
@GenerateSql({ params: [{ version: 'v1.123.0' }] })
create(version: Insertable<VersionHistory>): Promise<VersionHistoryEntity> {
create(version: Insertable<VersionHistory>) {
return this.db.insertInto('version_history').values(version).returningAll().executeTakeFirstOrThrow();
}
}