mirror of
				https://github.com/immich-app/immich.git
				synced 2025-11-03 19:17:11 -05:00 
			
		
		
		
	chore: organize config, validation, decorators (#8118)
* refactor: validation * refactor: utilities * refactor: config
This commit is contained in:
		
							parent
							
								
									92cc647cf6
								
							
						
					
					
						commit
						81f0265095
					
				@ -1,7 +1,42 @@
 | 
			
		||||
import { RegisterQueueOptions } from '@nestjs/bullmq';
 | 
			
		||||
import { ConfigModuleOptions } from '@nestjs/config';
 | 
			
		||||
import { QueueOptions } from 'bullmq';
 | 
			
		||||
import { RedisOptions } from 'ioredis';
 | 
			
		||||
import Joi from 'joi';
 | 
			
		||||
import { QueueName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { LogLevel } from 'src/infra/entities/system-config.entity';
 | 
			
		||||
 | 
			
		||||
const WHEN_DB_URL_SET = Joi.when('DB_URL', {
 | 
			
		||||
  is: Joi.exist(),
 | 
			
		||||
  then: Joi.string().optional(),
 | 
			
		||||
  otherwise: Joi.string().required(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const immichAppConfig: ConfigModuleOptions = {
 | 
			
		||||
  envFilePath: '.env',
 | 
			
		||||
  isGlobal: true,
 | 
			
		||||
  validationSchema: Joi.object({
 | 
			
		||||
    NODE_ENV: Joi.string().optional().valid('development', 'production', 'staging').default('development'),
 | 
			
		||||
    LOG_LEVEL: Joi.string()
 | 
			
		||||
      .optional()
 | 
			
		||||
      .valid(...Object.values(LogLevel)),
 | 
			
		||||
 | 
			
		||||
    DB_USERNAME: WHEN_DB_URL_SET,
 | 
			
		||||
    DB_PASSWORD: WHEN_DB_URL_SET,
 | 
			
		||||
    DB_DATABASE_NAME: WHEN_DB_URL_SET,
 | 
			
		||||
    DB_URL: Joi.string().optional(),
 | 
			
		||||
    DB_VECTOR_EXTENSION: Joi.string().optional().valid('pgvector', 'pgvecto.rs').default('pgvecto.rs'),
 | 
			
		||||
 | 
			
		||||
    MACHINE_LEARNING_PORT: Joi.number().optional(),
 | 
			
		||||
    MICROSERVICES_PORT: Joi.number().optional(),
 | 
			
		||||
    IMMICH_METRICS_PORT: Joi.number().optional(),
 | 
			
		||||
 | 
			
		||||
    IMMICH_METRICS: Joi.boolean().optional().default(false),
 | 
			
		||||
    IMMICH_HOST_METRICS: Joi.boolean().optional().default(false),
 | 
			
		||||
    IMMICH_API_METRICS: Joi.boolean().optional().default(false),
 | 
			
		||||
    IMMICH_IO_METRICS: Joi.boolean().optional().default(false),
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function parseRedisConfig(): RedisOptions {
 | 
			
		||||
  const redisUrl = process.env.REDIS_URL;
 | 
			
		||||
							
								
								
									
										124
									
								
								server/src/decorators.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								server/src/decorators.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
			
		||||
import { SetMetadata } from '@nestjs/common';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { setUnion } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
 | 
			
		||||
// maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching
 | 
			
		||||
// by a list of IDs) requires splitting the query into multiple chunks.
 | 
			
		||||
// We are rounding down this limit, as queries commonly include other filters and parameters.
 | 
			
		||||
export const DATABASE_PARAMETER_CHUNK_SIZE = 65_500;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Chunks an array or set into smaller collections of the same type and specified size.
 | 
			
		||||
 *
 | 
			
		||||
 * @param collection The collection to chunk.
 | 
			
		||||
 * @param size The size of each chunk.
 | 
			
		||||
 */
 | 
			
		||||
function chunks<T>(collection: Array<T>, size: number): Array<Array<T>>;
 | 
			
		||||
function chunks<T>(collection: Set<T>, size: number): Array<Set<T>>;
 | 
			
		||||
function chunks<T>(collection: Array<T> | Set<T>, size: number): Array<Array<T>> | Array<Set<T>> {
 | 
			
		||||
  if (collection instanceof Set) {
 | 
			
		||||
    const result = [];
 | 
			
		||||
    let chunk = new Set<T>();
 | 
			
		||||
    for (const element of collection) {
 | 
			
		||||
      chunk.add(element);
 | 
			
		||||
      if (chunk.size === size) {
 | 
			
		||||
        result.push(chunk);
 | 
			
		||||
        chunk = new Set<T>();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (chunk.size > 0) {
 | 
			
		||||
      result.push(chunk);
 | 
			
		||||
    }
 | 
			
		||||
    return result;
 | 
			
		||||
  } else {
 | 
			
		||||
    return _.chunk(collection, size);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wraps a method that takes a collection of parameters and sequentially calls it with chunks of the collection,
 | 
			
		||||
 * to overcome the maximum number of parameters allowed by the database driver.
 | 
			
		||||
 *
 | 
			
		||||
 * @param options.paramIndex The index of the function parameter to chunk. Defaults to 0.
 | 
			
		||||
 * @param options.flatten Whether to flatten the results. Defaults to false.
 | 
			
		||||
 */
 | 
			
		||||
export function Chunked(options: { paramIndex?: number; mergeFn?: (results: any) => any } = {}): MethodDecorator {
 | 
			
		||||
  return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
 | 
			
		||||
    const originalMethod = descriptor.value;
 | 
			
		||||
    const parameterIndex = options.paramIndex ?? 0;
 | 
			
		||||
    descriptor.value = async function (...arguments_: any[]) {
 | 
			
		||||
      const argument = arguments_[parameterIndex];
 | 
			
		||||
 | 
			
		||||
      // Early return if argument length is less than or equal to the chunk size.
 | 
			
		||||
      if (
 | 
			
		||||
        (Array.isArray(argument) && argument.length <= DATABASE_PARAMETER_CHUNK_SIZE) ||
 | 
			
		||||
        (argument instanceof Set && argument.size <= DATABASE_PARAMETER_CHUNK_SIZE)
 | 
			
		||||
      ) {
 | 
			
		||||
        return await originalMethod.apply(this, arguments_);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return Promise.all(
 | 
			
		||||
        chunks(argument, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => {
 | 
			
		||||
          await Reflect.apply(originalMethod, this, [
 | 
			
		||||
            ...arguments_.slice(0, parameterIndex),
 | 
			
		||||
            chunk,
 | 
			
		||||
            ...arguments_.slice(parameterIndex + 1),
 | 
			
		||||
          ]);
 | 
			
		||||
        }),
 | 
			
		||||
      ).then((results) => (options.mergeFn ? options.mergeFn(results) : results));
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ChunkedArray(options?: { paramIndex?: number }): MethodDecorator {
 | 
			
		||||
  return Chunked({ ...options, mergeFn: _.flatten });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator {
 | 
			
		||||
  return Chunked({ ...options, mergeFn: setUnion });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://stackoverflow.com/a/74898678
 | 
			
		||||
export function DecorateAll(
 | 
			
		||||
  decorator: <T>(
 | 
			
		||||
    target: any,
 | 
			
		||||
    propertyKey: string,
 | 
			
		||||
    descriptor: TypedPropertyDescriptor<T>,
 | 
			
		||||
  ) => TypedPropertyDescriptor<T> | void,
 | 
			
		||||
) {
 | 
			
		||||
  return (target: any) => {
 | 
			
		||||
    const descriptors = Object.getOwnPropertyDescriptors(target.prototype);
 | 
			
		||||
    for (const [propName, descriptor] of Object.entries(descriptors)) {
 | 
			
		||||
      const isMethod = typeof descriptor.value == 'function' && propName !== 'constructor';
 | 
			
		||||
      if (!isMethod) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      decorator({ ...target, constructor: { ...target.constructor, name: target.name } as any }, propName, descriptor);
 | 
			
		||||
      Object.defineProperty(target.prototype, propName, descriptor);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const UUID = '00000000-0000-4000-a000-000000000000';
 | 
			
		||||
 | 
			
		||||
export const DummyValue = {
 | 
			
		||||
  UUID,
 | 
			
		||||
  UUID_SET: new Set([UUID]),
 | 
			
		||||
  PAGINATION: { take: 10, skip: 0 },
 | 
			
		||||
  EMAIL: 'user@immich.app',
 | 
			
		||||
  STRING: 'abcdefghi',
 | 
			
		||||
  BUFFER: Buffer.from('abcdefghi'),
 | 
			
		||||
  DATE: new Date(),
 | 
			
		||||
  TIME_BUCKET: '2024-01-01T00:00:00.000Z',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const GENERATE_SQL_KEY = 'generate-sql-key';
 | 
			
		||||
 | 
			
		||||
export interface GenerateSqlQueries {
 | 
			
		||||
  name?: string;
 | 
			
		||||
  params: unknown[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Decorator to enable versioning/tracking of generated Sql */
 | 
			
		||||
export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options);
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { setDifference, setIsEqual, setUnion } from 'src/domain/domain.util';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
import { SharedLinkEntity } from 'src/infra/entities/shared-link.entity';
 | 
			
		||||
import { setDifference, setIsEqual, setUnion } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
export enum Permission {
 | 
			
		||||
  ACTIVITY_CREATE = 'activity.create',
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { UserDto, mapSimpleUser } from 'src/domain/user/response-dto/user-response.dto';
 | 
			
		||||
import { ActivityEntity } from 'src/infra/entities/activity.entity';
 | 
			
		||||
import { Optional, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export enum ReactionType {
 | 
			
		||||
  COMMENT = 'comment',
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { Optional } from 'src/domain/domain.util';
 | 
			
		||||
import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto';
 | 
			
		||||
import { AlbumEntity, AssetOrder } from 'src/infra/entities/album.entity';
 | 
			
		||||
import { Optional } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class AlbumResponseDto {
 | 
			
		||||
  id!: string;
 | 
			
		||||
 | 
			
		||||
@ -14,7 +14,6 @@ import { AlbumInfoDto } from 'src/domain/album/dto/album.dto';
 | 
			
		||||
import { GetAlbumsDto } from 'src/domain/album/dto/get-albums.dto';
 | 
			
		||||
import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { setUnion } from 'src/domain/domain.util';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/domain/repositories/album.repository';
 | 
			
		||||
import { IAssetRepository } from 'src/domain/repositories/asset.repository';
 | 
			
		||||
@ -22,6 +21,7 @@ import { IUserRepository } from 'src/domain/repositories/user.repository';
 | 
			
		||||
import { AlbumEntity } from 'src/infra/entities/album.entity';
 | 
			
		||||
import { AssetEntity } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { UserEntity } from 'src/infra/entities/user.entity';
 | 
			
		||||
import { setUnion } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AlbumService {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { ArrayNotEmpty } from 'class-validator';
 | 
			
		||||
import { ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class AddUsersDto {
 | 
			
		||||
  @ValidateUUID({ each: true })
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { Optional, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class CreateAlbumDto {
 | 
			
		||||
  @IsString()
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsEnum, IsString } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { AssetOrder } from 'src/infra/entities/album.entity';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class UpdateAlbumDto {
 | 
			
		||||
  @Optional()
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class AlbumInfoDto {
 | 
			
		||||
  @ValidateBoolean({ optional: true })
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ValidateBoolean, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class GetAlbumsDto {
 | 
			
		||||
  @ValidateBoolean({ optional: true })
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
import { Optional } from 'src/domain/domain.util';
 | 
			
		||||
import { Optional } from 'src/validation';
 | 
			
		||||
export class APIKeyCreateDto {
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
 | 
			
		||||
@ -21,7 +21,6 @@ import { MapMarkerResponseDto } from 'src/domain/asset/response-dto/map-marker-r
 | 
			
		||||
import { TimeBucketResponseDto } from 'src/domain/asset/response-dto/time-bucket-response.dto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { mimeTypes } from 'src/domain/domain.constant';
 | 
			
		||||
import { usePagination } from 'src/domain/domain.util';
 | 
			
		||||
import { JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IAssetDeletionJob, ISidecarWriteJob } from 'src/domain/job/job.interface';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
@ -38,6 +37,7 @@ import { SystemConfigCore } from 'src/domain/system-config/system-config.core';
 | 
			
		||||
import { AssetEntity } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { LibraryType } from 'src/infra/entities/library.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { usePagination } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
export enum UploadFieldName {
 | 
			
		||||
  ASSET_DATA = 'assetData',
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsEnum } from 'class-validator';
 | 
			
		||||
import { ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class AssetIdsDto {
 | 
			
		||||
  @ValidateUUID({ each: true })
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class UpdateStackParentDto {
 | 
			
		||||
  @ValidateUUID()
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { AssetStats } from 'src/domain/repositories/asset.repository';
 | 
			
		||||
import { AssetType } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class AssetStatsDto {
 | 
			
		||||
  @ValidateBoolean({ optional: true })
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,7 @@ import {
 | 
			
		||||
  ValidateIf,
 | 
			
		||||
} from 'class-validator';
 | 
			
		||||
import { BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class DeviceIdDto {
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ValidateBoolean, ValidateDate } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean, ValidateDate } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class MapMarkerDto {
 | 
			
		||||
  @ValidateBoolean({ optional: true })
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { TimeBucketSize } from 'src/domain/repositories/asset.repository';
 | 
			
		||||
import { AssetOrder } from 'src/infra/entities/album.entity';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class TimeBucketDto {
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
/** @deprecated Use `BulkIdResponseDto` instead */
 | 
			
		||||
export enum AssetIdErrorReason {
 | 
			
		||||
 | 
			
		||||
@ -1,9 +1,9 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { Type } from 'class-transformer';
 | 
			
		||||
import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateDate, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { EntityType } from 'src/infra/entities/audit.entity';
 | 
			
		||||
import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/infra/entities/move.entity';
 | 
			
		||||
import { Optional, ValidateDate, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,6 @@ import {
 | 
			
		||||
} from 'src/domain/audit/audit.dto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { AUDIT_LOG_MAX_DURATION } from 'src/domain/domain.constant';
 | 
			
		||||
import { usePagination } from 'src/domain/domain.util';
 | 
			
		||||
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
import { IAssetRepository } from 'src/domain/repositories/asset.repository';
 | 
			
		||||
@ -26,6 +25,7 @@ import { StorageCore, StorageFolder } from 'src/domain/storage/storage.core';
 | 
			
		||||
import { DatabaseAction } from 'src/infra/entities/audit.entity';
 | 
			
		||||
import { AssetPathType, PersonPathType, UserPathType } from 'src/infra/entities/move.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { usePagination } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class AuditService {
 | 
			
		||||
 | 
			
		||||
@ -34,7 +34,6 @@ import {
 | 
			
		||||
  mapLoginResponse,
 | 
			
		||||
  mapUserToken,
 | 
			
		||||
} from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { HumanReadableSize } from 'src/domain/domain.util';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
import { IKeyRepository } from 'src/domain/repositories/api-key.repository';
 | 
			
		||||
import { ICryptoRepository } from 'src/domain/repositories/crypto.repository';
 | 
			
		||||
@ -49,6 +48,7 @@ import { UserCore } from 'src/domain/user/user.core';
 | 
			
		||||
import { SystemConfig } from 'src/infra/entities/system-config.entity';
 | 
			
		||||
import { UserEntity } from 'src/infra/entities/user.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { HumanReadableSize } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
export interface LoginDetails {
 | 
			
		||||
  isSecure: boolean;
 | 
			
		||||
 | 
			
		||||
@ -1,36 +0,0 @@
 | 
			
		||||
// TODO: remove nestjs references from domain
 | 
			
		||||
import { ConfigModuleOptions } from '@nestjs/config';
 | 
			
		||||
import Joi from 'joi';
 | 
			
		||||
import { LogLevel } from 'src/infra/entities/system-config.entity';
 | 
			
		||||
 | 
			
		||||
const WHEN_DB_URL_SET = Joi.when('DB_URL', {
 | 
			
		||||
  is: Joi.exist(),
 | 
			
		||||
  then: Joi.string().optional(),
 | 
			
		||||
  otherwise: Joi.string().required(),
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const immichAppConfig: ConfigModuleOptions = {
 | 
			
		||||
  envFilePath: '.env',
 | 
			
		||||
  isGlobal: true,
 | 
			
		||||
  validationSchema: Joi.object({
 | 
			
		||||
    NODE_ENV: Joi.string().optional().valid('development', 'production', 'staging').default('development'),
 | 
			
		||||
    LOG_LEVEL: Joi.string()
 | 
			
		||||
      .optional()
 | 
			
		||||
      .valid(...Object.values(LogLevel)),
 | 
			
		||||
 | 
			
		||||
    DB_USERNAME: WHEN_DB_URL_SET,
 | 
			
		||||
    DB_PASSWORD: WHEN_DB_URL_SET,
 | 
			
		||||
    DB_DATABASE_NAME: WHEN_DB_URL_SET,
 | 
			
		||||
    DB_URL: Joi.string().optional(),
 | 
			
		||||
    DB_VECTOR_EXTENSION: Joi.string().optional().valid('pgvector', 'pgvecto.rs').default('pgvecto.rs'),
 | 
			
		||||
 | 
			
		||||
    MACHINE_LEARNING_PORT: Joi.number().optional(),
 | 
			
		||||
    MICROSERVICES_PORT: Joi.number().optional(),
 | 
			
		||||
    IMMICH_METRICS_PORT: Joi.number().optional(),
 | 
			
		||||
 | 
			
		||||
    IMMICH_METRICS: Joi.boolean().optional().default(false),
 | 
			
		||||
    IMMICH_HOST_METRICS: Joi.boolean().optional().default(false),
 | 
			
		||||
    IMMICH_API_METRICS: Joi.boolean().optional().default(false),
 | 
			
		||||
    IMMICH_IO_METRICS: Joi.boolean().optional().default(false),
 | 
			
		||||
  }),
 | 
			
		||||
};
 | 
			
		||||
@ -1,280 +0,0 @@
 | 
			
		||||
import { BadRequestException, applyDecorators } from '@nestjs/common';
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { Transform } from 'class-transformer';
 | 
			
		||||
import {
 | 
			
		||||
  IsArray,
 | 
			
		||||
  IsBoolean,
 | 
			
		||||
  IsDate,
 | 
			
		||||
  IsNotEmpty,
 | 
			
		||||
  IsOptional,
 | 
			
		||||
  IsString,
 | 
			
		||||
  IsUUID,
 | 
			
		||||
  ValidateIf,
 | 
			
		||||
  ValidationOptions,
 | 
			
		||||
  isDateString,
 | 
			
		||||
} from 'class-validator';
 | 
			
		||||
import { CronJob } from 'cron';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { basename, extname } from 'node:path';
 | 
			
		||||
import sanitize from 'sanitize-filename';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
 | 
			
		||||
export enum CacheControl {
 | 
			
		||||
  PRIVATE_WITH_CACHE = 'private_with_cache',
 | 
			
		||||
  PRIVATE_WITHOUT_CACHE = 'private_without_cache',
 | 
			
		||||
  NONE = 'none',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class ImmichFileResponse {
 | 
			
		||||
  public readonly path!: string;
 | 
			
		||||
  public readonly contentType!: string;
 | 
			
		||||
  public readonly cacheControl!: CacheControl;
 | 
			
		||||
 | 
			
		||||
  constructor(response: ImmichFileResponse) {
 | 
			
		||||
    Object.assign(this, response);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface OpenGraphTags {
 | 
			
		||||
  title: string;
 | 
			
		||||
  description: string;
 | 
			
		||||
  imageUrl?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED';
 | 
			
		||||
 | 
			
		||||
type UUIDOptions = { optional?: boolean; each?: boolean };
 | 
			
		||||
export const ValidateUUID = (options?: UUIDOptions) => {
 | 
			
		||||
  const { optional, each } = { optional: false, each: false, ...options };
 | 
			
		||||
  return applyDecorators(
 | 
			
		||||
    IsUUID('4', { each }),
 | 
			
		||||
    ApiProperty({ format: 'uuid' }),
 | 
			
		||||
    optional ? Optional() : IsNotEmpty(),
 | 
			
		||||
    each ? IsArray() : IsString(),
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type DateOptions = { optional?: boolean; nullable?: boolean; format?: 'date' | 'date-time' };
 | 
			
		||||
export const ValidateDate = (options?: DateOptions) => {
 | 
			
		||||
  const { optional, nullable, format } = { optional: false, nullable: false, format: 'date-time', ...options };
 | 
			
		||||
 | 
			
		||||
  const decorators = [
 | 
			
		||||
    ApiProperty({ format }),
 | 
			
		||||
    IsDate(),
 | 
			
		||||
    optional ? Optional({ nullable: true }) : IsNotEmpty(),
 | 
			
		||||
    Transform(({ key, value }) => {
 | 
			
		||||
      if (value === null || value === undefined) {
 | 
			
		||||
        return value;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!isDateString(value)) {
 | 
			
		||||
        throw new BadRequestException(`${key} must be a date string`);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return new Date(value as string);
 | 
			
		||||
    }),
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  if (optional) {
 | 
			
		||||
    decorators.push(Optional({ nullable }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return applyDecorators(...decorators);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
type BooleanOptions = { optional?: boolean };
 | 
			
		||||
export const ValidateBoolean = (options?: BooleanOptions) => {
 | 
			
		||||
  const { optional } = { optional: false, ...options };
 | 
			
		||||
  const decorators = [
 | 
			
		||||
    // ApiProperty(),
 | 
			
		||||
    IsBoolean(),
 | 
			
		||||
    Transform(({ value }) => {
 | 
			
		||||
      if (value == 'true') {
 | 
			
		||||
        return true;
 | 
			
		||||
      } else if (value == 'false') {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      return value;
 | 
			
		||||
    }),
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  if (optional) {
 | 
			
		||||
    decorators.push(Optional());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return applyDecorators(...decorators);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function validateCronExpression(expression: string) {
 | 
			
		||||
  try {
 | 
			
		||||
    new CronJob(expression, () => {});
 | 
			
		||||
  } catch {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type IValue = { value: string };
 | 
			
		||||
 | 
			
		||||
export const toEmail = ({ value }: IValue) => value?.toLowerCase();
 | 
			
		||||
 | 
			
		||||
export const toSanitized = ({ value }: IValue) => sanitize((value || '').replaceAll('.', ''));
 | 
			
		||||
 | 
			
		||||
export function getFileNameWithoutExtension(path: string): string {
 | 
			
		||||
  return basename(path, extname(path));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getLivePhotoMotionFilename(stillName: string, motionName: string) {
 | 
			
		||||
  return getFileNameWithoutExtension(stillName) + extname(motionName);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const KiB = Math.pow(1024, 1);
 | 
			
		||||
const MiB = Math.pow(1024, 2);
 | 
			
		||||
const GiB = Math.pow(1024, 3);
 | 
			
		||||
const TiB = Math.pow(1024, 4);
 | 
			
		||||
const PiB = Math.pow(1024, 5);
 | 
			
		||||
 | 
			
		||||
export const HumanReadableSize = { KiB, MiB, GiB, TiB, PiB };
 | 
			
		||||
 | 
			
		||||
export function asHumanReadable(bytes: number, precision = 1): string {
 | 
			
		||||
  const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
 | 
			
		||||
 | 
			
		||||
  let magnitude = 0;
 | 
			
		||||
  let remainder = bytes;
 | 
			
		||||
  while (remainder >= 1024) {
 | 
			
		||||
    if (magnitude + 1 < units.length) {
 | 
			
		||||
      magnitude++;
 | 
			
		||||
      remainder /= 1024;
 | 
			
		||||
    } else {
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PaginationOptions {
 | 
			
		||||
  take: number;
 | 
			
		||||
  skip?: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export enum PaginationMode {
 | 
			
		||||
  LIMIT_OFFSET = 'limit-offset',
 | 
			
		||||
  SKIP_TAKE = 'skip-take',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PaginatedBuilderOptions {
 | 
			
		||||
  take: number;
 | 
			
		||||
  skip?: number;
 | 
			
		||||
  mode?: PaginationMode;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PaginationResult<T> {
 | 
			
		||||
  items: T[];
 | 
			
		||||
  hasNextPage: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type Paginated<T> = Promise<PaginationResult<T>>;
 | 
			
		||||
 | 
			
		||||
export async function* usePagination<T>(
 | 
			
		||||
  pageSize: number,
 | 
			
		||||
  getNextPage: (pagination: PaginationOptions) => PaginationResult<T> | Paginated<T>,
 | 
			
		||||
) {
 | 
			
		||||
  let hasNextPage = true;
 | 
			
		||||
 | 
			
		||||
  for (let skip = 0; hasNextPage; skip += pageSize) {
 | 
			
		||||
    const result = await getNextPage({ take: pageSize, skip });
 | 
			
		||||
    hasNextPage = result.hasNextPage;
 | 
			
		||||
    yield result.items;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface OptionalOptions extends ValidationOptions {
 | 
			
		||||
  nullable?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Checks if value is missing and if so, ignores all validators.
 | 
			
		||||
 *
 | 
			
		||||
 * @param validationOptions {@link OptionalOptions}
 | 
			
		||||
 *
 | 
			
		||||
 * @see IsOptional exported from `class-validator.
 | 
			
		||||
 */
 | 
			
		||||
// https://stackoverflow.com/a/71353929
 | 
			
		||||
export function Optional({ nullable, ...validationOptions }: OptionalOptions = {}) {
 | 
			
		||||
  if (nullable === true) {
 | 
			
		||||
    return IsOptional(validationOptions);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return ValidateIf((object: any, v: any) => v !== undefined, validationOptions);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Chunks an array or set into smaller collections of the same type and specified size.
 | 
			
		||||
 *
 | 
			
		||||
 * @param collection The collection to chunk.
 | 
			
		||||
 * @param size The size of each chunk.
 | 
			
		||||
 */
 | 
			
		||||
export function chunks<T>(collection: Array<T>, size: number): Array<Array<T>>;
 | 
			
		||||
export function chunks<T>(collection: Set<T>, size: number): Array<Set<T>>;
 | 
			
		||||
export function chunks<T>(collection: Array<T> | Set<T>, size: number): Array<Array<T>> | Array<Set<T>> {
 | 
			
		||||
  if (collection instanceof Set) {
 | 
			
		||||
    const result = [];
 | 
			
		||||
    let chunk = new Set<T>();
 | 
			
		||||
    for (const element of collection) {
 | 
			
		||||
      chunk.add(element);
 | 
			
		||||
      if (chunk.size === size) {
 | 
			
		||||
        result.push(chunk);
 | 
			
		||||
        chunk = new Set<T>();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (chunk.size > 0) {
 | 
			
		||||
      result.push(chunk);
 | 
			
		||||
    }
 | 
			
		||||
    return result;
 | 
			
		||||
  } else {
 | 
			
		||||
    return _.chunk(collection, size);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NOTE: The following Set utils have been added here, to easily determine where they are used.
 | 
			
		||||
//       They should be replaced with native Set operations, when they are added to the language.
 | 
			
		||||
//       Proposal reference: https://github.com/tc39/proposal-set-methods
 | 
			
		||||
 | 
			
		||||
export const setUnion = <T>(...sets: Set<T>[]): Set<T> => {
 | 
			
		||||
  const union = new Set(sets[0]);
 | 
			
		||||
  for (const set of sets.slice(1)) {
 | 
			
		||||
    for (const element of set) {
 | 
			
		||||
      union.add(element);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return union;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const setDifference = <T>(setA: Set<T>, ...sets: Set<T>[]): Set<T> => {
 | 
			
		||||
  const difference = new Set(setA);
 | 
			
		||||
  for (const set of sets) {
 | 
			
		||||
    for (const element of set) {
 | 
			
		||||
      difference.delete(element);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return difference;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const setIsSuperset = <T>(set: Set<T>, subset: Set<T>): boolean => {
 | 
			
		||||
  for (const element of subset) {
 | 
			
		||||
    if (!set.has(element)) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const setIsEqual = <T>(setA: Set<T>, setB: Set<T>): boolean => {
 | 
			
		||||
  return setA.size === setB.size && setIsSuperset(setA, setB);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const handlePromiseError = <T>(promise: Promise<T>, logger: ImmichLogger): void => {
 | 
			
		||||
  promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack));
 | 
			
		||||
};
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsInt, IsPositive } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { Optional, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class DownloadInfoDto {
 | 
			
		||||
  @ValidateUUID({ each: true, optional: true })
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
import { BadRequestException } from '@nestjs/common';
 | 
			
		||||
import { when } from 'jest-when';
 | 
			
		||||
import { CacheControl, ImmichFileResponse } from 'src/domain/domain.util';
 | 
			
		||||
import { DownloadResponseDto } from 'src/domain/download/download.dto';
 | 
			
		||||
import { DownloadService } from 'src/domain/download/download.service';
 | 
			
		||||
import { IAssetRepository } from 'src/domain/repositories/asset.repository';
 | 
			
		||||
import { IStorageRepository } from 'src/domain/repositories/storage.repository';
 | 
			
		||||
import { CacheControl, ImmichFileResponse } from 'src/utils';
 | 
			
		||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
			
		||||
import { authStub } from 'test/fixtures/auth.stub';
 | 
			
		||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
 | 
			
		||||
 | 
			
		||||
@ -4,12 +4,12 @@ import { AccessCore, Permission } from 'src/domain/access/access.core';
 | 
			
		||||
import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { mimeTypes } from 'src/domain/domain.constant';
 | 
			
		||||
import { CacheControl, HumanReadableSize, ImmichFileResponse, usePagination } from 'src/domain/domain.util';
 | 
			
		||||
import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/domain/download/download.dto';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
import { IAssetRepository } from 'src/domain/repositories/asset.repository';
 | 
			
		||||
import { IStorageRepository, ImmichReadStream } from 'src/domain/repositories/storage.repository';
 | 
			
		||||
import { AssetEntity } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { CacheControl, HumanReadableSize, ImmichFileResponse, usePagination } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class DownloadService {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsEnum, IsNotEmpty } from 'class-validator';
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { JobCommand, QueueName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class JobIdParamDto {
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { ArrayMaxSize, ArrayUnique, IsEnum, IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { LibraryEntity, LibraryType } from 'src/infra/entities/library.entity';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class CreateLibraryDto {
 | 
			
		||||
  @IsEnum(LibraryType)
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,6 @@ import { Stats } from 'node:fs';
 | 
			
		||||
import path, { basename, parse } from 'node:path';
 | 
			
		||||
import picomatch from 'picomatch';
 | 
			
		||||
import { mimeTypes } from 'src/domain/domain.constant';
 | 
			
		||||
import { handlePromiseError, usePagination, validateCronExpression } from 'src/domain/domain.util';
 | 
			
		||||
import { JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob } from 'src/domain/job/job.interface';
 | 
			
		||||
import {
 | 
			
		||||
@ -35,6 +34,8 @@ import { SystemConfigCore } from 'src/domain/system-config/system-config.core';
 | 
			
		||||
import { AssetType } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { LibraryEntity, LibraryType } from 'src/infra/entities/library.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { handlePromiseError, usePagination } from 'src/utils';
 | 
			
		||||
import { validateCronExpression } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
const LIBRARY_SCAN_BATCH_SIZE = 5000;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
 | 
			
		||||
import { usePagination } from 'src/domain/domain.util';
 | 
			
		||||
import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IBaseJob, IEntityJob } from 'src/domain/job/job.interface';
 | 
			
		||||
import {
 | 
			
		||||
@ -39,6 +38,7 @@ import {
 | 
			
		||||
  VideoCodec,
 | 
			
		||||
} from 'src/infra/entities/system-config.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { usePagination } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class MediaService {
 | 
			
		||||
 | 
			
		||||
@ -6,7 +6,6 @@ import { Duration } from 'luxon';
 | 
			
		||||
import { constants } from 'node:fs/promises';
 | 
			
		||||
import path from 'node:path';
 | 
			
		||||
import { Subscription } from 'rxjs';
 | 
			
		||||
import { handlePromiseError, usePagination } from 'src/domain/domain.util';
 | 
			
		||||
import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IBaseJob, IEntityJob, ISidecarWriteJob } from 'src/domain/job/job.interface';
 | 
			
		||||
import { IAlbumRepository } from 'src/domain/repositories/album.repository';
 | 
			
		||||
@ -26,6 +25,7 @@ import { FeatureFlag, SystemConfigCore } from 'src/domain/system-config/system-c
 | 
			
		||||
import { AssetEntity, AssetType } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { ExifEntity } from 'src/infra/entities/exif.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { handlePromiseError, usePagination } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
/** look for a date from these tags (in order) */
 | 
			
		||||
const EXIF_DATE_TAGS: Array<keyof Tags> = [
 | 
			
		||||
 | 
			
		||||
@ -2,9 +2,9 @@ import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { Type } from 'class-transformer';
 | 
			
		||||
import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { AssetFaceEntity } from 'src/infra/entities/asset-face.entity';
 | 
			
		||||
import { PersonEntity } from 'src/infra/entities/person.entity';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class PersonCreateDto {
 | 
			
		||||
  /**
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,5 @@
 | 
			
		||||
import { BadRequestException, NotFoundException } from '@nestjs/common';
 | 
			
		||||
import { BulkIdErrorReason } from 'src/domain/asset/response-dto/asset-ids-response.dto';
 | 
			
		||||
import { CacheControl, ImmichFileResponse } from 'src/domain/domain.util';
 | 
			
		||||
import { JobName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { PersonResponseDto, mapFaces, mapPerson } from 'src/domain/person/person.dto';
 | 
			
		||||
import { PersonService } from 'src/domain/person/person.service';
 | 
			
		||||
@ -16,6 +15,7 @@ import { IStorageRepository } from 'src/domain/repositories/storage.repository';
 | 
			
		||||
import { ISystemConfigRepository } from 'src/domain/repositories/system-config.repository';
 | 
			
		||||
import { AssetFaceEntity } from 'src/infra/entities/asset-face.entity';
 | 
			
		||||
import { Colorspace, SystemConfigKey } from 'src/infra/entities/system-config.entity';
 | 
			
		||||
import { CacheControl, ImmichFileResponse } from 'src/utils';
 | 
			
		||||
import { assetStub } from 'test/fixtures/asset.stub';
 | 
			
		||||
import { authStub } from 'test/fixtures/auth.stub';
 | 
			
		||||
import { faceStub } from 'test/fixtures/face.stub';
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ import { BulkIdErrorReason, BulkIdResponseDto } from 'src/domain/asset/response-
 | 
			
		||||
import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { mimeTypes } from 'src/domain/domain.constant';
 | 
			
		||||
import { CacheControl, ImmichFileResponse, usePagination } from 'src/domain/domain.util';
 | 
			
		||||
import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IBaseJob, IDeferrableJob, IEntityJob } from 'src/domain/job/job.interface';
 | 
			
		||||
import { FACE_THUMBNAIL_SIZE } from 'src/domain/media/media.constant';
 | 
			
		||||
@ -39,6 +38,7 @@ import { SystemConfigCore } from 'src/domain/system-config/system-config.core';
 | 
			
		||||
import { PersonPathType } from 'src/infra/entities/move.entity';
 | 
			
		||||
import { PersonEntity } from 'src/infra/entities/person.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { CacheControl, ImmichFileResponse, usePagination } from 'src/utils';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
import { Paginated, PaginationOptions } from 'src/domain/domain.util';
 | 
			
		||||
import { ReverseGeocodeResult } from 'src/domain/repositories/metadata.repository';
 | 
			
		||||
import { AssetSearchOptions, SearchExploreItem } from 'src/domain/repositories/search.repository';
 | 
			
		||||
import { AssetOrder } from 'src/infra/entities/album.entity';
 | 
			
		||||
import { AssetJobStatusEntity } from 'src/infra/entities/asset-job-status.entity';
 | 
			
		||||
import { AssetEntity, AssetType } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { ExifEntity } from 'src/infra/entities/exif.entity';
 | 
			
		||||
import { Paginated, PaginationOptions } from 'src/utils';
 | 
			
		||||
import { FindOptionsRelations, FindOptionsSelect } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export type AssetStats = Record<AssetType, number>;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Paginated, PaginationOptions } from 'src/domain/domain.util';
 | 
			
		||||
import { AssetFaceEntity } from 'src/infra/entities/asset-face.entity';
 | 
			
		||||
import { AssetEntity } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { PersonEntity } from 'src/infra/entities/person.entity';
 | 
			
		||||
import { Paginated, PaginationOptions } from 'src/utils';
 | 
			
		||||
import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
export const IPersonRepository = 'IPersonRepository';
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { Paginated } from 'src/domain/domain.util';
 | 
			
		||||
import { AssetFaceEntity } from 'src/infra/entities/asset-face.entity';
 | 
			
		||||
import { AssetEntity, AssetType } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { GeodataPlacesEntity } from 'src/infra/entities/geodata-places.entity';
 | 
			
		||||
import { SmartInfoEntity } from 'src/infra/entities/smart-info.entity';
 | 
			
		||||
import { Paginated } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
export const ISearchRepository = 'ISearchRepository';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
import { Optional } from 'src/domain/domain.util';
 | 
			
		||||
import { Optional } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export enum SearchSuggestionType {
 | 
			
		||||
  COUNTRY = 'country',
 | 
			
		||||
 | 
			
		||||
@ -1,10 +1,10 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { Type } from 'class-transformer';
 | 
			
		||||
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { AssetOrder } from 'src/infra/entities/album.entity';
 | 
			
		||||
import { AssetType } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { GeodataPlacesEntity } from 'src/infra/entities/geodata-places.entity';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
class BaseSearchDto {
 | 
			
		||||
  @ValidateUUID({ optional: true })
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { DateTime } from 'luxon';
 | 
			
		||||
import { Version, isDev, mimeTypes, serverVersion } from 'src/domain/domain.constant';
 | 
			
		||||
import { asHumanReadable } from 'src/domain/domain.util';
 | 
			
		||||
import { ClientEvent, ICommunicationRepository } from 'src/domain/repositories/communication.repository';
 | 
			
		||||
import { IServerInfoRepository } from 'src/domain/repositories/server-info.repository';
 | 
			
		||||
import { IStorageRepository } from 'src/domain/repositories/storage.repository';
 | 
			
		||||
@ -21,6 +20,7 @@ import { StorageCore, StorageFolder } from 'src/domain/storage/storage.core';
 | 
			
		||||
import { SystemConfigCore } from 'src/domain/system-config/system-config.core';
 | 
			
		||||
import { SystemMetadataKey } from 'src/infra/entities/system-metadata.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { asHumanReadable } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ServerInfoService {
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsEnum, IsString } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { SharedLinkType } from 'src/infra/entities/shared-link.entity';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class SharedLinkCreateDto {
 | 
			
		||||
  @IsEnum(SharedLinkType)
 | 
			
		||||
 | 
			
		||||
@ -3,7 +3,6 @@ import { AccessCore, Permission } from 'src/domain/access/access.core';
 | 
			
		||||
import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
 | 
			
		||||
import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { OpenGraphTags } from 'src/domain/domain.util';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
import { ICryptoRepository } from 'src/domain/repositories/crypto.repository';
 | 
			
		||||
import { ISharedLinkRepository } from 'src/domain/repositories/shared-link.repository';
 | 
			
		||||
@ -15,6 +14,7 @@ import {
 | 
			
		||||
import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from 'src/domain/shared-link/shared-link.dto';
 | 
			
		||||
import { AssetEntity } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { SharedLinkEntity, SharedLinkType } from 'src/infra/entities/shared-link.entity';
 | 
			
		||||
import { OpenGraphTags } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class SharedLinkService {
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { Type } from 'class-transformer';
 | 
			
		||||
import { IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { CLIPMode, ModelType } from 'src/domain/repositories/machine-learning.repository';
 | 
			
		||||
import { Optional, ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class ModelConfig {
 | 
			
		||||
  @ValidateBoolean()
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,4 @@
 | 
			
		||||
import { Inject, Injectable } from '@nestjs/common';
 | 
			
		||||
import { usePagination } from 'src/domain/domain.util';
 | 
			
		||||
import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IBaseJob, IEntityJob } from 'src/domain/job/job.interface';
 | 
			
		||||
import { IAssetRepository, WithoutProperty } from 'src/domain/repositories/asset.repository';
 | 
			
		||||
@ -10,6 +9,7 @@ import { ISearchRepository } from 'src/domain/repositories/search.repository';
 | 
			
		||||
import { ISystemConfigRepository } from 'src/domain/repositories/system-config.repository';
 | 
			
		||||
import { SystemConfigCore } from 'src/domain/system-config/system-config.core';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { usePagination } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class SmartInfoService {
 | 
			
		||||
 | 
			
		||||
@ -4,7 +4,6 @@ import handlebar from 'handlebars';
 | 
			
		||||
import { DateTime } from 'luxon';
 | 
			
		||||
import path from 'node:path';
 | 
			
		||||
import sanitize from 'sanitize-filename';
 | 
			
		||||
import { getLivePhotoMotionFilename, usePagination } from 'src/domain/domain.util';
 | 
			
		||||
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IEntityJob } from 'src/domain/job/job.interface';
 | 
			
		||||
import { IAlbumRepository } from 'src/domain/repositories/album.repository';
 | 
			
		||||
@ -33,6 +32,7 @@ import { AssetEntity, AssetType } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { AssetPathType } from 'src/infra/entities/move.entity';
 | 
			
		||||
import { SystemConfig } from 'src/infra/entities/system-config.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { getLivePhotoMotionFilename, usePagination } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
export interface MoveAssetMetadata {
 | 
			
		||||
  storageLabel: string | null;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,6 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { Type } from 'class-transformer';
 | 
			
		||||
import { IsEnum, IsInt, IsString, Max, Min } from 'class-validator';
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import {
 | 
			
		||||
  AudioCodec,
 | 
			
		||||
  CQMode,
 | 
			
		||||
@ -10,6 +9,7 @@ import {
 | 
			
		||||
  TranscodePolicy,
 | 
			
		||||
  VideoCodec,
 | 
			
		||||
} from 'src/infra/entities/system-config.entity';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class SystemConfigFFmpegDto {
 | 
			
		||||
  @IsInt()
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import {
 | 
			
		||||
  ValidatorConstraint,
 | 
			
		||||
  ValidatorConstraintInterface,
 | 
			
		||||
} from 'class-validator';
 | 
			
		||||
import { ValidateBoolean, validateCronExpression } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean, validateCronExpression } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
const isEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsEnum } from 'class-validator';
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { LogLevel } from 'src/infra/entities/system-config.entity';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class SystemConfigLoggingDto {
 | 
			
		||||
  @ValidateBoolean()
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Type } from 'class-transformer';
 | 
			
		||||
import { IsObject, IsUrl, ValidateIf, ValidateNested } from 'class-validator';
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { CLIPConfig, RecognitionConfig } from 'src/domain/smart-info/dto/model-config.dto';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class SystemConfigMachineLearningDto {
 | 
			
		||||
  @ValidateBoolean()
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { IsString } from 'class-validator';
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class SystemConfigMapDto {
 | 
			
		||||
  @ValidateBoolean()
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class SystemConfigNewVersionCheckDto {
 | 
			
		||||
  @ValidateBoolean()
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { IsNotEmpty, IsNumber, IsString, IsUrl, Min, ValidateIf } from 'class-validator';
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
const isEnabled = (config: SystemConfigOAuthDto) => config.enabled;
 | 
			
		||||
const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled;
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class SystemConfigPasswordLoginDto {
 | 
			
		||||
  @ValidateBoolean()
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class SystemConfigReverseGeocodingDto {
 | 
			
		||||
  @ValidateBoolean()
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class SystemConfigStorageTemplateDto {
 | 
			
		||||
  @ValidateBoolean()
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { Type } from 'class-transformer';
 | 
			
		||||
import { IsInt, Min } from 'class-validator';
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class SystemConfigTrashDto {
 | 
			
		||||
  @ValidateBoolean()
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
import { Optional } from 'src/domain/domain.util';
 | 
			
		||||
import { TagType } from 'src/infra/entities/tag.entity';
 | 
			
		||||
import { Optional } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class CreateTagDto {
 | 
			
		||||
  @IsString()
 | 
			
		||||
 | 
			
		||||
@ -3,12 +3,12 @@ import { DateTime } from 'luxon';
 | 
			
		||||
import { AccessCore, Permission } from 'src/domain/access/access.core';
 | 
			
		||||
import { BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { usePagination } from 'src/domain/domain.util';
 | 
			
		||||
import { JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
import { IAssetRepository } from 'src/domain/repositories/asset.repository';
 | 
			
		||||
import { ClientEvent, ICommunicationRepository } from 'src/domain/repositories/communication.repository';
 | 
			
		||||
import { IJobRepository } from 'src/domain/repositories/job.repository';
 | 
			
		||||
import { usePagination } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
export class TrashService {
 | 
			
		||||
  private access: AccessCore;
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { Transform } from 'class-transformer';
 | 
			
		||||
import { IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/domain/domain.util';
 | 
			
		||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class CreateUserDto {
 | 
			
		||||
  @IsEmail({ require_tld: false })
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,4 @@
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class DeleteUserDto {
 | 
			
		||||
  @ValidateBoolean({ optional: true })
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { Transform } from 'class-transformer';
 | 
			
		||||
import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/domain/domain.util';
 | 
			
		||||
import { UserAvatarColor } from 'src/infra/entities/user.entity';
 | 
			
		||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class UpdateUserDto {
 | 
			
		||||
  @Optional()
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,6 @@ import {
 | 
			
		||||
  NotFoundException,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
import { when } from 'jest-when';
 | 
			
		||||
import { CacheControl, ImmichFileResponse } from 'src/domain/domain.util';
 | 
			
		||||
import { JobName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IAlbumRepository } from 'src/domain/repositories/album.repository';
 | 
			
		||||
import { ICryptoRepository } from 'src/domain/repositories/crypto.repository';
 | 
			
		||||
@ -18,6 +17,7 @@ import { UpdateUserDto } from 'src/domain/user/dto/update-user.dto';
 | 
			
		||||
import { mapUser } from 'src/domain/user/response-dto/user-response.dto';
 | 
			
		||||
import { UserService } from 'src/domain/user/user.service';
 | 
			
		||||
import { UserEntity, UserStatus } from 'src/infra/entities/user.entity';
 | 
			
		||||
import { CacheControl, ImmichFileResponse } from 'src/utils';
 | 
			
		||||
import { authStub } from 'test/fixtures/auth.stub';
 | 
			
		||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
 | 
			
		||||
import { userStub } from 'test/fixtures/user.stub';
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,6 @@ import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundEx
 | 
			
		||||
import { DateTime } from 'luxon';
 | 
			
		||||
import { randomBytes } from 'node:crypto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { CacheControl, ImmichFileResponse } from 'src/domain/domain.util';
 | 
			
		||||
import { JobName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IEntityJob } from 'src/domain/job/job.interface';
 | 
			
		||||
import { IAlbumRepository } from 'src/domain/repositories/album.repository';
 | 
			
		||||
@ -25,6 +24,7 @@ import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-resp
 | 
			
		||||
import { UserCore } from 'src/domain/user/user.core';
 | 
			
		||||
import { UserEntity, UserStatus } from 'src/infra/entities/user.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { CacheControl, ImmichFileResponse } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class UserService {
 | 
			
		||||
 | 
			
		||||
@ -29,16 +29,15 @@ import { AssetFileUploadResponseDto } from 'src/immich/api-v1/asset/response-dto
 | 
			
		||||
import { CheckExistingAssetsResponseDto } from 'src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto';
 | 
			
		||||
import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto';
 | 
			
		||||
import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto';
 | 
			
		||||
import FileNotEmptyValidator from 'src/immich/api-v1/validation/file-not-empty-validator';
 | 
			
		||||
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/immich/app.guard';
 | 
			
		||||
import { sendFile } from 'src/immich/app.utils';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import {
 | 
			
		||||
  FileUploadInterceptor,
 | 
			
		||||
  ImmichFile,
 | 
			
		||||
  Route,
 | 
			
		||||
  mapToUploadFile,
 | 
			
		||||
} from 'src/immich/interceptors/file-upload.interceptor';
 | 
			
		||||
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
interface UploadFiles {
 | 
			
		||||
  assetData: ImmichFile[];
 | 
			
		||||
 | 
			
		||||
@ -10,7 +10,6 @@ import { UploadFile } from 'src/domain/asset/asset.service';
 | 
			
		||||
import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { mimeTypes } from 'src/domain/domain.constant';
 | 
			
		||||
import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/domain/domain.util';
 | 
			
		||||
import { JobName } from 'src/domain/job/job.constants';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
import { IAssetRepository } from 'src/domain/repositories/asset.repository';
 | 
			
		||||
@ -37,6 +36,7 @@ import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/
 | 
			
		||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { LibraryType } from 'src/infra/entities/library.entity';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils';
 | 
			
		||||
import { QueryFailedError } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { Type } from 'class-transformer';
 | 
			
		||||
import { IsInt, IsUUID } from 'class-validator';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateDate } from 'src/domain/domain.util';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class AssetSearchDto {
 | 
			
		||||
  @ValidateBoolean({ optional: true })
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsNotEmpty, IsString } from 'class-validator';
 | 
			
		||||
import { UploadFieldName } from 'src/domain/asset/asset.service';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/domain/domain.util';
 | 
			
		||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class CreateAssetDto {
 | 
			
		||||
  @ValidateUUID({ optional: true })
 | 
			
		||||
 | 
			
		||||
@ -1,6 +1,6 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsEnum } from 'class-validator';
 | 
			
		||||
import { Optional } from 'src/domain/domain.util';
 | 
			
		||||
import { Optional } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export enum GetAssetThumbnailFormatEnum {
 | 
			
		||||
  JPEG = 'JPEG',
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { ValidateBoolean } from 'src/domain/domain.util';
 | 
			
		||||
import { ValidateBoolean } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
export class ServeFileDto {
 | 
			
		||||
  @ValidateBoolean({ optional: true })
 | 
			
		||||
 | 
			
		||||
@ -1,21 +0,0 @@
 | 
			
		||||
import { FileValidator, Injectable } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export default class FileNotEmptyValidator extends FileValidator {
 | 
			
		||||
  constructor(private requiredFields: string[]) {
 | 
			
		||||
    super({});
 | 
			
		||||
    this.requiredFields = requiredFields;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isValid(files?: any): boolean {
 | 
			
		||||
    if (!files) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.requiredFields.every((field) => files[field]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buildErrorMessage(): string {
 | 
			
		||||
    return `Field(s) ${this.requiredFields.join(', ')} should not be empty`;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,11 +0,0 @@
 | 
			
		||||
import { ArgumentMetadata, Injectable, ParseUUIDPipe } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ParseMeUUIDPipe extends ParseUUIDPipe {
 | 
			
		||||
  async transform(value: string, metadata: ArgumentMetadata) {
 | 
			
		||||
    if (value == 'me') {
 | 
			
		||||
      return value;
 | 
			
		||||
    }
 | 
			
		||||
    return super.transform(value, metadata);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -6,13 +6,13 @@ import { join } from 'node:path';
 | 
			
		||||
import { AuthService } from 'src/domain/auth/auth.service';
 | 
			
		||||
import { DatabaseService } from 'src/domain/database/database.service';
 | 
			
		||||
import { ONE_HOUR, WEB_ROOT } from 'src/domain/domain.constant';
 | 
			
		||||
import { OpenGraphTags } from 'src/domain/domain.util';
 | 
			
		||||
import { JobService } from 'src/domain/job/job.service';
 | 
			
		||||
import { ServerInfoService } from 'src/domain/server-info/server-info.service';
 | 
			
		||||
import { SharedLinkService } from 'src/domain/shared-link/shared-link.service';
 | 
			
		||||
import { StorageService } from 'src/domain/storage/storage.service';
 | 
			
		||||
import { SystemConfigService } from 'src/domain/system-config/system-config.service';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { OpenGraphTags } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
const render = (index: string, meta: OpenGraphTags) => {
 | 
			
		||||
  const tags = `
 | 
			
		||||
 | 
			
		||||
@ -15,10 +15,10 @@ import path, { isAbsolute } from 'node:path';
 | 
			
		||||
import { promisify } from 'node:util';
 | 
			
		||||
import { IMMICH_ACCESS_COOKIE, IMMICH_API_KEY_HEADER, IMMICH_API_KEY_NAME } from 'src/domain/auth/auth.constant';
 | 
			
		||||
import { serverVersion } from 'src/domain/domain.constant';
 | 
			
		||||
import { CacheControl, ImmichFileResponse, isConnectionAborted } from 'src/domain/domain.util';
 | 
			
		||||
import { ImmichReadStream } from 'src/domain/repositories/storage.repository';
 | 
			
		||||
import { Metadata } from 'src/immich/app.guard';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { CacheControl, ImmichFileResponse, isConnectionAborted } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
type SendFile = Parameters<Response['sendFile']>;
 | 
			
		||||
type SendFileOptions = SendFile[1];
 | 
			
		||||
 | 
			
		||||
@ -11,7 +11,7 @@ import {
 | 
			
		||||
import { ActivityService } from 'src/domain/activity/activity.service';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { Auth, Authenticated } from 'src/immich/app.guard';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Activity')
 | 
			
		||||
@Controller('activity')
 | 
			
		||||
 | 
			
		||||
@ -9,9 +9,8 @@ import { AlbumInfoDto } from 'src/domain/album/dto/album.dto';
 | 
			
		||||
import { GetAlbumsDto } from 'src/domain/album/dto/get-albums.dto';
 | 
			
		||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { ParseMeUUIDPipe } from 'src/immich/api-v1/validation/parse-me-uuid-pipe';
 | 
			
		||||
import { Auth, Authenticated, SharedLinkRoute } from 'src/immich/app.guard';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Album')
 | 
			
		||||
@Controller('album')
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import {
 | 
			
		||||
import { APIKeyService } from 'src/domain/api-key/api-key.service';
 | 
			
		||||
import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { Auth, Authenticated } from 'src/immich/app.guard';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('API Key')
 | 
			
		||||
@Controller('api-key')
 | 
			
		||||
 | 
			
		||||
@ -21,8 +21,8 @@ import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { MetadataSearchDto } from 'src/domain/search/dto/search.dto';
 | 
			
		||||
import { SearchService } from 'src/domain/search/search.service';
 | 
			
		||||
import { Auth, Authenticated, SharedLinkRoute } from 'src/immich/app.guard';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { Route } from 'src/immich/interceptors/file-upload.interceptor';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Asset')
 | 
			
		||||
@Controller('assets')
 | 
			
		||||
 | 
			
		||||
@ -15,7 +15,7 @@ import {
 | 
			
		||||
import { AuthService, LoginDetails } from 'src/domain/auth/auth.service';
 | 
			
		||||
import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto';
 | 
			
		||||
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/immich/app.guard';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Authentication')
 | 
			
		||||
@Controller('auth')
 | 
			
		||||
 | 
			
		||||
@ -7,7 +7,7 @@ import { DownloadInfoDto, DownloadResponseDto } from 'src/domain/download/downlo
 | 
			
		||||
import { DownloadService } from 'src/domain/download/download.service';
 | 
			
		||||
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/immich/app.guard';
 | 
			
		||||
import { asStreamableFile, sendFile } from 'src/immich/app.utils';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Download')
 | 
			
		||||
@Controller('download')
 | 
			
		||||
 | 
			
		||||
@ -1,9 +0,0 @@
 | 
			
		||||
import { ApiProperty } from '@nestjs/swagger';
 | 
			
		||||
import { IsNotEmpty, IsUUID } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
export class UUIDParamDto {
 | 
			
		||||
  @IsNotEmpty()
 | 
			
		||||
  @IsUUID('4')
 | 
			
		||||
  @ApiProperty({ format: 'uuid' })
 | 
			
		||||
  id!: string;
 | 
			
		||||
}
 | 
			
		||||
@ -4,7 +4,7 @@ import { AuthDto } from 'src/domain/auth/auth.dto';
 | 
			
		||||
import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/domain/person/person.dto';
 | 
			
		||||
import { PersonService } from 'src/domain/person/person.service';
 | 
			
		||||
import { Auth, Authenticated } from 'src/immich/app.guard';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Face')
 | 
			
		||||
@Controller('face')
 | 
			
		||||
 | 
			
		||||
@ -12,7 +12,7 @@ import {
 | 
			
		||||
} from 'src/domain/library/library.dto';
 | 
			
		||||
import { LibraryService } from 'src/domain/library/library.service';
 | 
			
		||||
import { AdminRoute, Authenticated } from 'src/immich/app.guard';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Library')
 | 
			
		||||
@Controller('library')
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { PartnerResponseDto, UpdatePartnerDto } from 'src/domain/partner/partner
 | 
			
		||||
import { PartnerService } from 'src/domain/partner/partner.service';
 | 
			
		||||
import { PartnerDirection } from 'src/domain/repositories/partner.repository';
 | 
			
		||||
import { Auth, Authenticated } from 'src/immich/app.guard';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Partner')
 | 
			
		||||
@Controller('partner')
 | 
			
		||||
 | 
			
		||||
@ -18,7 +18,7 @@ import {
 | 
			
		||||
import { PersonService } from 'src/domain/person/person.service';
 | 
			
		||||
import { Auth, Authenticated, FileResponse } from 'src/immich/app.guard';
 | 
			
		||||
import { sendFile } from 'src/immich/app.utils';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Person')
 | 
			
		||||
@Controller('person')
 | 
			
		||||
 | 
			
		||||
@ -9,7 +9,7 @@ import { SharedLinkResponseDto } from 'src/domain/shared-link/shared-link-respon
 | 
			
		||||
import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from 'src/domain/shared-link/shared-link.dto';
 | 
			
		||||
import { SharedLinkService } from 'src/domain/shared-link/shared-link.service';
 | 
			
		||||
import { Auth, Authenticated, SharedLinkRoute } from 'src/immich/app.guard';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Shared Link')
 | 
			
		||||
@Controller('shared-link')
 | 
			
		||||
 | 
			
		||||
@ -8,7 +8,7 @@ import { TagResponseDto } from 'src/domain/tag/tag-response.dto';
 | 
			
		||||
import { CreateTagDto, UpdateTagDto } from 'src/domain/tag/tag.dto';
 | 
			
		||||
import { TagService } from 'src/domain/tag/tag.service';
 | 
			
		||||
import { Auth, Authenticated } from 'src/immich/app.guard';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('Tag')
 | 
			
		||||
@Controller('tag')
 | 
			
		||||
 | 
			
		||||
@ -26,8 +26,8 @@ import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto'
 | 
			
		||||
import { UserService } from 'src/domain/user/user.service';
 | 
			
		||||
import { AdminRoute, Auth, Authenticated, FileResponse } from 'src/immich/app.guard';
 | 
			
		||||
import { sendFile } from 'src/immich/app.utils';
 | 
			
		||||
import { UUIDParamDto } from 'src/immich/controllers/dto/uuid-param.dto';
 | 
			
		||||
import { FileUploadInterceptor, Route } from 'src/immich/interceptors/file-upload.interceptor';
 | 
			
		||||
import { UUIDParamDto } from 'src/validation';
 | 
			
		||||
 | 
			
		||||
@ApiTags('User')
 | 
			
		||||
@Controller(Route.USER)
 | 
			
		||||
 | 
			
		||||
@ -7,9 +7,9 @@ import {
 | 
			
		||||
  NestInterceptor,
 | 
			
		||||
} from '@nestjs/common';
 | 
			
		||||
import { Observable, catchError, throwError } from 'rxjs';
 | 
			
		||||
import { isConnectionAborted } from 'src/domain/domain.util';
 | 
			
		||||
import { routeToErrorMessage } from 'src/immich/app.utils';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { isConnectionAborted } from 'src/utils';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ErrorInterceptor implements NestInterceptor {
 | 
			
		||||
 | 
			
		||||
@ -4,11 +4,11 @@ import { json } from 'body-parser';
 | 
			
		||||
import cookieParser from 'cookie-parser';
 | 
			
		||||
import { existsSync } from 'node:fs';
 | 
			
		||||
import sirv from 'sirv';
 | 
			
		||||
import { excludePaths } from 'src/config';
 | 
			
		||||
import { WEB_ROOT, envName, isDev, serverVersion } from 'src/domain/domain.constant';
 | 
			
		||||
import { AppModule } from 'src/immich/app.module';
 | 
			
		||||
import { AppService } from 'src/immich/app.service';
 | 
			
		||||
import { useSwagger } from 'src/immich/app.utils';
 | 
			
		||||
import { excludePaths } from 'src/infra/infra.config';
 | 
			
		||||
import { otelSDK } from 'src/infra/instrumentation';
 | 
			
		||||
import { ImmichLogger } from 'src/infra/logger';
 | 
			
		||||
import { WebSocketAdapter } from 'src/infra/websocket.adapter';
 | 
			
		||||
 | 
			
		||||
@ -5,7 +5,7 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
 | 
			
		||||
import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
 | 
			
		||||
import { TypeOrmModule } from '@nestjs/typeorm';
 | 
			
		||||
import { OpenTelemetryModule } from 'nestjs-otel';
 | 
			
		||||
import { immichAppConfig } from 'src/domain/domain.config';
 | 
			
		||||
import { bullConfig, bullQueues, immichAppConfig } from 'src/config';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
import { IActivityRepository } from 'src/domain/repositories/activity.repository';
 | 
			
		||||
import { IAlbumRepository } from 'src/domain/repositories/album.repository';
 | 
			
		||||
@ -35,7 +35,6 @@ import { IUserTokenRepository } from 'src/domain/repositories/user-token.reposit
 | 
			
		||||
import { IUserRepository } from 'src/domain/repositories/user.repository';
 | 
			
		||||
import { databaseConfig } from 'src/infra/database.config';
 | 
			
		||||
import { databaseEntities } from 'src/infra/entities';
 | 
			
		||||
import { bullConfig, bullQueues } from 'src/infra/infra.config';
 | 
			
		||||
import { otelConfig } from 'src/infra/instrumentation';
 | 
			
		||||
import { AccessRepository } from 'src/infra/repositories/access.repository';
 | 
			
		||||
import { ActivityRepository } from 'src/infra/repositories/activity.repository';
 | 
			
		||||
 | 
			
		||||
@ -1,30 +0,0 @@
 | 
			
		||||
import { SetMetadata } from '@nestjs/common';
 | 
			
		||||
 | 
			
		||||
export const GENERATE_SQL_KEY = 'generate-sql-key';
 | 
			
		||||
 | 
			
		||||
export interface GenerateSqlQueries {
 | 
			
		||||
  name?: string;
 | 
			
		||||
  params: unknown[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Decorator to enable versioning/tracking of generated Sql */
 | 
			
		||||
export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options);
 | 
			
		||||
 | 
			
		||||
const UUID = '00000000-0000-4000-a000-000000000000';
 | 
			
		||||
 | 
			
		||||
export const DummyValue = {
 | 
			
		||||
  UUID,
 | 
			
		||||
  UUID_SET: new Set([UUID]),
 | 
			
		||||
  PAGINATION: { take: 10, skip: 0 },
 | 
			
		||||
  EMAIL: 'user@immich.app',
 | 
			
		||||
  STRING: 'abcdefghi',
 | 
			
		||||
  BUFFER: Buffer.from('abcdefghi'),
 | 
			
		||||
  DATE: new Date(),
 | 
			
		||||
  TIME_BUCKET: '2024-01-01T00:00:00.000Z',
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
 | 
			
		||||
// maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching
 | 
			
		||||
// by a list of IDs) requires splitting the query into multiple chunks.
 | 
			
		||||
// We are rounding down this limit, as queries commonly include other filters and parameters.
 | 
			
		||||
export const DATABASE_PARAMETER_CHUNK_SIZE = 65_500;
 | 
			
		||||
@ -1,16 +1,7 @@
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import {
 | 
			
		||||
  Paginated,
 | 
			
		||||
  PaginatedBuilderOptions,
 | 
			
		||||
  PaginationMode,
 | 
			
		||||
  PaginationOptions,
 | 
			
		||||
  PaginationResult,
 | 
			
		||||
  chunks,
 | 
			
		||||
  setUnion,
 | 
			
		||||
} from 'src/domain/domain.util';
 | 
			
		||||
import { AssetSearchBuilderOptions } from 'src/domain/repositories/search.repository';
 | 
			
		||||
import { AssetEntity } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { DATABASE_PARAMETER_CHUNK_SIZE } from 'src/infra/infra.util';
 | 
			
		||||
import { Paginated, PaginatedBuilderOptions, PaginationMode, PaginationOptions, PaginationResult } from 'src/utils';
 | 
			
		||||
import {
 | 
			
		||||
  Between,
 | 
			
		||||
  FindManyOptions,
 | 
			
		||||
@ -37,11 +28,6 @@ export function OptionalBetween<T>(from?: T, to?: T) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => {
 | 
			
		||||
  const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options;
 | 
			
		||||
  return Number.isInteger(value) && value >= min && value <= max;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
function paginationHelper<Entity extends ObjectLiteral>(items: Entity[], take: number): PaginationResult<Entity> {
 | 
			
		||||
  const hasNextPage = items.length > take;
 | 
			
		||||
  items.splice(take);
 | 
			
		||||
@ -86,70 +72,6 @@ export async function paginatedBuilder<Entity extends ObjectLiteral>(
 | 
			
		||||
export const asVector = (embedding: number[], quote = false) =>
 | 
			
		||||
  quote ? `'[${embedding.join(',')}]'` : `[${embedding.join(',')}]`;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Wraps a method that takes a collection of parameters and sequentially calls it with chunks of the collection,
 | 
			
		||||
 * to overcome the maximum number of parameters allowed by the database driver.
 | 
			
		||||
 *
 | 
			
		||||
 * @param options.paramIndex The index of the function parameter to chunk. Defaults to 0.
 | 
			
		||||
 * @param options.flatten Whether to flatten the results. Defaults to false.
 | 
			
		||||
 */
 | 
			
		||||
export function Chunked(options: { paramIndex?: number; mergeFn?: (results: any) => any } = {}): MethodDecorator {
 | 
			
		||||
  return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
 | 
			
		||||
    const originalMethod = descriptor.value;
 | 
			
		||||
    const parameterIndex = options.paramIndex ?? 0;
 | 
			
		||||
    descriptor.value = async function (...arguments_: any[]) {
 | 
			
		||||
      const argument = arguments_[parameterIndex];
 | 
			
		||||
 | 
			
		||||
      // Early return if argument length is less than or equal to the chunk size.
 | 
			
		||||
      if (
 | 
			
		||||
        (Array.isArray(argument) && argument.length <= DATABASE_PARAMETER_CHUNK_SIZE) ||
 | 
			
		||||
        (argument instanceof Set && argument.size <= DATABASE_PARAMETER_CHUNK_SIZE)
 | 
			
		||||
      ) {
 | 
			
		||||
        return await originalMethod.apply(this, arguments_);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      return Promise.all(
 | 
			
		||||
        chunks(argument, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => {
 | 
			
		||||
          await Reflect.apply(originalMethod, this, [
 | 
			
		||||
            ...arguments_.slice(0, parameterIndex),
 | 
			
		||||
            chunk,
 | 
			
		||||
            ...arguments_.slice(parameterIndex + 1),
 | 
			
		||||
          ]);
 | 
			
		||||
        }),
 | 
			
		||||
      ).then((results) => (options.mergeFn ? options.mergeFn(results) : results));
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ChunkedArray(options?: { paramIndex?: number }): MethodDecorator {
 | 
			
		||||
  return Chunked({ ...options, mergeFn: _.flatten });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function ChunkedSet(options?: { paramIndex?: number }): MethodDecorator {
 | 
			
		||||
  return Chunked({ ...options, mergeFn: setUnion });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// https://stackoverflow.com/a/74898678
 | 
			
		||||
export function DecorateAll(
 | 
			
		||||
  decorator: <T>(
 | 
			
		||||
    target: any,
 | 
			
		||||
    propertyKey: string,
 | 
			
		||||
    descriptor: TypedPropertyDescriptor<T>,
 | 
			
		||||
  ) => TypedPropertyDescriptor<T> | void,
 | 
			
		||||
) {
 | 
			
		||||
  return (target: any) => {
 | 
			
		||||
    const descriptors = Object.getOwnPropertyDescriptors(target.prototype);
 | 
			
		||||
    for (const [propName, descriptor] of Object.entries(descriptors)) {
 | 
			
		||||
      const isMethod = typeof descriptor.value == 'function' && propName !== 'constructor';
 | 
			
		||||
      if (!isMethod) {
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      decorator({ ...target, constructor: { ...target.constructor, name: target.name } as any }, propName, descriptor);
 | 
			
		||||
      Object.defineProperty(target.prototype, propName, descriptor);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function searchAssetBuilder(
 | 
			
		||||
  builder: SelectQueryBuilder<AssetEntity>,
 | 
			
		||||
  options: AssetSearchBuilderOptions,
 | 
			
		||||
 | 
			
		||||
@ -13,9 +13,9 @@ import { snakeCase, startCase } from 'lodash';
 | 
			
		||||
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
 | 
			
		||||
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
 | 
			
		||||
import { performance } from 'node:perf_hooks';
 | 
			
		||||
import { excludePaths } from 'src/config';
 | 
			
		||||
import { DecorateAll } from 'src/decorators';
 | 
			
		||||
import { serverVersion } from 'src/domain/domain.constant';
 | 
			
		||||
import { excludePaths } from 'src/infra/infra.config';
 | 
			
		||||
import { DecorateAll } from 'src/infra/infra.utils';
 | 
			
		||||
 | 
			
		||||
let metricsEnabled = process.env.IMMICH_METRICS === 'true';
 | 
			
		||||
const hostMetrics =
 | 
			
		||||
 | 
			
		||||
@ -1,4 +1,5 @@
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators';
 | 
			
		||||
import { IAccessRepository } from 'src/domain/repositories/access.repository';
 | 
			
		||||
import { ActivityEntity } from 'src/infra/entities/activity.entity';
 | 
			
		||||
import { AlbumEntity } from 'src/infra/entities/album.entity';
 | 
			
		||||
@ -9,8 +10,6 @@ import { PartnerEntity } from 'src/infra/entities/partner.entity';
 | 
			
		||||
import { PersonEntity } from 'src/infra/entities/person.entity';
 | 
			
		||||
import { SharedLinkEntity } from 'src/infra/entities/shared-link.entity';
 | 
			
		||||
import { UserTokenEntity } from 'src/infra/entities/user-token.entity';
 | 
			
		||||
import { DummyValue, GenerateSql } from 'src/infra/infra.util';
 | 
			
		||||
import { ChunkedSet } from 'src/infra/infra.utils';
 | 
			
		||||
import { Instrumentation } from 'src/infra/instrumentation';
 | 
			
		||||
import { Brackets, In, Repository } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { DummyValue, GenerateSql } from 'src/decorators';
 | 
			
		||||
import { IActivityRepository } from 'src/domain/repositories/activity.repository';
 | 
			
		||||
import { ActivityEntity } from 'src/infra/entities/activity.entity';
 | 
			
		||||
import { DummyValue, GenerateSql } from 'src/infra/infra.util';
 | 
			
		||||
import { Instrumentation } from 'src/infra/instrumentation';
 | 
			
		||||
import { IsNull, Repository } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import _ from 'lodash';
 | 
			
		||||
import { setUnion } from 'src/domain/domain.util';
 | 
			
		||||
import { Chunked, ChunkedArray, DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from 'src/decorators';
 | 
			
		||||
import {
 | 
			
		||||
  AlbumAsset,
 | 
			
		||||
  AlbumAssetCount,
 | 
			
		||||
@ -12,9 +12,8 @@ import {
 | 
			
		||||
import { dataSource } from 'src/infra/database.config';
 | 
			
		||||
import { AlbumEntity } from 'src/infra/entities/album.entity';
 | 
			
		||||
import { AssetEntity } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from 'src/infra/infra.util';
 | 
			
		||||
import { Chunked, ChunkedArray } from 'src/infra/infra.utils';
 | 
			
		||||
import { Instrumentation } from 'src/infra/instrumentation';
 | 
			
		||||
import { setUnion } from 'src/utils';
 | 
			
		||||
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
@Instrumentation()
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { DummyValue, GenerateSql } from 'src/decorators';
 | 
			
		||||
import { IKeyRepository } from 'src/domain/repositories/api-key.repository';
 | 
			
		||||
import { APIKeyEntity } from 'src/infra/entities/api-key.entity';
 | 
			
		||||
import { DummyValue, GenerateSql } from 'src/infra/infra.util';
 | 
			
		||||
import { Instrumentation } from 'src/infra/instrumentation';
 | 
			
		||||
import { Repository } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { DateTime } from 'luxon';
 | 
			
		||||
import path from 'node:path';
 | 
			
		||||
import { Paginated, PaginationMode, PaginationOptions } from 'src/domain/domain.util';
 | 
			
		||||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
 | 
			
		||||
import {
 | 
			
		||||
  AssetBuilderOptions,
 | 
			
		||||
  AssetCreate,
 | 
			
		||||
@ -30,16 +30,9 @@ import { AssetJobStatusEntity } from 'src/infra/entities/asset-job-status.entity
 | 
			
		||||
import { AssetEntity, AssetType } from 'src/infra/entities/asset.entity';
 | 
			
		||||
import { ExifEntity } from 'src/infra/entities/exif.entity';
 | 
			
		||||
import { SmartInfoEntity } from 'src/infra/entities/smart-info.entity';
 | 
			
		||||
import { DummyValue, GenerateSql } from 'src/infra/infra.util';
 | 
			
		||||
import {
 | 
			
		||||
  Chunked,
 | 
			
		||||
  ChunkedArray,
 | 
			
		||||
  OptionalBetween,
 | 
			
		||||
  paginate,
 | 
			
		||||
  paginatedBuilder,
 | 
			
		||||
  searchAssetBuilder,
 | 
			
		||||
} from 'src/infra/infra.utils';
 | 
			
		||||
import { OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from 'src/infra/infra.utils';
 | 
			
		||||
import { Instrumentation } from 'src/infra/instrumentation';
 | 
			
		||||
import { Paginated, PaginationMode, PaginationOptions } from 'src/utils';
 | 
			
		||||
import {
 | 
			
		||||
  Brackets,
 | 
			
		||||
  FindOptionsRelations,
 | 
			
		||||
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user