1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-25 10:43:13 +02:00

refactor: api validators (boolean and date) (#7709)

* refactor: api validators (boolean and date)

* chore: open api

* revert: time bucket change
This commit is contained in:
Jason Rasmussen 2024-03-07 22:59:02 -05:00 committed by GitHub
parent 753842745d
commit a50f125dd1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 243 additions and 355 deletions

View File

@ -66,8 +66,8 @@ class Asset {
assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)), assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)),
deviceAssetId: this.deviceAssetId, deviceAssetId: this.deviceAssetId,
deviceId: 'CLI', deviceId: 'CLI',
fileCreatedAt: this.fileCreatedAt, fileCreatedAt: this.fileCreatedAt.toISOString(),
fileModifiedAt: this.fileModifiedAt, fileModifiedAt: this.fileModifiedAt.toISOString(),
isFavorite: String(false), isFavorite: String(false),
}; };
const formData = new FormData(); const formData = new FormData();

View File

@ -6,10 +6,9 @@ import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
const invalidBirthday = [ const invalidBirthday = [
// TODO: enable after replacing `@Type(() => Date)` { birthDate: 'false', response: 'birthDate must be a date string' },
// { birthDate: 'false', response: 'Invalid date' }, { birthDate: '123567', response: 'birthDate must be a date string' },
// { birthDate: '123567', response: 'Invalid date }, { birthDate: 123_567, response: 'birthDate must be a date string' },
// { birthDate: 123_567, response: ['Birth date cannot be in the future'] },
{ birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] }, { birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] },
]; ];
@ -152,16 +151,16 @@ describe('/person', () => {
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should not accept invalid birth dates', async () => {
for (const { birthDate, response } of invalidBirthday) { for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/person`) .post(`/person`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate }); .send({ birthDate });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(response)); expect(body).toEqual(errorDto.badRequest(response));
}
}); });
}
it('should create a person', async () => { it('should create a person', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
@ -202,16 +201,16 @@ describe('/person', () => {
}); });
} }
it('should not accept invalid birth dates', async () => {
for (const { birthDate, response } of invalidBirthday) { for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`) .put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate }); .send({ birthDate });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(response)); expect(body).toEqual(errorDto.badRequest(response));
}
}); });
}
it('should update a date of birth', async () => { it('should update a date of birth', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -4012,7 +4012,6 @@
"required": false, "required": false,
"in": "query", "in": "query",
"schema": { "schema": {
"default": false,
"type": "boolean" "type": "boolean"
} }
} }
@ -8937,7 +8936,6 @@
"ScanLibraryDto": { "ScanLibraryDto": {
"properties": { "properties": {
"refreshAllFiles": { "refreshAllFiles": {
"default": false,
"type": "boolean" "type": "boolean"
}, },
"refreshModifiedFiles": { "refreshModifiedFiles": {
@ -9346,7 +9344,6 @@
"type": "boolean" "type": "boolean"
}, },
"allowUpload": { "allowUpload": {
"default": false,
"type": "boolean" "type": "boolean"
}, },
"assetIds": { "assetIds": {

View File

@ -1,5 +1,5 @@
import { IsBoolean, IsString } from 'class-validator'; import { IsString } from 'class-validator';
import { Optional, ValidateUUID } from '../../domain.util'; import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util';
export class UpdateAlbumDto { export class UpdateAlbumDto {
@Optional() @Optional()
@ -13,7 +13,6 @@ export class UpdateAlbumDto {
@ValidateUUID({ optional: true }) @ValidateUUID({ optional: true })
albumThumbnailAssetId?: string; albumThumbnailAssetId?: string;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
isActivityEnabled?: boolean; isActivityEnabled?: boolean;
} }

View File

@ -1,10 +1,6 @@
import { Transform } from 'class-transformer'; import { ValidateBoolean } from '../../domain.util';
import { IsBoolean } from 'class-validator';
import { Optional, toBoolean } from '../../domain.util';
export class AlbumInfoDto { export class AlbumInfoDto {
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
withoutAssets?: boolean; withoutAssets?: boolean;
} }

View File

@ -1,13 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ValidateBoolean, ValidateUUID } from '../../domain.util';
import { Transform } from 'class-transformer';
import { IsBoolean } from 'class-validator';
import { Optional, toBoolean, ValidateUUID } from '../../domain.util';
export class GetAlbumsDto { export class GetAlbumsDto {
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
@ApiProperty()
/** /**
* true: only shared albums * true: only shared albums
* false: only non-shared own albums * false: only non-shared own albums

View File

@ -1,24 +1,16 @@
import { AssetType } from '@app/infra/entities'; import { AssetType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { ValidateBoolean } from '../../domain.util';
import { IsBoolean } from 'class-validator';
import { Optional, toBoolean } from '../../domain.util';
import { AssetStats } from '../../repositories'; import { AssetStats } from '../../repositories';
export class AssetStatsDto { export class AssetStatsDto {
@IsBoolean() @ValidateBoolean({ optional: true })
@Transform(toBoolean)
@Optional()
isArchived?: boolean; isArchived?: boolean;
@IsBoolean() @ValidateBoolean({ optional: true })
@Transform(toBoolean)
@Optional()
isFavorite?: boolean; isFavorite?: boolean;
@IsBoolean() @ValidateBoolean({ optional: true })
@Transform(toBoolean)
@Optional()
isTrashed?: boolean; isTrashed?: boolean;
} }

View File

@ -1,6 +1,5 @@
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
IsBoolean,
IsDateString, IsDateString,
IsInt, IsInt,
IsLatitude, IsLatitude,
@ -10,7 +9,7 @@ import {
IsString, IsString,
ValidateIf, ValidateIf,
} from 'class-validator'; } from 'class-validator';
import { Optional, ValidateUUID } from '../../domain.util'; import { Optional, ValidateBoolean, ValidateUUID } from '../../domain.util';
import { BulkIdsDto } from '../response-dto'; import { BulkIdsDto } from '../response-dto';
export class DeviceIdDto { export class DeviceIdDto {
@ -28,23 +27,13 @@ const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
o.latitude !== undefined || o.longitude !== undefined; o.latitude !== undefined || o.longitude !== undefined;
const ValidateGPS = () => ValidateIf(hasGPS); const ValidateGPS = () => ValidateIf(hasGPS);
export class AssetBulkUpdateDto extends BulkIdsDto { export class UpdateAssetBase {
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
isFavorite?: boolean; isFavorite?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
isArchived?: boolean; isArchived?: boolean;
@Optional()
@ValidateUUID()
stackParentId?: string;
@Optional()
@IsBoolean()
removeParent?: boolean;
@Optional() @Optional()
@IsDateString() @IsDateString()
dateTimeOriginal?: string; dateTimeOriginal?: string;
@ -60,32 +49,21 @@ export class AssetBulkUpdateDto extends BulkIdsDto {
longitude?: number; longitude?: number;
} }
export class UpdateAssetDto { export class AssetBulkUpdateDto extends UpdateAssetBase {
@Optional() @ValidateUUID({ each: true })
@IsBoolean() ids!: string[];
isFavorite?: boolean;
@Optional() @ValidateUUID({ optional: true })
@IsBoolean() stackParentId?: string;
isArchived?: boolean;
@ValidateBoolean({ optional: true })
removeParent?: boolean;
}
export class UpdateAssetDto extends UpdateAssetBase {
@Optional() @Optional()
@IsString() @IsString()
description?: string; description?: string;
@Optional()
@IsDateString()
dateTimeOriginal?: string;
@ValidateGPS()
@IsLatitude()
@IsNotEmpty()
latitude?: number;
@ValidateGPS()
@IsLongitude()
@IsNotEmpty()
longitude?: number;
} }
export class RandomAssetsDto { export class RandomAssetsDto {
@ -97,7 +75,6 @@ export class RandomAssetsDto {
} }
export class AssetBulkDeleteDto extends BulkIdsDto { export class AssetBulkDeleteDto extends BulkIdsDto {
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
force?: boolean; force?: boolean;
} }

View File

@ -1,34 +1,18 @@
import { ApiProperty } from '@nestjs/swagger'; import { ValidateBoolean, ValidateDate } from '../../domain.util';
import { Transform, Type } from 'class-transformer';
import { IsBoolean, IsDate } from 'class-validator';
import { Optional, toBoolean } from '../../domain.util';
export class MapMarkerDto { export class MapMarkerDto {
@ApiProperty() @ValidateBoolean({ optional: true })
@Optional()
@IsBoolean()
@Transform(toBoolean)
isArchived?: boolean; isArchived?: boolean;
@ApiProperty() @ValidateBoolean({ optional: true })
@Optional()
@IsBoolean()
@Transform(toBoolean)
isFavorite?: boolean; isFavorite?: boolean;
@Optional() @ValidateDate({ optional: true })
@IsDate()
@Type(() => Date)
fileCreatedAfter?: Date; fileCreatedAfter?: Date;
@Optional() @ValidateDate({ optional: true })
@IsDate()
@Type(() => Date)
fileCreatedBefore?: Date; fileCreatedBefore?: Date;
@ApiProperty() @ValidateBoolean({ optional: true })
@Optional()
@IsBoolean()
@Transform(toBoolean)
withPartners?: boolean; withPartners?: boolean;
} }

View File

@ -1,7 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { ValidateBoolean, ValidateUUID } from '../../domain.util';
import { Optional, ValidateUUID, toBoolean } from '../../domain.util';
import { TimeBucketSize } from '../../repositories'; import { TimeBucketSize } from '../../repositories';
export class TimeBucketDto { export class TimeBucketDto {
@ -19,34 +18,23 @@ export class TimeBucketDto {
@ValidateUUID({ optional: true }) @ValidateUUID({ optional: true })
personId?: string; personId?: string;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
isArchived?: boolean; isArchived?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
isFavorite?: boolean; isFavorite?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
isTrashed?: boolean; isTrashed?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
withStacked?: boolean; withStacked?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
withPartners?: boolean; withPartners?: boolean;
} }
export class TimeBucketAssetDto extends TimeBucketDto { export class TimeBucketAssetDto extends TimeBucketDto {
@IsString() @IsString()
@IsNotEmpty()
timeBucket!: string; timeBucket!: string;
} }

View File

@ -1,14 +1,13 @@
import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from '@app/infra/entities'; import { AssetPathType, EntityType, PathType, PersonPathType, UserPathType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsArray, IsDate, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator'; import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator';
import { Optional, ValidateUUID } from '../domain.util'; import { Optional, ValidateDate, ValidateUUID } from '../domain.util';
const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType }); const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType });
export class AuditDeletesDto { export class AuditDeletesDto {
@IsDate() @ValidateDate()
@Type(() => Date)
after!: Date; after!: Date;
@ApiProperty({ enum: EntityType, enumName: 'EntityType' }) @ApiProperty({ enum: EntityType, enumName: 'EntityType' })

View File

@ -1,7 +1,7 @@
import { ImmichLogger } from '@app/infra/logger'; import { ImmichLogger } from '@app/infra/logger';
import { applyDecorators } from '@nestjs/common'; import { BadRequestException, applyDecorators } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; import { Transform } from 'class-transformer';
import { import {
IsArray, IsArray,
IsBoolean, IsBoolean,
@ -12,6 +12,7 @@ import {
IsUUID, IsUUID,
ValidateIf, ValidateIf,
ValidationOptions, ValidationOptions,
isDateString,
} from 'class-validator'; } from 'class-validator';
import { CronJob } from 'cron'; import { CronJob } from 'cron';
import _ from 'lodash'; import _ from 'lodash';
@ -40,14 +41,10 @@ export interface OpenGraphTags {
imageUrl?: string; imageUrl?: string;
} }
export type Options = {
optional?: boolean;
each?: boolean;
};
export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED'; export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED';
export function ValidateUUID(options?: Options) { type UUIDOptions = { optional?: boolean; each?: boolean };
export const ValidateUUID = (options?: UUIDOptions) => {
const { optional, each } = { optional: false, each: false, ...options }; const { optional, each } = { optional: false, each: false, ...options };
return applyDecorators( return applyDecorators(
IsUUID('4', { each }), IsUUID('4', { each }),
@ -55,8 +52,59 @@ export function ValidateUUID(options?: Options) {
optional ? Optional() : IsNotEmpty(), optional ? Optional() : IsNotEmpty(),
each ? IsArray() : IsString(), 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) { export function validateCronExpression(expression: string) {
try { try {
new CronJob(expression, () => {}); new CronJob(expression, () => {});
@ -67,34 +115,7 @@ export function validateCronExpression(expression: string) {
return true; return true;
} }
interface IValue { type IValue = { value: string };
value?: string;
}
export const QueryBoolean = ({ optional }: { optional?: boolean }) => {
const decorators = [IsBoolean(), Transform(toBoolean)];
if (optional) {
decorators.push(Optional());
}
return applyDecorators(...decorators);
};
export const QueryDate = ({ optional }: { optional?: boolean }) => {
const decorators = [IsDate(), Type(() => Date)];
if (optional) {
decorators.push(Optional());
}
return applyDecorators(...decorators);
};
export const toBoolean = ({ value }: IValue) => {
if (value == 'true') {
return true;
} else if (value == 'false') {
return false;
}
return value;
};
export const toEmail = ({ value }: IValue) => value?.toLowerCase(); export const toEmail = ({ value }: IValue) => value?.toLowerCase();

View File

@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsEnum, IsNotEmpty } from 'class-validator'; import { IsEnum, IsNotEmpty } from 'class-validator';
import { Optional } from '../domain.util'; import { ValidateBoolean } from '../domain.util';
import { JobCommand, QueueName } from './job.constants'; import { JobCommand, QueueName } from './job.constants';
export class JobIdParamDto { export class JobIdParamDto {
@ -16,8 +16,7 @@ export class JobCommandDto {
@ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' }) @ApiProperty({ type: 'string', enum: JobCommand, enumName: 'JobCommand' })
command!: JobCommand; command!: JobCommand;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
force!: boolean; force!: boolean;
} }

View File

@ -1,7 +1,7 @@
import { LibraryEntity, LibraryType } from '@app/infra/entities'; import { LibraryEntity, LibraryType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { ArrayMaxSize, ArrayUnique, IsBoolean, IsEnum, IsNotEmpty, IsString } from 'class-validator'; import { ArrayMaxSize, ArrayUnique, IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { Optional, ValidateUUID } from '../domain.util'; import { Optional, ValidateBoolean, ValidateUUID } from '../domain.util';
export class CreateLibraryDto { export class CreateLibraryDto {
@IsEnum(LibraryType) @IsEnum(LibraryType)
@ -16,8 +16,7 @@ export class CreateLibraryDto {
@IsNotEmpty() @IsNotEmpty()
name?: string; name?: string;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
isVisible?: boolean; isVisible?: boolean;
@Optional() @Optional()
@ -34,8 +33,7 @@ export class CreateLibraryDto {
@ArrayMaxSize(128) @ArrayMaxSize(128)
exclusionPatterns?: string[]; exclusionPatterns?: string[];
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
isWatched?: boolean; isWatched?: boolean;
} }
@ -45,8 +43,7 @@ export class UpdateLibraryDto {
@IsNotEmpty() @IsNotEmpty()
name?: string; name?: string;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
isVisible?: boolean; isVisible?: boolean;
@Optional() @Optional()
@ -102,13 +99,11 @@ export class LibrarySearchDto {
} }
export class ScanLibraryDto { export class ScanLibraryDto {
@IsBoolean() @ValidateBoolean({ optional: true })
@Optional()
refreshModifiedFiles?: boolean; refreshModifiedFiles?: boolean;
@IsBoolean() @ValidateBoolean({ optional: true })
@Optional() refreshAllFiles?: boolean;
refreshAllFiles?: boolean = false;
} }
export class SearchLibraryDto { export class SearchLibraryDto {

View File

@ -1,9 +1,9 @@
import { AssetFaceEntity, PersonEntity } from '@app/infra/entities'; import { AssetFaceEntity, PersonEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsArray, IsBoolean, IsDate, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator'; import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator';
import { AuthDto } from '../auth'; import { AuthDto } from '../auth';
import { Optional, ValidateUUID, toBoolean } from '../domain.util'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from '../domain.util';
export class PersonCreateDto { export class PersonCreateDto {
/** /**
@ -17,18 +17,14 @@ export class PersonCreateDto {
* Person date of birth. * Person date of birth.
* Note: the mobile app cannot currently set the birth date to null. * Note: the mobile app cannot currently set the birth date to null.
*/ */
@Optional({ nullable: true })
@IsDate()
@Type(() => Date)
@MaxDate(() => new Date(), { message: 'Birth date cannot be in the future' }) @MaxDate(() => new Date(), { message: 'Birth date cannot be in the future' })
@ApiProperty({ format: 'date' }) @ValidateDate({ optional: true, nullable: true, format: 'date' })
birthDate?: Date | null; birthDate?: Date | null;
/** /**
* Person visibility * Person visibility
*/ */
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
isHidden?: boolean; isHidden?: boolean;
} }
@ -63,9 +59,8 @@ export class MergePersonDto {
} }
export class PersonSearchDto { export class PersonSearchDto {
@IsBoolean() @ValidateBoolean({ optional: true })
@Transform(toBoolean) withHidden?: boolean;
withHidden?: boolean = false;
} }
export class PersonResponseDto { export class PersonResponseDto {

View File

@ -1,9 +1,9 @@
import { AssetOrder } from '@app/domain/asset/dto/asset.dto'; import { AssetOrder } from '@app/domain/asset/dto/asset.dto';
import { AssetType, GeodataPlacesEntity } from '@app/infra/entities'; import { AssetType, GeodataPlacesEntity } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsBoolean, IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
import { Optional, QueryBoolean, QueryDate, ValidateUUID, toBoolean } from '../../domain.util'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from '../../domain.util';
class BaseSearchDto { class BaseSearchDto {
@ValidateUUID({ optional: true }) @ValidateUUID({ optional: true })
@ -19,62 +19,62 @@ class BaseSearchDto {
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType }) @ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
type?: AssetType; type?: AssetType;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isArchived?: boolean; isArchived?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
@ApiProperty({ default: false }) @ApiProperty({ default: false })
withArchived?: boolean; withArchived?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isEncoded?: boolean; isEncoded?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isExternal?: boolean; isExternal?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isFavorite?: boolean; isFavorite?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isMotion?: boolean; isMotion?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isOffline?: boolean; isOffline?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isReadOnly?: boolean; isReadOnly?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isVisible?: boolean; isVisible?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
withDeleted?: boolean; withDeleted?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
withExif?: boolean; withExif?: boolean;
@QueryDate({ optional: true }) @ValidateDate({ optional: true })
createdBefore?: Date; createdBefore?: Date;
@QueryDate({ optional: true }) @ValidateDate({ optional: true })
createdAfter?: Date; createdAfter?: Date;
@QueryDate({ optional: true }) @ValidateDate({ optional: true })
updatedBefore?: Date; updatedBefore?: Date;
@QueryDate({ optional: true }) @ValidateDate({ optional: true })
updatedAfter?: Date; updatedAfter?: Date;
@QueryDate({ optional: true }) @ValidateDate({ optional: true })
trashedBefore?: Date; trashedBefore?: Date;
@QueryDate({ optional: true }) @ValidateDate({ optional: true })
trashedAfter?: Date; trashedAfter?: Date;
@QueryDate({ optional: true }) @ValidateDate({ optional: true })
takenBefore?: Date; takenBefore?: Date;
@QueryDate({ optional: true }) @ValidateDate({ optional: true })
takenAfter?: Date; takenAfter?: Date;
@IsString() @IsString()
@ -120,7 +120,7 @@ class BaseSearchDto {
@Optional() @Optional()
size?: number; size?: number;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isNotInAlbum?: boolean; isNotInAlbum?: boolean;
@Optional() @Optional()
@ -141,10 +141,10 @@ export class MetadataSearchDto extends BaseSearchDto {
@Optional() @Optional()
checksum?: string; checksum?: string;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
withStacked?: boolean; withStacked?: boolean;
@QueryBoolean({ optional: true }) @ValidateBoolean({ optional: true })
withPeople?: boolean; withPeople?: boolean;
@IsString() @IsString()
@ -197,34 +197,24 @@ export class SearchDto {
@Optional() @Optional()
query?: string; query?: string;
@IsBoolean() @ValidateBoolean({ optional: true })
@Optional()
@Transform(toBoolean)
smart?: boolean; smart?: boolean;
/** @deprecated */ /** @deprecated */
@IsBoolean() @ValidateBoolean({ optional: true })
@Optional()
@Transform(toBoolean)
clip?: boolean; clip?: boolean;
@IsEnum(AssetType) @IsEnum(AssetType)
@Optional() @Optional()
type?: AssetType; type?: AssetType;
@IsBoolean() @ValidateBoolean({ optional: true })
@Optional()
@Transform(toBoolean)
recent?: boolean; recent?: boolean;
@IsBoolean() @ValidateBoolean({ optional: true })
@Optional()
@Transform(toBoolean)
motion?: boolean; motion?: boolean;
@IsBoolean() @ValidateBoolean({ optional: true })
@Optional()
@Transform(toBoolean)
withArchived?: boolean; withArchived?: boolean;
@IsInt() @IsInt()
@ -252,9 +242,7 @@ export class SearchPeopleDto {
@IsNotEmpty() @IsNotEmpty()
name!: string; name!: string;
@IsBoolean() @ValidateBoolean({ optional: true })
@Transform(toBoolean)
@Optional()
withHidden?: boolean; withHidden?: boolean;
} }

View File

@ -1,8 +1,7 @@
import { SharedLinkType } from '@app/infra/entities'; import { SharedLinkType } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { IsEnum, IsString } from 'class-validator';
import { IsBoolean, IsDate, IsEnum, IsString } from 'class-validator'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from '../domain.util';
import { Optional, ValidateUUID } from '../domain.util';
export class SharedLinkCreateDto { export class SharedLinkCreateDto {
@IsEnum(SharedLinkType) @IsEnum(SharedLinkType)
@ -23,21 +22,16 @@ export class SharedLinkCreateDto {
@Optional() @Optional()
password?: string; password?: string;
@IsDate() @ValidateDate({ optional: true, nullable: true })
@Type(() => Date)
@Optional({ nullable: true })
expiresAt?: Date | null = null; expiresAt?: Date | null = null;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean() allowUpload?: boolean;
allowUpload?: boolean = false;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
allowDownload?: boolean = true; allowDownload?: boolean = true;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
showMetadata?: boolean = true; showMetadata?: boolean = true;
} }
@ -54,10 +48,10 @@ export class SharedLinkEditDto {
@Optional() @Optional()
allowUpload?: boolean; allowUpload?: boolean;
@Optional() @ValidateBoolean({ optional: true })
allowDownload?: boolean; allowDownload?: boolean;
@Optional() @ValidateBoolean({ optional: true })
showMetadata?: boolean; showMetadata?: boolean;
/** /**
@ -65,8 +59,7 @@ export class SharedLinkEditDto {
* Setting this flag and not sending expiryAt is considered as null instead. * Setting this flag and not sending expiryAt is considered as null instead.
* Clients that can send null values can ignore this. * Clients that can send null values can ignore this.
*/ */
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
changeExpiryTime?: boolean; changeExpiryTime?: boolean;
} }

View File

@ -1,11 +1,11 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsBoolean, IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; import { IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator';
import { Optional } from '../../domain.util'; import { Optional, ValidateBoolean } from '../../domain.util';
import { CLIPMode, ModelType } from '../../repositories'; import { CLIPMode, ModelType } from '../../repositories';
export class ModelConfig { export class ModelConfig {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
@IsString() @IsString()

View File

@ -1,7 +1,8 @@
import { AudioCodec, CQMode, ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec } from '@app/infra/entities'; import { AudioCodec, CQMode, ToneMapping, TranscodeHWAccel, TranscodePolicy, VideoCodec } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsBoolean, IsEnum, IsInt, IsString, Max, Min } from 'class-validator'; import { IsEnum, IsInt, IsString, Max, Min } from 'class-validator';
import { ValidateBoolean } from '../../domain.util';
export class SystemConfigFFmpegDto { export class SystemConfigFFmpegDto {
@IsInt() @IsInt()
@ -68,14 +69,14 @@ export class SystemConfigFFmpegDto {
@ApiProperty({ type: 'integer' }) @ApiProperty({ type: 'integer' })
npl!: number; npl!: number;
@IsBoolean() @ValidateBoolean()
temporalAQ!: boolean; temporalAQ!: boolean;
@IsEnum(CQMode) @IsEnum(CQMode)
@ApiProperty({ enumName: 'CQMode', enum: CQMode }) @ApiProperty({ enumName: 'CQMode', enum: CQMode })
cqMode!: CQMode; cqMode!: CQMode;
@IsBoolean() @ValidateBoolean()
twoPass!: boolean; twoPass!: boolean;
@IsString() @IsString()

View File

@ -1,7 +1,5 @@
import { validateCronExpression } from '@app/domain';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { import {
IsBoolean,
IsNotEmpty, IsNotEmpty,
IsObject, IsObject,
IsString, IsString,
@ -11,6 +9,7 @@ import {
ValidatorConstraint, ValidatorConstraint,
ValidatorConstraintInterface, ValidatorConstraintInterface,
} from 'class-validator'; } from 'class-validator';
import { ValidateBoolean, validateCronExpression } from '../../domain.util';
const isEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; const isEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
@ -22,7 +21,7 @@ class CronValidator implements ValidatorConstraintInterface {
} }
export class SystemConfigLibraryScanDto { export class SystemConfigLibraryScanDto {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
@ValidateIf(isEnabled) @ValidateIf(isEnabled)
@ -33,7 +32,7 @@ export class SystemConfigLibraryScanDto {
} }
export class SystemConfigLibraryWatchDto { export class SystemConfigLibraryWatchDto {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
} }

View File

@ -1,9 +1,10 @@
import { LogLevel } from '@app/infra/entities'; import { LogLevel } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { IsBoolean, IsEnum } from 'class-validator'; import { IsEnum } from 'class-validator';
import { ValidateBoolean } from '../../domain.util';
export class SystemConfigLoggingDto { export class SystemConfigLoggingDto {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
@ApiProperty({ enum: LogLevel, enumName: 'LogLevel' }) @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' })

View File

@ -1,9 +1,10 @@
import { CLIPConfig, RecognitionConfig } from '@app/domain';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsBoolean, IsObject, IsUrl, ValidateIf, ValidateNested } from 'class-validator'; import { IsObject, IsUrl, ValidateIf, ValidateNested } from 'class-validator';
import { ValidateBoolean } from '../../domain.util';
import { CLIPConfig, RecognitionConfig } from '../../smart-info/dto/model-config.dto';
export class SystemConfigMachineLearningDto { export class SystemConfigMachineLearningDto {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
@IsUrl({ require_tld: false, allow_underscores: true }) @IsUrl({ require_tld: false, allow_underscores: true })

View File

@ -1,7 +1,8 @@
import { IsBoolean, IsString } from 'class-validator'; import { IsString } from 'class-validator';
import { ValidateBoolean } from '../../domain.util';
export class SystemConfigMapDto { export class SystemConfigMapDto {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
@IsString() @IsString()

View File

@ -1,6 +1,6 @@
import { IsBoolean } from 'class-validator'; import { ValidateBoolean } from '../../domain.util';
export class SystemConfigNewVersionCheckDto { export class SystemConfigNewVersionCheckDto {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
} }

View File

@ -1,13 +1,14 @@
import { IsBoolean, IsNotEmpty, IsNumber, IsString, IsUrl, Min, ValidateIf } from 'class-validator'; import { IsNotEmpty, IsNumber, IsString, IsUrl, Min, ValidateIf } from 'class-validator';
import { ValidateBoolean } from '../../domain.util';
const isEnabled = (config: SystemConfigOAuthDto) => config.enabled; const isEnabled = (config: SystemConfigOAuthDto) => config.enabled;
const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled; const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled;
export class SystemConfigOAuthDto { export class SystemConfigOAuthDto {
@IsBoolean() @ValidateBoolean()
autoLaunch!: boolean; autoLaunch!: boolean;
@IsBoolean() @ValidateBoolean()
autoRegister!: boolean; autoRegister!: boolean;
@IsString() @IsString()
@ -27,7 +28,7 @@ export class SystemConfigOAuthDto {
@Min(0) @Min(0)
defaultStorageQuota!: number; defaultStorageQuota!: number;
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
@ValidateIf(isEnabled) @ValidateIf(isEnabled)
@ -35,7 +36,7 @@ export class SystemConfigOAuthDto {
@IsString() @IsString()
issuerUrl!: string; issuerUrl!: string;
@IsBoolean() @ValidateBoolean()
mobileOverrideEnabled!: boolean; mobileOverrideEnabled!: boolean;
@ValidateIf(isOverrideEnabled) @ValidateIf(isOverrideEnabled)

View File

@ -1,6 +1,6 @@
import { IsBoolean } from 'class-validator'; import { ValidateBoolean } from '../../domain.util';
export class SystemConfigPasswordLoginDto { export class SystemConfigPasswordLoginDto {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
} }

View File

@ -1,6 +1,6 @@
import { IsBoolean } from 'class-validator'; import { ValidateBoolean } from '../../domain.util';
export class SystemConfigReverseGeocodingDto { export class SystemConfigReverseGeocodingDto {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
} }

View File

@ -1,10 +1,13 @@
import { IsBoolean, IsNotEmpty, IsString } from 'class-validator'; import { IsNotEmpty, IsString } from 'class-validator';
import { ValidateBoolean } from '../../domain.util';
export class SystemConfigStorageTemplateDto { export class SystemConfigStorageTemplateDto {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
@IsBoolean()
@ValidateBoolean()
hashVerificationEnabled!: boolean; hashVerificationEnabled!: boolean;
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
template!: string; template!: string;

View File

@ -1,9 +1,10 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsBoolean, IsInt, Min } from 'class-validator'; import { IsInt, Min } from 'class-validator';
import { ValidateBoolean } from '../../domain.util';
export class SystemConfigTrashDto { export class SystemConfigTrashDto {
@IsBoolean() @ValidateBoolean()
enabled!: boolean; enabled!: boolean;
@IsInt() @IsInt()

View File

@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; import { IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator';
import { Optional, toEmail, toSanitized } from '../../domain.util'; import { Optional, ValidateBoolean, toEmail, toSanitized } from '../../domain.util';
export class CreateUserDto { export class CreateUserDto {
@IsEmail({ require_tld: false }) @IsEmail({ require_tld: false })
@ -21,8 +21,7 @@ export class CreateUserDto {
@Transform(toSanitized) @Transform(toSanitized)
storageLabel?: string | null; storageLabel?: string | null;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
memoriesEnabled?: boolean; memoriesEnabled?: boolean;
@Optional({ nullable: true }) @Optional({ nullable: true })
@ -31,8 +30,7 @@ export class CreateUserDto {
@ApiProperty({ type: 'integer', format: 'int64' }) @ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes?: number | null; quotaSizeInBytes?: number | null;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
shouldChangePassword?: boolean; shouldChangePassword?: boolean;
} }

View File

@ -1,8 +1,8 @@
import { UserAvatarColor } from '@app/infra/entities'; import { UserAvatarColor } from '@app/infra/entities';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer'; import { Transform } from 'class-transformer';
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator'; import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
import { Optional, toEmail, toSanitized } from '../../domain.util'; import { Optional, ValidateBoolean, toEmail, toSanitized } from '../../domain.util';
export class UpdateUserDto { export class UpdateUserDto {
@Optional() @Optional()
@ -30,16 +30,13 @@ export class UpdateUserDto {
@ApiProperty({ format: 'uuid' }) @ApiProperty({ format: 'uuid' })
id!: string; id!: string;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
isAdmin?: boolean; isAdmin?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
shouldChangePassword?: boolean; shouldChangePassword?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
memoriesEnabled?: boolean; memoriesEnabled?: boolean;
@Optional() @Optional()

View File

@ -1,19 +1,13 @@
import { Optional, toBoolean } from '@app/domain'; import { Optional, ValidateBoolean, ValidateDate } from '@app/domain';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; import { Type } from 'class-transformer';
import { IsBoolean, IsDate, IsInt, IsNotEmpty, IsUUID } from 'class-validator'; import { IsInt, IsUUID } from 'class-validator';
export class AssetSearchDto { export class AssetSearchDto {
@Optional() @ValidateBoolean({ optional: true })
@IsNotEmpty()
@IsBoolean()
@Transform(toBoolean)
isFavorite?: boolean; isFavorite?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsNotEmpty()
@IsBoolean()
@Transform(toBoolean)
isArchived?: boolean; isArchived?: boolean;
@Optional() @Optional()
@ -33,13 +27,9 @@ export class AssetSearchDto {
@ApiProperty({ format: 'uuid' }) @ApiProperty({ format: 'uuid' })
userId?: string; userId?: string;
@Optional() @ValidateDate({ optional: true })
@IsDate()
@Type(() => Date)
updatedAfter?: Date; updatedAfter?: Date;
@Optional() @ValidateDate({ optional: true })
@IsDate()
@Type(() => Date)
updatedBefore?: Date; updatedBefore?: Date;
} }

View File

@ -1,7 +1,6 @@
import { Optional, toBoolean, UploadFieldName, ValidateUUID } from '@app/domain'; import { Optional, UploadFieldName, ValidateBoolean, ValidateDate, ValidateUUID } from '@app/domain';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform, Type } from 'class-transformer'; import { IsNotEmpty, IsString } from 'class-validator';
import { IsBoolean, IsDate, IsNotEmpty, IsString } from 'class-validator';
export class CreateAssetDto { export class CreateAssetDto {
@ValidateUUID({ optional: true }) @ValidateUUID({ optional: true })
@ -15,43 +14,29 @@ export class CreateAssetDto {
@IsString() @IsString()
deviceId!: string; deviceId!: string;
@IsNotEmpty() @ValidateDate()
@IsDate()
@Type(() => Date)
fileCreatedAt!: Date; fileCreatedAt!: Date;
@IsNotEmpty() @ValidateDate()
@IsDate()
@Type(() => Date)
fileModifiedAt!: Date; fileModifiedAt!: Date;
@Optional() @Optional()
@IsString() @IsString()
duration?: string; duration?: string;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
isFavorite?: boolean; isFavorite?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
isArchived?: boolean; isArchived?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
isVisible?: boolean; isVisible?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
isOffline?: boolean; isOffline?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean()
@Transform(toBoolean)
isReadOnly?: boolean; isReadOnly?: boolean;
// The properties below are added to correctly generate the API docs // The properties below are added to correctly generate the API docs

View File

@ -1,18 +1,12 @@
import { Optional, toBoolean } from '@app/domain'; import { ValidateBoolean } from '@app/domain';
import { ApiProperty } from '@nestjs/swagger'; import { ApiProperty } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsBoolean } from 'class-validator';
export class ServeFileDto { export class ServeFileDto {
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean() @ApiProperty({ title: 'Is serve thumbnail (resize) file' })
@Transform(toBoolean)
@ApiProperty({ type: Boolean, title: 'Is serve thumbnail (resize) file' })
isThumb?: boolean; isThumb?: boolean;
@Optional() @ValidateBoolean({ optional: true })
@IsBoolean() @ApiProperty({ title: 'Is request made from web' })
@Transform(toBoolean)
@ApiProperty({ type: Boolean, title: 'Is request made from web' })
isWeb?: boolean; isWeb?: boolean;
} }