mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Merge branch 'main' into feat/search-filter-album/web
This commit is contained in:
Generated
+1904
-881
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -44,18 +44,18 @@
|
||||
"@nestjs/swagger": "^11.0.2",
|
||||
"@nestjs/websockets": "^11.0.4",
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.60.0",
|
||||
"@opentelemetry/auto-instrumentations-node": "^0.62.0",
|
||||
"@opentelemetry/context-async-hooks": "^2.0.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.202.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.202.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.50.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.48.0",
|
||||
"@opentelemetry/instrumentation-pg": "^0.54.0",
|
||||
"@opentelemetry/exporter-prometheus": "^0.203.0",
|
||||
"@opentelemetry/instrumentation-http": "^0.203.0",
|
||||
"@opentelemetry/instrumentation-ioredis": "^0.51.0",
|
||||
"@opentelemetry/instrumentation-nestjs-core": "^0.49.0",
|
||||
"@opentelemetry/instrumentation-pg": "^0.55.0",
|
||||
"@opentelemetry/resources": "^2.0.1",
|
||||
"@opentelemetry/sdk-metrics": "^2.0.1",
|
||||
"@opentelemetry/sdk-node": "^0.202.0",
|
||||
"@opentelemetry/sdk-node": "^0.203.0",
|
||||
"@opentelemetry/semantic-conventions": "^1.34.0",
|
||||
"@react-email/components": "^0.1.0",
|
||||
"@react-email/components": "^0.2.0",
|
||||
"@react-email/render": "^1.1.2",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"archiver": "^7.0.0",
|
||||
|
||||
@@ -73,11 +73,11 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
);
|
||||
|
||||
this.eventRepository.setup({ services });
|
||||
await this.eventRepository.emit('app.bootstrap');
|
||||
await this.eventRepository.emit('AppBootstrap');
|
||||
}
|
||||
|
||||
async onModuleDestroy() {
|
||||
await this.eventRepository.emit('app.shutdown');
|
||||
await this.eventRepository.emit('AppShutdown');
|
||||
await teardownTelemetry();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,13 @@ describe(APIKeyController.name, () => {
|
||||
expect(status).toBe(400);
|
||||
expect(body).toEqual(factory.responses.badRequest(['id must be a UUID']));
|
||||
});
|
||||
|
||||
it('should allow updating just the name', async () => {
|
||||
const { status } = await request(ctx.getHttpServer())
|
||||
.put(`/api-keys/${factory.uuid()}`)
|
||||
.send({ name: 'new name' });
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /api-keys/:id', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
|
||||
import { IsNotEmpty, IsString, ValidateIf } from 'class-validator';
|
||||
import { Activity } from 'src/database';
|
||||
import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { Optional, ValidateUUID } from 'src/validation';
|
||||
import { ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export enum ReactionType {
|
||||
COMMENT = 'comment',
|
||||
@@ -19,7 +19,7 @@ export type MaybeDuplicate<T> = { duplicate: boolean; value: T };
|
||||
export class ActivityResponseDto {
|
||||
id!: string;
|
||||
createdAt!: Date;
|
||||
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
|
||||
@ValidateEnum({ enum: ReactionType, name: 'ReactionType' })
|
||||
type!: ReactionType;
|
||||
user!: UserResponseDto;
|
||||
assetId!: string | null;
|
||||
@@ -43,14 +43,10 @@ export class ActivityDto {
|
||||
}
|
||||
|
||||
export class ActivitySearchDto extends ActivityDto {
|
||||
@IsEnum(ReactionType)
|
||||
@Optional()
|
||||
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
|
||||
@ValidateEnum({ enum: ReactionType, name: 'ReactionType', optional: true })
|
||||
type?: ReactionType;
|
||||
|
||||
@IsEnum(ReactionLevel)
|
||||
@Optional()
|
||||
@ApiProperty({ enumName: 'ReactionLevel', enum: ReactionLevel })
|
||||
@ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', optional: true })
|
||||
level?: ReactionLevel;
|
||||
|
||||
@ValidateUUID({ optional: true })
|
||||
@@ -60,8 +56,7 @@ export class ActivitySearchDto extends ActivityDto {
|
||||
const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT;
|
||||
|
||||
export class ActivityCreateDto extends ActivityDto {
|
||||
@IsEnum(ReactionType)
|
||||
@ApiProperty({ enumName: 'ReactionType', enum: ReactionType })
|
||||
@ValidateEnum({ enum: ReactionType, name: 'ReactionType' })
|
||||
type!: ReactionType;
|
||||
|
||||
@ValidateIf(isComment)
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsEnum, IsString, ValidateNested } from 'class-validator';
|
||||
import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator';
|
||||
import _ from 'lodash';
|
||||
import { AlbumUser, AuthSharedLink, User } from 'src/database';
|
||||
import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
import { AlbumUserRole, AssetOrder } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class AlbumInfoDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
@@ -18,8 +18,7 @@ export class AlbumUserAddDto {
|
||||
@ValidateUUID()
|
||||
userId!: string;
|
||||
|
||||
@IsEnum(AlbumUserRole)
|
||||
@ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole', default: AlbumUserRole.EDITOR })
|
||||
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', default: AlbumUserRole.EDITOR })
|
||||
role?: AlbumUserRole;
|
||||
}
|
||||
|
||||
@@ -32,8 +31,7 @@ export class AlbumUserCreateDto {
|
||||
@ValidateUUID()
|
||||
userId!: string;
|
||||
|
||||
@IsEnum(AlbumUserRole)
|
||||
@ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' })
|
||||
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
|
||||
role!: AlbumUserRole;
|
||||
}
|
||||
|
||||
@@ -71,9 +69,7 @@ export class UpdateAlbumDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
isActivityEnabled?: boolean;
|
||||
|
||||
@IsEnum(AssetOrder)
|
||||
@Optional()
|
||||
@ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
|
||||
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
|
||||
order?: AssetOrder;
|
||||
}
|
||||
|
||||
@@ -107,14 +103,13 @@ export class AlbumStatisticsResponseDto {
|
||||
}
|
||||
|
||||
export class UpdateAlbumUserDto {
|
||||
@IsEnum(AlbumUserRole)
|
||||
@ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' })
|
||||
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
|
||||
role!: AlbumUserRole;
|
||||
}
|
||||
|
||||
export class AlbumUserResponseDto {
|
||||
user!: UserResponseDto;
|
||||
@ApiProperty({ enum: AlbumUserRole, enumName: 'AlbumUserRole' })
|
||||
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
|
||||
role!: AlbumUserRole;
|
||||
}
|
||||
|
||||
@@ -137,8 +132,7 @@ export class AlbumResponseDto {
|
||||
startDate?: Date;
|
||||
endDate?: Date;
|
||||
isActivityEnabled!: boolean;
|
||||
@Optional()
|
||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
|
||||
order?: AssetOrder;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayMinSize, IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { ArrayMinSize, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { Permission } from 'src/enum';
|
||||
import { Optional } from 'src/validation';
|
||||
import { Optional, ValidateEnum } from 'src/validation';
|
||||
export class APIKeyCreateDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
name?: string;
|
||||
|
||||
@IsEnum(Permission, { each: true })
|
||||
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
|
||||
@ValidateEnum({ enum: Permission, name: 'Permission', each: true })
|
||||
@ArrayMinSize(1)
|
||||
permissions!: Permission[];
|
||||
}
|
||||
@@ -20,9 +18,7 @@ export class APIKeyUpdateDto {
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@Optional()
|
||||
@IsEnum(Permission, { each: true })
|
||||
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
|
||||
@ValidateEnum({ enum: Permission, name: 'Permission', each: true, optional: true })
|
||||
@ArrayMinSize(1)
|
||||
permissions?: Permission[];
|
||||
}
|
||||
@@ -37,6 +33,6 @@ export class APIKeyResponseDto {
|
||||
name!: string;
|
||||
createdAt!: Date;
|
||||
updatedAt!: Date;
|
||||
@ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true })
|
||||
@ValidateEnum({ enum: Permission, name: 'Permission', each: true })
|
||||
permissions!: Permission[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ValidateEnum } from 'src/validation';
|
||||
|
||||
export enum AssetMediaStatus {
|
||||
CREATED = 'created',
|
||||
@@ -6,7 +6,7 @@ export enum AssetMediaStatus {
|
||||
DUPLICATE = 'duplicate',
|
||||
}
|
||||
export class AssetMediaResponseDto {
|
||||
@ApiProperty({ enum: AssetMediaStatus, enumName: 'AssetMediaStatus' })
|
||||
@ValidateEnum({ enum: AssetMediaStatus, name: 'AssetMediaStatus' })
|
||||
status!: AssetMediaStatus;
|
||||
id!: string;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsEnum, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||
import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||
import { AssetVisibility } from 'src/enum';
|
||||
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export enum AssetMediaSize {
|
||||
/**
|
||||
@@ -15,9 +15,7 @@ export enum AssetMediaSize {
|
||||
}
|
||||
|
||||
export class AssetMediaOptionsDto {
|
||||
@Optional()
|
||||
@IsEnum(AssetMediaSize)
|
||||
@ApiProperty({ enumName: 'AssetMediaSize', enum: AssetMediaSize })
|
||||
@ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', optional: true })
|
||||
size?: AssetMediaSize;
|
||||
}
|
||||
|
||||
@@ -60,7 +58,7 @@ export class AssetMediaCreateDto extends AssetMediaBase {
|
||||
@ValidateBoolean({ optional: true })
|
||||
isFavorite?: boolean;
|
||||
|
||||
@ValidateAssetVisibility({ optional: true })
|
||||
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
|
||||
visibility?: AssetVisibility;
|
||||
|
||||
@ValidateUUID({ optional: true })
|
||||
|
||||
@@ -15,10 +15,11 @@ import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
||||
import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { ValidateEnum } from 'src/validation';
|
||||
|
||||
export class SanitizedAssetResponseDto {
|
||||
id!: string;
|
||||
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
|
||||
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' })
|
||||
type!: AssetType;
|
||||
thumbhash!: string | null;
|
||||
originalMimeType?: string;
|
||||
@@ -72,7 +73,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||
isArchived!: boolean;
|
||||
isTrashed!: boolean;
|
||||
isOffline!: boolean;
|
||||
@ApiProperty({ enum: AssetVisibility, enumName: 'AssetVisibility' })
|
||||
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' })
|
||||
visibility!: AssetVisibility;
|
||||
exifInfo?: ExifResponseDto;
|
||||
tags?: TagResponseDto[];
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsDateString,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsLatitude,
|
||||
IsLongitude,
|
||||
@@ -16,7 +15,7 @@ import {
|
||||
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||
import { AssetType, AssetVisibility } from 'src/enum';
|
||||
import { AssetStats } from 'src/repositories/asset.repository';
|
||||
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class DeviceIdDto {
|
||||
@IsNotEmpty()
|
||||
@@ -32,7 +31,7 @@ export class UpdateAssetBase {
|
||||
@ValidateBoolean({ optional: true })
|
||||
isFavorite?: boolean;
|
||||
|
||||
@ValidateAssetVisibility({ optional: true })
|
||||
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
|
||||
visibility?: AssetVisibility;
|
||||
|
||||
@Optional()
|
||||
@@ -99,13 +98,12 @@ export enum AssetJobName {
|
||||
}
|
||||
|
||||
export class AssetJobsDto extends AssetIdsDto {
|
||||
@ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName })
|
||||
@IsEnum(AssetJobName)
|
||||
@ValidateEnum({ enum: AssetJobName, name: 'AssetJobName' })
|
||||
name!: AssetJobName;
|
||||
}
|
||||
|
||||
export class AssetStatsDto {
|
||||
@ValidateAssetVisibility({ optional: true })
|
||||
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
|
||||
visibility?: AssetVisibility;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
import { JobCommand, ManualJobName, QueueName } from 'src/enum';
|
||||
import { ValidateBoolean } from 'src/validation';
|
||||
import { ValidateBoolean, ValidateEnum } from 'src/validation';
|
||||
|
||||
export class JobIdParamDto {
|
||||
@IsNotEmpty()
|
||||
@IsEnum(QueueName)
|
||||
@ApiProperty({ type: String, enum: QueueName, enumName: 'JobName' })
|
||||
@ValidateEnum({ enum: QueueName, name: 'JobName' })
|
||||
id!: QueueName;
|
||||
}
|
||||
|
||||
export class JobCommandDto {
|
||||
@IsNotEmpty()
|
||||
@IsEnum(JobCommand)
|
||||
@ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' })
|
||||
@ValidateEnum({ enum: JobCommand, name: 'JobCommand' })
|
||||
command!: JobCommand;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
@@ -21,8 +16,7 @@ export class JobCommandDto {
|
||||
}
|
||||
|
||||
export class JobCreateDto {
|
||||
@IsEnum(ManualJobName)
|
||||
@ApiProperty({ type: 'string', enum: ManualJobName, enumName: 'ManualJobName' })
|
||||
@ValidateEnum({ enum: ManualJobName, name: 'ManualJobName' })
|
||||
name!: ManualJobName;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator';
|
||||
import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator';
|
||||
import { Memory } from 'src/database';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { MemoryType } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
class MemoryBaseDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
@@ -16,9 +16,7 @@ class MemoryBaseDto {
|
||||
}
|
||||
|
||||
export class MemorySearchDto {
|
||||
@Optional()
|
||||
@IsEnum(MemoryType)
|
||||
@ApiProperty({ enum: MemoryType, enumName: 'MemoryType' })
|
||||
@ValidateEnum({ enum: MemoryType, name: 'MemoryType', optional: true })
|
||||
type?: MemoryType;
|
||||
|
||||
@ValidateDate({ optional: true })
|
||||
@@ -45,8 +43,7 @@ export class MemoryUpdateDto extends MemoryBaseDto {
|
||||
}
|
||||
|
||||
export class MemoryCreateDto extends MemoryBaseDto {
|
||||
@IsEnum(MemoryType)
|
||||
@ApiProperty({ enum: MemoryType, enumName: 'MemoryType' })
|
||||
@ValidateEnum({ enum: MemoryType, name: 'MemoryType' })
|
||||
type!: MemoryType;
|
||||
|
||||
@IsObject()
|
||||
@@ -86,7 +83,7 @@ export class MemoryResponseDto {
|
||||
showAt?: Date;
|
||||
hideAt?: Date;
|
||||
ownerId!: string;
|
||||
@ApiProperty({ enumName: 'MemoryType', enum: MemoryType })
|
||||
@ValidateEnum({ enum: MemoryType, name: 'MemoryType' })
|
||||
type!: MemoryType;
|
||||
data!: MemoryData;
|
||||
isSaved!: boolean;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsString } from 'class-validator';
|
||||
import { IsString } from 'class-validator';
|
||||
import { NotificationLevel, NotificationType } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class TestEmailResponseDto {
|
||||
messageId!: string;
|
||||
@@ -19,9 +18,9 @@ export class NotificationDto {
|
||||
id!: string;
|
||||
@ValidateDate()
|
||||
createdAt!: Date;
|
||||
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||
@ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel' })
|
||||
level!: NotificationLevel;
|
||||
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||
@ValidateEnum({ enum: NotificationType, name: 'NotificationType' })
|
||||
type!: NotificationType;
|
||||
title!: string;
|
||||
description?: string;
|
||||
@@ -30,18 +29,13 @@ export class NotificationDto {
|
||||
}
|
||||
|
||||
export class NotificationSearchDto {
|
||||
@Optional()
|
||||
@ValidateUUID({ optional: true })
|
||||
id?: string;
|
||||
|
||||
@IsEnum(NotificationLevel)
|
||||
@Optional()
|
||||
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||
@ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true })
|
||||
level?: NotificationLevel;
|
||||
|
||||
@IsEnum(NotificationType)
|
||||
@Optional()
|
||||
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true })
|
||||
type?: NotificationType;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
@@ -49,14 +43,10 @@ export class NotificationSearchDto {
|
||||
}
|
||||
|
||||
export class NotificationCreateDto {
|
||||
@Optional()
|
||||
@IsEnum(NotificationLevel)
|
||||
@ApiProperty({ enum: NotificationLevel, enumName: 'NotificationLevel' })
|
||||
@ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true })
|
||||
level?: NotificationLevel;
|
||||
|
||||
@IsEnum(NotificationType)
|
||||
@Optional()
|
||||
@ApiProperty({ enum: NotificationType, enumName: 'NotificationType' })
|
||||
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true })
|
||||
type?: NotificationType;
|
||||
|
||||
@IsString()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { IsBoolean, IsNotEmpty } from 'class-validator';
|
||||
import { ValidateBoolean } from 'src/validation';
|
||||
|
||||
export class OnboardingDto {
|
||||
@IsBoolean()
|
||||
@IsNotEmpty()
|
||||
@ValidateBoolean()
|
||||
isOnboarded!: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsNotEmpty } from 'class-validator';
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { PartnerDirection } from 'src/repositories/partner.repository';
|
||||
import { ValidateEnum } from 'src/validation';
|
||||
|
||||
export class UpdatePartnerDto {
|
||||
@IsNotEmpty()
|
||||
@@ -9,8 +9,7 @@ export class UpdatePartnerDto {
|
||||
}
|
||||
|
||||
export class PartnerSearchDto {
|
||||
@IsEnum(PartnerDirection)
|
||||
@ApiProperty({ enum: PartnerDirection, enumName: 'PartnerDirection' })
|
||||
@ValidateEnum({ enum: PartnerDirection, name: 'PartnerDirection' })
|
||||
direction!: PartnerDirection;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
MaxDateString,
|
||||
Optional,
|
||||
ValidateBoolean,
|
||||
ValidateEnum,
|
||||
ValidateHexColor,
|
||||
ValidateUUID,
|
||||
} from 'src/validation';
|
||||
@@ -137,7 +138,7 @@ export class AssetFaceWithoutPersonResponseDto {
|
||||
boundingBoxY1!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
boundingBoxY2!: number;
|
||||
@ApiProperty({ enum: SourceType, enumName: 'SourceType' })
|
||||
@ValidateEnum({ enum: SourceType, name: 'SourceType' })
|
||||
sourceType?: SourceType;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||
import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||
import { Place } from 'src/database';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AssetOrder, AssetType, AssetVisibility } from 'src/enum';
|
||||
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
class BaseSearchDto {
|
||||
@ValidateUUID({ optional: true, nullable: true })
|
||||
@@ -17,9 +17,7 @@ class BaseSearchDto {
|
||||
@Optional()
|
||||
deviceId?: string;
|
||||
|
||||
@IsEnum(AssetType)
|
||||
@Optional()
|
||||
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
|
||||
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true })
|
||||
type?: AssetType;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
@@ -34,7 +32,7 @@ class BaseSearchDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
isOffline?: boolean;
|
||||
|
||||
@ValidateAssetVisibility({ optional: true })
|
||||
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
|
||||
visibility?: AssetVisibility;
|
||||
|
||||
@ValidateDate({ optional: true })
|
||||
@@ -172,9 +170,7 @@ export class MetadataSearchDto extends RandomSearchDto {
|
||||
@Optional()
|
||||
encodedVideoPath?: string;
|
||||
|
||||
@IsEnum(AssetOrder)
|
||||
@Optional()
|
||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder, default: AssetOrder.DESC })
|
||||
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, default: AssetOrder.DESC })
|
||||
order?: AssetOrder;
|
||||
|
||||
@IsInt()
|
||||
@@ -250,9 +246,7 @@ export enum SearchSuggestionType {
|
||||
}
|
||||
|
||||
export class SearchSuggestionRequestDto {
|
||||
@IsEnum(SearchSuggestionType)
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType })
|
||||
@ValidateEnum({ enum: SearchSuggestionType, name: 'SearchSuggestionType' })
|
||||
type!: SearchSuggestionType;
|
||||
|
||||
@IsString()
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsEnum, IsString } from 'class-validator';
|
||||
import { IsString } from 'class-validator';
|
||||
import _ from 'lodash';
|
||||
import { SharedLink } from 'src/database';
|
||||
import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { SharedLinkType } from 'src/enum';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class SharedLinkSearchDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
@@ -13,8 +13,7 @@ export class SharedLinkSearchDto {
|
||||
}
|
||||
|
||||
export class SharedLinkCreateDto {
|
||||
@IsEnum(SharedLinkType)
|
||||
@ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' })
|
||||
@ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' })
|
||||
type!: SharedLinkType;
|
||||
|
||||
@ValidateUUID({ each: true, optional: true })
|
||||
@@ -90,7 +89,7 @@ export class SharedLinkResponseDto {
|
||||
userId!: string;
|
||||
key!: string;
|
||||
|
||||
@ApiProperty({ enumName: 'SharedLinkType', enum: SharedLinkType })
|
||||
@ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' })
|
||||
type!: SharedLinkType;
|
||||
createdAt!: Date;
|
||||
expiresAt!: Date | null;
|
||||
|
||||
+10
-13
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-function-type */
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayMaxSize, IsEnum, IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AlbumUserRole,
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
UserMetadataKey,
|
||||
} from 'src/enum';
|
||||
import { UserMetadata } from 'src/types';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class AssetFullSyncDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
@@ -90,11 +90,11 @@ export class SyncAssetV1 {
|
||||
fileModifiedAt!: Date | null;
|
||||
localDateTime!: Date | null;
|
||||
duration!: string | null;
|
||||
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
|
||||
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' })
|
||||
type!: AssetType;
|
||||
deletedAt!: Date | null;
|
||||
isFavorite!: boolean;
|
||||
@ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility })
|
||||
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' })
|
||||
visibility!: AssetVisibility;
|
||||
livePhotoVideoId!: string | null;
|
||||
stackId!: string | null;
|
||||
@@ -159,7 +159,7 @@ export class SyncAlbumUserDeleteV1 {
|
||||
export class SyncAlbumUserV1 {
|
||||
albumId!: string;
|
||||
userId!: string;
|
||||
@ApiProperty({ enumName: 'AlbumUserRole', enum: AlbumUserRole })
|
||||
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
|
||||
role!: AlbumUserRole;
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ export class SyncAlbumV1 {
|
||||
updatedAt!: Date;
|
||||
thumbnailAssetId!: string | null;
|
||||
isActivityEnabled!: boolean;
|
||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' })
|
||||
order!: AssetOrder;
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export class SyncMemoryV1 {
|
||||
updatedAt!: Date;
|
||||
deletedAt!: Date | null;
|
||||
ownerId!: string;
|
||||
@ApiProperty({ enumName: 'MemoryType', enum: MemoryType })
|
||||
@ValidateEnum({ enum: MemoryType, name: 'MemoryType' })
|
||||
type!: MemoryType;
|
||||
data!: object;
|
||||
isSaved!: boolean;
|
||||
@@ -319,8 +319,7 @@ export type SyncItem = {
|
||||
};
|
||||
|
||||
export class SyncStreamDto {
|
||||
@IsEnum(SyncRequestType, { each: true })
|
||||
@ApiProperty({ enumName: 'SyncRequestType', enum: SyncRequestType, isArray: true })
|
||||
@ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true })
|
||||
types!: SyncRequestType[];
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
@@ -328,7 +327,7 @@ export class SyncStreamDto {
|
||||
}
|
||||
|
||||
export class SyncAckDto {
|
||||
@ApiProperty({ enumName: 'SyncEntityType', enum: SyncEntityType })
|
||||
@ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType' })
|
||||
type!: SyncEntityType;
|
||||
ack!: string;
|
||||
}
|
||||
@@ -340,8 +339,6 @@ export class SyncAckSetDto {
|
||||
}
|
||||
|
||||
export class SyncAckDeleteDto {
|
||||
@IsEnum(SyncEntityType, { each: true })
|
||||
@ApiProperty({ enumName: 'SyncEntityType', enum: SyncEntityType, isArray: true })
|
||||
@Optional()
|
||||
@ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', optional: true, each: true })
|
||||
types?: SyncEntityType[];
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Exclude, Transform, Type } from 'class-transformer';
|
||||
import {
|
||||
ArrayMinSize,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsInt,
|
||||
IsNotEmpty,
|
||||
IsNumber,
|
||||
@@ -34,7 +32,7 @@ import {
|
||||
VideoContainer,
|
||||
} from 'src/enum';
|
||||
import { ConcurrentQueueName } from 'src/types';
|
||||
import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean } from 'src/validation';
|
||||
import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean, ValidateEnum } from 'src/validation';
|
||||
|
||||
const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
|
||||
const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled;
|
||||
@@ -82,24 +80,19 @@ export class SystemConfigFFmpegDto {
|
||||
@IsString()
|
||||
preset!: string;
|
||||
|
||||
@IsEnum(VideoCodec)
|
||||
@ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec })
|
||||
@ValidateEnum({ enum: VideoCodec, name: 'VideoCodec' })
|
||||
targetVideoCodec!: VideoCodec;
|
||||
|
||||
@IsEnum(VideoCodec, { each: true })
|
||||
@ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true })
|
||||
@ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true })
|
||||
acceptedVideoCodecs!: VideoCodec[];
|
||||
|
||||
@IsEnum(AudioCodec)
|
||||
@ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec })
|
||||
@ValidateEnum({ enum: AudioCodec, name: 'AudioCodec' })
|
||||
targetAudioCodec!: AudioCodec;
|
||||
|
||||
@IsEnum(AudioCodec, { each: true })
|
||||
@ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true })
|
||||
@ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true })
|
||||
acceptedAudioCodecs!: AudioCodec[];
|
||||
|
||||
@IsEnum(VideoContainer, { each: true })
|
||||
@ApiProperty({ enumName: 'VideoContainer', enum: VideoContainer, isArray: true })
|
||||
@ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true })
|
||||
acceptedContainers!: VideoContainer[];
|
||||
|
||||
@IsString()
|
||||
@@ -131,8 +124,7 @@ export class SystemConfigFFmpegDto {
|
||||
@ValidateBoolean()
|
||||
temporalAQ!: boolean;
|
||||
|
||||
@IsEnum(CQMode)
|
||||
@ApiProperty({ enumName: 'CQMode', enum: CQMode })
|
||||
@ValidateEnum({ enum: CQMode, name: 'CQMode' })
|
||||
cqMode!: CQMode;
|
||||
|
||||
@ValidateBoolean()
|
||||
@@ -141,19 +133,16 @@ export class SystemConfigFFmpegDto {
|
||||
@IsString()
|
||||
preferredHwDevice!: string;
|
||||
|
||||
@IsEnum(TranscodePolicy)
|
||||
@ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy })
|
||||
@ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy' })
|
||||
transcode!: TranscodePolicy;
|
||||
|
||||
@IsEnum(TranscodeHWAccel)
|
||||
@ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel })
|
||||
@ValidateEnum({ enum: TranscodeHWAccel, name: 'TranscodeHWAccel' })
|
||||
accel!: TranscodeHWAccel;
|
||||
|
||||
@ValidateBoolean()
|
||||
accelDecode!: boolean;
|
||||
|
||||
@IsEnum(ToneMapping)
|
||||
@ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping })
|
||||
@ValidateEnum({ enum: ToneMapping, name: 'ToneMapping' })
|
||||
tonemap!: ToneMapping;
|
||||
}
|
||||
|
||||
@@ -264,8 +253,7 @@ class SystemConfigLoggingDto {
|
||||
@ValidateBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@ApiProperty({ enum: LogLevel, enumName: 'LogLevel' })
|
||||
@IsEnum(LogLevel)
|
||||
@ValidateEnum({ enum: LogLevel, name: 'LogLevel' })
|
||||
level!: LogLevel;
|
||||
}
|
||||
|
||||
@@ -306,8 +294,7 @@ enum MapTheme {
|
||||
}
|
||||
|
||||
export class MapThemeDto {
|
||||
@IsEnum(MapTheme)
|
||||
@ApiProperty({ enum: MapTheme, enumName: 'MapTheme' })
|
||||
@ValidateEnum({ enum: MapTheme, name: 'MapTheme' })
|
||||
theme!: MapTheme;
|
||||
}
|
||||
|
||||
@@ -368,8 +355,7 @@ class SystemConfigOAuthDto {
|
||||
@IsString()
|
||||
clientSecret!: string;
|
||||
|
||||
@IsEnum(OAuthTokenEndpointAuthMethod)
|
||||
@ApiProperty({ enum: OAuthTokenEndpointAuthMethod, enumName: 'OAuthTokenEndpointAuthMethod' })
|
||||
@ValidateEnum({ enum: OAuthTokenEndpointAuthMethod, name: 'OAuthTokenEndpointAuthMethod' })
|
||||
tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod;
|
||||
|
||||
@IsInt()
|
||||
@@ -431,7 +417,7 @@ class SystemConfigReverseGeocodingDto {
|
||||
}
|
||||
|
||||
class SystemConfigFacesDto {
|
||||
@IsBoolean()
|
||||
@ValidateBoolean()
|
||||
import!: boolean;
|
||||
}
|
||||
|
||||
@@ -450,12 +436,12 @@ class SystemConfigServerDto {
|
||||
@IsString()
|
||||
loginPageMessage!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ValidateBoolean()
|
||||
publicUsers!: boolean;
|
||||
}
|
||||
|
||||
class SystemConfigSmtpTransportDto {
|
||||
@IsBoolean()
|
||||
@ValidateBoolean()
|
||||
ignoreCert!: boolean;
|
||||
|
||||
@IsNotEmpty()
|
||||
@@ -475,7 +461,7 @@ class SystemConfigSmtpTransportDto {
|
||||
}
|
||||
|
||||
export class SystemConfigSmtpDto {
|
||||
@IsBoolean()
|
||||
@ValidateBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@ValidateIf(isEmailNotificationEnabled)
|
||||
@@ -548,8 +534,7 @@ export class SystemConfigThemeDto {
|
||||
}
|
||||
|
||||
class SystemConfigGeneratedImageDto {
|
||||
@IsEnum(ImageFormat)
|
||||
@ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
|
||||
@ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' })
|
||||
format!: ImageFormat;
|
||||
|
||||
@IsInt()
|
||||
@@ -567,13 +552,10 @@ class SystemConfigGeneratedImageDto {
|
||||
}
|
||||
|
||||
class SystemConfigGeneratedFullsizeImageDto {
|
||||
@IsBoolean()
|
||||
@Type(() => Boolean)
|
||||
@ApiProperty({ type: 'boolean' })
|
||||
@ValidateBoolean()
|
||||
enabled!: boolean;
|
||||
|
||||
@IsEnum(ImageFormat)
|
||||
@ApiProperty({ enumName: 'ImageFormat', enum: ImageFormat })
|
||||
@ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' })
|
||||
format!: ImageFormat;
|
||||
|
||||
@IsInt()
|
||||
@@ -600,8 +582,7 @@ export class SystemConfigImageDto {
|
||||
@IsObject()
|
||||
fullsize!: SystemConfigGeneratedFullsizeImageDto;
|
||||
|
||||
@IsEnum(Colorspace)
|
||||
@ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
|
||||
@ValidateEnum({ enum: Colorspace, name: 'Colorspace' })
|
||||
colorspace!: Colorspace;
|
||||
|
||||
@ValidateBoolean()
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { IsBoolean } from 'class-validator';
|
||||
import { ValidateBoolean } from 'src/validation';
|
||||
|
||||
export class AdminOnboardingUpdateDto {
|
||||
@IsBoolean()
|
||||
@ValidateBoolean()
|
||||
isOnboarded!: boolean;
|
||||
}
|
||||
|
||||
export class AdminOnboardingResponseDto {
|
||||
@ValidateBoolean()
|
||||
isOnboarded!: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
import { IsEnum, IsString } from 'class-validator';
|
||||
import { IsString } from 'class-validator';
|
||||
import { AssetOrder, AssetVisibility } from 'src/enum';
|
||||
import { Optional, ValidateAssetVisibility, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||
import { ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class TimeBucketDto {
|
||||
@ValidateUUID({ optional: true, description: 'Filter assets by specific user ID' })
|
||||
@@ -38,16 +38,17 @@ export class TimeBucketDto {
|
||||
@ValidateBoolean({ optional: true, description: 'Include assets shared by partners' })
|
||||
withPartners?: boolean;
|
||||
|
||||
@IsEnum(AssetOrder)
|
||||
@Optional()
|
||||
@ApiProperty({
|
||||
@ValidateEnum({
|
||||
enum: AssetOrder,
|
||||
enumName: 'AssetOrder',
|
||||
name: 'AssetOrder',
|
||||
description: 'Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)',
|
||||
optional: true,
|
||||
})
|
||||
order?: AssetOrder;
|
||||
|
||||
@ValidateAssetVisibility({
|
||||
@ValidateEnum({
|
||||
enum: AssetVisibility,
|
||||
name: 'AssetVisibility',
|
||||
optional: true,
|
||||
description: 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)',
|
||||
})
|
||||
@@ -93,10 +94,10 @@ export class TimeBucketAssetResponseDto {
|
||||
})
|
||||
isFavorite!: boolean[];
|
||||
|
||||
@ApiProperty({
|
||||
@ValidateEnum({
|
||||
enum: AssetVisibility,
|
||||
enumName: 'AssetVisibility',
|
||||
isArray: true,
|
||||
name: 'AssetVisibility',
|
||||
each: true,
|
||||
description: 'Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)',
|
||||
})
|
||||
visibility!: AssetVisibility[];
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator';
|
||||
import { IsDateString, IsInt, IsPositive, ValidateNested } from 'class-validator';
|
||||
import { AssetOrder, UserAvatarColor } from 'src/enum';
|
||||
import { UserPreferences } from 'src/types';
|
||||
import { Optional, ValidateBoolean } from 'src/validation';
|
||||
import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation';
|
||||
|
||||
class AvatarUpdate {
|
||||
@Optional()
|
||||
@IsEnum(UserAvatarColor)
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true })
|
||||
color?: UserAvatarColor;
|
||||
}
|
||||
|
||||
@@ -23,8 +21,7 @@ class RatingsUpdate {
|
||||
}
|
||||
|
||||
class AlbumsUpdate {
|
||||
@IsEnum(AssetOrder)
|
||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
|
||||
defaultAssetOrder?: AssetOrder;
|
||||
}
|
||||
|
||||
@@ -159,8 +156,7 @@ export class UserPreferencesUpdateDto {
|
||||
}
|
||||
|
||||
class AlbumsResponse {
|
||||
@IsEnum(AssetOrder)
|
||||
@ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder })
|
||||
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' })
|
||||
defaultAssetOrder: AssetOrder = AssetOrder.DESC;
|
||||
}
|
||||
|
||||
|
||||
+10
-19
@@ -1,10 +1,10 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
||||
import { IsEmail, IsNotEmpty, IsNumber, IsString, Min } from 'class-validator';
|
||||
import { User, UserAdmin } from 'src/database';
|
||||
import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum';
|
||||
import { UserMetadataItem } from 'src/types';
|
||||
import { Optional, PinCode, ValidateBoolean, ValidateUUID, toEmail, toSanitized } from 'src/validation';
|
||||
import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation';
|
||||
|
||||
export class UserUpdateMeDto {
|
||||
@Optional()
|
||||
@@ -23,9 +23,7 @@ export class UserUpdateMeDto {
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
@IsEnum(UserAvatarColor)
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true })
|
||||
avatarColor?: UserAvatarColor | null;
|
||||
}
|
||||
|
||||
@@ -34,7 +32,7 @@ export class UserResponseDto {
|
||||
name!: string;
|
||||
email!: string;
|
||||
profileImagePath!: string;
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor' })
|
||||
avatarColor!: UserAvatarColor;
|
||||
profileChangedAt!: Date;
|
||||
}
|
||||
@@ -84,9 +82,7 @@ export class UserAdminCreateDto {
|
||||
@IsString()
|
||||
name!: string;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
@IsEnum(UserAvatarColor)
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true })
|
||||
avatarColor?: UserAvatarColor | null;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
@@ -103,12 +99,10 @@ export class UserAdminCreateDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
shouldChangePassword?: boolean;
|
||||
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
@ValidateBoolean({ optional: true })
|
||||
notify?: boolean;
|
||||
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
@ValidateBoolean({ optional: true })
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
@@ -131,9 +125,7 @@ export class UserAdminUpdateDto {
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
@IsEnum(UserAvatarColor)
|
||||
@ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
|
||||
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true })
|
||||
avatarColor?: UserAvatarColor | null;
|
||||
|
||||
@Optional({ nullable: true })
|
||||
@@ -150,8 +142,7 @@ export class UserAdminUpdateDto {
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
quotaSizeInBytes?: number | null;
|
||||
|
||||
@Optional()
|
||||
@IsBoolean()
|
||||
@ValidateBoolean({ optional: true })
|
||||
isAdmin?: boolean;
|
||||
}
|
||||
|
||||
@@ -172,7 +163,7 @@ export class UserAdminResponseDto extends UserResponseDto {
|
||||
quotaSizeInBytes!: number | null;
|
||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||
quotaUsageInBytes!: number | null;
|
||||
@ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
|
||||
@ValidateEnum({ enum: UserStatus, name: 'UserStatus' })
|
||||
status!: string;
|
||||
license!: UserLicense | null;
|
||||
}
|
||||
|
||||
@@ -35,59 +35,59 @@ type Item<T extends EmitEvent> = {
|
||||
|
||||
type EventMap = {
|
||||
// app events
|
||||
'app.bootstrap': [];
|
||||
'app.shutdown': [];
|
||||
AppBootstrap: [];
|
||||
AppShutdown: [];
|
||||
|
||||
'config.init': [{ newConfig: SystemConfig }];
|
||||
ConfigInit: [{ newConfig: SystemConfig }];
|
||||
// config events
|
||||
'config.update': [
|
||||
ConfigUpdate: [
|
||||
{
|
||||
newConfig: SystemConfig;
|
||||
oldConfig: SystemConfig;
|
||||
},
|
||||
];
|
||||
'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
|
||||
ConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }];
|
||||
|
||||
// album events
|
||||
'album.update': [{ id: string; recipientId: string }];
|
||||
'album.invite': [{ id: string; userId: string }];
|
||||
AlbumUpdate: [{ id: string; recipientId: string }];
|
||||
AlbumInvite: [{ id: string; userId: string }];
|
||||
|
||||
// asset events
|
||||
'asset.tag': [{ assetId: string }];
|
||||
'asset.untag': [{ assetId: string }];
|
||||
'asset.hide': [{ assetId: string; userId: string }];
|
||||
'asset.show': [{ assetId: string; userId: string }];
|
||||
'asset.trash': [{ assetId: string; userId: string }];
|
||||
'asset.delete': [{ assetId: string; userId: string }];
|
||||
'asset.metadataExtracted': [{ assetId: string; userId: string; source?: JobSource }];
|
||||
AssetTag: [{ assetId: string }];
|
||||
AssetUntag: [{ assetId: string }];
|
||||
AssetHide: [{ assetId: string; userId: string }];
|
||||
AssetShow: [{ assetId: string; userId: string }];
|
||||
AssetTrash: [{ assetId: string; userId: string }];
|
||||
AssetDelete: [{ assetId: string; userId: string }];
|
||||
AssetMetadataExtracted: [{ assetId: string; userId: string; source?: JobSource }];
|
||||
|
||||
// asset bulk events
|
||||
'assets.trash': [{ assetIds: string[]; userId: string }];
|
||||
'assets.delete': [{ assetIds: string[]; userId: string }];
|
||||
'assets.restore': [{ assetIds: string[]; userId: string }];
|
||||
AssetTrashAll: [{ assetIds: string[]; userId: string }];
|
||||
AssetDeleteAll: [{ assetIds: string[]; userId: string }];
|
||||
AssetRestoreAll: [{ assetIds: string[]; userId: string }];
|
||||
|
||||
'job.start': [QueueName, JobItem];
|
||||
'job.failed': [{ job: JobItem; error: Error | any }];
|
||||
JobStart: [QueueName, JobItem];
|
||||
JobFailed: [{ job: JobItem; error: Error | any }];
|
||||
|
||||
// session events
|
||||
'session.delete': [{ sessionId: string }];
|
||||
SessionDelete: [{ sessionId: string }];
|
||||
|
||||
// stack events
|
||||
'stack.create': [{ stackId: string; userId: string }];
|
||||
'stack.update': [{ stackId: string; userId: string }];
|
||||
'stack.delete': [{ stackId: string; userId: string }];
|
||||
StackCreate: [{ stackId: string; userId: string }];
|
||||
StackUpdate: [{ stackId: string; userId: string }];
|
||||
StackDelete: [{ stackId: string; userId: string }];
|
||||
|
||||
// stack bulk events
|
||||
'stacks.delete': [{ stackIds: string[]; userId: string }];
|
||||
StackDeleteAll: [{ stackIds: string[]; userId: string }];
|
||||
|
||||
// user events
|
||||
'user.signup': [{ notify: boolean; id: string; tempPassword?: string }];
|
||||
UserSignup: [{ notify: boolean; id: string; tempPassword?: string }];
|
||||
|
||||
// websocket events
|
||||
'websocket.connect': [{ userId: string }];
|
||||
WebsocketConnect: [{ userId: string }];
|
||||
};
|
||||
|
||||
export const serverEvents = ['config.update'] as const;
|
||||
export const serverEvents = ['ConfigUpdate'] as const;
|
||||
export type ServerEvents = (typeof serverEvents)[number];
|
||||
|
||||
export type EmitEvent = keyof EventMap;
|
||||
@@ -213,7 +213,7 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect
|
||||
if (auth.session) {
|
||||
await client.join(auth.session.id);
|
||||
}
|
||||
await this.onEvent({ name: 'websocket.connect', args: [{ userId: auth.user.id }], server: false });
|
||||
await this.onEvent({ name: 'WebsocketConnect', args: [{ userId: auth.user.id }], server: false });
|
||||
} catch (error: Error | any) {
|
||||
this.logger.error(`Websocket connection error: ${error}`, error?.stack);
|
||||
client.emit('error', 'unauthorized');
|
||||
|
||||
@@ -89,7 +89,7 @@ export class JobRepository {
|
||||
this.logger.debug(`Starting worker for queue: ${queueName}`);
|
||||
this.workers[queueName] = new Worker(
|
||||
queueName,
|
||||
(job) => this.eventRepository.emit('job.start', queueName, job as JobItem),
|
||||
(job) => this.eventRepository.emit('JobStart', queueName, job as JobItem),
|
||||
{ ...bull.config, concurrency: 1 },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -265,6 +265,9 @@ export class MapRepository {
|
||||
throw new Error(`Geodata file ${cities500} not found`);
|
||||
}
|
||||
|
||||
this.logger.log(`Starting geodata import`);
|
||||
const startTime = performance.now();
|
||||
|
||||
const input = createReadStream(cities500, { highWaterMark: 512 * 1024 * 1024 });
|
||||
let bufferGeodata = [];
|
||||
const lineReader = readLine.createInterface({ input });
|
||||
@@ -314,7 +317,20 @@ export class MapRepository {
|
||||
}
|
||||
}
|
||||
|
||||
await this.db.insertInto('geodata_places').values(bufferGeodata).execute();
|
||||
if (bufferGeodata.length > 0) {
|
||||
await this.db.insertInto('geodata_places').values(bufferGeodata).execute();
|
||||
count += bufferGeodata.length;
|
||||
}
|
||||
|
||||
await Promise.all(futures);
|
||||
|
||||
const duration = performance.now() - startTime;
|
||||
const seconds = duration / 1000;
|
||||
const recordsPerSecond = Math.round(count / seconds);
|
||||
|
||||
this.logger.log(
|
||||
`Successfully imported ${count} geodata records in ${seconds.toFixed(2)}s (${recordsPerSecond} records/second)`,
|
||||
);
|
||||
}
|
||||
|
||||
private async loadAdmin(filePath: string) {
|
||||
|
||||
@@ -166,7 +166,7 @@ describe(AlbumService.name, () => {
|
||||
expect(mocks.user.get).toHaveBeenCalledWith('user-id', {});
|
||||
expect(mocks.user.getMetadata).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']), false);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
||||
id: albumStub.empty.id,
|
||||
userId: 'user-id',
|
||||
});
|
||||
@@ -209,7 +209,7 @@ describe(AlbumService.name, () => {
|
||||
expect(mocks.user.get).toHaveBeenCalledWith('user-id', {});
|
||||
expect(mocks.user.getMetadata).toHaveBeenCalledWith(authStub.admin.user.id);
|
||||
expect(mocks.access.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123']), false);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
||||
id: albumStub.empty.id,
|
||||
userId: 'user-id',
|
||||
});
|
||||
@@ -413,7 +413,7 @@ describe(AlbumService.name, () => {
|
||||
usersId: authStub.user2.user.id,
|
||||
albumsId: albumStub.sharedWithAdmin.id,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('album.invite', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumInvite', {
|
||||
id: albumStub.sharedWithAdmin.id,
|
||||
userId: userStub.user2.id,
|
||||
});
|
||||
@@ -662,7 +662,7 @@ describe(AlbumService.name, () => {
|
||||
albumThumbnailAssetId: 'asset-1',
|
||||
});
|
||||
expect(mocks.album.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('album.update', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AlbumUpdate', {
|
||||
id: 'album-123',
|
||||
recipientId: 'admin_id',
|
||||
});
|
||||
|
||||
@@ -130,7 +130,7 @@ export class AlbumService extends BaseService {
|
||||
);
|
||||
|
||||
for (const { userId } of albumUsers) {
|
||||
await this.eventRepository.emit('album.invite', { id: album.id, userId });
|
||||
await this.eventRepository.emit('AlbumInvite', { id: album.id, userId });
|
||||
}
|
||||
|
||||
return mapAlbumWithAssets(album);
|
||||
@@ -187,7 +187,7 @@ export class AlbumService extends BaseService {
|
||||
);
|
||||
|
||||
for (const recipientId of allUsersExceptUs) {
|
||||
await this.eventRepository.emit('album.update', { id, recipientId });
|
||||
await this.eventRepository.emit('AlbumUpdate', { id, recipientId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,7 +233,7 @@ export class AlbumService extends BaseService {
|
||||
}
|
||||
|
||||
await this.albumUserRepository.create({ usersId: userId, albumsId: id, role });
|
||||
await this.eventRepository.emit('album.invite', { id, userId });
|
||||
await this.eventRepository.emit('AlbumInvite', { id, userId });
|
||||
}
|
||||
|
||||
return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets);
|
||||
|
||||
@@ -180,7 +180,7 @@ export class AssetMediaService extends BaseService {
|
||||
const copiedPhoto = await this.createCopy(asset);
|
||||
// and immediate trash it
|
||||
await this.assetRepository.updateAll([copiedPhoto.id], { deletedAt: new Date(), status: AssetStatus.TRASHED });
|
||||
await this.eventRepository.emit('asset.trash', { 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);
|
||||
|
||||
|
||||
@@ -255,7 +255,7 @@ describe(AssetService.name, () => {
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
@@ -279,7 +279,7 @@ describe(AssetService.name, () => {
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
@@ -303,7 +303,7 @@ describe(AssetService.name, () => {
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.TIMELINE,
|
||||
});
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('asset.show', {
|
||||
expect(mocks.event.emit).not.toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
@@ -327,7 +327,7 @@ describe(AssetService.name, () => {
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: AssetVisibility.HIDDEN,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
@@ -360,7 +360,7 @@ describe(AssetService.name, () => {
|
||||
id: assetStub.livePhotoMotionAsset.id,
|
||||
visibility: assetStub.livePhotoStillAsset.visibility,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.show', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetShow', {
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
userId: userStub.admin.id,
|
||||
});
|
||||
@@ -484,7 +484,7 @@ describe(AssetService.name, () => {
|
||||
|
||||
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
|
||||
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('assets.delete', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetDeleteAll', {
|
||||
assetIds: ['asset1', 'asset2'],
|
||||
userId: 'user-id',
|
||||
});
|
||||
|
||||
@@ -208,7 +208,7 @@ export class AssetService extends BaseService {
|
||||
await this.userRepository.updateUsage(asset.ownerId, -(asset.exifInfo?.fileSizeInByte || 0));
|
||||
}
|
||||
|
||||
await this.eventRepository.emit('asset.delete', { assetId: id, userId: asset.ownerId });
|
||||
await this.eventRepository.emit('AssetDelete', { assetId: id, userId: asset.ownerId });
|
||||
|
||||
// delete the motion if it is not used by another asset
|
||||
if (asset.livePhotoVideoId) {
|
||||
@@ -241,7 +241,10 @@ export class AssetService extends BaseService {
|
||||
deletedAt: new Date(),
|
||||
status: force ? AssetStatus.DELETED : AssetStatus.TRASHED,
|
||||
});
|
||||
await this.eventRepository.emit(force ? 'assets.delete' : 'assets.trash', { assetIds: ids, userId: auth.user.id });
|
||||
await this.eventRepository.emit(force ? 'AssetDeleteAll' : 'AssetTrashAll', {
|
||||
assetIds: ids,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
}
|
||||
|
||||
async run(auth: AuthDto, dto: AssetJobsDto) {
|
||||
|
||||
@@ -179,7 +179,7 @@ describe(AuthService.name, () => {
|
||||
});
|
||||
|
||||
expect(mocks.session.delete).toHaveBeenCalledWith('token123');
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('session.delete', { sessionId: 'token123' });
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('SessionDelete', { sessionId: 'token123' });
|
||||
});
|
||||
|
||||
it('should return the default redirect if auth type is OAUTH but oauth is not enabled', async () => {
|
||||
|
||||
@@ -80,7 +80,7 @@ export class AuthService extends BaseService {
|
||||
async logout(auth: AuthDto, authType: AuthType): Promise<LogoutResponseDto> {
|
||||
if (auth.session) {
|
||||
await this.sessionRepository.delete(auth.session.id);
|
||||
await this.eventRepository.emit('session.delete', { sessionId: auth.session.id });
|
||||
await this.eventRepository.emit('SessionDelete', { sessionId: auth.session.id });
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -14,12 +14,12 @@ import { handlePromiseError } from 'src/utils/misc';
|
||||
export class BackupService extends BaseService {
|
||||
private backupLock = false;
|
||||
|
||||
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
|
||||
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] })
|
||||
async onConfigInit({
|
||||
newConfig: {
|
||||
backup: { database },
|
||||
},
|
||||
}: ArgOf<'config.init'>) {
|
||||
}: ArgOf<'ConfigInit'>) {
|
||||
this.backupLock = await this.databaseRepository.tryLock(DatabaseLock.BackupDatabase);
|
||||
|
||||
if (this.backupLock) {
|
||||
@@ -32,8 +32,8 @@ export class BackupService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.update', server: true })
|
||||
onConfigUpdate({ newConfig: { backup } }: ArgOf<'config.update'>) {
|
||||
@OnEvent({ name: 'ConfigUpdate', server: true })
|
||||
onConfigUpdate({ newConfig: { backup } }: ArgOf<'ConfigUpdate'>) {
|
||||
if (!this.backupLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ const messages = {
|
||||
|
||||
@Injectable()
|
||||
export class DatabaseService extends BaseService {
|
||||
@OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.DatabaseService })
|
||||
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.DatabaseService })
|
||||
async onBootstrap() {
|
||||
const version = await this.databaseRepository.getPostgresVersion();
|
||||
const current = semver.coerce(version);
|
||||
|
||||
@@ -67,8 +67,8 @@ export class JobService extends BaseService {
|
||||
private services: ClassConstructor<unknown>[] = [];
|
||||
private nightlyJobsLock = false;
|
||||
|
||||
@OnEvent({ name: 'config.init' })
|
||||
async onConfigInit({ newConfig: config }: ArgOf<'config.init'>) {
|
||||
@OnEvent({ name: 'ConfigInit' })
|
||||
async onConfigInit({ newConfig: config }: ArgOf<'ConfigInit'>) {
|
||||
if (this.worker === ImmichWorker.MICROSERVICES) {
|
||||
this.updateQueueConcurrency(config);
|
||||
return;
|
||||
@@ -87,8 +87,8 @@ export class JobService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.update', server: true })
|
||||
onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) {
|
||||
@OnEvent({ name: 'ConfigUpdate', server: true })
|
||||
onConfigUpdate({ newConfig: config }: ArgOf<'ConfigUpdate'>) {
|
||||
if (this.worker === ImmichWorker.MICROSERVICES) {
|
||||
this.updateQueueConcurrency(config);
|
||||
return;
|
||||
@@ -101,7 +101,7 @@ export class JobService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.JobService })
|
||||
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.JobService })
|
||||
onBootstrap() {
|
||||
this.jobRepository.setup(this.services);
|
||||
if (this.worker === ImmichWorker.MICROSERVICES) {
|
||||
@@ -243,8 +243,8 @@ export class JobService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'job.start' })
|
||||
async onJobStart(...[queueName, job]: ArgsOf<'job.start'>) {
|
||||
@OnEvent({ name: 'JobStart' })
|
||||
async onJobStart(...[queueName, job]: ArgsOf<'JobStart'>) {
|
||||
const queueMetric = `immich.queues.${snakeCase(queueName)}.active`;
|
||||
this.telemetryRepository.jobs.addToGauge(queueMetric, 1);
|
||||
try {
|
||||
@@ -255,7 +255,7 @@ export class JobService extends BaseService {
|
||||
await this.onDone(job);
|
||||
}
|
||||
} catch (error: Error | any) {
|
||||
await this.eventRepository.emit('job.failed', { job, error });
|
||||
await this.eventRepository.emit('JobFailed', { job, error });
|
||||
} finally {
|
||||
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
|
||||
}
|
||||
|
||||
@@ -32,12 +32,12 @@ export class LibraryService extends BaseService {
|
||||
private lock = false;
|
||||
private watchers: Record<string, () => Promise<void>> = {};
|
||||
|
||||
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
|
||||
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] })
|
||||
async onConfigInit({
|
||||
newConfig: {
|
||||
library: { watch, scan },
|
||||
},
|
||||
}: ArgOf<'config.init'>) {
|
||||
}: ArgOf<'ConfigInit'>) {
|
||||
// This ensures that library watching only occurs in one microservice
|
||||
this.lock = await this.databaseRepository.tryLock(DatabaseLock.Library);
|
||||
|
||||
@@ -58,8 +58,8 @@ export class LibraryService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.update', server: true })
|
||||
async onConfigUpdate({ newConfig: { library } }: ArgOf<'config.update'>) {
|
||||
@OnEvent({ name: 'ConfigUpdate', server: true })
|
||||
async onConfigUpdate({ newConfig: { library } }: ArgOf<'ConfigUpdate'>) {
|
||||
if (!this.lock) {
|
||||
return;
|
||||
}
|
||||
@@ -155,7 +155,7 @@ export class LibraryService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'app.shutdown' })
|
||||
@OnEvent({ name: 'AppShutdown' })
|
||||
async onShutdown() {
|
||||
await this.unwatchAll();
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ interface UpsertFileOptions {
|
||||
export class MediaService extends BaseService {
|
||||
videoInterfaces: VideoInterfaces = { dri: [], mali: false };
|
||||
|
||||
@OnEvent({ name: 'app.bootstrap' })
|
||||
@OnEvent({ name: 'AppBootstrap' })
|
||||
async onBootstrap() {
|
||||
const [dri, mali] = await Promise.all([this.getDevices(), this.hasMaliOpenCL()]);
|
||||
this.videoInterfaces = { dri, mali };
|
||||
|
||||
@@ -1368,7 +1368,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.hide', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetHide', {
|
||||
userId: assetStub.livePhotoMotionAsset.ownerId,
|
||||
assetId: assetStub.livePhotoMotionAsset.id,
|
||||
});
|
||||
@@ -1384,7 +1384,7 @@ describe(MetadataService.name, () => {
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.livePhotoStillAsset.id });
|
||||
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('asset.metadataExtracted', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('AssetMetadataExtracted', {
|
||||
assetId: assetStub.livePhotoStillAsset.id,
|
||||
userId: assetStub.livePhotoStillAsset.ownerId,
|
||||
});
|
||||
|
||||
@@ -126,24 +126,24 @@ type Dates = {
|
||||
|
||||
@Injectable()
|
||||
export class MetadataService extends BaseService {
|
||||
@OnEvent({ name: 'app.bootstrap', workers: [ImmichWorker.MICROSERVICES] })
|
||||
@OnEvent({ name: 'AppBootstrap', workers: [ImmichWorker.MICROSERVICES] })
|
||||
async onBootstrap() {
|
||||
this.logger.log('Bootstrapping metadata service');
|
||||
await this.init();
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'app.shutdown' })
|
||||
@OnEvent({ name: 'AppShutdown' })
|
||||
async onShutdown() {
|
||||
await this.metadataRepository.teardown();
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
|
||||
onConfigInit({ newConfig }: ArgOf<'config.init'>) {
|
||||
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] })
|
||||
onConfigInit({ newConfig }: ArgOf<'ConfigInit'>) {
|
||||
this.metadataRepository.setMaxConcurrency(newConfig.job.metadataExtraction.concurrency);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.update', workers: [ImmichWorker.MICROSERVICES], server: true })
|
||||
onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
|
||||
@OnEvent({ name: 'ConfigUpdate', workers: [ImmichWorker.MICROSERVICES], server: true })
|
||||
onConfigUpdate({ newConfig }: ArgOf<'ConfigUpdate'>) {
|
||||
this.metadataRepository.setMaxConcurrency(newConfig.job.metadataExtraction.concurrency);
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ export class MetadataService extends BaseService {
|
||||
this.albumRepository.removeAssetsFromAll([motionAsset.id]),
|
||||
]);
|
||||
|
||||
await this.eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId: motionAsset.ownerId });
|
||||
await this.eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId: motionAsset.ownerId });
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.QUEUE_METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
|
||||
@@ -313,7 +313,7 @@ export class MetadataService extends BaseService {
|
||||
|
||||
await this.assetRepository.upsertJobStatus({ assetId: asset.id, metadataExtractedAt: new Date() });
|
||||
|
||||
await this.eventRepository.emit('asset.metadataExtracted', {
|
||||
await this.eventRepository.emit('AssetMetadataExtracted', {
|
||||
assetId: asset.id,
|
||||
userId: asset.ownerId,
|
||||
source: data.source,
|
||||
@@ -351,13 +351,13 @@ export class MetadataService extends BaseService {
|
||||
return this.processSidecar(id, false);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'asset.tag' })
|
||||
async handleTagAsset({ assetId }: ArgOf<'asset.tag'>) {
|
||||
@OnEvent({ name: 'AssetTag' })
|
||||
async handleTagAsset({ assetId }: ArgOf<'AssetTag'>) {
|
||||
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } });
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'asset.untag' })
|
||||
async handleUntagAsset({ assetId }: ArgOf<'asset.untag'>) {
|
||||
@OnEvent({ name: 'AssetUntag' })
|
||||
async handleUntagAsset({ assetId }: ArgOf<'AssetUntag'>) {
|
||||
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } });
|
||||
}
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ describe(NotificationService.name, () => {
|
||||
const update = { oldConfig: defaults, newConfig: defaults };
|
||||
expect(sut.onConfigUpdate(update)).toBeUndefined();
|
||||
expect(mocks.event.clientBroadcast).toHaveBeenCalledWith('on_config_update');
|
||||
expect(mocks.event.serverSend).toHaveBeenCalledWith('config.update', update);
|
||||
expect(mocks.event.serverSend).toHaveBeenCalledWith('ConfigUpdate', update);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -77,8 +77,8 @@ export class NotificationService extends BaseService {
|
||||
await this.notificationRepository.cleanup();
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'job.failed' })
|
||||
async onJobFailed({ job, error }: ArgOf<'job.failed'>) {
|
||||
@OnEvent({ name: 'JobFailed' })
|
||||
async onJobFailed({ job, error }: ArgOf<'JobFailed'>) {
|
||||
const admin = await this.userRepository.getAdmin();
|
||||
if (!admin) {
|
||||
return;
|
||||
@@ -107,14 +107,14 @@ export class NotificationService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.update' })
|
||||
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
|
||||
@OnEvent({ name: 'ConfigUpdate' })
|
||||
onConfigUpdate({ oldConfig, newConfig }: ArgOf<'ConfigUpdate'>) {
|
||||
this.eventRepository.clientBroadcast('on_config_update');
|
||||
this.eventRepository.serverSend('config.update', { oldConfig, newConfig });
|
||||
this.eventRepository.serverSend('ConfigUpdate', { oldConfig, newConfig });
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.validate', priority: -100 })
|
||||
async onConfigValidate({ oldConfig, newConfig }: ArgOf<'config.validate'>) {
|
||||
@OnEvent({ name: 'ConfigValidate', priority: -100 })
|
||||
async onConfigValidate({ oldConfig, newConfig }: ArgOf<'ConfigValidate'>) {
|
||||
try {
|
||||
if (
|
||||
newConfig.notifications.smtp.enabled &&
|
||||
@@ -128,33 +128,33 @@ export class NotificationService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'asset.hide' })
|
||||
onAssetHide({ assetId, userId }: ArgOf<'asset.hide'>) {
|
||||
@OnEvent({ name: 'AssetHide' })
|
||||
onAssetHide({ assetId, userId }: ArgOf<'AssetHide'>) {
|
||||
this.eventRepository.clientSend('on_asset_hidden', userId, assetId);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'asset.show' })
|
||||
async onAssetShow({ assetId }: ArgOf<'asset.show'>) {
|
||||
@OnEvent({ name: 'AssetShow' })
|
||||
async onAssetShow({ assetId }: ArgOf<'AssetShow'>) {
|
||||
await this.jobRepository.queue({ name: JobName.GENERATE_THUMBNAILS, data: { id: assetId, notify: true } });
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'asset.trash' })
|
||||
onAssetTrash({ assetId, userId }: ArgOf<'asset.trash'>) {
|
||||
@OnEvent({ name: 'AssetTrash' })
|
||||
onAssetTrash({ assetId, userId }: ArgOf<'AssetTrash'>) {
|
||||
this.eventRepository.clientSend('on_asset_trash', userId, [assetId]);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'asset.delete' })
|
||||
onAssetDelete({ assetId, userId }: ArgOf<'asset.delete'>) {
|
||||
@OnEvent({ name: 'AssetDelete' })
|
||||
onAssetDelete({ assetId, userId }: ArgOf<'AssetDelete'>) {
|
||||
this.eventRepository.clientSend('on_asset_delete', userId, assetId);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'assets.trash' })
|
||||
onAssetsTrash({ assetIds, userId }: ArgOf<'assets.trash'>) {
|
||||
@OnEvent({ name: 'AssetTrashAll' })
|
||||
onAssetsTrash({ assetIds, userId }: ArgOf<'AssetTrashAll'>) {
|
||||
this.eventRepository.clientSend('on_asset_trash', userId, assetIds);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'asset.metadataExtracted' })
|
||||
async onAssetMetadataExtracted({ assetId, userId, source }: ArgOf<'asset.metadataExtracted'>) {
|
||||
@OnEvent({ name: 'AssetMetadataExtracted' })
|
||||
async onAssetMetadataExtracted({ assetId, userId, source }: ArgOf<'AssetMetadataExtracted'>) {
|
||||
if (source !== 'sidecar-write') {
|
||||
return;
|
||||
}
|
||||
@@ -165,40 +165,40 @@ export class NotificationService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'assets.restore' })
|
||||
onAssetsRestore({ assetIds, userId }: ArgOf<'assets.restore'>) {
|
||||
@OnEvent({ name: 'AssetRestoreAll' })
|
||||
onAssetsRestore({ assetIds, userId }: ArgOf<'AssetRestoreAll'>) {
|
||||
this.eventRepository.clientSend('on_asset_restore', userId, assetIds);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'stack.create' })
|
||||
onStackCreate({ userId }: ArgOf<'stack.create'>) {
|
||||
@OnEvent({ name: 'StackCreate' })
|
||||
onStackCreate({ userId }: ArgOf<'StackCreate'>) {
|
||||
this.eventRepository.clientSend('on_asset_stack_update', userId);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'stack.update' })
|
||||
onStackUpdate({ userId }: ArgOf<'stack.update'>) {
|
||||
@OnEvent({ name: 'StackUpdate' })
|
||||
onStackUpdate({ userId }: ArgOf<'StackUpdate'>) {
|
||||
this.eventRepository.clientSend('on_asset_stack_update', userId);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'stack.delete' })
|
||||
onStackDelete({ userId }: ArgOf<'stack.delete'>) {
|
||||
@OnEvent({ name: 'StackDelete' })
|
||||
onStackDelete({ userId }: ArgOf<'StackDelete'>) {
|
||||
this.eventRepository.clientSend('on_asset_stack_update', userId);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'stacks.delete' })
|
||||
onStacksDelete({ userId }: ArgOf<'stacks.delete'>) {
|
||||
@OnEvent({ name: 'StackDeleteAll' })
|
||||
onStacksDelete({ userId }: ArgOf<'StackDeleteAll'>) {
|
||||
this.eventRepository.clientSend('on_asset_stack_update', userId);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'user.signup' })
|
||||
async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) {
|
||||
@OnEvent({ name: 'UserSignup' })
|
||||
async onUserSignup({ notify, id, tempPassword }: ArgOf<'UserSignup'>) {
|
||||
if (notify) {
|
||||
await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id, tempPassword } });
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'album.update' })
|
||||
async onAlbumUpdate({ id, recipientId }: ArgOf<'album.update'>) {
|
||||
@OnEvent({ name: 'AlbumUpdate' })
|
||||
async onAlbumUpdate({ id, recipientId }: ArgOf<'AlbumUpdate'>) {
|
||||
await this.jobRepository.removeJob(JobName.NOTIFY_ALBUM_UPDATE, `${id}/${recipientId}`);
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.NOTIFY_ALBUM_UPDATE,
|
||||
@@ -206,13 +206,13 @@ export class NotificationService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'album.invite' })
|
||||
async onAlbumInvite({ id, userId }: ArgOf<'album.invite'>) {
|
||||
@OnEvent({ name: 'AlbumInvite' })
|
||||
async onAlbumInvite({ id, userId }: ArgOf<'AlbumInvite'>) {
|
||||
await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } });
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'session.delete' })
|
||||
onSessionDelete({ sessionId }: ArgOf<'session.delete'>) {
|
||||
@OnEvent({ name: 'SessionDelete' })
|
||||
onSessionDelete({ sessionId }: ArgOf<'SessionDelete'>) {
|
||||
// after the response is sent
|
||||
setTimeout(() => this.eventRepository.clientSend('on_session_delete', sessionId, sessionId), 500);
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchE
|
||||
|
||||
@Injectable()
|
||||
export class ServerService extends BaseService {
|
||||
@OnEvent({ name: 'app.bootstrap' })
|
||||
@OnEvent({ name: 'AppBootstrap' })
|
||||
async onBootstrap(): Promise<void> {
|
||||
const featureFlags = await this.getFeatures();
|
||||
if (featureFlags.configFile) {
|
||||
|
||||
@@ -10,18 +10,18 @@ import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
|
||||
|
||||
@Injectable()
|
||||
export class SmartInfoService extends BaseService {
|
||||
@OnEvent({ name: 'config.init', workers: [ImmichWorker.MICROSERVICES] })
|
||||
async onConfigInit({ newConfig }: ArgOf<'config.init'>) {
|
||||
@OnEvent({ name: 'ConfigInit', workers: [ImmichWorker.MICROSERVICES] })
|
||||
async onConfigInit({ newConfig }: ArgOf<'ConfigInit'>) {
|
||||
await this.init(newConfig);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.update', workers: [ImmichWorker.MICROSERVICES], server: true })
|
||||
async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) {
|
||||
@OnEvent({ name: 'ConfigUpdate', workers: [ImmichWorker.MICROSERVICES], server: true })
|
||||
async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'ConfigUpdate'>) {
|
||||
await this.init(newConfig, oldConfig);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.validate' })
|
||||
onConfigValidate({ newConfig }: ArgOf<'config.validate'>) {
|
||||
@OnEvent({ name: 'ConfigValidate' })
|
||||
onConfigValidate({ newConfig }: ArgOf<'ConfigValidate'>) {
|
||||
try {
|
||||
getCLIPModelInfo(newConfig.machineLearning.clip.modelName);
|
||||
} catch {
|
||||
|
||||
@@ -52,7 +52,7 @@ describe(StackService.name, () => {
|
||||
],
|
||||
});
|
||||
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('stack.create', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('StackCreate', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
@@ -138,7 +138,7 @@ describe(StackService.name, () => {
|
||||
id: 'stack-id',
|
||||
primaryAssetId: assetStub.image1.id,
|
||||
});
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('stack.update', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('StackUpdate', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
@@ -160,7 +160,7 @@ describe(StackService.name, () => {
|
||||
await sut.delete(authStub.admin, 'stack-id');
|
||||
|
||||
expect(mocks.stack.delete).toHaveBeenCalledWith('stack-id');
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('stack.delete', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('StackDelete', {
|
||||
stackId: 'stack-id',
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
@@ -182,7 +182,7 @@ describe(StackService.name, () => {
|
||||
await sut.deleteAll(authStub.admin, { ids: ['stack-id'] });
|
||||
|
||||
expect(mocks.stack.deleteAll).toHaveBeenCalledWith(['stack-id']);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('stacks.delete', {
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith('StackDeleteAll', {
|
||||
stackIds: ['stack-id'],
|
||||
userId: authStub.admin.user.id,
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export class StackService extends BaseService {
|
||||
|
||||
const stack = await this.stackRepository.create({ ownerId: auth.user.id }, dto.assetIds);
|
||||
|
||||
await this.eventRepository.emit('stack.create', { stackId: stack.id, userId: auth.user.id });
|
||||
await this.eventRepository.emit('StackCreate', { stackId: stack.id, userId: auth.user.id });
|
||||
|
||||
return mapStack(stack, { auth });
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export class StackService extends BaseService {
|
||||
|
||||
const updatedStack = await this.stackRepository.update(id, { id, primaryAssetId: dto.primaryAssetId });
|
||||
|
||||
await this.eventRepository.emit('stack.update', { stackId: id, userId: auth.user.id });
|
||||
await this.eventRepository.emit('StackUpdate', { stackId: id, userId: auth.user.id });
|
||||
|
||||
return mapStack(updatedStack, { auth });
|
||||
}
|
||||
@@ -49,13 +49,13 @@ export class StackService extends BaseService {
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: [id] });
|
||||
await this.stackRepository.delete(id);
|
||||
await this.eventRepository.emit('stack.delete', { stackId: id, userId: auth.user.id });
|
||||
await this.eventRepository.emit('StackDelete', { stackId: id, userId: auth.user.id });
|
||||
}
|
||||
|
||||
async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise<void> {
|
||||
await this.requireAccess({ auth, permission: Permission.STACK_DELETE, ids: dto.ids });
|
||||
await this.stackRepository.deleteAll(dto.ids);
|
||||
await this.eventRepository.emit('stacks.delete', { stackIds: dto.ids, userId: auth.user.id });
|
||||
await this.eventRepository.emit('StackDeleteAll', { stackIds: dto.ids, userId: auth.user.id });
|
||||
}
|
||||
|
||||
private async findOrFail(id: string) {
|
||||
|
||||
@@ -75,8 +75,8 @@ export class StorageTemplateService extends BaseService {
|
||||
return this._template;
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.init' })
|
||||
onConfigInit({ newConfig }: ArgOf<'config.init'>) {
|
||||
@OnEvent({ name: 'ConfigInit' })
|
||||
onConfigInit({ newConfig }: ArgOf<'ConfigInit'>) {
|
||||
const template = newConfig.storageTemplate.template;
|
||||
if (!this._template || template !== this.template.raw) {
|
||||
this.logger.debug(`Compiling new storage template: ${template}`);
|
||||
@@ -84,13 +84,13 @@ export class StorageTemplateService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.update', server: true })
|
||||
onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
|
||||
@OnEvent({ name: 'ConfigUpdate', server: true })
|
||||
onConfigUpdate({ newConfig }: ArgOf<'ConfigUpdate'>) {
|
||||
this.onConfigInit({ newConfig });
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.validate' })
|
||||
onConfigValidate({ newConfig }: ArgOf<'config.validate'>) {
|
||||
@OnEvent({ name: 'ConfigValidate' })
|
||||
onConfigValidate({ newConfig }: ArgOf<'ConfigValidate'>) {
|
||||
try {
|
||||
const { compiled } = this.compile(newConfig.storageTemplate.template);
|
||||
this.render(compiled, {
|
||||
@@ -116,8 +116,8 @@ export class StorageTemplateService extends BaseService {
|
||||
return { ...storageTokens, presetOptions: storagePresets };
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'asset.metadataExtracted' })
|
||||
async onAssetMetadataExtracted({ source, assetId }: ArgOf<'asset.metadataExtracted'>) {
|
||||
@OnEvent({ name: 'AssetMetadataExtracted' })
|
||||
async onAssetMetadataExtracted({ source, assetId }: ArgOf<'AssetMetadataExtracted'>) {
|
||||
await this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: { source, id: assetId } });
|
||||
}
|
||||
|
||||
@@ -182,8 +182,8 @@ export class StorageTemplateService extends BaseService {
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'asset.delete' })
|
||||
async handleMoveHistoryCleanup({ assetId }: ArgOf<'asset.delete'>) {
|
||||
@OnEvent({ name: 'AssetDelete' })
|
||||
async handleMoveHistoryCleanup({ assetId }: ArgOf<'AssetDelete'>) {
|
||||
this.logger.debug(`Cleaning up move history for asset ${assetId}`);
|
||||
await this.moveRepository.cleanMoveHistorySingle(assetId);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ const docsMessage = `Please see https://immich.app/docs/administration/system-in
|
||||
|
||||
@Injectable()
|
||||
export class StorageService extends BaseService {
|
||||
@OnEvent({ name: 'app.bootstrap' })
|
||||
@OnEvent({ name: 'AppBootstrap' })
|
||||
async onBootstrap() {
|
||||
const envData = this.configRepository.getEnv();
|
||||
|
||||
|
||||
@@ -412,7 +412,7 @@ describe(SystemConfigService.name, () => {
|
||||
mocks.systemMetadata.get.mockResolvedValue(partialConfig);
|
||||
await expect(sut.updateSystemConfig(updatedConfig)).resolves.toEqual(updatedConfig);
|
||||
expect(mocks.event.emit).toHaveBeenCalledWith(
|
||||
'config.update',
|
||||
'ConfigUpdate',
|
||||
expect.objectContaining({ oldConfig: expect.any(Object), newConfig: updatedConfig }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -12,10 +12,10 @@ import { toPlainObject } from 'src/utils/object';
|
||||
|
||||
@Injectable()
|
||||
export class SystemConfigService extends BaseService {
|
||||
@OnEvent({ name: 'app.bootstrap', priority: BootstrapEventPriority.SystemConfig })
|
||||
@OnEvent({ name: 'AppBootstrap', priority: BootstrapEventPriority.SystemConfig })
|
||||
async onBootstrap() {
|
||||
const config = await this.getConfig({ withCache: false });
|
||||
await this.eventRepository.emit('config.init', { newConfig: config });
|
||||
await this.eventRepository.emit('ConfigInit', { newConfig: config });
|
||||
}
|
||||
|
||||
async getSystemConfig(): Promise<SystemConfigDto> {
|
||||
@@ -27,8 +27,8 @@ export class SystemConfigService extends BaseService {
|
||||
return mapConfig(defaults);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.init', priority: -100 })
|
||||
onConfigInit({ newConfig: { logging } }: ArgOf<'config.init'>) {
|
||||
@OnEvent({ name: 'ConfigInit', priority: -100 })
|
||||
onConfigInit({ newConfig: { logging } }: ArgOf<'ConfigInit'>) {
|
||||
const { logLevel: envLevel } = this.configRepository.getEnv();
|
||||
const configLevel = logging.enabled ? logging.level : false;
|
||||
const level = envLevel ?? configLevel;
|
||||
@@ -36,14 +36,14 @@ export class SystemConfigService extends BaseService {
|
||||
this.logger.log(`LogLevel=${level} ${envLevel ? '(set via IMMICH_LOG_LEVEL)' : '(set via system config)'}`);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.update', server: true })
|
||||
onConfigUpdate({ newConfig }: ArgOf<'config.update'>) {
|
||||
@OnEvent({ name: 'ConfigUpdate', server: true })
|
||||
onConfigUpdate({ newConfig }: ArgOf<'ConfigUpdate'>) {
|
||||
this.onConfigInit({ newConfig });
|
||||
clearConfigCache();
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'config.validate' })
|
||||
onConfigValidate({ newConfig, oldConfig }: ArgOf<'config.validate'>) {
|
||||
@OnEvent({ name: 'ConfigValidate' })
|
||||
onConfigValidate({ newConfig, oldConfig }: ArgOf<'ConfigValidate'>) {
|
||||
const { logLevel } = this.configRepository.getEnv();
|
||||
if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && logLevel) {
|
||||
throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.');
|
||||
@@ -59,7 +59,7 @@ export class SystemConfigService extends BaseService {
|
||||
const oldConfig = await this.getConfig({ withCache: false });
|
||||
|
||||
try {
|
||||
await this.eventRepository.emit('config.validate', { newConfig: toPlainObject(dto), oldConfig });
|
||||
await this.eventRepository.emit('ConfigValidate', { newConfig: toPlainObject(dto), oldConfig });
|
||||
} catch (error) {
|
||||
this.logger.warn(`Unable to save system config due to a validation error: ${error}`);
|
||||
throw new BadRequestException(error instanceof Error ? error.message : error);
|
||||
@@ -67,7 +67,7 @@ export class SystemConfigService extends BaseService {
|
||||
|
||||
const newConfig = await this.updateConfig(dto);
|
||||
|
||||
await this.eventRepository.emit('config.update', { newConfig, oldConfig });
|
||||
await this.eventRepository.emit('ConfigUpdate', { newConfig, oldConfig });
|
||||
|
||||
return mapConfig(newConfig);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ export class TagService extends BaseService {
|
||||
|
||||
const results = await this.tagRepository.upsertAssetIds(items);
|
||||
for (const assetId of new Set(results.map((item) => item.assetsId))) {
|
||||
await this.eventRepository.emit('asset.tag', { assetId });
|
||||
await this.eventRepository.emit('AssetTag', { assetId });
|
||||
}
|
||||
|
||||
return { count: results.length };
|
||||
@@ -107,7 +107,7 @@ export class TagService extends BaseService {
|
||||
|
||||
for (const { id: assetId, success } of results) {
|
||||
if (success) {
|
||||
await this.eventRepository.emit('asset.tag', { assetId });
|
||||
await this.eventRepository.emit('AssetTag', { assetId });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,7 +125,7 @@ export class TagService extends BaseService {
|
||||
|
||||
for (const { id: assetId, success } of results) {
|
||||
if (success) {
|
||||
await this.eventRepository.emit('asset.untag', { assetId });
|
||||
await this.eventRepository.emit('AssetUntag', { assetId });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export class TrashService extends BaseService {
|
||||
|
||||
await this.requireAccess({ auth, permission: Permission.ASSET_DELETE, ids });
|
||||
await this.trashRepository.restoreAll(ids);
|
||||
await this.eventRepository.emit('assets.restore', { assetIds: ids, userId: auth.user.id });
|
||||
await this.eventRepository.emit('AssetRestoreAll', { assetIds: ids, userId: auth.user.id });
|
||||
|
||||
this.logger.log(`Restored ${ids.length} asset(s) from trash`);
|
||||
|
||||
@@ -40,7 +40,7 @@ export class TrashService extends BaseService {
|
||||
return { count };
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'assets.delete' })
|
||||
@OnEvent({ name: 'AssetDeleteAll' })
|
||||
async onAssetsDelete() {
|
||||
await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ export class UserAdminService extends BaseService {
|
||||
|
||||
const user = await this.createUser(userDto);
|
||||
|
||||
await this.eventRepository.emit('user.signup', {
|
||||
await this.eventRepository.emit('UserSignup', {
|
||||
notify: !!notify,
|
||||
id: user.id,
|
||||
tempPassword: user.shouldChangePassword ? userDto.password : undefined,
|
||||
|
||||
@@ -20,7 +20,7 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re
|
||||
|
||||
@Injectable()
|
||||
export class VersionService extends BaseService {
|
||||
@OnEvent({ name: 'app.bootstrap' })
|
||||
@OnEvent({ name: 'AppBootstrap' })
|
||||
async onBootstrap(): Promise<void> {
|
||||
await this.handleVersionCheck();
|
||||
|
||||
@@ -102,8 +102,8 @@ export class VersionService extends BaseService {
|
||||
return JobStatus.SUCCESS;
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'websocket.connect' })
|
||||
async onWebsocketConnection({ userId }: ArgOf<'websocket.connect'>) {
|
||||
@OnEvent({ name: 'WebsocketConnect' })
|
||||
async onWebsocketConnection({ userId }: ArgOf<'WebsocketConnect'>) {
|
||||
this.eventRepository.clientSend('on_server_version', userId, serverVersion);
|
||||
const metadata = await this.systemMetadataRepository.get(SystemMetadataKey.VERSION_CHECK_STATE);
|
||||
if (metadata) {
|
||||
|
||||
@@ -152,7 +152,7 @@ export const onBeforeLink = async (
|
||||
|
||||
if (motionAsset && motionAsset.visibility === AssetVisibility.TIMELINE) {
|
||||
await assetRepository.update({ id: livePhotoVideoId, visibility: AssetVisibility.HIDDEN });
|
||||
await eventRepository.emit('asset.hide', { assetId: motionAsset.id, userId });
|
||||
await eventRepository.emit('AssetHide', { assetId: motionAsset.id, userId });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -177,7 +177,7 @@ export const onAfterUnlink = async (
|
||||
{ userId, livePhotoVideoId, visibility }: { userId: string; livePhotoVideoId: string; visibility: AssetVisibility },
|
||||
) => {
|
||||
await assetRepository.update({ id: livePhotoVideoId, visibility });
|
||||
await eventRepository.emit('asset.show', { assetId: livePhotoVideoId, userId });
|
||||
await eventRepository.emit('AssetShow', { assetId: livePhotoVideoId, userId });
|
||||
};
|
||||
|
||||
export function mapToUploadFile(file: ImmichFile): UploadFile {
|
||||
|
||||
+28
-21
@@ -31,7 +31,6 @@ import {
|
||||
import { CronJob } from 'cron';
|
||||
import { DateTime } from 'luxon';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { AssetVisibility } from 'src/enum';
|
||||
import { isIP, isIPRange } from 'validator';
|
||||
|
||||
@Injectable()
|
||||
@@ -181,23 +180,9 @@ export const ValidateDate = (options?: DateOptions & ApiPropertyOptions) => {
|
||||
return applyDecorators(...decorators);
|
||||
};
|
||||
|
||||
type AssetVisibilityOptions = { optional?: boolean };
|
||||
export const ValidateAssetVisibility = (options?: AssetVisibilityOptions & ApiPropertyOptions) => {
|
||||
const { optional, ...apiPropertyOptions } = { optional: false, ...options };
|
||||
const decorators = [
|
||||
IsEnum(AssetVisibility),
|
||||
ApiProperty({ enumName: 'AssetVisibility', enum: AssetVisibility, ...apiPropertyOptions }),
|
||||
];
|
||||
|
||||
if (optional) {
|
||||
decorators.push(Optional());
|
||||
}
|
||||
return applyDecorators(...decorators);
|
||||
};
|
||||
|
||||
type BooleanOptions = { optional?: boolean };
|
||||
type BooleanOptions = { optional?: boolean; nullable?: boolean };
|
||||
export const ValidateBoolean = (options?: BooleanOptions & ApiPropertyOptions) => {
|
||||
const { optional, ...apiPropertyOptions } = { optional: false, ...options };
|
||||
const { optional, nullable, ...apiPropertyOptions } = options || {};
|
||||
const decorators = [
|
||||
ApiProperty(apiPropertyOptions),
|
||||
IsBoolean(),
|
||||
@@ -209,15 +194,37 @@ export const ValidateBoolean = (options?: BooleanOptions & ApiPropertyOptions) =
|
||||
}
|
||||
return value;
|
||||
}),
|
||||
optional ? Optional({ nullable }) : IsNotEmpty(),
|
||||
];
|
||||
|
||||
if (optional) {
|
||||
decorators.push(Optional());
|
||||
}
|
||||
|
||||
return applyDecorators(...decorators);
|
||||
};
|
||||
|
||||
type EnumOptions<T> = {
|
||||
enum: T;
|
||||
name: string;
|
||||
each?: boolean;
|
||||
optional?: boolean;
|
||||
nullable?: boolean;
|
||||
default?: T[keyof T];
|
||||
description?: string;
|
||||
};
|
||||
export const ValidateEnum = <T extends object>({
|
||||
name,
|
||||
enum: value,
|
||||
each,
|
||||
optional,
|
||||
nullable,
|
||||
default: defaultValue,
|
||||
description,
|
||||
}: EnumOptions<T>) => {
|
||||
return applyDecorators(
|
||||
optional ? Optional({ nullable }) : IsNotEmpty(),
|
||||
IsEnum(value, { each }),
|
||||
ApiProperty({ enumName: name, enum: value, isArray: each, default: defaultValue, description }),
|
||||
);
|
||||
};
|
||||
|
||||
@ValidatorConstraint({ name: 'cronValidator' })
|
||||
class CronValidator implements ValidatorConstraintInterface {
|
||||
validate(expression: string): boolean {
|
||||
|
||||
Reference in New Issue
Block a user