mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
refactor!: migrate class-validator to zod (#26597)
This commit is contained in:
@@ -13,6 +13,7 @@ import { SessionFactory } from 'test/factories/session.factory';
|
||||
import { UserFactory } from 'test/factories/user.factory';
|
||||
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
|
||||
import { systemConfigStub } from 'test/fixtures/system-config.stub';
|
||||
import { userStub } from 'test/fixtures/user.stub';
|
||||
import { newUuid } from 'test/small.factory';
|
||||
import { newTestService, ServiceMocks } from 'test/utils';
|
||||
|
||||
@@ -209,11 +210,13 @@ describe(AuthService.name, () => {
|
||||
it('should sign up the admin', async () => {
|
||||
mocks.user.getAdmin.mockResolvedValue(void 0);
|
||||
mocks.user.create.mockResolvedValue({
|
||||
...userStub.admin,
|
||||
...dto,
|
||||
id: 'admin',
|
||||
name: 'immich admin',
|
||||
createdAt: new Date('2021-01-01'),
|
||||
metadata: [] as UserMetadataItem[],
|
||||
} as unknown as UserAdmin);
|
||||
} as UserAdmin);
|
||||
|
||||
await expect(sut.adminSignUp(dto)).resolves.toMatchObject({
|
||||
avatarColor: expect.any(String),
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { isString } from 'class-validator';
|
||||
import { parse } from 'cookie';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IncomingHttpHeaders } from 'node:http';
|
||||
@@ -312,7 +311,7 @@ export class AuthService extends BaseService {
|
||||
const storageLabel = this.getClaim(profile, {
|
||||
key: storageLabelClaim,
|
||||
default: '',
|
||||
isValid: isString,
|
||||
isValid: (value: unknown): value is string => typeof value === 'string',
|
||||
});
|
||||
const storageQuota = this.getClaim(profile, {
|
||||
key: storageQuotaClaim,
|
||||
@@ -322,7 +321,7 @@ export class AuthService extends BaseService {
|
||||
const role = this.getClaim<'admin' | 'user'>(profile, {
|
||||
key: roleClaim,
|
||||
default: 'user',
|
||||
isValid: (value: unknown) => isString(value) && ['admin', 'user'].includes(value),
|
||||
isValid: (value: unknown) => typeof value === 'string' && ['admin', 'user'].includes(value),
|
||||
});
|
||||
|
||||
user = await this.createUser({
|
||||
|
||||
@@ -283,6 +283,7 @@ export class LibraryService extends BaseService {
|
||||
private async validateImportPath(importPath: string): Promise<ValidateLibraryImportPathResponseDto> {
|
||||
const validation = new ValidateLibraryImportPathResponseDto();
|
||||
validation.importPath = importPath;
|
||||
validation.isValid = false;
|
||||
|
||||
if (StorageCore.isImmichPath(importPath)) {
|
||||
validation.message = 'Cannot use media upload folder for external libraries';
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { defaults, SystemConfig } from 'src/config';
|
||||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||
import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum';
|
||||
@@ -102,7 +101,7 @@ describe(NotificationService.name, () => {
|
||||
|
||||
it('skips smtp validation with DTO when there are no changes', async () => {
|
||||
const oldConfig = { ...configs.smtpEnabled };
|
||||
const newConfig = plainToInstance(SystemConfigDto, configs.smtpEnabled);
|
||||
const newConfig = configs.smtpEnabled as SystemConfigDto;
|
||||
|
||||
await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow();
|
||||
expect(mocks.email.verifySmtp).not.toHaveBeenCalled();
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Plugin as ExtismPlugin, newPlugin } from '@extism/extism';
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateOrReject } from 'class-validator';
|
||||
import { join } from 'node:path';
|
||||
import { Asset, WorkflowAction, WorkflowFilter } from 'src/database';
|
||||
import { OnEvent, OnJob } from 'src/decorators';
|
||||
import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto';
|
||||
import { PluginManifestDto, PluginManifestSchema } from 'src/dtos/plugin-manifest.dto';
|
||||
import { mapPlugin, PluginResponseDto, PluginTriggerResponseDto } from 'src/dtos/plugin.dto';
|
||||
import { JobName, JobStatus, PluginTriggerType, QueueName } from 'src/enum';
|
||||
import { pluginTriggers } from 'src/plugins';
|
||||
@@ -138,14 +136,7 @@ export class PluginService extends BaseService {
|
||||
private async readAndValidateManifest(manifestPath: string): Promise<PluginManifestDto> {
|
||||
const content = await this.storageRepository.readTextFile(manifestPath);
|
||||
const manifestData = JSON.parse(content);
|
||||
const manifest = plainToInstance(PluginManifestDto, manifestData);
|
||||
|
||||
await validateOrReject(manifest, {
|
||||
whitelist: true,
|
||||
forbidNonWhitelisted: true,
|
||||
});
|
||||
|
||||
return manifest;
|
||||
return PluginManifestSchema.parse(manifestData);
|
||||
}
|
||||
|
||||
///////////////////////////////////////////
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ClassConstructor } from 'class-transformer';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
@@ -39,7 +38,7 @@ const asNightlyTasksCron = (config: SystemConfig) => {
|
||||
|
||||
@Injectable()
|
||||
export class QueueService extends BaseService {
|
||||
private services: ClassConstructor<unknown>[] = [];
|
||||
private services: (new (...args: any[]) => unknown)[] = [];
|
||||
private nightlyJobsLock = false;
|
||||
|
||||
@OnEvent({ name: 'ConfigInit' })
|
||||
@@ -96,7 +95,7 @@ export class QueueService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
setServices(services: ClassConstructor<unknown>[]) {
|
||||
setServices(services: (new (...args: any[]) => unknown)[]) {
|
||||
this.services = services;
|
||||
}
|
||||
|
||||
|
||||
@@ -138,6 +138,12 @@ export class ServerService extends BaseService {
|
||||
async getStatistics(): Promise<ServerStatsResponseDto> {
|
||||
const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats();
|
||||
const serverStats = new ServerStatsResponseDto();
|
||||
serverStats.photos ??= 0;
|
||||
serverStats.videos ??= 0;
|
||||
serverStats.usage ??= 0;
|
||||
serverStats.usagePhotos ??= 0;
|
||||
serverStats.usageVideos ??= 0;
|
||||
serverStats.usageByUser ??= [];
|
||||
|
||||
for (const user of userStats) {
|
||||
const usage = new UsageByUserDto();
|
||||
|
||||
@@ -311,9 +311,7 @@ describe(SystemConfigService.name, () => {
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } }));
|
||||
|
||||
await expect(sut.getSystemConfig()).rejects.toThrow(
|
||||
'library.scan.cronExpression has failed the following constraints: cronValidator',
|
||||
);
|
||||
await expect(sut.getSystemConfig()).rejects.toThrow('[library.scan.cronExpression] Invalid cron expression');
|
||||
});
|
||||
|
||||
it('should log errors with the config file', async () => {
|
||||
@@ -402,10 +400,26 @@ describe(SystemConfigService.name, () => {
|
||||
});
|
||||
|
||||
const tests = [
|
||||
{ should: 'validate numbers', config: { ffmpeg: { crf: 'not-a-number' } } },
|
||||
{ should: 'validate booleans', config: { oauth: { enabled: 'invalid' } } },
|
||||
{ should: 'validate enums', config: { ffmpeg: { transcode: 'unknown' } } },
|
||||
{ should: 'validate required oauth fields', config: { oauth: { enabled: true } } },
|
||||
{
|
||||
should: 'validate numbers',
|
||||
config: { ffmpeg: { crf: 'not-a-number' } },
|
||||
throws: '[ffmpeg.crf] Invalid input: expected number, received NaN',
|
||||
},
|
||||
{
|
||||
should: 'validate booleans',
|
||||
config: { oauth: { enabled: 'invalid' } },
|
||||
throws: '[oauth.enabled] Invalid input: expected boolean, received string',
|
||||
},
|
||||
{
|
||||
should: 'validate enums',
|
||||
config: { ffmpeg: { transcode: 'unknown' } },
|
||||
throws: '[ffmpeg.transcode] Invalid option: expected one of',
|
||||
},
|
||||
{
|
||||
should: 'validate required oauth fields',
|
||||
config: { oauth: { enabled: true } },
|
||||
check: (c: SystemConfig) => expect(c.oauth.enabled).toBe(true),
|
||||
},
|
||||
{ should: 'warn for top level unknown options', warn: true, config: { unknownOption: true } },
|
||||
{ should: 'warn for nested unknown options', warn: true, config: { ffmpeg: { unknownOption: true } } },
|
||||
];
|
||||
@@ -415,11 +429,14 @@ describe(SystemConfigService.name, () => {
|
||||
mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' }));
|
||||
mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(test.config));
|
||||
|
||||
if (test.warn) {
|
||||
if (test.throws) {
|
||||
await expect(sut.getSystemConfig()).rejects.toThrow(test.throws);
|
||||
} else if (test.warn) {
|
||||
await sut.getSystemConfig();
|
||||
expect(mocks.logger.warn).toHaveBeenCalled();
|
||||
} else {
|
||||
await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error);
|
||||
const config = await sut.getSystemConfig();
|
||||
test.check!(config);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { instanceToPlain } from 'class-transformer';
|
||||
import _ from 'lodash';
|
||||
import { defaults } from 'src/config';
|
||||
import { OnEvent } from 'src/decorators';
|
||||
@@ -61,7 +60,7 @@ export class SystemConfigService extends BaseService {
|
||||
@OnEvent({ name: 'ConfigValidate' })
|
||||
onConfigValidate({ newConfig, oldConfig }: ArgOf<'ConfigValidate'>) {
|
||||
const { logLevel } = this.configRepository.getEnv();
|
||||
if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && logLevel) {
|
||||
if (!_.isEqual(toPlainObject(newConfig.logging), oldConfig.logging) && logLevel) {
|
||||
throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user