refactor: enum casing (#19946)

This commit is contained in:
Jason Rasmussen 2025-07-15 14:50:13 -04:00 committed by GitHub
parent 920d7de349
commit e73abe0762
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
174 changed files with 2675 additions and 2459 deletions

View File

@ -85,13 +85,13 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
@Module({ @Module({
imports: [...imports, ScheduleModule.forRoot()], imports: [...imports, ScheduleModule.forRoot()],
controllers: [...controllers], controllers: [...controllers],
providers: [...common, ...middleware, { provide: IWorker, useValue: ImmichWorker.API }], providers: [...common, ...middleware, { provide: IWorker, useValue: ImmichWorker.Api }],
}) })
export class ApiModule extends BaseModule {} export class ApiModule extends BaseModule {}
@Module({ @Module({
imports: [...imports], imports: [...imports],
providers: [...common, { provide: IWorker, useValue: ImmichWorker.MICROSERVICES }, SchedulerRegistry], providers: [...common, { provide: IWorker, useValue: ImmichWorker.Microservices }, SchedulerRegistry],
}) })
export class MicroservicesModule extends BaseModule {} export class MicroservicesModule extends BaseModule {}

View File

@ -8,7 +8,7 @@ import {
OAuthTokenEndpointAuthMethod, OAuthTokenEndpointAuthMethod,
QueueName, QueueName,
ToneMapping, ToneMapping,
TranscodeHWAccel, TranscodeHardwareAcceleration,
TranscodePolicy, TranscodePolicy,
VideoCodec, VideoCodec,
VideoContainer, VideoContainer,
@ -42,7 +42,7 @@ export interface SystemConfig {
twoPass: boolean; twoPass: boolean;
preferredHwDevice: string; preferredHwDevice: string;
transcode: TranscodePolicy; transcode: TranscodePolicy;
accel: TranscodeHWAccel; accel: TranscodeHardwareAcceleration;
accelDecode: boolean; accelDecode: boolean;
tonemap: ToneMapping; tonemap: ToneMapping;
}; };
@ -190,39 +190,39 @@ export const defaults = Object.freeze<SystemConfig>({
preset: 'ultrafast', preset: 'ultrafast',
targetVideoCodec: VideoCodec.H264, targetVideoCodec: VideoCodec.H264,
acceptedVideoCodecs: [VideoCodec.H264], acceptedVideoCodecs: [VideoCodec.H264],
targetAudioCodec: AudioCodec.AAC, targetAudioCodec: AudioCodec.Aac,
acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS, AudioCodec.PCMS16LE], acceptedAudioCodecs: [AudioCodec.Aac, AudioCodec.Mp3, AudioCodec.LibOpus, AudioCodec.PcmS16le],
acceptedContainers: [VideoContainer.MOV, VideoContainer.OGG, VideoContainer.WEBM], acceptedContainers: [VideoContainer.Mov, VideoContainer.Ogg, VideoContainer.Webm],
targetResolution: '720', targetResolution: '720',
maxBitrate: '0', maxBitrate: '0',
bframes: -1, bframes: -1,
refs: 0, refs: 0,
gopSize: 0, gopSize: 0,
temporalAQ: false, temporalAQ: false,
cqMode: CQMode.AUTO, cqMode: CQMode.Auto,
twoPass: false, twoPass: false,
preferredHwDevice: 'auto', preferredHwDevice: 'auto',
transcode: TranscodePolicy.REQUIRED, transcode: TranscodePolicy.Required,
tonemap: ToneMapping.HABLE, tonemap: ToneMapping.Hable,
accel: TranscodeHWAccel.DISABLED, accel: TranscodeHardwareAcceleration.Disabled,
accelDecode: false, accelDecode: false,
}, },
job: { job: {
[QueueName.BACKGROUND_TASK]: { concurrency: 5 }, [QueueName.BackgroundTask]: { concurrency: 5 },
[QueueName.SMART_SEARCH]: { concurrency: 2 }, [QueueName.SmartSearch]: { concurrency: 2 },
[QueueName.METADATA_EXTRACTION]: { concurrency: 5 }, [QueueName.MetadataExtraction]: { concurrency: 5 },
[QueueName.FACE_DETECTION]: { concurrency: 2 }, [QueueName.FaceDetection]: { concurrency: 2 },
[QueueName.SEARCH]: { concurrency: 5 }, [QueueName.Search]: { concurrency: 5 },
[QueueName.SIDECAR]: { concurrency: 5 }, [QueueName.Sidecar]: { concurrency: 5 },
[QueueName.LIBRARY]: { concurrency: 5 }, [QueueName.Library]: { concurrency: 5 },
[QueueName.MIGRATION]: { concurrency: 5 }, [QueueName.Migration]: { concurrency: 5 },
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 3 }, [QueueName.ThumbnailGeneration]: { concurrency: 3 },
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 }, [QueueName.VideoConversion]: { concurrency: 1 },
[QueueName.NOTIFICATION]: { concurrency: 5 }, [QueueName.Notification]: { concurrency: 5 },
}, },
logging: { logging: {
enabled: true, enabled: true,
level: LogLevel.LOG, level: LogLevel.Log,
}, },
machineLearning: { machineLearning: {
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false', enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
@ -273,7 +273,7 @@ export const defaults = Object.freeze<SystemConfig>({
storageLabelClaim: 'preferred_username', storageLabelClaim: 'preferred_username',
storageQuotaClaim: 'immich_quota', storageQuotaClaim: 'immich_quota',
roleClaim: 'immich_role', roleClaim: 'immich_role',
tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST, tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod.ClientSecretPost,
timeout: 30_000, timeout: 30_000,
}, },
passwordLogin: { passwordLogin: {
@ -286,12 +286,12 @@ export const defaults = Object.freeze<SystemConfig>({
}, },
image: { image: {
thumbnail: { thumbnail: {
format: ImageFormat.WEBP, format: ImageFormat.Webp,
size: 250, size: 250,
quality: 80, quality: 80,
}, },
preview: { preview: {
format: ImageFormat.JPEG, format: ImageFormat.Jpeg,
size: 1440, size: 1440,
quality: 80, quality: 80,
}, },
@ -299,7 +299,7 @@ export const defaults = Object.freeze<SystemConfig>({
extractEmbedded: false, extractEmbedded: false,
fullsize: { fullsize: {
enabled: false, enabled: false,
format: ImageFormat.JPEG, format: ImageFormat.Jpeg,
quality: 80, quality: 80,
}, },
}, },

View File

@ -25,14 +25,14 @@ export const EXTENSION_NAMES: Record<DatabaseExtension, string> = {
} as const; } as const;
export const VECTOR_EXTENSIONS = [ export const VECTOR_EXTENSIONS = [
DatabaseExtension.VECTORCHORD, DatabaseExtension.VectorChord,
DatabaseExtension.VECTORS, DatabaseExtension.Vectors,
DatabaseExtension.VECTOR, DatabaseExtension.Vector,
] as const; ] as const;
export const VECTOR_INDEX_TABLES = { export const VECTOR_INDEX_TABLES = {
[VectorIndex.CLIP]: 'smart_search', [VectorIndex.Clip]: 'smart_search',
[VectorIndex.FACE]: 'face_search', [VectorIndex.Face]: 'face_search',
} as const; } as const;
export const VECTORCHORD_LIST_SLACK_FACTOR = 1.2; export const VECTORCHORD_LIST_SLACK_FACTOR = 1.2;

View File

@ -20,13 +20,13 @@ export class ActivityController {
constructor(private service: ActivityService) {} constructor(private service: ActivityService) {}
@Get() @Get()
@Authenticated({ permission: Permission.ACTIVITY_READ }) @Authenticated({ permission: Permission.ActivityRead })
getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise<ActivityResponseDto[]> { getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
return this.service.getAll(auth, dto); return this.service.getAll(auth, dto);
} }
@Post() @Post()
@Authenticated({ permission: Permission.ACTIVITY_CREATE }) @Authenticated({ permission: Permission.ActivityCreate })
async createActivity( async createActivity(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: ActivityCreateDto, @Body() dto: ActivityCreateDto,
@ -40,14 +40,14 @@ export class ActivityController {
} }
@Get('statistics') @Get('statistics')
@Authenticated({ permission: Permission.ACTIVITY_STATISTICS }) @Authenticated({ permission: Permission.ActivityStatistics })
getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<ActivityStatisticsResponseDto> { getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
return this.service.getStatistics(auth, dto); return this.service.getStatistics(auth, dto);
} }
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.ACTIVITY_DELETE }) @Authenticated({ permission: Permission.ActivityDelete })
deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View File

@ -23,24 +23,24 @@ export class AlbumController {
constructor(private service: AlbumService) {} constructor(private service: AlbumService) {}
@Get() @Get()
@Authenticated({ permission: Permission.ALBUM_READ }) @Authenticated({ permission: Permission.AlbumRead })
getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> { getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise<AlbumResponseDto[]> {
return this.service.getAll(auth, query); return this.service.getAll(auth, query);
} }
@Post() @Post()
@Authenticated({ permission: Permission.ALBUM_CREATE }) @Authenticated({ permission: Permission.AlbumCreate })
createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise<AlbumResponseDto> { createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise<AlbumResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get('statistics') @Get('statistics')
@Authenticated({ permission: Permission.ALBUM_STATISTICS }) @Authenticated({ permission: Permission.AlbumStatistics })
getAlbumStatistics(@Auth() auth: AuthDto): Promise<AlbumStatisticsResponseDto> { getAlbumStatistics(@Auth() auth: AuthDto): Promise<AlbumStatisticsResponseDto> {
return this.service.getStatistics(auth); return this.service.getStatistics(auth);
} }
@Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true }) @Authenticated({ permission: Permission.AlbumRead, sharedLink: true })
@Get(':id') @Get(':id')
getAlbumInfo( getAlbumInfo(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@ -51,7 +51,7 @@ export class AlbumController {
} }
@Patch(':id') @Patch(':id')
@Authenticated({ permission: Permission.ALBUM_UPDATE }) @Authenticated({ permission: Permission.AlbumUpdate })
updateAlbumInfo( updateAlbumInfo(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -61,7 +61,7 @@ export class AlbumController {
} }
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.ALBUM_DELETE }) @Authenticated({ permission: Permission.AlbumDelete })
deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View File

@ -55,7 +55,7 @@ describe(APIKeyController.name, () => {
it('should require a valid uuid', async () => { it('should require a valid uuid', async () => {
const { status, body } = await request(ctx.getHttpServer()) const { status, body } = await request(ctx.getHttpServer())
.put(`/api-keys/123`) .put(`/api-keys/123`)
.send({ name: 'new name', permissions: [Permission.ALL] }); .send({ name: 'new name', permissions: [Permission.All] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
}); });

View File

@ -13,25 +13,25 @@ export class APIKeyController {
constructor(private service: ApiKeyService) {} constructor(private service: ApiKeyService) {}
@Post() @Post()
@Authenticated({ permission: Permission.API_KEY_CREATE }) @Authenticated({ permission: Permission.ApiKeyCreate })
createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> { createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get() @Get()
@Authenticated({ permission: Permission.API_KEY_READ }) @Authenticated({ permission: Permission.ApiKeyRead })
getApiKeys(@Auth() auth: AuthDto): Promise<APIKeyResponseDto[]> { getApiKeys(@Auth() auth: AuthDto): Promise<APIKeyResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.API_KEY_READ }) @Authenticated({ permission: Permission.ApiKeyRead })
getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> { getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<APIKeyResponseDto> {
return this.service.getById(auth, id); return this.service.getById(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.API_KEY_UPDATE }) @Authenticated({ permission: Permission.ApiKeyUpdate })
updateApiKey( updateApiKey(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -42,7 +42,7 @@ export class APIKeyController {
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.API_KEY_DELETE }) @Authenticated({ permission: Permission.ApiKeyDelete })
deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View File

@ -45,7 +45,7 @@ import { ImmichFileResponse, sendFile } from 'src/utils/file';
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation'; import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
@ApiTags('Assets') @ApiTags('Assets')
@Controller(RouteKey.ASSET) @Controller(RouteKey.Asset)
export class AssetMediaController { export class AssetMediaController {
constructor( constructor(
private logger: LoggingRepository, private logger: LoggingRepository,
@ -56,7 +56,7 @@ export class AssetMediaController {
@UseInterceptors(AssetUploadInterceptor, FileUploadInterceptor) @UseInterceptors(AssetUploadInterceptor, FileUploadInterceptor)
@ApiConsumes('multipart/form-data') @ApiConsumes('multipart/form-data')
@ApiHeader({ @ApiHeader({
name: ImmichHeader.CHECKSUM, name: ImmichHeader.Checksum,
description: 'sha1 checksum that can be used for duplicate detection before the file is uploaded', description: 'sha1 checksum that can be used for duplicate detection before the file is uploaded',
required: false, required: false,
}) })

View File

@ -19,7 +19,7 @@ import { AssetService } from 'src/services/asset.service';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Assets') @ApiTags('Assets')
@Controller(RouteKey.ASSET) @Controller(RouteKey.Asset)
export class AssetController { export class AssetController {
constructor(private service: AssetService) {} constructor(private service: AssetService) {}

View File

@ -36,9 +36,9 @@ export class AuthController {
return respondWithCookie(res, body, { return respondWithCookie(res, body, {
isSecure: loginDetails.isSecure, isSecure: loginDetails.isSecure,
values: [ values: [
{ key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, { key: ImmichCookie.AccessToken, value: body.accessToken },
{ key: ImmichCookie.AUTH_TYPE, value: AuthType.PASSWORD }, { key: ImmichCookie.AuthType, value: AuthType.Password },
{ key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, { key: ImmichCookie.IsAuthenticated, value: 'true' },
], ],
}); });
} }
@ -70,13 +70,13 @@ export class AuthController {
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
): Promise<LogoutResponseDto> { ): Promise<LogoutResponseDto> {
const authType = (request.cookies || {})[ImmichCookie.AUTH_TYPE]; const authType = (request.cookies || {})[ImmichCookie.AuthType];
const body = await this.service.logout(auth, authType); const body = await this.service.logout(auth, authType);
return respondWithoutCookie(res, body, [ return respondWithoutCookie(res, body, [
ImmichCookie.ACCESS_TOKEN, ImmichCookie.AccessToken,
ImmichCookie.AUTH_TYPE, ImmichCookie.AuthType,
ImmichCookie.IS_AUTHENTICATED, ImmichCookie.IsAuthenticated,
]); ]);
} }

View File

@ -19,19 +19,19 @@ export class FaceController {
constructor(private service: PersonService) {} constructor(private service: PersonService) {}
@Post() @Post()
@Authenticated({ permission: Permission.FACE_CREATE }) @Authenticated({ permission: Permission.FaceCreate })
createFace(@Auth() auth: AuthDto, @Body() dto: AssetFaceCreateDto) { createFace(@Auth() auth: AuthDto, @Body() dto: AssetFaceCreateDto) {
return this.service.createFace(auth, dto); return this.service.createFace(auth, dto);
} }
@Get() @Get()
@Authenticated({ permission: Permission.FACE_READ }) @Authenticated({ permission: Permission.FaceRead })
getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> { getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise<AssetFaceResponseDto[]> {
return this.service.getFacesById(auth, dto); return this.service.getFacesById(auth, dto);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.FACE_UPDATE }) @Authenticated({ permission: Permission.FaceUpdate })
reassignFacesById( reassignFacesById(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -41,7 +41,7 @@ export class FaceController {
} }
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.FACE_DELETE }) @Authenticated({ permission: Permission.FaceDelete })
deleteFace(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetFaceDeleteDto) { deleteFace(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: AssetFaceDeleteDto) {
return this.service.deleteFace(auth, id, dto); return this.service.deleteFace(auth, id, dto);
} }

View File

@ -19,32 +19,32 @@ export class LibraryController {
constructor(private service: LibraryService) {} constructor(private service: LibraryService) {}
@Get() @Get()
@Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) @Authenticated({ permission: Permission.LibraryRead, admin: true })
getAllLibraries(): Promise<LibraryResponseDto[]> { getAllLibraries(): Promise<LibraryResponseDto[]> {
return this.service.getAll(); return this.service.getAll();
} }
@Post() @Post()
@Authenticated({ permission: Permission.LIBRARY_CREATE, admin: true }) @Authenticated({ permission: Permission.LibraryCreate, admin: true })
createLibrary(@Body() dto: CreateLibraryDto): Promise<LibraryResponseDto> { createLibrary(@Body() dto: CreateLibraryDto): Promise<LibraryResponseDto> {
return this.service.create(dto); return this.service.create(dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) @Authenticated({ permission: Permission.LibraryRead, admin: true })
getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> { getLibrary(@Param() { id }: UUIDParamDto): Promise<LibraryResponseDto> {
return this.service.get(id); return this.service.get(id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true }) @Authenticated({ permission: Permission.LibraryUpdate, admin: true })
updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> { updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise<LibraryResponseDto> {
return this.service.update(id, dto); return this.service.update(id, dto);
} }
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true }) @Authenticated({ permission: Permission.LibraryDelete, admin: true })
deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> { deleteLibrary(@Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(id); return this.service.delete(id);
} }
@ -58,14 +58,14 @@ export class LibraryController {
} }
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true }) @Authenticated({ permission: Permission.LibraryStatistics, admin: true })
getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> { getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise<LibraryStatsResponseDto> {
return this.service.getStatistics(id); return this.service.getStatistics(id);
} }
@Post(':id/scan') @Post(':id/scan')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true }) @Authenticated({ permission: Permission.LibraryUpdate, admin: true })
scanLibrary(@Param() { id }: UUIDParamDto) { scanLibrary(@Param() { id }: UUIDParamDto) {
return this.service.queueScan(id); return this.service.queueScan(id);
} }

View File

@ -20,31 +20,31 @@ export class MemoryController {
constructor(private service: MemoryService) {} constructor(private service: MemoryService) {}
@Get() @Get()
@Authenticated({ permission: Permission.MEMORY_READ }) @Authenticated({ permission: Permission.MemoryRead })
searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryResponseDto[]> { searchMemories(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryResponseDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@Post() @Post()
@Authenticated({ permission: Permission.MEMORY_CREATE }) @Authenticated({ permission: Permission.MemoryCreate })
createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise<MemoryResponseDto> { createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise<MemoryResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get('statistics') @Get('statistics')
@Authenticated({ permission: Permission.MEMORY_READ }) @Authenticated({ permission: Permission.MemoryRead })
memoriesStatistics(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryStatisticsResponseDto> { memoriesStatistics(@Auth() auth: AuthDto, @Query() dto: MemorySearchDto): Promise<MemoryStatisticsResponseDto> {
return this.service.statistics(auth, dto); return this.service.statistics(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.MEMORY_READ }) @Authenticated({ permission: Permission.MemoryRead })
getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<MemoryResponseDto> { getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<MemoryResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.MEMORY_UPDATE }) @Authenticated({ permission: Permission.MemoryUpdate })
updateMemory( updateMemory(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -55,7 +55,7 @@ export class MemoryController {
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.MEMORY_DELETE }) @Authenticated({ permission: Permission.MemoryDelete })
deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }

View File

@ -19,31 +19,31 @@ export class NotificationController {
constructor(private service: NotificationService) {} constructor(private service: NotificationService) {}
@Get() @Get()
@Authenticated({ permission: Permission.NOTIFICATION_READ }) @Authenticated({ permission: Permission.NotificationRead })
getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise<NotificationDto[]> { getNotifications(@Auth() auth: AuthDto, @Query() dto: NotificationSearchDto): Promise<NotificationDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@Put() @Put()
@Authenticated({ permission: Permission.NOTIFICATION_UPDATE }) @Authenticated({ permission: Permission.NotificationUpdate })
updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise<void> { updateNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationUpdateAllDto): Promise<void> {
return this.service.updateAll(auth, dto); return this.service.updateAll(auth, dto);
} }
@Delete() @Delete()
@Authenticated({ permission: Permission.NOTIFICATION_DELETE }) @Authenticated({ permission: Permission.NotificationDelete })
deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise<void> { deleteNotifications(@Auth() auth: AuthDto, @Body() dto: NotificationDeleteAllDto): Promise<void> {
return this.service.deleteAll(auth, dto); return this.service.deleteAll(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.NOTIFICATION_READ }) @Authenticated({ permission: Permission.NotificationRead })
getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<NotificationDto> { getNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<NotificationDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.NOTIFICATION_UPDATE }) @Authenticated({ permission: Permission.NotificationUpdate })
updateNotification( updateNotification(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -53,7 +53,7 @@ export class NotificationController {
} }
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.NOTIFICATION_DELETE }) @Authenticated({ permission: Permission.NotificationDelete })
deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteNotification(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View File

@ -41,8 +41,8 @@ export class OAuthController {
{ {
isSecure: loginDetails.isSecure, isSecure: loginDetails.isSecure,
values: [ values: [
{ key: ImmichCookie.OAUTH_STATE, value: state }, { key: ImmichCookie.OAuthState, value: state },
{ key: ImmichCookie.OAUTH_CODE_VERIFIER, value: codeVerifier }, { key: ImmichCookie.OAuthCodeVerifier, value: codeVerifier },
], ],
}, },
); );
@ -56,14 +56,14 @@ export class OAuthController {
@GetLoginDetails() loginDetails: LoginDetails, @GetLoginDetails() loginDetails: LoginDetails,
): Promise<LoginResponseDto> { ): Promise<LoginResponseDto> {
const body = await this.service.callback(dto, request.headers, loginDetails); const body = await this.service.callback(dto, request.headers, loginDetails);
res.clearCookie(ImmichCookie.OAUTH_STATE); res.clearCookie(ImmichCookie.OAuthState);
res.clearCookie(ImmichCookie.OAUTH_CODE_VERIFIER); res.clearCookie(ImmichCookie.OAuthCodeVerifier);
return respondWithCookie(res, body, { return respondWithCookie(res, body, {
isSecure: loginDetails.isSecure, isSecure: loginDetails.isSecure,
values: [ values: [
{ key: ImmichCookie.ACCESS_TOKEN, value: body.accessToken }, { key: ImmichCookie.AccessToken, value: body.accessToken },
{ key: ImmichCookie.AUTH_TYPE, value: AuthType.OAUTH }, { key: ImmichCookie.AuthType, value: AuthType.OAuth },
{ key: ImmichCookie.IS_AUTHENTICATED, value: 'true' }, { key: ImmichCookie.IsAuthenticated, value: 'true' },
], ],
}); });
} }

View File

@ -13,19 +13,19 @@ export class PartnerController {
constructor(private service: PartnerService) {} constructor(private service: PartnerService) {}
@Get() @Get()
@Authenticated({ permission: Permission.PARTNER_READ }) @Authenticated({ permission: Permission.PartnerRead })
getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> { getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise<PartnerResponseDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@Post(':id') @Post(':id')
@Authenticated({ permission: Permission.PARTNER_CREATE }) @Authenticated({ permission: Permission.PartnerCreate })
createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> { createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PartnerResponseDto> {
return this.service.create(auth, id); return this.service.create(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.PARTNER_UPDATE }) @Authenticated({ permission: Permission.PartnerUpdate })
updatePartner( updatePartner(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -35,7 +35,7 @@ export class PartnerController {
} }
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.PARTNER_DELETE }) @Authenticated({ permission: Permission.PartnerDelete })
removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }

View File

@ -45,38 +45,38 @@ export class PersonController {
} }
@Get() @Get()
@Authenticated({ permission: Permission.PERSON_READ }) @Authenticated({ permission: Permission.PersonRead })
getAllPeople(@Auth() auth: AuthDto, @Query() options: PersonSearchDto): Promise<PeopleResponseDto> { getAllPeople(@Auth() auth: AuthDto, @Query() options: PersonSearchDto): Promise<PeopleResponseDto> {
return this.service.getAll(auth, options); return this.service.getAll(auth, options);
} }
@Post() @Post()
@Authenticated({ permission: Permission.PERSON_CREATE }) @Authenticated({ permission: Permission.PersonCreate })
createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise<PersonResponseDto> { createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise<PersonResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Put() @Put()
@Authenticated({ permission: Permission.PERSON_UPDATE }) @Authenticated({ permission: Permission.PersonUpdate })
updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> { updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise<BulkIdResponseDto[]> {
return this.service.updateAll(auth, dto); return this.service.updateAll(auth, dto);
} }
@Delete() @Delete()
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.PERSON_DELETE }) @Authenticated({ permission: Permission.PersonDelete })
deletePeople(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> { deletePeople(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.deleteAll(auth, dto); return this.service.deleteAll(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.PERSON_READ }) @Authenticated({ permission: Permission.PersonRead })
getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> { getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonResponseDto> {
return this.service.getById(auth, id); return this.service.getById(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.PERSON_UPDATE }) @Authenticated({ permission: Permission.PersonUpdate })
updatePerson( updatePerson(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -87,20 +87,20 @@ export class PersonController {
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.PERSON_DELETE }) @Authenticated({ permission: Permission.PersonDelete })
deletePerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deletePerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated({ permission: Permission.PERSON_STATISTICS }) @Authenticated({ permission: Permission.PersonStatistics })
getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> { getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<PersonStatisticsResponseDto> {
return this.service.getStatistics(auth, id); return this.service.getStatistics(auth, id);
} }
@Get(':id/thumbnail') @Get(':id/thumbnail')
@FileResponse() @FileResponse()
@Authenticated({ permission: Permission.PERSON_READ }) @Authenticated({ permission: Permission.PersonRead })
async getPersonThumbnail( async getPersonThumbnail(
@Res() res: Response, @Res() res: Response,
@Next() next: NextFunction, @Next() next: NextFunction,
@ -111,7 +111,7 @@ export class PersonController {
} }
@Put(':id/reassign') @Put(':id/reassign')
@Authenticated({ permission: Permission.PERSON_REASSIGN }) @Authenticated({ permission: Permission.PersonReassign })
reassignFaces( reassignFaces(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -121,7 +121,7 @@ export class PersonController {
} }
@Post(':id/merge') @Post(':id/merge')
@Authenticated({ permission: Permission.PERSON_MERGE }) @Authenticated({ permission: Permission.PersonMerge })
mergePerson( mergePerson(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,

View File

@ -13,26 +13,26 @@ export class SessionController {
constructor(private service: SessionService) {} constructor(private service: SessionService) {}
@Post() @Post()
@Authenticated({ permission: Permission.SESSION_CREATE }) @Authenticated({ permission: Permission.SessionCreate })
createSession(@Auth() auth: AuthDto, @Body() dto: SessionCreateDto): Promise<SessionCreateResponseDto> { createSession(@Auth() auth: AuthDto, @Body() dto: SessionCreateDto): Promise<SessionCreateResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get() @Get()
@Authenticated({ permission: Permission.SESSION_READ }) @Authenticated({ permission: Permission.SessionRead })
getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> { getSessions(@Auth() auth: AuthDto): Promise<SessionResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@Delete() @Delete()
@Authenticated({ permission: Permission.SESSION_DELETE }) @Authenticated({ permission: Permission.SessionDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteAllSessions(@Auth() auth: AuthDto): Promise<void> { deleteAllSessions(@Auth() auth: AuthDto): Promise<void> {
return this.service.deleteAll(auth); return this.service.deleteAll(auth);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.SESSION_UPDATE }) @Authenticated({ permission: Permission.SessionUpdate })
updateSession( updateSession(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -42,14 +42,14 @@ export class SessionController {
} }
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.SESSION_DELETE }) @Authenticated({ permission: Permission.SessionDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }
@Post(':id/lock') @Post(':id/lock')
@Authenticated({ permission: Permission.SESSION_LOCK }) @Authenticated({ permission: Permission.SessionLock })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { lockSession(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.lock(auth, id); return this.service.lock(auth, id);

View File

@ -24,7 +24,7 @@ export class SharedLinkController {
constructor(private service: SharedLinkService) {} constructor(private service: SharedLinkService) {}
@Get() @Get()
@Authenticated({ permission: Permission.SHARED_LINK_READ }) @Authenticated({ permission: Permission.SharedLinkRead })
getAllSharedLinks(@Auth() auth: AuthDto, @Query() dto: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> { getAllSharedLinks(@Auth() auth: AuthDto, @Query() dto: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
return this.service.getAll(auth, dto); return this.service.getAll(auth, dto);
} }
@ -38,31 +38,31 @@ export class SharedLinkController {
@Res({ passthrough: true }) res: Response, @Res({ passthrough: true }) res: Response,
@GetLoginDetails() loginDetails: LoginDetails, @GetLoginDetails() loginDetails: LoginDetails,
): Promise<SharedLinkResponseDto> { ): Promise<SharedLinkResponseDto> {
const sharedLinkToken = request.cookies?.[ImmichCookie.SHARED_LINK_TOKEN]; const sharedLinkToken = request.cookies?.[ImmichCookie.SharedLinkToken];
if (sharedLinkToken) { if (sharedLinkToken) {
dto.token = sharedLinkToken; dto.token = sharedLinkToken;
} }
const body = await this.service.getMine(auth, dto); const body = await this.service.getMine(auth, dto);
return respondWithCookie(res, body, { return respondWithCookie(res, body, {
isSecure: loginDetails.isSecure, isSecure: loginDetails.isSecure,
values: body.token ? [{ key: ImmichCookie.SHARED_LINK_TOKEN, value: body.token }] : [], values: body.token ? [{ key: ImmichCookie.SharedLinkToken, value: body.token }] : [],
}); });
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.SHARED_LINK_READ }) @Authenticated({ permission: Permission.SharedLinkRead })
getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> { getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<SharedLinkResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Post() @Post()
@Authenticated({ permission: Permission.SHARED_LINK_CREATE }) @Authenticated({ permission: Permission.SharedLinkCreate })
createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) { createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Patch(':id') @Patch(':id')
@Authenticated({ permission: Permission.SHARED_LINK_UPDATE }) @Authenticated({ permission: Permission.SharedLinkUpdate })
updateSharedLink( updateSharedLink(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -72,7 +72,7 @@ export class SharedLinkController {
} }
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.SHARED_LINK_DELETE }) @Authenticated({ permission: Permission.SharedLinkDelete })
removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }

View File

@ -14,32 +14,32 @@ export class StackController {
constructor(private service: StackService) {} constructor(private service: StackService) {}
@Get() @Get()
@Authenticated({ permission: Permission.STACK_READ }) @Authenticated({ permission: Permission.StackRead })
searchStacks(@Auth() auth: AuthDto, @Query() query: StackSearchDto): Promise<StackResponseDto[]> { searchStacks(@Auth() auth: AuthDto, @Query() query: StackSearchDto): Promise<StackResponseDto[]> {
return this.service.search(auth, query); return this.service.search(auth, query);
} }
@Post() @Post()
@Authenticated({ permission: Permission.STACK_CREATE }) @Authenticated({ permission: Permission.StackCreate })
createStack(@Auth() auth: AuthDto, @Body() dto: StackCreateDto): Promise<StackResponseDto> { createStack(@Auth() auth: AuthDto, @Body() dto: StackCreateDto): Promise<StackResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Delete() @Delete()
@Authenticated({ permission: Permission.STACK_DELETE }) @Authenticated({ permission: Permission.StackDelete })
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> { deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<void> {
return this.service.deleteAll(auth, dto); return this.service.deleteAll(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.STACK_READ }) @Authenticated({ permission: Permission.StackRead })
getStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<StackResponseDto> { getStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<StackResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.STACK_UPDATE }) @Authenticated({ permission: Permission.StackUpdate })
updateStack( updateStack(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -50,7 +50,7 @@ export class StackController {
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.STACK_DELETE }) @Authenticated({ permission: Permission.StackDelete })
deleteStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.delete(auth, id); return this.service.delete(auth, id);
} }

View File

@ -15,25 +15,25 @@ export class SystemConfigController {
) {} ) {}
@Get() @Get()
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) @Authenticated({ permission: Permission.SystemConfigRead, admin: true })
getConfig(): Promise<SystemConfigDto> { getConfig(): Promise<SystemConfigDto> {
return this.service.getSystemConfig(); return this.service.getSystemConfig();
} }
@Get('defaults') @Get('defaults')
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) @Authenticated({ permission: Permission.SystemConfigRead, admin: true })
getConfigDefaults(): SystemConfigDto { getConfigDefaults(): SystemConfigDto {
return this.service.getDefaults(); return this.service.getDefaults();
} }
@Put() @Put()
@Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true }) @Authenticated({ permission: Permission.SystemConfigUpdate, admin: true })
updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> { updateConfig(@Body() dto: SystemConfigDto): Promise<SystemConfigDto> {
return this.service.updateSystemConfig(dto); return this.service.updateSystemConfig(dto);
} }
@Get('storage-template-options') @Get('storage-template-options')
@Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) @Authenticated({ permission: Permission.SystemConfigRead, admin: true })
getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto {
return this.storageTemplateService.getStorageTemplateOptions(); return this.storageTemplateService.getStorageTemplateOptions();
} }

View File

@ -15,26 +15,26 @@ export class SystemMetadataController {
constructor(private service: SystemMetadataService) {} constructor(private service: SystemMetadataService) {}
@Get('admin-onboarding') @Get('admin-onboarding')
@Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) @Authenticated({ permission: Permission.SystemMetadataRead, admin: true })
getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> { getAdminOnboarding(): Promise<AdminOnboardingUpdateDto> {
return this.service.getAdminOnboarding(); return this.service.getAdminOnboarding();
} }
@Post('admin-onboarding') @Post('admin-onboarding')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.SYSTEM_METADATA_UPDATE, admin: true }) @Authenticated({ permission: Permission.SystemMetadataUpdate, admin: true })
updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> { updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise<void> {
return this.service.updateAdminOnboarding(dto); return this.service.updateAdminOnboarding(dto);
} }
@Get('reverse-geocoding-state') @Get('reverse-geocoding-state')
@Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) @Authenticated({ permission: Permission.SystemMetadataRead, admin: true })
getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> { getReverseGeocodingState(): Promise<ReverseGeocodingStateResponseDto> {
return this.service.getReverseGeocodingState(); return this.service.getReverseGeocodingState();
} }
@Get('version-check-state') @Get('version-check-state')
@Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) @Authenticated({ permission: Permission.SystemMetadataRead, admin: true })
getVersionCheckState(): Promise<VersionCheckStateResponseDto> { getVersionCheckState(): Promise<VersionCheckStateResponseDto> {
return this.service.getVersionCheckState(); return this.service.getVersionCheckState();
} }

View File

@ -21,50 +21,50 @@ export class TagController {
constructor(private service: TagService) {} constructor(private service: TagService) {}
@Post() @Post()
@Authenticated({ permission: Permission.TAG_CREATE }) @Authenticated({ permission: Permission.TagCreate })
createTag(@Auth() auth: AuthDto, @Body() dto: TagCreateDto): Promise<TagResponseDto> { createTag(@Auth() auth: AuthDto, @Body() dto: TagCreateDto): Promise<TagResponseDto> {
return this.service.create(auth, dto); return this.service.create(auth, dto);
} }
@Get() @Get()
@Authenticated({ permission: Permission.TAG_READ }) @Authenticated({ permission: Permission.TagRead })
getAllTags(@Auth() auth: AuthDto): Promise<TagResponseDto[]> { getAllTags(@Auth() auth: AuthDto): Promise<TagResponseDto[]> {
return this.service.getAll(auth); return this.service.getAll(auth);
} }
@Put() @Put()
@Authenticated({ permission: Permission.TAG_CREATE }) @Authenticated({ permission: Permission.TagCreate })
upsertTags(@Auth() auth: AuthDto, @Body() dto: TagUpsertDto): Promise<TagResponseDto[]> { upsertTags(@Auth() auth: AuthDto, @Body() dto: TagUpsertDto): Promise<TagResponseDto[]> {
return this.service.upsert(auth, dto); return this.service.upsert(auth, dto);
} }
@Put('assets') @Put('assets')
@Authenticated({ permission: Permission.TAG_ASSET }) @Authenticated({ permission: Permission.TagAsset })
bulkTagAssets(@Auth() auth: AuthDto, @Body() dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> { bulkTagAssets(@Auth() auth: AuthDto, @Body() dto: TagBulkAssetsDto): Promise<TagBulkAssetsResponseDto> {
return this.service.bulkTagAssets(auth, dto); return this.service.bulkTagAssets(auth, dto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.TAG_READ }) @Authenticated({ permission: Permission.TagRead })
getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> { getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<TagResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.TAG_UPDATE }) @Authenticated({ permission: Permission.TagUpdate })
updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: TagUpdateDto): Promise<TagResponseDto> { updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: TagUpdateDto): Promise<TagResponseDto> {
return this.service.update(auth, id, dto); return this.service.update(auth, id, dto);
} }
@Delete(':id') @Delete(':id')
@HttpCode(HttpStatus.NO_CONTENT) @HttpCode(HttpStatus.NO_CONTENT)
@Authenticated({ permission: Permission.TAG_DELETE }) @Authenticated({ permission: Permission.TagDelete })
deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> { deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<void> {
return this.service.remove(auth, id); return this.service.remove(auth, id);
} }
@Put(':id/assets') @Put(':id/assets')
@Authenticated({ permission: Permission.TAG_ASSET }) @Authenticated({ permission: Permission.TagAsset })
tagAssets( tagAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -74,7 +74,7 @@ export class TagController {
} }
@Delete(':id/assets') @Delete(':id/assets')
@Authenticated({ permission: Permission.TAG_ASSET }) @Authenticated({ permission: Permission.TagAsset })
untagAssets( untagAssets(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Body() dto: BulkIdsDto, @Body() dto: BulkIdsDto,

View File

@ -12,13 +12,13 @@ export class TimelineController {
constructor(private service: TimelineService) {} constructor(private service: TimelineService) {}
@Get('buckets') @Get('buckets')
@Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) @Authenticated({ permission: Permission.AssetRead, sharedLink: true })
getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto) { getTimeBuckets(@Auth() auth: AuthDto, @Query() dto: TimeBucketDto) {
return this.service.getTimeBuckets(auth, dto); return this.service.getTimeBuckets(auth, dto);
} }
@Get('bucket') @Get('bucket')
@Authenticated({ permission: Permission.ASSET_READ, sharedLink: true }) @Authenticated({ permission: Permission.AssetRead, sharedLink: true })
@ApiOkResponse({ type: TimeBucketAssetResponseDto }) @ApiOkResponse({ type: TimeBucketAssetResponseDto })
@Header('Content-Type', 'application/json') @Header('Content-Type', 'application/json')
getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto) { getTimeBucket(@Auth() auth: AuthDto, @Query() dto: TimeBucketAssetDto) {

View File

@ -14,21 +14,21 @@ export class TrashController {
@Post('empty') @Post('empty')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Authenticated({ permission: Permission.ASSET_DELETE }) @Authenticated({ permission: Permission.AssetDelete })
emptyTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> { emptyTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
return this.service.empty(auth); return this.service.empty(auth);
} }
@Post('restore') @Post('restore')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Authenticated({ permission: Permission.ASSET_DELETE }) @Authenticated({ permission: Permission.AssetDelete })
restoreTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> { restoreTrash(@Auth() auth: AuthDto): Promise<TrashResponseDto> {
return this.service.restore(auth); return this.service.restore(auth);
} }
@Post('restore/assets') @Post('restore/assets')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Authenticated({ permission: Permission.ASSET_DELETE }) @Authenticated({ permission: Permission.AssetDelete })
restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<TrashResponseDto> { restoreAssets(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise<TrashResponseDto> {
return this.service.restoreAssets(auth, dto); return this.service.restoreAssets(auth, dto);
} }

View File

@ -21,25 +21,25 @@ export class UserAdminController {
constructor(private service: UserAdminService) {} constructor(private service: UserAdminService) {}
@Get() @Get()
@Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> { searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise<UserAdminResponseDto[]> {
return this.service.search(auth, dto); return this.service.search(auth, dto);
} }
@Post() @Post()
@Authenticated({ permission: Permission.ADMIN_USER_CREATE, admin: true }) @Authenticated({ permission: Permission.AdminUserCreate, admin: true })
createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise<UserAdminResponseDto> { createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise<UserAdminResponseDto> {
return this.service.create(createUserDto); return this.service.create(createUserDto);
} }
@Get(':id') @Get(':id')
@Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> { getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
return this.service.get(auth, id); return this.service.get(auth, id);
} }
@Put(':id') @Put(':id')
@Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true }) @Authenticated({ permission: Permission.AdminUserUpdate, admin: true })
updateUserAdmin( updateUserAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -49,7 +49,7 @@ export class UserAdminController {
} }
@Delete(':id') @Delete(':id')
@Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) @Authenticated({ permission: Permission.AdminUserDelete, admin: true })
deleteUserAdmin( deleteUserAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -59,7 +59,7 @@ export class UserAdminController {
} }
@Get(':id/statistics') @Get(':id/statistics')
@Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
getUserStatisticsAdmin( getUserStatisticsAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -69,13 +69,13 @@ export class UserAdminController {
} }
@Get(':id/preferences') @Get(':id/preferences')
@Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) @Authenticated({ permission: Permission.AdminUserRead, admin: true })
getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserPreferencesResponseDto> { getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserPreferencesResponseDto> {
return this.service.getPreferences(auth, id); return this.service.getPreferences(auth, id);
} }
@Put(':id/preferences') @Put(':id/preferences')
@Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true }) @Authenticated({ permission: Permission.AdminUserUpdate, admin: true })
updateUserPreferencesAdmin( updateUserPreferencesAdmin(
@Auth() auth: AuthDto, @Auth() auth: AuthDto,
@Param() { id }: UUIDParamDto, @Param() { id }: UUIDParamDto,
@ -85,7 +85,7 @@ export class UserAdminController {
} }
@Post(':id/restore') @Post(':id/restore')
@Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) @Authenticated({ permission: Permission.AdminUserDelete, admin: true })
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> { restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise<UserAdminResponseDto> {
return this.service.restore(auth, id); return this.service.restore(auth, id);

View File

@ -30,7 +30,7 @@ import { sendFile } from 'src/utils/file';
import { UUIDParamDto } from 'src/validation'; import { UUIDParamDto } from 'src/validation';
@ApiTags('Users') @ApiTags('Users')
@Controller(RouteKey.USER) @Controller(RouteKey.User)
export class UserController { export class UserController {
constructor( constructor(
private service: UserService, private service: UserService,

View File

@ -25,8 +25,8 @@ export interface MoveRequest {
}; };
} }
export type GeneratedImageType = AssetPathType.PREVIEW | AssetPathType.THUMBNAIL | AssetPathType.FULLSIZE; export type GeneratedImageType = AssetPathType.Preview | AssetPathType.Thumbnail | AssetPathType.FullSize;
export type GeneratedAssetType = GeneratedImageType | AssetPathType.ENCODED_VIDEO; export type GeneratedAssetType = GeneratedImageType | AssetPathType.EncodedVideo;
export type ThumbnailPathEntity = { id: string; ownerId: string }; export type ThumbnailPathEntity = { id: string; ownerId: string };
@ -79,7 +79,7 @@ export class StorageCore {
} }
static getLibraryFolder(user: { storageLabel: string | null; id: string }) { static getLibraryFolder(user: { storageLabel: string | null; id: string }) {
return join(StorageCore.getBaseFolder(StorageFolder.LIBRARY), user.storageLabel || user.id); return join(StorageCore.getBaseFolder(StorageFolder.Library), user.storageLabel || user.id);
} }
static getBaseFolder(folder: StorageFolder) { static getBaseFolder(folder: StorageFolder) {
@ -87,23 +87,23 @@ export class StorageCore {
} }
static getPersonThumbnailPath(person: ThumbnailPathEntity) { static getPersonThumbnailPath(person: ThumbnailPathEntity) {
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`); return StorageCore.getNestedPath(StorageFolder.Thumbnails, person.ownerId, `${person.id}.jpeg`);
} }
static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') { static getImagePath(asset: ThumbnailPathEntity, type: GeneratedImageType, format: 'jpeg' | 'webp') {
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}-${type}.${format}`); return StorageCore.getNestedPath(StorageFolder.Thumbnails, asset.ownerId, `${asset.id}-${type}.${format}`);
} }
static getEncodedVideoPath(asset: ThumbnailPathEntity) { static getEncodedVideoPath(asset: ThumbnailPathEntity) {
return StorageCore.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}.mp4`); return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${asset.id}.mp4`);
} }
static getAndroidMotionPath(asset: ThumbnailPathEntity, uuid: string) { static getAndroidMotionPath(asset: ThumbnailPathEntity, uuid: string) {
return StorageCore.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${uuid}-MP.mp4`); return StorageCore.getNestedPath(StorageFolder.EncodedVideo, asset.ownerId, `${uuid}-MP.mp4`);
} }
static isAndroidMotionPath(originalPath: string) { static isAndroidMotionPath(originalPath: string) {
return originalPath.startsWith(StorageCore.getBaseFolder(StorageFolder.ENCODED_VIDEO)); return originalPath.startsWith(StorageCore.getBaseFolder(StorageFolder.EncodedVideo));
} }
static isImmichPath(path: string) { static isImmichPath(path: string) {
@ -130,7 +130,7 @@ export class StorageCore {
async moveAssetVideo(asset: StorageAsset) { async moveAssetVideo(asset: StorageAsset) {
return this.moveFile({ return this.moveFile({
entityId: asset.id, entityId: asset.id,
pathType: AssetPathType.ENCODED_VIDEO, pathType: AssetPathType.EncodedVideo,
oldPath: asset.encodedVideoPath, oldPath: asset.encodedVideoPath,
newPath: StorageCore.getEncodedVideoPath(asset), newPath: StorageCore.getEncodedVideoPath(asset),
}); });
@ -139,7 +139,7 @@ export class StorageCore {
async movePersonFile(person: { id: string; ownerId: string; thumbnailPath: string }, pathType: PersonPathType) { async movePersonFile(person: { id: string; ownerId: string; thumbnailPath: string }, pathType: PersonPathType) {
const { id: entityId, thumbnailPath } = person; const { id: entityId, thumbnailPath } = person;
switch (pathType) { switch (pathType) {
case PersonPathType.FACE: { case PersonPathType.Face: {
await this.moveFile({ await this.moveFile({
entityId, entityId,
pathType, pathType,
@ -188,7 +188,7 @@ export class StorageCore {
move = await this.moveRepository.create({ entityId, pathType, oldPath, newPath }); move = await this.moveRepository.create({ entityId, pathType, oldPath, newPath });
} }
if (pathType === AssetPathType.ORIGINAL && !assetInfo) { if (pathType === AssetPathType.Original && !assetInfo) {
this.logger.warn(`Unable to complete move. Missing asset info for ${entityId}`); this.logger.warn(`Unable to complete move. Missing asset info for ${entityId}`);
return; return;
} }
@ -274,25 +274,25 @@ export class StorageCore {
private savePath(pathType: PathType, id: string, newPath: string) { private savePath(pathType: PathType, id: string, newPath: string) {
switch (pathType) { switch (pathType) {
case AssetPathType.ORIGINAL: { case AssetPathType.Original: {
return this.assetRepository.update({ id, originalPath: newPath }); return this.assetRepository.update({ id, originalPath: newPath });
} }
case AssetPathType.FULLSIZE: { case AssetPathType.FullSize: {
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FULLSIZE, path: newPath }); return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FullSize, path: newPath });
} }
case AssetPathType.PREVIEW: { case AssetPathType.Preview: {
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: newPath }); return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Preview, path: newPath });
} }
case AssetPathType.THUMBNAIL: { case AssetPathType.Thumbnail: {
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: newPath }); return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Thumbnail, path: newPath });
} }
case AssetPathType.ENCODED_VIDEO: { case AssetPathType.EncodedVideo: {
return this.assetRepository.update({ id, encodedVideoPath: newPath }); return this.assetRepository.update({ id, encodedVideoPath: newPath });
} }
case AssetPathType.SIDECAR: { case AssetPathType.Sidecar: {
return this.assetRepository.update({ id, sidecarPath: newPath }); return this.assetRepository.update({ id, sidecarPath: newPath });
} }
case PersonPathType.FACE: { case PersonPathType.Face: {
return this.personRepository.update({ id, thumbnailPath: newPath }); return this.personRepository.update({ id, thumbnailPath: newPath });
} }
} }

View File

@ -131,7 +131,7 @@ export interface GenerateSqlQueries {
} }
export const Telemetry = (options: { enabled?: boolean }) => export const Telemetry = (options: { enabled?: boolean }) =>
SetMetadata(MetadataKey.TELEMETRY_ENABLED, options?.enabled ?? true); SetMetadata(MetadataKey.TelemetryEnabled, options?.enabled ?? true);
/** Decorator to enable versioning/tracking of generated Sql */ /** Decorator to enable versioning/tracking of generated Sql */
export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options); export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options);
@ -145,13 +145,13 @@ export type EventConfig = {
/** register events for these workers, defaults to all workers */ /** register events for these workers, defaults to all workers */
workers?: ImmichWorker[]; workers?: ImmichWorker[];
}; };
export const OnEvent = (config: EventConfig) => SetMetadata(MetadataKey.EVENT_CONFIG, config); export const OnEvent = (config: EventConfig) => SetMetadata(MetadataKey.EventConfig, config);
export type JobConfig = { export type JobConfig = {
name: JobName; name: JobName;
queue: QueueName; queue: QueueName;
}; };
export const OnJob = (config: JobConfig) => SetMetadata(MetadataKey.JOB_CONFIG, config); export const OnJob = (config: JobConfig) => SetMetadata(MetadataKey.JobConfig, config);
type LifecycleRelease = 'NEXT_RELEASE' | string; type LifecycleRelease = 'NEXT_RELEASE' | string;
type LifecycleMetadata = { type LifecycleMetadata = {

View File

@ -18,7 +18,7 @@ export class AlbumUserAddDto {
@ValidateUUID() @ValidateUUID()
userId!: string; userId!: string;
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', default: AlbumUserRole.EDITOR }) @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', default: AlbumUserRole.Editor })
role?: AlbumUserRole; role?: AlbumUserRole;
} }

View File

@ -205,7 +205,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset
localDateTime: entity.localDateTime, localDateTime: entity.localDateTime,
updatedAt: entity.updatedAt, updatedAt: entity.updatedAt,
isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false, isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false,
isArchived: entity.visibility === AssetVisibility.ARCHIVE, isArchived: entity.visibility === AssetVisibility.Archive,
isTrashed: !!entity.deletedAt, isTrashed: !!entity.deletedAt,
visibility: entity.visibility, visibility: entity.visibility,
duration: entity.duration ?? '0:00:00.00000', duration: entity.duration ?? '0:00:00.00000',

View File

@ -126,8 +126,8 @@ export class AssetStatsResponseDto {
export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
return { return {
images: stats[AssetType.IMAGE], images: stats[AssetType.Image],
videos: stats[AssetType.VIDEO], videos: stats[AssetType.Video],
total: Object.values(stats).reduce((total, value) => total + value, 0), total: Object.values(stats).reduce((total, value) => total + value, 0),
}; };
}; };

View File

@ -45,7 +45,7 @@ export class LoginResponseDto {
export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto { export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto {
const onboardingMetadata = entity.metadata.find( const onboardingMetadata = entity.metadata.find(
(item): item is UserMetadataItem<UserMetadataKey.ONBOARDING> => item.key === UserMetadataKey.ONBOARDING, (item): item is UserMetadataItem<UserMetadataKey.Onboarding> => item.key === UserMetadataKey.Onboarding,
)?.value; )?.value;
return { return {

View File

@ -50,47 +50,47 @@ export class JobStatusDto {
export class AllJobStatusResponseDto implements Record<QueueName, JobStatusDto> { export class AllJobStatusResponseDto implements Record<QueueName, JobStatusDto> {
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.THUMBNAIL_GENERATION]!: JobStatusDto; [QueueName.ThumbnailGeneration]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.METADATA_EXTRACTION]!: JobStatusDto; [QueueName.MetadataExtraction]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.VIDEO_CONVERSION]!: JobStatusDto; [QueueName.VideoConversion]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.SMART_SEARCH]!: JobStatusDto; [QueueName.SmartSearch]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.STORAGE_TEMPLATE_MIGRATION]!: JobStatusDto; [QueueName.StorageTemplateMigration]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.MIGRATION]!: JobStatusDto; [QueueName.Migration]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.BACKGROUND_TASK]!: JobStatusDto; [QueueName.BackgroundTask]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.SEARCH]!: JobStatusDto; [QueueName.Search]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.DUPLICATE_DETECTION]!: JobStatusDto; [QueueName.DuplicateDetection]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.FACE_DETECTION]!: JobStatusDto; [QueueName.FaceDetection]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.FACIAL_RECOGNITION]!: JobStatusDto; [QueueName.FacialRecognition]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.SIDECAR]!: JobStatusDto; [QueueName.Sidecar]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.LIBRARY]!: JobStatusDto; [QueueName.Library]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.NOTIFICATION]!: JobStatusDto; [QueueName.Notification]!: JobStatusDto;
@ApiProperty({ type: JobStatusDto }) @ApiProperty({ type: JobStatusDto })
[QueueName.BACKUP_DATABASE]!: JobStatusDto; [QueueName.BackupDatabase]!: JobStatusDto;
} }

View File

@ -50,7 +50,7 @@ export class MemoryCreateDto extends MemoryBaseDto {
@ValidateNested() @ValidateNested()
@Type((options) => { @Type((options) => {
switch (options?.object.type) { switch (options?.object.type) {
case MemoryType.ON_THIS_DAY: { case MemoryType.OnThisDay: {
return OnThisDayDto; return OnThisDayDto;
} }

View File

@ -170,7 +170,7 @@ export class MetadataSearchDto extends RandomSearchDto {
@Optional() @Optional()
encodedVideoPath?: string; encodedVideoPath?: string;
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, default: AssetOrder.DESC }) @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, default: AssetOrder.Desc })
order?: AssetOrder; order?: AssetOrder;
@IsInt() @IsInt()

View File

@ -26,7 +26,7 @@ import {
OAuthTokenEndpointAuthMethod, OAuthTokenEndpointAuthMethod,
QueueName, QueueName,
ToneMapping, ToneMapping,
TranscodeHWAccel, TranscodeHardwareAcceleration,
TranscodePolicy, TranscodePolicy,
VideoCodec, VideoCodec,
VideoContainer, VideoContainer,
@ -136,8 +136,8 @@ export class SystemConfigFFmpegDto {
@ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy' }) @ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy' })
transcode!: TranscodePolicy; transcode!: TranscodePolicy;
@ValidateEnum({ enum: TranscodeHWAccel, name: 'TranscodeHWAccel' }) @ValidateEnum({ enum: TranscodeHardwareAcceleration, name: 'TranscodeHWAccel' })
accel!: TranscodeHWAccel; accel!: TranscodeHardwareAcceleration;
@ValidateBoolean() @ValidateBoolean()
accelDecode!: boolean; accelDecode!: boolean;
@ -158,67 +158,67 @@ class SystemConfigJobDto implements Record<ConcurrentQueueName, JobSettingsDto>
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.THUMBNAIL_GENERATION]!: JobSettingsDto; [QueueName.ThumbnailGeneration]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto }) @ApiProperty({ type: JobSettingsDto })
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.METADATA_EXTRACTION]!: JobSettingsDto; [QueueName.MetadataExtraction]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto }) @ApiProperty({ type: JobSettingsDto })
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.VIDEO_CONVERSION]!: JobSettingsDto; [QueueName.VideoConversion]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto }) @ApiProperty({ type: JobSettingsDto })
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.SMART_SEARCH]!: JobSettingsDto; [QueueName.SmartSearch]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto }) @ApiProperty({ type: JobSettingsDto })
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.MIGRATION]!: JobSettingsDto; [QueueName.Migration]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto }) @ApiProperty({ type: JobSettingsDto })
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.BACKGROUND_TASK]!: JobSettingsDto; [QueueName.BackgroundTask]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto }) @ApiProperty({ type: JobSettingsDto })
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.SEARCH]!: JobSettingsDto; [QueueName.Search]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto }) @ApiProperty({ type: JobSettingsDto })
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.FACE_DETECTION]!: JobSettingsDto; [QueueName.FaceDetection]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto }) @ApiProperty({ type: JobSettingsDto })
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.SIDECAR]!: JobSettingsDto; [QueueName.Sidecar]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto }) @ApiProperty({ type: JobSettingsDto })
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.LIBRARY]!: JobSettingsDto; [QueueName.Library]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto }) @ApiProperty({ type: JobSettingsDto })
@ValidateNested() @ValidateNested()
@IsObject() @IsObject()
@Type(() => JobSettingsDto) @Type(() => JobSettingsDto)
[QueueName.NOTIFICATION]!: JobSettingsDto; [QueueName.Notification]!: JobSettingsDto;
} }
class SystemConfigLibraryScanDto { class SystemConfigLibraryScanDto {

View File

@ -157,7 +157,7 @@ export class UserPreferencesUpdateDto {
class AlbumsResponse { class AlbumsResponse {
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' }) @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' })
defaultAssetOrder: AssetOrder = AssetOrder.DESC; defaultAssetOrder: AssetOrder = AssetOrder.Desc;
} }
class RatingsResponse { class RatingsResponse {

View File

@ -171,7 +171,7 @@ export class UserAdminResponseDto extends UserResponseDto {
export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto {
const metadata = entity.metadata || []; const metadata = entity.metadata || [];
const license = metadata.find( const license = metadata.find(
(item): item is UserMetadataItem<UserMetadataKey.LICENSE> => item.key === UserMetadataKey.LICENSE, (item): item is UserMetadataItem<UserMetadataKey.License> => item.key === UserMetadataKey.License,
)?.value; )?.value;
return { return {
...mapUser(entity), ...mapUser(entity),

View File

@ -1,402 +1,397 @@
export enum AuthType { export enum AuthType {
PASSWORD = 'password', Password = 'password',
OAUTH = 'oauth', OAuth = 'oauth',
} }
export enum ImmichCookie { export enum ImmichCookie {
ACCESS_TOKEN = 'immich_access_token', AccessToken = 'immich_access_token',
AUTH_TYPE = 'immich_auth_type', AuthType = 'immich_auth_type',
IS_AUTHENTICATED = 'immich_is_authenticated', IsAuthenticated = 'immich_is_authenticated',
SHARED_LINK_TOKEN = 'immich_shared_link_token', SharedLinkToken = 'immich_shared_link_token',
OAUTH_STATE = 'immich_oauth_state', OAuthState = 'immich_oauth_state',
OAUTH_CODE_VERIFIER = 'immich_oauth_code_verifier', OAuthCodeVerifier = 'immich_oauth_code_verifier',
} }
export enum ImmichHeader { export enum ImmichHeader {
API_KEY = 'x-api-key', ApiKey = 'x-api-key',
USER_TOKEN = 'x-immich-user-token', UserToken = 'x-immich-user-token',
SESSION_TOKEN = 'x-immich-session-token', SessionToken = 'x-immich-session-token',
SHARED_LINK_KEY = 'x-immich-share-key', SharedLinkKey = 'x-immich-share-key',
CHECKSUM = 'x-immich-checksum', Checksum = 'x-immich-checksum',
CID = 'x-immich-cid', Cid = 'x-immich-cid',
} }
export enum ImmichQuery { export enum ImmichQuery {
SHARED_LINK_KEY = 'key', SharedLinkKey = 'key',
API_KEY = 'apiKey', ApiKey = 'apiKey',
SESSION_KEY = 'sessionKey', SessionKey = 'sessionKey',
} }
export enum AssetType { export enum AssetType {
IMAGE = 'IMAGE', Image = 'IMAGE',
VIDEO = 'VIDEO', Video = 'VIDEO',
AUDIO = 'AUDIO', Audio = 'AUDIO',
OTHER = 'OTHER', Other = 'OTHER',
} }
export enum AssetFileType { export enum AssetFileType {
/** /**
* An full/large-size image extracted/converted from RAW photos * An full/large-size image extracted/converted from RAW photos
*/ */
FULLSIZE = 'fullsize', FullSize = 'fullsize',
PREVIEW = 'preview', Preview = 'preview',
THUMBNAIL = 'thumbnail', Thumbnail = 'thumbnail',
} }
export enum AlbumUserRole { export enum AlbumUserRole {
EDITOR = 'editor', Editor = 'editor',
VIEWER = 'viewer', Viewer = 'viewer',
} }
export enum AssetOrder { export enum AssetOrder {
ASC = 'asc', Asc = 'asc',
DESC = 'desc', Desc = 'desc',
} }
export enum DatabaseAction { export enum DatabaseAction {
CREATE = 'CREATE', Create = 'CREATE',
UPDATE = 'UPDATE', Update = 'UPDATE',
DELETE = 'DELETE', Delete = 'DELETE',
} }
export enum EntityType { export enum EntityType {
ASSET = 'ASSET', Asset = 'ASSET',
ALBUM = 'ALBUM', Album = 'ALBUM',
} }
export enum MemoryType { export enum MemoryType {
/** pictures taken on this day X years ago */ /** pictures taken on this day X years ago */
ON_THIS_DAY = 'on_this_day', OnThisDay = 'on_this_day',
} }
export enum Permission { export enum Permission {
ALL = 'all', All = 'all',
ACTIVITY_CREATE = 'activity.create', ActivityCreate = 'activity.create',
ACTIVITY_READ = 'activity.read', ActivityRead = 'activity.read',
ACTIVITY_UPDATE = 'activity.update', ActivityUpdate = 'activity.update',
ACTIVITY_DELETE = 'activity.delete', ActivityDelete = 'activity.delete',
ACTIVITY_STATISTICS = 'activity.statistics', ActivityStatistics = 'activity.statistics',
API_KEY_CREATE = 'apiKey.create', ApiKeyCreate = 'apiKey.create',
API_KEY_READ = 'apiKey.read', ApiKeyRead = 'apiKey.read',
API_KEY_UPDATE = 'apiKey.update', ApiKeyUpdate = 'apiKey.update',
API_KEY_DELETE = 'apiKey.delete', ApiKeyDelete = 'apiKey.delete',
// ASSET_CREATE = 'asset.create', // ASSET_CREATE = 'asset.create',
ASSET_READ = 'asset.read', AssetRead = 'asset.read',
ASSET_UPDATE = 'asset.update', AssetUpdate = 'asset.update',
ASSET_DELETE = 'asset.delete', AssetDelete = 'asset.delete',
ASSET_SHARE = 'asset.share', AssetShare = 'asset.share',
ASSET_VIEW = 'asset.view', AssetView = 'asset.view',
ASSET_DOWNLOAD = 'asset.download', AssetDownload = 'asset.download',
ASSET_UPLOAD = 'asset.upload', AssetUpload = 'asset.upload',
ALBUM_CREATE = 'album.create', AlbumCreate = 'album.create',
ALBUM_READ = 'album.read', AlbumRead = 'album.read',
ALBUM_UPDATE = 'album.update', AlbumUpdate = 'album.update',
ALBUM_DELETE = 'album.delete', AlbumDelete = 'album.delete',
ALBUM_STATISTICS = 'album.statistics', AlbumStatistics = 'album.statistics',
ALBUM_ADD_ASSET = 'album.addAsset', AlbumAddAsset = 'album.addAsset',
ALBUM_REMOVE_ASSET = 'album.removeAsset', AlbumRemoveAsset = 'album.removeAsset',
ALBUM_SHARE = 'album.share', AlbumShare = 'album.share',
ALBUM_DOWNLOAD = 'album.download', AlbumDownload = 'album.download',
AUTH_DEVICE_DELETE = 'authDevice.delete', AuthDeviceDelete = 'authDevice.delete',
ARCHIVE_READ = 'archive.read', ArchiveRead = 'archive.read',
FACE_CREATE = 'face.create', FaceCreate = 'face.create',
FACE_READ = 'face.read', FaceRead = 'face.read',
FACE_UPDATE = 'face.update', FaceUpdate = 'face.update',
FACE_DELETE = 'face.delete', FaceDelete = 'face.delete',
LIBRARY_CREATE = 'library.create', LibraryCreate = 'library.create',
LIBRARY_READ = 'library.read', LibraryRead = 'library.read',
LIBRARY_UPDATE = 'library.update', LibraryUpdate = 'library.update',
LIBRARY_DELETE = 'library.delete', LibraryDelete = 'library.delete',
LIBRARY_STATISTICS = 'library.statistics', LibraryStatistics = 'library.statistics',
TIMELINE_READ = 'timeline.read', TimelineRead = 'timeline.read',
TIMELINE_DOWNLOAD = 'timeline.download', TimelineDownload = 'timeline.download',
MEMORY_CREATE = 'memory.create', MemoryCreate = 'memory.create',
MEMORY_READ = 'memory.read', MemoryRead = 'memory.read',
MEMORY_UPDATE = 'memory.update', MemoryUpdate = 'memory.update',
MEMORY_DELETE = 'memory.delete', MemoryDelete = 'memory.delete',
NOTIFICATION_CREATE = 'notification.create', NotificationCreate = 'notification.create',
NOTIFICATION_READ = 'notification.read', NotificationRead = 'notification.read',
NOTIFICATION_UPDATE = 'notification.update', NotificationUpdate = 'notification.update',
NOTIFICATION_DELETE = 'notification.delete', NotificationDelete = 'notification.delete',
PARTNER_CREATE = 'partner.create', PartnerCreate = 'partner.create',
PARTNER_READ = 'partner.read', PartnerRead = 'partner.read',
PARTNER_UPDATE = 'partner.update', PartnerUpdate = 'partner.update',
PARTNER_DELETE = 'partner.delete', PartnerDelete = 'partner.delete',
PERSON_CREATE = 'person.create', PersonCreate = 'person.create',
PERSON_READ = 'person.read', PersonRead = 'person.read',
PERSON_UPDATE = 'person.update', PersonUpdate = 'person.update',
PERSON_DELETE = 'person.delete', PersonDelete = 'person.delete',
PERSON_STATISTICS = 'person.statistics', PersonStatistics = 'person.statistics',
PERSON_MERGE = 'person.merge', PersonMerge = 'person.merge',
PERSON_REASSIGN = 'person.reassign', PersonReassign = 'person.reassign',
SESSION_CREATE = 'session.create', SessionCreate = 'session.create',
SESSION_READ = 'session.read', SessionRead = 'session.read',
SESSION_UPDATE = 'session.update', SessionUpdate = 'session.update',
SESSION_DELETE = 'session.delete', SessionDelete = 'session.delete',
SESSION_LOCK = 'session.lock', SessionLock = 'session.lock',
SHARED_LINK_CREATE = 'sharedLink.create', SharedLinkCreate = 'sharedLink.create',
SHARED_LINK_READ = 'sharedLink.read', SharedLinkRead = 'sharedLink.read',
SHARED_LINK_UPDATE = 'sharedLink.update', SharedLinkUpdate = 'sharedLink.update',
SHARED_LINK_DELETE = 'sharedLink.delete', SharedLinkDelete = 'sharedLink.delete',
STACK_CREATE = 'stack.create', StackCreate = 'stack.create',
STACK_READ = 'stack.read', StackRead = 'stack.read',
STACK_UPDATE = 'stack.update', StackUpdate = 'stack.update',
STACK_DELETE = 'stack.delete', StackDelete = 'stack.delete',
SYSTEM_CONFIG_READ = 'systemConfig.read', SystemConfigRead = 'systemConfig.read',
SYSTEM_CONFIG_UPDATE = 'systemConfig.update', SystemConfigUpdate = 'systemConfig.update',
SYSTEM_METADATA_READ = 'systemMetadata.read', SystemMetadataRead = 'systemMetadata.read',
SYSTEM_METADATA_UPDATE = 'systemMetadata.update', SystemMetadataUpdate = 'systemMetadata.update',
TAG_CREATE = 'tag.create', TagCreate = 'tag.create',
TAG_READ = 'tag.read', TagRead = 'tag.read',
TAG_UPDATE = 'tag.update', TagUpdate = 'tag.update',
TAG_DELETE = 'tag.delete', TagDelete = 'tag.delete',
TAG_ASSET = 'tag.asset', TagAsset = 'tag.asset',
ADMIN_USER_CREATE = 'admin.user.create', AdminUserCreate = 'admin.user.create',
ADMIN_USER_READ = 'admin.user.read', AdminUserRead = 'admin.user.read',
ADMIN_USER_UPDATE = 'admin.user.update', AdminUserUpdate = 'admin.user.update',
ADMIN_USER_DELETE = 'admin.user.delete', AdminUserDelete = 'admin.user.delete',
} }
export enum SharedLinkType { export enum SharedLinkType {
ALBUM = 'ALBUM', Album = 'ALBUM',
/** /**
* Individual asset * Individual asset
* or group of assets that are not in an album * or group of assets that are not in an album
*/ */
INDIVIDUAL = 'INDIVIDUAL', Individual = 'INDIVIDUAL',
} }
export enum StorageFolder { export enum StorageFolder {
ENCODED_VIDEO = 'encoded-video', EncodedVideo = 'encoded-video',
LIBRARY = 'library', Library = 'library',
UPLOAD = 'upload', Upload = 'upload',
PROFILE = 'profile', Profile = 'profile',
THUMBNAILS = 'thumbs', Thumbnails = 'thumbs',
BACKUPS = 'backups', Backups = 'backups',
} }
export enum SystemMetadataKey { export enum SystemMetadataKey {
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state', ReverseGeocodingState = 'reverse-geocoding-state',
FACIAL_RECOGNITION_STATE = 'facial-recognition-state', FacialRecognitionState = 'facial-recognition-state',
MEMORIES_STATE = 'memories-state', MemoriesState = 'memories-state',
ADMIN_ONBOARDING = 'admin-onboarding', AdminOnboarding = 'admin-onboarding',
SYSTEM_CONFIG = 'system-config', SystemConfig = 'system-config',
SYSTEM_FLAGS = 'system-flags', SystemFlags = 'system-flags',
VERSION_CHECK_STATE = 'version-check-state', VersionCheckState = 'version-check-state',
LICENSE = 'license', License = 'license',
} }
export enum UserMetadataKey { export enum UserMetadataKey {
PREFERENCES = 'preferences', Preferences = 'preferences',
LICENSE = 'license', License = 'license',
ONBOARDING = 'onboarding', Onboarding = 'onboarding',
} }
export enum UserAvatarColor { export enum UserAvatarColor {
PRIMARY = 'primary', Primary = 'primary',
PINK = 'pink', Pink = 'pink',
RED = 'red', Red = 'red',
YELLOW = 'yellow', Yellow = 'yellow',
BLUE = 'blue', Blue = 'blue',
GREEN = 'green', Green = 'green',
PURPLE = 'purple', Purple = 'purple',
ORANGE = 'orange', Orange = 'orange',
GRAY = 'gray', Gray = 'gray',
AMBER = 'amber', Amber = 'amber',
} }
export enum UserStatus { export enum UserStatus {
ACTIVE = 'active', Active = 'active',
REMOVING = 'removing', Removing = 'removing',
DELETED = 'deleted', Deleted = 'deleted',
} }
export enum AssetStatus { export enum AssetStatus {
ACTIVE = 'active', Active = 'active',
TRASHED = 'trashed', Trashed = 'trashed',
DELETED = 'deleted', Deleted = 'deleted',
} }
export enum SourceType { export enum SourceType {
MACHINE_LEARNING = 'machine-learning', MachineLearning = 'machine-learning',
EXIF = 'exif', Exif = 'exif',
MANUAL = 'manual', Manual = 'manual',
} }
export enum ManualJobName { export enum ManualJobName {
PERSON_CLEANUP = 'person-cleanup', PersonCleanup = 'person-cleanup',
TAG_CLEANUP = 'tag-cleanup', TagCleanup = 'tag-cleanup',
USER_CLEANUP = 'user-cleanup', UserCleanup = 'user-cleanup',
MEMORY_CLEANUP = 'memory-cleanup', MemoryCleanup = 'memory-cleanup',
MEMORY_CREATE = 'memory-create', MemoryCreate = 'memory-create',
BACKUP_DATABASE = 'backup-database', BackupDatabase = 'backup-database',
} }
export enum AssetPathType { export enum AssetPathType {
ORIGINAL = 'original', Original = 'original',
FULLSIZE = 'fullsize', FullSize = 'fullsize',
PREVIEW = 'preview', Preview = 'preview',
THUMBNAIL = 'thumbnail', Thumbnail = 'thumbnail',
ENCODED_VIDEO = 'encoded_video', EncodedVideo = 'encoded_video',
SIDECAR = 'sidecar', Sidecar = 'sidecar',
} }
export enum PersonPathType { export enum PersonPathType {
FACE = 'face', Face = 'face',
} }
export enum UserPathType { export enum UserPathType {
PROFILE = 'profile', Profile = 'profile',
} }
export type PathType = AssetPathType | PersonPathType | UserPathType; export type PathType = AssetPathType | PersonPathType | UserPathType;
export enum TranscodePolicy { export enum TranscodePolicy {
ALL = 'all', All = 'all',
OPTIMAL = 'optimal', Optimal = 'optimal',
BITRATE = 'bitrate', Bitrate = 'bitrate',
REQUIRED = 'required', Required = 'required',
DISABLED = 'disabled', Disabled = 'disabled',
} }
export enum TranscodeTarget { export enum TranscodeTarget {
NONE, None = 'NONE',
AUDIO, Audio = 'AUDIO',
VIDEO, Video = 'VIDEO',
ALL, All = 'ALL',
} }
export enum VideoCodec { export enum VideoCodec {
H264 = 'h264', H264 = 'h264',
HEVC = 'hevc', Hevc = 'hevc',
VP9 = 'vp9', Vp9 = 'vp9',
AV1 = 'av1', Av1 = 'av1',
} }
export enum AudioCodec { export enum AudioCodec {
MP3 = 'mp3', Mp3 = 'mp3',
AAC = 'aac', Aac = 'aac',
LIBOPUS = 'libopus', LibOpus = 'libopus',
PCMS16LE = 'pcm_s16le', PcmS16le = 'pcm_s16le',
} }
export enum VideoContainer { export enum VideoContainer {
MOV = 'mov', Mov = 'mov',
MP4 = 'mp4', Mp4 = 'mp4',
OGG = 'ogg', Ogg = 'ogg',
WEBM = 'webm', Webm = 'webm',
} }
export enum TranscodeHWAccel { export enum TranscodeHardwareAcceleration {
NVENC = 'nvenc', Nvenc = 'nvenc',
QSV = 'qsv', Qsv = 'qsv',
VAAPI = 'vaapi', Vaapi = 'vaapi',
RKMPP = 'rkmpp', Rkmpp = 'rkmpp',
DISABLED = 'disabled', Disabled = 'disabled',
} }
export enum ToneMapping { export enum ToneMapping {
HABLE = 'hable', Hable = 'hable',
MOBIUS = 'mobius', Mobius = 'mobius',
REINHARD = 'reinhard', Reinhard = 'reinhard',
DISABLED = 'disabled', Disabled = 'disabled',
} }
export enum CQMode { export enum CQMode {
AUTO = 'auto', Auto = 'auto',
CQP = 'cqp', Cqp = 'cqp',
ICQ = 'icq', Icq = 'icq',
} }
export enum Colorspace { export enum Colorspace {
SRGB = 'srgb', Srgb = 'srgb',
P3 = 'p3', P3 = 'p3',
} }
export enum ImageFormat { export enum ImageFormat {
JPEG = 'jpeg', Jpeg = 'jpeg',
WEBP = 'webp', Webp = 'webp',
} }
export enum RawExtractedFormat { export enum RawExtractedFormat {
JPEG = 'jpeg', Jpeg = 'jpeg',
JXL = 'jxl', Jxl = 'jxl',
} }
export enum LogLevel { export enum LogLevel {
VERBOSE = 'verbose', Verbose = 'verbose',
DEBUG = 'debug', Debug = 'debug',
LOG = 'log', Log = 'log',
WARN = 'warn', Warn = 'warn',
ERROR = 'error', Error = 'error',
FATAL = 'fatal', Fatal = 'fatal',
} }
export enum MetadataKey { export enum MetadataKey {
AUTH_ROUTE = 'auth_route', AuthRoute = 'auth_route',
ADMIN_ROUTE = 'admin_route', AdminRoute = 'admin_route',
SHARED_ROUTE = 'shared_route', SharedRoute = 'shared_route',
API_KEY_SECURITY = 'api_key', ApiKeySecurity = 'api_key',
EVENT_CONFIG = 'event_config', EventConfig = 'event_config',
JOB_CONFIG = 'job_config', JobConfig = 'job_config',
TELEMETRY_ENABLED = 'telemetry_enabled', TelemetryEnabled = 'telemetry_enabled',
} }
export enum RouteKey { export enum RouteKey {
ASSET = 'assets', Asset = 'assets',
USER = 'users', User = 'users',
} }
export enum CacheControl { export enum CacheControl {
PRIVATE_WITH_CACHE = 'private_with_cache', PrivateWithCache = 'private_with_cache',
PRIVATE_WITHOUT_CACHE = 'private_without_cache', PrivateWithoutCache = 'private_without_cache',
NONE = 'none', None = 'none',
}
export enum PaginationMode {
LIMIT_OFFSET = 'limit-offset',
SKIP_TAKE = 'skip-take',
} }
export enum ImmichEnvironment { export enum ImmichEnvironment {
DEVELOPMENT = 'development', Development = 'development',
TESTING = 'testing', Testing = 'testing',
PRODUCTION = 'production', Production = 'production',
} }
export enum ImmichWorker { export enum ImmichWorker {
API = 'api', Api = 'api',
MICROSERVICES = 'microservices', Microservices = 'microservices',
} }
export enum ImmichTelemetry { export enum ImmichTelemetry {
HOST = 'host', Host = 'host',
API = 'api', Api = 'api',
IO = 'io', Io = 'io',
REPO = 'repo', Repo = 'repo',
JOB = 'job', Job = 'job',
} }
export enum ExifOrientation { export enum ExifOrientation {
@ -411,11 +406,11 @@ export enum ExifOrientation {
} }
export enum DatabaseExtension { export enum DatabaseExtension {
CUBE = 'cube', Cube = 'cube',
EARTH_DISTANCE = 'earthdistance', EarthDistance = 'earthdistance',
VECTOR = 'vector', Vector = 'vector',
VECTORS = 'vectors', Vectors = 'vectors',
VECTORCHORD = 'vchord', VectorChord = 'vchord',
} }
export enum BootstrapEventPriority { export enum BootstrapEventPriority {
@ -428,135 +423,135 @@ export enum BootstrapEventPriority {
} }
export enum QueueName { export enum QueueName {
THUMBNAIL_GENERATION = 'thumbnailGeneration', ThumbnailGeneration = 'thumbnailGeneration',
METADATA_EXTRACTION = 'metadataExtraction', MetadataExtraction = 'metadataExtraction',
VIDEO_CONVERSION = 'videoConversion', VideoConversion = 'videoConversion',
FACE_DETECTION = 'faceDetection', FaceDetection = 'faceDetection',
FACIAL_RECOGNITION = 'facialRecognition', FacialRecognition = 'facialRecognition',
SMART_SEARCH = 'smartSearch', SmartSearch = 'smartSearch',
DUPLICATE_DETECTION = 'duplicateDetection', DuplicateDetection = 'duplicateDetection',
BACKGROUND_TASK = 'backgroundTask', BackgroundTask = 'backgroundTask',
STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration', StorageTemplateMigration = 'storageTemplateMigration',
MIGRATION = 'migration', Migration = 'migration',
SEARCH = 'search', Search = 'search',
SIDECAR = 'sidecar', Sidecar = 'sidecar',
LIBRARY = 'library', Library = 'library',
NOTIFICATION = 'notifications', Notification = 'notifications',
BACKUP_DATABASE = 'backupDatabase', BackupDatabase = 'backupDatabase',
} }
export enum JobName { export enum JobName {
//backups //backups
BACKUP_DATABASE = 'database-backup', BackupDatabase = 'database-backup',
// conversion // conversion
QUEUE_VIDEO_CONVERSION = 'queue-video-conversion', QueueVideoConversion = 'queue-video-conversion',
VIDEO_CONVERSION = 'video-conversion', VideoConversation = 'video-conversion',
// thumbnails // thumbnails
QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails', QueueGenerateThumbnails = 'queue-generate-thumbnails',
GENERATE_THUMBNAILS = 'generate-thumbnails', GenerateThumbnails = 'generate-thumbnails',
GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail', GeneratePersonThumbnail = 'generate-person-thumbnail',
// metadata // metadata
QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction', QueueMetadataExtraction = 'queue-metadata-extraction',
METADATA_EXTRACTION = 'metadata-extraction', MetadataExtraction = 'metadata-extraction',
// user // user
USER_DELETION = 'user-deletion', UserDeletion = 'user-deletion',
USER_DELETE_CHECK = 'user-delete-check', UserDeleteCheck = 'user-delete-check',
USER_SYNC_USAGE = 'user-sync-usage', userSyncUsage = 'user-sync-usage',
// asset // asset
ASSET_DELETION = 'asset-deletion', AssetDeletion = 'asset-deletion',
ASSET_DELETION_CHECK = 'asset-deletion-check', AssetDeletionCheck = 'asset-deletion-check',
// storage template // storage template
STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration', StorageTemplateMigration = 'storage-template-migration',
STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single', StorageTemplateMigrationSingle = 'storage-template-migration-single',
// tags // tags
TAG_CLEANUP = 'tag-cleanup', TagCleanup = 'tag-cleanup',
// migration // migration
QUEUE_MIGRATION = 'queue-migration', QueueMigration = 'queue-migration',
MIGRATE_ASSET = 'migrate-asset', MigrateAsset = 'migrate-asset',
MIGRATE_PERSON = 'migrate-person', MigratePerson = 'migrate-person',
// facial recognition // facial recognition
PERSON_CLEANUP = 'person-cleanup', PersonCleanup = 'person-cleanup',
QUEUE_FACE_DETECTION = 'queue-face-detection', QueueFaceDetection = 'queue-face-detection',
FACE_DETECTION = 'face-detection', FaceDetection = 'face-detection',
QUEUE_FACIAL_RECOGNITION = 'queue-facial-recognition', QueueFacialRecognition = 'queue-facial-recognition',
FACIAL_RECOGNITION = 'facial-recognition', FacialRecognition = 'facial-recognition',
// library management // library management
LIBRARY_QUEUE_SYNC_FILES = 'library-queue-sync-files', LibraryQueueSyncFiles = 'library-queue-sync-files',
LIBRARY_QUEUE_SYNC_ASSETS = 'library-queue-sync-assets', LibraryQueueSyncAssets = 'library-queue-sync-assets',
LIBRARY_SYNC_FILES = 'library-sync-files', LibrarySyncFiles = 'library-sync-files',
LIBRARY_SYNC_ASSETS = 'library-sync-assets', LibrarySyncAssets = 'library-sync-assets',
LIBRARY_ASSET_REMOVAL = 'handle-library-file-deletion', LibraryAssetRemoval = 'handle-library-file-deletion',
LIBRARY_DELETE = 'library-delete', LibraryDelete = 'library-delete',
LIBRARY_QUEUE_SCAN_ALL = 'library-queue-scan-all', LibraryQueueScanAll = 'library-queue-scan-all',
LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', LibraryQueueCleanup = 'library-queue-cleanup',
// cleanup // cleanup
DELETE_FILES = 'delete-files', DeleteFiles = 'delete-files',
CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs', CleanOldAuditLogs = 'clean-old-audit-logs',
CLEAN_OLD_SESSION_TOKENS = 'clean-old-session-tokens', CleanOldSessionTokens = 'clean-old-session-tokens',
// memories // memories
MEMORIES_CLEANUP = 'memories-cleanup', MemoriesCleanup = 'memories-cleanup',
MEMORIES_CREATE = 'memories-create', MemoriesCreate = 'memories-create',
// smart search // smart search
QUEUE_SMART_SEARCH = 'queue-smart-search', QueueSmartSearch = 'queue-smart-search',
SMART_SEARCH = 'smart-search', SmartSearch = 'smart-search',
QUEUE_TRASH_EMPTY = 'queue-trash-empty', QueueTrashEmpty = 'queue-trash-empty',
// duplicate detection // duplicate detection
QUEUE_DUPLICATE_DETECTION = 'queue-duplicate-detection', QueueDuplicateDetection = 'queue-duplicate-detection',
DUPLICATE_DETECTION = 'duplicate-detection', DuplicateDetection = 'duplicate-detection',
// XMP sidecars // XMP sidecars
QUEUE_SIDECAR = 'queue-sidecar', QueueSidecar = 'queue-sidecar',
SIDECAR_DISCOVERY = 'sidecar-discovery', SidecarDiscovery = 'sidecar-discovery',
SIDECAR_SYNC = 'sidecar-sync', SidecarSync = 'sidecar-sync',
SIDECAR_WRITE = 'sidecar-write', SidecarWrite = 'sidecar-write',
// Notification // Notification
NOTIFY_SIGNUP = 'notify-signup', NotifySignup = 'notify-signup',
NOTIFY_ALBUM_INVITE = 'notify-album-invite', NotifyAlbumInvite = 'notify-album-invite',
NOTIFY_ALBUM_UPDATE = 'notify-album-update', NotifyAlbumUpdate = 'notify-album-update',
NOTIFICATIONS_CLEANUP = 'notifications-cleanup', NotificationsCleanup = 'notifications-cleanup',
SEND_EMAIL = 'notification-send-email', SendMail = 'notification-send-email',
// Version check // Version check
VERSION_CHECK = 'version-check', VersionCheck = 'version-check',
} }
export enum JobCommand { export enum JobCommand {
START = 'start', Start = 'start',
PAUSE = 'pause', Pause = 'pause',
RESUME = 'resume', Resume = 'resume',
EMPTY = 'empty', Empty = 'empty',
CLEAR_FAILED = 'clear-failed', ClearFailed = 'clear-failed',
} }
export enum JobStatus { export enum JobStatus {
SUCCESS = 'success', Success = 'success',
FAILED = 'failed', Failed = 'failed',
SKIPPED = 'skipped', Skipped = 'skipped',
} }
export enum QueueCleanType { export enum QueueCleanType {
FAILED = 'failed', Failed = 'failed',
} }
export enum VectorIndex { export enum VectorIndex {
CLIP = 'clip_index', Clip = 'clip_index',
FACE = 'face_index', Face = 'face_index',
} }
export enum DatabaseLock { export enum DatabaseLock {
@ -663,8 +658,8 @@ export enum NotificationType {
} }
export enum OAuthTokenEndpointAuthMethod { export enum OAuthTokenEndpointAuthMethod {
CLIENT_SECRET_POST = 'client_secret_post', ClientSecretPost = 'client_secret_post',
CLIENT_SECRET_BASIC = 'client_secret_basic', ClientSecretBasic = 'client_secret_basic',
} }
export enum DatabaseSslMode { export enum DatabaseSslMode {
@ -676,14 +671,14 @@ export enum DatabaseSslMode {
} }
export enum AssetVisibility { export enum AssetVisibility {
ARCHIVE = 'archive', Archive = 'archive',
TIMELINE = 'timeline', Timeline = 'timeline',
/** /**
* Video part of the LivePhotos and MotionPhotos * Video part of the LivePhotos and MotionPhotos
*/ */
HIDDEN = 'hidden', Hidden = 'hidden',
LOCKED = 'locked', Locked = 'locked',
} }
export enum CronJob { export enum CronJob {

View File

@ -20,7 +20,7 @@ const onExit = (name: string, exitCode: number | null) => {
if (exitCode !== 0) { if (exitCode !== 0) {
console.error(`${name} worker exited with code ${exitCode}`); console.error(`${name} worker exited with code ${exitCode}`);
if (apiProcess && name !== ImmichWorker.API) { if (apiProcess && name !== ImmichWorker.Api) {
console.error('Killing api process'); console.error('Killing api process');
apiProcess.kill('SIGTERM'); apiProcess.kill('SIGTERM');
apiProcess = undefined; apiProcess = undefined;
@ -34,7 +34,7 @@ function bootstrapWorker(name: ImmichWorker) {
console.log(`Starting ${name} worker`); console.log(`Starting ${name} worker`);
let worker: Worker | ChildProcess; let worker: Worker | ChildProcess;
if (name === ImmichWorker.API) { if (name === ImmichWorker.Api) {
worker = fork(`./dist/workers/${name}.js`, [], { worker = fork(`./dist/workers/${name}.js`, [], {
execArgv: process.execArgv.map((arg) => (arg.startsWith('--inspect') ? '--inspect=0.0.0.0:9231' : arg)), execArgv: process.execArgv.map((arg) => (arg.startsWith('--inspect') ? '--inspect=0.0.0.0:9231' : arg)),
}); });
@ -50,7 +50,7 @@ function bootstrapWorker(name: ImmichWorker) {
function bootstrap() { function bootstrap() {
if (immichApp === 'immich-admin') { if (immichApp === 'immich-admin') {
process.title = 'immich_admin_cli'; process.title = 'immich_admin_cli';
process.env.IMMICH_LOG_LEVEL = LogLevel.WARN; process.env.IMMICH_LOG_LEVEL = LogLevel.Warn;
return CommandFactory.run(ImmichAdminModule); return CommandFactory.run(ImmichAdminModule);
} }

View File

@ -15,7 +15,7 @@ export class AssetUploadInterceptor implements NestInterceptor {
const req = context.switchToHttp().getRequest<AuthenticatedRequest>(); const req = context.switchToHttp().getRequest<AuthenticatedRequest>();
const res = context.switchToHttp().getResponse<Response<AssetMediaResponseDto>>(); const res = context.switchToHttp().getResponse<Response<AssetMediaResponseDto>>();
const checksum = fromMaybeArray(req.headers[ImmichHeader.CHECKSUM]); const checksum = fromMaybeArray(req.headers[ImmichHeader.Checksum]);
const response = await this.service.getUploadAssetIdByChecksum(req.user, checksum); const response = await this.service.getUploadAssetIdByChecksum(req.user, checksum);
if (response) { if (response) {
res.status(200); res.status(200);

View File

@ -23,12 +23,12 @@ export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator =
const decorators: MethodDecorator[] = [ const decorators: MethodDecorator[] = [
ApiBearerAuth(), ApiBearerAuth(),
ApiCookieAuth(), ApiCookieAuth(),
ApiSecurity(MetadataKey.API_KEY_SECURITY), ApiSecurity(MetadataKey.ApiKeySecurity),
SetMetadata(MetadataKey.AUTH_ROUTE, options || {}), SetMetadata(MetadataKey.AuthRoute, options || {}),
]; ];
if ((options as SharedLinkRoute)?.sharedLink) { if ((options as SharedLinkRoute)?.sharedLink) {
decorators.push(ApiQuery({ name: ImmichQuery.SHARED_LINK_KEY, type: String, required: false })); decorators.push(ApiQuery({ name: ImmichQuery.SharedLinkKey, type: String, required: false }));
} }
return applyDecorators(...decorators); return applyDecorators(...decorators);
@ -76,7 +76,7 @@ export class AuthGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> { async canActivate(context: ExecutionContext): Promise<boolean> {
const targets = [context.getHandler()]; const targets = [context.getHandler()];
const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AUTH_ROUTE, targets); const options = this.reflector.getAllAndOverride<AuthenticatedOptions | undefined>(MetadataKey.AuthRoute, targets);
if (!options) { if (!options) {
return true; return true;
} }

View File

@ -154,11 +154,11 @@ export class FileUploadInterceptor implements NestInterceptor {
private getHandler(route: RouteKey) { private getHandler(route: RouteKey) {
switch (route) { switch (route) {
case RouteKey.ASSET: { case RouteKey.Asset: {
return this.handlers.assetUpload; return this.handlers.assetUpload;
} }
case RouteKey.USER: { case RouteKey.User: {
return this.handlers.userProfile; return this.handlers.userProfile;
} }

View File

@ -6,7 +6,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddFaceSearchRelation1718486162779 implements MigrationInterface { export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> { public async up(queryRunner: QueryRunner): Promise<void> {
const vectorExtension = await getVectorExtension(queryRunner); const vectorExtension = await getVectorExtension(queryRunner);
if (vectorExtension === DatabaseExtension.VECTORS) { if (vectorExtension === DatabaseExtension.Vectors) {
await queryRunner.query(`SET search_path TO "$user", public, vectors`); await queryRunner.query(`SET search_path TO "$user", public, vectors`);
} }
@ -52,7 +52,7 @@ export class AddFaceSearchRelation1718486162779 implements MigrationInterface {
public async down(queryRunner: QueryRunner): Promise<void> { public async down(queryRunner: QueryRunner): Promise<void> {
const vectorExtension = await getVectorExtension(queryRunner); const vectorExtension = await getVectorExtension(queryRunner);
if (vectorExtension === DatabaseExtension.VECTORS) { if (vectorExtension === DatabaseExtension.Vectors) {
await queryRunner.query(`SET search_path TO "$user", public, vectors`); await queryRunner.query(`SET search_path TO "$user", public, vectors`);
} }

View File

@ -91,7 +91,7 @@ class AlbumAccess {
} }
const accessRole = const accessRole =
access === AlbumUserRole.EDITOR ? [AlbumUserRole.EDITOR] : [AlbumUserRole.EDITOR, AlbumUserRole.VIEWER]; access === AlbumUserRole.Editor ? [AlbumUserRole.Editor] : [AlbumUserRole.Editor, AlbumUserRole.Viewer];
return this.db return this.db
.selectFrom('album') .selectFrom('album')
@ -178,7 +178,7 @@ class AssetAccess {
.select('asset.id') .select('asset.id')
.where('asset.id', 'in', [...assetIds]) .where('asset.id', 'in', [...assetIds])
.where('asset.ownerId', '=', userId) .where('asset.ownerId', '=', userId)
.$if(!hasElevatedPermission, (eb) => eb.where('asset.visibility', '!=', AssetVisibility.LOCKED)) .$if(!hasElevatedPermission, (eb) => eb.where('asset.visibility', '!=', AssetVisibility.Locked))
.execute() .execute()
.then((assets) => new Set(assets.map((asset) => asset.id))); .then((assets) => new Set(assets.map((asset) => asset.id)));
} }
@ -200,8 +200,8 @@ class AssetAccess {
.where('partner.sharedWithId', '=', userId) .where('partner.sharedWithId', '=', userId)
.where((eb) => .where((eb) =>
eb.or([ eb.or([
eb('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)), eb('asset.visibility', '=', sql.lit(AssetVisibility.Timeline)),
eb('asset.visibility', '=', sql.lit(AssetVisibility.HIDDEN)), eb('asset.visibility', '=', sql.lit(AssetVisibility.Hidden)),
]), ]),
) )

View File

@ -90,7 +90,7 @@ export class ActivityRepository {
.where('activity.albumId', '=', albumId) .where('activity.albumId', '=', albumId)
.where(({ or, and, eb }) => .where(({ or, and, eb }) =>
or([ or([
and([eb('asset.deletedAt', 'is', null), eb('asset.visibility', '!=', sql.lit(AssetVisibility.LOCKED))]), and([eb('asset.deletedAt', 'is', null), eb('asset.visibility', '!=', sql.lit(AssetVisibility.Locked))]),
eb('asset.id', 'is', null), eb('asset.id', 'is', null),
]), ]),
) )

View File

@ -24,7 +24,7 @@ export class AlbumUserRepository {
.executeTakeFirstOrThrow(); .executeTakeFirstOrThrow();
} }
@GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.VIEWER }] }) @GenerateSql({ params: [{ usersId: DummyValue.UUID, albumsId: DummyValue.UUID }, { role: AlbumUserRole.Viewer }] })
update({ usersId, albumsId }: AlbumPermissionId, dto: Updateable<AlbumUserTable>) { update({ usersId, albumsId }: AlbumPermissionId, dto: Updateable<AlbumUserTable>) {
return this.db return this.db
.updateTable('album_user') .updateTable('album_user')

View File

@ -62,7 +62,7 @@ export class AssetJobRepository {
.select(['asset.id', 'asset.thumbhash']) .select(['asset.id', 'asset.thumbhash'])
.select(withFiles) .select(withFiles)
.where('asset.deletedAt', 'is', null) .where('asset.deletedAt', 'is', null)
.where('asset.visibility', '!=', AssetVisibility.HIDDEN) .where('asset.visibility', '!=', AssetVisibility.Hidden)
.$if(!force, (qb) => .$if(!force, (qb) =>
qb qb
// If there aren't any entries, metadata extraction hasn't run yet which is required for thumbnails // If there aren't any entries, metadata extraction hasn't run yet which is required for thumbnails
@ -117,7 +117,7 @@ export class AssetJobRepository {
.executeTakeFirst(); .executeTakeFirst();
} }
@GenerateSql({ params: [DummyValue.UUID, AssetFileType.THUMBNAIL] }) @GenerateSql({ params: [DummyValue.UUID, AssetFileType.Thumbnail] })
getAlbumThumbnailFiles(id: string, fileType?: AssetFileType) { getAlbumThumbnailFiles(id: string, fileType?: AssetFileType) {
return this.db return this.db
.selectFrom('asset_file') .selectFrom('asset_file')
@ -130,7 +130,7 @@ export class AssetJobRepository {
private assetsWithPreviews() { private assetsWithPreviews() {
return this.db return this.db
.selectFrom('asset') .selectFrom('asset')
.where('asset.visibility', '!=', AssetVisibility.HIDDEN) .where('asset.visibility', '!=', AssetVisibility.Hidden)
.where('asset.deletedAt', 'is', null) .where('asset.deletedAt', 'is', null)
.innerJoin('asset_job_status as job_status', 'assetId', 'asset.id') .innerJoin('asset_job_status as job_status', 'assetId', 'asset.id')
.where('job_status.previewAt', 'is not', null); .where('job_status.previewAt', 'is not', null);
@ -167,7 +167,7 @@ export class AssetJobRepository {
return this.db return this.db
.selectFrom('asset') .selectFrom('asset')
.select(['asset.id', 'asset.visibility']) .select(['asset.id', 'asset.visibility'])
.select((eb) => withFiles(eb, AssetFileType.PREVIEW)) .select((eb) => withFiles(eb, AssetFileType.Preview))
.where('asset.id', '=', id) .where('asset.id', '=', id)
.executeTakeFirst(); .executeTakeFirst();
} }
@ -179,7 +179,7 @@ export class AssetJobRepository {
.select(['asset.id', 'asset.visibility']) .select(['asset.id', 'asset.visibility'])
.$call(withExifInner) .$call(withExifInner)
.select((eb) => withFaces(eb, true)) .select((eb) => withFaces(eb, true))
.select((eb) => withFiles(eb, AssetFileType.PREVIEW)) .select((eb) => withFiles(eb, AssetFileType.Preview))
.where('asset.id', '=', id) .where('asset.id', '=', id)
.executeTakeFirst(); .executeTakeFirst();
} }
@ -225,7 +225,7 @@ export class AssetJobRepository {
.select(['stack.id', 'stack.primaryAssetId']) .select(['stack.id', 'stack.primaryAssetId'])
.select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets')) .select((eb) => eb.fn<Asset[]>('array_agg', [eb.table('stacked')]).as('assets'))
.where('stacked.deletedAt', 'is not', null) .where('stacked.deletedAt', 'is not', null)
.where('stacked.visibility', '=', AssetVisibility.TIMELINE) .where('stacked.visibility', '=', AssetVisibility.Timeline)
.whereRef('stacked.stackId', '=', 'stack.id') .whereRef('stacked.stackId', '=', 'stack.id')
.groupBy('stack.id') .groupBy('stack.id')
.as('stacked_assets'), .as('stacked_assets'),
@ -241,11 +241,11 @@ export class AssetJobRepository {
return this.db return this.db
.selectFrom('asset') .selectFrom('asset')
.select(['asset.id']) .select(['asset.id'])
.where('asset.type', '=', AssetType.VIDEO) .where('asset.type', '=', AssetType.Video)
.$if(!force, (qb) => .$if(!force, (qb) =>
qb qb
.where((eb) => eb.or([eb('asset.encodedVideoPath', 'is', null), eb('asset.encodedVideoPath', '=', '')])) .where((eb) => eb.or([eb('asset.encodedVideoPath', 'is', null), eb('asset.encodedVideoPath', '=', '')]))
.where('asset.visibility', '!=', AssetVisibility.HIDDEN), .where('asset.visibility', '!=', AssetVisibility.Hidden),
) )
.where('asset.deletedAt', 'is', null) .where('asset.deletedAt', 'is', null)
.stream(); .stream();
@ -257,7 +257,7 @@ export class AssetJobRepository {
.selectFrom('asset') .selectFrom('asset')
.select(['asset.id', 'asset.ownerId', 'asset.originalPath', 'asset.encodedVideoPath']) .select(['asset.id', 'asset.ownerId', 'asset.originalPath', 'asset.encodedVideoPath'])
.where('asset.id', '=', id) .where('asset.id', '=', id)
.where('asset.type', '=', AssetType.VIDEO) .where('asset.type', '=', AssetType.Video)
.executeTakeFirst(); .executeTakeFirst();
} }
@ -327,7 +327,7 @@ export class AssetJobRepository {
.$if(!force, (qb) => .$if(!force, (qb) =>
qb.where((eb) => eb.or([eb('asset.sidecarPath', '=', ''), eb('asset.sidecarPath', 'is', null)])), qb.where((eb) => eb.or([eb('asset.sidecarPath', '=', ''), eb('asset.sidecarPath', 'is', null)])),
) )
.where('asset.visibility', '!=', AssetVisibility.HIDDEN) .where('asset.visibility', '!=', AssetVisibility.Hidden)
.stream(); .stream();
} }

View File

@ -230,13 +230,13 @@ export class AssetRepository {
.where('asset_job_status.previewAt', 'is not', null) .where('asset_job_status.previewAt', 'is not', null)
.where(sql`(asset."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`) .where(sql`(asset."localDateTime" at time zone 'UTC')::date`, '=', sql`today.date`)
.where('asset.ownerId', '=', anyUuid(ownerIds)) .where('asset.ownerId', '=', anyUuid(ownerIds))
.where('asset.visibility', '=', AssetVisibility.TIMELINE) .where('asset.visibility', '=', AssetVisibility.Timeline)
.where((eb) => .where((eb) =>
eb.exists((qb) => eb.exists((qb) =>
qb qb
.selectFrom('asset_file') .selectFrom('asset_file')
.whereRef('assetId', '=', 'asset.id') .whereRef('assetId', '=', 'asset.id')
.where('asset_file.type', '=', AssetFileType.PREVIEW), .where('asset_file.type', '=', AssetFileType.Preview),
), ),
) )
.where('asset.deletedAt', 'is', null) .where('asset.deletedAt', 'is', null)
@ -318,7 +318,7 @@ export class AssetRepository {
.select(['deviceAssetId']) .select(['deviceAssetId'])
.where('ownerId', '=', asUuid(ownerId)) .where('ownerId', '=', asUuid(ownerId))
.where('deviceId', '=', deviceId) .where('deviceId', '=', deviceId)
.where('visibility', '!=', AssetVisibility.HIDDEN) .where('visibility', '!=', AssetVisibility.Hidden)
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.execute(); .execute();
@ -363,7 +363,7 @@ export class AssetRepository {
.whereRef('stacked.stackId', '=', 'stack.id') .whereRef('stacked.stackId', '=', 'stack.id')
.whereRef('stacked.id', '!=', 'stack.primaryAssetId') .whereRef('stacked.id', '!=', 'stack.primaryAssetId')
.where('stacked.deletedAt', 'is', null) .where('stacked.deletedAt', 'is', null)
.where('stacked.visibility', '=', AssetVisibility.TIMELINE) .where('stacked.visibility', '=', AssetVisibility.Timeline)
.groupBy('stack.id') .groupBy('stack.id')
.as('stacked_assets'), .as('stacked_assets'),
(join) => join.on('stack.id', 'is not', null), (join) => join.on('stack.id', 'is not', null),
@ -463,15 +463,15 @@ export class AssetRepository {
getStatistics(ownerId: string, { visibility, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> { getStatistics(ownerId: string, { visibility, isFavorite, isTrashed }: AssetStatsOptions): Promise<AssetStats> {
return this.db return this.db
.selectFrom('asset') .selectFrom('asset')
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.AUDIO).as(AssetType.AUDIO)) .select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.Audio).as(AssetType.Audio))
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.IMAGE).as(AssetType.IMAGE)) .select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.Image).as(AssetType.Image))
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO)) .select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.Video).as(AssetType.Video))
.select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER)) .select((eb) => eb.fn.countAll<number>().filterWhere('type', '=', AssetType.Other).as(AssetType.Other))
.where('ownerId', '=', asUuid(ownerId)) .where('ownerId', '=', asUuid(ownerId))
.$if(visibility === undefined, withDefaultVisibility) .$if(visibility === undefined, withDefaultVisibility)
.$if(!!visibility, (qb) => qb.where('asset.visibility', '=', visibility!)) .$if(!!visibility, (qb) => qb.where('asset.visibility', '=', visibility!))
.$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!)) .$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!))
.$if(!!isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.DELETED)) .$if(!!isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted))
.where('deletedAt', isTrashed ? 'is not' : 'is', null) .where('deletedAt', isTrashed ? 'is not' : 'is', null)
.executeTakeFirstOrThrow(); .executeTakeFirstOrThrow();
} }
@ -496,7 +496,7 @@ export class AssetRepository {
qb qb
.selectFrom('asset') .selectFrom('asset')
.select(truncatedDate<Date>().as('timeBucket')) .select(truncatedDate<Date>().as('timeBucket'))
.$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.DELETED)) .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted))
.where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null) .where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null)
.$if(options.visibility === undefined, withDefaultVisibility) .$if(options.visibility === undefined, withDefaultVisibility)
.$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!)) .$if(!!options.visibility, (qb) => qb.where('asset.visibility', '=', options.visibility!))
@ -606,7 +606,7 @@ export class AssetRepository {
.select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack')) .select(sql`array[stacked."stackId"::text, count('stacked')::text]`.as('stack'))
.whereRef('stacked.stackId', '=', 'asset.stackId') .whereRef('stacked.stackId', '=', 'asset.stackId')
.where('stacked.deletedAt', 'is', null) .where('stacked.deletedAt', 'is', null)
.where('stacked.visibility', '=', AssetVisibility.TIMELINE) .where('stacked.visibility', '=', AssetVisibility.Timeline)
.groupBy('stacked.stackId') .groupBy('stacked.stackId')
.as('stacked_assets'), .as('stacked_assets'),
(join) => join.onTrue(), (join) => join.onTrue(),
@ -617,7 +617,7 @@ export class AssetRepository {
.$if(options.isDuplicate !== undefined, (qb) => .$if(options.isDuplicate !== undefined, (qb) =>
qb.where('asset.duplicateId', options.isDuplicate ? 'is not' : 'is', null), qb.where('asset.duplicateId', options.isDuplicate ? 'is not' : 'is', null),
) )
.$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.DELETED)) .$if(!!options.isTrashed, (qb) => qb.where('asset.status', '!=', AssetStatus.Deleted))
.$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!)) .$if(!!options.tagId, (qb) => withTagId(qb, options.tagId!))
.orderBy('asset.fileCreatedAt', options.order ?? 'desc'), .orderBy('asset.fileCreatedAt', options.order ?? 'desc'),
) )
@ -671,8 +671,8 @@ export class AssetRepository {
.select(['assetId as data', 'asset_exif.city as value']) .select(['assetId as data', 'asset_exif.city as value'])
.$narrowType<{ value: NotNull }>() .$narrowType<{ value: NotNull }>()
.where('ownerId', '=', asUuid(ownerId)) .where('ownerId', '=', asUuid(ownerId))
.where('visibility', '=', AssetVisibility.TIMELINE) .where('visibility', '=', AssetVisibility.Timeline)
.where('type', '=', AssetType.IMAGE) .where('type', '=', AssetType.Image)
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.limit(maxFields) .limit(maxFields)
.execute(); .execute();
@ -710,7 +710,7 @@ export class AssetRepository {
) )
.select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack')) .select((eb) => eb.fn.toJson(eb.table('stacked_assets')).$castTo<Stack | null>().as('stack'))
.where('asset.ownerId', '=', asUuid(ownerId)) .where('asset.ownerId', '=', asUuid(ownerId))
.where('asset.visibility', '!=', AssetVisibility.HIDDEN) .where('asset.visibility', '!=', AssetVisibility.Hidden)
.where('asset.updatedAt', '<=', updatedUntil) .where('asset.updatedAt', '<=', updatedUntil)
.$if(!!lastId, (qb) => qb.where('asset.id', '>', lastId!)) .$if(!!lastId, (qb) => qb.where('asset.id', '>', lastId!))
.orderBy('asset.id') .orderBy('asset.id')
@ -738,7 +738,7 @@ export class AssetRepository {
) )
.select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo<Stack | null>()).as('stack')) .select((eb) => eb.fn.toJson(eb.table('stacked_assets').$castTo<Stack | null>()).as('stack'))
.where('asset.ownerId', '=', anyUuid(options.userIds)) .where('asset.ownerId', '=', anyUuid(options.userIds))
.where('asset.visibility', '!=', AssetVisibility.HIDDEN) .where('asset.visibility', '!=', AssetVisibility.Hidden)
.where('asset.updatedAt', '>', options.updatedAfter) .where('asset.updatedAt', '>', options.updatedAfter)
.limit(options.limit) .limit(options.limit)
.execute(); .execute();

View File

@ -18,7 +18,7 @@ export class AuditRepository {
@GenerateSql({ @GenerateSql({
params: [ params: [
DummyValue.DATE, DummyValue.DATE,
{ action: DatabaseAction.CREATE, entityType: EntityType.ASSET, userIds: [DummyValue.UUID] }, { action: DatabaseAction.Create, entityType: EntityType.Asset, userIds: [DummyValue.UUID] },
], ],
}) })
async getAfter(since: Date, options: AuditSearch): Promise<string[]> { async getAfter(since: Date, options: AuditSearch): Promise<string[]> {

View File

@ -275,14 +275,14 @@ describe('getEnv', () => {
process.env.IMMICH_TELEMETRY_EXCLUDE = 'job'; process.env.IMMICH_TELEMETRY_EXCLUDE = 'job';
const { telemetry } = getEnv(); const { telemetry } = getEnv();
expect(telemetry.metrics).toEqual( expect(telemetry.metrics).toEqual(
new Set([ImmichTelemetry.API, ImmichTelemetry.HOST, ImmichTelemetry.IO, ImmichTelemetry.REPO]), new Set([ImmichTelemetry.Api, ImmichTelemetry.Host, ImmichTelemetry.Io, ImmichTelemetry.Repo]),
); );
}); });
it('should run with specific telemetry metrics', () => { it('should run with specific telemetry metrics', () => {
process.env.IMMICH_TELEMETRY_INCLUDE = 'io, host, api'; process.env.IMMICH_TELEMETRY_INCLUDE = 'io, host, api';
const { telemetry } = getEnv(); const { telemetry } = getEnv();
expect(telemetry.metrics).toEqual(new Set([ImmichTelemetry.API, ImmichTelemetry.HOST, ImmichTelemetry.IO])); expect(telemetry.metrics).toEqual(new Set([ImmichTelemetry.Api, ImmichTelemetry.Host, ImmichTelemetry.Io]));
}); });
}); });
}); });

View File

@ -136,7 +136,7 @@ const getEnv = (): EnvData => {
); );
} }
const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.API, ImmichWorker.MICROSERVICES]); const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.Api, ImmichWorker.Microservices]);
const excludedWorkers = asSet(dto.IMMICH_WORKERS_EXCLUDE, []); const excludedWorkers = asSet(dto.IMMICH_WORKERS_EXCLUDE, []);
const workers = [...setDifference(includedWorkers, excludedWorkers)]; const workers = [...setDifference(includedWorkers, excludedWorkers)];
for (const worker of workers) { for (const worker of workers) {
@ -145,8 +145,8 @@ const getEnv = (): EnvData => {
} }
} }
const environment = dto.IMMICH_ENV || ImmichEnvironment.PRODUCTION; const environment = dto.IMMICH_ENV || ImmichEnvironment.Production;
const isProd = environment === ImmichEnvironment.PRODUCTION; const isProd = environment === ImmichEnvironment.Production;
const buildFolder = dto.IMMICH_BUILD_DATA || '/build'; const buildFolder = dto.IMMICH_BUILD_DATA || '/build';
const folders = { const folders = {
geodata: join(buildFolder, 'geodata'), geodata: join(buildFolder, 'geodata'),
@ -199,15 +199,15 @@ const getEnv = (): EnvData => {
let vectorExtension: VectorExtension | undefined; let vectorExtension: VectorExtension | undefined;
switch (dto.DB_VECTOR_EXTENSION) { switch (dto.DB_VECTOR_EXTENSION) {
case 'pgvector': { case 'pgvector': {
vectorExtension = DatabaseExtension.VECTOR; vectorExtension = DatabaseExtension.Vector;
break; break;
} }
case 'pgvecto.rs': { case 'pgvecto.rs': {
vectorExtension = DatabaseExtension.VECTORS; vectorExtension = DatabaseExtension.Vectors;
break; break;
} }
case 'vectorchord': { case 'vectorchord': {
vectorExtension = DatabaseExtension.VECTORCHORD; vectorExtension = DatabaseExtension.VectorChord;
break; break;
} }
} }
@ -254,11 +254,11 @@ const getEnv = (): EnvData => {
mount: true, mount: true,
generateId: true, generateId: true,
setup: (cls, req: Request, res: Response) => { setup: (cls, req: Request, res: Response) => {
const headerValues = req.headers[ImmichHeader.CID]; const headerValues = req.headers[ImmichHeader.Cid];
const headerValue = Array.isArray(headerValues) ? headerValues[0] : headerValues; const headerValue = Array.isArray(headerValues) ? headerValues[0] : headerValues;
const cid = headerValue || cls.get(CLS_ID); const cid = headerValue || cls.get(CLS_ID);
cls.set(CLS_ID, cid); cls.set(CLS_ID, cid);
res.header(ImmichHeader.CID, cid); res.header(ImmichHeader.Cid, cid);
}, },
}, },
}, },
@ -278,9 +278,9 @@ const getEnv = (): EnvData => {
otel: { otel: {
metrics: { metrics: {
hostMetrics: telemetries.has(ImmichTelemetry.HOST), hostMetrics: telemetries.has(ImmichTelemetry.Host),
apiMetrics: { apiMetrics: {
enable: telemetries.has(ImmichTelemetry.API), enable: telemetries.has(ImmichTelemetry.Api),
ignoreRoutes: excludePaths, ignoreRoutes: excludePaths,
}, },
}, },
@ -335,7 +335,7 @@ export class ConfigRepository {
} }
isDev() { isDev() {
return this.getEnv().environment === ImmichEnvironment.DEVELOPMENT; return this.getEnv().environment === ImmichEnvironment.Development;
} }
getWorker() { getWorker() {

View File

@ -53,8 +53,8 @@ export async function getVectorExtension(runner: Kysely<DB> | QueryRunner): Prom
} }
export const probes: Record<VectorIndex, number> = { export const probes: Record<VectorIndex, number> = {
[VectorIndex.CLIP]: 1, [VectorIndex.Clip]: 1,
[VectorIndex.FACE]: 1, [VectorIndex.Face]: 1,
}; };
@Injectable() @Injectable()
@ -77,7 +77,7 @@ export class DatabaseRepository {
return getVectorExtension(this.db); return getVectorExtension(this.db);
} }
@GenerateSql({ params: [[DatabaseExtension.VECTORS]] }) @GenerateSql({ params: [[DatabaseExtension.Vectors]] })
async getExtensionVersions(extensions: readonly DatabaseExtension[]): Promise<ExtensionVersion[]> { async getExtensionVersions(extensions: readonly DatabaseExtension[]): Promise<ExtensionVersion[]> {
const { rows } = await sql<ExtensionVersion>` const { rows } = await sql<ExtensionVersion>`
SELECT name, default_version as "availableVersion", installed_version as "installedVersion" SELECT name, default_version as "availableVersion", installed_version as "installedVersion"
@ -89,13 +89,13 @@ export class DatabaseRepository {
getExtensionVersionRange(extension: VectorExtension): string { getExtensionVersionRange(extension: VectorExtension): string {
switch (extension) { switch (extension) {
case DatabaseExtension.VECTORCHORD: { case DatabaseExtension.VectorChord: {
return VECTORCHORD_VERSION_RANGE; return VECTORCHORD_VERSION_RANGE;
} }
case DatabaseExtension.VECTORS: { case DatabaseExtension.Vectors: {
return VECTORS_VERSION_RANGE; return VECTORS_VERSION_RANGE;
} }
case DatabaseExtension.VECTOR: { case DatabaseExtension.Vector: {
return VECTOR_VERSION_RANGE; return VECTOR_VERSION_RANGE;
} }
default: { default: {
@ -117,7 +117,7 @@ export class DatabaseRepository {
async createExtension(extension: DatabaseExtension): Promise<void> { async createExtension(extension: DatabaseExtension): Promise<void> {
this.logger.log(`Creating ${EXTENSION_NAMES[extension]} extension`); this.logger.log(`Creating ${EXTENSION_NAMES[extension]} extension`);
await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(extension)} CASCADE`.execute(this.db); await sql`CREATE EXTENSION IF NOT EXISTS ${sql.raw(extension)} CASCADE`.execute(this.db);
if (extension === DatabaseExtension.VECTORCHORD) { if (extension === DatabaseExtension.VectorChord) {
const dbName = sql.id(await this.getDatabaseName()); const dbName = sql.id(await this.getDatabaseName());
await sql`ALTER DATABASE ${dbName} SET vchordrq.probes = 1`.execute(this.db); await sql`ALTER DATABASE ${dbName} SET vchordrq.probes = 1`.execute(this.db);
await sql`SET vchordrq.probes = 1`.execute(this.db); await sql`SET vchordrq.probes = 1`.execute(this.db);
@ -147,8 +147,8 @@ export class DatabaseRepository {
} }
await Promise.all([ await Promise.all([
this.db.schema.dropIndex(VectorIndex.CLIP).ifExists().execute(), this.db.schema.dropIndex(VectorIndex.Clip).ifExists().execute(),
this.db.schema.dropIndex(VectorIndex.FACE).ifExists().execute(), this.db.schema.dropIndex(VectorIndex.Face).ifExists().execute(),
]); ]);
await this.db.transaction().execute(async (tx) => { await this.db.transaction().execute(async (tx) => {
@ -156,14 +156,14 @@ export class DatabaseRepository {
await sql`ALTER EXTENSION ${sql.raw(extension)} UPDATE TO ${sql.lit(targetVersion)}`.execute(tx); await sql`ALTER EXTENSION ${sql.raw(extension)} UPDATE TO ${sql.lit(targetVersion)}`.execute(tx);
if (extension === DatabaseExtension.VECTORS && (diff === 'major' || diff === 'minor')) { if (extension === DatabaseExtension.Vectors && (diff === 'major' || diff === 'minor')) {
await sql`SELECT pgvectors_upgrade()`.execute(tx); await sql`SELECT pgvectors_upgrade()`.execute(tx);
restartRequired = true; restartRequired = true;
} }
}); });
if (!restartRequired) { if (!restartRequired) {
await Promise.all([this.reindexVectors(VectorIndex.CLIP), this.reindexVectors(VectorIndex.FACE)]); await Promise.all([this.reindexVectors(VectorIndex.Clip), this.reindexVectors(VectorIndex.Face)]);
} }
return { restartRequired }; return { restartRequired };
@ -171,7 +171,7 @@ export class DatabaseRepository {
async prewarm(index: VectorIndex): Promise<void> { async prewarm(index: VectorIndex): Promise<void> {
const vectorExtension = await getVectorExtension(this.db); const vectorExtension = await getVectorExtension(this.db);
if (vectorExtension !== DatabaseExtension.VECTORCHORD) { if (vectorExtension !== DatabaseExtension.VectorChord) {
return; return;
} }
this.logger.debug(`Prewarming ${index}`); this.logger.debug(`Prewarming ${index}`);
@ -196,19 +196,19 @@ export class DatabaseRepository {
} }
switch (vectorExtension) { switch (vectorExtension) {
case DatabaseExtension.VECTOR: { case DatabaseExtension.Vector: {
if (!row.indexdef.toLowerCase().includes('using hnsw')) { if (!row.indexdef.toLowerCase().includes('using hnsw')) {
promises.push(this.reindexVectors(indexName)); promises.push(this.reindexVectors(indexName));
} }
break; break;
} }
case DatabaseExtension.VECTORS: { case DatabaseExtension.Vectors: {
if (!row.indexdef.toLowerCase().includes('using vectors')) { if (!row.indexdef.toLowerCase().includes('using vectors')) {
promises.push(this.reindexVectors(indexName)); promises.push(this.reindexVectors(indexName));
} }
break; break;
} }
case DatabaseExtension.VECTORCHORD: { case DatabaseExtension.VectorChord: {
const matches = row.indexdef.match(/(?<=lists = \[)\d+/g); const matches = row.indexdef.match(/(?<=lists = \[)\d+/g);
const lists = matches && matches.length > 0 ? Number(matches[0]) : 1; const lists = matches && matches.length > 0 ? Number(matches[0]) : 1;
promises.push( promises.push(
@ -264,7 +264,7 @@ export class DatabaseRepository {
await sql`ALTER TABLE ${sql.raw(table)} ADD COLUMN embedding real[] NOT NULL`.execute(tx); await sql`ALTER TABLE ${sql.raw(table)} ADD COLUMN embedding real[] NOT NULL`.execute(tx);
} }
await sql`ALTER TABLE ${sql.raw(table)} ALTER COLUMN embedding SET DATA TYPE real[]`.execute(tx); await sql`ALTER TABLE ${sql.raw(table)} ALTER COLUMN embedding SET DATA TYPE real[]`.execute(tx);
const schema = vectorExtension === DatabaseExtension.VECTORS ? 'vectors.' : ''; const schema = vectorExtension === DatabaseExtension.Vectors ? 'vectors.' : '';
await sql` await sql`
ALTER TABLE ${sql.raw(table)} ALTER TABLE ${sql.raw(table)}
ALTER COLUMN embedding ALTER COLUMN embedding
@ -329,11 +329,11 @@ export class DatabaseRepository {
.alterColumn('embedding', (col) => col.setDataType(sql.raw(`vector(${dimSize})`))) .alterColumn('embedding', (col) => col.setDataType(sql.raw(`vector(${dimSize})`)))
.execute(); .execute();
await sql await sql
.raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: VectorIndex.CLIP })) .raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: VectorIndex.Clip }))
.execute(trx); .execute(trx);
await trx.schema.alterTable('smart_search').dropConstraint('dim_size_constraint').ifExists().execute(); await trx.schema.alterTable('smart_search').dropConstraint('dim_size_constraint').ifExists().execute();
}); });
probes[VectorIndex.CLIP] = 1; probes[VectorIndex.Clip] = 1;
await sql`vacuum analyze ${sql.table('smart_search')}`.execute(this.db); await sql`vacuum analyze ${sql.table('smart_search')}`.execute(this.db);
} }

View File

@ -34,7 +34,7 @@ export class DownloadRepository {
downloadUserId(userId: string) { downloadUserId(userId: string) {
return builder(this.db) return builder(this.db)
.where('asset.ownerId', '=', userId) .where('asset.ownerId', '=', userId)
.where('asset.visibility', '!=', AssetVisibility.HIDDEN) .where('asset.visibility', '!=', AssetVisibility.Hidden)
.stream(); .stream();
} }
} }

View File

@ -109,14 +109,14 @@ export class DuplicateRepository {
assetId: DummyValue.UUID, assetId: DummyValue.UUID,
embedding: DummyValue.VECTOR, embedding: DummyValue.VECTOR,
maxDistance: 0.6, maxDistance: 0.6,
type: AssetType.IMAGE, type: AssetType.Image,
userIds: [DummyValue.UUID], userIds: [DummyValue.UUID],
}, },
], ],
}) })
search({ assetId, embedding, maxDistance, type, userIds }: DuplicateSearch) { search({ assetId, embedding, maxDistance, type, userIds }: DuplicateSearch) {
return this.db.transaction().execute(async (trx) => { return this.db.transaction().execute(async (trx) => {
await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx); await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.Clip])}`.execute(trx);
return await trx return await trx
.with('cte', (qb) => .with('cte', (qb) =>
qb qb

View File

@ -166,7 +166,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
continue; continue;
} }
const event = reflector.get<EventConfig>(MetadataKey.EVENT_CONFIG, handler); const event = reflector.get<EventConfig>(MetadataKey.EventConfig, handler);
if (!event) { if (!event) {
continue; continue;
} }

View File

@ -41,7 +41,7 @@ export class JobRepository {
const instance = this.moduleRef.get<any>(Service); const instance = this.moduleRef.get<any>(Service);
for (const methodName of getMethodNames(instance)) { for (const methodName of getMethodNames(instance)) {
const handler = instance[methodName]; const handler = instance[methodName];
const config = reflector.get<JobConfig>(MetadataKey.JOB_CONFIG, handler); const config = reflector.get<JobConfig>(MetadataKey.JobConfig, handler);
if (!config) { if (!config) {
continue; continue;
} }
@ -99,7 +99,7 @@ export class JobRepository {
const item = this.handlers[name as JobName]; const item = this.handlers[name as JobName];
if (!item) { if (!item) {
this.logger.warn(`Skipping unknown job: "${name}"`); this.logger.warn(`Skipping unknown job: "${name}"`);
return JobStatus.SKIPPED; return JobStatus.Skipped;
} }
return item.handler(data); return item.handler(data);
@ -205,20 +205,20 @@ export class JobRepository {
private getJobOptions(item: JobItem): JobsOptions | null { private getJobOptions(item: JobItem): JobsOptions | null {
switch (item.name) { switch (item.name) {
case JobName.NOTIFY_ALBUM_UPDATE: { case JobName.NotifyAlbumUpdate: {
return { return {
jobId: `${item.data.id}/${item.data.recipientId}`, jobId: `${item.data.id}/${item.data.recipientId}`,
delay: item.data?.delay, delay: item.data?.delay,
}; };
} }
case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: { case JobName.StorageTemplateMigrationSingle: {
return { jobId: item.data.id }; return { jobId: item.data.id };
} }
case JobName.GENERATE_PERSON_THUMBNAIL: { case JobName.GeneratePersonThumbnail: {
return { priority: 1 }; return { priority: 1 };
} }
case JobName.QUEUE_FACIAL_RECOGNITION: { case JobName.QueueFacialRecognition: {
return { jobId: JobName.QUEUE_FACIAL_RECOGNITION }; return { jobId: JobName.QueueFacialRecognition };
} }
default: { default: {
return null; return null;

View File

@ -79,7 +79,7 @@ export class LibraryRepository {
eb.fn eb.fn
.countAll<number>() .countAll<number>()
.filterWhere((eb) => .filterWhere((eb) =>
eb.and([eb('asset.type', '=', AssetType.IMAGE), eb('asset.visibility', '!=', AssetVisibility.HIDDEN)]), eb.and([eb('asset.type', '=', AssetType.Image), eb('asset.visibility', '!=', AssetVisibility.Hidden)]),
) )
.as('photos'), .as('photos'),
) )
@ -87,7 +87,7 @@ export class LibraryRepository {
eb.fn eb.fn
.countAll<number>() .countAll<number>()
.filterWhere((eb) => .filterWhere((eb) =>
eb.and([eb('asset.type', '=', AssetType.VIDEO), eb('asset.visibility', '!=', AssetVisibility.HIDDEN)]), eb.and([eb('asset.type', '=', AssetType.Video), eb('asset.visibility', '!=', AssetVisibility.Hidden)]),
) )
.as('videos'), .as('videos'),
) )

View File

@ -22,7 +22,7 @@ describe(LoggingRepository.name, () => {
describe('formatContext', () => { describe('formatContext', () => {
it('should use colors', () => { it('should use colors', () => {
sut = new LoggingRepository(clsMock, configMock); sut = new LoggingRepository(clsMock, configMock);
sut.setAppName(ImmichWorker.API); sut.setAppName(ImmichWorker.Api);
const logger = new MyConsoleLogger(clsMock, { color: true }); const logger = new MyConsoleLogger(clsMock, { color: true });
@ -31,7 +31,7 @@ describe(LoggingRepository.name, () => {
it('should not use colors when color is false', () => { it('should not use colors when color is false', () => {
sut = new LoggingRepository(clsMock, configMock); sut = new LoggingRepository(clsMock, configMock);
sut.setAppName(ImmichWorker.API); sut.setAppName(ImmichWorker.Api);
const logger = new MyConsoleLogger(clsMock, { color: false }); const logger = new MyConsoleLogger(clsMock, { color: false });

View File

@ -8,7 +8,7 @@ import { ConfigRepository } from 'src/repositories/config.repository';
type LogDetails = any; type LogDetails = any;
type LogFunction = () => string; type LogFunction = () => string;
const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; const LOG_LEVELS = [LogLevel.Verbose, LogLevel.Debug, LogLevel.Log, LogLevel.Warn, LogLevel.Error, LogLevel.Fatal];
enum LogColor { enum LogColor {
RED = 31, RED = 31,
@ -20,7 +20,7 @@ enum LogColor {
} }
let appName: string | undefined; let appName: string | undefined;
let logLevels: LogLevel[] = [LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; let logLevels: LogLevel[] = [LogLevel.Log, LogLevel.Warn, LogLevel.Error, LogLevel.Fatal];
export class MyConsoleLogger extends ConsoleLogger { export class MyConsoleLogger extends ConsoleLogger {
private isColorEnabled: boolean; private isColorEnabled: boolean;
@ -106,35 +106,35 @@ export class LoggingRepository {
} }
verbose(message: string, ...details: LogDetails) { verbose(message: string, ...details: LogDetails) {
this.handleMessage(LogLevel.VERBOSE, message, details); this.handleMessage(LogLevel.Verbose, message, details);
} }
verboseFn(message: LogFunction, ...details: LogDetails) { verboseFn(message: LogFunction, ...details: LogDetails) {
this.handleFunction(LogLevel.VERBOSE, message, details); this.handleFunction(LogLevel.Verbose, message, details);
} }
debug(message: string, ...details: LogDetails) { debug(message: string, ...details: LogDetails) {
this.handleMessage(LogLevel.DEBUG, message, details); this.handleMessage(LogLevel.Debug, message, details);
} }
debugFn(message: LogFunction, ...details: LogDetails) { debugFn(message: LogFunction, ...details: LogDetails) {
this.handleFunction(LogLevel.DEBUG, message, details); this.handleFunction(LogLevel.Debug, message, details);
} }
log(message: string, ...details: LogDetails) { log(message: string, ...details: LogDetails) {
this.handleMessage(LogLevel.LOG, message, details); this.handleMessage(LogLevel.Log, message, details);
} }
warn(message: string, ...details: LogDetails) { warn(message: string, ...details: LogDetails) {
this.handleMessage(LogLevel.WARN, message, details); this.handleMessage(LogLevel.Warn, message, details);
} }
error(message: string | Error, ...details: LogDetails) { error(message: string | Error, ...details: LogDetails) {
this.handleMessage(LogLevel.ERROR, message, details); this.handleMessage(LogLevel.Error, message, details);
} }
fatal(message: string, ...details: LogDetails) { fatal(message: string, ...details: LogDetails) {
this.handleMessage(LogLevel.FATAL, message, details); this.handleMessage(LogLevel.Fatal, message, details);
} }
private handleFunction(level: LogLevel, message: LogFunction, details: LogDetails[]) { private handleFunction(level: LogLevel, message: LogFunction, details: LogDetails[]) {
@ -145,32 +145,32 @@ export class LoggingRepository {
private handleMessage(level: LogLevel, message: string | Error, details: LogDetails[]) { private handleMessage(level: LogLevel, message: string | Error, details: LogDetails[]) {
switch (level) { switch (level) {
case LogLevel.VERBOSE: { case LogLevel.Verbose: {
this.logger.verbose(message, ...details); this.logger.verbose(message, ...details);
break; break;
} }
case LogLevel.DEBUG: { case LogLevel.Debug: {
this.logger.debug(message, ...details); this.logger.debug(message, ...details);
break; break;
} }
case LogLevel.LOG: { case LogLevel.Log: {
this.logger.log(message, ...details); this.logger.log(message, ...details);
break; break;
} }
case LogLevel.WARN: { case LogLevel.Warn: {
this.logger.warn(message, ...details); this.logger.warn(message, ...details);
break; break;
} }
case LogLevel.ERROR: { case LogLevel.Error: {
this.logger.error(message, ...details); this.logger.error(message, ...details);
break; break;
} }
case LogLevel.FATAL: { case LogLevel.Fatal: {
this.logger.fatal(message, ...details); this.logger.fatal(message, ...details);
break; break;
} }

View File

@ -61,14 +61,14 @@ export class MapRepository {
const geodataDate = await readFile(resourcePaths.geodata.dateFile, 'utf8'); const geodataDate = await readFile(resourcePaths.geodata.dateFile, 'utf8');
// TODO move to service init // TODO move to service init
const geocodingMetadata = await this.metadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); const geocodingMetadata = await this.metadataRepository.get(SystemMetadataKey.ReverseGeocodingState);
if (geocodingMetadata?.lastUpdate === geodataDate) { if (geocodingMetadata?.lastUpdate === geodataDate) {
return; return;
} }
await Promise.all([this.importGeodata(), this.importNaturalEarthCountries()]); await Promise.all([this.importGeodata(), this.importNaturalEarthCountries()]);
await this.metadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, { await this.metadataRepository.set(SystemMetadataKey.ReverseGeocodingState, {
lastUpdate: geodataDate, lastUpdate: geodataDate,
lastImportFileName: citiesFile, lastImportFileName: citiesFile,
}); });
@ -102,13 +102,13 @@ export class MapRepository {
.$if(isArchived === true, (qb) => .$if(isArchived === true, (qb) =>
qb.where((eb) => qb.where((eb) =>
eb.or([ eb.or([
eb('asset.visibility', '=', AssetVisibility.TIMELINE), eb('asset.visibility', '=', AssetVisibility.Timeline),
eb('asset.visibility', '=', AssetVisibility.ARCHIVE), eb('asset.visibility', '=', AssetVisibility.Archive),
]), ]),
), ),
) )
.$if(isArchived === false || isArchived === undefined, (qb) => .$if(isArchived === false || isArchived === undefined, (qb) =>
qb.where('asset.visibility', '=', AssetVisibility.TIMELINE), qb.where('asset.visibility', '=', AssetVisibility.Timeline),
) )
.$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!)) .$if(isFavorite !== undefined, (q) => q.where('isFavorite', '=', isFavorite!))
.$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!)) .$if(fileCreatedAfter !== undefined, (q) => q.where('fileCreatedAt', '>=', fileCreatedAfter!))

View File

@ -55,28 +55,28 @@ export class MediaRepository {
async extract(input: string): Promise<ExtractResult | null> { async extract(input: string): Promise<ExtractResult | null> {
try { try {
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input); const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw2', input);
return { buffer, format: RawExtractedFormat.JPEG }; return { buffer, format: RawExtractedFormat.Jpeg };
} catch (error: any) { } catch (error: any) {
this.logger.debug('Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next', error.message); this.logger.debug('Could not extract JpgFromRaw2 buffer from image, trying JPEG from RAW next', error.message);
} }
try { try {
const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input); const buffer = await exiftool.extractBinaryTagToBuffer('JpgFromRaw', input);
return { buffer, format: RawExtractedFormat.JPEG }; return { buffer, format: RawExtractedFormat.Jpeg };
} catch (error: any) { } catch (error: any) {
this.logger.debug('Could not extract JPEG buffer from image, trying PreviewJXL next', error.message); this.logger.debug('Could not extract JPEG buffer from image, trying PreviewJXL next', error.message);
} }
try { try {
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input); const buffer = await exiftool.extractBinaryTagToBuffer('PreviewJXL', input);
return { buffer, format: RawExtractedFormat.JXL }; return { buffer, format: RawExtractedFormat.Jxl };
} catch (error: any) { } catch (error: any) {
this.logger.debug('Could not extract PreviewJXL buffer from image, trying PreviewImage next', error.message); this.logger.debug('Could not extract PreviewJXL buffer from image, trying PreviewImage next', error.message);
} }
try { try {
const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input); const buffer = await exiftool.extractBinaryTagToBuffer('PreviewImage', input);
return { buffer, format: RawExtractedFormat.JPEG }; return { buffer, format: RawExtractedFormat.Jpeg };
} catch (error: any) { } catch (error: any) {
this.logger.debug('Could not extract preview buffer from image', error.message); this.logger.debug('Could not extract preview buffer from image', error.message);
return null; return null;
@ -142,7 +142,7 @@ export class MediaRepository {
limitInputPixels: false, limitInputPixels: false,
raw: options.raw, raw: options.raw,
}) })
.pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16') .pipelineColorspace(options.colorspace === Colorspace.Srgb ? 'srgb' : 'rgb16')
.withIccProfile(options.colorspace); .withIccProfile(options.colorspace);
if (!options.raw) { if (!options.raw) {
@ -267,7 +267,7 @@ export class MediaRepository {
const { frameCount, percentInterval } = options.progress; const { frameCount, percentInterval } = options.progress;
const frameInterval = Math.ceil(frameCount / (100 / percentInterval)); const frameInterval = Math.ceil(frameCount / (100 / percentInterval));
if (this.logger.isLevelEnabled(LogLevel.DEBUG) && frameCount && frameInterval) { if (this.logger.isLevelEnabled(LogLevel.Debug) && frameCount && frameInterval) {
let lastProgressFrame: number = 0; let lastProgressFrame: number = 0;
ffmpegCall.on('progress', (progress: ProgressEvent) => { ffmpegCall.on('progress', (progress: ProgressEvent) => {
if (progress.frames - lastProgressFrame < frameInterval) { if (progress.frames - lastProgressFrame < frameInterval) {

View File

@ -19,7 +19,7 @@ export class MemoryRepository implements IBulkAsset {
.deleteFrom('memory_asset') .deleteFrom('memory_asset')
.using('asset') .using('asset')
.whereRef('memory_asset.assetsId', '=', 'asset.id') .whereRef('memory_asset.assetsId', '=', 'asset.id')
.where('asset.visibility', '!=', AssetVisibility.TIMELINE) .where('asset.visibility', '!=', AssetVisibility.Timeline)
.execute(); .execute();
return this.db return this.db
@ -67,7 +67,7 @@ export class MemoryRepository implements IBulkAsset {
.innerJoin('memory_asset', 'asset.id', 'memory_asset.assetsId') .innerJoin('memory_asset', 'asset.id', 'memory_asset.assetsId')
.whereRef('memory_asset.memoriesId', '=', 'memory.id') .whereRef('memory_asset.memoriesId', '=', 'memory.id')
.orderBy('asset.fileCreatedAt', 'asc') .orderBy('asset.fileCreatedAt', 'asc')
.where('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
.where('asset.deletedAt', 'is', null), .where('asset.deletedAt', 'is', null),
).as('assets'), ).as('assets'),
) )
@ -158,7 +158,7 @@ export class MemoryRepository implements IBulkAsset {
.innerJoin('memory_asset', 'asset.id', 'memory_asset.assetsId') .innerJoin('memory_asset', 'asset.id', 'memory_asset.assetsId')
.whereRef('memory_asset.memoriesId', '=', 'memory.id') .whereRef('memory_asset.memoriesId', '=', 'memory.id')
.orderBy('asset.fileCreatedAt', 'asc') .orderBy('asset.fileCreatedAt', 'asc')
.where('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
.where('asset.deletedAt', 'is', null), .where('asset.deletedAt', 'is', null),
).as('assets'), ).as('assets'),
) )

View File

@ -48,7 +48,7 @@ export class MoveRepository {
eb.selectFrom('asset').select('id').whereRef('asset.id', '=', 'move_history.entityId'), eb.selectFrom('asset').select('id').whereRef('asset.id', '=', 'move_history.entityId'),
), ),
) )
.where('move_history.pathType', '=', sql.lit(AssetPathType.ORIGINAL)) .where('move_history.pathType', '=', sql.lit(AssetPathType.Original))
.execute(); .execute();
} }
@ -56,7 +56,7 @@ export class MoveRepository {
async cleanMoveHistorySingle(assetId: string): Promise<void> { async cleanMoveHistorySingle(assetId: string): Promise<void> {
await this.db await this.db
.deleteFrom('move_history') .deleteFrom('move_history')
.where('move_history.pathType', '=', sql.lit(AssetPathType.ORIGINAL)) .where('move_history.pathType', '=', sql.lit(AssetPathType.Original))
.where('entityId', '=', assetId) .where('entityId', '=', assetId)
.execute(); .execute();
} }

View File

@ -138,11 +138,11 @@ export class OAuthRepository {
} }
switch (tokenEndpointAuthMethod) { switch (tokenEndpointAuthMethod) {
case OAuthTokenEndpointAuthMethod.CLIENT_SECRET_POST: { case OAuthTokenEndpointAuthMethod.ClientSecretPost: {
return ClientSecretPost(clientSecret); return ClientSecretPost(clientSecret);
} }
case OAuthTokenEndpointAuthMethod.CLIENT_SECRET_BASIC: { case OAuthTokenEndpointAuthMethod.ClientSecretBasic: {
return ClientSecretBasic(clientSecret); return ClientSecretBasic(clientSecret);
} }

View File

@ -151,7 +151,7 @@ export class PersonRepository {
.innerJoin('asset', (join) => .innerJoin('asset', (join) =>
join join
.onRef('asset_face.assetId', '=', 'asset.id') .onRef('asset_face.assetId', '=', 'asset.id')
.on('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
.on('asset.deletedAt', 'is', null), .on('asset.deletedAt', 'is', null),
) )
.where('person.ownerId', '=', userId) .where('person.ownerId', '=', userId)
@ -276,7 +276,7 @@ export class PersonRepository {
.selectFrom('asset_file') .selectFrom('asset_file')
.select('asset_file.path') .select('asset_file.path')
.whereRef('asset_file.assetId', '=', 'asset.id') .whereRef('asset_file.assetId', '=', 'asset.id')
.where('asset_file.type', '=', sql.lit(AssetFileType.PREVIEW)) .where('asset_file.type', '=', sql.lit(AssetFileType.Preview))
.as('previewPath'), .as('previewPath'),
) )
.where('person.id', '=', id) .where('person.id', '=', id)
@ -341,7 +341,7 @@ export class PersonRepository {
join join
.onRef('asset.id', '=', 'asset_face.assetId') .onRef('asset.id', '=', 'asset_face.assetId')
.on('asset_face.personId', '=', personId) .on('asset_face.personId', '=', personId)
.on('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .on('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
.on('asset.deletedAt', 'is', null), .on('asset.deletedAt', 'is', null),
) )
.select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count')) .select((eb) => eb.fn.count(eb.fn('distinct', ['asset.id'])).as('count'))
@ -369,7 +369,7 @@ export class PersonRepository {
eb eb
.selectFrom('asset') .selectFrom('asset')
.whereRef('asset.id', '=', 'asset_face.assetId') .whereRef('asset.id', '=', 'asset_face.assetId')
.where('asset.visibility', '=', sql.lit(AssetVisibility.TIMELINE)) .where('asset.visibility', '=', sql.lit(AssetVisibility.Timeline))
.where('asset.deletedAt', 'is', null), .where('asset.deletedAt', 'is', null),
), ),
), ),

View File

@ -256,7 +256,7 @@ export class SearchRepository {
} }
return this.db.transaction().execute(async (trx) => { return this.db.transaction().execute(async (trx) => {
await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx); await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.Clip])}`.execute(trx);
const items = await searchAssetBuilder(trx, options) const items = await searchAssetBuilder(trx, options)
.selectAll('asset') .selectAll('asset')
.innerJoin('smart_search', 'asset.id', 'smart_search.assetId') .innerJoin('smart_search', 'asset.id', 'smart_search.assetId')
@ -284,7 +284,7 @@ export class SearchRepository {
} }
return this.db.transaction().execute(async (trx) => { return this.db.transaction().execute(async (trx) => {
await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.FACE])}`.execute(trx); await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.Face])}`.execute(trx);
return await trx return await trx
.with('cte', (qb) => .with('cte', (qb) =>
qb qb
@ -351,8 +351,8 @@ export class SearchRepository {
.select(['city', 'assetId']) .select(['city', 'assetId'])
.innerJoin('asset', 'asset.id', 'asset_exif.assetId') .innerJoin('asset', 'asset.id', 'asset_exif.assetId')
.where('asset.ownerId', '=', anyUuid(userIds)) .where('asset.ownerId', '=', anyUuid(userIds))
.where('asset.visibility', '=', AssetVisibility.TIMELINE) .where('asset.visibility', '=', AssetVisibility.Timeline)
.where('asset.type', '=', AssetType.IMAGE) .where('asset.type', '=', AssetType.Image)
.where('asset.deletedAt', 'is', null) .where('asset.deletedAt', 'is', null)
.orderBy('city') .orderBy('city')
.limit(1); .limit(1);
@ -367,8 +367,8 @@ export class SearchRepository {
.select(['city', 'assetId']) .select(['city', 'assetId'])
.innerJoin('asset', 'asset.id', 'asset_exif.assetId') .innerJoin('asset', 'asset.id', 'asset_exif.assetId')
.where('asset.ownerId', '=', anyUuid(userIds)) .where('asset.ownerId', '=', anyUuid(userIds))
.where('asset.visibility', '=', AssetVisibility.TIMELINE) .where('asset.visibility', '=', AssetVisibility.Timeline)
.where('asset.type', '=', AssetType.IMAGE) .where('asset.type', '=', AssetType.Image)
.where('asset.deletedAt', 'is', null) .where('asset.deletedAt', 'is', null)
.whereRef('asset_exif.city', '>', 'cte.city') .whereRef('asset_exif.city', '>', 'cte.city')
.orderBy('city') .orderBy('city')
@ -450,7 +450,7 @@ export class SearchRepository {
.distinctOn(field) .distinctOn(field)
.innerJoin('asset', 'asset.id', 'asset_exif.assetId') .innerJoin('asset', 'asset.id', 'asset_exif.assetId')
.where('ownerId', '=', anyUuid(userIds)) .where('ownerId', '=', anyUuid(userIds))
.where('visibility', '=', AssetVisibility.TIMELINE) .where('visibility', '=', AssetVisibility.Timeline)
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.where(field, 'is not', null); .where(field, 'is not', null);
} }

View File

@ -103,7 +103,7 @@ export class SharedLinkRepository {
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album')) .select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
.where('shared_link.id', '=', id) .where('shared_link.id', '=', id)
.where('shared_link.userId', '=', userId) .where('shared_link.userId', '=', userId)
.where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)]))
.orderBy('shared_link.createdAt', 'desc') .orderBy('shared_link.createdAt', 'desc')
.executeTakeFirst(); .executeTakeFirst();
} }
@ -165,7 +165,7 @@ export class SharedLinkRepository {
(join) => join.onTrue(), (join) => join.onTrue(),
) )
.select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album')) .select((eb) => eb.fn.toJson('album').$castTo<Album | null>().as('album'))
.where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)]))
.$if(!!albumId, (eb) => eb.where('shared_link.albumId', '=', albumId!)) .$if(!!albumId, (eb) => eb.where('shared_link.albumId', '=', albumId!))
.orderBy('shared_link.createdAt', 'desc') .orderBy('shared_link.createdAt', 'desc')
.distinctOn(['shared_link.createdAt']) .distinctOn(['shared_link.createdAt'])
@ -185,7 +185,7 @@ export class SharedLinkRepository {
eb.selectFrom('user').select(columns.authUser).whereRef('user.id', '=', 'shared_link.userId'), eb.selectFrom('user').select(columns.authUser).whereRef('user.id', '=', 'shared_link.userId'),
).as('user'), ).as('user'),
]) ])
.where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.INDIVIDUAL), eb('album.id', 'is not', null)])) .where((eb) => eb.or([eb('shared_link.type', '=', SharedLinkType.Individual), eb('album.id', 'is not', null)]))
.executeTakeFirst(); .executeTakeFirst();
} }

View File

@ -112,21 +112,21 @@ export class TelemetryRepository {
const { telemetry } = this.configRepository.getEnv(); const { telemetry } = this.configRepository.getEnv();
const { metrics } = telemetry; const { metrics } = telemetry;
this.api = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.API) }); this.api = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.Api) });
this.host = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.HOST) }); this.host = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.Host) });
this.jobs = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.JOB) }); this.jobs = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.Job) });
this.repo = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.REPO) }); this.repo = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.Repo) });
} }
setup({ repositories }: { repositories: ClassConstructor<unknown>[] }) { setup({ repositories }: { repositories: ClassConstructor<unknown>[] }) {
const { telemetry } = this.configRepository.getEnv(); const { telemetry } = this.configRepository.getEnv();
const { metrics } = telemetry; const { metrics } = telemetry;
if (!metrics.has(ImmichTelemetry.REPO)) { if (!metrics.has(ImmichTelemetry.Repo)) {
return; return;
} }
for (const Repository of repositories) { for (const Repository of repositories) {
const isEnabled = this.reflect.get(MetadataKey.TELEMETRY_ENABLED, Repository) ?? true; const isEnabled = this.reflect.get(MetadataKey.TelemetryEnabled, Repository) ?? true;
if (!isEnabled) { if (!isEnabled) {
this.logger.debug(`Telemetry disabled for ${Repository.name}`); this.logger.debug(`Telemetry disabled for ${Repository.name}`);
continue; continue;

View File

@ -8,7 +8,7 @@ export class TrashRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {} constructor(@InjectKysely() private db: Kysely<DB>) {}
getDeletedIds(): AsyncIterableIterator<{ id: string }> { getDeletedIds(): AsyncIterableIterator<{ id: string }> {
return this.db.selectFrom('asset').select(['id']).where('status', '=', AssetStatus.DELETED).stream(); return this.db.selectFrom('asset').select(['id']).where('status', '=', AssetStatus.Deleted).stream();
} }
@GenerateSql({ params: [DummyValue.UUID] }) @GenerateSql({ params: [DummyValue.UUID] })
@ -16,8 +16,8 @@ export class TrashRepository {
const { numUpdatedRows } = await this.db const { numUpdatedRows } = await this.db
.updateTable('asset') .updateTable('asset')
.where('ownerId', '=', userId) .where('ownerId', '=', userId)
.where('status', '=', AssetStatus.TRASHED) .where('status', '=', AssetStatus.Trashed)
.set({ status: AssetStatus.ACTIVE, deletedAt: null }) .set({ status: AssetStatus.Active, deletedAt: null })
.executeTakeFirst(); .executeTakeFirst();
return Number(numUpdatedRows); return Number(numUpdatedRows);
@ -28,8 +28,8 @@ export class TrashRepository {
const { numUpdatedRows } = await this.db const { numUpdatedRows } = await this.db
.updateTable('asset') .updateTable('asset')
.where('ownerId', '=', userId) .where('ownerId', '=', userId)
.where('status', '=', AssetStatus.TRASHED) .where('status', '=', AssetStatus.Trashed)
.set({ status: AssetStatus.DELETED }) .set({ status: AssetStatus.Deleted })
.executeTakeFirst(); .executeTakeFirst();
return Number(numUpdatedRows); return Number(numUpdatedRows);
@ -43,9 +43,9 @@ export class TrashRepository {
const { numUpdatedRows } = await this.db const { numUpdatedRows } = await this.db
.updateTable('asset') .updateTable('asset')
.where('status', '=', AssetStatus.TRASHED) .where('status', '=', AssetStatus.Trashed)
.where('id', 'in', ids) .where('id', 'in', ids)
.set({ status: AssetStatus.ACTIVE, deletedAt: null }) .set({ status: AssetStatus.Active, deletedAt: null })
.executeTakeFirst(); .executeTakeFirst();
return Number(numUpdatedRows); return Number(numUpdatedRows);

View File

@ -187,7 +187,7 @@ export class UserRepository {
restore(id: string) { restore(id: string) {
return this.db return this.db
.updateTable('user') .updateTable('user')
.set({ status: UserStatus.ACTIVE, deletedAt: null }) .set({ status: UserStatus.Active, deletedAt: null })
.where('user.id', '=', asUuid(id)) .where('user.id', '=', asUuid(id))
.returning(columns.userAdmin) .returning(columns.userAdmin)
.returning(withMetadata) .returning(withMetadata)
@ -229,8 +229,8 @@ export class UserRepository {
.countAll<number>() .countAll<number>()
.filterWhere((eb) => .filterWhere((eb) =>
eb.and([ eb.and([
eb('asset.type', '=', sql.lit(AssetType.IMAGE)), eb('asset.type', '=', sql.lit(AssetType.Image)),
eb('asset.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)), eb('asset.visibility', '!=', sql.lit(AssetVisibility.Hidden)),
]), ]),
) )
.as('photos'), .as('photos'),
@ -238,8 +238,8 @@ export class UserRepository {
.countAll<number>() .countAll<number>()
.filterWhere((eb) => .filterWhere((eb) =>
eb.and([ eb.and([
eb('asset.type', '=', sql.lit(AssetType.VIDEO)), eb('asset.type', '=', sql.lit(AssetType.Video)),
eb('asset.visibility', '!=', sql.lit(AssetVisibility.HIDDEN)), eb('asset.visibility', '!=', sql.lit(AssetVisibility.Hidden)),
]), ]),
) )
.as('videos'), .as('videos'),
@ -254,7 +254,7 @@ export class UserRepository {
eb.fn eb.fn
.sum<number>('asset_exif.fileSizeInByte') .sum<number>('asset_exif.fileSizeInByte')
.filterWhere((eb) => .filterWhere((eb) =>
eb.and([eb('asset.libraryId', 'is', null), eb('asset.type', '=', sql.lit(AssetType.IMAGE))]), eb.and([eb('asset.libraryId', 'is', null), eb('asset.type', '=', sql.lit(AssetType.Image))]),
), ),
eb.lit(0), eb.lit(0),
) )
@ -264,7 +264,7 @@ export class UserRepository {
eb.fn eb.fn
.sum<number>('asset_exif.fileSizeInByte') .sum<number>('asset_exif.fileSizeInByte')
.filterWhere((eb) => .filterWhere((eb) =>
eb.and([eb('asset.libraryId', 'is', null), eb('asset.type', '=', sql.lit(AssetType.VIDEO))]), eb.and([eb('asset.libraryId', 'is', null), eb('asset.type', '=', sql.lit(AssetType.Video))]),
), ),
eb.lit(0), eb.lit(0),
) )

View File

@ -15,7 +15,7 @@ export class ViewRepository {
.select((eb) => eb.fn<string>('substring', ['asset.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath')) .select((eb) => eb.fn<string>('substring', ['asset.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath'))
.distinct() .distinct()
.where('ownerId', '=', asUuid(userId)) .where('ownerId', '=', asUuid(userId))
.where('visibility', '=', AssetVisibility.TIMELINE) .where('visibility', '=', AssetVisibility.Timeline)
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.where('fileCreatedAt', 'is not', null) .where('fileCreatedAt', 'is not', null)
.where('fileModifiedAt', 'is not', null) .where('fileModifiedAt', 'is not', null)
@ -34,7 +34,7 @@ export class ViewRepository {
.selectAll('asset') .selectAll('asset')
.$call(withExif) .$call(withExif)
.where('ownerId', '=', asUuid(userId)) .where('ownerId', '=', asUuid(userId))
.where('visibility', '=', AssetVisibility.TIMELINE) .where('visibility', '=', AssetVisibility.Timeline)
.where('deletedAt', 'is', null) .where('deletedAt', 'is', null)
.where('fileCreatedAt', 'is not', null) .where('fileCreatedAt', 'is not', null)
.where('fileModifiedAt', 'is not', null) .where('fileModifiedAt', 'is not', null)

View File

@ -16,9 +16,7 @@ export async function up(db: Kysely<any>): Promise<void> {
rows: [lastMigration], rows: [lastMigration],
} = await lastMigrationSql.execute(db); } = await lastMigrationSql.execute(db);
if (lastMigration?.name !== 'AddMissingIndex1744910873956') { if (lastMigration?.name !== 'AddMissingIndex1744910873956') {
throw new Error( throw new Error('Invalid upgrade path. For more information, see https://immich.app/errors#typeorm-upgrade');
'Invalid upgrade path. For more information, see https://immich.app/errors#typeorm-upgrade',
);
} }
logger.log('Database has up to date TypeORM migrations, skipping initial Kysely migration'); logger.log('Database has up to date TypeORM migrations, skipping initial Kysely migration');
return; return;
@ -108,152 +106,344 @@ export async function up(db: Kysely<any>): Promise<void> {
RETURN NULL; RETURN NULL;
END; END;
$$;`.execute(db); $$;`.execute(db);
if (vectorExtension === DatabaseExtension.VECTORS) { if (vectorExtension === DatabaseExtension.Vectors) {
await sql`SET search_path TO "$user", public, vectors`.execute(db); await sql`SET search_path TO "$user", public, vectors`.execute(db);
} }
await sql`CREATE TYPE "assets_status_enum" AS ENUM ('active','trashed','deleted');`.execute(db); await sql`CREATE TYPE "assets_status_enum" AS ENUM ('active','trashed','deleted');`.execute(db);
await sql`CREATE TYPE "sourcetype" AS ENUM ('machine-learning','exif','manual');`.execute(db); await sql`CREATE TYPE "sourcetype" AS ENUM ('machine-learning','exif','manual');`.execute(db);
await sql`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "password" character varying NOT NULL DEFAULT '', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "profileImagePath" character varying NOT NULL DEFAULT '', "isAdmin" boolean NOT NULL DEFAULT false, "shouldChangePassword" boolean NOT NULL DEFAULT true, "deletedAt" timestamp with time zone, "oauthId" character varying NOT NULL DEFAULT '', "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "storageLabel" character varying, "name" character varying NOT NULL DEFAULT '', "quotaSizeInBytes" bigint, "quotaUsageInBytes" bigint NOT NULL DEFAULT 0, "status" character varying NOT NULL DEFAULT 'active', "profileChangedAt" timestamp with time zone NOT NULL DEFAULT now(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); await sql`CREATE TABLE "users" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "email" character varying NOT NULL, "password" character varying NOT NULL DEFAULT '', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "profileImagePath" character varying NOT NULL DEFAULT '', "isAdmin" boolean NOT NULL DEFAULT false, "shouldChangePassword" boolean NOT NULL DEFAULT true, "deletedAt" timestamp with time zone, "oauthId" character varying NOT NULL DEFAULT '', "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "storageLabel" character varying, "name" character varying NOT NULL DEFAULT '', "quotaSizeInBytes" bigint, "quotaUsageInBytes" bigint NOT NULL DEFAULT 0, "status" character varying NOT NULL DEFAULT 'active', "profileChangedAt" timestamp with time zone NOT NULL DEFAULT now(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
await sql`CREATE TABLE "libraries" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "ownerId" uuid NOT NULL, "importPaths" text[] NOT NULL, "exclusionPatterns" text[] NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "refreshedAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); db,
await sql`CREATE TABLE "asset_stack" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "primaryAssetId" uuid NOT NULL, "ownerId" uuid NOT NULL);`.execute(db); );
await sql`CREATE TABLE "assets" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deviceAssetId" character varying NOT NULL, "ownerId" uuid NOT NULL, "deviceId" character varying NOT NULL, "type" character varying NOT NULL, "originalPath" character varying NOT NULL, "fileCreatedAt" timestamp with time zone NOT NULL, "fileModifiedAt" timestamp with time zone NOT NULL, "isFavorite" boolean NOT NULL DEFAULT false, "duration" character varying, "encodedVideoPath" character varying DEFAULT '', "checksum" bytea NOT NULL, "isVisible" boolean NOT NULL DEFAULT true, "livePhotoVideoId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "isArchived" boolean NOT NULL DEFAULT false, "originalFileName" character varying NOT NULL, "sidecarPath" character varying, "thumbhash" bytea, "isOffline" boolean NOT NULL DEFAULT false, "libraryId" uuid, "isExternal" boolean NOT NULL DEFAULT false, "deletedAt" timestamp with time zone, "localDateTime" timestamp with time zone NOT NULL, "stackId" uuid, "duplicateId" uuid, "status" assets_status_enum NOT NULL DEFAULT 'active', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); await sql`CREATE TABLE "libraries" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying NOT NULL, "ownerId" uuid NOT NULL, "importPaths" text[] NOT NULL, "exclusionPatterns" text[] NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "refreshedAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
await sql`CREATE TABLE "albums" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ownerId" uuid NOT NULL, "albumName" character varying NOT NULL DEFAULT 'Untitled Album', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "albumThumbnailAssetId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "description" text NOT NULL DEFAULT '', "deletedAt" timestamp with time zone, "isActivityEnabled" boolean NOT NULL DEFAULT true, "order" character varying NOT NULL DEFAULT 'desc', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); db,
);
await sql`CREATE TABLE "asset_stack" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "primaryAssetId" uuid NOT NULL, "ownerId" uuid NOT NULL);`.execute(
db,
);
await sql`CREATE TABLE "assets" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "deviceAssetId" character varying NOT NULL, "ownerId" uuid NOT NULL, "deviceId" character varying NOT NULL, "type" character varying NOT NULL, "originalPath" character varying NOT NULL, "fileCreatedAt" timestamp with time zone NOT NULL, "fileModifiedAt" timestamp with time zone NOT NULL, "isFavorite" boolean NOT NULL DEFAULT false, "duration" character varying, "encodedVideoPath" character varying DEFAULT '', "checksum" bytea NOT NULL, "isVisible" boolean NOT NULL DEFAULT true, "livePhotoVideoId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "isArchived" boolean NOT NULL DEFAULT false, "originalFileName" character varying NOT NULL, "sidecarPath" character varying, "thumbhash" bytea, "isOffline" boolean NOT NULL DEFAULT false, "libraryId" uuid, "isExternal" boolean NOT NULL DEFAULT false, "deletedAt" timestamp with time zone, "localDateTime" timestamp with time zone NOT NULL, "stackId" uuid, "duplicateId" uuid, "status" assets_status_enum NOT NULL DEFAULT 'active', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
db,
);
await sql`CREATE TABLE "albums" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "ownerId" uuid NOT NULL, "albumName" character varying NOT NULL DEFAULT 'Untitled Album', "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "albumThumbnailAssetId" uuid, "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "description" text NOT NULL DEFAULT '', "deletedAt" timestamp with time zone, "isActivityEnabled" boolean NOT NULL DEFAULT true, "order" character varying NOT NULL DEFAULT 'desc', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
db,
);
await sql`COMMENT ON COLUMN "albums"."albumThumbnailAssetId" IS 'Asset ID to be used as thumbnail';`.execute(db); await sql`COMMENT ON COLUMN "albums"."albumThumbnailAssetId" IS 'Asset ID to be used as thumbnail';`.execute(db);
await sql`CREATE TABLE "activity" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "assetId" uuid, "comment" text, "isLiked" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); await sql`CREATE TABLE "activity" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "albumId" uuid NOT NULL, "userId" uuid NOT NULL, "assetId" uuid, "comment" text, "isLiked" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
await sql`CREATE TABLE "albums_assets_assets" ("albumsId" uuid NOT NULL, "assetsId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute(db); db,
await sql`CREATE TABLE "albums_shared_users_users" ("albumsId" uuid NOT NULL, "usersId" uuid NOT NULL, "role" character varying NOT NULL DEFAULT 'editor');`.execute(db); );
await sql`CREATE TABLE "api_keys" ("name" character varying NOT NULL, "key" character varying NOT NULL, "userId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "permissions" character varying[] NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); await sql`CREATE TABLE "albums_assets_assets" ("albumsId" uuid NOT NULL, "assetsId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute(
await sql`CREATE TABLE "assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "assetId" uuid NOT NULL, "ownerId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db); db,
await sql`CREATE TABLE "person" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ownerId" uuid NOT NULL, "name" character varying NOT NULL DEFAULT '', "thumbnailPath" character varying NOT NULL DEFAULT '', "isHidden" boolean NOT NULL DEFAULT false, "birthDate" date, "faceAssetId" uuid, "isFavorite" boolean NOT NULL DEFAULT false, "color" character varying, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); );
await sql`CREATE TABLE "asset_faces" ("assetId" uuid NOT NULL, "personId" uuid, "imageWidth" integer NOT NULL DEFAULT 0, "imageHeight" integer NOT NULL DEFAULT 0, "boundingBoxX1" integer NOT NULL DEFAULT 0, "boundingBoxY1" integer NOT NULL DEFAULT 0, "boundingBoxX2" integer NOT NULL DEFAULT 0, "boundingBoxY2" integer NOT NULL DEFAULT 0, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "sourceType" sourcetype NOT NULL DEFAULT 'machine-learning', "deletedAt" timestamp with time zone);`.execute(db); await sql`CREATE TABLE "albums_shared_users_users" ("albumsId" uuid NOT NULL, "usersId" uuid NOT NULL, "role" character varying NOT NULL DEFAULT 'editor');`.execute(
await sql`CREATE TABLE "asset_files" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "assetId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "type" character varying NOT NULL, "path" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); db,
await sql`CREATE TABLE "asset_job_status" ("assetId" uuid NOT NULL, "facesRecognizedAt" timestamp with time zone, "metadataExtractedAt" timestamp with time zone, "duplicatesDetectedAt" timestamp with time zone, "previewAt" timestamp with time zone, "thumbnailAt" timestamp with time zone);`.execute(db); );
await sql`CREATE TABLE "audit" ("id" serial NOT NULL, "entityType" character varying NOT NULL, "entityId" uuid NOT NULL, "action" character varying NOT NULL, "ownerId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute(db); await sql`CREATE TABLE "api_keys" ("name" character varying NOT NULL, "key" character varying NOT NULL, "userId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "permissions" character varying[] NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
await sql`CREATE TABLE "exif" ("assetId" uuid NOT NULL, "make" character varying, "model" character varying, "exifImageWidth" integer, "exifImageHeight" integer, "fileSizeInByte" bigint, "orientation" character varying, "dateTimeOriginal" timestamp with time zone, "modifyDate" timestamp with time zone, "lensModel" character varying, "fNumber" double precision, "focalLength" double precision, "iso" integer, "latitude" double precision, "longitude" double precision, "city" character varying, "state" character varying, "country" character varying, "description" text NOT NULL DEFAULT '', "fps" double precision, "exposureTime" character varying, "livePhotoCID" character varying, "timeZone" character varying, "projectionType" character varying, "profileDescription" character varying, "colorspace" character varying, "bitsPerSample" integer, "autoStackId" character varying, "rating" integer, "updatedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); db,
);
await sql`CREATE TABLE "assets_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "assetId" uuid NOT NULL, "ownerId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(
db,
);
await sql`CREATE TABLE "person" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ownerId" uuid NOT NULL, "name" character varying NOT NULL DEFAULT '', "thumbnailPath" character varying NOT NULL DEFAULT '', "isHidden" boolean NOT NULL DEFAULT false, "birthDate" date, "faceAssetId" uuid, "isFavorite" boolean NOT NULL DEFAULT false, "color" character varying, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
db,
);
await sql`CREATE TABLE "asset_faces" ("assetId" uuid NOT NULL, "personId" uuid, "imageWidth" integer NOT NULL DEFAULT 0, "imageHeight" integer NOT NULL DEFAULT 0, "boundingBoxX1" integer NOT NULL DEFAULT 0, "boundingBoxY1" integer NOT NULL DEFAULT 0, "boundingBoxX2" integer NOT NULL DEFAULT 0, "boundingBoxY2" integer NOT NULL DEFAULT 0, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "sourceType" sourcetype NOT NULL DEFAULT 'machine-learning', "deletedAt" timestamp with time zone);`.execute(
db,
);
await sql`CREATE TABLE "asset_files" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "assetId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "type" character varying NOT NULL, "path" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
db,
);
await sql`CREATE TABLE "asset_job_status" ("assetId" uuid NOT NULL, "facesRecognizedAt" timestamp with time zone, "metadataExtractedAt" timestamp with time zone, "duplicatesDetectedAt" timestamp with time zone, "previewAt" timestamp with time zone, "thumbnailAt" timestamp with time zone);`.execute(
db,
);
await sql`CREATE TABLE "audit" ("id" serial NOT NULL, "entityType" character varying NOT NULL, "entityId" uuid NOT NULL, "action" character varying NOT NULL, "ownerId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now());`.execute(
db,
);
await sql`CREATE TABLE "exif" ("assetId" uuid NOT NULL, "make" character varying, "model" character varying, "exifImageWidth" integer, "exifImageHeight" integer, "fileSizeInByte" bigint, "orientation" character varying, "dateTimeOriginal" timestamp with time zone, "modifyDate" timestamp with time zone, "lensModel" character varying, "fNumber" double precision, "focalLength" double precision, "iso" integer, "latitude" double precision, "longitude" double precision, "city" character varying, "state" character varying, "country" character varying, "description" text NOT NULL DEFAULT '', "fps" double precision, "exposureTime" character varying, "livePhotoCID" character varying, "timeZone" character varying, "projectionType" character varying, "profileDescription" character varying, "colorspace" character varying, "bitsPerSample" integer, "autoStackId" character varying, "rating" integer, "updatedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
db,
);
await sql`CREATE TABLE "face_search" ("faceId" uuid NOT NULL, "embedding" vector(512) NOT NULL);`.execute(db); await sql`CREATE TABLE "face_search" ("faceId" uuid NOT NULL, "embedding" vector(512) NOT NULL);`.execute(db);
await sql`CREATE TABLE "geodata_places" ("id" integer NOT NULL, "name" character varying(200) NOT NULL, "longitude" double precision NOT NULL, "latitude" double precision NOT NULL, "countryCode" character(2) NOT NULL, "admin1Code" character varying(20), "admin2Code" character varying(80), "modificationDate" date NOT NULL, "admin1Name" character varying, "admin2Name" character varying, "alternateNames" character varying);`.execute(db); await sql`CREATE TABLE "geodata_places" ("id" integer NOT NULL, "name" character varying(200) NOT NULL, "longitude" double precision NOT NULL, "latitude" double precision NOT NULL, "countryCode" character(2) NOT NULL, "admin1Code" character varying(20), "admin2Code" character varying(80), "modificationDate" date NOT NULL, "admin1Name" character varying, "admin2Name" character varying, "alternateNames" character varying);`.execute(
await sql`CREATE TABLE "memories" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "ownerId" uuid NOT NULL, "type" character varying NOT NULL, "data" jsonb NOT NULL, "isSaved" boolean NOT NULL DEFAULT false, "memoryAt" timestamp with time zone NOT NULL, "seenAt" timestamp with time zone, "showAt" timestamp with time zone, "hideAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); db,
);
await sql`CREATE TABLE "memories" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "deletedAt" timestamp with time zone, "ownerId" uuid NOT NULL, "type" character varying NOT NULL, "data" jsonb NOT NULL, "isSaved" boolean NOT NULL DEFAULT false, "memoryAt" timestamp with time zone NOT NULL, "seenAt" timestamp with time zone, "showAt" timestamp with time zone, "hideAt" timestamp with time zone, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
db,
);
await sql`CREATE TABLE "memories_assets_assets" ("memoriesId" uuid NOT NULL, "assetsId" uuid NOT NULL);`.execute(db); await sql`CREATE TABLE "memories_assets_assets" ("memoriesId" uuid NOT NULL, "assetsId" uuid NOT NULL);`.execute(db);
await sql`CREATE TABLE "move_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "entityId" uuid NOT NULL, "pathType" character varying NOT NULL, "oldPath" character varying NOT NULL, "newPath" character varying NOT NULL);`.execute(db); await sql`CREATE TABLE "move_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "entityId" uuid NOT NULL, "pathType" character varying NOT NULL, "oldPath" character varying NOT NULL, "newPath" character varying NOT NULL);`.execute(
await sql`CREATE TABLE "naturalearth_countries" ("id" integer NOT NULL GENERATED ALWAYS AS IDENTITY, "admin" character varying(50) NOT NULL, "admin_a3" character varying(3) NOT NULL, "type" character varying(50) NOT NULL, "coordinates" polygon NOT NULL);`.execute(db); db,
await sql`CREATE TABLE "partners_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(db); );
await sql`CREATE TABLE "partners" ("sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "inTimeline" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); await sql`CREATE TABLE "naturalearth_countries" ("id" integer NOT NULL GENERATED ALWAYS AS IDENTITY, "admin" character varying(50) NOT NULL, "admin_a3" character varying(3) NOT NULL, "type" character varying(50) NOT NULL, "coordinates" polygon NOT NULL);`.execute(
await sql`CREATE TABLE "sessions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "userId" uuid NOT NULL, "deviceType" character varying NOT NULL DEFAULT '', "deviceOS" character varying NOT NULL DEFAULT '', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); db,
await sql`CREATE TABLE "shared_links" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "description" character varying, "userId" uuid NOT NULL, "key" bytea NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "expiresAt" timestamp with time zone, "allowUpload" boolean NOT NULL DEFAULT false, "albumId" uuid, "allowDownload" boolean NOT NULL DEFAULT true, "showExif" boolean NOT NULL DEFAULT true, "password" character varying);`.execute(db); );
await sql`CREATE TABLE "partners_audit" ("id" uuid NOT NULL DEFAULT immich_uuid_v7(), "sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp());`.execute(
db,
);
await sql`CREATE TABLE "partners" ("sharedById" uuid NOT NULL, "sharedWithId" uuid NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "inTimeline" boolean NOT NULL DEFAULT false, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
db,
);
await sql`CREATE TABLE "sessions" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "token" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "userId" uuid NOT NULL, "deviceType" character varying NOT NULL DEFAULT '', "deviceOS" character varying NOT NULL DEFAULT '', "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
db,
);
await sql`CREATE TABLE "shared_links" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "description" character varying, "userId" uuid NOT NULL, "key" bytea NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "expiresAt" timestamp with time zone, "allowUpload" boolean NOT NULL DEFAULT false, "albumId" uuid, "allowDownload" boolean NOT NULL DEFAULT true, "showExif" boolean NOT NULL DEFAULT true, "password" character varying);`.execute(
db,
);
await sql`CREATE TABLE "shared_link__asset" ("assetsId" uuid NOT NULL, "sharedLinksId" uuid NOT NULL);`.execute(db); await sql`CREATE TABLE "shared_link__asset" ("assetsId" uuid NOT NULL, "sharedLinksId" uuid NOT NULL);`.execute(db);
await sql`CREATE TABLE "smart_search" ("assetId" uuid NOT NULL, "embedding" vector(512) NOT NULL);`.execute(db); await sql`CREATE TABLE "smart_search" ("assetId" uuid NOT NULL, "embedding" vector(512) NOT NULL);`.execute(db);
await sql`ALTER TABLE "smart_search" ALTER COLUMN "embedding" SET STORAGE EXTERNAL;`.execute(db); await sql`ALTER TABLE "smart_search" ALTER COLUMN "embedding" SET STORAGE EXTERNAL;`.execute(db);
await sql`CREATE TABLE "session_sync_checkpoints" ("sessionId" uuid NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ack" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); await sql`CREATE TABLE "session_sync_checkpoints" ("sessionId" uuid NOT NULL, "type" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "ack" character varying NOT NULL, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
db,
);
await sql`CREATE TABLE "system_metadata" ("key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute(db); await sql`CREATE TABLE "system_metadata" ("key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute(db);
await sql`CREATE TABLE "tags" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "value" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "color" character varying, "parentId" uuid, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); await sql`CREATE TABLE "tags" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "userId" uuid NOT NULL, "value" character varying NOT NULL, "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "updatedAt" timestamp with time zone NOT NULL DEFAULT now(), "color" character varying, "parentId" uuid, "updateId" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
db,
);
await sql`CREATE TABLE "tag_asset" ("assetsId" uuid NOT NULL, "tagsId" uuid NOT NULL);`.execute(db); await sql`CREATE TABLE "tag_asset" ("assetsId" uuid NOT NULL, "tagsId" uuid NOT NULL);`.execute(db);
await sql`CREATE TABLE "tags_closure" ("id_ancestor" uuid NOT NULL, "id_descendant" uuid NOT NULL);`.execute(db); await sql`CREATE TABLE "tags_closure" ("id_ancestor" uuid NOT NULL, "id_descendant" uuid NOT NULL);`.execute(db);
await sql`CREATE TABLE "users_audit" ("userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "id" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(db); await sql`CREATE TABLE "users_audit" ("userId" uuid NOT NULL, "deletedAt" timestamp with time zone NOT NULL DEFAULT clock_timestamp(), "id" uuid NOT NULL DEFAULT immich_uuid_v7());`.execute(
await sql`CREATE TABLE "user_metadata" ("userId" uuid NOT NULL, "key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute(db); db,
await sql`CREATE TABLE "version_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "version" character varying NOT NULL);`.execute(db); );
await sql`CREATE TABLE "user_metadata" ("userId" uuid NOT NULL, "key" character varying NOT NULL, "value" jsonb NOT NULL);`.execute(
db,
);
await sql`CREATE TABLE "version_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" timestamp with time zone NOT NULL DEFAULT now(), "version" character varying NOT NULL);`.execute(
db,
);
await sql`ALTER TABLE "users" ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "users" ADD CONSTRAINT "PK_a3ffb1c0c8416b9fc6f907b7433" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "libraries" ADD CONSTRAINT "PK_505fedfcad00a09b3734b4223de" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "libraries" ADD CONSTRAINT "PK_505fedfcad00a09b3734b4223de" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "PK_74a27e7fcbd5852463d0af3034b" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "PK_74a27e7fcbd5852463d0af3034b" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "assets" ADD CONSTRAINT "PK_da96729a8b113377cfb6a62439c" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "assets" ADD CONSTRAINT "PK_da96729a8b113377cfb6a62439c" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "albums" ADD CONSTRAINT "PK_7f71c7b5bc7c87b8f94c9a93a00" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "albums" ADD CONSTRAINT "PK_7f71c7b5bc7c87b8f94c9a93a00" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "activity" ADD CONSTRAINT "PK_24625a1d6b1b089c8ae206fe467" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "activity" ADD CONSTRAINT "PK_24625a1d6b1b089c8ae206fe467" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "PK_c67bc36fa845fb7b18e0e398180" PRIMARY KEY ("albumsId", "assetsId");`.execute(db); await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "PK_c67bc36fa845fb7b18e0e398180" PRIMARY KEY ("albumsId", "assetsId");`.execute(
await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "PK_7df55657e0b2e8b626330a0ebc8" PRIMARY KEY ("albumsId", "usersId");`.execute(db); db,
);
await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "PK_7df55657e0b2e8b626330a0ebc8" PRIMARY KEY ("albumsId", "usersId");`.execute(
db,
);
await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "PK_5c8a79801b44bd27b79228e1dad" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "assets_audit" ADD CONSTRAINT "PK_99bd5c015f81a641927a32b4212" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "assets_audit" ADD CONSTRAINT "PK_99bd5c015f81a641927a32b4212" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "person" ADD CONSTRAINT "PK_5fdaf670315c4b7e70cce85daa3" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "person" ADD CONSTRAINT "PK_5fdaf670315c4b7e70cce85daa3" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "PK_6df76ab2eb6f5b57b7c2f1fc684" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "PK_6df76ab2eb6f5b57b7c2f1fc684" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "PK_c41dc3e9ef5e1c57ca5a08a0004" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "PK_c41dc3e9ef5e1c57ca5a08a0004" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "PK_420bec36fc02813bddf5c8b73d4" PRIMARY KEY ("assetId");`.execute(db); await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "PK_420bec36fc02813bddf5c8b73d4" PRIMARY KEY ("assetId");`.execute(
db,
);
await sql`ALTER TABLE "audit" ADD CONSTRAINT "PK_1d3d120ddaf7bc9b1ed68ed463a" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "audit" ADD CONSTRAINT "PK_1d3d120ddaf7bc9b1ed68ed463a" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "exif" ADD CONSTRAINT "PK_c0117fdbc50b917ef9067740c44" PRIMARY KEY ("assetId");`.execute(db); await sql`ALTER TABLE "exif" ADD CONSTRAINT "PK_c0117fdbc50b917ef9067740c44" PRIMARY KEY ("assetId");`.execute(db);
await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_pkey" PRIMARY KEY ("faceId");`.execute(db); await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_pkey" PRIMARY KEY ("faceId");`.execute(db);
await sql`ALTER TABLE "geodata_places" ADD CONSTRAINT "PK_c29918988912ef4036f3d7fbff4" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "geodata_places" ADD CONSTRAINT "PK_c29918988912ef4036f3d7fbff4" PRIMARY KEY ("id");`.execute(
db,
);
await sql`ALTER TABLE "memories" ADD CONSTRAINT "PK_aaa0692d9496fe827b0568612f8" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "memories" ADD CONSTRAINT "PK_aaa0692d9496fe827b0568612f8" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "PK_fcaf7112a013d1703c011c6793d" PRIMARY KEY ("memoriesId", "assetsId");`.execute(db); await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "PK_fcaf7112a013d1703c011c6793d" PRIMARY KEY ("memoriesId", "assetsId");`.execute(
db,
);
await sql`ALTER TABLE "move_history" ADD CONSTRAINT "PK_af608f132233acf123f2949678d" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "move_history" ADD CONSTRAINT "PK_af608f132233acf123f2949678d" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "naturalearth_countries" ADD CONSTRAINT "PK_21a6d86d1ab5d841648212e5353" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "naturalearth_countries" ADD CONSTRAINT "PK_21a6d86d1ab5d841648212e5353" PRIMARY KEY ("id");`.execute(
await sql`ALTER TABLE "partners_audit" ADD CONSTRAINT "PK_952b50217ff78198a7e380f0359" PRIMARY KEY ("id");`.execute(db); db,
await sql`ALTER TABLE "partners" ADD CONSTRAINT "PK_f1cc8f73d16b367f426261a8736" PRIMARY KEY ("sharedById", "sharedWithId");`.execute(db); );
await sql`ALTER TABLE "partners_audit" ADD CONSTRAINT "PK_952b50217ff78198a7e380f0359" PRIMARY KEY ("id");`.execute(
db,
);
await sql`ALTER TABLE "partners" ADD CONSTRAINT "PK_f1cc8f73d16b367f426261a8736" PRIMARY KEY ("sharedById", "sharedWithId");`.execute(
db,
);
await sql`ALTER TABLE "sessions" ADD CONSTRAINT "PK_48cb6b5c20faa63157b3c1baf7f" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "sessions" ADD CONSTRAINT "PK_48cb6b5c20faa63157b3c1baf7f" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "PK_642e2b0f619e4876e5f90a43465" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "PK_642e2b0f619e4876e5f90a43465" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "PK_9b4f3687f9b31d1e311336b05e3" PRIMARY KEY ("assetsId", "sharedLinksId");`.execute(db); await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "PK_9b4f3687f9b31d1e311336b05e3" PRIMARY KEY ("assetsId", "sharedLinksId");`.execute(
db,
);
await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_pkey" PRIMARY KEY ("assetId");`.execute(db); await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_pkey" PRIMARY KEY ("assetId");`.execute(db);
await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "PK_b846ab547a702863ef7cd9412fb" PRIMARY KEY ("sessionId", "type");`.execute(db); await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "PK_b846ab547a702863ef7cd9412fb" PRIMARY KEY ("sessionId", "type");`.execute(
await sql`ALTER TABLE "system_metadata" ADD CONSTRAINT "PK_fa94f6857470fb5b81ec6084465" PRIMARY KEY ("key");`.execute(db); db,
);
await sql`ALTER TABLE "system_metadata" ADD CONSTRAINT "PK_fa94f6857470fb5b81ec6084465" PRIMARY KEY ("key");`.execute(
db,
);
await sql`ALTER TABLE "tags" ADD CONSTRAINT "PK_e7dc17249a1148a1970748eda99" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "tags" ADD CONSTRAINT "PK_e7dc17249a1148a1970748eda99" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" PRIMARY KEY ("assetsId", "tagsId");`.execute(db); await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "PK_ef5346fe522b5fb3bc96454747e" PRIMARY KEY ("assetsId", "tagsId");`.execute(
await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "PK_eab38eb12a3ec6df8376c95477c" PRIMARY KEY ("id_ancestor", "id_descendant");`.execute(db); db,
);
await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "PK_eab38eb12a3ec6df8376c95477c" PRIMARY KEY ("id_ancestor", "id_descendant");`.execute(
db,
);
await sql`ALTER TABLE "users_audit" ADD CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180" PRIMARY KEY ("id");`.execute(db); await sql`ALTER TABLE "users_audit" ADD CONSTRAINT "PK_e9b2bdfd90e7eb5961091175180" PRIMARY KEY ("id");`.execute(db);
await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "PK_5931462150b3438cbc83277fe5a" PRIMARY KEY ("userId", "key");`.execute(db); await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "PK_5931462150b3438cbc83277fe5a" PRIMARY KEY ("userId", "key");`.execute(
await sql`ALTER TABLE "version_history" ADD CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" PRIMARY KEY ("id");`.execute(db); db,
await sql`ALTER TABLE "libraries" ADD CONSTRAINT "FK_0f6fc2fb195f24d19b0fb0d57c1" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_91704e101438fd0653f582426dc" FOREIGN KEY ("primaryAssetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;`.execute(db); await sql`ALTER TABLE "version_history" ADD CONSTRAINT "PK_5db259cbb09ce82c0d13cfd1b23" PRIMARY KEY ("id");`.execute(
await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); await sql`ALTER TABLE "libraries" ADD CONSTRAINT "FK_0f6fc2fb195f24d19b0fb0d57c1" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c" FOREIGN KEY ("libraryId") REFERENCES "libraries" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); );
await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_91704e101438fd0653f582426dc" FOREIGN KEY ("primaryAssetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE NO ACTION;`.execute(
await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_05895aa505a670300d4816debce" FOREIGN KEY ("albumThumbnailAssetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); db,
await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_1af8519996fbfb3684b58df280b" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "FK_c05079e542fd74de3b5ecb5c1c8" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_8091ea76b12338cb4428d33d782" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_e590fa396c6898fcd4a50e40927" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_2c5ac0d6fb58b238fd2068de67d" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_427c350ad49bd3935a50baab737" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06" FOREIGN KEY ("usersId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_16294b83fa8c0149719a1f631ef" FOREIGN KEY ("livePhotoVideoId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(
await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_5527cc99f530a547093f9e577b6" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE SET NULL;`.execute(db); );
await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_02a43fd0b3c50fb6d7f0cb7282c" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c" FOREIGN KEY ("libraryId") REFERENCES "libraries" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_95ad7106dd7b484275443f580f9" FOREIGN KEY ("personId") REFERENCES "person" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(db); db,
await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "FK_420bec36fc02813bddf5c8b73d4" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(
await sql`ALTER TABLE "exif" ADD CONSTRAINT "FK_c0117fdbc50b917ef9067740c44" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_faceId_fkey" FOREIGN KEY ("faceId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "memories" ADD CONSTRAINT "FK_575842846f0c28fa5da46c99b19" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e" FOREIGN KEY ("memoriesId") REFERENCES "memories" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_7e077a8b70b3530138610ff5e04" FOREIGN KEY ("sharedById") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "albums" ADD CONSTRAINT "FK_05895aa505a670300d4816debce" FOREIGN KEY ("albumThumbnailAssetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(
await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_d7e875c6c60e661723dbf372fd3" FOREIGN KEY ("sharedWithId") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "sessions" ADD CONSTRAINT "FK_57de40bc620f456c7311aa3a1e6" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_1af8519996fbfb3684b58df280b" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab" FOREIGN KEY ("sharedLinksId") REFERENCES "shared_links" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "FK_d8ddd9d687816cc490432b3d4bc" FOREIGN KEY ("sessionId") REFERENCES "sessions" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "activity" ADD CONSTRAINT "FK_8091ea76b12338cb4428d33d782" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99" FOREIGN KEY ("parentId") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_e590fa396c6898fcd4a50e40927" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c" FOREIGN KEY ("id_ancestor") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); db,
await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1" FOREIGN KEY ("id_descendant") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(db); );
await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "FK_6afb43681a21cf7815932bc38ac" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(db); await sql`ALTER TABLE "albums_assets_assets" ADD CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_427c350ad49bd3935a50baab737" FOREIGN KEY ("albumsId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "albums_shared_users_users" ADD CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06" FOREIGN KEY ("usersId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "api_keys" ADD CONSTRAINT "FK_6c2e267ae764a9413b863a29342" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_5527cc99f530a547093f9e577b6" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "person" ADD CONSTRAINT "FK_2bbabe31656b6778c6b87b61023" FOREIGN KEY ("faceAssetId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE SET NULL;`.execute(
db,
);
await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_02a43fd0b3c50fb6d7f0cb7282c" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "asset_faces" ADD CONSTRAINT "FK_95ad7106dd7b484275443f580f9" FOREIGN KEY ("personId") REFERENCES "person" ("id") ON UPDATE CASCADE ON DELETE SET NULL;`.execute(
db,
);
await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "asset_job_status" ADD CONSTRAINT "FK_420bec36fc02813bddf5c8b73d4" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "exif" ADD CONSTRAINT "FK_c0117fdbc50b917ef9067740c44" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "face_search" ADD CONSTRAINT "face_search_faceId_fkey" FOREIGN KEY ("faceId") REFERENCES "asset_faces" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "memories" ADD CONSTRAINT "FK_575842846f0c28fa5da46c99b19" FOREIGN KEY ("ownerId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e" FOREIGN KEY ("memoriesId") REFERENCES "memories" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "memories_assets_assets" ADD CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_7e077a8b70b3530138610ff5e04" FOREIGN KEY ("sharedById") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "partners" ADD CONSTRAINT "FK_d7e875c6c60e661723dbf372fd3" FOREIGN KEY ("sharedWithId") REFERENCES "users" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "sessions" ADD CONSTRAINT "FK_57de40bc620f456c7311aa3a1e6" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_66fe3837414c5a9f1c33ca49340" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66" FOREIGN KEY ("albumId") REFERENCES "albums" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "shared_link__asset" ADD CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab" FOREIGN KEY ("sharedLinksId") REFERENCES "shared_links" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "smart_search" ADD CONSTRAINT "smart_search_assetId_fkey" FOREIGN KEY ("assetId") REFERENCES "assets" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "session_sync_checkpoints" ADD CONSTRAINT "FK_d8ddd9d687816cc490432b3d4bc" FOREIGN KEY ("sessionId") REFERENCES "sessions" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "tags" ADD CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99" FOREIGN KEY ("parentId") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c" FOREIGN KEY ("id_ancestor") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1" FOREIGN KEY ("id_descendant") REFERENCES "tags" ("id") ON UPDATE NO ACTION ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "user_metadata" ADD CONSTRAINT "FK_6afb43681a21cf7815932bc38ac" FOREIGN KEY ("userId") REFERENCES "users" ("id") ON UPDATE CASCADE ON DELETE CASCADE;`.execute(
db,
);
await sql`ALTER TABLE "users" ADD CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email");`.execute(db); await sql`ALTER TABLE "users" ADD CONSTRAINT "UQ_97672ac88f789774dd47f7c8be3" UNIQUE ("email");`.execute(db);
await sql`ALTER TABLE "users" ADD CONSTRAINT "UQ_b309cf34fa58137c416b32cea3a" UNIQUE ("storageLabel");`.execute(db); await sql`ALTER TABLE "users" ADD CONSTRAINT "UQ_b309cf34fa58137c416b32cea3a" UNIQUE ("storageLabel");`.execute(db);
await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "REL_91704e101438fd0653f582426d" UNIQUE ("primaryAssetId");`.execute(db); await sql`ALTER TABLE "asset_stack" ADD CONSTRAINT "REL_91704e101438fd0653f582426d" UNIQUE ("primaryAssetId");`.execute(
db,
);
await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "UQ_assetId_type" UNIQUE ("assetId", "type");`.execute(db); await sql`ALTER TABLE "asset_files" ADD CONSTRAINT "UQ_assetId_type" UNIQUE ("assetId", "type");`.execute(db);
await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_newPath" UNIQUE ("newPath");`.execute(db); await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_newPath" UNIQUE ("newPath");`.execute(db);
await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_entityId_pathType" UNIQUE ("entityId", "pathType");`.execute(db); await sql`ALTER TABLE "move_history" ADD CONSTRAINT "UQ_entityId_pathType" UNIQUE ("entityId", "pathType");`.execute(
db,
);
await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "UQ_sharedlink_key" UNIQUE ("key");`.execute(db); await sql`ALTER TABLE "shared_links" ADD CONSTRAINT "UQ_sharedlink_key" UNIQUE ("key");`.execute(db);
await sql`ALTER TABLE "tags" ADD CONSTRAINT "UQ_79d6f16e52bb2c7130375246793" UNIQUE ("userId", "value");`.execute(db); await sql`ALTER TABLE "tags" ADD CONSTRAINT "UQ_79d6f16e52bb2c7130375246793" UNIQUE ("userId", "value");`.execute(db);
await sql`ALTER TABLE "activity" ADD CONSTRAINT "CHK_2ab1e70f113f450eb40c1e3ec8" CHECK (("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false));`.execute(db); await sql`ALTER TABLE "activity" ADD CONSTRAINT "CHK_2ab1e70f113f450eb40c1e3ec8" CHECK (("comment" IS NULL AND "isLiked" = true) OR ("comment" IS NOT NULL AND "isLiked" = false));`.execute(
await sql`ALTER TABLE "person" ADD CONSTRAINT "CHK_b0f82b0ed662bfc24fbb58bb45" CHECK ("birthDate" <= CURRENT_DATE);`.execute(db); db,
);
await sql`ALTER TABLE "person" ADD CONSTRAINT "CHK_b0f82b0ed662bfc24fbb58bb45" CHECK ("birthDate" <= CURRENT_DATE);`.execute(
db,
);
await sql`CREATE INDEX "IDX_users_updated_at_asc_id_asc" ON "users" ("updatedAt", "id")`.execute(db); await sql`CREATE INDEX "IDX_users_updated_at_asc_id_asc" ON "users" ("updatedAt", "id")`.execute(db);
await sql`CREATE INDEX "IDX_users_update_id" ON "users" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_users_update_id" ON "users" ("updateId")`.execute(db);
await sql`CREATE INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c" ON "libraries" ("ownerId")`.execute(db); await sql`CREATE INDEX "IDX_0f6fc2fb195f24d19b0fb0d57c" ON "libraries" ("ownerId")`.execute(db);
await sql`CREATE INDEX "IDX_libraries_update_id" ON "libraries" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_libraries_update_id" ON "libraries" ("updateId")`.execute(db);
await sql`CREATE INDEX "IDX_91704e101438fd0653f582426d" ON "asset_stack" ("primaryAssetId")`.execute(db); await sql`CREATE INDEX "IDX_91704e101438fd0653f582426d" ON "asset_stack" ("primaryAssetId")`.execute(db);
await sql`CREATE INDEX "IDX_c05079e542fd74de3b5ecb5c1c" ON "asset_stack" ("ownerId")`.execute(db); await sql`CREATE INDEX "IDX_c05079e542fd74de3b5ecb5c1c" ON "asset_stack" ("ownerId")`.execute(db);
await sql`CREATE INDEX "idx_originalfilename_trigram" ON "assets" USING gin (f_unaccent("originalFileName") gin_trgm_ops)`.execute(db); await sql`CREATE INDEX "idx_originalfilename_trigram" ON "assets" USING gin (f_unaccent("originalFileName") gin_trgm_ops)`.execute(
db,
);
await sql`CREATE INDEX "IDX_asset_id_stackId" ON "assets" ("id", "stackId")`.execute(db); await sql`CREATE INDEX "IDX_asset_id_stackId" ON "assets" ("id", "stackId")`.execute(db);
await sql`CREATE INDEX "IDX_originalPath_libraryId" ON "assets" ("originalPath", "libraryId")`.execute(db); await sql`CREATE INDEX "IDX_originalPath_libraryId" ON "assets" ("originalPath", "libraryId")`.execute(db);
await sql`CREATE INDEX "idx_local_date_time_month" ON "assets" ((date_trunc('MONTH'::text, ("localDateTime" AT TIME ZONE 'UTC'::text)) AT TIME ZONE 'UTC'::text))`.execute(db); await sql`CREATE INDEX "idx_local_date_time_month" ON "assets" ((date_trunc('MONTH'::text, ("localDateTime" AT TIME ZONE 'UTC'::text)) AT TIME ZONE 'UTC'::text))`.execute(
db,
);
await sql`CREATE INDEX "idx_local_date_time" ON "assets" ((("localDateTime" at time zone 'UTC')::date))`.execute(db); await sql`CREATE INDEX "idx_local_date_time" ON "assets" ((("localDateTime" at time zone 'UTC')::date))`.execute(db);
await sql`CREATE UNIQUE INDEX "UQ_assets_owner_library_checksum" ON "assets" ("ownerId", "libraryId", "checksum") WHERE ("libraryId" IS NOT NULL)`.execute(db); await sql`CREATE UNIQUE INDEX "UQ_assets_owner_library_checksum" ON "assets" ("ownerId", "libraryId", "checksum") WHERE ("libraryId" IS NOT NULL)`.execute(
await sql`CREATE UNIQUE INDEX "UQ_assets_owner_checksum" ON "assets" ("ownerId", "checksum") WHERE ("libraryId" IS NULL)`.execute(db); db,
);
await sql`CREATE UNIQUE INDEX "UQ_assets_owner_checksum" ON "assets" ("ownerId", "checksum") WHERE ("libraryId" IS NULL)`.execute(
db,
);
await sql`CREATE INDEX "IDX_2c5ac0d6fb58b238fd2068de67" ON "assets" ("ownerId")`.execute(db); await sql`CREATE INDEX "IDX_2c5ac0d6fb58b238fd2068de67" ON "assets" ("ownerId")`.execute(db);
await sql`CREATE INDEX "idx_asset_file_created_at" ON "assets" ("fileCreatedAt")`.execute(db); await sql`CREATE INDEX "idx_asset_file_created_at" ON "assets" ("fileCreatedAt")`.execute(db);
await sql`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum")`.execute(db); await sql`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum")`.execute(db);
@ -266,7 +456,9 @@ export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE INDEX "IDX_b22c53f35ef20c28c21637c85f" ON "albums" ("ownerId")`.execute(db); await sql`CREATE INDEX "IDX_b22c53f35ef20c28c21637c85f" ON "albums" ("ownerId")`.execute(db);
await sql`CREATE INDEX "IDX_05895aa505a670300d4816debc" ON "albums" ("albumThumbnailAssetId")`.execute(db); await sql`CREATE INDEX "IDX_05895aa505a670300d4816debc" ON "albums" ("albumThumbnailAssetId")`.execute(db);
await sql`CREATE INDEX "IDX_albums_update_id" ON "albums" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_albums_update_id" ON "albums" ("updateId")`.execute(db);
await sql`CREATE UNIQUE INDEX "IDX_activity_like" ON "activity" ("assetId", "userId", "albumId") WHERE ("isLiked" = true)`.execute(db); await sql`CREATE UNIQUE INDEX "IDX_activity_like" ON "activity" ("assetId", "userId", "albumId") WHERE ("isLiked" = true)`.execute(
db,
);
await sql`CREATE INDEX "IDX_1af8519996fbfb3684b58df280" ON "activity" ("albumId")`.execute(db); await sql`CREATE INDEX "IDX_1af8519996fbfb3684b58df280" ON "activity" ("albumId")`.execute(db);
await sql`CREATE INDEX "IDX_3571467bcbe021f66e2bdce96e" ON "activity" ("userId")`.execute(db); await sql`CREATE INDEX "IDX_3571467bcbe021f66e2bdce96e" ON "activity" ("userId")`.execute(db);
await sql`CREATE INDEX "IDX_8091ea76b12338cb4428d33d78" ON "activity" ("assetId")`.execute(db); await sql`CREATE INDEX "IDX_8091ea76b12338cb4428d33d78" ON "activity" ("assetId")`.execute(db);
@ -295,11 +487,21 @@ export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE INDEX "IDX_auto_stack_id" ON "exif" ("autoStackId")`.execute(db); await sql`CREATE INDEX "IDX_auto_stack_id" ON "exif" ("autoStackId")`.execute(db);
await sql`CREATE INDEX "IDX_asset_exif_update_id" ON "exif" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_asset_exif_update_id" ON "exif" ("updateId")`.execute(db);
await sql.raw(vectorIndexQuery({ vectorExtension, table: 'face_search', indexName: 'face_index' })).execute(db); await sql.raw(vectorIndexQuery({ vectorExtension, table: 'face_search', indexName: 'face_index' })).execute(db);
await sql`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" (ll_to_earth_public(latitude, longitude))`.execute(db); await sql`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" (ll_to_earth_public(latitude, longitude))`.execute(
await sql`CREATE INDEX "idx_geodata_places_name" ON "geodata_places" USING gin (f_unaccent("name") gin_trgm_ops)`.execute(db); db,
await sql`CREATE INDEX "idx_geodata_places_admin2_name" ON "geodata_places" USING gin (f_unaccent("admin2Name") gin_trgm_ops)`.execute(db); );
await sql`CREATE INDEX "idx_geodata_places_admin1_name" ON "geodata_places" USING gin (f_unaccent("admin1Name") gin_trgm_ops)`.execute(db); await sql`CREATE INDEX "idx_geodata_places_name" ON "geodata_places" USING gin (f_unaccent("name") gin_trgm_ops)`.execute(
await sql`CREATE INDEX "idx_geodata_places_alternate_names" ON "geodata_places" USING gin (f_unaccent("alternateNames") gin_trgm_ops)`.execute(db); db,
);
await sql`CREATE INDEX "idx_geodata_places_admin2_name" ON "geodata_places" USING gin (f_unaccent("admin2Name") gin_trgm_ops)`.execute(
db,
);
await sql`CREATE INDEX "idx_geodata_places_admin1_name" ON "geodata_places" USING gin (f_unaccent("admin1Name") gin_trgm_ops)`.execute(
db,
);
await sql`CREATE INDEX "idx_geodata_places_alternate_names" ON "geodata_places" USING gin (f_unaccent("alternateNames") gin_trgm_ops)`.execute(
db,
);
await sql`CREATE INDEX "IDX_575842846f0c28fa5da46c99b1" ON "memories" ("ownerId")`.execute(db); await sql`CREATE INDEX "IDX_575842846f0c28fa5da46c99b1" ON "memories" ("ownerId")`.execute(db);
await sql`CREATE INDEX "IDX_memories_update_id" ON "memories" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_memories_update_id" ON "memories" ("updateId")`.execute(db);
await sql`CREATE INDEX "IDX_984e5c9ab1f04d34538cd32334" ON "memories_assets_assets" ("memoriesId")`.execute(db); await sql`CREATE INDEX "IDX_984e5c9ab1f04d34538cd32334" ON "memories_assets_assets" ("memoriesId")`.execute(db);
@ -319,7 +521,9 @@ export async function up(db: Kysely<any>): Promise<void> {
await sql`CREATE INDEX "IDX_c9fab4aa97ffd1b034f3d6581a" ON "shared_link__asset" ("sharedLinksId")`.execute(db); await sql`CREATE INDEX "IDX_c9fab4aa97ffd1b034f3d6581a" ON "shared_link__asset" ("sharedLinksId")`.execute(db);
await sql.raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' })).execute(db); await sql.raw(vectorIndexQuery({ vectorExtension, table: 'smart_search', indexName: 'clip_index' })).execute(db);
await sql`CREATE INDEX "IDX_d8ddd9d687816cc490432b3d4b" ON "session_sync_checkpoints" ("sessionId")`.execute(db); await sql`CREATE INDEX "IDX_d8ddd9d687816cc490432b3d4b" ON "session_sync_checkpoints" ("sessionId")`.execute(db);
await sql`CREATE INDEX "IDX_session_sync_checkpoints_update_id" ON "session_sync_checkpoints" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_session_sync_checkpoints_update_id" ON "session_sync_checkpoints" ("updateId")`.execute(
db,
);
await sql`CREATE INDEX "IDX_92e67dc508c705dd66c9461557" ON "tags" ("userId")`.execute(db); await sql`CREATE INDEX "IDX_92e67dc508c705dd66c9461557" ON "tags" ("userId")`.execute(db);
await sql`CREATE INDEX "IDX_9f9590cc11561f1f48ff034ef9" ON "tags" ("parentId")`.execute(db); await sql`CREATE INDEX "IDX_9f9590cc11561f1f48ff034ef9" ON "tags" ("parentId")`.execute(db);
await sql`CREATE INDEX "IDX_tags_update_id" ON "tags" ("updateId")`.execute(db); await sql`CREATE INDEX "IDX_tags_update_id" ON "tags" ("updateId")`.execute(db);
@ -407,5 +611,5 @@ export async function up(db: Kysely<any>): Promise<void> {
} }
export async function down(): Promise<void> { export async function down(): Promise<void> {
// not implemented // not implemented
} }

View File

@ -2,11 +2,11 @@ import { Kysely, sql } from 'kysely';
import { UserMetadataKey } from 'src/enum'; import { UserMetadataKey } from 'src/enum';
export async function up(db: Kysely<any>): Promise<void> { export async function up(db: Kysely<any>): Promise<void> {
await sql`INSERT INTO user_metadata SELECT id, ${UserMetadataKey.ONBOARDING}, '{"isOnboarded": true}' FROM users await sql`INSERT INTO user_metadata SELECT id, ${UserMetadataKey.Onboarding}, '{"isOnboarded": true}' FROM users
ON CONFLICT ("userId", key) DO NOTHING ON CONFLICT ("userId", key) DO NOTHING
`.execute(db); `.execute(db);
} }
export async function down(db: Kysely<any>): Promise<void> { export async function down(db: Kysely<any>): Promise<void> {
await sql`DELETE FROM user_metadata WHERE key = ${UserMetadataKey.ONBOARDING}`.execute(db); await sql`DELETE FROM user_metadata WHERE key = ${UserMetadataKey.Onboarding}`.execute(db);
} }

View File

@ -47,7 +47,7 @@ export class AlbumUserTable {
}) })
usersId!: string; usersId!: string;
@Column({ type: 'character varying', default: AlbumUserRole.EDITOR }) @Column({ type: 'character varying', default: AlbumUserRole.Editor })
role!: Generated<AlbumUserRole>; role!: Generated<AlbumUserRole>;
@CreateIdColumn({ index: true }) @CreateIdColumn({ index: true })

View File

@ -57,7 +57,7 @@ export class AlbumTable {
@Column({ type: 'boolean', default: true }) @Column({ type: 'boolean', default: true })
isActivityEnabled!: Generated<boolean>; isActivityEnabled!: Generated<boolean>;
@Column({ default: AssetOrder.DESC }) @Column({ default: AssetOrder.Desc })
order!: Generated<AssetOrder>; order!: Generated<AssetOrder>;
@UpdateIdColumn({ index: true }) @UpdateIdColumn({ index: true })

View File

@ -56,7 +56,7 @@ export class AssetFaceTable {
@Column({ default: 0, type: 'integer' }) @Column({ default: 0, type: 'integer' })
boundingBoxY2!: Generated<number>; boundingBoxY2!: Generated<number>;
@Column({ default: SourceType.MACHINE_LEARNING, enum: asset_face_source_type }) @Column({ default: SourceType.MachineLearning, enum: asset_face_source_type })
sourceType!: Generated<SourceType>; sourceType!: Generated<SourceType>;
@DeleteDateColumn() @DeleteDateColumn()

View File

@ -132,12 +132,12 @@ export class AssetTable {
@Column({ type: 'uuid', nullable: true, index: true }) @Column({ type: 'uuid', nullable: true, index: true })
duplicateId!: string | null; duplicateId!: string | null;
@Column({ enum: assets_status_enum, default: AssetStatus.ACTIVE }) @Column({ enum: assets_status_enum, default: AssetStatus.Active })
status!: Generated<AssetStatus>; status!: Generated<AssetStatus>;
@UpdateIdColumn({ index: true }) @UpdateIdColumn({ index: true })
updateId!: Generated<string>; updateId!: Generated<string>;
@Column({ enum: asset_visibility_enum, default: AssetVisibility.TIMELINE }) @Column({ enum: asset_visibility_enum, default: AssetVisibility.Timeline })
visibility!: Generated<AssetVisibility>; visibility!: Generated<AssetVisibility>;
} }

View File

@ -73,7 +73,7 @@ export class UserTable {
@Column({ type: 'bigint', default: 0 }) @Column({ type: 'bigint', default: 0 })
quotaUsageInBytes!: Generated<ColumnType<number>>; quotaUsageInBytes!: Generated<ColumnType<number>>;
@Column({ type: 'character varying', default: UserStatus.ACTIVE }) @Column({ type: 'character varying', default: UserStatus.Active })
status!: Generated<UserStatus>; status!: Generated<UserStatus>;
@Column({ type: 'timestamp with time zone', default: () => 'now()' }) @Column({ type: 'timestamp with time zone', default: () => 'now()' })

View File

@ -18,7 +18,7 @@ import { BaseService } from 'src/services/base.service';
@Injectable() @Injectable()
export class ActivityService extends BaseService { export class ActivityService extends BaseService {
async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> { async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise<ActivityResponseDto[]> {
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] });
const activities = await this.activityRepository.search({ const activities = await this.activityRepository.search({
userId: dto.userId, userId: dto.userId,
albumId: dto.albumId, albumId: dto.albumId,
@ -30,12 +30,12 @@ export class ActivityService extends BaseService {
} }
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> { async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] });
return await this.activityRepository.getStatistics({ albumId: dto.albumId, assetId: dto.assetId }); return await this.activityRepository.getStatistics({ albumId: dto.albumId, assetId: dto.assetId });
} }
async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> { async create(auth: AuthDto, dto: ActivityCreateDto): Promise<MaybeDuplicate<ActivityResponseDto>> {
await this.requireAccess({ auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] }); await this.requireAccess({ auth, permission: Permission.ActivityCreate, ids: [dto.albumId] });
const common = { const common = {
userId: auth.user.id, userId: auth.user.id,
@ -69,7 +69,7 @@ export class ActivityService extends BaseService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
await this.requireAccess({ auth, permission: Permission.ACTIVITY_DELETE, ids: [id] }); await this.requireAccess({ auth, permission: Permission.ActivityDelete, ids: [id] });
await this.activityRepository.delete(id); await this.activityRepository.delete(id);
} }
} }

View File

@ -146,7 +146,7 @@ describe(AlbumService.name, () => {
await sut.create(authStub.admin, { await sut.create(authStub.admin, {
albumName: 'Empty album', albumName: 'Empty album',
albumUsers: [{ userId: 'user-id', role: AlbumUserRole.EDITOR }], albumUsers: [{ userId: 'user-id', role: AlbumUserRole.Editor }],
description: '', description: '',
assetIds: ['123'], assetIds: ['123'],
}); });
@ -160,7 +160,7 @@ describe(AlbumService.name, () => {
albumThumbnailAssetId: '123', albumThumbnailAssetId: '123',
}, },
['123'], ['123'],
[{ userId: 'user-id', role: AlbumUserRole.EDITOR }], [{ userId: 'user-id', role: AlbumUserRole.Editor }],
); );
expect(mocks.user.get).toHaveBeenCalledWith('user-id', {}); expect(mocks.user.get).toHaveBeenCalledWith('user-id', {});
@ -177,10 +177,10 @@ describe(AlbumService.name, () => {
mocks.user.get.mockResolvedValue(userStub.user1); mocks.user.get.mockResolvedValue(userStub.user1);
mocks.user.getMetadata.mockResolvedValue([ mocks.user.getMetadata.mockResolvedValue([
{ {
key: UserMetadataKey.PREFERENCES, key: UserMetadataKey.Preferences,
value: { value: {
albums: { albums: {
defaultAssetOrder: AssetOrder.ASC, defaultAssetOrder: AssetOrder.Asc,
}, },
}, },
}, },
@ -189,7 +189,7 @@ describe(AlbumService.name, () => {
await sut.create(authStub.admin, { await sut.create(authStub.admin, {
albumName: 'Empty album', albumName: 'Empty album',
albumUsers: [{ userId: 'user-id', role: AlbumUserRole.EDITOR }], albumUsers: [{ userId: 'user-id', role: AlbumUserRole.Editor }],
description: '', description: '',
assetIds: ['123'], assetIds: ['123'],
}); });
@ -203,7 +203,7 @@ describe(AlbumService.name, () => {
albumThumbnailAssetId: '123', albumThumbnailAssetId: '123',
}, },
['123'], ['123'],
[{ userId: 'user-id', role: AlbumUserRole.EDITOR }], [{ userId: 'user-id', role: AlbumUserRole.Editor }],
); );
expect(mocks.user.get).toHaveBeenCalledWith('user-id', {}); expect(mocks.user.get).toHaveBeenCalledWith('user-id', {});
@ -220,7 +220,7 @@ describe(AlbumService.name, () => {
await expect( await expect(
sut.create(authStub.admin, { sut.create(authStub.admin, {
albumName: 'Empty album', albumName: 'Empty album',
albumUsers: [{ userId: 'user-3', role: AlbumUserRole.EDITOR }], albumUsers: [{ userId: 'user-3', role: AlbumUserRole.Editor }],
}), }),
).rejects.toBeInstanceOf(BadRequestException); ).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.user.get).toHaveBeenCalledWith('user-3', {}); expect(mocks.user.get).toHaveBeenCalledWith('user-3', {});
@ -262,7 +262,7 @@ describe(AlbumService.name, () => {
await expect( await expect(
sut.create(authStub.admin, { sut.create(authStub.admin, {
albumName: 'Empty album', albumName: 'Empty album',
albumUsers: [{ userId: userStub.admin.id, role: AlbumUserRole.EDITOR }], albumUsers: [{ userId: userStub.admin.id, role: AlbumUserRole.Editor }],
}), }),
).rejects.toBeInstanceOf(BadRequestException); ).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.album.create).not.toHaveBeenCalled(); expect(mocks.album.create).not.toHaveBeenCalled();
@ -404,7 +404,7 @@ describe(AlbumService.name, () => {
mocks.albumUser.create.mockResolvedValue({ mocks.albumUser.create.mockResolvedValue({
usersId: userStub.user2.id, usersId: userStub.user2.id,
albumsId: albumStub.sharedWithAdmin.id, albumsId: albumStub.sharedWithAdmin.id,
role: AlbumUserRole.EDITOR, role: AlbumUserRole.Editor,
}); });
await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, { await sut.addUsers(authStub.user1, albumStub.sharedWithAdmin.id, {
albumUsers: [{ userId: authStub.user2.user.id }], albumUsers: [{ userId: authStub.user2.user.id }],
@ -512,11 +512,11 @@ describe(AlbumService.name, () => {
mocks.albumUser.update.mockResolvedValue(null as any); mocks.albumUser.update.mockResolvedValue(null as any);
await sut.updateUser(authStub.user1, albumStub.sharedWithAdmin.id, userStub.admin.id, { await sut.updateUser(authStub.user1, albumStub.sharedWithAdmin.id, userStub.admin.id, {
role: AlbumUserRole.EDITOR, role: AlbumUserRole.Editor,
}); });
expect(mocks.albumUser.update).toHaveBeenCalledWith( expect(mocks.albumUser.update).toHaveBeenCalledWith(
{ albumsId: albumStub.sharedWithAdmin.id, usersId: userStub.admin.id }, { albumsId: albumStub.sharedWithAdmin.id, usersId: userStub.admin.id },
{ role: AlbumUserRole.EDITOR }, { role: AlbumUserRole.Editor },
); );
}); });
}); });
@ -585,7 +585,7 @@ describe(AlbumService.name, () => {
expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
authStub.user1.user.id, authStub.user1.user.id,
new Set(['album-123']), new Set(['album-123']),
AlbumUserRole.VIEWER, AlbumUserRole.Viewer,
); );
}); });
@ -596,7 +596,7 @@ describe(AlbumService.name, () => {
expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith( expect(mocks.access.album.checkSharedAlbumAccess).toHaveBeenCalledWith(
authStub.admin.user.id, authStub.admin.user.id,
new Set(['album-123']), new Set(['album-123']),
AlbumUserRole.VIEWER, AlbumUserRole.Viewer,
); );
}); });
}); });

View File

@ -71,7 +71,7 @@ export class AlbumService extends BaseService {
} }
async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> { async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise<AlbumResponseDto> {
await this.requireAccess({ auth, permission: Permission.ALBUM_READ, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [id] });
await this.albumRepository.updateThumbnails(); await this.albumRepository.updateThumbnails();
const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets;
const album = await this.findOrFail(id, { withAssets }); const album = await this.findOrFail(id, { withAssets });
@ -102,7 +102,7 @@ export class AlbumService extends BaseService {
const allowedAssetIdsSet = await this.checkAccess({ const allowedAssetIdsSet = await this.checkAccess({
auth, auth,
permission: Permission.ASSET_SHARE, permission: Permission.AssetShare,
ids: dto.assetIds || [], ids: dto.assetIds || [],
}); });
const assetIds = [...allowedAssetIdsSet].map((id) => id); const assetIds = [...allowedAssetIdsSet].map((id) => id);
@ -129,7 +129,7 @@ export class AlbumService extends BaseService {
} }
async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> { async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise<AlbumResponseDto> {
await this.requireAccess({ auth, permission: Permission.ALBUM_UPDATE, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AlbumUpdate, ids: [id] });
const album = await this.findOrFail(id, { withAssets: true }); const album = await this.findOrFail(id, { withAssets: true });
@ -152,13 +152,13 @@ export class AlbumService extends BaseService {
} }
async delete(auth: AuthDto, id: string): Promise<void> { async delete(auth: AuthDto, id: string): Promise<void> {
await this.requireAccess({ auth, permission: Permission.ALBUM_DELETE, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AlbumDelete, ids: [id] });
await this.albumRepository.delete(id); await this.albumRepository.delete(id);
} }
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
const album = await this.findOrFail(id, { withAssets: false }); const album = await this.findOrFail(id, { withAssets: false });
await this.requireAccess({ auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AlbumAddAsset, ids: [id] });
const results = await addAssets( const results = await addAssets(
auth, auth,
@ -187,13 +187,13 @@ export class AlbumService extends BaseService {
} }
async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> { async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {
await this.requireAccess({ auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AlbumRemoveAsset, ids: [id] });
const album = await this.findOrFail(id, { withAssets: false }); const album = await this.findOrFail(id, { withAssets: false });
const results = await removeAssets( const results = await removeAssets(
auth, auth,
{ access: this.accessRepository, bulk: this.albumRepository }, { access: this.accessRepository, bulk: this.albumRepository },
{ parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE }, { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.AlbumDelete },
); );
const removedIds = results.filter(({ success }) => success).map(({ id }) => id); const removedIds = results.filter(({ success }) => success).map(({ id }) => id);
@ -205,7 +205,7 @@ export class AlbumService extends BaseService {
} }
async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> { async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise<AlbumResponseDto> {
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });
const album = await this.findOrFail(id, { withAssets: false }); const album = await this.findOrFail(id, { withAssets: false });
@ -249,14 +249,14 @@ export class AlbumService extends BaseService {
// non-admin can remove themselves // non-admin can remove themselves
if (auth.user.id !== userId) { if (auth.user.id !== userId) {
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });
} }
await this.albumUserRepository.delete({ albumsId: id, usersId: userId }); await this.albumUserRepository.delete({ albumsId: id, usersId: userId });
} }
async updateUser(auth: AuthDto, id: string, userId: string, dto: UpdateAlbumUserDto): Promise<void> { async updateUser(auth: AuthDto, id: string, userId: string, dto: UpdateAlbumUserDto): Promise<void> {
await this.requireAccess({ auth, permission: Permission.ALBUM_SHARE, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AlbumShare, ids: [id] });
await this.albumUserRepository.update({ albumsId: id, usersId: userId }, { role: dto.role }); await this.albumUserRepository.update({ albumsId: id, usersId: userId }, { role: dto.role });
} }

View File

@ -15,7 +15,7 @@ describe(ApiKeyService.name, () => {
describe('create', () => { describe('create', () => {
it('should create a new key', async () => { it('should create a new key', async () => {
const auth = factory.auth(); const auth = factory.auth();
const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.ALL] }); const apiKey = factory.apiKey({ userId: auth.user.id, permissions: [Permission.All] });
const key = 'super-secret'; const key = 'super-secret';
mocks.crypto.randomBytesAsText.mockReturnValue(key); mocks.crypto.randomBytesAsText.mockReturnValue(key);
@ -41,12 +41,12 @@ describe(ApiKeyService.name, () => {
mocks.crypto.randomBytesAsText.mockReturnValue(key); mocks.crypto.randomBytesAsText.mockReturnValue(key);
mocks.apiKey.create.mockResolvedValue(apiKey); mocks.apiKey.create.mockResolvedValue(apiKey);
await sut.create(auth, { permissions: [Permission.ALL] }); await sut.create(auth, { permissions: [Permission.All] });
expect(mocks.apiKey.create).toHaveBeenCalledWith({ expect(mocks.apiKey.create).toHaveBeenCalledWith({
key: 'super-secret (hashed)', key: 'super-secret (hashed)',
name: 'API Key', name: 'API Key',
permissions: [Permission.ALL], permissions: [Permission.All],
userId: auth.user.id, userId: auth.user.id,
}); });
expect(mocks.crypto.randomBytesAsText).toHaveBeenCalled(); expect(mocks.crypto.randomBytesAsText).toHaveBeenCalled();
@ -54,9 +54,9 @@ describe(ApiKeyService.name, () => {
}); });
it('should throw an error if the api key does not have sufficient permissions', async () => { it('should throw an error if the api key does not have sufficient permissions', async () => {
const auth = factory.auth({ apiKey: { permissions: [Permission.ASSET_READ] } }); const auth = factory.auth({ apiKey: { permissions: [Permission.AssetRead] } });
await expect(sut.create(auth, { permissions: [Permission.ASSET_UPDATE] })).rejects.toBeInstanceOf( await expect(sut.create(auth, { permissions: [Permission.AssetUpdate] })).rejects.toBeInstanceOf(
BadRequestException, BadRequestException,
); );
}); });
@ -69,7 +69,7 @@ describe(ApiKeyService.name, () => {
mocks.apiKey.getById.mockResolvedValue(void 0); mocks.apiKey.getById.mockResolvedValue(void 0);
await expect(sut.update(auth, id, { name: 'New Name', permissions: [Permission.ALL] })).rejects.toBeInstanceOf( await expect(sut.update(auth, id, { name: 'New Name', permissions: [Permission.All] })).rejects.toBeInstanceOf(
BadRequestException, BadRequestException,
); );
@ -84,18 +84,18 @@ describe(ApiKeyService.name, () => {
mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.getById.mockResolvedValue(apiKey);
mocks.apiKey.update.mockResolvedValue(apiKey); mocks.apiKey.update.mockResolvedValue(apiKey);
await sut.update(auth, apiKey.id, { name: newName, permissions: [Permission.ALL] }); await sut.update(auth, apiKey.id, { name: newName, permissions: [Permission.All] });
expect(mocks.apiKey.update).toHaveBeenCalledWith(auth.user.id, apiKey.id, { expect(mocks.apiKey.update).toHaveBeenCalledWith(auth.user.id, apiKey.id, {
name: newName, name: newName,
permissions: [Permission.ALL], permissions: [Permission.All],
}); });
}); });
it('should update permissions', async () => { it('should update permissions', async () => {
const auth = factory.auth(); const auth = factory.auth();
const apiKey = factory.apiKey({ userId: auth.user.id }); const apiKey = factory.apiKey({ userId: auth.user.id });
const newPermissions = [Permission.ACTIVITY_CREATE, Permission.ACTIVITY_READ, Permission.ACTIVITY_UPDATE]; const newPermissions = [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate];
mocks.apiKey.getById.mockResolvedValue(apiKey); mocks.apiKey.getById.mockResolvedValue(apiKey);
mocks.apiKey.update.mockResolvedValue(apiKey); mocks.apiKey.update.mockResolvedValue(apiKey);

View File

@ -157,7 +157,7 @@ const assetEntity = Object.freeze({
ownerId: 'user_id_1', ownerId: 'user_id_1',
deviceAssetId: 'device_asset_id_1', deviceAssetId: 'device_asset_id_1',
deviceId: 'device_id_1', deviceId: 'device_id_1',
type: AssetType.VIDEO, type: AssetType.Video,
originalPath: 'fake_path/asset_1.jpeg', originalPath: 'fake_path/asset_1.jpeg',
fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'),
fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'),
@ -177,7 +177,7 @@ const assetEntity = Object.freeze({
const existingAsset = Object.freeze({ const existingAsset = Object.freeze({
...assetEntity, ...assetEntity,
duration: null, duration: null,
type: AssetType.IMAGE, type: AssetType.Image,
checksum: Buffer.from('_getExistingAsset', 'utf8'), checksum: Buffer.from('_getExistingAsset', 'utf8'),
libraryId: 'libraryId', libraryId: 'libraryId',
originalFileName: 'existing-filename.jpeg', originalFileName: 'existing-filename.jpeg',
@ -384,7 +384,7 @@ describe(AssetMediaService.name, () => {
}); });
expect(mocks.job.queue).toHaveBeenCalledWith({ expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES, name: JobName.DeleteFiles,
data: { files: ['fake_path/asset_1.jpeg', undefined] }, data: { files: ['fake_path/asset_1.jpeg', undefined] },
}); });
expect(mocks.user.updateUsage).not.toHaveBeenCalled(); expect(mocks.user.updateUsage).not.toHaveBeenCalled();
@ -409,7 +409,7 @@ describe(AssetMediaService.name, () => {
); );
expect(mocks.job.queue).toHaveBeenCalledWith({ expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES, name: JobName.DeleteFiles,
data: { files: ['fake_path/asset_1.jpeg', undefined] }, data: { files: ['fake_path/asset_1.jpeg', undefined] },
}); });
expect(mocks.user.updateUsage).not.toHaveBeenCalled(); expect(mocks.user.updateUsage).not.toHaveBeenCalled();
@ -437,7 +437,7 @@ describe(AssetMediaService.name, () => {
it('should hide the linked motion asset', async () => { it('should hide the linked motion asset', async () => {
mocks.asset.getById.mockResolvedValueOnce({ mocks.asset.getById.mockResolvedValueOnce({
...assetStub.livePhotoMotionAsset, ...assetStub.livePhotoMotionAsset,
visibility: AssetVisibility.TIMELINE, visibility: AssetVisibility.Timeline,
}); });
mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset); mocks.asset.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
@ -455,7 +455,7 @@ describe(AssetMediaService.name, () => {
expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset'); expect(mocks.asset.getById).toHaveBeenCalledWith('live-photo-motion-asset');
expect(mocks.asset.update).toHaveBeenCalledWith({ expect(mocks.asset.update).toHaveBeenCalledWith({
id: 'live-photo-motion-asset', id: 'live-photo-motion-asset',
visibility: AssetVisibility.HIDDEN, visibility: AssetVisibility.Hidden,
}); });
}); });
@ -506,7 +506,7 @@ describe(AssetMediaService.name, () => {
new ImmichFileResponse({ new ImmichFileResponse({
path: '/original/path.jpg', path: '/original/path.jpg',
contentType: 'image/jpeg', contentType: 'image/jpeg',
cacheControl: CacheControl.PRIVATE_WITH_CACHE, cacheControl: CacheControl.PrivateWithCache,
}), }),
); );
}); });
@ -546,7 +546,7 @@ describe(AssetMediaService.name, () => {
{ {
id: '42', id: '42',
path: '/path/to/preview', path: '/path/to/preview',
type: AssetFileType.THUMBNAIL, type: AssetFileType.Thumbnail,
}, },
], ],
}); });
@ -563,7 +563,7 @@ describe(AssetMediaService.name, () => {
{ {
id: '42', id: '42',
path: '/path/to/preview.jpg', path: '/path/to/preview.jpg',
type: AssetFileType.PREVIEW, type: AssetFileType.Preview,
}, },
], ],
}); });
@ -573,7 +573,7 @@ describe(AssetMediaService.name, () => {
).resolves.toEqual( ).resolves.toEqual(
new ImmichFileResponse({ new ImmichFileResponse({
path: '/path/to/preview.jpg', path: '/path/to/preview.jpg',
cacheControl: CacheControl.PRIVATE_WITH_CACHE, cacheControl: CacheControl.PrivateWithCache,
contentType: 'image/jpeg', contentType: 'image/jpeg',
fileName: 'asset-id_thumbnail.jpg', fileName: 'asset-id_thumbnail.jpg',
}), }),
@ -588,7 +588,7 @@ describe(AssetMediaService.name, () => {
).resolves.toEqual( ).resolves.toEqual(
new ImmichFileResponse({ new ImmichFileResponse({
path: '/uploads/user-id/thumbs/path.jpg', path: '/uploads/user-id/thumbs/path.jpg',
cacheControl: CacheControl.PRIVATE_WITH_CACHE, cacheControl: CacheControl.PrivateWithCache,
contentType: 'image/jpeg', contentType: 'image/jpeg',
fileName: 'asset-id_preview.jpg', fileName: 'asset-id_preview.jpg',
}), }),
@ -603,7 +603,7 @@ describe(AssetMediaService.name, () => {
).resolves.toEqual( ).resolves.toEqual(
new ImmichFileResponse({ new ImmichFileResponse({
path: '/uploads/user-id/webp/path.ext', path: '/uploads/user-id/webp/path.ext',
cacheControl: CacheControl.PRIVATE_WITH_CACHE, cacheControl: CacheControl.PrivateWithCache,
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
fileName: 'asset-id_thumbnail.ext', fileName: 'asset-id_thumbnail.ext',
}), }),
@ -640,7 +640,7 @@ describe(AssetMediaService.name, () => {
await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual( await expect(sut.playbackVideo(authStub.admin, assetStub.hasEncodedVideo.id)).resolves.toEqual(
new ImmichFileResponse({ new ImmichFileResponse({
path: assetStub.hasEncodedVideo.encodedVideoPath!, path: assetStub.hasEncodedVideo.encodedVideoPath!,
cacheControl: CacheControl.PRIVATE_WITH_CACHE, cacheControl: CacheControl.PrivateWithCache,
contentType: 'video/mp4', contentType: 'video/mp4',
}), }),
); );
@ -653,7 +653,7 @@ describe(AssetMediaService.name, () => {
await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual( await expect(sut.playbackVideo(authStub.admin, assetStub.video.id)).resolves.toEqual(
new ImmichFileResponse({ new ImmichFileResponse({
path: assetStub.video.originalPath, path: assetStub.video.originalPath,
cacheControl: CacheControl.PRIVATE_WITH_CACHE, cacheControl: CacheControl.PrivateWithCache,
contentType: 'application/octet-stream', contentType: 'application/octet-stream',
}), }),
); );
@ -723,7 +723,7 @@ describe(AssetMediaService.name, () => {
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], { expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
deletedAt: expect.any(Date), deletedAt: expect.any(Date),
status: AssetStatus.TRASHED, status: AssetStatus.Trashed,
}); });
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
expect(mocks.storage.utimes).toHaveBeenCalledWith( expect(mocks.storage.utimes).toHaveBeenCalledWith(
@ -754,7 +754,7 @@ describe(AssetMediaService.name, () => {
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], { expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
deletedAt: expect.any(Date), deletedAt: expect.any(Date),
status: AssetStatus.TRASHED, status: AssetStatus.Trashed,
}); });
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
expect(mocks.storage.utimes).toHaveBeenCalledWith( expect(mocks.storage.utimes).toHaveBeenCalledWith(
@ -783,7 +783,7 @@ describe(AssetMediaService.name, () => {
expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], { expect(mocks.asset.updateAll).toHaveBeenCalledWith([copiedAsset.id], {
deletedAt: expect.any(Date), deletedAt: expect.any(Date),
status: AssetStatus.TRASHED, status: AssetStatus.Trashed,
}); });
expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size); expect(mocks.user.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, updatedFile.size);
expect(mocks.storage.utimes).toHaveBeenCalledWith( expect(mocks.storage.utimes).toHaveBeenCalledWith(
@ -815,7 +815,7 @@ describe(AssetMediaService.name, () => {
expect(mocks.asset.create).not.toHaveBeenCalled(); expect(mocks.asset.create).not.toHaveBeenCalled();
expect(mocks.asset.updateAll).not.toHaveBeenCalled(); expect(mocks.asset.updateAll).not.toHaveBeenCalled();
expect(mocks.job.queue).toHaveBeenCalledWith({ expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES, name: JobName.DeleteFiles,
data: { files: [updatedFile.originalPath, undefined] }, data: { files: [updatedFile.originalPath, undefined] },
}); });
expect(mocks.user.updateUsage).not.toHaveBeenCalled(); expect(mocks.user.updateUsage).not.toHaveBeenCalled();
@ -912,7 +912,7 @@ describe(AssetMediaService.name, () => {
await sut.onUploadError(request, file); await sut.onUploadError(request, file);
expect(mocks.job.queue).toHaveBeenCalledWith({ expect(mocks.job.queue).toHaveBeenCalledWith({
name: JobName.DELETE_FILES, name: JobName.DeleteFiles,
data: { files: ['upload/upload/user-id/ra/nd/random-uuid.jpg'] }, data: { files: ['upload/upload/user-id/ra/nd/random-uuid.jpg'] },
}); });
}); });

View File

@ -106,9 +106,9 @@ export class AssetMediaService extends BaseService {
getUploadFolder({ auth, fieldName, file }: UploadRequest): string { getUploadFolder({ auth, fieldName, file }: UploadRequest): string {
auth = requireUploadAccess(auth); auth = requireUploadAccess(auth);
let folder = StorageCore.getNestedFolder(StorageFolder.UPLOAD, auth.user.id, file.uuid); let folder = StorageCore.getNestedFolder(StorageFolder.Upload, auth.user.id, file.uuid);
if (fieldName === UploadFieldName.PROFILE_DATA) { if (fieldName === UploadFieldName.PROFILE_DATA) {
folder = StorageCore.getFolderLocation(StorageFolder.PROFILE, auth.user.id); folder = StorageCore.getFolderLocation(StorageFolder.Profile, auth.user.id);
} }
this.storageRepository.mkdirSync(folder); this.storageRepository.mkdirSync(folder);
@ -121,7 +121,7 @@ export class AssetMediaService extends BaseService {
const uploadFolder = this.getUploadFolder(asRequest(request, file)); const uploadFolder = this.getUploadFolder(asRequest(request, file));
const uploadPath = `${uploadFolder}/${uploadFilename}`; const uploadPath = `${uploadFolder}/${uploadFilename}`;
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [uploadPath] } }); await this.jobRepository.queue({ name: JobName.DeleteFiles, data: { files: [uploadPath] } });
} }
async uploadAsset( async uploadAsset(
@ -133,7 +133,7 @@ export class AssetMediaService extends BaseService {
try { try {
await this.requireAccess({ await this.requireAccess({
auth, auth,
permission: Permission.ASSET_UPLOAD, permission: Permission.AssetUpload,
// do not need an id here, but the interface requires it // do not need an id here, but the interface requires it
ids: [auth.user.id], ids: [auth.user.id],
}); });
@ -164,7 +164,7 @@ export class AssetMediaService extends BaseService {
sidecarFile?: UploadFile, sidecarFile?: UploadFile,
): Promise<AssetMediaResponseDto> { ): Promise<AssetMediaResponseDto> {
try { try {
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] });
const asset = await this.assetRepository.getById(id); const asset = await this.assetRepository.getById(id);
if (!asset) { if (!asset) {
@ -179,7 +179,7 @@ export class AssetMediaService extends BaseService {
// but the local variable holds the original file data paths. // but the local variable holds the original file data paths.
const copiedPhoto = await this.createCopy(asset); const copiedPhoto = await this.createCopy(asset);
// and immediate trash it // and immediate trash it
await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.TRASHED }); await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.Trashed });
await this.eventRepository.emit('AssetTrash', { assetId: copiedPhoto.id, userId: auth.user.id }); await this.eventRepository.emit('AssetTrash', { assetId: copiedPhoto.id, userId: auth.user.id });
await this.userRepository.updateUsage(auth.user.id, file.size); await this.userRepository.updateUsage(auth.user.id, file.size);
@ -191,14 +191,14 @@ export class AssetMediaService extends BaseService {
} }
async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> { async downloadOriginal(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: [id] });
const asset = await this.findOrFail(id); const asset = await this.findOrFail(id);
return new ImmichFileResponse({ return new ImmichFileResponse({
path: asset.originalPath, path: asset.originalPath,
contentType: mimeTypes.lookup(asset.originalPath), contentType: mimeTypes.lookup(asset.originalPath),
cacheControl: CacheControl.PRIVATE_WITH_CACHE, cacheControl: CacheControl.PrivateWithCache,
}); });
} }
@ -207,7 +207,7 @@ export class AssetMediaService extends BaseService {
id: string, id: string,
dto: AssetMediaOptionsDto, dto: AssetMediaOptionsDto,
): Promise<ImmichFileResponse | AssetMediaRedirectResponse> { ): Promise<ImmichFileResponse | AssetMediaRedirectResponse> {
await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
const asset = await this.findOrFail(id); const asset = await this.findOrFail(id);
const size = dto.size ?? AssetMediaSize.THUMBNAIL; const size = dto.size ?? AssetMediaSize.THUMBNAIL;
@ -240,16 +240,16 @@ export class AssetMediaService extends BaseService {
fileName, fileName,
path: filepath, path: filepath,
contentType: mimeTypes.lookup(filepath), contentType: mimeTypes.lookup(filepath),
cacheControl: CacheControl.PRIVATE_WITH_CACHE, cacheControl: CacheControl.PrivateWithCache,
}); });
} }
async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> { async playbackVideo(auth: AuthDto, id: string): Promise<ImmichFileResponse> {
await this.requireAccess({ auth, permission: Permission.ASSET_VIEW, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AssetView, ids: [id] });
const asset = await this.findOrFail(id); const asset = await this.findOrFail(id);
if (asset.type !== AssetType.VIDEO) { if (asset.type !== AssetType.Video) {
throw new BadRequestException('Asset is not a video'); throw new BadRequestException('Asset is not a video');
} }
@ -258,7 +258,7 @@ export class AssetMediaService extends BaseService {
return new ImmichFileResponse({ return new ImmichFileResponse({
path: filepath, path: filepath,
contentType: mimeTypes.lookup(filepath), contentType: mimeTypes.lookup(filepath),
cacheControl: CacheControl.PRIVATE_WITH_CACHE, cacheControl: CacheControl.PrivateWithCache,
}); });
} }
@ -312,7 +312,7 @@ export class AssetMediaService extends BaseService {
): Promise<AssetMediaResponseDto> { ): Promise<AssetMediaResponseDto> {
// clean up files // clean up files
await this.jobRepository.queue({ await this.jobRepository.queue({
name: JobName.DELETE_FILES, name: JobName.DeleteFiles,
data: { files: [file.originalPath, sidecarFile?.originalPath] }, data: { files: [file.originalPath, sidecarFile?.originalPath] },
}); });
@ -365,7 +365,7 @@ export class AssetMediaService extends BaseService {
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
await this.assetRepository.upsertExif({ assetId, fileSizeInByte: file.size }); await this.assetRepository.upsertExif({ assetId, fileSizeInByte: file.size });
await this.jobRepository.queue({ await this.jobRepository.queue({
name: JobName.METADATA_EXTRACTION, name: JobName.MetadataExtraction,
data: { id: assetId, source: 'upload' }, data: { id: assetId, source: 'upload' },
}); });
} }
@ -394,7 +394,7 @@ export class AssetMediaService extends BaseService {
const { size } = await this.storageRepository.stat(created.originalPath); const { size } = await this.storageRepository.stat(created.originalPath);
await this.assetRepository.upsertExif({ assetId: created.id, fileSizeInByte: size }); await this.assetRepository.upsertExif({ assetId: created.id, fileSizeInByte: size });
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: created.id, source: 'copy' } }); await this.jobRepository.queue({ name: JobName.MetadataExtraction, data: { id: created.id, source: 'copy' } });
return created; return created;
} }
@ -416,7 +416,7 @@ export class AssetMediaService extends BaseService {
type: mimeTypes.assetType(file.originalPath), type: mimeTypes.assetType(file.originalPath),
isFavorite: dto.isFavorite, isFavorite: dto.isFavorite,
duration: dto.duration || null, duration: dto.duration || null,
visibility: dto.visibility ?? AssetVisibility.TIMELINE, visibility: dto.visibility ?? AssetVisibility.Timeline,
livePhotoVideoId: dto.livePhotoVideoId, livePhotoVideoId: dto.livePhotoVideoId,
originalFileName: dto.filename || file.originalName, originalFileName: dto.filename || file.originalName,
sidecarPath: sidecarFile?.originalPath, sidecarPath: sidecarFile?.originalPath,
@ -427,7 +427,7 @@ export class AssetMediaService extends BaseService {
} }
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt)); await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
await this.assetRepository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size }); await this.assetRepository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size });
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } }); await this.jobRepository.queue({ name: JobName.MetadataExtraction, data: { id: asset.id, source: 'upload' } });
return asset; return asset;
} }

View File

@ -13,10 +13,10 @@ import { factory } from 'test/small.factory';
import { makeStream, newTestService, ServiceMocks } from 'test/utils'; import { makeStream, newTestService, ServiceMocks } from 'test/utils';
const stats: AssetStats = { const stats: AssetStats = {
[AssetType.IMAGE]: 10, [AssetType.Image]: 10,
[AssetType.VIDEO]: 23, [AssetType.Video]: 23,
[AssetType.AUDIO]: 0, [AssetType.Audio]: 0,
[AssetType.OTHER]: 0, [AssetType.Other]: 0,
}; };
const statResponse: AssetStatsResponseDto = { const statResponse: AssetStatsResponseDto = {
@ -46,21 +46,21 @@ describe(AssetService.name, () => {
describe('getStatistics', () => { describe('getStatistics', () => {
it('should get the statistics for a user, excluding archived assets', async () => { it('should get the statistics for a user, excluding archived assets', async () => {
mocks.asset.getStatistics.mockResolvedValue(stats); mocks.asset.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.TIMELINE })).resolves.toEqual( await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.Timeline })).resolves.toEqual(
statResponse, statResponse,
); );
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
visibility: AssetVisibility.TIMELINE, visibility: AssetVisibility.Timeline,
}); });
}); });
it('should get the statistics for a user for archived assets', async () => { it('should get the statistics for a user for archived assets', async () => {
mocks.asset.getStatistics.mockResolvedValue(stats); mocks.asset.getStatistics.mockResolvedValue(stats);
await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.ARCHIVE })).resolves.toEqual( await expect(sut.getStatistics(authStub.admin, { visibility: AssetVisibility.Archive })).resolves.toEqual(
statResponse, statResponse,
); );
expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, { expect(mocks.asset.getStatistics).toHaveBeenCalledWith(authStub.admin.user.id, {
visibility: AssetVisibility.ARCHIVE, visibility: AssetVisibility.Archive,
}); });
}); });
@ -202,7 +202,7 @@ describe(AssetService.name, () => {
describe('update', () => { describe('update', () => {
it('should require asset write access for the id', async () => { it('should require asset write access for the id', async () => {
await expect( await expect(
sut.update(authStub.admin, 'asset-1', { visibility: AssetVisibility.TIMELINE }), sut.update(authStub.admin, 'asset-1', { visibility: AssetVisibility.Timeline }),
).rejects.toBeInstanceOf(BadRequestException); ).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.asset.update).not.toHaveBeenCalled(); expect(mocks.asset.update).not.toHaveBeenCalled();
@ -253,7 +253,7 @@ describe(AssetService.name, () => {
}); });
expect(mocks.asset.update).not.toHaveBeenCalledWith({ expect(mocks.asset.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id, id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE, visibility: AssetVisibility.Timeline,
}); });
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
assetId: assetStub.livePhotoMotionAsset.id, assetId: assetStub.livePhotoMotionAsset.id,
@ -277,7 +277,7 @@ describe(AssetService.name, () => {
}); });
expect(mocks.asset.update).not.toHaveBeenCalledWith({ expect(mocks.asset.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id, id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE, visibility: AssetVisibility.Timeline,
}); });
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
assetId: assetStub.livePhotoMotionAsset.id, assetId: assetStub.livePhotoMotionAsset.id,
@ -301,7 +301,7 @@ describe(AssetService.name, () => {
}); });
expect(mocks.asset.update).not.toHaveBeenCalledWith({ expect(mocks.asset.update).not.toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id, id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.TIMELINE, visibility: AssetVisibility.Timeline,
}); });
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', { expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
assetId: assetStub.livePhotoMotionAsset.id, assetId: assetStub.livePhotoMotionAsset.id,
@ -314,7 +314,7 @@ describe(AssetService.name, () => {
mocks.asset.getById.mockResolvedValueOnce({ mocks.asset.getById.mockResolvedValueOnce({
...assetStub.livePhotoMotionAsset, ...assetStub.livePhotoMotionAsset,
ownerId: authStub.admin.user.id, ownerId: authStub.admin.user.id,
visibility: AssetVisibility.TIMELINE, visibility: AssetVisibility.Timeline,
}); });
mocks.asset.getById.mockResolvedValueOnce(assetStub.image); mocks.asset.getById.mockResolvedValueOnce(assetStub.image);
mocks.asset.update.mockResolvedValue(assetStub.image); mocks.asset.update.mockResolvedValue(assetStub.image);
@ -325,7 +325,7 @@ describe(AssetService.name, () => {
expect(mocks.asset.update).toHaveBeenCalledWith({ expect(mocks.asset.update).toHaveBeenCalledWith({
id: assetStub.livePhotoMotionAsset.id, id: assetStub.livePhotoMotionAsset.id,
visibility: AssetVisibility.HIDDEN, visibility: AssetVisibility.Hidden,
}); });
expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', { expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', {
assetId: assetStub.livePhotoMotionAsset.id, assetId: assetStub.livePhotoMotionAsset.id,
@ -392,10 +392,10 @@ describe(AssetService.name, () => {
it('should update all assets', async () => { it('should update all assets', async () => {
mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2'])); mocks.access.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2']));
await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.ARCHIVE }); await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], visibility: AssetVisibility.Archive });
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], {
visibility: AssetVisibility.ARCHIVE, visibility: AssetVisibility.Archive,
}); });
}); });
@ -428,7 +428,7 @@ describe(AssetService.name, () => {
expect(mocks.asset.updateAll).toHaveBeenCalled(); expect(mocks.asset.updateAll).toHaveBeenCalled();
expect(mocks.asset.updateAllExif).toHaveBeenCalledWith(['asset-1'], { latitude: 0, longitude: 0 }); expect(mocks.asset.updateAllExif).toHaveBeenCalledWith(['asset-1'], { latitude: 0, longitude: 0 });
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ name: JobName.SIDECAR_WRITE, data: { id: 'asset-1', latitude: 0, longitude: 0 } }, { name: JobName.SidecarWrite, data: { id: 'asset-1', latitude: 0, longitude: 0 } },
]); ]);
}); });
@ -451,7 +451,7 @@ describe(AssetService.name, () => {
longitude: 50, longitude: 50,
}); });
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ name: JobName.SIDECAR_WRITE, data: { id: 'asset-1', dateTimeOriginal, latitude: 30, longitude: 50 } }, { name: JobName.SidecarWrite, data: { id: 'asset-1', dateTimeOriginal, latitude: 30, longitude: 50 } },
]); ]);
}); });
@ -497,7 +497,7 @@ describe(AssetService.name, () => {
expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset1', 'asset2'], { expect(mocks.asset.updateAll).toHaveBeenCalledWith(['asset1', 'asset2'], {
deletedAt: expect.any(Date), deletedAt: expect.any(Date),
status: AssetStatus.TRASHED, status: AssetStatus.Trashed,
}); });
expect(mocks.job.queue.mock.calls).toEqual([]); expect(mocks.job.queue.mock.calls).toEqual([]);
}); });
@ -518,11 +518,11 @@ describe(AssetService.name, () => {
mocks.assetJob.streamForDeletedJob.mockReturnValue(makeStream([asset])); mocks.assetJob.streamForDeletedJob.mockReturnValue(makeStream([asset]));
mocks.systemMetadata.get.mockResolvedValue({ trash: { enabled: false } }); mocks.systemMetadata.get.mockResolvedValue({ trash: { enabled: false } });
await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.Success);
expect(mocks.assetJob.streamForDeletedJob).toHaveBeenCalledWith(new Date()); expect(mocks.assetJob.streamForDeletedJob).toHaveBeenCalledWith(new Date());
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ name: JobName.ASSET_DELETION, data: { id: asset.id, deleteOnDisk: true } }, { name: JobName.AssetDeletion, data: { id: asset.id, deleteOnDisk: true } },
]); ]);
}); });
@ -532,11 +532,11 @@ describe(AssetService.name, () => {
mocks.assetJob.streamForDeletedJob.mockReturnValue(makeStream([asset])); mocks.assetJob.streamForDeletedJob.mockReturnValue(makeStream([asset]));
mocks.systemMetadata.get.mockResolvedValue({ trash: { enabled: true, days: 7 } }); mocks.systemMetadata.get.mockResolvedValue({ trash: { enabled: true, days: 7 } });
await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleAssetDeletionCheck()).resolves.toBe(JobStatus.Success);
expect(mocks.assetJob.streamForDeletedJob).toHaveBeenCalledWith(DateTime.now().minus({ days: 7 }).toJSDate()); expect(mocks.assetJob.streamForDeletedJob).toHaveBeenCalledWith(DateTime.now().minus({ days: 7 }).toJSDate());
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ name: JobName.ASSET_DELETION, data: { id: asset.id, deleteOnDisk: true } }, { name: JobName.AssetDeletion, data: { id: asset.id, deleteOnDisk: true } },
]); ]);
}); });
}); });
@ -552,7 +552,7 @@ describe(AssetService.name, () => {
expect(mocks.job.queue.mock.calls).toEqual([ expect(mocks.job.queue.mock.calls).toEqual([
[ [
{ {
name: JobName.DELETE_FILES, name: JobName.DeleteFiles,
data: { data: {
files: [ files: [
'/uploads/user-id/webp/path.ext', '/uploads/user-id/webp/path.ext',
@ -606,7 +606,7 @@ describe(AssetService.name, () => {
expect(mocks.job.queue.mock.calls).toEqual([ expect(mocks.job.queue.mock.calls).toEqual([
[ [
{ {
name: JobName.ASSET_DELETION, name: JobName.AssetDeletion,
data: { data: {
id: assetStub.livePhotoMotionAsset.id, id: assetStub.livePhotoMotionAsset.id,
deleteOnDisk: true, deleteOnDisk: true,
@ -615,7 +615,7 @@ describe(AssetService.name, () => {
], ],
[ [
{ {
name: JobName.DELETE_FILES, name: JobName.DeleteFiles,
data: { data: {
files: [ files: [
'/uploads/user-id/webp/path.ext', '/uploads/user-id/webp/path.ext',
@ -643,7 +643,7 @@ describe(AssetService.name, () => {
expect(mocks.job.queue.mock.calls).toEqual([ expect(mocks.job.queue.mock.calls).toEqual([
[ [
{ {
name: JobName.DELETE_FILES, name: JobName.DeleteFiles,
data: { data: {
files: [ files: [
'/uploads/user-id/webp/path.ext', '/uploads/user-id/webp/path.ext',
@ -668,7 +668,7 @@ describe(AssetService.name, () => {
it('should fail if asset could not be found', async () => { it('should fail if asset could not be found', async () => {
mocks.assetJob.getForAssetDeletion.mockResolvedValue(void 0); mocks.assetJob.getForAssetDeletion.mockResolvedValue(void 0);
await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe( await expect(sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true })).resolves.toBe(
JobStatus.FAILED, JobStatus.Failed,
); );
}); });
}); });
@ -679,7 +679,7 @@ describe(AssetService.name, () => {
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_FACES }); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_FACES });
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.FACE_DETECTION, data: { id: 'asset-1' } }]); expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.FaceDetection, data: { id: 'asset-1' } }]);
}); });
it('should run the refresh metadata job', async () => { it('should run the refresh metadata job', async () => {
@ -687,7 +687,7 @@ describe(AssetService.name, () => {
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA }); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REFRESH_METADATA });
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.METADATA_EXTRACTION, data: { id: 'asset-1' } }]); expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.MetadataExtraction, data: { id: 'asset-1' } }]);
}); });
it('should run the refresh thumbnails job', async () => { it('should run the refresh thumbnails job', async () => {
@ -695,7 +695,7 @@ describe(AssetService.name, () => {
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL }); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.REGENERATE_THUMBNAIL });
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }]); expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.GenerateThumbnails, data: { id: 'asset-1' } }]);
}); });
it('should run the transcode video', async () => { it('should run the transcode video', async () => {
@ -703,7 +703,7 @@ describe(AssetService.name, () => {
await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.TRANSCODE_VIDEO }); await sut.run(authStub.admin, { assetIds: ['asset-1'], name: AssetJobName.TRANSCODE_VIDEO });
expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.VIDEO_CONVERSION, data: { id: 'asset-1' } }]); expect(mocks.job.queueAll).toHaveBeenCalledWith([{ name: JobName.VideoConversation, data: { id: 'asset-1' } }]);
}); });
}); });

View File

@ -23,7 +23,7 @@ import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUn
@Injectable() @Injectable()
export class AssetService extends BaseService { export class AssetService extends BaseService {
async getStatistics(auth: AuthDto, dto: AssetStatsDto) { async getStatistics(auth: AuthDto, dto: AssetStatsDto) {
if (dto.visibility === AssetVisibility.LOCKED) { if (dto.visibility === AssetVisibility.Locked) {
requireElevatedPermission(auth); requireElevatedPermission(auth);
} }
@ -46,7 +46,7 @@ export class AssetService extends BaseService {
} }
async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> { async get(auth: AuthDto, id: string): Promise<AssetResponseDto | SanitizedAssetResponseDto> {
await this.requireAccess({ auth, permission: Permission.ASSET_READ, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [id] });
const asset = await this.assetRepository.getById(id, { const asset = await this.assetRepository.getById(id, {
exifInfo: true, exifInfo: true,
@ -78,7 +78,7 @@ export class AssetService extends BaseService {
} }
async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> { async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise<AssetResponseDto> {
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: [id] }); await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: [id] });
const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto;
const repos = { asset: this.assetRepository, event: this.eventRepository }; const repos = { asset: this.assetRepository, event: this.eventRepository };
@ -114,7 +114,7 @@ export class AssetService extends BaseService {
async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> { async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise<void> {
const { ids, description, dateTimeOriginal, latitude, longitude, ...options } = dto; const { ids, description, dateTimeOriginal, latitude, longitude, ...options } = dto;
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids }); await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids });
if ( if (
description !== undefined || description !== undefined ||
@ -125,7 +125,7 @@ export class AssetService extends BaseService {
await this.assetRepository.updateAllExif(ids, { description, dateTimeOriginal, latitude, longitude }); await this.assetRepository.updateAllExif(ids, { description, dateTimeOriginal, latitude, longitude });
await this.jobRepository.queueAll( await this.jobRepository.queueAll(
ids.map((id) => ({ ids.map((id) => ({
name: JobName.SIDECAR_WRITE, name: JobName.SidecarWrite,
data: { id, description, dateTimeOriginal, latitude, longitude }, data: { id, description, dateTimeOriginal, latitude, longitude },
})), })),
); );
@ -139,13 +139,13 @@ export class AssetService extends BaseService {
) { ) {
await this.assetRepository.updateAll(ids, options); await this.assetRepository.updateAll(ids, options);
if (options.visibility === AssetVisibility.LOCKED) { if (options.visibility === AssetVisibility.Locked) {
await this.albumRepository.removeAssetsFromAll(ids); await this.albumRepository.removeAssetsFromAll(ids);
} }
} }
} }
@OnJob({ name: JobName.ASSET_DELETION_CHECK, queue: QueueName.BACKGROUND_TASK }) @OnJob({ name: JobName.AssetDeletionCheck, queue: QueueName.BackgroundTask })
async handleAssetDeletionCheck(): Promise<JobStatus> { async handleAssetDeletionCheck(): Promise<JobStatus> {
const config = await this.getConfig({ withCache: false }); const config = await this.getConfig({ withCache: false });
const trashedDays = config.trash.enabled ? config.trash.days : 0; const trashedDays = config.trash.enabled ? config.trash.days : 0;
@ -158,7 +158,7 @@ export class AssetService extends BaseService {
if (chunk.length > 0) { if (chunk.length > 0) {
await this.jobRepository.queueAll( await this.jobRepository.queueAll(
chunk.map(({ id, isOffline }) => ({ chunk.map(({ id, isOffline }) => ({
name: JobName.ASSET_DELETION, name: JobName.AssetDeletion,
data: { id, deleteOnDisk: !isOffline }, data: { id, deleteOnDisk: !isOffline },
})), })),
); );
@ -176,17 +176,17 @@ export class AssetService extends BaseService {
await queueChunk(); await queueChunk();
return JobStatus.SUCCESS; return JobStatus.Success;
} }
@OnJob({ name: JobName.ASSET_DELETION, queue: QueueName.BACKGROUND_TASK }) @OnJob({ name: JobName.AssetDeletion, queue: QueueName.BackgroundTask })
async handleAssetDeletion(job: JobOf<JobName.ASSET_DELETION>): Promise<JobStatus> { async handleAssetDeletion(job: JobOf<JobName.AssetDeletion>): Promise<JobStatus> {
const { id, deleteOnDisk } = job; const { id, deleteOnDisk } = job;
const asset = await this.assetJobRepository.getForAssetDeletion(id); const asset = await this.assetJobRepository.getForAssetDeletion(id);
if (!asset) { if (!asset) {
return JobStatus.FAILED; return JobStatus.Failed;
} }
// Replace the parent of the stack children with a new asset // Replace the parent of the stack children with a new asset
@ -215,7 +215,7 @@ export class AssetService extends BaseService {
const count = await this.assetRepository.getLivePhotoCount(asset.livePhotoVideoId); const count = await this.assetRepository.getLivePhotoCount(asset.livePhotoVideoId);
if (count === 0) { if (count === 0) {
await this.jobRepository.queue({ await this.jobRepository.queue({
name: JobName.ASSET_DELETION, name: JobName.AssetDeletion,
data: { id: asset.livePhotoVideoId, deleteOnDisk }, data: { id: asset.livePhotoVideoId, deleteOnDisk },
}); });
} }
@ -228,18 +228,18 @@ export class AssetService extends BaseService {
files.push(asset.sidecarPath, asset.originalPath); files.push(asset.sidecarPath, asset.originalPath);
} }
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files } }); await this.jobRepository.queue({ name: JobName.DeleteFiles, data: { files } });
return JobStatus.SUCCESS; return JobStatus.Success;
} }
async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> { async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise<void> {
const { ids, force } = dto; const { ids, force } = dto;
await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids }); await this.requireAccess({ auth, permission: Permission.AssetDelete, ids });
await this.assetRepository.updateAll(ids, { await this.assetRepository.updateAll(ids, {
deletedAt: new Date(), deletedAt: new Date(),
status: force ? AssetStatus.DELETED : AssetStatus.TRASHED, status: force ? AssetStatus.Deleted : AssetStatus.Trashed,
}); });
await this.eventRepository.emit(force ? 'AssetDeleteAll' : 'AssetTrashAll', { await this.eventRepository.emit(force ? 'AssetDeleteAll' : 'AssetTrashAll', {
assetIds: ids, assetIds: ids,
@ -248,29 +248,29 @@ export class AssetService extends BaseService {
} }
async run(auth: AuthDto, dto: AssetJobsDto) { async run(auth: AuthDto, dto: AssetJobsDto) {
await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); await this.requireAccess({ auth, permission: Permission.AssetUpdate, ids: dto.assetIds });
const jobs: JobItem[] = []; const jobs: JobItem[] = [];
for (const id of dto.assetIds) { for (const id of dto.assetIds) {
switch (dto.name) { switch (dto.name) {
case AssetJobName.REFRESH_FACES: { case AssetJobName.REFRESH_FACES: {
jobs.push({ name: JobName.FACE_DETECTION, data: { id } }); jobs.push({ name: JobName.FaceDetection, data: { id } });
break; break;
} }
case AssetJobName.REFRESH_METADATA: { case AssetJobName.REFRESH_METADATA: {
jobs.push({ name: JobName.METADATA_EXTRACTION, data: { id } }); jobs.push({ name: JobName.MetadataExtraction, data: { id } });
break; break;
} }
case AssetJobName.REGENERATE_THUMBNAIL: { case AssetJobName.REGENERATE_THUMBNAIL: {
jobs.push({ name: JobName.GENERATE_THUMBNAILS, data: { id } }); jobs.push({ name: JobName.GenerateThumbnails, data: { id } });
break; break;
} }
case AssetJobName.TRANSCODE_VIDEO: { case AssetJobName.TRANSCODE_VIDEO: {
jobs.push({ name: JobName.VIDEO_CONVERSION, data: { id } }); jobs.push({ name: JobName.VideoConversation, data: { id } });
break; break;
} }
} }
@ -292,7 +292,7 @@ export class AssetService extends BaseService {
const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined); const writes = _.omitBy({ description, dateTimeOriginal, latitude, longitude, rating }, _.isUndefined);
if (Object.keys(writes).length > 0) { if (Object.keys(writes).length > 0) {
await this.assetRepository.upsertExif({ assetId: id, ...writes }); await this.assetRepository.upsertExif({ assetId: id, ...writes });
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id, ...writes } }); await this.jobRepository.queue({ name: JobName.SidecarWrite, data: { id, ...writes } });
} }
} }
} }

View File

@ -18,7 +18,7 @@ describe(AuditService.name, () => {
it('should delete old audit entries', async () => { it('should delete old audit entries', async () => {
mocks.audit.removeBefore.mockResolvedValue(); mocks.audit.removeBefore.mockResolvedValue();
await expect(sut.handleCleanup()).resolves.toBe(JobStatus.SUCCESS); await expect(sut.handleCleanup()).resolves.toBe(JobStatus.Success);
expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date)); expect(mocks.audit.removeBefore).toHaveBeenCalledWith(expect.any(Date));
}); });

View File

@ -7,9 +7,9 @@ import { BaseService } from 'src/services/base.service';
@Injectable() @Injectable()
export class AuditService extends BaseService { export class AuditService extends BaseService {
@OnJob({ name: JobName.CLEAN_OLD_AUDIT_LOGS, queue: QueueName.BACKGROUND_TASK }) @OnJob({ name: JobName.CleanOldAuditLogs, queue: QueueName.BackgroundTask })
async handleCleanup(): Promise<JobStatus> { async handleCleanup(): Promise<JobStatus> {
await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate()); await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
return JobStatus.SUCCESS; return JobStatus.Success;
} }
} }

View File

@ -154,7 +154,7 @@ describe(AuthService.name, () => {
mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled); mocks.systemMetadata.get.mockResolvedValue(systemConfigStub.enabled);
await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({ await expect(sut.logout(auth, AuthType.OAuth)).resolves.toEqual({
successful: true, successful: true,
redirectUri: 'http://end-session-endpoint', redirectUri: 'http://end-session-endpoint',
}); });
@ -163,7 +163,7 @@ describe(AuthService.name, () => {
it('should return the default redirect', async () => { it('should return the default redirect', async () => {
const auth = factory.auth(); const auth = factory.auth();
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ await expect(sut.logout(auth, AuthType.Password)).resolves.toEqual({
successful: true, successful: true,
redirectUri: '/auth/login?autoLaunch=0', redirectUri: '/auth/login?autoLaunch=0',
}); });
@ -173,7 +173,7 @@ describe(AuthService.name, () => {
const auth = { user: { id: '123' }, session: { id: 'token123' } } as AuthDto; const auth = { user: { id: '123' }, session: { id: 'token123' } } as AuthDto;
mocks.session.delete.mockResolvedValue(); mocks.session.delete.mockResolvedValue();
await expect(sut.logout(auth, AuthType.PASSWORD)).resolves.toEqual({ await expect(sut.logout(auth, AuthType.Password)).resolves.toEqual({
successful: true, successful: true,
redirectUri: '/auth/login?autoLaunch=0', redirectUri: '/auth/login?autoLaunch=0',
}); });
@ -185,7 +185,7 @@ describe(AuthService.name, () => {
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => { it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
const auth = { user: { id: '123' } } as AuthDto; const auth = { user: { id: '123' } } as AuthDto;
await expect(sut.logout(auth, AuthType.OAUTH)).resolves.toEqual({ await expect(sut.logout(auth, AuthType.OAuth)).resolves.toEqual({
successful: true, successful: true,
redirectUri: '/auth/login?autoLaunch=0', redirectUri: '/auth/login?autoLaunch=0',
}); });
@ -463,7 +463,7 @@ describe(AuthService.name, () => {
sut.authenticate({ sut.authenticate({
headers: { 'x-api-key': 'auth_token' }, headers: { 'x-api-key': 'auth_token' },
queryParams: {}, queryParams: {},
metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test', permission: Permission.ASSET_READ }, metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test', permission: Permission.AssetRead },
}), }),
).rejects.toBeInstanceOf(ForbiddenException); ).rejects.toBeInstanceOf(ForbiddenException);
}); });

View File

@ -194,13 +194,13 @@ export class AuthService extends BaseService {
} }
private async validate({ headers, queryParams }: Omit<ValidateRequest, 'metadata'>): Promise<AuthDto> { private async validate({ headers, queryParams }: Omit<ValidateRequest, 'metadata'>): Promise<AuthDto> {
const shareKey = (headers[ImmichHeader.SHARED_LINK_KEY] || queryParams[ImmichQuery.SHARED_LINK_KEY]) as string; const shareKey = (headers[ImmichHeader.SharedLinkKey] || queryParams[ImmichQuery.SharedLinkKey]) as string;
const session = (headers[ImmichHeader.USER_TOKEN] || const session = (headers[ImmichHeader.UserToken] ||
headers[ImmichHeader.SESSION_TOKEN] || headers[ImmichHeader.SessionToken] ||
queryParams[ImmichQuery.SESSION_KEY] || queryParams[ImmichQuery.SessionKey] ||
this.getBearerToken(headers) || this.getBearerToken(headers) ||
this.getCookieToken(headers)) as string; this.getCookieToken(headers)) as string;
const apiKey = (headers[ImmichHeader.API_KEY] || queryParams[ImmichQuery.API_KEY]) as string; const apiKey = (headers[ImmichHeader.ApiKey] || queryParams[ImmichQuery.ApiKey]) as string;
if (shareKey) { if (shareKey) {
return this.validateSharedLink(shareKey); return this.validateSharedLink(shareKey);
@ -321,7 +321,7 @@ export class AuthService extends BaseService {
const { contentType, data } = await this.oauthRepository.getProfilePicture(url); const { contentType, data } = await this.oauthRepository.getProfilePicture(url);
const extensionWithDot = mimeTypes.toExtension(contentType || 'image/jpeg') ?? 'jpg'; const extensionWithDot = mimeTypes.toExtension(contentType || 'image/jpeg') ?? 'jpg';
const profileImagePath = join( const profileImagePath = join(
StorageCore.getFolderLocation(StorageFolder.PROFILE, user.id), StorageCore.getFolderLocation(StorageFolder.Profile, user.id),
`${this.cryptoRepository.randomUUID()}${extensionWithDot}`, `${this.cryptoRepository.randomUUID()}${extensionWithDot}`,
); );
@ -330,7 +330,7 @@ export class AuthService extends BaseService {
await this.userRepository.update(user.id, { profileImagePath, profileChangedAt: new Date() }); await this.userRepository.update(user.id, { profileImagePath, profileChangedAt: new Date() });
if (oldPath) { if (oldPath) {
await this.jobRepository.queue({ name: JobName.DELETE_FILES, data: { files: [oldPath] } }); await this.jobRepository.queue({ name: JobName.DeleteFiles, data: { files: [oldPath] } });
} }
} catch (error: Error | any) { } catch (error: Error | any) {
this.logger.warn(`Unable to sync oauth profile picture: ${error}`, error?.stack); this.logger.warn(`Unable to sync oauth profile picture: ${error}`, error?.stack);
@ -366,7 +366,7 @@ export class AuthService extends BaseService {
} }
private async getLogoutEndpoint(authType: AuthType): Promise<string> { private async getLogoutEndpoint(authType: AuthType): Promise<string> {
if (authType !== AuthType.OAUTH) { if (authType !== AuthType.OAuth) {
return LOGIN_URL; return LOGIN_URL;
} }
@ -389,17 +389,17 @@ export class AuthService extends BaseService {
private getCookieToken(headers: IncomingHttpHeaders): string | null { private getCookieToken(headers: IncomingHttpHeaders): string | null {
const cookies = parse(headers.cookie || ''); const cookies = parse(headers.cookie || '');
return cookies[ImmichCookie.ACCESS_TOKEN] || null; return cookies[ImmichCookie.AccessToken] || null;
} }
private getCookieOauthState(headers: IncomingHttpHeaders): string | null { private getCookieOauthState(headers: IncomingHttpHeaders): string | null {
const cookies = parse(headers.cookie || ''); const cookies = parse(headers.cookie || '');
return cookies[ImmichCookie.OAUTH_STATE] || null; return cookies[ImmichCookie.OAuthState] || null;
} }
private getCookieCodeVerifier(headers: IncomingHttpHeaders): string | null { private getCookieCodeVerifier(headers: IncomingHttpHeaders): string | null {
const cookies = parse(headers.cookie || ''); const cookies = parse(headers.cookie || '');
return cookies[ImmichCookie.OAUTH_CODE_VERIFIER] || null; return cookies[ImmichCookie.OAuthCodeVerifier] || null;
} }
async validateSharedLink(key: string | string[]): Promise<AuthDto> { async validateSharedLink(key: string | string[]): Promise<AuthDto> {

View File

@ -38,7 +38,7 @@ describe(BackupService.name, () => {
}); });
it('should not initialise backup database job when running on microservices', async () => { it('should not initialise backup database job when running on microservices', async () => {
mocks.config.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES); mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices);
await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig }); await sut.onConfigInit({ newConfig: systemConfigStub.backupEnabled as SystemConfig });
expect(mocks.cron.create).not.toHaveBeenCalled(); expect(mocks.cron.create).not.toHaveBeenCalled();
@ -98,10 +98,10 @@ describe(BackupService.name, () => {
await sut.cleanupDatabaseBackups(); await sut.cleanupDatabaseBackups();
expect(mocks.storage.unlink).toHaveBeenCalledTimes(2); expect(mocks.storage.unlink).toHaveBeenCalledTimes(2);
expect(mocks.storage.unlink).toHaveBeenCalledWith( expect(mocks.storage.unlink).toHaveBeenCalledWith(
`${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-123.sql.gz.tmp`, `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-123.sql.gz.tmp`,
); );
expect(mocks.storage.unlink).toHaveBeenCalledWith( expect(mocks.storage.unlink).toHaveBeenCalledWith(
`${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-345.sql.gz.tmp`, `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-345.sql.gz.tmp`,
); );
}); });
@ -111,7 +111,7 @@ describe(BackupService.name, () => {
await sut.cleanupDatabaseBackups(); await sut.cleanupDatabaseBackups();
expect(mocks.storage.unlink).toHaveBeenCalledTimes(1); expect(mocks.storage.unlink).toHaveBeenCalledTimes(1);
expect(mocks.storage.unlink).toHaveBeenCalledWith( expect(mocks.storage.unlink).toHaveBeenCalledWith(
`${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-1.sql.gz`, `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1.sql.gz`,
); );
}); });
@ -125,10 +125,10 @@ describe(BackupService.name, () => {
await sut.cleanupDatabaseBackups(); await sut.cleanupDatabaseBackups();
expect(mocks.storage.unlink).toHaveBeenCalledTimes(2); expect(mocks.storage.unlink).toHaveBeenCalledTimes(2);
expect(mocks.storage.unlink).toHaveBeenCalledWith( expect(mocks.storage.unlink).toHaveBeenCalledWith(
`${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-1.sql.gz.tmp`, `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-1.sql.gz.tmp`,
); );
expect(mocks.storage.unlink).toHaveBeenCalledWith( expect(mocks.storage.unlink).toHaveBeenCalledWith(
`${StorageCore.getBaseFolder(StorageFolder.BACKUPS)}/immich-db-backup-2.sql.gz`, `${StorageCore.getBaseFolder(StorageFolder.Backups)}/immich-db-backup-2.sql.gz`,
); );
}); });
}); });
@ -145,13 +145,13 @@ describe(BackupService.name, () => {
it('should run a database backup successfully', async () => { it('should run a database backup successfully', async () => {
const result = await sut.handleBackupDatabase(); const result = await sut.handleBackupDatabase();
expect(result).toBe(JobStatus.SUCCESS); expect(result).toBe(JobStatus.Success);
expect(mocks.storage.createWriteStream).toHaveBeenCalled(); expect(mocks.storage.createWriteStream).toHaveBeenCalled();
}); });
it('should rename file on success', async () => { it('should rename file on success', async () => {
const result = await sut.handleBackupDatabase(); const result = await sut.handleBackupDatabase();
expect(result).toBe(JobStatus.SUCCESS); expect(result).toBe(JobStatus.Success);
expect(mocks.storage.rename).toHaveBeenCalled(); expect(mocks.storage.rename).toHaveBeenCalled();
}); });
@ -219,7 +219,7 @@ describe(BackupService.name, () => {
mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion); mocks.database.getPostgresVersion.mockResolvedValue(postgresVersion);
const result = await sut.handleBackupDatabase(); const result = await sut.handleBackupDatabase();
expect(mocks.process.spawn).not.toHaveBeenCalled(); expect(mocks.process.spawn).not.toHaveBeenCalled();
expect(result).toBe(JobStatus.FAILED); expect(result).toBe(JobStatus.Failed);
}); });
}); });
}); });

View File

@ -14,7 +14,7 @@ import { handlePromiseError } from 'src/utils/misc';
export class BackupService extends BaseService { export class BackupService extends BaseService {
private backupLock = false; private backupLock = false;
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] }) @OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.Microservices] })
async onConfigInit({ async onConfigInit({
newConfig: { newConfig: {
backup: { database }, backup: { database },
@ -26,7 +26,7 @@ export class BackupService extends BaseService {
this.cronRepository.create({ this.cronRepository.create({
name: 'backupDatabase', name: 'backupDatabase',
expression: database.cronExpression, expression: database.cronExpression,
onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.BACKUP_DATABASE }), this.logger), onTick: () => handlePromiseError(this.jobRepository.queue({ name: JobName.BackupDatabase }), this.logger),
start: database.enabled, start: database.enabled,
}); });
} }
@ -51,7 +51,7 @@ export class BackupService extends BaseService {
backup: { database: config }, backup: { database: config },
} = await this.getConfig({ withCache: false }); } = await this.getConfig({ withCache: false });
const backupsFolder = StorageCore.getBaseFolder(StorageFolder.BACKUPS); const backupsFolder = StorageCore.getBaseFolder(StorageFolder.Backups);
const files = await this.storageRepository.readdir(backupsFolder); const files = await this.storageRepository.readdir(backupsFolder);
const failedBackups = files.filter((file) => file.match(/immich-db-backup-\d+\.sql\.gz\.tmp$/)); const failedBackups = files.filter((file) => file.match(/immich-db-backup-\d+\.sql\.gz\.tmp$/));
const backups = files const backups = files
@ -68,7 +68,7 @@ export class BackupService extends BaseService {
this.logger.debug(`Database Backup Cleanup Finished, deleted ${toDelete.length} backups`); this.logger.debug(`Database Backup Cleanup Finished, deleted ${toDelete.length} backups`);
} }
@OnJob({ name: JobName.BACKUP_DATABASE, queue: QueueName.BACKUP_DATABASE }) @OnJob({ name: JobName.BackupDatabase, queue: QueueName.BackupDatabase })
async handleBackupDatabase(): Promise<JobStatus> { async handleBackupDatabase(): Promise<JobStatus> {
this.logger.debug(`Database Backup Started`); this.logger.debug(`Database Backup Started`);
const { database } = this.configRepository.getEnv(); const { database } = this.configRepository.getEnv();
@ -92,7 +92,7 @@ export class BackupService extends BaseService {
databaseParams.push('--clean', '--if-exists'); databaseParams.push('--clean', '--if-exists');
const databaseVersion = await this.databaseRepository.getPostgresVersion(); const databaseVersion = await this.databaseRepository.getPostgresVersion();
const backupFilePath = path.join( const backupFilePath = path.join(
StorageCore.getBaseFolder(StorageFolder.BACKUPS), StorageCore.getBaseFolder(StorageFolder.Backups),
`immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`, `immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz.tmp`,
); );
const databaseSemver = semver.coerce(databaseVersion); const databaseSemver = semver.coerce(databaseVersion);
@ -100,7 +100,7 @@ export class BackupService extends BaseService {
if (!databaseMajorVersion || !databaseSemver || !semver.satisfies(databaseSemver, '>=14.0.0 <18.0.0')) { if (!databaseMajorVersion || !databaseSemver || !semver.satisfies(databaseSemver, '>=14.0.0 <18.0.0')) {
this.logger.error(`Database Backup Failure: Unsupported PostgreSQL version: ${databaseVersion}`); this.logger.error(`Database Backup Failure: Unsupported PostgreSQL version: ${databaseVersion}`);
return JobStatus.FAILED; return JobStatus.Failed;
} }
this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`); this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`);
@ -179,6 +179,6 @@ export class BackupService extends BaseService {
this.logger.log(`Database Backup Success`); this.logger.log(`Database Backup Success`);
await this.cleanupDatabaseBackups(); await this.cleanupDatabaseBackups();
return JobStatus.SUCCESS; return JobStatus.Success;
} }
} }

View File

@ -19,7 +19,7 @@ describe(DatabaseService.name, () => {
({ sut, mocks } = newTestService(DatabaseService)); ({ sut, mocks } = newTestService(DatabaseService));
extensionRange = '0.2.x'; extensionRange = '0.2.x';
mocks.database.getVectorExtension.mockResolvedValue(DatabaseExtension.VECTORCHORD); mocks.database.getVectorExtension.mockResolvedValue(DatabaseExtension.VectorChord);
mocks.database.getExtensionVersionRange.mockReturnValue(extensionRange); mocks.database.getExtensionVersionRange.mockReturnValue(extensionRange);
versionBelowRange = '0.1.0'; versionBelowRange = '0.1.0';
@ -28,7 +28,7 @@ describe(DatabaseService.name, () => {
versionAboveRange = '0.3.0'; versionAboveRange = '0.3.0';
mocks.database.getExtensionVersions.mockResolvedValue([ mocks.database.getExtensionVersions.mockResolvedValue([
{ {
name: DatabaseExtension.VECTORCHORD, name: DatabaseExtension.VectorChord,
installedVersion: null, installedVersion: null,
availableVersion: minVersionInRange, availableVersion: minVersionInRange,
}, },
@ -49,9 +49,9 @@ describe(DatabaseService.name, () => {
}); });
describe.each(<Array<{ extension: VectorExtension; extensionName: string }>>[ describe.each(<Array<{ extension: VectorExtension; extensionName: string }>>[
{ extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] }, { extension: DatabaseExtension.Vector, extensionName: EXTENSION_NAMES[DatabaseExtension.Vector] },
{ extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] }, { extension: DatabaseExtension.Vectors, extensionName: EXTENSION_NAMES[DatabaseExtension.Vectors] },
{ extension: DatabaseExtension.VECTORCHORD, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORCHORD] }, { extension: DatabaseExtension.VectorChord, extensionName: EXTENSION_NAMES[DatabaseExtension.VectorChord] },
])('should work with $extensionName', ({ extension, extensionName }) => { ])('should work with $extensionName', ({ extension, extensionName }) => {
beforeEach(() => { beforeEach(() => {
mocks.database.getExtensionVersions.mockResolvedValue([ mocks.database.getExtensionVersions.mockResolvedValue([
@ -292,8 +292,8 @@ describe(DatabaseService.name, () => {
await expect(sut.onBootstrap()).resolves.toBeUndefined(); await expect(sut.onBootstrap()).resolves.toBeUndefined();
expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledExactlyOnceWith([ expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledExactlyOnceWith([
VectorIndex.CLIP, VectorIndex.Clip,
VectorIndex.FACE, VectorIndex.Face,
]); ]);
expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledTimes(1); expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledTimes(1);
expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1); expect(mocks.database.runMigrations).toHaveBeenCalledTimes(1);
@ -306,8 +306,8 @@ describe(DatabaseService.name, () => {
await expect(sut.onBootstrap()).rejects.toBeDefined(); await expect(sut.onBootstrap()).rejects.toBeDefined();
expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledExactlyOnceWith([ expect(mocks.database.reindexVectorsIfNeeded).toHaveBeenCalledExactlyOnceWith([
VectorIndex.CLIP, VectorIndex.Clip,
VectorIndex.FACE, VectorIndex.Face,
]); ]);
expect(mocks.database.runMigrations).not.toHaveBeenCalled(); expect(mocks.database.runMigrations).not.toHaveBeenCalled();
expect(mocks.logger.fatal).not.toHaveBeenCalled(); expect(mocks.logger.fatal).not.toHaveBeenCalled();
@ -330,7 +330,7 @@ describe(DatabaseService.name, () => {
database: 'immich', database: 'immich',
}, },
skipMigrations: true, skipMigrations: true,
vectorExtension: DatabaseExtension.VECTORS, vectorExtension: DatabaseExtension.Vectors,
}, },
}), }),
); );
@ -356,12 +356,12 @@ describe(DatabaseService.name, () => {
it(`should drop unused extension`, async () => { it(`should drop unused extension`, async () => {
mocks.database.getExtensionVersions.mockResolvedValue([ mocks.database.getExtensionVersions.mockResolvedValue([
{ {
name: DatabaseExtension.VECTORS, name: DatabaseExtension.Vectors,
installedVersion: minVersionInRange, installedVersion: minVersionInRange,
availableVersion: minVersionInRange, availableVersion: minVersionInRange,
}, },
{ {
name: DatabaseExtension.VECTORCHORD, name: DatabaseExtension.VectorChord,
installedVersion: null, installedVersion: null,
availableVersion: minVersionInRange, availableVersion: minVersionInRange,
}, },
@ -369,19 +369,19 @@ describe(DatabaseService.name, () => {
await expect(sut.onBootstrap()).resolves.toBeUndefined(); await expect(sut.onBootstrap()).resolves.toBeUndefined();
expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORCHORD); expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VectorChord);
expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORS); expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.Vectors);
}); });
it(`should warn if unused extension could not be dropped`, async () => { it(`should warn if unused extension could not be dropped`, async () => {
mocks.database.getExtensionVersions.mockResolvedValue([ mocks.database.getExtensionVersions.mockResolvedValue([
{ {
name: DatabaseExtension.VECTORS, name: DatabaseExtension.Vectors,
installedVersion: minVersionInRange, installedVersion: minVersionInRange,
availableVersion: minVersionInRange, availableVersion: minVersionInRange,
}, },
{ {
name: DatabaseExtension.VECTORCHORD, name: DatabaseExtension.VectorChord,
installedVersion: null, installedVersion: null,
availableVersion: minVersionInRange, availableVersion: minVersionInRange,
}, },
@ -390,8 +390,8 @@ describe(DatabaseService.name, () => {
await expect(sut.onBootstrap()).resolves.toBeUndefined(); await expect(sut.onBootstrap()).resolves.toBeUndefined();
expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORCHORD); expect(mocks.database.createExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VectorChord);
expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.VECTORS); expect(mocks.database.dropExtension).toHaveBeenCalledExactlyOnceWith(DatabaseExtension.Vectors);
expect(mocks.logger.warn).toHaveBeenCalledTimes(1); expect(mocks.logger.warn).toHaveBeenCalledTimes(1);
expect(mocks.logger.warn.mock.calls[0][0]).toContain('DROP EXTENSION vectors'); expect(mocks.logger.warn.mock.calls[0][0]).toContain('DROP EXTENSION vectors');
}); });
@ -399,12 +399,12 @@ describe(DatabaseService.name, () => {
it(`should not try to drop pgvector when using vectorchord`, async () => { it(`should not try to drop pgvector when using vectorchord`, async () => {
mocks.database.getExtensionVersions.mockResolvedValue([ mocks.database.getExtensionVersions.mockResolvedValue([
{ {
name: DatabaseExtension.VECTOR, name: DatabaseExtension.Vector,
installedVersion: minVersionInRange, installedVersion: minVersionInRange,
availableVersion: minVersionInRange, availableVersion: minVersionInRange,
}, },
{ {
name: DatabaseExtension.VECTORCHORD, name: DatabaseExtension.VectorChord,
installedVersion: minVersionInRange, installedVersion: minVersionInRange,
availableVersion: minVersionInRange, availableVersion: minVersionInRange,
}, },

View File

@ -100,7 +100,7 @@ export class DatabaseService extends BaseService {
} }
try { try {
await this.databaseRepository.reindexVectorsIfNeeded([VectorIndex.CLIP, VectorIndex.FACE]); await this.databaseRepository.reindexVectorsIfNeeded([VectorIndex.Clip, VectorIndex.Face]);
} catch (error) { } catch (error) {
this.logger.warn( this.logger.warn(
'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance. If you are upgrading directly from a version below 1.107.2, please upgrade to 1.107.2 first.', 'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance. If you are upgrading directly from a version below 1.107.2, please upgrade to 1.107.2 first.',
@ -109,7 +109,7 @@ export class DatabaseService extends BaseService {
} }
for (const { name: dbName, installedVersion } of extensionVersions) { for (const { name: dbName, installedVersion } of extensionVersions) {
const isDepended = dbName === DatabaseExtension.VECTOR && extension === DatabaseExtension.VECTORCHORD; const isDepended = dbName === DatabaseExtension.Vector && extension === DatabaseExtension.VectorChord;
if (dbName !== extension && installedVersion && !isDepended) { if (dbName !== extension && installedVersion && !isDepended) {
await this.dropExtension(dbName); await this.dropExtension(dbName);
} }
@ -120,8 +120,8 @@ export class DatabaseService extends BaseService {
await this.databaseRepository.runMigrations(); await this.databaseRepository.runMigrations();
} }
await Promise.all([ await Promise.all([
this.databaseRepository.prewarm(VectorIndex.CLIP), this.databaseRepository.prewarm(VectorIndex.Clip),
this.databaseRepository.prewarm(VectorIndex.FACE), this.databaseRepository.prewarm(VectorIndex.Face),
]); ]);
}); });
} }

View File

@ -17,15 +17,15 @@ export class DownloadService extends BaseService {
if (dto.assetIds) { if (dto.assetIds) {
const assetIds = dto.assetIds; const assetIds = dto.assetIds;
await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds }); await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: assetIds });
assets = this.downloadRepository.downloadAssetIds(assetIds); assets = this.downloadRepository.downloadAssetIds(assetIds);
} else if (dto.albumId) { } else if (dto.albumId) {
const albumId = dto.albumId; const albumId = dto.albumId;
await this.requireAccess({ auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] }); await this.requireAccess({ auth, permission: Permission.AlbumDownload, ids: [albumId] });
assets = this.downloadRepository.downloadAlbumId(albumId); assets = this.downloadRepository.downloadAlbumId(albumId);
} else if (dto.userId) { } else if (dto.userId) {
const userId = dto.userId; const userId = dto.userId;
await this.requireAccess({ auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] }); await this.requireAccess({ auth, permission: Permission.TimelineDownload, ids: [userId] });
assets = this.downloadRepository.downloadUserId(userId); assets = this.downloadRepository.downloadUserId(userId);
} else { } else {
throw new BadRequestException('assetIds, albumId, or userId is required'); throw new BadRequestException('assetIds, albumId, or userId is required');
@ -81,7 +81,7 @@ export class DownloadService extends BaseService {
} }
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> { async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
await this.requireAccess({ auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds }); await this.requireAccess({ auth, permission: Permission.AssetDownload, ids: dto.assetIds });
const zip = this.storageRepository.createZipStream(); const zip = this.storageRepository.createZipStream();
const assets = await this.assetRepository.getByIds(dto.assetIds); const assets = await this.assetRepository.getByIds(dto.assetIds);

View File

@ -12,10 +12,10 @@ const hasEmbedding = {
id: 'asset-1', id: 'asset-1',
ownerId: 'user-id', ownerId: 'user-id',
stackId: null, stackId: null,
type: AssetType.IMAGE, type: AssetType.Image,
duplicateId: null, duplicateId: null,
embedding: '[1, 2, 3, 4]', embedding: '[1, 2, 3, 4]',
visibility: AssetVisibility.TIMELINE, visibility: AssetVisibility.Timeline,
}; };
const hasDupe = { const hasDupe = {
@ -78,7 +78,7 @@ describe(SearchService.name, () => {
}, },
}); });
await expect(sut.handleQueueSearchDuplicates({})).resolves.toBe(JobStatus.SKIPPED); await expect(sut.handleQueueSearchDuplicates({})).resolves.toBe(JobStatus.Skipped);
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled();
expect(mocks.systemMetadata.get).toHaveBeenCalled(); expect(mocks.systemMetadata.get).toHaveBeenCalled();
@ -94,7 +94,7 @@ describe(SearchService.name, () => {
}, },
}); });
await expect(sut.handleQueueSearchDuplicates({})).resolves.toBe(JobStatus.SKIPPED); await expect(sut.handleQueueSearchDuplicates({})).resolves.toBe(JobStatus.Skipped);
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled();
expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled();
expect(mocks.systemMetadata.get).toHaveBeenCalled(); expect(mocks.systemMetadata.get).toHaveBeenCalled();
@ -108,7 +108,7 @@ describe(SearchService.name, () => {
expect(mocks.assetJob.streamForSearchDuplicates).toHaveBeenCalledWith(undefined); expect(mocks.assetJob.streamForSearchDuplicates).toHaveBeenCalledWith(undefined);
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.DUPLICATE_DETECTION, name: JobName.DuplicateDetection,
data: { id: assetStub.image.id }, data: { id: assetStub.image.id },
}, },
]); ]);
@ -122,7 +122,7 @@ describe(SearchService.name, () => {
expect(mocks.assetJob.streamForSearchDuplicates).toHaveBeenCalledWith(true); expect(mocks.assetJob.streamForSearchDuplicates).toHaveBeenCalledWith(true);
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ {
name: JobName.DUPLICATE_DETECTION, name: JobName.DuplicateDetection,
data: { id: assetStub.image.id }, data: { id: assetStub.image.id },
}, },
]); ]);
@ -154,7 +154,7 @@ describe(SearchService.name, () => {
const result = await sut.handleSearchDuplicates({ id }); const result = await sut.handleSearchDuplicates({ id });
expect(result).toBe(JobStatus.SKIPPED); expect(result).toBe(JobStatus.Skipped);
expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled(); expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled();
}); });
@ -171,7 +171,7 @@ describe(SearchService.name, () => {
const result = await sut.handleSearchDuplicates({ id }); const result = await sut.handleSearchDuplicates({ id });
expect(result).toBe(JobStatus.SKIPPED); expect(result).toBe(JobStatus.Skipped);
expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled(); expect(mocks.assetJob.getForSearchDuplicatesJob).not.toHaveBeenCalled();
}); });
@ -180,7 +180,7 @@ describe(SearchService.name, () => {
const result = await sut.handleSearchDuplicates({ id: assetStub.image.id }); const result = await sut.handleSearchDuplicates({ id: assetStub.image.id });
expect(result).toBe(JobStatus.FAILED); expect(result).toBe(JobStatus.Failed);
expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`); expect(mocks.logger.error).toHaveBeenCalledWith(`Asset ${assetStub.image.id} not found`);
}); });
@ -190,7 +190,7 @@ describe(SearchService.name, () => {
const result = await sut.handleSearchDuplicates({ id }); const result = await sut.handleSearchDuplicates({ id });
expect(result).toBe(JobStatus.SKIPPED); expect(result).toBe(JobStatus.Skipped);
expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is part of a stack, skipping`); expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is part of a stack, skipping`);
}); });
@ -198,12 +198,12 @@ describe(SearchService.name, () => {
const id = assetStub.livePhotoMotionAsset.id; const id = assetStub.livePhotoMotionAsset.id;
mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({ mocks.assetJob.getForSearchDuplicatesJob.mockResolvedValue({
...hasEmbedding, ...hasEmbedding,
visibility: AssetVisibility.HIDDEN, visibility: AssetVisibility.Hidden,
}); });
const result = await sut.handleSearchDuplicates({ id }); const result = await sut.handleSearchDuplicates({ id });
expect(result).toBe(JobStatus.SKIPPED); expect(result).toBe(JobStatus.Skipped);
expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is not visible, skipping`); expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${id} is not visible, skipping`);
}); });
@ -212,7 +212,7 @@ describe(SearchService.name, () => {
const result = await sut.handleSearchDuplicates({ id: assetStub.image.id }); const result = await sut.handleSearchDuplicates({ id: assetStub.image.id });
expect(result).toBe(JobStatus.FAILED); expect(result).toBe(JobStatus.Failed);
expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${assetStub.image.id} is missing embedding`); expect(mocks.logger.debug).toHaveBeenCalledWith(`Asset ${assetStub.image.id} is missing embedding`);
}); });
@ -226,7 +226,7 @@ describe(SearchService.name, () => {
const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id }); const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id });
expect(result).toBe(JobStatus.SUCCESS); expect(result).toBe(JobStatus.Success);
expect(mocks.duplicateRepository.search).toHaveBeenCalledWith({ expect(mocks.duplicateRepository.search).toHaveBeenCalledWith({
assetId: hasEmbedding.id, assetId: hasEmbedding.id,
embedding: hasEmbedding.embedding, embedding: hasEmbedding.embedding,
@ -253,7 +253,7 @@ describe(SearchService.name, () => {
const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id }); const result = await sut.handleSearchDuplicates({ id: hasEmbedding.id });
expect(result).toBe(JobStatus.SUCCESS); expect(result).toBe(JobStatus.Success);
expect(mocks.duplicateRepository.search).toHaveBeenCalledWith({ expect(mocks.duplicateRepository.search).toHaveBeenCalledWith({
assetId: hasEmbedding.id, assetId: hasEmbedding.id,
embedding: hasEmbedding.embedding, embedding: hasEmbedding.embedding,
@ -277,7 +277,7 @@ describe(SearchService.name, () => {
const result = await sut.handleSearchDuplicates({ id: hasDupe.id }); const result = await sut.handleSearchDuplicates({ id: hasDupe.id });
expect(result).toBe(JobStatus.SUCCESS); expect(result).toBe(JobStatus.Success);
expect(mocks.asset.update).toHaveBeenCalledWith({ id: hasDupe.id, duplicateId: null }); expect(mocks.asset.update).toHaveBeenCalledWith({ id: hasDupe.id, duplicateId: null });
expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith({ expect(mocks.asset.upsertJobStatus).toHaveBeenCalledWith({
assetId: hasDupe.id, assetId: hasDupe.id,

View File

@ -29,11 +29,11 @@ export class DuplicateService extends BaseService {
await this.duplicateRepository.deleteAll(auth.user.id, dto.ids); await this.duplicateRepository.deleteAll(auth.user.id, dto.ids);
} }
@OnJob({ name: JobName.QUEUE_DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION }) @OnJob({ name: JobName.QueueDuplicateDetection, queue: QueueName.DuplicateDetection })
async handleQueueSearchDuplicates({ force }: JobOf<JobName.QUEUE_DUPLICATE_DETECTION>): Promise<JobStatus> { async handleQueueSearchDuplicates({ force }: JobOf<JobName.QueueDuplicateDetection>): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: false }); const { machineLearning } = await this.getConfig({ withCache: false });
if (!isDuplicateDetectionEnabled(machineLearning)) { if (!isDuplicateDetectionEnabled(machineLearning)) {
return JobStatus.SKIPPED; return JobStatus.Skipped;
} }
let jobs: JobItem[] = []; let jobs: JobItem[] = [];
@ -44,7 +44,7 @@ export class DuplicateService extends BaseService {
const assets = this.assetJobRepository.streamForSearchDuplicates(force); const assets = this.assetJobRepository.streamForSearchDuplicates(force);
for await (const asset of assets) { for await (const asset of assets) {
jobs.push({ name: JobName.DUPLICATE_DETECTION, data: { id: asset.id } }); jobs.push({ name: JobName.DuplicateDetection, data: { id: asset.id } });
if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) { if (jobs.length >= JOBS_ASSET_PAGINATION_SIZE) {
await queueAll(); await queueAll();
} }
@ -52,40 +52,40 @@ export class DuplicateService extends BaseService {
await queueAll(); await queueAll();
return JobStatus.SUCCESS; return JobStatus.Success;
} }
@OnJob({ name: JobName.DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION }) @OnJob({ name: JobName.DuplicateDetection, queue: QueueName.DuplicateDetection })
async handleSearchDuplicates({ id }: JobOf<JobName.DUPLICATE_DETECTION>): Promise<JobStatus> { async handleSearchDuplicates({ id }: JobOf<JobName.DuplicateDetection>): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: true }); const { machineLearning } = await this.getConfig({ withCache: true });
if (!isDuplicateDetectionEnabled(machineLearning)) { if (!isDuplicateDetectionEnabled(machineLearning)) {
return JobStatus.SKIPPED; return JobStatus.Skipped;
} }
const asset = await this.assetJobRepository.getForSearchDuplicatesJob(id); const asset = await this.assetJobRepository.getForSearchDuplicatesJob(id);
if (!asset) { if (!asset) {
this.logger.error(`Asset ${id} not found`); this.logger.error(`Asset ${id} not found`);
return JobStatus.FAILED; return JobStatus.Failed;
} }
if (asset.stackId) { if (asset.stackId) {
this.logger.debug(`Asset ${id} is part of a stack, skipping`); this.logger.debug(`Asset ${id} is part of a stack, skipping`);
return JobStatus.SKIPPED; return JobStatus.Skipped;
} }
if (asset.visibility === AssetVisibility.HIDDEN) { if (asset.visibility === AssetVisibility.Hidden) {
this.logger.debug(`Asset ${id} is not visible, skipping`); this.logger.debug(`Asset ${id} is not visible, skipping`);
return JobStatus.SKIPPED; return JobStatus.Skipped;
} }
if (asset.visibility === AssetVisibility.LOCKED) { if (asset.visibility === AssetVisibility.Locked) {
this.logger.debug(`Asset ${id} is locked, skipping`); this.logger.debug(`Asset ${id} is locked, skipping`);
return JobStatus.SKIPPED; return JobStatus.Skipped;
} }
if (!asset.embedding) { if (!asset.embedding) {
this.logger.debug(`Asset ${id} is missing embedding`); this.logger.debug(`Asset ${id} is missing embedding`);
return JobStatus.FAILED; return JobStatus.Failed;
} }
const duplicateAssets = await this.duplicateRepository.search({ const duplicateAssets = await this.duplicateRepository.search({
@ -110,7 +110,7 @@ export class DuplicateService extends BaseService {
const duplicatesDetectedAt = new Date(); const duplicatesDetectedAt = new Date();
await this.assetRepository.upsertJobStatus(...assetIds.map((assetId) => ({ assetId, duplicatesDetectedAt }))); await this.assetRepository.upsertJobStatus(...assetIds.map((assetId) => ({ assetId, duplicatesDetectedAt })));
return JobStatus.SUCCESS; return JobStatus.Success;
} }
private async updateDuplicates( private async updateDuplicates(

View File

@ -13,7 +13,7 @@ describe(JobService.name, () => {
beforeEach(() => { beforeEach(() => {
({ sut, mocks } = newTestService(JobService, {})); ({ sut, mocks } = newTestService(JobService, {}));
mocks.config.getWorker.mockReturnValue(ImmichWorker.MICROSERVICES); mocks.config.getWorker.mockReturnValue(ImmichWorker.Microservices);
}); });
it('should work', () => { it('should work', () => {
@ -25,10 +25,10 @@ describe(JobService.name, () => {
sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig }); sut.onConfigUpdate({ newConfig: defaults, oldConfig: {} as SystemConfig });
expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(15); expect(mocks.job.setConcurrency).toHaveBeenCalledTimes(15);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FACIAL_RECOGNITION, 1); expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(5, QueueName.FacialRecognition, 1);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DUPLICATE_DETECTION, 1); expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(7, QueueName.DuplicateDetection, 1);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BACKGROUND_TASK, 5); expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(8, QueueName.BackgroundTask, 5);
expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.STORAGE_TEMPLATE_MIGRATION, 1); expect(mocks.job.setConcurrency).toHaveBeenNthCalledWith(9, QueueName.StorageTemplateMigration, 1);
}); });
}); });
@ -37,16 +37,16 @@ describe(JobService.name, () => {
await sut.handleNightlyJobs(); await sut.handleNightlyJobs();
expect(mocks.job.queueAll).toHaveBeenCalledWith([ expect(mocks.job.queueAll).toHaveBeenCalledWith([
{ name: JobName.ASSET_DELETION_CHECK }, { name: JobName.AssetDeletionCheck },
{ name: JobName.USER_DELETE_CHECK }, { name: JobName.UserDeleteCheck },
{ name: JobName.PERSON_CLEANUP }, { name: JobName.PersonCleanup },
{ name: JobName.MEMORIES_CLEANUP }, { name: JobName.MemoriesCleanup },
{ name: JobName.CLEAN_OLD_SESSION_TOKENS }, { name: JobName.CleanOldSessionTokens },
{ name: JobName.CLEAN_OLD_AUDIT_LOGS }, { name: JobName.CleanOldAuditLogs },
{ name: JobName.MEMORIES_CREATE }, { name: JobName.MemoriesCreate },
{ name: JobName.USER_SYNC_USAGE }, { name: JobName.userSyncUsage },
{ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }, { name: JobName.QueueGenerateThumbnails, data: { force: false } },
{ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } }, { name: JobName.QueueFacialRecognition, data: { force: false, nightly: true } },
]); ]);
}); });
}); });
@ -82,49 +82,49 @@ describe(JobService.name, () => {
}; };
await expect(sut.getAllJobsStatus()).resolves.toEqual({ await expect(sut.getAllJobsStatus()).resolves.toEqual({
[QueueName.BACKGROUND_TASK]: expectedJobStatus, [QueueName.BackgroundTask]: expectedJobStatus,
[QueueName.DUPLICATE_DETECTION]: expectedJobStatus, [QueueName.DuplicateDetection]: expectedJobStatus,
[QueueName.SMART_SEARCH]: expectedJobStatus, [QueueName.SmartSearch]: expectedJobStatus,
[QueueName.METADATA_EXTRACTION]: expectedJobStatus, [QueueName.MetadataExtraction]: expectedJobStatus,
[QueueName.SEARCH]: expectedJobStatus, [QueueName.Search]: expectedJobStatus,
[QueueName.STORAGE_TEMPLATE_MIGRATION]: expectedJobStatus, [QueueName.StorageTemplateMigration]: expectedJobStatus,
[QueueName.MIGRATION]: expectedJobStatus, [QueueName.Migration]: expectedJobStatus,
[QueueName.THUMBNAIL_GENERATION]: expectedJobStatus, [QueueName.ThumbnailGeneration]: expectedJobStatus,
[QueueName.VIDEO_CONVERSION]: expectedJobStatus, [QueueName.VideoConversion]: expectedJobStatus,
[QueueName.FACE_DETECTION]: expectedJobStatus, [QueueName.FaceDetection]: expectedJobStatus,
[QueueName.FACIAL_RECOGNITION]: expectedJobStatus, [QueueName.FacialRecognition]: expectedJobStatus,
[QueueName.SIDECAR]: expectedJobStatus, [QueueName.Sidecar]: expectedJobStatus,
[QueueName.LIBRARY]: expectedJobStatus, [QueueName.Library]: expectedJobStatus,
[QueueName.NOTIFICATION]: expectedJobStatus, [QueueName.Notification]: expectedJobStatus,
[QueueName.BACKUP_DATABASE]: expectedJobStatus, [QueueName.BackupDatabase]: expectedJobStatus,
}); });
}); });
}); });
describe('handleCommand', () => { describe('handleCommand', () => {
it('should handle a pause command', async () => { it('should handle a pause command', async () => {
await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.PAUSE, force: false }); await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Pause, force: false });
expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); expect(mocks.job.pause).toHaveBeenCalledWith(QueueName.MetadataExtraction);
}); });
it('should handle a resume command', async () => { it('should handle a resume command', async () => {
await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.RESUME, force: false }); await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Resume, force: false });
expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); expect(mocks.job.resume).toHaveBeenCalledWith(QueueName.MetadataExtraction);
}); });
it('should handle an empty command', async () => { it('should handle an empty command', async () => {
await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.EMPTY, force: false }); await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Empty, force: false });
expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.METADATA_EXTRACTION); expect(mocks.job.empty).toHaveBeenCalledWith(QueueName.MetadataExtraction);
}); });
it('should not start a job that is already running', async () => { it('should not start a job that is already running', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: true, isPaused: false });
await expect( await expect(
sut.handleCommand(QueueName.VIDEO_CONVERSION, { command: JobCommand.START, force: false }), sut.handleCommand(QueueName.VideoConversion, { command: JobCommand.Start, force: false }),
).rejects.toBeInstanceOf(BadRequestException); ).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled();
@ -134,80 +134,80 @@ describe(JobService.name, () => {
it('should handle a start video conversion command', async () => { it('should handle a start video conversion command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.VIDEO_CONVERSION, { command: JobCommand.START, force: false }); await sut.handleCommand(QueueName.VideoConversion, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_VIDEO_CONVERSION, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QueueVideoConversion, data: { force: false } });
}); });
it('should handle a start storage template migration command', async () => { it('should handle a start storage template migration command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.STORAGE_TEMPLATE_MIGRATION, { command: JobCommand.START, force: false }); await sut.handleCommand(QueueName.StorageTemplateMigration, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.STORAGE_TEMPLATE_MIGRATION }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.StorageTemplateMigration });
}); });
it('should handle a start smart search command', async () => { it('should handle a start smart search command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.SMART_SEARCH, { command: JobCommand.START, force: false }); await sut.handleCommand(QueueName.SmartSearch, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_SMART_SEARCH, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QueueSmartSearch, data: { force: false } });
}); });
it('should handle a start metadata extraction command', async () => { it('should handle a start metadata extraction command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.METADATA_EXTRACTION, { command: JobCommand.START, force: false }); await sut.handleCommand(QueueName.MetadataExtraction, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QueueMetadataExtraction, data: { force: false } });
}); });
it('should handle a start sidecar command', async () => { it('should handle a start sidecar command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.SIDECAR, { command: JobCommand.START, force: false }); await sut.handleCommand(QueueName.Sidecar, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_SIDECAR, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QueueSidecar, data: { force: false } });
}); });
it('should handle a start thumbnail generation command', async () => { it('should handle a start thumbnail generation command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.THUMBNAIL_GENERATION, { command: JobCommand.START, force: false }); await sut.handleCommand(QueueName.ThumbnailGeneration, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QueueGenerateThumbnails, data: { force: false } });
}); });
it('should handle a start face detection command', async () => { it('should handle a start face detection command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.FACE_DETECTION, { command: JobCommand.START, force: false }); await sut.handleCommand(QueueName.FaceDetection, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_FACE_DETECTION, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QueueFaceDetection, data: { force: false } });
}); });
it('should handle a start facial recognition command', async () => { it('should handle a start facial recognition command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.FACIAL_RECOGNITION, { command: JobCommand.START, force: false }); await sut.handleCommand(QueueName.FacialRecognition, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.QueueFacialRecognition, data: { force: false } });
}); });
it('should handle a start backup database command', async () => { it('should handle a start backup database command', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await sut.handleCommand(QueueName.BACKUP_DATABASE, { command: JobCommand.START, force: false }); await sut.handleCommand(QueueName.BackupDatabase, { command: JobCommand.Start, force: false });
expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.BACKUP_DATABASE, data: { force: false } }); expect(mocks.job.queue).toHaveBeenCalledWith({ name: JobName.BackupDatabase, data: { force: false } });
}); });
it('should throw a bad request when an invalid queue is used', async () => { it('should throw a bad request when an invalid queue is used', async () => {
mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); mocks.job.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false });
await expect( await expect(
sut.handleCommand(QueueName.BACKGROUND_TASK, { command: JobCommand.START, force: false }), sut.handleCommand(QueueName.BackgroundTask, { command: JobCommand.Start, force: false }),
).rejects.toBeInstanceOf(BadRequestException); ).rejects.toBeInstanceOf(BadRequestException);
expect(mocks.job.queue).not.toHaveBeenCalled(); expect(mocks.job.queue).not.toHaveBeenCalled();
@ -217,10 +217,10 @@ describe(JobService.name, () => {
describe('onJobStart', () => { describe('onJobStart', () => {
it('should process a successful job', async () => { it('should process a successful job', async () => {
mocks.job.run.mockResolvedValue(JobStatus.SUCCESS); mocks.job.run.mockResolvedValue(JobStatus.Success);
await sut.onJobStart(QueueName.BACKGROUND_TASK, { await sut.onJobStart(QueueName.BackgroundTask, {
name: JobName.DELETE_FILES, name: JobName.DeleteFiles,
data: { files: ['path/to/file'] }, data: { files: ['path/to/file'] },
}); });
@ -232,55 +232,55 @@ describe(JobService.name, () => {
const tests: Array<{ item: JobItem; jobs: JobName[]; stub?: any }> = [ const tests: Array<{ item: JobItem; jobs: JobName[]; stub?: any }> = [
{ {
item: { name: JobName.SIDECAR_SYNC, data: { id: 'asset-1' } }, item: { name: JobName.SidecarSync, data: { id: 'asset-1' } },
jobs: [JobName.METADATA_EXTRACTION], jobs: [JobName.MetadataExtraction],
}, },
{ {
item: { name: JobName.SIDECAR_DISCOVERY, data: { id: 'asset-1' } }, item: { name: JobName.SidecarDiscovery, data: { id: 'asset-1' } },
jobs: [JobName.METADATA_EXTRACTION], jobs: [JobName.MetadataExtraction],
}, },
{ {
item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1', source: 'upload' } }, item: { name: JobName.StorageTemplateMigrationSingle, data: { id: 'asset-1', source: 'upload' } },
jobs: [JobName.GENERATE_THUMBNAILS], jobs: [JobName.GenerateThumbnails],
}, },
{ {
item: { name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { id: 'asset-1' } }, item: { name: JobName.StorageTemplateMigrationSingle, data: { id: 'asset-1' } },
jobs: [], jobs: [],
}, },
{ {
item: { name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: 'asset-1' } }, item: { name: JobName.GeneratePersonThumbnail, data: { id: 'asset-1' } },
jobs: [], jobs: [],
}, },
{ {
item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }, item: { name: JobName.GenerateThumbnails, data: { id: 'asset-1' } },
jobs: [], jobs: [],
stub: [assetStub.image], stub: [assetStub.image],
}, },
{ {
item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1' } }, item: { name: JobName.GenerateThumbnails, data: { id: 'asset-1' } },
jobs: [], jobs: [],
stub: [assetStub.video], stub: [assetStub.video],
}, },
{ {
item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1', source: 'upload' } }, item: { name: JobName.GenerateThumbnails, data: { id: 'asset-1', source: 'upload' } },
jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION], jobs: [JobName.SmartSearch, JobName.FaceDetection],
stub: [assetStub.livePhotoStillAsset], stub: [assetStub.livePhotoStillAsset],
}, },
{ {
item: { name: JobName.GENERATE_THUMBNAILS, data: { id: 'asset-1', source: 'upload' } }, item: { name: JobName.GenerateThumbnails, data: { id: 'asset-1', source: 'upload' } },
jobs: [JobName.SMART_SEARCH, JobName.FACE_DETECTION, JobName.VIDEO_CONVERSION], jobs: [JobName.SmartSearch, JobName.FaceDetection, JobName.VideoConversation],
stub: [assetStub.video], stub: [assetStub.video],
}, },
{ {
item: { name: JobName.SMART_SEARCH, data: { id: 'asset-1' } }, item: { name: JobName.SmartSearch, data: { id: 'asset-1' } },
jobs: [], jobs: [],
}, },
{ {
item: { name: JobName.FACE_DETECTION, data: { id: 'asset-1' } }, item: { name: JobName.FaceDetection, data: { id: 'asset-1' } },
jobs: [], jobs: [],
}, },
{ {
item: { name: JobName.FACIAL_RECOGNITION, data: { id: 'asset-1' } }, item: { name: JobName.FacialRecognition, data: { id: 'asset-1' } },
jobs: [], jobs: [],
}, },
]; ];
@ -291,9 +291,9 @@ describe(JobService.name, () => {
mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue(stub); mocks.asset.getByIdsWithAllRelationsButStacks.mockResolvedValue(stub);
} }
mocks.job.run.mockResolvedValue(JobStatus.SUCCESS); mocks.job.run.mockResolvedValue(JobStatus.Success);
await sut.onJobStart(QueueName.BACKGROUND_TASK, item); await sut.onJobStart(QueueName.BackgroundTask, item);
if (jobs.length > 1) { if (jobs.length > 1) {
expect(mocks.job.queueAll).toHaveBeenCalledWith( expect(mocks.job.queueAll).toHaveBeenCalledWith(
@ -308,9 +308,9 @@ describe(JobService.name, () => {
}); });
it(`should not queue any jobs when ${item.name} fails`, async () => { it(`should not queue any jobs when ${item.name} fails`, async () => {
mocks.job.run.mockResolvedValue(JobStatus.FAILED); mocks.job.run.mockResolvedValue(JobStatus.Failed);
await sut.onJobStart(QueueName.BACKGROUND_TASK, item); await sut.onJobStart(QueueName.BackgroundTask, item);
expect(mocks.job.queueAll).not.toHaveBeenCalled(); expect(mocks.job.queueAll).not.toHaveBeenCalled();
}); });

Some files were not shown because too many files have changed in this diff Show More