docs(openapi): add descriptions to OpenAPI specification (#25185)

* faces

* add openapi descriptions

* remove dto descriptions

* gen openapi

* dtos

* fix dtos

* fix more

* fix build

* more

* complete dtos

* descriptions on rebase

* gen rebase

* revert correct integer type conversion

* gen after revert

* revert correct nullables

* regen after revert

* actually incorrect adding default here

* revert correct number type conversion

* regen after revert

* revert nullable usage

* regen fully

* readd some comments

* one more

* one more

* use enum

* add missing

* add missing controllers

* add missing dtos

* complete it

* more

* describe global key and slug

* add remaining body and param descriptions

* lint and format

* cleanup

* response and schema descriptions

* test patch according to suggestion

* revert added api response objects

* revert added api body objects

* revert added api param object

* revert added api query objects

* revert reorganized http code objects

* revert reorganize ApiOkResponse objects

* revert added api response objects (2)

* revert added api tag object

* revert added api schema objects

* migrate missing asset.dto.ts

* regenerate openapi builds

* delete generated mustache files

* remove descriptions from properties that are schemas

* lint

* revert nullable type changes

* revert int/num type changes

* remove explicit default

* readd comment

* lint

* pr fixes

* last bits and pieces

* lint and format

* chore: remove rejected patches

* fix: deleting asset from asset-viewer on search results (#25596)

* fix: escape handling in search asset viewer (#25621)

* fix: correctly show owner in album options modal (#25618)

* fix: validation issues

* fix: validation issues

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
Co-authored-by: Min Idzelis <min123@gmail.com>
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
Co-authored-by: Paul Makles <me@insrt.uk>
This commit is contained in:
Timon
2026-01-29 14:49:15 +01:00
committed by GitHub
parent eadb2f89af
commit 8db61d341f
377 changed files with 5554 additions and 735 deletions

View File

@@ -0,0 +1,39 @@
import { MaintenanceController } from 'src/controllers/maintenance.controller';
import { MaintenanceAction } from 'src/enum';
import { MaintenanceService } from 'src/services/maintenance.service';
import request from 'supertest';
import { errorDto } from 'test/medium/responses';
import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils';
describe(MaintenanceController.name, () => {
let ctx: ControllerContext;
const service = mockBaseService(MaintenanceService);
beforeAll(async () => {
ctx = await controllerSetup(MaintenanceController, [{ provide: MaintenanceService, useValue: service }]);
return () => ctx.close();
});
beforeEach(() => {
service.resetAllMocks();
ctx.reset();
});
describe('POST /admin/maintenance', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/admin/maintenance').send();
expect(ctx.authenticate).toHaveBeenCalled();
});
it('should require a backup file when action is restore', async () => {
const { status, body } = await request(ctx.getHttpServer()).post('/admin/maintenance').send({
action: MaintenanceAction.RestoreDatabase,
});
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest(['restoreBackupFilename must be a string', 'restoreBackupFilename should not be empty']),
);
expect(ctx.authenticate).toHaveBeenCalled();
});
});
});

View File

@@ -171,7 +171,7 @@ export const Endpoint = ({ history, ...options }: EndpointOptions) => {
return applyDecorators(...decorators);
};
type PropertyOptions = ApiPropertyOptions & { history?: HistoryBuilder };
export type PropertyOptions = ApiPropertyOptions & { history?: HistoryBuilder };
export const Property = ({ history, ...options }: PropertyOptions) => {
const extensions = history?.getExtensions() ?? {};

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsString, ValidateIf } from 'class-validator';
import { Activity } from 'src/database';
import { mapUser, UserResponseDto } from 'src/dtos/user.dto';
@@ -17,48 +17,55 @@ export enum ReactionLevel {
export type MaybeDuplicate<T> = { duplicate: boolean; value: T };
export class ActivityResponseDto {
@ApiProperty({ description: 'Activity ID' })
id!: string;
@ApiProperty({ description: 'Creation date', format: 'date-time' })
createdAt!: Date;
@ValidateEnum({ enum: ReactionType, name: 'ReactionType' })
@ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type' })
type!: ReactionType;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
user!: UserResponseDto;
@ApiProperty({ description: 'Asset ID (if activity is for an asset)' })
assetId!: string | null;
@ApiPropertyOptional({ description: 'Comment text (for comment activities)' })
comment?: string | null;
}
export class ActivityStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of comments' })
comments!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of likes' })
likes!: number;
}
export class ActivityDto {
@ValidateUUID()
@ValidateUUID({ description: 'Album ID' })
albumId!: string;
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Asset ID (if activity is for an asset)' })
assetId?: string;
}
export class ActivitySearchDto extends ActivityDto {
@ValidateEnum({ enum: ReactionType, name: 'ReactionType', optional: true })
@ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Filter by activity type', optional: true })
type?: ReactionType;
@ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', optional: true })
@ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', description: 'Filter by activity level', optional: true })
level?: ReactionLevel;
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Filter by user ID' })
userId?: string;
}
const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT;
export class ActivityCreateDto extends ActivityDto {
@ValidateEnum({ enum: ReactionType, name: 'ReactionType' })
@ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type (like or comment)' })
type!: ReactionType;
@ApiPropertyOptional({ description: 'Comment text (required if type is comment)' })
@ValidateIf(isComment)
@IsNotEmpty()
@IsString()

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator';
import _ from 'lodash';
@@ -11,156 +11,181 @@ import { AlbumUserRole, AssetOrder } from 'src/enum';
import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation';
export class AlbumInfoDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Exclude assets from response' })
withoutAssets?: boolean;
}
export class AlbumUserAddDto {
@ValidateUUID()
@ValidateUUID({ description: 'User ID' })
userId!: string;
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', default: AlbumUserRole.Editor })
@ValidateEnum({
enum: AlbumUserRole,
name: 'AlbumUserRole',
description: 'Album user role',
default: AlbumUserRole.Editor,
})
role?: AlbumUserRole;
}
export class AddUsersDto {
@ApiProperty({ description: 'Album users to add' })
@ArrayNotEmpty()
albumUsers!: AlbumUserAddDto[];
}
export class AlbumUserCreateDto {
@ValidateUUID()
@ValidateUUID({ description: 'User ID' })
userId!: string;
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' })
role!: AlbumUserRole;
}
export class CreateAlbumDto {
@ApiProperty({ description: 'Album name' })
@IsString()
@ApiProperty()
albumName!: string;
@ApiPropertyOptional({ description: 'Album description' })
@IsString()
@Optional()
description?: string;
@ApiPropertyOptional({ description: 'Album users' })
@Optional()
@IsArray()
@ValidateNested({ each: true })
@Type(() => AlbumUserCreateDto)
albumUsers?: AlbumUserCreateDto[];
@ValidateUUID({ optional: true, each: true })
@ValidateUUID({ optional: true, each: true, description: 'Initial asset IDs' })
assetIds?: string[];
}
export class AlbumsAddAssetsDto {
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'Album IDs' })
albumIds!: string[];
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'Asset IDs' })
assetIds!: string[];
}
export class AlbumsAddAssetsResponseDto {
@ApiProperty({ description: 'Operation success' })
success!: boolean;
@ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', optional: true })
@ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', description: 'Error reason', optional: true })
error?: BulkIdErrorReason;
}
export class UpdateAlbumDto {
@ApiPropertyOptional({ description: 'Album name' })
@Optional()
@IsString()
albumName?: string;
@ApiPropertyOptional({ description: 'Album description' })
@Optional()
@IsString()
description?: string;
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Album thumbnail asset ID' })
albumThumbnailAssetId?: string;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Enable activity feed' })
isActivityEnabled?: boolean;
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true })
order?: AssetOrder;
}
export class GetAlbumsDto {
@ValidateBoolean({ optional: true })
/**
* true: only shared albums
* false: only non-shared own albums
* undefined: shared and owned albums
*/
@ValidateBoolean({
optional: true,
description: 'Filter by shared status: true = only shared, false = only own, undefined = all',
})
shared?: boolean;
/**
* Only returns albums that contain the asset
* Ignores the shared parameter
* undefined: get all albums
*/
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Filter albums containing this asset ID (ignores shared parameter)' })
assetId?: string;
}
export class AlbumStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of owned albums' })
owned!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of shared albums' })
shared!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of non-shared albums' })
notShared!: number;
}
export class UpdateAlbumUserDto {
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' })
role!: AlbumUserRole;
}
export class AlbumUserResponseDto {
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
user!: UserResponseDto;
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' })
role!: AlbumUserRole;
}
export class ContributorCountResponseDto {
@ApiProperty()
@ApiProperty({ description: 'User ID' })
userId!: string;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of assets contributed' })
assetCount!: number;
}
export class AlbumResponseDto {
@ApiProperty({ description: 'Album ID' })
id!: string;
@ApiProperty({ description: 'Owner user ID' })
ownerId!: string;
@ApiProperty({ description: 'Album name' })
albumName!: string;
@ApiProperty({ description: 'Album description' })
description!: string;
@ApiProperty({ description: 'Creation date' })
createdAt!: Date;
@ApiProperty({ description: 'Last update date' })
updatedAt!: Date;
@ApiProperty({ description: 'Thumbnail asset ID' })
albumThumbnailAssetId!: string | null;
@ApiProperty({ description: 'Is shared album' })
shared!: boolean;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
albumUsers!: AlbumUserResponseDto[];
@ApiProperty({ description: 'Has shared link' })
hasSharedLink!: boolean;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
assets!: AssetResponseDto[];
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
owner!: UserResponseDto;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of assets' })
assetCount!: number;
@ApiPropertyOptional({ description: 'Last modified asset timestamp' })
lastModifiedAssetTimestamp?: Date;
@ApiPropertyOptional({ description: 'Start date (earliest asset)' })
startDate?: Date;
@ApiPropertyOptional({ description: 'End date (latest asset)' })
endDate?: Date;
@ApiProperty({ description: 'Activity feed enabled' })
isActivityEnabled!: boolean;
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true })
order?: AssetOrder;
// Optional per-user contribution counts for shared albums
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Type(() => ContributorCountResponseDto)
@ApiProperty({ type: [ContributorCountResponseDto], required: false })
contributorCounts?: ContributorCountResponseDto[];
}

View File

@@ -1,38 +1,55 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ArrayMinSize, IsNotEmpty, IsString } from 'class-validator';
import { Permission } from 'src/enum';
import { Optional, ValidateEnum } from 'src/validation';
export class APIKeyCreateDto {
@ApiPropertyOptional({ description: 'API key name' })
@IsString()
@IsNotEmpty()
@Optional()
name?: string;
@ValidateEnum({ enum: Permission, name: 'Permission', each: true })
@ValidateEnum({ enum: Permission, name: 'Permission', each: true, description: 'List of permissions' })
@ArrayMinSize(1)
permissions!: Permission[];
}
export class APIKeyUpdateDto {
@ApiPropertyOptional({ description: 'API key name' })
@Optional()
@IsString()
@IsNotEmpty()
name?: string;
@ValidateEnum({ enum: Permission, name: 'Permission', each: true, optional: true })
@ValidateEnum({
enum: Permission,
name: 'Permission',
description: 'List of permissions',
each: true,
optional: true,
})
@ArrayMinSize(1)
permissions?: Permission[];
}
export class APIKeyCreateResponseDto {
secret!: string;
apiKey!: APIKeyResponseDto;
}
export class APIKeyResponseDto {
@ApiProperty({ description: 'API key ID' })
id!: string;
@ApiProperty({ description: 'API key name' })
name!: string;
@ApiProperty({ description: 'Creation date' })
createdAt!: Date;
@ApiProperty({ description: 'Last update date' })
updatedAt!: Date;
@ValidateEnum({ enum: Permission, name: 'Permission', each: true })
@ValidateEnum({ enum: Permission, name: 'Permission', each: true, description: 'List of permissions' })
permissions!: Permission[];
}
export class APIKeyCreateResponseDto {
@ApiProperty({ description: 'API key secret (only shown once)' })
secret!: string;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
apiKey!: APIKeyResponseDto;
}

View File

@@ -1,3 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ValidateUUID } from 'src/validation';
/** @deprecated Use `BulkIdResponseDto` instead */
@@ -9,8 +10,11 @@ export enum AssetIdErrorReason {
/** @deprecated Use `BulkIdResponseDto` instead */
export class AssetIdsResponseDto {
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
@ApiProperty({ description: 'Whether operation succeeded' })
success!: boolean;
@ApiPropertyOptional({ description: 'Error reason if failed', enum: AssetIdErrorReason })
error?: AssetIdErrorReason;
}
@@ -22,12 +26,15 @@ export enum BulkIdErrorReason {
}
export class BulkIdsDto {
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'IDs to process' })
ids!: string[];
}
export class BulkIdResponseDto {
@ApiProperty({ description: 'ID' })
id!: string;
@ApiProperty({ description: 'Whether operation succeeded' })
success!: boolean;
@ApiPropertyOptional({ description: 'Error reason if failed', enum: BulkIdErrorReason })
error?: BulkIdErrorReason;
}

View File

@@ -1,3 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ValidateEnum } from 'src/validation';
export enum AssetMediaStatus {
@@ -6,8 +7,9 @@ export enum AssetMediaStatus {
DUPLICATE = 'duplicate',
}
export class AssetMediaResponseDto {
@ValidateEnum({ enum: AssetMediaStatus, name: 'AssetMediaStatus' })
@ValidateEnum({ enum: AssetMediaStatus, name: 'AssetMediaStatus', description: 'Upload status' })
status!: AssetMediaStatus;
@ApiProperty({ description: 'Asset media ID' })
id!: string;
}
@@ -22,17 +24,24 @@ export enum AssetRejectReason {
}
export class AssetBulkUploadCheckResult {
@ApiProperty({ description: 'Asset ID' })
id!: string;
@ApiProperty({ description: 'Upload action', enum: AssetUploadAction })
action!: AssetUploadAction;
@ApiPropertyOptional({ description: 'Rejection reason if rejected', enum: AssetRejectReason })
reason?: AssetRejectReason;
@ApiPropertyOptional({ description: 'Existing asset ID if duplicate' })
assetId?: string;
@ApiPropertyOptional({ description: 'Whether existing asset is trashed' })
isTrashed?: boolean;
}
export class AssetBulkUploadCheckResponseDto {
@ApiProperty({ description: 'Upload check results' })
results!: AssetBulkUploadCheckResult[];
}
export class CheckExistingAssetsResponseDto {
@ApiProperty({ description: 'Existing asset IDs' })
existingIds!: string[];
}

View File

@@ -1,5 +1,5 @@
import { BadRequestException } from '@nestjs/common';
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { plainToInstance, Transform, Type } from 'class-transformer';
import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto';
@@ -18,10 +18,10 @@ export enum AssetMediaSize {
}
export class AssetMediaOptionsDto {
@ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', optional: true })
@ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', description: 'Asset media size', optional: true })
size?: AssetMediaSize;
@ValidateBoolean({ optional: true, default: false })
@ValidateBoolean({ optional: true, description: 'Return edited asset if available', default: false })
edited?: boolean;
}
@@ -32,44 +32,49 @@ export enum UploadFieldName {
}
class AssetMediaBase {
@ApiProperty({ description: 'Device asset ID' })
@IsNotEmpty()
@IsString()
deviceAssetId!: string;
@ApiProperty({ description: 'Device ID' })
@IsNotEmpty()
@IsString()
deviceId!: string;
@ValidateDate()
@ValidateDate({ description: 'File creation date' })
fileCreatedAt!: Date;
@ValidateDate()
@ValidateDate({ description: 'File modification date' })
fileModifiedAt!: Date;
@ApiPropertyOptional({ description: 'Duration (for videos)' })
@Optional()
@IsString()
duration?: string;
@ApiPropertyOptional({ description: 'Filename' })
@Optional()
@IsString()
filename?: string;
// The properties below are added to correctly generate the API docs
// and client SDKs. Validation should be handled in the controller.
@ApiProperty({ type: 'string', format: 'binary' })
@ApiProperty({ type: 'string', format: 'binary', description: 'Asset file data' })
[UploadFieldName.ASSET_DATA]!: any;
}
export class AssetMediaCreateDto extends AssetMediaBase {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Mark as favorite' })
isFavorite?: boolean;
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility', optional: true })
visibility?: AssetVisibility;
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Live photo video ID' })
livePhotoVideoId?: string;
@ApiPropertyOptional({ description: 'Asset metadata items' })
@Transform(({ value }) => {
try {
const json = JSON.parse(value);
@@ -84,24 +89,26 @@ export class AssetMediaCreateDto extends AssetMediaBase {
@IsArray()
metadata?: AssetMetadataUpsertItemDto[];
@ApiProperty({ type: 'string', format: 'binary', required: false })
@ApiProperty({ type: 'string', format: 'binary', required: false, description: 'Sidecar file data' })
[UploadFieldName.SIDECAR_DATA]?: any;
}
export class AssetMediaReplaceDto extends AssetMediaBase {}
export class AssetBulkUploadCheckItem {
@ApiProperty({ description: 'Asset ID' })
@IsString()
@IsNotEmpty()
id!: string;
/** base64 or hex encoded sha1 hash */
@ApiProperty({ description: 'Base64 or hex encoded SHA1 hash' })
@IsString()
@IsNotEmpty()
checksum!: string;
}
export class AssetBulkUploadCheckDto {
@ApiProperty({ description: 'Assets to check' })
@IsArray()
@ValidateNested({ each: true })
@Type(() => AssetBulkUploadCheckItem)
@@ -109,11 +116,13 @@ export class AssetBulkUploadCheckDto {
}
export class CheckExistingAssetsDto {
@ApiProperty({ description: 'Device asset IDs to check' })
@ArrayNotEmpty()
@IsString({ each: true })
@IsNotEmpty({ each: true })
deviceAssetIds!: string[];
@ApiProperty({ description: 'Device ID' })
@IsNotEmpty()
deviceId!: string;
}

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Selectable } from 'kysely';
import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database';
import { HistoryBuilder, Property } from 'src/decorators';
@@ -18,13 +18,16 @@ import { ImageDimensions } from 'src/types';
import { getDimensions } from 'src/utils/asset.util';
import { hexOrBufferToBase64 } from 'src/utils/bytes';
import { mimeTypes } from 'src/utils/mime-types';
import { ValidateEnum } from 'src/validation';
import { ValidateEnum, ValidateUUID } from 'src/validation';
export class SanitizedAssetResponseDto {
@ApiProperty({ description: 'Asset ID' })
id!: string;
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' })
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' })
type!: AssetType;
@ApiProperty({ description: 'Thumbhash for thumbnail generation' })
thumbhash!: string | null;
@ApiPropertyOptional({ description: 'Original MIME type' })
originalMimeType?: string;
@ApiProperty({
type: 'string',
@@ -34,10 +37,15 @@ export class SanitizedAssetResponseDto {
example: '2024-01-15T14:30:00.000Z',
})
localDateTime!: Date;
@ApiProperty({ description: 'Video duration (for videos)' })
duration!: string;
@ApiPropertyOptional({ description: 'Live photo video ID' })
livePhotoVideoId?: string | null;
@ApiProperty({ description: 'Whether asset has metadata' })
hasMetadata!: boolean;
@ApiProperty({ description: 'Asset width' })
width!: number | null;
@ApiProperty({ description: 'Asset height' })
height!: number | null;
}
@@ -49,13 +57,24 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
example: '2024-01-15T20:30:00.000Z',
})
createdAt!: Date;
@ApiProperty({ description: 'Device asset ID' })
deviceAssetId!: string;
@ApiProperty({ description: 'Device ID' })
deviceId!: string;
@ApiProperty({ description: 'Owner user ID' })
ownerId!: string;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
owner?: UserResponseDto;
@Property({ history: new HistoryBuilder().added('v1').deprecated('v1') })
@ValidateUUID({
nullable: true,
description: 'Library ID',
history: new HistoryBuilder().added('v1').deprecated('v1'),
})
libraryId?: string | null;
@ApiProperty({ description: 'Original file path' })
originalPath!: string;
@ApiProperty({ description: 'Original file name' })
originalFileName!: string;
@ApiProperty({
type: 'string',
@@ -81,24 +100,39 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
example: '2024-01-16T12:45:30.000Z',
})
updatedAt!: Date;
@ApiProperty({ description: 'Is favorite' })
isFavorite!: boolean;
@ApiProperty({ description: 'Is archived' })
isArchived!: boolean;
@ApiProperty({ description: 'Is trashed' })
isTrashed!: boolean;
@ApiProperty({ description: 'Is offline' })
isOffline!: boolean;
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility' })
visibility!: AssetVisibility;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
exifInfo?: ExifResponseDto;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
tags?: TagResponseDto[];
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
people?: PersonWithFacesResponseDto[];
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
unassignedFaces?: AssetFaceWithoutPersonResponseDto[];
/**base64 encoded sha1 hash */
@ApiProperty({ description: 'Base64 encoded SHA1 hash' })
checksum!: string;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
stack?: AssetStackResponseDto | null;
@ApiPropertyOptional({ description: 'Duplicate group ID' })
duplicateId?: string | null;
@Property({ history: new HistoryBuilder().added('v1').deprecated('v1.113.0') })
@Property({ description: 'Is resized', history: new HistoryBuilder().added('v1').deprecated('v1.113.0') })
resized?: boolean;
@Property({ history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0') })
@Property({ description: 'Is edited', history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0') })
isEdited!: boolean;
}
@@ -143,11 +177,13 @@ export type MapAsset = {
};
export class AssetStackResponseDto {
@ApiProperty({ description: 'Stack ID' })
id!: string;
@ApiProperty({ description: 'Primary asset ID' })
primaryAssetId!: string;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of assets in stack' })
assetCount!: number;
}

View File

@@ -22,6 +22,7 @@ import { AssetStats } from 'src/repositories/asset.repository';
import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation';
export class DeviceIdDto {
@ApiProperty({ description: 'Device ID' })
@IsNotEmpty()
@IsString()
deviceId!: string;
@@ -32,49 +33,57 @@ const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
const ValidateGPS = () => ValidateIf(hasGPS);
export class UpdateAssetBase {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Mark as favorite' })
isFavorite?: boolean;
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true, description: 'Asset visibility' })
visibility?: AssetVisibility;
@ApiProperty({ description: 'Original date and time' })
@Optional()
@IsDateString()
dateTimeOriginal?: string;
@ApiProperty({ description: 'Latitude coordinate' })
@ValidateGPS()
@IsLatitude()
@IsNotEmpty()
latitude?: number;
@ApiProperty({ description: 'Longitude coordinate' })
@ValidateGPS()
@IsLongitude()
@IsNotEmpty()
longitude?: number;
@ApiProperty({ description: 'Rating' })
@Optional()
@IsInt()
@Max(5)
@Min(-1)
rating?: number;
@ApiProperty({ description: 'Asset description' })
@Optional()
@IsString()
description?: string;
}
export class AssetBulkUpdateDto extends UpdateAssetBase {
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'Asset IDs to update' })
ids!: string[];
@ApiProperty({ description: 'Duplicate asset ID' })
@Optional()
duplicateId?: string | null;
@ApiProperty({ description: 'Relative time offset in seconds' })
@IsNotSiblingOf(['dateTimeOriginal'])
@Optional()
@IsInt()
dateTimeRelative?: number;
@ApiProperty({ description: 'Time zone (IANA timezone)' })
@IsNotSiblingOf(['dateTimeOriginal'])
@IsTimeZone()
@Optional()
@@ -82,11 +91,12 @@ export class AssetBulkUpdateDto extends UpdateAssetBase {
}
export class UpdateAssetDto extends UpdateAssetBase {
@ValidateUUID({ optional: true, nullable: true })
@ValidateUUID({ optional: true, nullable: true, description: 'Live photo video ID' })
livePhotoVideoId?: string | null;
}
export class RandomAssetsDto {
@ApiProperty({ description: 'Number of random assets to return' })
@Optional()
@IsInt()
@IsPositive()
@@ -95,12 +105,12 @@ export class RandomAssetsDto {
}
export class AssetBulkDeleteDto extends BulkIdsDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Force delete even if in use' })
force?: boolean;
}
export class AssetIdsDto {
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'Asset IDs' })
assetIds!: string[];
}
@@ -112,41 +122,42 @@ export enum AssetJobName {
}
export class AssetJobsDto extends AssetIdsDto {
@ValidateEnum({ enum: AssetJobName, name: 'AssetJobName' })
@ValidateEnum({ enum: AssetJobName, name: 'AssetJobName', description: 'Job name' })
name!: AssetJobName;
}
export class AssetStatsDto {
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Filter by visibility', optional: true })
visibility?: AssetVisibility;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter by favorite status' })
isFavorite?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter by trash status' })
isTrashed?: boolean;
}
export class AssetStatsResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ description: 'Number of images', type: 'integer' })
images!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ description: 'Number of videos', type: 'integer' })
videos!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ description: 'Total number of assets', type: 'integer' })
total!: number;
}
export class AssetMetadataRouteParams {
@ValidateUUID()
@ValidateUUID({ description: 'Asset ID' })
id!: string;
@ValidateString()
@ValidateString({ description: 'Metadata key' })
key!: string;
}
export class AssetMetadataUpsertDto {
@ApiProperty({ description: 'Metadata items to upsert' })
@IsArray()
@ValidateNested({ each: true })
@Type(() => AssetMetadataUpsertItemDto)
@@ -154,14 +165,16 @@ export class AssetMetadataUpsertDto {
}
export class AssetMetadataUpsertItemDto {
@ValidateString()
@ValidateString({ description: 'Metadata key' })
key!: string;
@ApiProperty({ description: 'Metadata value (object)' })
@IsObject()
value!: object;
}
export class AssetMetadataBulkUpsertDto {
@ApiProperty({ description: 'Metadata items to upsert' })
@IsArray()
@ValidateNested({ each: true })
@Type(() => AssetMetadataBulkUpsertItemDto)
@@ -169,17 +182,19 @@ export class AssetMetadataBulkUpsertDto {
}
export class AssetMetadataBulkUpsertItemDto {
@ValidateUUID()
@ValidateUUID({ description: 'Asset ID' })
assetId!: string;
@ValidateString()
@ValidateString({ description: 'Metadata key' })
key!: string;
@ApiProperty({ description: 'Metadata value (object)' })
@IsObject()
value!: object;
}
export class AssetMetadataBulkDeleteDto {
@ApiProperty({ description: 'Metadata items to delete' })
@IsArray()
@ValidateNested({ each: true })
@Type(() => AssetMetadataBulkDeleteItemDto)
@@ -187,49 +202,54 @@ export class AssetMetadataBulkDeleteDto {
}
export class AssetMetadataBulkDeleteItemDto {
@ValidateUUID()
@ValidateUUID({ description: 'Asset ID' })
assetId!: string;
@ValidateString()
@ValidateString({ description: 'Metadata key' })
key!: string;
}
export class AssetMetadataResponseDto {
@ValidateString()
@ValidateString({ description: 'Metadata key' })
key!: string;
@ApiProperty({ description: 'Metadata value (object)' })
value!: object;
@ApiProperty({ description: 'Last update date' })
updatedAt!: Date;
}
export class AssetMetadataBulkResponseDto extends AssetMetadataResponseDto {
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
}
export class AssetCopyDto {
@ValidateUUID()
@ValidateUUID({ description: 'Source asset ID' })
sourceId!: string;
@ValidateUUID()
@ValidateUUID({ description: 'Target asset ID' })
targetId!: string;
@ValidateBoolean({ optional: true, default: true })
@ValidateBoolean({ optional: true, description: 'Copy shared links', default: true })
sharedLinks?: boolean;
@ValidateBoolean({ optional: true, default: true })
@ValidateBoolean({ optional: true, description: 'Copy album associations', default: true })
albums?: boolean;
@ValidateBoolean({ optional: true, default: true })
@ValidateBoolean({ optional: true, description: 'Copy sidecar file', default: true })
sidecar?: boolean;
@ValidateBoolean({ optional: true, default: true })
@ValidateBoolean({ optional: true, description: 'Copy stack association', default: true })
stack?: boolean;
@ValidateBoolean({ optional: true, default: true })
@ValidateBoolean({ optional: true, description: 'Copy favorite status', default: true })
favorite?: boolean;
}
export class AssetDownloadOriginalDto {
@ValidateBoolean({ optional: true, default: false })
@ValidateBoolean({ optional: true, description: 'Return edited asset if available', default: false })
edited?: boolean;
}

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';
import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database';
@@ -12,34 +12,46 @@ export type CookieResponse = {
};
export class AuthDto {
@ApiProperty({ description: 'Authenticated user' })
user!: AuthUser;
@ApiPropertyOptional({ description: 'API key (if authenticated via API key)' })
apiKey?: AuthApiKey;
@ApiPropertyOptional({ description: 'Shared link (if authenticated via shared link)' })
sharedLink?: AuthSharedLink;
@ApiPropertyOptional({ description: 'Session (if authenticated via session)' })
session?: AuthSession;
}
export class LoginCredentialDto {
@ApiProperty({ example: 'testuser@email.com', description: 'User email' })
@IsEmail({ require_tld: false })
@Transform(toEmail)
@IsNotEmpty()
@ApiProperty({ example: 'testuser@email.com' })
email!: string;
@ApiProperty({ example: 'password', description: 'User password' })
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
}
export class LoginResponseDto {
@ApiProperty({ description: 'Access token' })
accessToken!: string;
@ApiProperty({ description: 'User ID' })
userId!: string;
@ApiProperty({ description: 'User email' })
userEmail!: string;
@ApiProperty({ description: 'User name' })
name!: string;
@ApiProperty({ description: 'Profile image path' })
profileImagePath!: string;
@ApiProperty({ description: 'Is admin user' })
isAdmin!: boolean;
@ApiProperty({ description: 'Should change password' })
shouldChangePassword!: boolean;
@ApiProperty({ description: 'Is onboarded' })
isOnboarded!: boolean;
}
@@ -61,42 +73,47 @@ export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginR
}
export class LogoutResponseDto {
@ApiProperty({ description: 'Logout successful' })
successful!: boolean;
@ApiProperty({ description: 'Redirect URI' })
redirectUri!: string;
}
export class SignUpDto extends LoginCredentialDto {
@ApiProperty({ example: 'Admin', description: 'User name' })
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'Admin' })
name!: string;
}
export class ChangePasswordDto {
@ApiProperty({ example: 'password', description: 'Current password' })
@IsString()
@IsNotEmpty()
@ApiProperty({ example: 'password' })
password!: string;
@ApiProperty({ example: 'password', description: 'New password (min 8 characters)' })
@IsString()
@IsNotEmpty()
@MinLength(8)
@ApiProperty({ example: 'password' })
newPassword!: string;
@ValidateBoolean({ optional: true, default: false })
@ValidateBoolean({ optional: true, default: false, description: 'Invalidate all other sessions' })
invalidateSessions?: boolean;
}
export class PinCodeSetupDto {
@ApiProperty({ description: 'PIN code (4-6 digits)' })
@PinCode()
pinCode!: string;
}
export class PinCodeResetDto {
@ApiPropertyOptional({ description: 'New PIN code (4-6 digits)' })
@PinCode({ optional: true })
pinCode?: string;
@ApiPropertyOptional({ description: 'User password (required if PIN code is not provided)' })
@Optional()
@IsString()
@IsNotEmpty()
@@ -106,51 +123,64 @@ export class PinCodeResetDto {
export class SessionUnlockDto extends PinCodeResetDto {}
export class PinCodeChangeDto extends PinCodeResetDto {
@ApiProperty({ description: 'New PIN code (4-6 digits)' })
@PinCode()
newPinCode!: string;
}
export class ValidateAccessTokenResponseDto {
@ApiProperty({ description: 'Authentication status' })
authStatus!: boolean;
}
export class OAuthCallbackDto {
@ApiProperty({ description: 'OAuth callback URL' })
@IsNotEmpty()
@IsString()
@ApiProperty()
url!: string;
@ApiPropertyOptional({ description: 'OAuth state parameter' })
@Optional()
@IsString()
state?: string;
@ApiPropertyOptional({ description: 'OAuth code verifier (PKCE)' })
@Optional()
@IsString()
codeVerifier?: string;
}
export class OAuthConfigDto {
@ApiProperty({ description: 'OAuth redirect URI' })
@IsNotEmpty()
@IsString()
redirectUri!: string;
@ApiPropertyOptional({ description: 'OAuth state parameter' })
@Optional()
@IsString()
state?: string;
@ApiPropertyOptional({ description: 'OAuth code challenge (PKCE)' })
@Optional()
@IsString()
codeChallenge?: string;
}
export class OAuthAuthorizeResponseDto {
@ApiProperty({ description: 'OAuth authorization URL' })
url!: string;
}
export class AuthStatusResponseDto {
@ApiProperty({ description: 'Has PIN code set' })
pinCode!: boolean;
@ApiProperty({ description: 'Has password set' })
password!: boolean;
@ApiProperty({ description: 'Is elevated session' })
isElevated!: boolean;
@ApiPropertyOptional({ description: 'Session expiration date' })
expiresAt?: string;
@ApiPropertyOptional({ description: 'PIN expiration date' })
pinExpiresAt?: string;
}

View File

@@ -1,32 +1,34 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsInt, IsPositive } from 'class-validator';
import { Optional, ValidateUUID } from 'src/validation';
export class DownloadInfoDto {
@ValidateUUID({ each: true, optional: true })
@ValidateUUID({ each: true, optional: true, description: 'Asset IDs to download' })
assetIds?: string[];
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Album ID to download' })
albumId?: string;
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'User ID to download assets from' })
userId?: string;
@ApiPropertyOptional({ type: 'integer', description: 'Archive size limit in bytes' })
@IsInt()
@IsPositive()
@Optional()
@ApiProperty({ type: 'integer' })
archiveSize?: number;
}
export class DownloadResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Total size in bytes' })
totalSize!: number;
@ApiProperty({ description: 'Archive information' })
archives!: DownloadArchiveInfo[];
}
export class DownloadArchiveInfo {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Archive size in bytes' })
size!: number;
@ApiProperty({ description: 'Asset IDs in this archive' })
assetIds!: string[];
}

View File

@@ -1,6 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
export class DuplicateResponseDto {
@ApiProperty({ description: 'Duplicate group ID' })
duplicateId!: string;
@ApiProperty({ description: 'Duplicate assets' })
assets!: AssetResponseDto[];
}

View File

@@ -50,28 +50,31 @@ export class MirrorParameters {
class AssetEditActionBase {
@IsEnum(AssetEditAction)
@ApiProperty({ enum: AssetEditAction, enumName: 'AssetEditAction' })
@ApiProperty({ enum: AssetEditAction, enumName: 'AssetEditAction', description: 'Type of edit action to perform' })
action!: AssetEditAction;
}
export class AssetEditActionCrop extends AssetEditActionBase {
@ValidateNested()
@Type(() => CropParameters)
@ApiProperty({ type: CropParameters })
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
parameters!: CropParameters;
}
export class AssetEditActionRotate extends AssetEditActionBase {
@ValidateNested()
@Type(() => RotateParameters)
@ApiProperty({ type: RotateParameters })
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
parameters!: RotateParameters;
}
export class AssetEditActionMirror extends AssetEditActionBase {
@ValidateNested()
@Type(() => MirrorParameters)
@ApiProperty({ type: MirrorParameters })
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
parameters!: MirrorParameters;
}
@@ -114,12 +117,14 @@ export class AssetEditActionListDto {
@Transform(({ value: edits }) =>
Array.isArray(edits) ? edits.map((item) => plainToInstance(getActionClass(item), item)) : edits,
)
@ApiProperty({ anyOf: Object.values(actionToClass).map((target) => ({ $ref: getSchemaPath(target) })) })
@ApiProperty({
anyOf: Object.values(actionToClass).map((target) => ({ $ref: getSchemaPath(target) })),
description: 'List of edit actions to apply (crop, rotate, or mirror)',
})
edits!: AssetEditActionItem[];
}
export class AssetEditsDto extends AssetEditActionListDto {
@ValidateUUID()
@ApiProperty()
@ValidateUUID({ description: 'Asset ID to apply edits to' })
assetId!: string;
}

View File

@@ -1,30 +1,51 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Exif } from 'src/database';
export class ExifResponseDto {
@ApiPropertyOptional({ description: 'Camera make' })
make?: string | null = null;
@ApiPropertyOptional({ description: 'Camera model' })
model?: string | null = null;
@ApiPropertyOptional({ type: 'number', description: 'Image width in pixels' })
exifImageWidth?: number | null = null;
@ApiPropertyOptional({ type: 'number', description: 'Image height in pixels' })
exifImageHeight?: number | null = null;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'File size in bytes' })
fileSizeInByte?: number | null = null;
@ApiPropertyOptional({ description: 'Image orientation' })
orientation?: string | null = null;
@ApiPropertyOptional({ description: 'Original date/time', format: 'date-time' })
dateTimeOriginal?: Date | null = null;
@ApiPropertyOptional({ description: 'Modification date/time', format: 'date-time' })
modifyDate?: Date | null = null;
@ApiPropertyOptional({ description: 'Time zone' })
timeZone?: string | null = null;
@ApiPropertyOptional({ description: 'Lens model' })
lensModel?: string | null = null;
@ApiPropertyOptional({ type: 'number', description: 'F-number (aperture)' })
fNumber?: number | null = null;
@ApiPropertyOptional({ type: 'number', description: 'Focal length in mm' })
focalLength?: number | null = null;
@ApiPropertyOptional({ type: 'number', description: 'ISO sensitivity' })
iso?: number | null = null;
@ApiPropertyOptional({ description: 'Exposure time' })
exposureTime?: string | null = null;
@ApiPropertyOptional({ type: 'number', description: 'GPS latitude' })
latitude?: number | null = null;
@ApiPropertyOptional({ type: 'number', description: 'GPS longitude' })
longitude?: number | null = null;
@ApiPropertyOptional({ description: 'City name' })
city?: string | null = null;
@ApiPropertyOptional({ description: 'State/province name' })
state?: string | null = null;
@ApiPropertyOptional({ description: 'Country name' })
country?: string | null = null;
@ApiPropertyOptional({ description: 'Image description' })
description?: string | null = null;
@ApiPropertyOptional({ description: 'Projection type' })
projectionType?: string | null = null;
@ApiPropertyOptional({ type: 'number', description: 'Rating' })
rating?: number | null = null;
}

View File

@@ -2,6 +2,6 @@ import { ManualJobName } from 'src/enum';
import { ValidateEnum } from 'src/validation';
export class JobCreateDto {
@ValidateEnum({ enum: ManualJobName, name: 'ManualJobName' })
@ValidateEnum({ enum: ManualJobName, name: 'ManualJobName', description: 'Job name' })
name!: ManualJobName;
}

View File

@@ -1,17 +1,19 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator';
import { Library } from 'src/database';
import { Optional, ValidateUUID } from 'src/validation';
export class CreateLibraryDto {
@ValidateUUID()
@ValidateUUID({ description: 'Owner user ID' })
ownerId!: string;
@ApiPropertyOptional({ description: 'Library name' })
@IsString()
@Optional()
@IsNotEmpty()
name?: string;
@ApiPropertyOptional({ description: 'Import paths (max 128)' })
@Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@@ -19,6 +21,7 @@ export class CreateLibraryDto {
@ArrayMaxSize(128)
importPaths?: string[];
@ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' })
@Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@@ -28,11 +31,13 @@ export class CreateLibraryDto {
}
export class UpdateLibraryDto {
@ApiPropertyOptional({ description: 'Library name' })
@Optional()
@IsString()
@IsNotEmpty()
name?: string;
@ApiPropertyOptional({ description: 'Import paths (max 128)' })
@Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@@ -40,6 +45,7 @@ export class UpdateLibraryDto {
@ArrayMaxSize(128)
importPaths?: string[];
@ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' })
@Optional()
@IsNotEmpty({ each: true })
@IsString({ each: true })
@@ -59,6 +65,7 @@ export interface WalkOptionsDto extends CrawlOptionsDto {
}
export class ValidateLibraryDto {
@ApiPropertyOptional({ description: 'Import paths to validate (max 128)' })
@Optional()
@IsString({ each: true })
@IsNotEmpty({ each: true })
@@ -66,6 +73,7 @@ export class ValidateLibraryDto {
@ArrayMaxSize(128)
importPaths?: string[];
@ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' })
@Optional()
@IsNotEmpty({ each: true })
@IsString({ each: true })
@@ -75,48 +83,60 @@ export class ValidateLibraryDto {
}
export class ValidateLibraryResponseDto {
@ApiPropertyOptional({ description: 'Validation results for import paths' })
importPaths?: ValidateLibraryImportPathResponseDto[];
}
export class ValidateLibraryImportPathResponseDto {
@ApiProperty({ description: 'Import path' })
importPath!: string;
@ApiProperty({ description: 'Is valid' })
isValid: boolean = false;
@ApiPropertyOptional({ description: 'Validation message' })
message?: string;
}
export class LibrarySearchDto {
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Filter by user ID' })
userId?: string;
}
export class LibraryResponseDto {
@ApiProperty({ description: 'Library ID' })
id!: string;
@ApiProperty({ description: 'Owner user ID' })
ownerId!: string;
@ApiProperty({ description: 'Library name' })
name!: string;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of assets' })
assetCount!: number;
@ApiProperty({ description: 'Import paths' })
importPaths!: string[];
@ApiProperty({ description: 'Exclusion patterns' })
exclusionPatterns!: string[];
@ApiProperty({ description: 'Creation date' })
createdAt!: Date;
@ApiProperty({ description: 'Last update date' })
updatedAt!: Date;
@ApiProperty({ description: 'Last refresh date' })
refreshedAt!: Date | null;
}
export class LibraryStatsResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of photos' })
photos = 0;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of videos' })
videos = 0;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Total number of assets' })
total = 0;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' })
usage = 0;
}

View File

@@ -1,16 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString, Matches } from 'class-validator';
export class LicenseKeyDto {
@ApiProperty({ description: 'License key (format: IM(SV|CL)(-XXXX){8})' })
@IsString()
@IsNotEmpty()
@Matches(/IM(SV|CL)(-[\dA-Za-z]{4}){8}/)
licenseKey!: string;
@ApiProperty({ description: 'Activation key' })
@IsString()
@IsNotEmpty()
activationKey!: string;
}
export class LicenseResponseDto extends LicenseKeyDto {
@ApiProperty({ description: 'Activation date' })
activatedAt!: Date;
}

View File

@@ -1,29 +1,31 @@
import { ApiProperty } from '@nestjs/swagger';
import { ValidateIf } from 'class-validator';
import { MaintenanceAction, StorageFolder } from 'src/enum';
import { ValidateEnum, ValidateString } from 'src/validation';
import { ValidateBoolean, ValidateEnum, ValidateString } from 'src/validation';
export class SetMaintenanceModeDto {
@ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction' })
@ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' })
action!: MaintenanceAction;
@ValidateIf((o) => o.action === MaintenanceAction.RestoreDatabase)
@ValidateString()
@ValidateString({ description: 'Restore backup filename' })
restoreBackupFilename?: string;
}
export class MaintenanceLoginDto {
@ValidateString({ optional: true })
@ValidateString({ optional: true, description: 'Maintenance token' })
token?: string;
}
export class MaintenanceAuthDto {
@ApiProperty({ description: 'Maintenance username' })
username!: string;
}
export class MaintenanceStatusResponseDto {
active!: boolean;
@ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction' })
@ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' })
action!: MaintenanceAction;
progress?: number;
@@ -32,10 +34,13 @@ export class MaintenanceStatusResponseDto {
}
export class MaintenanceDetectInstallStorageFolderDto {
@ValidateEnum({ enum: StorageFolder, name: 'StorageFolder' })
@ValidateEnum({ enum: StorageFolder, name: 'StorageFolder', description: 'Storage folder' })
folder!: StorageFolder;
@ValidateBoolean({ description: 'Whether the folder is readable' })
readable!: boolean;
@ValidateBoolean({ description: 'Whether the folder is writable' })
writable!: boolean;
@ApiProperty({ description: 'Number of files in the folder' })
files!: number;
}

View File

@@ -4,64 +4,64 @@ import { IsLatitude, IsLongitude } from 'class-validator';
import { ValidateBoolean, ValidateDate } from 'src/validation';
export class MapReverseGeocodeDto {
@ApiProperty({ format: 'double' })
@ApiProperty({ format: 'double', description: 'Latitude (-90 to 90)' })
@Type(() => Number)
@IsLatitude({ message: ({ property }) => `${property} must be a number between -90 and 90` })
lat!: number;
@ApiProperty({ format: 'double' })
@ApiProperty({ format: 'double', description: 'Longitude (-180 to 180)' })
@Type(() => Number)
@IsLongitude({ message: ({ property }) => `${property} must be a number between -180 and 180` })
lon!: number;
}
export class MapReverseGeocodeResponseDto {
@ApiProperty()
@ApiProperty({ description: 'City name' })
city!: string | null;
@ApiProperty()
@ApiProperty({ description: 'State/Province name' })
state!: string | null;
@ApiProperty()
@ApiProperty({ description: 'Country name' })
country!: string | null;
}
export class MapMarkerDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter by archived status' })
isArchived?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter by favorite status' })
isFavorite?: boolean;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter assets created after this date' })
fileCreatedAfter?: Date;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter assets created before this date' })
fileCreatedBefore?: Date;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Include partner assets' })
withPartners?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Include shared album assets' })
withSharedAlbums?: boolean;
}
export class MapMarkerResponseDto {
@ApiProperty()
@ApiProperty({ description: 'Asset ID' })
id!: string;
@ApiProperty({ format: 'double' })
@ApiProperty({ format: 'double', description: 'Latitude' })
lat!: number;
@ApiProperty({ format: 'double' })
@ApiProperty({ format: 'double', description: 'Longitude' })
lon!: number;
@ApiProperty()
@ApiProperty({ description: 'City name' })
city!: string | null;
@ApiProperty()
@ApiProperty({ description: 'State/Province name' })
state!: string | null;
@ApiProperty()
@ApiProperty({ description: 'Country name' })
country!: string | null;
}

View File

@@ -8,24 +8,24 @@ import { AssetOrderWithRandom, MemoryType } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
class MemoryBaseDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Is memory saved' })
isSaved?: boolean;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Date when memory was seen' })
seenAt?: Date;
}
export class MemorySearchDto {
@ValidateEnum({ enum: MemoryType, name: 'MemoryType', optional: true })
@ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type', optional: true })
type?: MemoryType;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter by date' })
for?: Date;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Include trashed memories' })
isTrashed?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter by saved status' })
isSaved?: boolean;
@IsInt()
@@ -35,11 +35,12 @@ export class MemorySearchDto {
@ApiProperty({ type: 'integer', description: 'Number of memories to return' })
size?: number;
@ValidateEnum({ enum: AssetOrderWithRandom, name: 'MemorySearchOrder', optional: true })
@ValidateEnum({ enum: AssetOrderWithRandom, name: 'MemorySearchOrder', description: 'Sort order', optional: true })
order?: AssetOrderWithRandom;
}
class OnThisDayDto {
@ApiProperty({ type: 'number', description: 'Year for on this day memory', minimum: 1 })
@IsInt()
@IsPositive()
year!: number;
@@ -48,14 +49,16 @@ class OnThisDayDto {
type MemoryData = OnThisDayDto;
export class MemoryUpdateDto extends MemoryBaseDto {
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Memory date' })
memoryAt?: Date;
}
export class MemoryCreateDto extends MemoryBaseDto {
@ValidateEnum({ enum: MemoryType, name: 'MemoryType' })
@ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' })
type!: MemoryType;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@IsObject()
@ValidateNested()
@Type((options) => {
@@ -71,32 +74,46 @@ export class MemoryCreateDto extends MemoryBaseDto {
})
data!: MemoryData;
@ValidateDate()
@ValidateDate({ description: 'Memory date' })
memoryAt!: Date;
@ValidateUUID({ optional: true, each: true })
@ValidateUUID({ optional: true, each: true, description: 'Asset IDs to associate with memory' })
assetIds?: string[];
}
export class MemoryStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Total number of memories' })
total!: number;
}
export class MemoryResponseDto {
@ApiProperty({ description: 'Memory ID' })
id!: string;
@ValidateDate({ description: 'Creation date' })
createdAt!: Date;
@ValidateDate({ description: 'Last update date' })
updatedAt!: Date;
@ValidateDate({ optional: true, description: 'Deletion date' })
deletedAt?: Date;
@ValidateDate({ description: 'Memory date' })
memoryAt!: Date;
@ValidateDate({ optional: true, description: 'Date when memory was seen' })
seenAt?: Date;
@ValidateDate({ optional: true, description: 'Date when memory should be shown' })
showAt?: Date;
@ValidateDate({ optional: true, description: 'Date when memory should be hidden' })
hideAt?: Date;
@ApiProperty({ description: 'Owner user ID' })
ownerId!: string;
@ValidateEnum({ enum: MemoryType, name: 'MemoryType' })
@ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' })
type!: MemoryType;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
data!: MemoryData;
@ApiProperty({ description: 'Is memory saved' })
isSaved!: boolean;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
assets!: AssetResponseDto[];
}

View File

@@ -4,11 +4,12 @@ import { IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator';
import { ValidateBoolean } from 'src/validation';
export class TaskConfig {
@ValidateBoolean()
@ValidateBoolean({ description: 'Whether the task is enabled' })
enabled!: boolean;
}
export class ModelConfig extends TaskConfig {
@ApiProperty({ description: 'Name of the model to use' })
@IsString()
@IsNotEmpty()
modelName!: string;
@@ -21,7 +22,11 @@ export class DuplicateDetectionConfig extends TaskConfig {
@Min(0.001)
@Max(0.1)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({
type: 'number',
format: 'double',
description: 'Maximum distance threshold for duplicate detection',
})
maxDistance!: number;
}
@@ -30,20 +35,24 @@ export class FacialRecognitionConfig extends ModelConfig {
@Min(0.1)
@Max(1)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for face detection' })
minScore!: number;
@IsNumber()
@Min(0.1)
@Max(2)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({
type: 'number',
format: 'double',
description: 'Maximum distance threshold for face recognition',
})
maxDistance!: number;
@IsNumber()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Minimum number of faces required for recognition' })
minFaces!: number;
}
@@ -51,20 +60,24 @@ export class OcrConfig extends ModelConfig {
@IsNumber()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Maximum resolution for OCR processing' })
maxResolution!: number;
@IsNumber()
@Min(0.1)
@Max(1)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for text detection' })
minDetectionScore!: number;
@IsNumber()
@Min(0.1)
@Max(1)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({
type: 'number',
format: 'double',
description: 'Minimum confidence score for text recognition',
})
minRecognitionScore!: number;
}

View File

@@ -1,86 +1,115 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { NotificationLevel, NotificationType } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
export class TestEmailResponseDto {
@ApiProperty({ description: 'Email message ID' })
messageId!: string;
}
export class TemplateResponseDto {
@ApiProperty({ description: 'Template name' })
name!: string;
@ApiProperty({ description: 'Template HTML content' })
html!: string;
}
export class TemplateDto {
@ApiProperty({ description: 'Template name' })
@IsString()
template!: string;
}
export class NotificationDto {
@ApiProperty({ description: 'Notification ID' })
id!: string;
@ValidateDate()
@ValidateDate({ description: 'Creation date' })
createdAt!: Date;
@ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel' })
@ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', description: 'Notification level' })
level!: NotificationLevel;
@ValidateEnum({ enum: NotificationType, name: 'NotificationType' })
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', description: 'Notification type' })
type!: NotificationType;
@ApiProperty({ description: 'Notification title' })
title!: string;
@ApiPropertyOptional({ description: 'Notification description' })
description?: string;
@ApiPropertyOptional({ description: 'Additional notification data' })
data?: any;
@ApiPropertyOptional({ description: 'Date when notification was read', format: 'date-time' })
readAt?: Date;
}
export class NotificationSearchDto {
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Filter by notification ID' })
id?: string;
@ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true })
@ValidateEnum({
enum: NotificationLevel,
name: 'NotificationLevel',
optional: true,
description: 'Filter by notification level',
})
level?: NotificationLevel;
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true })
@ValidateEnum({
enum: NotificationType,
name: 'NotificationType',
optional: true,
description: 'Filter by notification type',
})
type?: NotificationType;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter by unread status' })
unread?: boolean;
}
export class NotificationCreateDto {
@ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', optional: true })
@ValidateEnum({
enum: NotificationLevel,
name: 'NotificationLevel',
optional: true,
description: 'Notification level',
})
level?: NotificationLevel;
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true })
@ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true, description: 'Notification type' })
type?: NotificationType;
@ApiProperty({ description: 'Notification title' })
@IsString()
title!: string;
@ApiPropertyOptional({ description: 'Notification description' })
@IsString()
@Optional({ nullable: true })
description?: string | null;
@ApiPropertyOptional({ description: 'Additional notification data' })
@Optional({ nullable: true })
data?: any;
@ValidateDate({ optional: true, nullable: true })
@ValidateDate({ optional: true, description: 'Date when notification was read' })
readAt?: Date | null;
@ValidateUUID()
@ValidateUUID({ description: 'User ID to send notification to' })
userId!: string;
}
export class NotificationUpdateDto {
@ValidateDate({ optional: true, nullable: true })
@ValidateDate({ optional: true, description: 'Date when notification was read' })
readAt?: Date | null;
}
export class NotificationUpdateAllDto {
@ValidateUUID({ each: true, optional: true })
@ValidateUUID({ each: true, optional: true, description: 'Notification IDs to update' })
ids!: string[];
@ValidateDate({ optional: true, nullable: true })
@ValidateDate({ optional: true, description: 'Date when notifications were read' })
readAt?: Date | null;
}
export class NotificationDeleteAllDto {
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'Notification IDs to delete' })
ids!: string[];
}

View File

@@ -1,7 +1,7 @@
import { ValidateBoolean } from 'src/validation';
export class OnboardingDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Is user onboarded' })
isOnboarded!: boolean;
}

View File

@@ -1,23 +1,26 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty } from 'class-validator';
import { UserResponseDto } from 'src/dtos/user.dto';
import { PartnerDirection } from 'src/repositories/partner.repository';
import { ValidateEnum, ValidateUUID } from 'src/validation';
export class PartnerCreateDto {
@ValidateUUID()
@ValidateUUID({ description: 'User ID to share with' })
sharedWithId!: string;
}
export class PartnerUpdateDto {
@ApiProperty({ description: 'Show partner assets in timeline' })
@IsNotEmpty()
inTimeline!: boolean;
}
export class PartnerSearchDto {
@ValidateEnum({ enum: PartnerDirection, name: 'PartnerDirection' })
@ValidateEnum({ enum: PartnerDirection, name: 'PartnerDirection', description: 'Partner direction' })
direction!: PartnerDirection;
}
export class PartnerResponseDto extends UserResponseDto {
@ApiPropertyOptional({ description: 'Show in timeline' })
inTimeline?: boolean;
}

View File

@@ -23,46 +23,37 @@ import {
} from 'src/validation';
export class PersonCreateDto {
/**
* Person name.
*/
@ApiPropertyOptional({ description: 'Person name' })
@Optional()
@IsString()
name?: string;
/**
* Person date of birth.
* Note: the mobile app cannot currently set the birth date to null.
*/
@ApiProperty({ format: 'date' })
// Note: the mobile app cannot currently set the birth date to null.
@ApiProperty({ format: 'date', description: 'Person date of birth', required: false })
@MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' })
@IsDateStringFormat('yyyy-MM-dd')
@Optional({ nullable: true, emptyToNull: true })
birthDate?: Date | null;
/**
* Person visibility
*/
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Person visibility (hidden)' })
isHidden?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Mark as favorite' })
isFavorite?: boolean;
@ApiPropertyOptional({ description: 'Person color (hex)' })
@Optional({ emptyToNull: true, nullable: true })
@ValidateHexColor()
color?: string | null;
}
export class PersonUpdateDto extends PersonCreateDto {
/**
* Asset is used to get the feature face thumbnail.
*/
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Asset ID used for feature face thumbnail' })
featureFaceAssetId?: string;
}
export class PeopleUpdateDto {
@ApiProperty({ description: 'People to update' })
@IsArray()
@ValidateNested({ each: true })
@Type(() => PeopleUpdateItem)
@@ -70,36 +61,32 @@ export class PeopleUpdateDto {
}
export class PeopleUpdateItem extends PersonUpdateDto {
/**
* Person id.
*/
@ApiProperty({ description: 'Person ID' })
@IsString()
@IsNotEmpty()
id!: string;
}
export class MergePersonDto {
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'Person IDs to merge' })
ids!: string[];
}
export class PersonSearchDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Include hidden people' })
withHidden?: boolean;
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Closest person ID for similarity search' })
closestPersonId?: string;
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Closest asset ID for similarity search' })
closestAssetId?: string;
/** Page number for pagination */
@ApiPropertyOptional()
@ApiPropertyOptional({ description: 'Page number for pagination', default: 1 })
@IsInt()
@Min(1)
@Type(() => Number)
page: number = 1;
/** Number of items per page */
@ApiPropertyOptional()
@ApiPropertyOptional({ description: 'Number of items per page', default: 500 })
@IsInt()
@Min(1)
@Max(1000)
@@ -108,48 +95,55 @@ export class PersonSearchDto {
}
export class PersonResponseDto {
@ApiProperty({ description: 'Person ID' })
id!: string;
@ApiProperty({ description: 'Person name' })
name!: string;
@ApiProperty({ format: 'date' })
@ApiProperty({ format: 'date', description: 'Person date of birth' })
birthDate!: string | null;
@ApiProperty({ description: 'Thumbnail path' })
thumbnailPath!: string;
@ApiProperty({ description: 'Is hidden' })
isHidden!: boolean;
@Property({ history: new HistoryBuilder().added('v1.107.0').stable('v2') })
@Property({ description: 'Last update date', history: new HistoryBuilder().added('v1.107.0').stable('v2') })
updatedAt?: Date;
@Property({ history: new HistoryBuilder().added('v1.126.0').stable('v2') })
@Property({ description: 'Is favorite', history: new HistoryBuilder().added('v1.126.0').stable('v2') })
isFavorite?: boolean;
@Property({ history: new HistoryBuilder().added('v1.126.0').stable('v2') })
@Property({ description: 'Person color (hex)', history: new HistoryBuilder().added('v1.126.0').stable('v2') })
color?: string;
}
export class PersonWithFacesResponseDto extends PersonResponseDto {
@ApiProperty({ description: 'Face detections' })
faces!: AssetFaceWithoutPersonResponseDto[];
}
export class AssetFaceWithoutPersonResponseDto {
@ValidateUUID()
@ValidateUUID({ description: 'Face ID' })
id!: string;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Image height in pixels' })
imageHeight!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Image width in pixels' })
imageWidth!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Bounding box X1 coordinate' })
boundingBoxX1!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Bounding box X2 coordinate' })
boundingBoxX2!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Bounding box Y1 coordinate' })
boundingBoxY1!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Bounding box Y2 coordinate' })
boundingBoxY2!: number;
@ValidateEnum({ enum: SourceType, name: 'SourceType' })
@ValidateEnum({ enum: SourceType, name: 'SourceType', optional: true, description: 'Face detection source type' })
sourceType?: SourceType;
}
export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto {
@ApiProperty({ description: 'Person associated with face' })
person!: PersonResponseDto | null;
}
export class AssetFaceUpdateDto {
@ApiProperty({ description: 'Face update items' })
@IsArray()
@ValidateNested({ each: true })
@Type(() => AssetFaceUpdateItem)
@@ -157,69 +151,74 @@ export class AssetFaceUpdateDto {
}
export class FaceDto {
@ValidateUUID()
@ValidateUUID({ description: 'Face ID' })
id!: string;
}
export class AssetFaceUpdateItem {
@ValidateUUID()
@ValidateUUID({ description: 'Person ID' })
personId!: string;
@ValidateUUID()
@ValidateUUID({ description: 'Asset ID' })
assetId!: string;
}
export class AssetFaceCreateDto extends AssetFaceUpdateItem {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Image width in pixels' })
@IsNotEmpty()
@IsNumber()
imageWidth!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Image height in pixels' })
@IsNotEmpty()
@IsNumber()
imageHeight!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Face bounding box X coordinate' })
@IsNotEmpty()
@IsNumber()
x!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Face bounding box Y coordinate' })
@IsNotEmpty()
@IsNumber()
y!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Face bounding box width' })
@IsNotEmpty()
@IsNumber()
width!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Face bounding box height' })
@IsNotEmpty()
@IsNumber()
height!: number;
}
export class AssetFaceDeleteDto {
@ApiProperty({ description: 'Force delete even if person has other faces' })
@IsNotEmpty()
force!: boolean;
}
export class PersonStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of assets' })
assets!: number;
}
export class PeopleResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Total number of people' })
total!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of hidden people' })
hidden!: number;
@ApiProperty({ description: 'List of people' })
people!: PersonResponseDto[];
// TODO: make required after a few versions
@Property({ history: new HistoryBuilder().added('v1.110.0').stable('v2') })
@Property({
description: 'Whether there are more pages',
history: new HistoryBuilder().added('v1.110.0').stable('v2'),
})
hasNextPage?: boolean;
}

View File

@@ -1,3 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import {
ArrayMinSize,
@@ -16,58 +17,68 @@ import { JSONSchema } from 'src/types/plugin-schema.types';
import { ValidateEnum } from 'src/validation';
class PluginManifestWasmDto {
@ApiProperty({ description: 'WASM file path' })
@IsString()
@IsNotEmpty()
path!: string;
}
class PluginManifestFilterDto {
@ApiProperty({ description: 'Filter method name' })
@IsString()
@IsNotEmpty()
methodName!: string;
@ApiProperty({ description: 'Filter title' })
@IsString()
@IsNotEmpty()
title!: string;
@ApiProperty({ description: 'Filter description' })
@IsString()
@IsNotEmpty()
description!: string;
@ApiProperty({ description: 'Supported contexts', enum: PluginContext, isArray: true })
@IsArray()
@ArrayMinSize(1)
@IsEnum(PluginContext, { each: true })
supportedContexts!: PluginContext[];
@ApiPropertyOptional({ description: 'Filter schema' })
@IsObject()
@IsOptional()
schema?: JSONSchema;
}
class PluginManifestActionDto {
@ApiProperty({ description: 'Action method name' })
@IsString()
@IsNotEmpty()
methodName!: string;
@ApiProperty({ description: 'Action title' })
@IsString()
@IsNotEmpty()
title!: string;
@ApiProperty({ description: 'Action description' })
@IsString()
@IsNotEmpty()
description!: string;
@IsArray()
@ArrayMinSize(1)
@ValidateEnum({ enum: PluginContext, name: 'PluginContext', each: true })
@ValidateEnum({ enum: PluginContext, name: 'PluginContext', each: true, description: 'Supported contexts' })
supportedContexts!: PluginContext[];
@ApiPropertyOptional({ description: 'Action schema' })
@IsObject()
@IsOptional()
schema?: JSONSchema;
}
export class PluginManifestDto {
@ApiProperty({ description: 'Plugin name (lowercase, numbers, hyphens only)' })
@IsString()
@IsNotEmpty()
@Matches(/^[a-z0-9-]+[a-z0-9]$/, {
@@ -75,33 +86,40 @@ export class PluginManifestDto {
})
name!: string;
@ApiProperty({ description: 'Plugin version (semver)' })
@IsString()
@IsNotEmpty()
@IsSemVer()
version!: string;
@ApiProperty({ description: 'Plugin title' })
@IsString()
@IsNotEmpty()
title!: string;
@ApiProperty({ description: 'Plugin description' })
@IsString()
@IsNotEmpty()
description!: string;
@ApiProperty({ description: 'Plugin author' })
@IsString()
@IsNotEmpty()
author!: string;
@ApiProperty({ description: 'WASM configuration' })
@ValidateNested()
@Type(() => PluginManifestWasmDto)
wasm!: PluginManifestWasmDto;
@ApiPropertyOptional({ description: 'Plugin filters' })
@IsArray()
@ValidateNested({ each: true })
@Type(() => PluginManifestFilterDto)
@IsOptional()
filters?: PluginManifestFilterDto[];
@ApiPropertyOptional({ description: 'Plugin actions' })
@IsArray()
@ValidateNested({ each: true })
@Type(() => PluginManifestActionDto)

View File

@@ -1,3 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsNotEmpty, IsString } from 'class-validator';
import { PluginAction, PluginFilter } from 'src/database';
import { PluginContext as PluginContextType, PluginTriggerType } from 'src/enum';
@@ -5,50 +6,73 @@ import type { JSONSchema } from 'src/types/plugin-schema.types';
import { ValidateEnum } from 'src/validation';
export class PluginTriggerResponseDto {
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' })
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Trigger type' })
type!: PluginTriggerType;
@ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' })
@ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', description: 'Context type' })
contextType!: PluginContextType;
}
export class PluginResponseDto {
@ApiProperty({ description: 'Plugin ID' })
id!: string;
@ApiProperty({ description: 'Plugin name' })
name!: string;
@ApiProperty({ description: 'Plugin title' })
title!: string;
@ApiProperty({ description: 'Plugin description' })
description!: string;
@ApiProperty({ description: 'Plugin author' })
author!: string;
@ApiProperty({ description: 'Plugin version' })
version!: string;
@ApiProperty({ description: 'Creation date' })
createdAt!: string;
@ApiProperty({ description: 'Last update date' })
updatedAt!: string;
@ApiProperty({ description: 'Plugin filters' })
filters!: PluginFilterResponseDto[];
@ApiProperty({ description: 'Plugin actions' })
actions!: PluginActionResponseDto[];
}
export class PluginFilterResponseDto {
@ApiProperty({ description: 'Filter ID' })
id!: string;
@ApiProperty({ description: 'Plugin ID' })
pluginId!: string;
@ApiProperty({ description: 'Method name' })
methodName!: string;
@ApiProperty({ description: 'Filter title' })
title!: string;
@ApiProperty({ description: 'Filter description' })
description!: string;
@ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' })
@ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' })
supportedContexts!: PluginContextType[];
@ApiProperty({ description: 'Filter schema' })
schema!: JSONSchema | null;
}
export class PluginActionResponseDto {
@ApiProperty({ description: 'Action ID' })
id!: string;
@ApiProperty({ description: 'Plugin ID' })
pluginId!: string;
@ApiProperty({ description: 'Method name' })
methodName!: string;
@ApiProperty({ description: 'Action title' })
title!: string;
@ApiProperty({ description: 'Action description' })
description!: string;
@ValidateEnum({ enum: PluginContextType, name: 'PluginContextType' })
@ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' })
supportedContexts!: PluginContextType[];
@ApiProperty({ description: 'Action schema' })
schema!: JSONSchema | null;
}
export class PluginInstallDto {
@ApiProperty({ description: 'Path to plugin manifest file' })
@IsString()
@IsNotEmpty()
manifestPath!: string;

View File

@@ -3,15 +3,19 @@ import { QueueResponseDto, QueueStatisticsDto } from 'src/dtos/queue.dto';
import { QueueName } from 'src/enum';
export class QueueStatusLegacyDto {
@ApiProperty({ description: 'Whether the queue is currently active (has running jobs)' })
isActive!: boolean;
@ApiProperty({ description: 'Whether the queue is paused' })
isPaused!: boolean;
}
export class QueueResponseLegacyDto {
@ApiProperty({ type: QueueStatusLegacyDto })
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
queueStatus!: QueueStatusLegacyDto;
@ApiProperty({ type: QueueStatisticsDto })
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
jobCounts!: QueueStatisticsDto;
}

View File

@@ -1,29 +1,29 @@
import { ApiProperty } from '@nestjs/swagger';
import { HistoryBuilder, Property } from 'src/decorators';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { HistoryBuilder } from 'src/decorators';
import { JobName, QueueCommand, QueueJobStatus, QueueName } from 'src/enum';
import { ValidateBoolean, ValidateEnum } from 'src/validation';
export class QueueNameParamDto {
@ValidateEnum({ enum: QueueName, name: 'QueueName' })
@ValidateEnum({ enum: QueueName, name: 'QueueName', description: 'Queue name' })
name!: QueueName;
}
export class QueueCommandDto {
@ValidateEnum({ enum: QueueCommand, name: 'QueueCommand' })
@ValidateEnum({ enum: QueueCommand, name: 'QueueCommand', description: 'Queue command to execute' })
command!: QueueCommand;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Force the command execution (if applicable)' })
force?: boolean; // TODO: this uses undefined as a third state, which should be refactored to be more explicit
}
export class QueueUpdateDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether to pause the queue' })
isPaused?: boolean;
}
export class QueueDeleteDto {
@ValidateBoolean({ optional: true })
@Property({
@ValidateBoolean({
optional: true,
description: 'If true, will also remove failed jobs from the queue.',
history: new HistoryBuilder().added('v2.4.0').alpha('v2.4.0'),
})
@@ -31,42 +31,52 @@ export class QueueDeleteDto {
}
export class QueueJobSearchDto {
@ValidateEnum({ enum: QueueJobStatus, name: 'QueueJobStatus', optional: true, each: true })
@ValidateEnum({
enum: QueueJobStatus,
name: 'QueueJobStatus',
optional: true,
each: true,
description: 'Filter jobs by status',
})
status?: QueueJobStatus[];
}
export class QueueJobResponseDto {
@ApiPropertyOptional({ description: 'Job ID' })
id?: string;
@ValidateEnum({ enum: JobName, name: 'JobName' })
@ValidateEnum({ enum: JobName, name: 'JobName', description: 'Job name' })
name!: JobName;
@ApiProperty({ description: 'Job data payload', type: Object })
data!: object;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Job creation timestamp' })
timestamp!: number;
}
export class QueueResponseDto {
@ValidateEnum({ enum: QueueName, name: 'QueueName' })
name!: QueueName;
@ValidateBoolean()
isPaused!: boolean;
statistics!: QueueStatisticsDto;
}
export class QueueStatisticsDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of active jobs' })
active!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of completed jobs' })
completed!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of failed jobs' })
failed!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of delayed jobs' })
delayed!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of waiting jobs' })
waiting!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of paused jobs' })
paused!: number;
}
export class QueueResponseDto {
@ValidateEnum({ enum: QueueName, name: 'QueueName', description: 'Queue name' })
name!: QueueName;
@ValidateBoolean({ description: 'Whether the queue is paused' })
isPaused!: boolean;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
statistics!: QueueStatisticsDto;
}

View File

@@ -1,107 +1,116 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
import { Place } from 'src/database';
import { HistoryBuilder, Property } from 'src/decorators';
import { HistoryBuilder } from 'src/decorators';
import { AlbumResponseDto } from 'src/dtos/album.dto';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AssetOrder, AssetType, AssetVisibility } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation';
class BaseSearchDto {
@ValidateUUID({ optional: true, nullable: true })
@ValidateUUID({ optional: true, nullable: true, description: 'Library ID to filter by' })
libraryId?: string | null;
@ApiPropertyOptional({ description: 'Device ID to filter by' })
@IsString()
@IsNotEmpty()
@Optional()
deviceId?: string;
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true })
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true, description: 'Asset type filter' })
type?: AssetType;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter by encoded status' })
isEncoded?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter by favorite status' })
isFavorite?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter by motion photo status' })
isMotion?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter by offline status' })
isOffline?: boolean;
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true, description: 'Filter by visibility' })
visibility?: AssetVisibility;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter by creation date (before)' })
createdBefore?: Date;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter by creation date (after)' })
createdAfter?: Date;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter by update date (before)' })
updatedBefore?: Date;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter by update date (after)' })
updatedAfter?: Date;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter by trash date (before)' })
trashedBefore?: Date;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter by trash date (after)' })
trashedAfter?: Date;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter by taken date (before)' })
takenBefore?: Date;
@ValidateDate({ optional: true })
@ValidateDate({ optional: true, description: 'Filter by taken date (after)' })
takenAfter?: Date;
@ApiPropertyOptional({ description: 'Filter by city name' })
@IsString()
@Optional({ nullable: true, emptyToNull: true })
city?: string | null;
@ApiPropertyOptional({ description: 'Filter by state/province name' })
@IsString()
@Optional({ nullable: true, emptyToNull: true })
state?: string | null;
@ApiPropertyOptional({ description: 'Filter by country name' })
@IsString()
@IsNotEmpty()
@Optional({ nullable: true, emptyToNull: true })
country?: string | null;
@ApiPropertyOptional({ description: 'Filter by camera make' })
@IsString()
@Optional({ nullable: true, emptyToNull: true })
make?: string;
@ApiPropertyOptional({ description: 'Filter by camera model' })
@IsString()
@Optional({ nullable: true, emptyToNull: true })
model?: string | null;
@ApiPropertyOptional({ description: 'Filter by lens model' })
@IsString()
@Optional({ nullable: true, emptyToNull: true })
lensModel?: string | null;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Filter assets not in any album' })
isNotInAlbum?: boolean;
@ValidateUUID({ each: true, optional: true })
@ValidateUUID({ each: true, optional: true, description: 'Filter by person IDs' })
personIds?: string[];
@ValidateUUID({ each: true, optional: true, nullable: true })
@ValidateUUID({ each: true, optional: true, description: 'Filter by tag IDs' })
tagIds?: string[] | null;
@ValidateUUID({ each: true, optional: true })
@ValidateUUID({ each: true, optional: true, description: 'Filter by album IDs' })
albumIds?: string[];
@ApiPropertyOptional({ type: 'number', description: 'Filter by rating', minimum: -1, maximum: 5 })
@Optional()
@IsInt()
@Max(5)
@Min(-1)
rating?: number;
@ApiPropertyOptional({ description: 'Filter by OCR text content' })
@IsString()
@IsNotEmpty()
@Optional()
@@ -109,12 +118,13 @@ class BaseSearchDto {
}
class BaseSearchWithResultsDto extends BaseSearchDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Include deleted assets' })
withDeleted?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Include EXIF data in response' })
withExif?: boolean;
@ApiPropertyOptional({ type: 'number', description: 'Number of results to return', minimum: 1, maximum: 1000 })
@IsInt()
@Min(1)
@Max(1000)
@@ -124,65 +134,78 @@ class BaseSearchWithResultsDto extends BaseSearchDto {
}
export class RandomSearchDto extends BaseSearchWithResultsDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Include stacked assets' })
withStacked?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Include assets with people' })
withPeople?: boolean;
}
export class LargeAssetSearchDto extends BaseSearchWithResultsDto {
@ApiPropertyOptional({ type: 'integer', description: 'Minimum file size in bytes', minimum: 0 })
@Optional()
@IsInt()
@Min(0)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
minFileSize?: number;
}
export class MetadataSearchDto extends RandomSearchDto {
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Filter by asset ID' })
id?: string;
@ApiPropertyOptional({ description: 'Filter by device asset ID' })
@IsString()
@IsNotEmpty()
@Optional()
deviceAssetId?: string;
@ValidateString({ optional: true, trim: true })
@ValidateString({ optional: true, trim: true, description: 'Filter by description text' })
description?: string;
@ApiPropertyOptional({ description: 'Filter by file checksum' })
@IsString()
@IsNotEmpty()
@Optional()
checksum?: string;
@ValidateString({ optional: true, trim: true })
@ValidateString({ optional: true, trim: true, description: 'Filter by original file name' })
originalFileName?: string;
@ApiPropertyOptional({ description: 'Filter by original file path' })
@IsString()
@IsNotEmpty()
@Optional()
originalPath?: string;
@ApiPropertyOptional({ description: 'Filter by preview file path' })
@IsString()
@IsNotEmpty()
@Optional()
previewPath?: string;
@ApiPropertyOptional({ description: 'Filter by thumbnail file path' })
@IsString()
@IsNotEmpty()
@Optional()
thumbnailPath?: string;
@ApiPropertyOptional({ description: 'Filter by encoded video file path' })
@IsString()
@IsNotEmpty()
@Optional()
encodedVideoPath?: string;
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, default: AssetOrder.Desc })
@ValidateEnum({
enum: AssetOrder,
name: 'AssetOrder',
optional: true,
default: AssetOrder.Desc,
description: 'Sort order',
})
order?: AssetOrder;
@ApiPropertyOptional({ type: 'number', description: 'Page number', minimum: 1 })
@IsInt()
@Min(1)
@Type(() => Number)
@@ -191,23 +214,24 @@ export class MetadataSearchDto extends RandomSearchDto {
}
export class StatisticsSearchDto extends BaseSearchDto {
@ValidateString({ optional: true, trim: true })
@ValidateString({ optional: true, trim: true, description: 'Filter by description text' })
description?: string;
}
export class SmartSearchDto extends BaseSearchWithResultsDto {
@ValidateString({ optional: true, trim: true })
@ValidateString({ optional: true, trim: true, description: 'Natural language search query' })
query?: string;
@ValidateUUID({ optional: true })
@Optional()
@ValidateUUID({ optional: true, description: 'Asset ID to use as search reference' })
queryAssetId?: string;
@ApiPropertyOptional({ description: 'Search language code' })
@IsString()
@IsNotEmpty()
@Optional()
language?: string;
@ApiPropertyOptional({ type: 'number', description: 'Page number', minimum: 1 })
@IsInt()
@Min(1)
@Type(() => Number)
@@ -216,25 +240,32 @@ export class SmartSearchDto extends BaseSearchWithResultsDto {
}
export class SearchPlacesDto {
@ApiProperty({ description: 'Place name to search for' })
@IsString()
@IsNotEmpty()
name!: string;
}
export class SearchPeopleDto {
@ApiProperty({ description: 'Person name to search for' })
@IsString()
@IsNotEmpty()
name!: string;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Include hidden people' })
withHidden?: boolean;
}
export class PlacesResponseDto {
@ApiProperty({ description: 'Place name' })
name!: string;
@ApiProperty({ type: 'number', description: 'Latitude coordinate' })
latitude!: number;
@ApiProperty({ type: 'number', description: 'Longitude coordinate' })
longitude!: number;
@ApiPropertyOptional({ description: 'Administrative level 1 name (state/province)' })
admin1name?: string;
@ApiPropertyOptional({ description: 'Administrative level 2 name (county/district)' })
admin2name?: string;
}
@@ -258,96 +289,126 @@ export enum SearchSuggestionType {
}
export class SearchSuggestionRequestDto {
@ValidateEnum({ enum: SearchSuggestionType, name: 'SearchSuggestionType' })
@ValidateEnum({ enum: SearchSuggestionType, name: 'SearchSuggestionType', description: 'Suggestion type' })
type!: SearchSuggestionType;
@ApiPropertyOptional({ description: 'Filter by country' })
@IsString()
@Optional()
country?: string;
@ApiPropertyOptional({ description: 'Filter by state/province' })
@IsString()
@Optional()
state?: string;
@ApiPropertyOptional({ description: 'Filter by camera make' })
@IsString()
@Optional()
make?: string;
@ApiPropertyOptional({ description: 'Filter by camera model' })
@IsString()
@Optional()
model?: string;
@ApiPropertyOptional({ description: 'Filter by lens model' })
@IsString()
@Optional()
lensModel?: string;
@ValidateBoolean({ optional: true })
@Property({ history: new HistoryBuilder().added('v1.111.0').stable('v2') })
@ValidateBoolean({
optional: true,
description: 'Include null values in suggestions',
history: new HistoryBuilder().added('v1.111.0').stable('v2'),
})
includeNull?: boolean;
}
class SearchFacetCountResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of assets with this facet value' })
count!: number;
@ApiProperty({ description: 'Facet value' })
value!: string;
}
class SearchFacetResponseDto {
@ApiProperty({ description: 'Facet field name' })
fieldName!: string;
@ApiProperty({ description: 'Facet counts' })
counts!: SearchFacetCountResponseDto[];
}
class SearchAlbumResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Total number of matching albums' })
total!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of albums in this page' })
count!: number;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
items!: AlbumResponseDto[];
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
facets!: SearchFacetResponseDto[];
}
class SearchAssetResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Total number of matching assets' })
total!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of assets in this page' })
count!: number;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
items!: AssetResponseDto[];
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
facets!: SearchFacetResponseDto[];
@ApiProperty({ description: 'Next page token' })
nextPage!: string | null;
}
export class SearchResponseDto {
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
albums!: SearchAlbumResponseDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
assets!: SearchAssetResponseDto;
}
export class SearchStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Total number of matching assets' })
total!: number;
}
class SearchExploreItem {
@ApiProperty({ description: 'Explore value' })
value!: string;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
data!: AssetResponseDto;
}
export class SearchExploreResponseDto {
@ApiProperty({ description: 'Explore field name' })
fieldName!: string;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
items!: SearchExploreItem[];
}
export class MemoryLaneDto {
@ApiProperty({ type: 'integer', description: 'Day of month' })
@IsInt()
@Type(() => Number)
@Max(31)
@Min(1)
@ApiProperty({ type: 'integer' })
day!: number;
@ApiProperty({ type: 'integer', description: 'Month' })
@IsInt()
@Type(() => Number)
@Max(12)
@Min(1)
@ApiProperty({ type: 'integer' })
month!: number;
}

View File

@@ -1,4 +1,4 @@
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional, ApiResponseProperty } from '@nestjs/swagger';
import { SemVer } from 'semver';
import { SystemConfigThemeDto } from 'src/dtos/system-config.dto';
@@ -8,66 +8,94 @@ export class ServerPingResponse {
}
export class ServerAboutResponseDto {
@ApiProperty({ description: 'Server version' })
version!: string;
@ApiProperty({ description: 'URL to version information' })
versionUrl!: string;
@ApiPropertyOptional({ description: 'Repository name' })
repository?: string;
@ApiPropertyOptional({ description: 'Repository URL' })
repositoryUrl?: string;
@ApiPropertyOptional({ description: 'Source reference (branch/tag)' })
sourceRef?: string;
@ApiPropertyOptional({ description: 'Source commit hash' })
sourceCommit?: string;
@ApiPropertyOptional({ description: 'Source URL' })
sourceUrl?: string;
@ApiPropertyOptional({ description: 'Build identifier' })
build?: string;
@ApiPropertyOptional({ description: 'Build URL' })
buildUrl?: string;
@ApiPropertyOptional({ description: 'Build image name' })
buildImage?: string;
@ApiPropertyOptional({ description: 'Build image URL' })
buildImageUrl?: string;
@ApiPropertyOptional({ description: 'Node.js version' })
nodejs?: string;
@ApiPropertyOptional({ description: 'FFmpeg version' })
ffmpeg?: string;
@ApiPropertyOptional({ description: 'ImageMagick version' })
imagemagick?: string;
@ApiPropertyOptional({ description: 'libvips version' })
libvips?: string;
@ApiPropertyOptional({ description: 'ExifTool version' })
exiftool?: string;
@ApiProperty({ description: 'Whether the server is licensed' })
licensed!: boolean;
@ApiPropertyOptional({ description: 'Third-party source URL' })
thirdPartySourceUrl?: string;
@ApiPropertyOptional({ description: 'Third-party bug/feature URL' })
thirdPartyBugFeatureUrl?: string;
@ApiPropertyOptional({ description: 'Third-party documentation URL' })
thirdPartyDocumentationUrl?: string;
@ApiPropertyOptional({ description: 'Third-party support URL' })
thirdPartySupportUrl?: string;
}
export class ServerApkLinksDto {
@ApiProperty({ description: 'APK download link for ARM64 v8a architecture' })
arm64v8a!: string;
@ApiProperty({ description: 'APK download link for ARM EABI v7a architecture' })
armeabiv7a!: string;
@ApiProperty({ description: 'APK download link for universal architecture' })
universal!: string;
@ApiProperty({ description: 'APK download link for x86_64 architecture' })
x86_64!: string;
}
export class ServerStorageResponseDto {
@ApiProperty({ description: 'Total disk size (human-readable format)' })
diskSize!: string;
@ApiProperty({ description: 'Used disk space (human-readable format)' })
diskUse!: string;
@ApiProperty({ description: 'Available disk space (human-readable format)' })
diskAvailable!: string;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Total disk size in bytes' })
diskSizeRaw!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Used disk space in bytes' })
diskUseRaw!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Available disk space in bytes' })
diskAvailableRaw!: number;
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({ type: 'number', format: 'double', description: 'Disk usage percentage (0-100)' })
diskUsagePercentage!: number;
}
export class ServerVersionResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Major version number' })
major!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Minor version number' })
minor!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Patch version number' })
patch!: number;
static fromSemVer(value: SemVer) {
@@ -76,44 +104,52 @@ export class ServerVersionResponseDto {
}
export class ServerVersionHistoryResponseDto {
@ApiProperty({ description: 'Version history entry ID' })
id!: string;
@ApiProperty({ description: 'When this version was first seen', format: 'date-time' })
createdAt!: Date;
@ApiProperty({ description: 'Version string' })
version!: string;
}
export class UsageByUserDto {
@ApiProperty({ type: 'string' })
@ApiProperty({ type: 'string', description: 'User ID' })
userId!: string;
@ApiProperty({ type: 'string' })
@ApiProperty({ type: 'string', description: 'User name' })
userName!: string;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of photos' })
photos!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of videos' })
videos!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Total storage usage in bytes' })
usage!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for photos in bytes' })
usagePhotos!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for videos in bytes' })
usageVideos!: number;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({
type: 'integer',
format: 'int64',
nullable: true,
description: 'User quota size in bytes (null if unlimited)',
})
quotaSizeInBytes!: number | null;
}
export class ServerStatsResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Total number of photos' })
photos = 0;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Total number of videos' })
videos = 0;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Total storage usage in bytes' })
usage = 0;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for photos in bytes' })
usagePhotos = 0;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for videos in bytes' })
usageVideos = 0;
@ApiProperty({
@@ -134,44 +170,71 @@ export class ServerStatsResponseDto {
}
export class ServerMediaTypesResponseDto {
@ApiProperty({ description: 'Supported video MIME types' })
video!: string[];
@ApiProperty({ description: 'Supported image MIME types' })
image!: string[];
@ApiProperty({ description: 'Supported sidecar MIME types' })
sidecar!: string[];
}
export class ServerThemeDto extends SystemConfigThemeDto {}
export class ServerConfigDto {
@ApiProperty({ description: 'OAuth button text' })
oauthButtonText!: string;
@ApiProperty({ description: 'Login page message' })
loginPageMessage!: string;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of days before trashed assets are permanently deleted' })
trashDays!: number;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Delay in days before deleted users are permanently removed' })
userDeleteDelay!: number;
@ApiProperty({ description: 'Whether the server has been initialized' })
isInitialized!: boolean;
@ApiProperty({ description: 'Whether the admin has completed onboarding' })
isOnboarded!: boolean;
@ApiProperty({ description: 'External domain URL' })
externalDomain!: string;
@ApiProperty({ description: 'Whether public user registration is enabled' })
publicUsers!: boolean;
@ApiProperty({ description: 'Map dark style URL' })
mapDarkStyleUrl!: string;
@ApiProperty({ description: 'Map light style URL' })
mapLightStyleUrl!: string;
@ApiProperty({ description: 'Whether maintenance mode is active' })
maintenanceMode!: boolean;
}
export class ServerFeaturesDto {
@ApiProperty({ description: 'Whether smart search is enabled' })
smartSearch!: boolean;
@ApiProperty({ description: 'Whether duplicate detection is enabled' })
duplicateDetection!: boolean;
@ApiProperty({ description: 'Whether config file is available' })
configFile!: boolean;
@ApiProperty({ description: 'Whether facial recognition is enabled' })
facialRecognition!: boolean;
@ApiProperty({ description: 'Whether map feature is enabled' })
map!: boolean;
@ApiProperty({ description: 'Whether trash feature is enabled' })
trash!: boolean;
@ApiProperty({ description: 'Whether reverse geocoding is enabled' })
reverseGeocoding!: boolean;
@ApiProperty({ description: 'Whether face import is enabled' })
importFaces!: boolean;
@ApiProperty({ description: 'Whether OAuth is enabled' })
oauth!: boolean;
@ApiProperty({ description: 'Whether OAuth auto-launch is enabled' })
oauthAutoLaunch!: boolean;
@ApiProperty({ description: 'Whether password login is enabled' })
passwordLogin!: boolean;
@ApiProperty({ description: 'Whether sidecar files are supported' })
sidecar!: boolean;
@ApiProperty({ description: 'Whether search is enabled' })
search!: boolean;
@ApiProperty({ description: 'Whether email notifications are enabled' })
email!: boolean;
@ApiProperty({ description: 'Whether OCR is enabled' })
ocr!: boolean;
}

View File

@@ -1,44 +1,55 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Equals, IsInt, IsPositive, IsString } from 'class-validator';
import { Session } from 'src/database';
import { Optional, ValidateBoolean } from 'src/validation';
export class SessionCreateDto {
/**
* session duration, in seconds
*/
@ApiPropertyOptional({ type: 'number', description: 'Session duration in seconds' })
@IsInt()
@IsPositive()
@Optional()
duration?: number;
@ApiPropertyOptional({ description: 'Device type' })
@IsString()
@Optional()
deviceType?: string;
@ApiPropertyOptional({ description: 'Device OS' })
@IsString()
@Optional()
deviceOS?: string;
}
export class SessionUpdateDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Reset pending sync state' })
@Equals(true)
isPendingSyncReset?: true;
}
export class SessionResponseDto {
@ApiProperty({ description: 'Session ID' })
id!: string;
@ApiProperty({ description: 'Creation date' })
createdAt!: string;
@ApiProperty({ description: 'Last update date' })
updatedAt!: string;
@ApiPropertyOptional({ description: 'Expiration date' })
expiresAt?: string;
@ApiProperty({ description: 'Is current session' })
current!: boolean;
@ApiProperty({ description: 'Device type' })
deviceType!: string;
@ApiProperty({ description: 'Device OS' })
deviceOS!: string;
@ApiProperty({ description: 'App version' })
appVersion!: string | null;
@ApiProperty({ description: 'Is pending sync reset' })
isPendingSyncReset!: boolean;
}
export class SessionCreateResponseDto extends SessionResponseDto {
@ApiProperty({ description: 'Session token' })
token!: string;
}

View File

@@ -1,119 +1,145 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsString } from 'class-validator';
import { SharedLink } from 'src/database';
import { HistoryBuilder, Property } from 'src/decorators';
import { HistoryBuilder } from 'src/decorators';
import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { SharedLinkType } from 'src/enum';
import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
export class SharedLinkSearchDto {
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Filter by album ID' })
albumId?: string;
@ValidateUUID({ optional: true })
@Property({ history: new HistoryBuilder().added('v2.5.0') })
@ValidateUUID({
optional: true,
description: 'Filter by shared link ID',
history: new HistoryBuilder().added('v2.5.0'),
})
id?: string;
}
export class SharedLinkCreateDto {
@ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' })
@ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType', description: 'Shared link type' })
type!: SharedLinkType;
@ValidateUUID({ each: true, optional: true })
@ValidateUUID({ each: true, optional: true, description: 'Asset IDs (for individual assets)' })
assetIds?: string[];
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Album ID (for album sharing)' })
albumId?: string;
@ApiPropertyOptional({ description: 'Link description' })
@Optional({ nullable: true, emptyToNull: true })
@IsString()
description?: string | null;
@ApiPropertyOptional({ description: 'Link password' })
@Optional({ nullable: true, emptyToNull: true })
@IsString()
password?: string | null;
@ApiPropertyOptional({ description: 'Custom URL slug' })
@Optional({ nullable: true, emptyToNull: true })
@IsString()
slug?: string | null;
@ValidateDate({ optional: true, nullable: true })
@ValidateDate({ optional: true, description: 'Expiration date' })
expiresAt?: Date | null = null;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Allow uploads' })
allowUpload?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Allow downloads', default: true })
allowDownload?: boolean = true;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Show metadata', default: true })
showMetadata?: boolean = true;
}
export class SharedLinkEditDto {
@ApiPropertyOptional({ description: 'Link description' })
@Optional({ nullable: true, emptyToNull: true })
@IsString()
description?: string | null;
@ApiPropertyOptional({ description: 'Link password' })
@Optional({ nullable: true, emptyToNull: true })
@IsString()
password?: string | null;
@ApiPropertyOptional({ description: 'Custom URL slug' })
@Optional({ nullable: true, emptyToNull: true })
@IsString()
slug?: string | null;
@ApiPropertyOptional({ description: 'Expiration date' })
@Optional({ nullable: true })
expiresAt?: Date | null;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Allow uploads' })
allowUpload?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Allow downloads' })
allowDownload?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Show metadata' })
showMetadata?: boolean;
/**
* Few clients cannot send null to set the expiryTime to never.
* Setting this flag and not sending expiryAt is considered as null instead.
* Clients that can send null values can ignore this.
*/
@ValidateBoolean({ optional: true })
@ValidateBoolean({
optional: true,
description:
'Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.',
})
changeExpiryTime?: boolean;
}
export class SharedLinkPasswordDto {
@ApiPropertyOptional({ example: 'password', description: 'Link password' })
@IsString()
@Optional()
@ApiProperty({ example: 'password' })
password?: string;
@ApiPropertyOptional({ description: 'Access token' })
@IsString()
@Optional()
token?: string;
}
export class SharedLinkResponseDto {
@ApiProperty({ description: 'Shared link ID' })
id!: string;
@ApiProperty({ description: 'Link description' })
description!: string | null;
@ApiProperty({ description: 'Has password' })
password!: string | null;
@ApiPropertyOptional({ description: 'Access token' })
token?: string | null;
@ApiProperty({ description: 'Owner user ID' })
userId!: string;
@ApiProperty({ description: 'Encryption key (base64url)' })
key!: string;
@ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType' })
@ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType', description: 'Shared link type' })
type!: SharedLinkType;
@ApiProperty({ description: 'Creation date' })
createdAt!: Date;
@ApiProperty({ description: 'Expiration date' })
expiresAt!: Date | null;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
assets!: AssetResponseDto[];
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
album?: AlbumResponseDto;
@ApiProperty({ description: 'Allow uploads' })
allowUpload!: boolean;
@ApiProperty({ description: 'Allow downloads' })
allowDownload!: boolean;
@ApiProperty({ description: 'Show metadata' })
showMetadata!: boolean;
@ApiProperty({ description: 'Custom URL slug' })
slug!: string | null;
}

View File

@@ -1,3 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ArrayMinSize } from 'class-validator';
import { Stack } from 'src/database';
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
@@ -5,25 +6,27 @@ import { AuthDto } from 'src/dtos/auth.dto';
import { ValidateUUID } from 'src/validation';
export class StackCreateDto {
/** first asset becomes the primary */
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'Asset IDs (first becomes primary, min 2)' })
@ArrayMinSize(2)
assetIds!: string[];
}
export class StackSearchDto {
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Filter by primary asset ID' })
primaryAssetId?: string;
}
export class StackUpdateDto {
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Primary asset ID' })
primaryAssetId?: string;
}
export class StackResponseDto {
@ApiProperty({ description: 'Stack ID' })
id!: string;
@ApiProperty({ description: 'Primary asset ID' })
primaryAssetId!: string;
@ApiProperty({ description: 'Stack assets' })
assets!: AssetResponseDto[];
}

View File

@@ -17,32 +17,35 @@ import { UserMetadata } from 'src/types';
import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation';
export class AssetFullSyncDto {
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Last asset ID (pagination)' })
lastId?: string;
@ValidateDate()
@ValidateDate({ description: 'Sync assets updated until this date' })
updatedUntil!: Date;
@ApiProperty({ type: 'integer', description: 'Maximum number of assets to return' })
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
limit!: number;
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'Filter by user ID' })
userId?: string;
}
export class AssetDeltaSyncDto {
@ValidateDate()
@ValidateDate({ description: 'Sync assets updated after this date' })
updatedAfter!: Date;
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'User IDs to sync' })
userIds!: string[];
}
export class AssetDeltaSyncResponseDto {
@ApiProperty({ description: 'Whether full sync is needed' })
needsFullSync!: boolean;
@ApiProperty({ description: 'Upserted assets' })
upserted!: AssetResponseDto[];
@ApiProperty({ description: 'Deleted asset IDs' })
deleted!: string[];
}
@@ -57,21 +60,31 @@ export const ExtraModel = (): ClassDecorator => {
@ExtraModel()
export class SyncUserV1 {
@ApiProperty({ description: 'User ID' })
id!: string;
@ApiProperty({ description: 'User name' })
name!: string;
@ApiProperty({ description: 'User email' })
email!: string;
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', nullable: true })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'User avatar color' })
avatarColor!: UserAvatarColor | null;
@ApiProperty({ description: 'User deleted at' })
deletedAt!: Date | null;
@ApiProperty({ description: 'User has profile image' })
hasProfileImage!: boolean;
@ApiProperty({ description: 'User profile changed at' })
profileChangedAt!: Date;
}
@ExtraModel()
export class SyncAuthUserV1 extends SyncUserV1 {
@ApiProperty({ description: 'User is admin' })
isAdmin!: boolean;
@ApiProperty({ description: 'User pin code' })
pinCode!: string | null;
@ApiProperty({ description: 'User OAuth ID' })
oauthId!: string;
@ApiProperty({ description: 'User storage label' })
storageLabel!: string | null;
@ApiProperty({ type: 'integer' })
quotaSizeInBytes!: number | null;
@@ -81,135 +94,189 @@ export class SyncAuthUserV1 extends SyncUserV1 {
@ExtraModel()
export class SyncUserDeleteV1 {
@ApiProperty({ description: 'User ID' })
userId!: string;
}
@ExtraModel()
export class SyncPartnerV1 {
@ApiProperty({ description: 'Shared by ID' })
sharedById!: string;
@ApiProperty({ description: 'Shared with ID' })
sharedWithId!: string;
@ApiProperty({ description: 'In timeline' })
inTimeline!: boolean;
}
@ExtraModel()
export class SyncPartnerDeleteV1 {
@ApiProperty({ description: 'Shared by ID' })
sharedById!: string;
@ApiProperty({ description: 'Shared with ID' })
sharedWithId!: string;
}
@ExtraModel()
export class SyncAssetV1 {
@ApiProperty({ description: 'Asset ID' })
id!: string;
@ApiProperty({ description: 'Owner ID' })
ownerId!: string;
@ApiProperty({ description: 'Original file name' })
originalFileName!: string;
@ApiProperty({ description: 'Thumbhash' })
thumbhash!: string | null;
@ApiProperty({ description: 'Checksum' })
checksum!: string;
@ApiProperty({ description: 'File created at' })
fileCreatedAt!: Date | null;
@ApiProperty({ description: 'File modified at' })
fileModifiedAt!: Date | null;
@ApiProperty({ description: 'Local date time' })
localDateTime!: Date | null;
@ApiProperty({ description: 'Duration' })
duration!: string | null;
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum' })
@ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' })
type!: AssetType;
@ApiProperty({ description: 'Deleted at' })
deletedAt!: Date | null;
@ApiProperty({ description: 'Is favorite' })
isFavorite!: boolean;
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility' })
@ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility' })
visibility!: AssetVisibility;
@ApiProperty({ description: 'Live photo video ID' })
livePhotoVideoId!: string | null;
@ApiProperty({ description: 'Stack ID' })
stackId!: string | null;
@ApiProperty({ description: 'Library ID' })
libraryId!: string | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Asset width' })
width!: number | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Asset height' })
height!: number | null;
@ApiProperty({ type: 'boolean' })
@ApiProperty({ description: 'Is edited' })
isEdited!: boolean;
}
@ExtraModel()
export class SyncAssetDeleteV1 {
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
}
@ExtraModel()
export class SyncAssetExifV1 {
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
@ApiProperty({ description: 'Description' })
description!: string | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Exif image width' })
exifImageWidth!: number | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Exif image height' })
exifImageHeight!: number | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'File size in byte' })
fileSizeInByte!: number | null;
@ApiProperty({ description: 'Orientation' })
orientation!: string | null;
@ApiProperty({ description: 'Date time original' })
dateTimeOriginal!: Date | null;
@ApiProperty({ description: 'Modify date' })
modifyDate!: Date | null;
@ApiProperty({ description: 'Time zone' })
timeZone!: string | null;
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({ type: 'number', format: 'double', description: 'Latitude' })
latitude!: number | null;
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({ type: 'number', format: 'double', description: 'Longitude' })
longitude!: number | null;
@ApiProperty({ description: 'Projection type' })
projectionType!: string | null;
@ApiProperty({ description: 'City' })
city!: string | null;
@ApiProperty({ description: 'State' })
state!: string | null;
@ApiProperty({ description: 'Country' })
country!: string | null;
@ApiProperty({ description: 'Make' })
make!: string | null;
@ApiProperty({ description: 'Model' })
model!: string | null;
@ApiProperty({ description: 'Lens model' })
lensModel!: string | null;
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({ type: 'number', format: 'double', description: 'F number' })
fNumber!: number | null;
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({ type: 'number', format: 'double', description: 'Focal length' })
focalLength!: number | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'ISO' })
iso!: number | null;
@ApiProperty({ description: 'Exposure time' })
exposureTime!: string | null;
@ApiProperty({ description: 'Profile description' })
profileDescription!: string | null;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Rating' })
rating!: number | null;
@ApiProperty({ type: 'number', format: 'double' })
@ApiProperty({ type: 'number', format: 'double', description: 'FPS' })
fps!: number | null;
}
@ExtraModel()
export class SyncAssetMetadataV1 {
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
@ApiProperty({ description: 'Key' })
key!: string;
@ApiProperty({ description: 'Value' })
value!: object;
}
@ExtraModel()
export class SyncAssetMetadataDeleteV1 {
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
@ApiProperty({ description: 'Key' })
key!: string;
}
@ExtraModel()
export class SyncAlbumDeleteV1 {
@ApiProperty({ description: 'Album ID' })
albumId!: string;
}
@ExtraModel()
export class SyncAlbumUserDeleteV1 {
@ApiProperty({ description: 'Album ID' })
albumId!: string;
@ApiProperty({ description: 'User ID' })
userId!: string;
}
@ExtraModel()
export class SyncAlbumUserV1 {
@ApiProperty({ description: 'Album ID' })
albumId!: string;
@ApiProperty({ description: 'User ID' })
userId!: string;
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole' })
@ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' })
role!: AlbumUserRole;
}
@ExtraModel()
export class SyncAlbumV1 {
@ApiProperty({ description: 'Album ID' })
id!: string;
@ApiProperty({ description: 'Owner ID' })
ownerId!: string;
@ApiProperty({ description: 'Album name' })
name!: string;
@ApiProperty({ description: 'Album description' })
description!: string;
@ApiProperty({ description: 'Created at' })
createdAt!: Date;
@ApiProperty({ description: 'Updated at' })
updatedAt!: Date;
@ApiProperty({ description: 'Thumbnail asset ID' })
thumbnailAssetId!: string | null;
@ApiProperty({ description: 'Is activity enabled' })
isActivityEnabled!: boolean;
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' })
order!: AssetOrder;
@@ -217,87 +284,127 @@ export class SyncAlbumV1 {
@ExtraModel()
export class SyncAlbumToAssetV1 {
@ApiProperty({ description: 'Album ID' })
albumId!: string;
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
}
@ExtraModel()
export class SyncAlbumToAssetDeleteV1 {
@ApiProperty({ description: 'Album ID' })
albumId!: string;
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
}
@ExtraModel()
export class SyncMemoryV1 {
@ApiProperty({ description: 'Memory ID' })
id!: string;
@ApiProperty({ description: 'Created at' })
createdAt!: Date;
@ApiProperty({ description: 'Updated at' })
updatedAt!: Date;
@ApiProperty({ description: 'Deleted at' })
deletedAt!: Date | null;
@ApiProperty({ description: 'Owner ID' })
ownerId!: string;
@ValidateEnum({ enum: MemoryType, name: 'MemoryType' })
@ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' })
type!: MemoryType;
@ApiProperty({ description: 'Data' })
data!: object;
@ApiProperty({ description: 'Is saved' })
isSaved!: boolean;
@ApiProperty({ description: 'Memory at' })
memoryAt!: Date;
@ApiProperty({ description: 'Seen at' })
seenAt!: Date | null;
@ApiProperty({ description: 'Show at' })
showAt!: Date | null;
@ApiProperty({ description: 'Hide at' })
hideAt!: Date | null;
}
@ExtraModel()
export class SyncMemoryDeleteV1 {
@ApiProperty({ description: 'Memory ID' })
memoryId!: string;
}
@ExtraModel()
export class SyncMemoryAssetV1 {
@ApiProperty({ description: 'Memory ID' })
memoryId!: string;
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
}
@ExtraModel()
export class SyncMemoryAssetDeleteV1 {
@ApiProperty({ description: 'Memory ID' })
memoryId!: string;
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
}
@ExtraModel()
export class SyncStackV1 {
@ApiProperty({ description: 'Stack ID' })
id!: string;
@ApiProperty({ description: 'Created at' })
createdAt!: Date;
@ApiProperty({ description: 'Updated at' })
updatedAt!: Date;
@ApiProperty({ description: 'Primary asset ID' })
primaryAssetId!: string;
@ApiProperty({ description: 'Owner ID' })
ownerId!: string;
}
@ExtraModel()
export class SyncStackDeleteV1 {
@ApiProperty({ description: 'Stack ID' })
stackId!: string;
}
@ExtraModel()
export class SyncPersonV1 {
@ApiProperty({ description: 'Person ID' })
id!: string;
@ApiProperty({ description: 'Created at' })
createdAt!: Date;
@ApiProperty({ description: 'Updated at' })
updatedAt!: Date;
@ApiProperty({ description: 'Owner ID' })
ownerId!: string;
@ApiProperty({ description: 'Person name' })
name!: string;
@ApiProperty({ description: 'Birth date' })
birthDate!: Date | null;
@ApiProperty({ description: 'Is hidden' })
isHidden!: boolean;
@ApiProperty({ description: 'Is favorite' })
isFavorite!: boolean;
@ApiProperty({ description: 'Color' })
color!: string | null;
@ApiProperty({ description: 'Face asset ID' })
faceAssetId!: string | null;
}
@ExtraModel()
export class SyncPersonDeleteV1 {
@ApiProperty({ description: 'Person ID' })
personId!: string;
}
@ExtraModel()
export class SyncAssetFaceV1 {
@ApiProperty({ description: 'Asset face ID' })
id!: string;
@ApiProperty({ description: 'Asset ID' })
assetId!: string;
@ApiProperty({ description: 'Person ID' })
personId!: string | null;
@ApiProperty({ type: 'integer' })
imageWidth!: number;
@@ -311,26 +418,31 @@ export class SyncAssetFaceV1 {
boundingBoxX2!: number;
@ApiProperty({ type: 'integer' })
boundingBoxY2!: number;
@ApiProperty({ description: 'Source type' })
sourceType!: string;
}
@ExtraModel()
export class SyncAssetFaceDeleteV1 {
@ApiProperty({ description: 'Asset face ID' })
assetFaceId!: string;
}
@ExtraModel()
export class SyncUserMetadataV1 {
@ApiProperty({ description: 'User ID' })
userId!: string;
@ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey' })
@ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey', description: 'User metadata key' })
key!: UserMetadataKey;
@ApiProperty({ description: 'User metadata value' })
value!: UserMetadata[UserMetadataKey];
}
@ExtraModel()
export class SyncUserMetadataDeleteV1 {
@ApiProperty({ description: 'User ID' })
userId!: string;
@ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey' })
@ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey', description: 'User metadata key' })
key!: UserMetadataKey;
}
@@ -394,26 +506,34 @@ export type SyncItem = {
};
export class SyncStreamDto {
@ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true })
@ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true, description: 'Sync request types' })
types!: SyncRequestType[];
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Reset sync state' })
reset?: boolean;
}
export class SyncAckDto {
@ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType' })
@ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', description: 'Sync entity type' })
type!: SyncEntityType;
@ApiProperty({ description: 'Acknowledgment ID' })
ack!: string;
}
export class SyncAckSetDto {
@ApiProperty({ description: 'Acknowledgment IDs (max 1000)' })
@ArrayMaxSize(1000)
@IsString({ each: true })
acks!: string[];
}
export class SyncAckDeleteDto {
@ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', optional: true, each: true })
@ValidateEnum({
enum: SyncEntityType,
name: 'SyncEntityType',
optional: true,
each: true,
description: 'Sync entity types to delete acks for',
})
types?: SyncEntityType[];
}

View File

@@ -40,18 +40,20 @@ const isEmailNotificationEnabled = (config: SystemConfigSmtpDto) => config.enabl
const isDatabaseBackupEnabled = (config: DatabaseBackupConfig) => config.enabled;
export class DatabaseBackupConfig {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
@ValidateIf(isDatabaseBackupEnabled)
@IsNotEmpty()
@IsCronExpression()
@IsString()
@ApiProperty({ description: 'Cron expression' })
cronExpression!: string;
@IsInt()
@IsPositive()
@IsNotEmpty()
@ApiProperty({ description: 'Keep last amount' })
keepLastAmount!: number;
}
@@ -67,171 +69,179 @@ export class SystemConfigFFmpegDto {
@Min(0)
@Max(51)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'CRF' })
crf!: number;
@IsInt()
@Min(0)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Threads' })
threads!: number;
@IsString()
@ApiProperty({ description: 'Preset' })
preset!: string;
@ValidateEnum({ enum: VideoCodec, name: 'VideoCodec' })
@ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', description: 'Target video codec' })
targetVideoCodec!: VideoCodec;
@ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true })
@ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true, description: 'Accepted video codecs' })
acceptedVideoCodecs!: VideoCodec[];
@ValidateEnum({ enum: AudioCodec, name: 'AudioCodec' })
@ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', description: 'Target audio codec' })
targetAudioCodec!: AudioCodec;
@ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true })
@ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true, description: 'Accepted audio codecs' })
acceptedAudioCodecs!: AudioCodec[];
@ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true })
@ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true, description: 'Accepted containers' })
acceptedContainers!: VideoContainer[];
@IsString()
@ApiProperty({ description: 'Target resolution' })
targetResolution!: string;
@IsString()
@ApiProperty({ description: 'Max bitrate' })
maxBitrate!: string;
@IsInt()
@Min(-1)
@Max(16)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'B-frames' })
bframes!: number;
@IsInt()
@Min(0)
@Max(6)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'References' })
refs!: number;
@IsInt()
@Min(0)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'GOP size' })
gopSize!: number;
@ValidateBoolean()
@ValidateBoolean({ description: 'Temporal AQ' })
temporalAQ!: boolean;
@ValidateEnum({ enum: CQMode, name: 'CQMode' })
@ValidateEnum({ enum: CQMode, name: 'CQMode', description: 'CQ mode' })
cqMode!: CQMode;
@ValidateBoolean()
@ValidateBoolean({ description: 'Two pass' })
twoPass!: boolean;
@ApiProperty({ description: 'Preferred hardware device' })
@IsString()
preferredHwDevice!: string;
@ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy' })
@ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy', description: 'Transcode policy' })
transcode!: TranscodePolicy;
@ValidateEnum({ enum: TranscodeHardwareAcceleration, name: 'TranscodeHWAccel' })
@ValidateEnum({
enum: TranscodeHardwareAcceleration,
name: 'TranscodeHWAccel',
description: 'Transcode hardware acceleration',
})
accel!: TranscodeHardwareAcceleration;
@ValidateBoolean()
@ValidateBoolean({ description: 'Accelerated decode' })
accelDecode!: boolean;
@ValidateEnum({ enum: ToneMapping, name: 'ToneMapping' })
@ValidateEnum({ enum: ToneMapping, name: 'ToneMapping', description: 'Tone mapping' })
tonemap!: ToneMapping;
}
class JobSettingsDto {
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Concurrency' })
concurrency!: number;
}
class SystemConfigJobDto implements Record<ConcurrentQueueName, JobSettingsDto> {
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.ThumbnailGeneration]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.MetadataExtraction]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.VideoConversion]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.SmartSearch]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.Migration]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.BackgroundTask]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.Search]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.FaceDetection]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.Ocr]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.Sidecar]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.Library]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.Notification]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
[QueueName.Workflow]!: JobSettingsDto;
@ApiProperty({ type: JobSettingsDto })
@ApiProperty({ type: JobSettingsDto, description: undefined })
@ValidateNested()
@IsObject()
@Type(() => JobSettingsDto)
@@ -239,7 +249,7 @@ class SystemConfigJobDto implements Record<ConcurrentQueueName, JobSettingsDto>
}
class SystemConfigLibraryScanDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
@ValidateIf(isLibraryScanEnabled)
@@ -250,7 +260,7 @@ class SystemConfigLibraryScanDto {
}
class SystemConfigLibraryWatchDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
}
@@ -267,7 +277,7 @@ class SystemConfigLibraryDto {
}
class SystemConfigLoggingDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
@ValidateEnum({ enum: LogLevel, name: 'LogLevel' })
@@ -275,7 +285,7 @@ class SystemConfigLoggingDto {
}
class MachineLearningAvailabilityChecksDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
@IsInt()
@@ -286,7 +296,7 @@ class MachineLearningAvailabilityChecksDto {
}
class SystemConfigMachineLearningDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
@IsUrl({ require_tld: false, allow_underscores: true }, { each: true })
@@ -332,7 +342,7 @@ export class MapThemeDto {
}
class SystemConfigMapDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
@IsNotEmpty()
@@ -345,7 +355,7 @@ class SystemConfigMapDto {
}
class SystemConfigNewVersionCheckDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
}
@@ -353,72 +363,82 @@ class SystemConfigNightlyTasksDto {
@IsDateStringFormat('HH:mm', { message: 'startTime must be in HH:mm format' })
startTime!: string;
@ValidateBoolean()
@ValidateBoolean({ description: 'Database cleanup' })
databaseCleanup!: boolean;
@ValidateBoolean()
@ValidateBoolean({ description: 'Missing thumbnails' })
missingThumbnails!: boolean;
@ValidateBoolean()
@ValidateBoolean({ description: 'Cluster new faces' })
clusterNewFaces!: boolean;
@ValidateBoolean()
@ValidateBoolean({ description: 'Generate memories' })
generateMemories!: boolean;
@ValidateBoolean()
@ValidateBoolean({ description: 'Sync quota usage' })
syncQuotaUsage!: boolean;
}
class SystemConfigOAuthDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Auto launch' })
autoLaunch!: boolean;
@ValidateBoolean()
@ValidateBoolean({ description: 'Auto register' })
autoRegister!: boolean;
@IsString()
@ApiProperty({ description: 'Button text' })
buttonText!: string;
@ValidateIf(isOAuthEnabled)
@IsNotEmpty()
@IsString()
@ApiProperty({ description: 'Client ID' })
clientId!: string;
@ValidateIf(isOAuthEnabled)
@IsString()
@ApiProperty({ description: 'Client secret' })
clientSecret!: string;
@ValidateEnum({ enum: OAuthTokenEndpointAuthMethod, name: 'OAuthTokenEndpointAuthMethod' })
@ValidateEnum({
enum: OAuthTokenEndpointAuthMethod,
name: 'OAuthTokenEndpointAuthMethod',
description: 'Token endpoint auth method',
})
tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod;
@IsInt()
@IsPositive()
@Optional()
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Timeout' })
timeout!: number;
@IsNumber()
@Min(0)
@Optional({ nullable: true })
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Default storage quota' })
defaultStorageQuota!: number | null;
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
@ValidateIf(isOAuthEnabled)
@IsNotEmpty()
@IsString()
@ApiProperty({ description: 'Issuer URL' })
issuerUrl!: string;
@ValidateBoolean()
@ValidateBoolean({ description: 'Mobile override enabled' })
mobileOverrideEnabled!: boolean;
@ValidateIf(isOAuthOverrideEnabled)
@IsUrl()
@ApiProperty({ description: 'Mobile redirect URI' })
mobileRedirectUri!: string;
@IsString()
@ApiProperty({ description: 'Scope' })
scope!: string;
@IsString()
@@ -427,30 +447,34 @@ class SystemConfigOAuthDto {
@IsString()
@IsNotEmpty()
@ApiProperty({ description: 'Profile signing algorithm' })
profileSigningAlgorithm!: string;
@IsString()
@ApiProperty({ description: 'Storage label claim' })
storageLabelClaim!: string;
@IsString()
@ApiProperty({ description: 'Storage quota claim' })
storageQuotaClaim!: string;
@IsString()
@ApiProperty({ description: 'Role claim' })
roleClaim!: string;
}
class SystemConfigPasswordLoginDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
}
class SystemConfigReverseGeocodingDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
}
class SystemConfigFacesDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Import' })
import!: boolean;
}
@@ -464,51 +488,61 @@ class SystemConfigMetadataDto {
class SystemConfigServerDto {
@ValidateIf((_, value: string) => value !== '')
@IsUrl({ require_tld: false, require_protocol: true, protocols: ['http', 'https'] })
@ApiProperty({ description: 'External domain' })
externalDomain!: string;
@IsString()
@ApiProperty({ description: 'Login page message' })
loginPageMessage!: string;
@ValidateBoolean()
@ValidateBoolean({ description: 'Public users' })
publicUsers!: boolean;
}
class SystemConfigSmtpTransportDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Whether to ignore SSL certificate errors' })
ignoreCert!: boolean;
@ApiProperty({ description: 'SMTP server hostname' })
@IsNotEmpty()
@IsString()
host!: string;
@ApiProperty({ description: 'SMTP server port', type: Number, minimum: 0, maximum: 65_535 })
@IsNumber()
@Min(0)
@Max(65_535)
port!: number;
@ValidateBoolean()
@ValidateBoolean({ description: 'Whether to use secure connection (TLS/SSL)' })
secure!: boolean;
@ApiProperty({ description: 'SMTP username' })
@IsString()
username!: string;
@ApiProperty({ description: 'SMTP password' })
@IsString()
password!: string;
}
export class SystemConfigSmtpDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Whether SMTP email notifications are enabled' })
enabled!: boolean;
@ApiProperty({ description: 'Email address to send from' })
@ValidateIf(isEmailNotificationEnabled)
@IsNotEmpty()
@IsString()
@IsNotEmpty()
from!: string;
@ApiProperty({ description: 'Email address for replies' })
@IsString()
replyTo!: string;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@ValidateIf(isEmailNotificationEnabled)
@Type(() => SystemConfigSmtpTransportDto)
@ValidateNested()
@@ -542,48 +576,58 @@ class SystemConfigTemplatesDto {
}
class SystemConfigStorageTemplateDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
@ValidateBoolean()
@ValidateBoolean({ description: 'Hash verification enabled' })
hashVerificationEnabled!: boolean;
@IsNotEmpty()
@IsString()
@ApiProperty({ description: 'Template' })
template!: string;
}
export class SystemConfigTemplateStorageOptionDto {
@ApiProperty({ description: 'Available year format options for storage template' })
yearOptions!: string[];
@ApiProperty({ description: 'Available month format options for storage template' })
monthOptions!: string[];
@ApiProperty({ description: 'Available week format options for storage template' })
weekOptions!: string[];
@ApiProperty({ description: 'Available day format options for storage template' })
dayOptions!: string[];
@ApiProperty({ description: 'Available hour format options for storage template' })
hourOptions!: string[];
@ApiProperty({ description: 'Available minute format options for storage template' })
minuteOptions!: string[];
@ApiProperty({ description: 'Available second format options for storage template' })
secondOptions!: string[];
@ApiProperty({ description: 'Available preset template options' })
presetOptions!: string[];
}
export class SystemConfigThemeDto {
@ApiProperty({ description: 'Custom CSS for theming' })
@IsString()
customCss!: string;
}
class SystemConfigGeneratedImageDto {
@ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' })
@ValidateEnum({ enum: ImageFormat, name: 'ImageFormat', description: 'Image format' })
format!: ImageFormat;
@IsInt()
@Min(1)
@Max(100)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Quality' })
quality!: number;
@IsInt()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Size' })
size!: number;
@ValidateBoolean({ optional: true, default: false })
@@ -591,20 +635,20 @@ class SystemConfigGeneratedImageDto {
}
class SystemConfigGeneratedFullsizeImageDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
@ValidateEnum({ enum: ImageFormat, name: 'ImageFormat' })
@ValidateEnum({ enum: ImageFormat, name: 'ImageFormat', description: 'Image format' })
format!: ImageFormat;
@IsInt()
@Min(1)
@Max(100)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Quality' })
quality!: number;
@ValidateBoolean({ optional: true, default: false })
@ValidateBoolean({ optional: true, default: false, description: 'Progressive' })
progressive?: boolean;
}
@@ -612,33 +656,39 @@ export class SystemConfigImageDto {
@Type(() => SystemConfigGeneratedImageDto)
@ValidateNested()
@IsObject()
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
thumbnail!: SystemConfigGeneratedImageDto;
@Type(() => SystemConfigGeneratedImageDto)
@ValidateNested()
@IsObject()
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
preview!: SystemConfigGeneratedImageDto;
@Type(() => SystemConfigGeneratedFullsizeImageDto)
@ValidateNested()
@IsObject()
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
fullsize!: SystemConfigGeneratedFullsizeImageDto;
@ValidateEnum({ enum: Colorspace, name: 'Colorspace' })
@ValidateEnum({ enum: Colorspace, name: 'Colorspace', description: 'Colorspace' })
colorspace!: Colorspace;
@ValidateBoolean()
@ValidateBoolean({ description: 'Extract embedded' })
extractEmbedded!: boolean;
}
class SystemConfigTrashDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Enabled' })
enabled!: boolean;
@IsInt()
@Min(0)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Days' })
days!: number;
}
@@ -646,111 +696,153 @@ class SystemConfigUserDto {
@IsInt()
@Min(1)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Delete delay' })
deleteDelay!: number;
}
export class SystemConfigDto implements SystemConfig {
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigBackupsDto)
@ValidateNested()
@IsObject()
backup!: SystemConfigBackupsDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigFFmpegDto)
@ValidateNested()
@IsObject()
ffmpeg!: SystemConfigFFmpegDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigLoggingDto)
@ValidateNested()
@IsObject()
logging!: SystemConfigLoggingDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigMachineLearningDto)
@ValidateNested()
@IsObject()
machineLearning!: SystemConfigMachineLearningDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigMapDto)
@ValidateNested()
@IsObject()
map!: SystemConfigMapDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigNewVersionCheckDto)
@ValidateNested()
@IsObject()
newVersionCheck!: SystemConfigNewVersionCheckDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigNightlyTasksDto)
@ValidateNested()
@IsObject()
nightlyTasks!: SystemConfigNightlyTasksDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigOAuthDto)
@ValidateNested()
@IsObject()
oauth!: SystemConfigOAuthDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigPasswordLoginDto)
@ValidateNested()
@IsObject()
passwordLogin!: SystemConfigPasswordLoginDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigReverseGeocodingDto)
@ValidateNested()
@IsObject()
reverseGeocoding!: SystemConfigReverseGeocodingDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigMetadataDto)
@ValidateNested()
@IsObject()
metadata!: SystemConfigMetadataDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigStorageTemplateDto)
@ValidateNested()
@IsObject()
storageTemplate!: SystemConfigStorageTemplateDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigJobDto)
@ValidateNested()
@IsObject()
job!: SystemConfigJobDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigImageDto)
@ValidateNested()
@IsObject()
image!: SystemConfigImageDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigTrashDto)
@ValidateNested()
@IsObject()
trash!: SystemConfigTrashDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigThemeDto)
@ValidateNested()
@IsObject()
theme!: SystemConfigThemeDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigLibraryDto)
@ValidateNested()
@IsObject()
library!: SystemConfigLibraryDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigNotificationsDto)
@ValidateNested()
@IsObject()
notifications!: SystemConfigNotificationsDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigTemplatesDto)
@ValidateNested()
@IsObject()
templates!: SystemConfigTemplatesDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigServerDto)
@ValidateNested()
@IsObject()
server!: SystemConfigServerDto;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
@Type(() => SystemConfigUserDto)
@ValidateNested()
@IsObject()

View File

@@ -1,21 +1,26 @@
import { ApiProperty } from '@nestjs/swagger';
import { ValidateBoolean } from 'src/validation';
export class AdminOnboardingUpdateDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Is admin onboarded' })
isOnboarded!: boolean;
}
export class AdminOnboardingResponseDto {
@ValidateBoolean()
@ValidateBoolean({ description: 'Is admin onboarded' })
isOnboarded!: boolean;
}
export class ReverseGeocodingStateResponseDto {
@ApiProperty({ description: 'Last update timestamp' })
lastUpdate!: string | null;
@ApiProperty({ description: 'Last import file name' })
lastImportFileName!: string | null;
}
export class VersionCheckStateResponseDto {
@ApiProperty({ description: 'Last check timestamp' })
checkedAt!: string | null;
@ApiProperty({ description: 'Release version' })
releaseVersion!: string | null;
}

View File

@@ -1,53 +1,64 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsHexColor, IsNotEmpty, IsString } from 'class-validator';
import { Tag } from 'src/database';
import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation';
export class TagCreateDto {
@ApiProperty({ description: 'Tag name' })
@IsString()
@IsNotEmpty()
name!: string;
@ValidateUUID({ optional: true, nullable: true })
@ValidateUUID({ optional: true, description: 'Parent tag ID' })
parentId?: string | null;
@ApiPropertyOptional({ description: 'Tag color (hex)' })
@IsHexColor()
@Optional({ nullable: true, emptyToNull: true })
color?: string;
}
export class TagUpdateDto {
@Optional({ emptyToNull: true, nullable: true })
@ApiPropertyOptional({ description: 'Tag color (hex)' })
@Optional({ emptyToNull: true })
@ValidateHexColor()
color?: string | null;
}
export class TagUpsertDto {
@ApiProperty({ description: 'Tag names to upsert' })
@IsString({ each: true })
@IsNotEmpty({ each: true })
tags!: string[];
}
export class TagBulkAssetsDto {
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'Tag IDs' })
tagIds!: string[];
@ValidateUUID({ each: true })
@ValidateUUID({ each: true, description: 'Asset IDs' })
assetIds!: string[];
}
export class TagBulkAssetsResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of assets tagged' })
count!: number;
}
export class TagResponseDto {
@ApiProperty({ description: 'Tag ID' })
id!: string;
@ApiPropertyOptional({ description: 'Parent tag ID' })
parentId?: string;
@ApiProperty({ description: 'Tag name' })
name!: string;
@ApiProperty({ description: 'Tag value (full path)' })
value!: string;
@ApiProperty({ description: 'Creation date' })
createdAt!: Date;
@ApiProperty({ description: 'Last update date' })
updatedAt!: Date;
@ApiPropertyOptional({ description: 'Tag color (hex)' })
color?: string;
}

View File

@@ -132,7 +132,7 @@ export class TimeBucketAssetResponseDto {
@ApiProperty({
type: 'array',
items: { type: 'string' },
description: 'Array of file creation timestamps in UTC (ISO 8601 format, without timezone)',
description: 'Array of file creation timestamps in UTC',
})
fileCreatedAt!: string[];

View File

@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
export class TrashResponseDto {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Number of items in trash' })
count!: number;
}

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional, ApiSchema } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsDateString, IsInt, IsPositive, ValidateNested } from 'class-validator';
import { AssetOrder, UserAvatarColor } from 'src/enum';
@@ -6,71 +6,72 @@ import { UserPreferences } from 'src/types';
import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation';
class AvatarUpdate {
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' })
color?: UserAvatarColor;
}
class MemoriesUpdate {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether memories are enabled' })
enabled?: boolean;
@Optional()
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Memory duration in seconds' })
duration?: number;
}
class RatingsUpdate {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether ratings are enabled' })
enabled?: boolean;
}
@ApiSchema({ description: 'Album preferences' })
class AlbumsUpdate {
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true })
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, description: 'Default asset order for albums' })
defaultAssetOrder?: AssetOrder;
}
class FoldersUpdate {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether folders are enabled' })
enabled?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether folders appear in web sidebar' })
sidebarWeb?: boolean;
}
class PeopleUpdate {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether people are enabled' })
enabled?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether people appear in web sidebar' })
sidebarWeb?: boolean;
}
class SharedLinksUpdate {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether shared links are enabled' })
enabled?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether shared links appear in web sidebar' })
sidebarWeb?: boolean;
}
class TagsUpdate {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether tags are enabled' })
enabled?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether tags appear in web sidebar' })
sidebarWeb?: boolean;
}
class EmailNotificationsUpdate {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether email notifications are enabled' })
enabled?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album invites' })
albumInvite?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album updates' })
albumUpdate?: boolean;
}
@@ -78,83 +79,108 @@ class DownloadUpdate implements Partial<DownloadResponse> {
@Optional()
@IsInt()
@IsPositive()
@ApiProperty({ type: 'integer' })
@ApiPropertyOptional({ type: 'integer', description: 'Maximum archive size in bytes' })
archiveSize?: number;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether to include embedded videos in downloads' })
includeEmbeddedVideos?: boolean;
}
class PurchaseUpdate {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether to show support badge' })
showSupportBadge?: boolean;
@ApiPropertyOptional({ description: 'Date until which to hide buy button' })
@IsDateString()
@Optional()
hideBuyButtonUntil?: string;
}
class CastUpdate {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Whether Google Cast is enabled' })
gCastEnabled?: boolean;
}
export class UserPreferencesUpdateDto {
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => AlbumsUpdate)
albums?: AlbumsUpdate;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => FoldersUpdate)
folders?: FoldersUpdate;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => MemoriesUpdate)
memories?: MemoriesUpdate;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => PeopleUpdate)
people?: PeopleUpdate;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => RatingsUpdate)
ratings?: RatingsUpdate;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined, required: false })
@Optional()
@ValidateNested()
@Type(() => SharedLinksUpdate)
sharedLinks?: SharedLinksUpdate;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => TagsUpdate)
tags?: TagsUpdate;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => AvatarUpdate)
avatar?: AvatarUpdate;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => EmailNotificationsUpdate)
emailNotifications?: EmailNotificationsUpdate;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => DownloadUpdate)
download?: DownloadUpdate;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => PurchaseUpdate)
purchase?: PurchaseUpdate;
// Description lives on schema to avoid duplication
@ApiPropertyOptional({ description: undefined })
@Optional()
@ValidateNested()
@Type(() => CastUpdate)
@@ -162,74 +188,113 @@ export class UserPreferencesUpdateDto {
}
class AlbumsResponse {
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' })
@ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Default asset order for albums' })
defaultAssetOrder: AssetOrder = AssetOrder.Desc;
}
class RatingsResponse {
@ApiProperty({ description: 'Whether ratings are enabled' })
enabled: boolean = false;
}
class MemoriesResponse {
@ApiProperty({ description: 'Whether memories are enabled' })
enabled: boolean = true;
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Memory duration in seconds' })
duration: number = 5;
}
class FoldersResponse {
@ApiProperty({ description: 'Whether folders are enabled' })
enabled: boolean = false;
@ApiProperty({ description: 'Whether folders appear in web sidebar' })
sidebarWeb: boolean = false;
}
class PeopleResponse {
@ApiProperty({ description: 'Whether people are enabled' })
enabled: boolean = true;
@ApiProperty({ description: 'Whether people appear in web sidebar' })
sidebarWeb: boolean = false;
}
class TagsResponse {
@ApiProperty({ description: 'Whether tags are enabled' })
enabled: boolean = true;
@ApiProperty({ description: 'Whether tags appear in web sidebar' })
sidebarWeb: boolean = true;
}
class SharedLinksResponse {
@ApiProperty({ description: 'Whether shared links are enabled' })
enabled: boolean = true;
@ApiProperty({ description: 'Whether shared links appear in web sidebar' })
sidebarWeb: boolean = false;
}
class EmailNotificationsResponse {
@ApiProperty({ description: 'Whether email notifications are enabled' })
enabled!: boolean;
@ApiProperty({ description: 'Whether to receive email notifications for album invites' })
albumInvite!: boolean;
@ApiProperty({ description: 'Whether to receive email notifications for album updates' })
albumUpdate!: boolean;
}
class DownloadResponse {
@ApiProperty({ type: 'integer' })
@ApiProperty({ type: 'integer', description: 'Maximum archive size in bytes' })
archiveSize!: number;
@ApiProperty({ description: 'Whether to include embedded videos in downloads' })
includeEmbeddedVideos: boolean = false;
}
class PurchaseResponse {
@ApiProperty({ description: 'Whether to show support badge' })
showSupportBadge!: boolean;
@ApiProperty({ description: 'Date until which to hide buy button' })
hideBuyButtonUntil!: string;
}
class CastResponse {
@ApiProperty({ description: 'Whether Google Cast is enabled' })
gCastEnabled: boolean = false;
}
export class UserPreferencesResponseDto implements UserPreferences {
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
albums!: AlbumsResponse;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
folders!: FoldersResponse;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
memories!: MemoriesResponse;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
people!: PeopleResponse;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
ratings!: RatingsResponse;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
sharedLinks!: SharedLinksResponse;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
tags!: TagsResponse;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
emailNotifications!: EmailNotificationsResponse;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
download!: DownloadResponse;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
purchase!: PurchaseResponse;
// Description lives on schema to avoid duplication
@ApiProperty({ description: undefined })
cast!: CastResponse;
}

View File

@@ -2,12 +2,15 @@ import { ApiProperty } from '@nestjs/swagger';
import { UploadFieldName } from 'src/dtos/asset-media.dto';
export class CreateProfileImageDto {
@ApiProperty({ type: 'string', format: 'binary' })
@ApiProperty({ type: 'string', format: 'binary', description: 'Profile image file' })
[UploadFieldName.PROFILE_DATA]!: Express.Multer.File;
}
export class CreateProfileImageResponseDto {
@ApiProperty({ description: 'User ID' })
userId!: string;
@ApiProperty({ description: 'Profile image change date', format: 'date-time' })
profileChangedAt!: Date;
@ApiProperty({ description: 'Profile image file path' })
profileImagePath!: string;
}

View File

@@ -1,4 +1,4 @@
import { ApiProperty } from '@nestjs/swagger';
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator';
import { User, UserAdmin } from 'src/database';
@@ -7,39 +7,50 @@ import { UserMetadataItem } from 'src/types';
import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation';
export class UserUpdateMeDto {
@ApiPropertyOptional({ description: 'User email' })
@Optional()
@IsEmail({ require_tld: false })
@Transform(toEmail)
email?: string;
// TODO: migrate to the other change password endpoint
@ApiPropertyOptional({ description: 'User password (deprecated, use change password endpoint)' })
@Optional()
@IsNotEmpty()
@IsString()
password?: string;
@ApiPropertyOptional({ description: 'User name' })
@Optional()
@IsString()
@IsNotEmpty()
name?: string;
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' })
avatarColor?: UserAvatarColor | null;
}
export class UserResponseDto {
@ApiProperty({ description: 'User ID' })
id!: string;
@ApiProperty({ description: 'User name' })
name!: string;
@ApiProperty({ description: 'User email' })
email!: string;
@ApiProperty({ description: 'Profile image path' })
profileImagePath!: string;
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor' })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'Avatar color' })
avatarColor!: UserAvatarColor;
@ApiProperty({ description: 'Profile change date' })
profileChangedAt!: Date;
}
export class UserLicense {
@ApiProperty({ description: 'License key' })
licenseKey!: string;
@ApiProperty({ description: 'Activation key' })
activationKey!: string;
@ApiProperty({ description: 'Activation date' })
activatedAt!: Date;
}
@@ -63,108 +74,125 @@ export const mapUser = (entity: User | UserAdmin): UserResponseDto => {
};
export class UserAdminSearchDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Include deleted users' })
withDeleted?: boolean;
@ValidateUUID({ optional: true })
@ValidateUUID({ optional: true, description: 'User ID filter' })
id?: string;
}
export class UserAdminCreateDto {
@ApiProperty({ description: 'User email' })
@IsEmail({ require_tld: false })
@Transform(toEmail)
email!: string;
@ApiProperty({ description: 'User password' })
@IsString()
password!: string;
@ApiProperty({ description: 'User name' })
@IsNotEmpty()
@IsString()
name!: string;
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' })
avatarColor?: UserAvatarColor | null;
@ApiPropertyOptional({ description: 'Storage label' })
@Optional({ nullable: true })
@IsString()
@Transform(toSanitized)
storageLabel?: string | null;
@ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' })
@Optional({ nullable: true })
@IsInt()
@Min(0)
@ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes?: number | null;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Require password change on next login' })
shouldChangePassword?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Send notification email' })
notify?: boolean;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Grant admin privileges' })
isAdmin?: boolean;
}
export class UserAdminUpdateDto {
@ApiPropertyOptional({ description: 'User email' })
@Optional()
@IsEmail({ require_tld: false })
@Transform(toEmail)
email?: string;
@ApiPropertyOptional({ description: 'User password' })
@Optional()
@IsNotEmpty()
@IsString()
password?: string;
@PinCode({ optional: true, nullable: true, emptyToNull: true })
@ApiPropertyOptional({ description: 'PIN code' })
@PinCode({ optional: true, emptyToNull: true })
pinCode?: string | null;
@ApiPropertyOptional({ description: 'User name' })
@Optional()
@IsString()
@IsNotEmpty()
name?: string;
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, nullable: true })
@ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' })
avatarColor?: UserAvatarColor | null;
@ApiPropertyOptional({ description: 'Storage label' })
@Optional({ nullable: true })
@IsString()
@Transform(toSanitized)
storageLabel?: string | null;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Require password change on next login' })
shouldChangePassword?: boolean;
@ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' })
@Optional({ nullable: true })
@IsInt()
@Min(0)
@ApiProperty({ type: 'integer', format: 'int64' })
quotaSizeInBytes?: number | null;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Grant admin privileges' })
isAdmin?: boolean;
}
export class UserAdminDeleteDto {
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Force delete even if user has assets' })
force?: boolean;
}
export class UserAdminResponseDto extends UserResponseDto {
@ApiProperty({ description: 'Storage label' })
storageLabel!: string | null;
@ApiProperty({ description: 'Require password change on next login' })
shouldChangePassword!: boolean;
@ApiProperty({ description: 'Is admin user' })
isAdmin!: boolean;
@ApiProperty({ description: 'Creation date' })
createdAt!: Date;
@ApiProperty({ description: 'Deletion date' })
deletedAt!: Date | null;
@ApiProperty({ description: 'Last update date' })
updatedAt!: Date;
@ApiProperty({ description: 'OAuth ID' })
oauthId!: string;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' })
quotaSizeInBytes!: number | null;
@ApiProperty({ type: 'integer', format: 'int64' })
@ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' })
quotaUsageInBytes!: number | null;
@ValidateEnum({ enum: UserStatus, name: 'UserStatus' })
@ValidateEnum({ enum: UserStatus, name: 'UserStatus', description: 'User status' })
status!: string;
@ApiProperty({ description: 'User license' })
license!: UserLicense | null;
}

View File

@@ -1,3 +1,4 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsNotEmpty, IsObject, IsString, IsUUID, ValidateNested } from 'class-validator';
import { WorkflowAction, WorkflowFilter } from 'src/database';
@@ -6,68 +7,85 @@ import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types';
import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation';
export class WorkflowFilterItemDto {
@ApiProperty({ description: 'Plugin filter ID' })
@IsUUID()
pluginFilterId!: string;
@ApiPropertyOptional({ description: 'Filter configuration' })
@IsObject()
@Optional()
filterConfig?: FilterConfig;
}
export class WorkflowActionItemDto {
@ApiProperty({ description: 'Plugin action ID' })
@IsUUID()
pluginActionId!: string;
@ApiPropertyOptional({ description: 'Action configuration' })
@IsObject()
@Optional()
actionConfig?: ActionConfig;
}
export class WorkflowCreateDto {
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' })
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Workflow trigger type' })
triggerType!: PluginTriggerType;
@ApiProperty({ description: 'Workflow name' })
@IsString()
@IsNotEmpty()
name!: string;
@ApiPropertyOptional({ description: 'Workflow description' })
@IsString()
@Optional()
description?: string;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Workflow enabled' })
enabled?: boolean;
@ApiProperty({ description: 'Workflow filters' })
@ValidateNested({ each: true })
@Type(() => WorkflowFilterItemDto)
filters!: WorkflowFilterItemDto[];
@ApiProperty({ description: 'Workflow actions' })
@ValidateNested({ each: true })
@Type(() => WorkflowActionItemDto)
actions!: WorkflowActionItemDto[];
}
export class WorkflowUpdateDto {
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', optional: true })
@ValidateEnum({
enum: PluginTriggerType,
name: 'PluginTriggerType',
optional: true,
description: 'Workflow trigger type',
})
triggerType?: PluginTriggerType;
@ApiPropertyOptional({ description: 'Workflow name' })
@IsString()
@IsNotEmpty()
@Optional()
name?: string;
@ApiPropertyOptional({ description: 'Workflow description' })
@IsString()
@Optional()
description?: string;
@ValidateBoolean({ optional: true })
@ValidateBoolean({ optional: true, description: 'Workflow enabled' })
enabled?: boolean;
@ApiPropertyOptional({ description: 'Workflow filters' })
@ValidateNested({ each: true })
@Type(() => WorkflowFilterItemDto)
@Optional()
filters?: WorkflowFilterItemDto[];
@ApiPropertyOptional({ description: 'Workflow actions' })
@ValidateNested({ each: true })
@Type(() => WorkflowActionItemDto)
@Optional()
@@ -75,31 +93,49 @@ export class WorkflowUpdateDto {
}
export class WorkflowResponseDto {
@ApiProperty({ description: 'Workflow ID' })
id!: string;
@ApiProperty({ description: 'Owner user ID' })
ownerId!: string;
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType' })
@ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Workflow trigger type' })
triggerType!: PluginTriggerType;
@ApiProperty({ description: 'Workflow name' })
name!: string | null;
@ApiProperty({ description: 'Workflow description' })
description!: string;
@ApiProperty({ description: 'Creation date' })
createdAt!: string;
@ApiProperty({ description: 'Workflow enabled' })
enabled!: boolean;
@ApiProperty({ description: 'Workflow filters' })
filters!: WorkflowFilterResponseDto[];
@ApiProperty({ description: 'Workflow actions' })
actions!: WorkflowActionResponseDto[];
}
export class WorkflowFilterResponseDto {
@ApiProperty({ description: 'Filter ID' })
id!: string;
@ApiProperty({ description: 'Workflow ID' })
workflowId!: string;
@ApiProperty({ description: 'Plugin filter ID' })
pluginFilterId!: string;
@ApiProperty({ description: 'Filter configuration' })
filterConfig!: FilterConfig | null;
@ApiProperty({ description: 'Filter order', type: 'number' })
order!: number;
}
export class WorkflowActionResponseDto {
@ApiProperty({ description: 'Action ID' })
id!: string;
@ApiProperty({ description: 'Workflow ID' })
workflowId!: string;
@ApiProperty({ description: 'Plugin action ID' })
pluginActionId!: string;
@ApiProperty({ description: 'Action configuration' })
actionConfig!: ActionConfig | null;
@ApiProperty({ description: 'Action order', type: 'number' })
order!: number;
}

View File

@@ -33,6 +33,7 @@ import {
import { CronJob } from 'cron';
import { DateTime } from 'luxon';
import sanitize from 'sanitize-filename';
import { Property, PropertyOptions } from 'src/decorators';
import { isIP, isIPRange } from 'validator';
@Injectable()
@@ -66,7 +67,7 @@ export class FileNotEmptyValidator extends FileValidator {
}
type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean };
export const ValidateUUID = (options?: UUIDOptions & ApiPropertyOptions) => {
export const ValidateUUID = (options?: UUIDOptions & PropertyOptions) => {
const { optional, each, nullable, ...apiPropertyOptions } = {
optional: false,
each: false,
@@ -75,7 +76,7 @@ export const ValidateUUID = (options?: UUIDOptions & ApiPropertyOptions) => {
};
return applyDecorators(
IsUUID('4', { each }),
ApiProperty({ format: 'uuid', ...apiPropertyOptions }),
Property({ format: 'uuid', ...apiPropertyOptions }),
optional ? Optional({ nullable }) : IsNotEmpty(),
each ? IsArray() : IsString(),
);
@@ -277,10 +278,10 @@ export const ValidateString = (options?: StringOptions & ApiPropertyOptions) =>
};
type BooleanOptions = { optional?: boolean; nullable?: boolean };
export const ValidateBoolean = (options?: BooleanOptions & ApiPropertyOptions) => {
export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => {
const { optional, nullable, ...apiPropertyOptions } = options || {};
const decorators = [
ApiProperty(apiPropertyOptions),
Property(apiPropertyOptions),
IsBoolean(),
Transform(({ value }) => {
if (value == 'true') {