From 7d8f843be6fa76fb919fbeed0e57702f40387b7a Mon Sep 17 00:00:00 2001 From: Timon Date: Tue, 14 Apr 2026 23:39:03 +0200 Subject: [PATCH] refactor!: migrate class-validator to zod (#26597) --- cli/src/commands/asset.spec.ts | 12 +- cli/src/commands/asset.ts | 4 +- e2e/src/specs/server/api/asset.e2e-spec.ts | 7 +- e2e/src/specs/server/api/library.e2e-spec.ts | 14 +- e2e/src/specs/server/api/map.e2e-spec.ts | 8 +- e2e/src/specs/server/api/oauth.e2e-spec.ts | 6 +- e2e/src/specs/server/api/tag.e2e-spec.ts | 4 +- .../specs/server/api/user-admin.e2e-spec.ts | 3 +- e2e/src/specs/server/api/user.e2e-spec.ts | 8 +- .../ui/mock-network/broken-asset-network.ts | 2 +- .../lib/domain/utils/migrate_cloud_ids.dart | 14 +- .../repositories/album_api.repository.dart | 2 +- mobile/lib/utils/openapi_patching.dart | 14 +- mobile/openapi/README.md | 8 +- mobile/openapi/lib/api.dart | 8 +- mobile/openapi/lib/api/activities_api.dart | 4 - mobile/openapi/lib/api/assets_api.dart | 6 - .../lib/api/database_backups_admin_api.dart | 2 + mobile/openapi/lib/api/deprecated_api.dart | 2 - mobile/openapi/lib/api/jobs_api.dart | 2 - mobile/openapi/lib/api/memories_api.dart | 8 - mobile/openapi/lib/api/notifications_api.dart | 4 - mobile/openapi/lib/api/partners_api.dart | 2 - mobile/openapi/lib/api/queues_api.dart | 8 - mobile/openapi/lib/api/search_api.dart | 6 - mobile/openapi/lib/api/server_api.dart | 8 +- mobile/openapi/lib/api/timeline_api.dart | 4 +- mobile/openapi/lib/api/users_admin_api.dart | 2 - mobile/openapi/lib/api/users_api.dart | 8 +- mobile/openapi/lib/api_client.dart | 16 +- mobile/openapi/lib/api_helper.dart | 12 + .../lib/model/activity_create_dto.dart | 1 - .../lib/model/activity_response_dto.dart | 7 +- .../activity_statistics_response_dto.dart | 6 + .../openapi/lib/model/album_response_dto.dart | 4 +- .../model/album_statistics_response_dto.dart | 9 + .../openapi/lib/model/album_user_add_dto.dart | 19 +- .../lib/model/album_user_create_dto.dart | 1 - .../lib/model/album_user_response_dto.dart | 1 - .../model/albums_add_assets_response_dto.dart | 1 - mobile/openapi/lib/model/albums_response.dart | 3 +- mobile/openapi/lib/model/albums_update.dart | 1 - .../lib/model/api_key_response_dto.dart | 12 +- .../lib/model/asset_bulk_update_dto.dart | 13 +- .../model/asset_bulk_upload_check_result.dart | 164 +- .../lib/model/asset_delta_sync_dto.dart | 6 +- .../model/asset_delta_sync_response_dto.dart | 1 - .../lib/model/asset_edit_action_item_dto.dart | 1 - ...asset_edit_action_item_dto_parameters.dart | 1 - .../asset_edit_action_item_response_dto.dart | 2 +- .../lib/model/asset_face_create_dto.dart | 18 + .../lib/model/asset_face_response_dto.dart | 20 +- ...sset_face_without_person_response_dto.dart | 19 +- .../lib/model/asset_full_sync_dto.dart | 7 +- .../lib/model/asset_id_error_reason.dart | 88 + .../lib/model/asset_ids_response_dto.dart | 88 +- mobile/openapi/lib/model/asset_jobs_dto.dart | 1 - .../lib/model/asset_media_response_dto.dart | 1 - .../openapi/lib/model/asset_media_size.dart | 2 +- .../asset_metadata_bulk_response_dto.dart | 14 +- .../asset_metadata_bulk_upsert_item_dto.dart | 8 +- .../model/asset_metadata_response_dto.dart | 14 +- .../model/asset_metadata_upsert_item_dto.dart | 8 +- .../lib/model/asset_reject_reason.dart | 85 + .../openapi/lib/model/asset_response_dto.dart | 6 +- .../lib/model/asset_stack_response_dto.dart | 3 + .../lib/model/asset_stats_response_dto.dart | 9 + .../lib/model/asset_upload_action.dart | 85 + mobile/openapi/lib/model/avatar_update.dart | 1 - .../lib/model/bulk_id_response_dto.dart | 94 +- mobile/openapi/lib/model/cast_response.dart | 2 +- .../model/contributor_count_response_dto.dart | 3 + .../openapi/lib/model/create_library_dto.dart | 20 +- .../create_profile_image_response_dto.dart | 6 +- .../lib/model/database_backup_delete_dto.dart | 1 + .../lib/model/database_backup_dto.dart | 3 + .../database_backup_list_response_dto.dart | 1 + .../lib/model/download_archive_info.dart | 3 + .../openapi/lib/model/download_info_dto.dart | 1 + .../openapi/lib/model/download_response.dart | 5 +- .../lib/model/download_response_dto.dart | 3 + mobile/openapi/lib/model/download_update.dart | 1 + .../openapi/lib/model/exif_response_dto.dart | 7 + .../lib/model/facial_recognition_config.dart | 1 + .../openapi/lib/model/folders_response.dart | 4 +- mobile/openapi/lib/model/job_create_dto.dart | 1 - .../openapi/lib/model/job_settings_dto.dart | 1 + .../lib/model/library_response_dto.dart | 21 +- .../lib/model/library_stats_response_dto.dart | 20 +- mobile/openapi/lib/model/license_key_dto.dart | 2 +- .../lib/model/license_response_dto.dart | 118 -- mobile/openapi/lib/model/log_level.dart | 2 +- ...nce_detect_install_storage_folder_dto.dart | 1 - .../maintenance_status_response_dto.dart | 1 - mobile/openapi/lib/model/manual_job_name.dart | 2 +- .../openapi/lib/model/memories_response.dart | 7 +- mobile/openapi/lib/model/memories_update.dart | 1 + .../openapi/lib/model/memory_create_dto.dart | 25 +- .../lib/model/memory_response_dto.dart | 43 +- .../lib/model/memory_search_order.dart | 2 +- .../model/memory_statistics_response_dto.dart | 3 + mobile/openapi/lib/model/memory_type.dart | 2 +- .../openapi/lib/model/memory_update_dto.dart | 12 +- .../lib/model/metadata_search_dto.dart | 75 +- .../openapi/lib/model/mirror_parameters.dart | 1 - .../lib/model/notification_create_dto.dart | 28 +- .../openapi/lib/model/notification_dto.dart | 34 +- .../openapi/lib/model/notification_level.dart | 2 +- .../openapi/lib/model/notification_type.dart | 2 +- .../model/notification_update_all_dto.dart | 6 +- .../lib/model/notification_update_dto.dart | 6 +- .../o_auth_token_endpoint_auth_method.dart | 2 +- mobile/openapi/lib/model/ocr_config.dart | 1 + mobile/openapi/lib/model/on_this_day_dto.dart | 7 +- .../openapi/lib/model/partner_direction.dart | 2 +- .../lib/model/partner_response_dto.dart | 1 - mobile/openapi/lib/model/people_response.dart | 4 +- .../lib/model/people_response_dto.dart | 7 +- .../model/person_statistics_response_dto.dart | 3 + .../model/person_with_faces_response_dto.dart | 1 - .../lib/model/plugin_action_response_dto.dart | 4 +- .../lib/model/plugin_context_type.dart | 2 +- .../lib/model/plugin_filter_response_dto.dart | 4 +- .../openapi/lib/model/plugin_json_schema.dart | 158 ++ .../model/plugin_json_schema_property.dart | 195 ++ ...schema_property_additional_properties.dart | 195 ++ .../lib/model/plugin_json_schema_type.dart | 100 + .../model/plugin_trigger_response_dto.dart | 2 - .../lib/model/plugin_trigger_type.dart | 2 +- .../openapi/lib/model/queue_command_dto.dart | 1 - .../lib/model/queue_job_response_dto.dart | 12 +- .../openapi/lib/model/queue_job_status.dart | 2 +- mobile/openapi/lib/model/queue_name.dart | 2 +- .../openapi/lib/model/queue_response_dto.dart | 1 - .../lib/model/queue_statistics_dto.dart | 18 + .../openapi/lib/model/random_search_dto.dart | 56 +- .../openapi/lib/model/ratings_response.dart | 2 +- mobile/openapi/lib/model/reaction_level.dart | 2 +- mobile/openapi/lib/model/reaction_type.dart | 2 +- .../lib/model/search_album_response_dto.dart | 6 + .../lib/model/search_asset_response_dto.dart | 6 + .../search_facet_count_response_dto.dart | 3 + .../lib/model/search_facet_response_dto.dart | 1 - .../model/search_statistics_response_dto.dart | 3 + .../lib/model/search_suggestion_type.dart | 2 +- .../openapi/lib/model/server_config_dto.dart | 6 + .../lib/model/server_stats_response_dto.dart | 26 +- .../model/server_storage_response_dto.dart | 9 + .../server_version_history_response_dto.dart | 6 +- .../model/server_version_response_dto.dart | 9 + .../lib/model/set_maintenance_mode_dto.dart | 1 - .../lib/model/shared_link_create_dto.dart | 7 +- .../lib/model/shared_link_edit_dto.dart | 6 +- .../lib/model/shared_link_response_dto.dart | 13 +- .../lib/model/shared_links_response.dart | 4 +- .../openapi/lib/model/smart_search_dto.dart | 56 +- .../openapi/lib/model/stack_response_dto.dart | 1 - .../lib/model/statistics_search_dto.dart | 56 +- mobile/openapi/lib/model/sync_ack_dto.dart | 1 - .../openapi/lib/model/sync_album_user_v1.dart | 1 - mobile/openapi/lib/model/sync_album_v1.dart | 12 +- .../lib/model/sync_asset_edit_delete_v1.dart | 1 + .../openapi/lib/model/sync_asset_edit_v1.dart | 15 +- .../openapi/lib/model/sync_asset_exif_v1.dart | 27 +- .../openapi/lib/model/sync_asset_face_v1.dart | 24 + .../openapi/lib/model/sync_asset_face_v2.dart | 30 +- .../lib/model/sync_asset_metadata_v1.dart | 8 +- mobile/openapi/lib/model/sync_asset_v1.dart | 32 +- .../openapi/lib/model/sync_auth_user_v1.dart | 24 +- mobile/openapi/lib/model/sync_memory_v1.dart | 51 +- mobile/openapi/lib/model/sync_person_v1.dart | 18 +- .../openapi/lib/model/sync_request_type.dart | 2 +- mobile/openapi/lib/model/sync_stack_v1.dart | 12 +- .../model/sync_user_metadata_delete_v1.dart | 1 - .../lib/model/sync_user_metadata_v1.dart | 9 +- mobile/openapi/lib/model/sync_user_v1.dart | 16 +- .../lib/model/system_config_f_fmpeg_dto.dart | 8 +- ...m_config_generated_fullsize_image_dto.dart | 19 +- .../system_config_generated_image_dto.dart | 21 +- .../lib/model/system_config_image_dto.dart | 1 - .../model/system_config_library_scan_dto.dart | 1 + .../system_config_machine_learning_dto.dart | 1 + .../lib/model/system_config_map_dto.dart | 2 + .../system_config_nightly_tasks_dto.dart | 1 + .../lib/model/system_config_o_auth_dto.dart | 11 +- .../system_config_template_emails_dto.dart | 3 + .../lib/model/system_config_trash_dto.dart | 1 + .../lib/model/system_config_user_dto.dart | 1 + .../model/tag_bulk_assets_response_dto.dart | 3 + mobile/openapi/lib/model/tag_create_dto.dart | 6 - mobile/openapi/lib/model/tags_response.dart | 4 +- .../lib/model/time_buckets_response_dto.dart | 3 + .../openapi/lib/model/trash_response_dto.dart | 3 + .../openapi/lib/model/update_album_dto.dart | 1 - .../lib/model/update_album_user_dto.dart | 1 - .../openapi/lib/model/update_asset_dto.dart | 13 +- .../openapi/lib/model/update_library_dto.dart | 20 +- .../openapi/lib/model/usage_by_user_dto.dart | 18 + .../lib/model/user_admin_create_dto.dart | 2 +- .../lib/model/user_admin_response_dto.dart | 27 +- .../lib/model/user_admin_update_dto.dart | 2 +- .../openapi/lib/model/user_avatar_color.dart | 2 +- mobile/openapi/lib/model/user_license.dart | 8 +- .../openapi/lib/model/user_response_dto.dart | 1 - .../openapi/lib/model/user_update_me_dto.dart | 1 - .../lib/model/validate_library_dto.dart | 20 +- ...date_library_import_path_response_dto.dart | 2 +- mobile/openapi/lib/model/video_container.dart | 2 +- .../lib/model/workflow_action_item_dto.dart | 21 +- .../model/workflow_action_response_dto.dart | 7 +- .../lib/model/workflow_create_dto.dart | 1 - .../lib/model/workflow_filter_item_dto.dart | 21 +- .../model/workflow_filter_response_dto.dart | 7 +- .../lib/model/workflow_response_dto.dart | 1 - .../lib/model/workflow_update_dto.dart | 1 - .../modules/utils/openapi_patching_test.dart | 2 +- open-api/immich-openapi-specs.json | 1733 ++++++++++------- open-api/typescript-sdk/src/fetch-client.ts | 287 ++- pnpm-lock.yaml | 77 +- server/package.json | 6 +- server/src/app.module.ts | 6 +- server/src/bin/sync-sql.ts | 3 +- .../controllers/activity.controller.spec.ts | 18 +- .../src/controllers/album.controller.spec.ts | 4 +- .../controllers/api-key.controller.spec.ts | 6 +- .../asset-media.controller.spec.ts | 24 +- .../src/controllers/asset.controller.spec.ts | 55 +- .../src/controllers/auth.controller.spec.ts | 18 +- .../controllers/duplicate.controller.spec.ts | 2 +- .../maintenance.controller.spec.ts | 2 +- .../src/controllers/memory.controller.spec.ts | 16 +- .../notification.controller.spec.ts | 8 +- .../controllers/partner.controller.spec.ts | 13 +- .../src/controllers/person.controller.spec.ts | 34 +- .../src/controllers/search.controller.spec.ts | 41 +- .../src/controllers/sync.controller.spec.ts | 10 +- .../system-config.controller.spec.ts | 40 +- server/src/controllers/tag.controller.spec.ts | 2 +- .../controllers/timeline.controller.spec.ts | 30 + .../controllers/user-admin.controller.spec.ts | 18 +- server/src/database.ts | 2 +- server/src/dtos/activity.dto.ts | 118 +- server/src/dtos/album.dto.ts | 308 ++- server/src/dtos/api-key.dto.ts | 85 +- server/src/dtos/asset-ids.response.dto.ts | 60 +- server/src/dtos/asset-media-response.dto.ts | 69 +- server/src/dtos/asset-media.dto.ts | 156 +- server/src/dtos/asset-response.dto.ts | 245 ++- server/src/dtos/asset.dto.ts | 396 ++-- server/src/dtos/auth.dto.ts | 246 +-- server/src/dtos/bbox.dto.ts | 38 +- server/src/dtos/database-backup.dto.ts | 46 +- server/src/dtos/download.dto.ts | 65 +- server/src/dtos/duplicate.dto.ts | 56 +- server/src/dtos/editing.dto.ts | 199 +- server/src/dtos/env.dto.ts | 283 +-- server/src/dtos/exif.dto.ts | 92 +- server/src/dtos/job.dto.ts | 16 +- server/src/dtos/library.dto.ts | 193 +- server/src/dtos/license.dto.ts | 26 +- server/src/dtos/maintenance.dto.ts | 92 +- server/src/dtos/map.dto.ts | 102 +- server/src/dtos/memory.dto.ts | 193 +- server/src/dtos/model-config.dto.ts | 124 +- server/src/dtos/notification.dto.ts | 176 +- server/src/dtos/ocr.dto.ts | 60 +- server/src/dtos/onboarding.dto.ts | 12 +- server/src/dtos/partner.dto.ts | 51 +- server/src/dtos/person.dto.ts | 380 ++-- server/src/dtos/plugin-manifest.dto.ts | 170 +- server/src/dtos/plugin.dto.ts | 123 +- server/src/dtos/queue-legacy.dto.ts | 110 +- server/src/dtos/queue.dto.ts | 134 +- server/src/dtos/search.dto.ts | 575 ++---- server/src/dtos/server.dto.ts | 369 ++-- server/src/dtos/session.dto.ts | 82 +- server/src/dtos/shared-link.dto.ts | 232 +-- server/src/dtos/stack.dto.ts | 56 +- server/src/dtos/sync.dto.ts | 875 ++++----- server/src/dtos/system-config.dto.ts | 1177 ++++------- server/src/dtos/system-metadata.dto.ts | 51 +- server/src/dtos/tag.dto.ts | 105 +- server/src/dtos/time-bucket.dto.ts | 340 ++-- server/src/dtos/trash.dto.ts | 14 +- server/src/dtos/user-preferences.dto.ts | 454 ++--- server/src/dtos/user-profile.dto.ts | 20 +- server/src/dtos/user.dto.spec.ts | 80 +- server/src/dtos/user.dto.ts | 286 +-- server/src/dtos/workflow.dto.ts | 203 +- server/src/enum.ts | 223 ++- .../src/middleware/global-exception.filter.ts | 15 + server/src/repositories/asset.repository.ts | 2 +- .../repositories/config.repository.spec.ts | 10 +- server/src/repositories/config.repository.ts | 17 +- server/src/repositories/event.repository.ts | 3 +- server/src/repositories/job.repository.ts | 3 +- .../src/repositories/telemetry.repository.ts | 5 +- .../src/schema/tables/asset-metadata.table.ts | 2 +- server/src/schema/tables/memory.table.ts | 2 +- server/src/services/auth.service.spec.ts | 5 +- server/src/services/auth.service.ts | 5 +- server/src/services/library.service.ts | 1 + .../src/services/notification.service.spec.ts | 3 +- server/src/services/plugin.service.ts | 13 +- server/src/services/queue.service.ts | 5 +- server/src/services/server.service.ts | 6 + .../services/system-config.service.spec.ts | 35 +- server/src/services/system-config.service.ts | 3 +- server/src/types/plugin-schema.types.ts | 71 +- server/src/utils/bbox.ts | 32 - server/src/utils/config.ts | 23 +- server/src/utils/date.ts | 12 + server/src/utils/duplicate.spec.ts | 20 +- server/src/utils/misc.ts | 33 +- server/src/validation.spec.ts | 117 +- server/src/validation.ts | 583 ++---- server/test/utils.ts | 21 +- web/src/lib/utils/file-uploader.ts | 4 +- 318 files changed, 7830 insertions(+), 8316 deletions(-) create mode 100644 mobile/openapi/lib/model/asset_id_error_reason.dart create mode 100644 mobile/openapi/lib/model/asset_reject_reason.dart create mode 100644 mobile/openapi/lib/model/asset_upload_action.dart delete mode 100644 mobile/openapi/lib/model/license_response_dto.dart create mode 100644 mobile/openapi/lib/model/plugin_json_schema.dart create mode 100644 mobile/openapi/lib/model/plugin_json_schema_property.dart create mode 100644 mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart create mode 100644 mobile/openapi/lib/model/plugin_json_schema_type.dart delete mode 100644 server/src/utils/bbox.ts diff --git a/cli/src/commands/asset.spec.ts b/cli/src/commands/asset.spec.ts index 21700ef963..f179b350c9 100644 --- a/cli/src/commands/asset.spec.ts +++ b/cli/src/commands/asset.spec.ts @@ -4,7 +4,7 @@ import path from 'node:path'; import { setTimeout as sleep } from 'node:timers/promises'; import { describe, expect, it, MockedFunction, vi } from 'vitest'; -import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk'; +import { AssetRejectReason, AssetUploadAction, checkBulkUpload, defaults, getSupportedMediaTypes } from '@immich/sdk'; import createFetchMock from 'vitest-fetch-mock'; import { @@ -120,7 +120,7 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], @@ -144,10 +144,10 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Reject, + action: AssetUploadAction.Reject, id: testFilePath, assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5', - reason: Reason.Duplicate, + reason: AssetRejectReason.Duplicate, }, ], }); @@ -167,7 +167,7 @@ describe('checkForDuplicates', () => { vi.mocked(checkBulkUpload).mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], @@ -187,7 +187,7 @@ describe('checkForDuplicates', () => { mocked.mockResolvedValue({ results: [ { - action: Action.Accept, + action: AssetUploadAction.Accept, id: testFilePath, }, ], diff --git a/cli/src/commands/asset.ts b/cli/src/commands/asset.ts index 7d4b09b69d..c3ad820547 100644 --- a/cli/src/commands/asset.ts +++ b/cli/src/commands/asset.ts @@ -1,9 +1,9 @@ import { - Action, AssetBulkUploadCheckItem, AssetBulkUploadCheckResult, AssetMediaResponseDto, AssetMediaStatus, + AssetUploadAction, Permission, addAssetsToAlbum, checkBulkUpload, @@ -234,7 +234,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas const results = response.results as AssetBulkUploadCheckResults; for (const { id: filepath, assetId, action } of results) { - if (action === Action.Accept) { + if (action === AssetUploadAction.Accept) { newFiles.push(filepath); } else { // rejects are always duplicates diff --git a/e2e/src/specs/server/api/asset.e2e-spec.ts b/e2e/src/specs/server/api/asset.e2e-spec.ts index 11e825a7cd..2d9a325289 100644 --- a/e2e/src/specs/server/api/asset.e2e-spec.ts +++ b/e2e/src/specs/server/api/asset.e2e-spec.ts @@ -95,8 +95,8 @@ describe('/asset', () => { utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken, { isFavorite: true, - fileCreatedAt: yesterday.toISO(), - fileModifiedAt: yesterday.toISO(), + fileCreatedAt: yesterday.toUTC().toISO(), + fileModifiedAt: yesterday.toUTC().toISO(), assetData: { filename: 'example.mp4' }, }), utils.createAsset(user1.accessToken), @@ -435,7 +435,8 @@ describe('/asset', () => { it('should require access', async () => { const { status, body } = await request(app) .put(`/assets/${user2Assets[0].id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({}); expect(status).toBe(400); expect(body).toEqual(errorDto.noPermission); }); diff --git a/e2e/src/specs/server/api/library.e2e-spec.ts b/e2e/src/specs/server/api/library.e2e-spec.ts index 4d67a84647..719436a66d 100644 --- a/e2e/src/specs/server/api/library.e2e-spec.ts +++ b/e2e/src/specs/server/api/library.e2e-spec.ts @@ -110,7 +110,7 @@ describe('/libraries', () => { }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items'])); }); it('should not create an external library with duplicate exclusion patterns', async () => { @@ -125,7 +125,7 @@ describe('/libraries', () => { }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items'])); }); }); @@ -157,7 +157,7 @@ describe('/libraries', () => { .send({ name: '' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['name should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[name] Too small: expected string to have >=1 characters'])); }); it('should change the import paths', async () => { @@ -181,7 +181,7 @@ describe('/libraries', () => { .send({ importPaths: [''] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in importPaths should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array items must not be empty'])); }); it('should reject duplicate import paths', async () => { @@ -191,7 +191,7 @@ describe('/libraries', () => { .send({ importPaths: ['/path', '/path'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items'])); }); it('should change the exclusion pattern', async () => { @@ -215,7 +215,7 @@ describe('/libraries', () => { .send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items'])); }); it('should reject an empty exclusion pattern', async () => { @@ -225,7 +225,7 @@ describe('/libraries', () => { .send({ exclusionPatterns: [''] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in exclusionPatterns should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array items must not be empty'])); }); }); diff --git a/e2e/src/specs/server/api/map.e2e-spec.ts b/e2e/src/specs/server/api/map.e2e-spec.ts index 977638aa24..c280deb134 100644 --- a/e2e/src/specs/server/api/map.e2e-spec.ts +++ b/e2e/src/specs/server/api/map.e2e-spec.ts @@ -109,7 +109,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lon=123') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN'])); }); it('should throw an error if a lat is not a number', async () => { @@ -117,7 +117,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=abc&lon=123.456') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN'])); }); it('should throw an error if a lat is out of range', async () => { @@ -125,7 +125,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=91&lon=123.456') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90'])); + expect(body).toEqual(errorDto.badRequest(['[lat] Too big: expected number to be <=90'])); }); it('should throw an error if a lon is not provided', async () => { @@ -133,7 +133,7 @@ describe('/map', () => { .get('/map/reverse-geocode?lat=75') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['lon must be a number between -180 and 180'])); + expect(body).toEqual(errorDto.badRequest(['[lon] Invalid input: expected number, received NaN'])); }); const reverseGeocodeTestCases = [ diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts index ae9064375f..a0ae1dc819 100644 --- a/e2e/src/specs/server/api/oauth.e2e-spec.ts +++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts @@ -101,7 +101,7 @@ describe(`/oauth`, () => { it(`should throw an error if a redirect uri is not provided`, async () => { const { status, body } = await request(app).post('/oauth/authorize').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[redirectUri] Invalid input: expected string, received undefined'])); }); it('should return a redirect uri', async () => { @@ -123,13 +123,13 @@ describe(`/oauth`, () => { it(`should throw an error if a url is not provided`, async () => { const { status, body } = await request(app).post('/oauth/callback').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['url must be a string', 'url should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[url] Invalid input: expected string, received undefined'])); }); it(`should throw an error if the url is empty`, async () => { const { status, body } = await request(app).post('/oauth/callback').send({ url: '' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['url should not be empty'])); + expect(body).toEqual(errorDto.badRequest(['[url] Too small: expected string to have >=1 characters'])); }); it(`should throw an error if the state is not provided`, async () => { diff --git a/e2e/src/specs/server/api/tag.e2e-spec.ts b/e2e/src/specs/server/api/tag.e2e-spec.ts index d69536f3a3..7b5a2f16de 100644 --- a/e2e/src/specs/server/api/tag.e2e-spec.ts +++ b/e2e/src/specs/server/api/tag.e2e-spec.ts @@ -309,7 +309,7 @@ describe('/tags', () => { .get(`/tags/${uuidDto.invalid}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should get tag details', async () => { @@ -427,7 +427,7 @@ describe('/tags', () => { .delete(`/tags/${uuidDto.invalid}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should delete a tag', async () => { diff --git a/e2e/src/specs/server/api/user-admin.e2e-spec.ts b/e2e/src/specs/server/api/user-admin.e2e-spec.ts index 793c508a36..6751b21e84 100644 --- a/e2e/src/specs/server/api/user-admin.e2e-spec.ts +++ b/e2e/src/specs/server/api/user-admin.e2e-spec.ts @@ -287,7 +287,8 @@ describe('/admin/users', () => { it('should delete user', async () => { const { status, body } = await request(app) .delete(`/admin/users/${userToDelete.userId}`) - .set('Authorization', `Bearer ${admin.accessToken}`); + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({}); expect(status).toBe(200); expect(body).toMatchObject({ diff --git a/e2e/src/specs/server/api/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts index 3f280dddf5..ee13a29c1b 100644 --- a/e2e/src/specs/server/api/user.e2e-spec.ts +++ b/e2e/src/specs/server/api/user.e2e-spec.ts @@ -178,7 +178,9 @@ describe('/users', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['download.archiveSize must be an integer number'])); + expect(body).toEqual( + errorDto.badRequest(['[download.archiveSize] Invalid input: expected int, received number']), + ); }); it('should update download archive size', async () => { @@ -204,7 +206,9 @@ describe('/users', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['download.includeEmbeddedVideos must be a boolean value'])); + expect(body).toEqual( + errorDto.badRequest(['[download.includeEmbeddedVideos] Invalid input: expected boolean, received number']), + ); }); it('should update download include embedded videos', async () => { diff --git a/e2e/src/ui/mock-network/broken-asset-network.ts b/e2e/src/ui/mock-network/broken-asset-network.ts index 1494b40531..eff93a8d72 100644 --- a/e2e/src/ui/mock-network/broken-asset-network.ts +++ b/e2e/src/ui/mock-network/broken-asset-network.ts @@ -69,7 +69,7 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => { tags: [], people: [], unassignedFaces: [], - stack: null, + stack: undefined, isOffline: false, hasMetadata: true, duplicateId: null, diff --git a/mobile/lib/domain/utils/migrate_cloud_ids.dart b/mobile/lib/domain/utils/migrate_cloud_ids.dart index 33a8eca94d..32188b4838 100644 --- a/mobile/lib/domain/utils/migrate_cloud_ids.dart +++ b/mobile/lib/domain/utils/migrate_cloud_ids.dart @@ -80,12 +80,14 @@ Future _processCloudIdMappingsInBatches( AssetMetadataBulkUpsertItemDto( assetId: mapping.remoteAssetId, key: kMobileMetadataKey, - value: RemoteAssetMobileAppMetadata( - cloudId: mapping.localAsset.cloudId, - createdAt: mapping.localAsset.createdAt.toIso8601String(), - adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(), - latitude: mapping.localAsset.latitude?.toString(), - longitude: mapping.localAsset.longitude?.toString(), + value: Map.from( + RemoteAssetMobileAppMetadata( + cloudId: mapping.localAsset.cloudId, + createdAt: mapping.localAsset.createdAt.toIso8601String(), + adjustmentTime: mapping.localAsset.adjustmentTime?.toIso8601String(), + latitude: mapping.localAsset.latitude?.toString(), + longitude: mapping.localAsset.longitude?.toString(), + ).toJson(), ), ), ); diff --git a/mobile/lib/repositories/album_api.repository.dart b/mobile/lib/repositories/album_api.repository.dart index 525f0906ba..367c2447f2 100644 --- a/mobile/lib/repositories/album_api.repository.dart +++ b/mobile/lib/repositories/album_api.repository.dart @@ -97,7 +97,7 @@ class AlbumApiRepository extends ApiRepository { for (final result in response) { if (result.success) { added.add(result.id); - } else if (result.error == BulkIdResponseDtoErrorEnum.duplicate) { + } else if (result.error == BulkIdErrorReason.duplicate) { duplicates.add(result.id); } } diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 090889ff32..38c805a42e 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -5,13 +5,13 @@ dynamic upgradeDto(dynamic value, String targetType) { case 'UserPreferencesResponseDto': if (value is Map) { addDefault(value, 'download.includeEmbeddedVideos', false); - addDefault(value, 'folders', FoldersResponse().toJson()); - addDefault(value, 'memories', MemoriesResponse().toJson()); - addDefault(value, 'ratings', RatingsResponse().toJson()); - addDefault(value, 'people', PeopleResponse().toJson()); - addDefault(value, 'tags', TagsResponse().toJson()); - addDefault(value, 'sharedLinks', SharedLinksResponse().toJson()); - addDefault(value, 'cast', CastResponse().toJson()); + addDefault(value, 'folders', FoldersResponse(enabled: false, sidebarWeb: false).toJson()); + addDefault(value, 'memories', MemoriesResponse(enabled: true, duration: 5).toJson()); + addDefault(value, 'ratings', RatingsResponse(enabled: false).toJson()); + addDefault(value, 'people', PeopleResponse(enabled: true, sidebarWeb: false).toJson()); + addDefault(value, 'tags', TagsResponse(enabled: false, sidebarWeb: false).toJson()); + addDefault(value, 'sharedLinks', SharedLinksResponse(enabled: true, sidebarWeb: false).toJson()); + addDefault(value, 'cast', CastResponse(gCastEnabled: false).toJson()); addDefault(value, 'albums', {'defaultAssetOrder': 'desc'}); } break; diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index d01e743a3f..b1df5f240c 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -371,6 +371,7 @@ Class | Method | HTTP request | Description - [AssetFaceUpdateItem](doc//AssetFaceUpdateItem.md) - [AssetFaceWithoutPersonResponseDto](doc//AssetFaceWithoutPersonResponseDto.md) - [AssetFullSyncDto](doc//AssetFullSyncDto.md) + - [AssetIdErrorReason](doc//AssetIdErrorReason.md) - [AssetIdsDto](doc//AssetIdsDto.md) - [AssetIdsResponseDto](doc//AssetIdsResponseDto.md) - [AssetJobName](doc//AssetJobName.md) @@ -388,10 +389,12 @@ Class | Method | HTTP request | Description - [AssetMetadataUpsertItemDto](doc//AssetMetadataUpsertItemDto.md) - [AssetOcrResponseDto](doc//AssetOcrResponseDto.md) - [AssetOrder](doc//AssetOrder.md) + - [AssetRejectReason](doc//AssetRejectReason.md) - [AssetResponseDto](doc//AssetResponseDto.md) - [AssetStackResponseDto](doc//AssetStackResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetTypeEnum](doc//AssetTypeEnum.md) + - [AssetUploadAction](doc//AssetUploadAction.md) - [AssetVisibility](doc//AssetVisibility.md) - [AudioCodec](doc//AudioCodec.md) - [AuthStatusResponseDto](doc//AuthStatusResponseDto.md) @@ -440,7 +443,6 @@ Class | Method | HTTP request | Description - [LibraryResponseDto](doc//LibraryResponseDto.md) - [LibraryStatsResponseDto](doc//LibraryStatsResponseDto.md) - [LicenseKeyDto](doc//LicenseKeyDto.md) - - [LicenseResponseDto](doc//LicenseResponseDto.md) - [LogLevel](doc//LogLevel.md) - [LoginCredentialDto](doc//LoginCredentialDto.md) - [LoginResponseDto](doc//LoginResponseDto.md) @@ -504,6 +506,10 @@ Class | Method | HTTP request | Description - [PluginActionResponseDto](doc//PluginActionResponseDto.md) - [PluginContextType](doc//PluginContextType.md) - [PluginFilterResponseDto](doc//PluginFilterResponseDto.md) + - [PluginJsonSchema](doc//PluginJsonSchema.md) + - [PluginJsonSchemaProperty](doc//PluginJsonSchemaProperty.md) + - [PluginJsonSchemaPropertyAdditionalProperties](doc//PluginJsonSchemaPropertyAdditionalProperties.md) + - [PluginJsonSchemaType](doc//PluginJsonSchemaType.md) - [PluginResponseDto](doc//PluginResponseDto.md) - [PluginTriggerResponseDto](doc//PluginTriggerResponseDto.md) - [PluginTriggerType](doc//PluginTriggerType.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 6b554fb644..9403852ef0 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -109,6 +109,7 @@ part 'model/asset_face_update_dto.dart'; part 'model/asset_face_update_item.dart'; part 'model/asset_face_without_person_response_dto.dart'; part 'model/asset_full_sync_dto.dart'; +part 'model/asset_id_error_reason.dart'; part 'model/asset_ids_dto.dart'; part 'model/asset_ids_response_dto.dart'; part 'model/asset_job_name.dart'; @@ -126,10 +127,12 @@ part 'model/asset_metadata_upsert_dto.dart'; part 'model/asset_metadata_upsert_item_dto.dart'; part 'model/asset_ocr_response_dto.dart'; part 'model/asset_order.dart'; +part 'model/asset_reject_reason.dart'; part 'model/asset_response_dto.dart'; part 'model/asset_stack_response_dto.dart'; part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; +part 'model/asset_upload_action.dart'; part 'model/asset_visibility.dart'; part 'model/audio_codec.dart'; part 'model/auth_status_response_dto.dart'; @@ -178,7 +181,6 @@ part 'model/job_settings_dto.dart'; part 'model/library_response_dto.dart'; part 'model/library_stats_response_dto.dart'; part 'model/license_key_dto.dart'; -part 'model/license_response_dto.dart'; part 'model/log_level.dart'; part 'model/login_credential_dto.dart'; part 'model/login_response_dto.dart'; @@ -242,6 +244,10 @@ part 'model/places_response_dto.dart'; part 'model/plugin_action_response_dto.dart'; part 'model/plugin_context_type.dart'; part 'model/plugin_filter_response_dto.dart'; +part 'model/plugin_json_schema.dart'; +part 'model/plugin_json_schema_property.dart'; +part 'model/plugin_json_schema_property_additional_properties.dart'; +part 'model/plugin_json_schema_type.dart'; part 'model/plugin_response_dto.dart'; part 'model/plugin_trigger_response_dto.dart'; part 'model/plugin_trigger_type.dart'; diff --git a/mobile/openapi/lib/api/activities_api.dart b/mobile/openapi/lib/api/activities_api.dart index 697598ac97..e0a393948c 100644 --- a/mobile/openapi/lib/api/activities_api.dart +++ b/mobile/openapi/lib/api/activities_api.dart @@ -136,10 +136,8 @@ class ActivitiesApi { /// Asset ID (if activity is for an asset) /// /// * [ReactionLevel] level: - /// Filter by activity level /// /// * [ReactionType] type: - /// Filter by activity type /// /// * [String] userId: /// Filter by user ID @@ -195,10 +193,8 @@ class ActivitiesApi { /// Asset ID (if activity is for an asset) /// /// * [ReactionLevel] level: - /// Filter by activity level /// /// * [ReactionType] type: - /// Filter by activity type /// /// * [String] userId: /// Filter by user ID diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index a026b99028..831c19683d 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -864,7 +864,6 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getAssetStatisticsWithHttpInfo({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets/statistics'; @@ -913,7 +912,6 @@ class AssetsApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getAssetStatistics({ bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { final response = await getAssetStatisticsWithHttpInfo( isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -1592,7 +1590,6 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - /// Asset visibility Future uploadAssetWithHttpInfo(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/assets'; @@ -1731,7 +1728,6 @@ class AssetsApi { /// Sidecar file data /// /// * [AssetVisibility] visibility: - /// Asset visibility Future uploadAsset(MultipartFile assetData, String deviceAssetId, String deviceId, DateTime fileCreatedAt, DateTime fileModifiedAt, { String? key, String? slug, String? xImmichChecksum, String? duration, String? filename, bool? isFavorite, String? livePhotoVideoId, List? metadata, MultipartFile? sidecarData, AssetVisibility? visibility, }) async { final response = await uploadAssetWithHttpInfo(assetData, deviceAssetId, deviceId, fileCreatedAt, fileModifiedAt, key: key, slug: slug, xImmichChecksum: xImmichChecksum, duration: duration, filename: filename, isFavorite: isFavorite, livePhotoVideoId: livePhotoVideoId, metadata: metadata, sidecarData: sidecarData, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -1763,7 +1759,6 @@ class AssetsApi { /// * [String] key: /// /// * [AssetMediaSize] size: - /// Asset media size /// /// * [String] slug: Future viewAssetWithHttpInfo(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { @@ -1819,7 +1814,6 @@ class AssetsApi { /// * [String] key: /// /// * [AssetMediaSize] size: - /// Asset media size /// /// * [String] slug: Future viewAsset(String id, { bool? edited, String? key, AssetMediaSize? size, String? slug, }) async { diff --git a/mobile/openapi/lib/api/database_backups_admin_api.dart b/mobile/openapi/lib/api/database_backups_admin_api.dart index fbd485f86f..768185db1e 100644 --- a/mobile/openapi/lib/api/database_backups_admin_api.dart +++ b/mobile/openapi/lib/api/database_backups_admin_api.dart @@ -218,6 +218,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [MultipartFile] file: + /// Database backup file Future uploadDatabaseBackupWithHttpInfo({ MultipartFile? file, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/database-backups/upload'; @@ -260,6 +261,7 @@ class DatabaseBackupsAdminApi { /// Parameters: /// /// * [MultipartFile] file: + /// Database backup file Future uploadDatabaseBackup({ MultipartFile? file, }) async { final response = await uploadDatabaseBackupWithHttpInfo( file: file, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index 33bcaf062c..94b7e2e738 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -520,7 +520,6 @@ class DeprecatedApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { @@ -556,7 +555,6 @@ class DeprecatedApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { diff --git a/mobile/openapi/lib/api/jobs_api.dart b/mobile/openapi/lib/api/jobs_api.dart index 41517f8144..9dda59a883 100644 --- a/mobile/openapi/lib/api/jobs_api.dart +++ b/mobile/openapi/lib/api/jobs_api.dart @@ -121,7 +121,6 @@ class JobsApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacyWithHttpInfo(QueueName name, QueueCommandDto queueCommandDto,) async { @@ -157,7 +156,6 @@ class JobsApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueCommandDto] queueCommandDto (required): Future runQueueCommandLegacy(QueueName name, QueueCommandDto queueCommandDto,) async { diff --git a/mobile/openapi/lib/api/memories_api.dart b/mobile/openapi/lib/api/memories_api.dart index 913205428e..0cd96ac442 100644 --- a/mobile/openapi/lib/api/memories_api.dart +++ b/mobile/openapi/lib/api/memories_api.dart @@ -260,13 +260,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future memoriesStatisticsWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories/statistics'; @@ -327,13 +325,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future memoriesStatistics({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { final response = await memoriesStatisticsWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { @@ -431,13 +427,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future searchMemoriesWithHttpInfo({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { // ignore: prefer_const_declarations final apiPath = r'/memories'; @@ -498,13 +492,11 @@ class MemoriesApi { /// Include trashed memories /// /// * [MemorySearchOrder] order: - /// Sort order /// /// * [int] size: /// Number of memories to return /// /// * [MemoryType] type: - /// Memory type Future?> searchMemories({ DateTime? for_, bool? isSaved, bool? isTrashed, MemorySearchOrder? order, int? size, MemoryType? type, }) async { final response = await searchMemoriesWithHttpInfo( for_: for_, isSaved: isSaved, isTrashed: isTrashed, order: order, size: size, type: type, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/notifications_api.dart b/mobile/openapi/lib/api/notifications_api.dart index d4e2b1d80f..ab0be3e8f3 100644 --- a/mobile/openapi/lib/api/notifications_api.dart +++ b/mobile/openapi/lib/api/notifications_api.dart @@ -182,10 +182,8 @@ class NotificationsApi { /// Filter by notification ID /// /// * [NotificationLevel] level: - /// Filter by notification level /// /// * [NotificationType] type: - /// Filter by notification type /// /// * [bool] unread: /// Filter by unread status @@ -237,10 +235,8 @@ class NotificationsApi { /// Filter by notification ID /// /// * [NotificationLevel] level: - /// Filter by notification level /// /// * [NotificationType] type: - /// Filter by notification type /// /// * [bool] unread: /// Filter by unread status diff --git a/mobile/openapi/lib/api/partners_api.dart b/mobile/openapi/lib/api/partners_api.dart index 3b15b90909..7d18f6d867 100644 --- a/mobile/openapi/lib/api/partners_api.dart +++ b/mobile/openapi/lib/api/partners_api.dart @@ -138,7 +138,6 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - /// Partner direction Future getPartnersWithHttpInfo(PartnerDirection direction,) async { // ignore: prefer_const_declarations final apiPath = r'/partners'; @@ -173,7 +172,6 @@ class PartnersApi { /// Parameters: /// /// * [PartnerDirection] direction (required): - /// Partner direction Future?> getPartners(PartnerDirection direction,) async { final response = await getPartnersWithHttpInfo(direction,); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/queues_api.dart b/mobile/openapi/lib/api/queues_api.dart index ecb556e434..1312cb5952 100644 --- a/mobile/openapi/lib/api/queues_api.dart +++ b/mobile/openapi/lib/api/queues_api.dart @@ -25,7 +25,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueDeleteDto] queueDeleteDto (required): Future emptyQueueWithHttpInfo(QueueName name, QueueDeleteDto queueDeleteDto,) async { @@ -61,7 +60,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueDeleteDto] queueDeleteDto (required): Future emptyQueue(QueueName name, QueueDeleteDto queueDeleteDto,) async { @@ -80,7 +78,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name Future getQueueWithHttpInfo(QueueName name,) async { // ignore: prefer_const_declarations final apiPath = r'/queues/{name}' @@ -114,7 +111,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name Future getQueue(QueueName name,) async { final response = await getQueueWithHttpInfo(name,); if (response.statusCode >= HttpStatus.badRequest) { @@ -139,7 +135,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [List] status: /// Filter jobs by status @@ -180,7 +175,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [List] status: /// Filter jobs by status @@ -262,7 +256,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueUpdateDto] queueUpdateDto (required): Future updateQueueWithHttpInfo(QueueName name, QueueUpdateDto queueUpdateDto,) async { @@ -298,7 +291,6 @@ class QueuesApi { /// Parameters: /// /// * [QueueName] name (required): - /// Queue name /// /// * [QueueUpdateDto] queueUpdateDto (required): Future updateQueue(QueueName name, QueueUpdateDto queueUpdateDto,) async { diff --git a/mobile/openapi/lib/api/search_api.dart b/mobile/openapi/lib/api/search_api.dart index 085958de66..46fc8594a8 100644 --- a/mobile/openapi/lib/api/search_api.dart +++ b/mobile/openapi/lib/api/search_api.dart @@ -127,7 +127,6 @@ class SearchApi { /// Parameters: /// /// * [SearchSuggestionType] type (required): - /// Suggestion type /// /// * [String] country: /// Filter by country @@ -198,7 +197,6 @@ class SearchApi { /// Parameters: /// /// * [SearchSuggestionType] type (required): - /// Suggestion type /// /// * [String] country: /// Filter by country @@ -434,7 +432,6 @@ class SearchApi { /// Filter by trash date (before) /// /// * [AssetTypeEnum] type: - /// Asset type filter /// /// * [DateTime] updatedAfter: /// Filter by update date (after) @@ -443,7 +440,6 @@ class SearchApi { /// Filter by update date (before) /// /// * [AssetVisibility] visibility: - /// Filter by visibility /// /// * [bool] withDeleted: /// Include deleted assets @@ -657,7 +653,6 @@ class SearchApi { /// Filter by trash date (before) /// /// * [AssetTypeEnum] type: - /// Asset type filter /// /// * [DateTime] updatedAfter: /// Filter by update date (after) @@ -666,7 +661,6 @@ class SearchApi { /// Filter by update date (before) /// /// * [AssetVisibility] visibility: - /// Filter by visibility /// /// * [bool] withDeleted: /// Include deleted assets diff --git a/mobile/openapi/lib/api/server_api.dart b/mobile/openapi/lib/api/server_api.dart index f5b70a9ea4..4e43ec28eb 100644 --- a/mobile/openapi/lib/api/server_api.dart +++ b/mobile/openapi/lib/api/server_api.dart @@ -281,7 +281,7 @@ class ServerApi { /// Get product key /// /// Retrieve information about whether the server currently has a product key registered. - Future getServerLicense() async { + Future getServerLicense() async { final response = await getServerLicenseWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -290,7 +290,7 @@ class ServerApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -724,7 +724,7 @@ class ServerApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setServerLicense(LicenseKeyDto licenseKeyDto,) async { + Future setServerLicense(LicenseKeyDto licenseKeyDto,) async { final response = await setServerLicenseWithHttpInfo(licenseKeyDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -733,7 +733,7 @@ class ServerApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index f82c362ff7..30a4c123f1 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -25,7 +25,7 @@ class TimelineApi { /// Parameters: /// /// * [String] timeBucket (required): - /// Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024) + /// Time bucket identifier in YYYY-MM-DD format /// /// * [String] albumId: /// Filter assets belonging to a specific album @@ -142,7 +142,7 @@ class TimelineApi { /// Parameters: /// /// * [String] timeBucket (required): - /// Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024) + /// Time bucket identifier in YYYY-MM-DD format /// /// * [String] albumId: /// Filter assets belonging to a specific album diff --git a/mobile/openapi/lib/api/users_admin_api.dart b/mobile/openapi/lib/api/users_admin_api.dart index 59a4b60096..5e165ffd5d 100644 --- a/mobile/openapi/lib/api/users_admin_api.dart +++ b/mobile/openapi/lib/api/users_admin_api.dart @@ -324,7 +324,6 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getUserStatisticsAdminWithHttpInfo(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { // ignore: prefer_const_declarations final apiPath = r'/admin/users/{id}/statistics' @@ -376,7 +375,6 @@ class UsersAdminApi { /// Filter by trash status /// /// * [AssetVisibility] visibility: - /// Filter by visibility Future getUserStatisticsAdmin(String id, { bool? isFavorite, bool? isTrashed, AssetVisibility? visibility, }) async { final response = await getUserStatisticsAdminWithHttpInfo(id, isFavorite: isFavorite, isTrashed: isTrashed, visibility: visibility, ); if (response.statusCode >= HttpStatus.badRequest) { diff --git a/mobile/openapi/lib/api/users_api.dart b/mobile/openapi/lib/api/users_api.dart index 7ccae02c76..1d905b1e22 100644 --- a/mobile/openapi/lib/api/users_api.dart +++ b/mobile/openapi/lib/api/users_api.dart @@ -447,7 +447,7 @@ class UsersApi { /// Retrieve user product key /// /// Retrieve information about whether the current user has a registered product key. - Future getUserLicense() async { + Future getUserLicense() async { final response = await getUserLicenseWithHttpInfo(); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -456,7 +456,7 @@ class UsersApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; @@ -602,7 +602,7 @@ class UsersApi { /// Parameters: /// /// * [LicenseKeyDto] licenseKeyDto (required): - Future setUserLicense(LicenseKeyDto licenseKeyDto,) async { + Future setUserLicense(LicenseKeyDto licenseKeyDto,) async { final response = await setUserLicenseWithHttpInfo(licenseKeyDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); @@ -611,7 +611,7 @@ class UsersApi { // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'LicenseResponseDto',) as LicenseResponseDto; + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'UserLicense',) as UserLicense; } return null; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 48e5f5874b..3ed1f7529f 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -264,6 +264,8 @@ class ApiClient { return AssetFaceWithoutPersonResponseDto.fromJson(value); case 'AssetFullSyncDto': return AssetFullSyncDto.fromJson(value); + case 'AssetIdErrorReason': + return AssetIdErrorReasonTypeTransformer().decode(value); case 'AssetIdsDto': return AssetIdsDto.fromJson(value); case 'AssetIdsResponseDto': @@ -298,6 +300,8 @@ class ApiClient { return AssetOcrResponseDto.fromJson(value); case 'AssetOrder': return AssetOrderTypeTransformer().decode(value); + case 'AssetRejectReason': + return AssetRejectReasonTypeTransformer().decode(value); case 'AssetResponseDto': return AssetResponseDto.fromJson(value); case 'AssetStackResponseDto': @@ -306,6 +310,8 @@ class ApiClient { return AssetStatsResponseDto.fromJson(value); case 'AssetTypeEnum': return AssetTypeEnumTypeTransformer().decode(value); + case 'AssetUploadAction': + return AssetUploadActionTypeTransformer().decode(value); case 'AssetVisibility': return AssetVisibilityTypeTransformer().decode(value); case 'AudioCodec': @@ -402,8 +408,6 @@ class ApiClient { return LibraryStatsResponseDto.fromJson(value); case 'LicenseKeyDto': return LicenseKeyDto.fromJson(value); - case 'LicenseResponseDto': - return LicenseResponseDto.fromJson(value); case 'LogLevel': return LogLevelTypeTransformer().decode(value); case 'LoginCredentialDto': @@ -530,6 +534,14 @@ class ApiClient { return PluginContextTypeTypeTransformer().decode(value); case 'PluginFilterResponseDto': return PluginFilterResponseDto.fromJson(value); + case 'PluginJsonSchema': + return PluginJsonSchema.fromJson(value); + case 'PluginJsonSchemaProperty': + return PluginJsonSchemaProperty.fromJson(value); + case 'PluginJsonSchemaPropertyAdditionalProperties': + return PluginJsonSchemaPropertyAdditionalProperties.fromJson(value); + case 'PluginJsonSchemaType': + return PluginJsonSchemaTypeTypeTransformer().decode(value); case 'PluginResponseDto': return PluginResponseDto.fromJson(value); case 'PluginTriggerResponseDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 830325a5b6..3b36b23d6c 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -61,6 +61,9 @@ String parameterToString(dynamic value) { if (value is AssetEditAction) { return AssetEditActionTypeTransformer().encode(value).toString(); } + if (value is AssetIdErrorReason) { + return AssetIdErrorReasonTypeTransformer().encode(value).toString(); + } if (value is AssetJobName) { return AssetJobNameTypeTransformer().encode(value).toString(); } @@ -73,9 +76,15 @@ String parameterToString(dynamic value) { if (value is AssetOrder) { return AssetOrderTypeTransformer().encode(value).toString(); } + if (value is AssetRejectReason) { + return AssetRejectReasonTypeTransformer().encode(value).toString(); + } if (value is AssetTypeEnum) { return AssetTypeEnumTypeTransformer().encode(value).toString(); } + if (value is AssetUploadAction) { + return AssetUploadActionTypeTransformer().encode(value).toString(); + } if (value is AssetVisibility) { return AssetVisibilityTypeTransformer().encode(value).toString(); } @@ -133,6 +142,9 @@ String parameterToString(dynamic value) { if (value is PluginContextType) { return PluginContextTypeTypeTransformer().encode(value).toString(); } + if (value is PluginJsonSchemaType) { + return PluginJsonSchemaTypeTypeTransformer().encode(value).toString(); + } if (value is PluginTriggerType) { return PluginTriggerTypeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/activity_create_dto.dart b/mobile/openapi/lib/model/activity_create_dto.dart index fb4b6d084e..bc220e64ce 100644 --- a/mobile/openapi/lib/model/activity_create_dto.dart +++ b/mobile/openapi/lib/model/activity_create_dto.dart @@ -40,7 +40,6 @@ class ActivityCreateDto { /// String? comment; - /// Activity type (like or comment) ReactionType type; @override diff --git a/mobile/openapi/lib/model/activity_response_dto.dart b/mobile/openapi/lib/model/activity_response_dto.dart index dadb45d8ac..1b0e279ab7 100644 --- a/mobile/openapi/lib/model/activity_response_dto.dart +++ b/mobile/openapi/lib/model/activity_response_dto.dart @@ -33,7 +33,6 @@ class ActivityResponseDto { /// Activity ID String id; - /// Activity type ReactionType type; UserResponseDto user; @@ -72,7 +71,9 @@ class ActivityResponseDto { } else { // json[r'comment'] = null; } - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'type'] = this.type; json[r'user'] = this.user; @@ -90,7 +91,7 @@ class ActivityResponseDto { return ActivityResponseDto( assetId: mapValueOfType(json, r'assetId'), comment: mapValueOfType(json, r'comment'), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, type: ReactionType.fromJson(json[r'type'])!, user: UserResponseDto.fromJson(json[r'user'])!, diff --git a/mobile/openapi/lib/model/activity_statistics_response_dto.dart b/mobile/openapi/lib/model/activity_statistics_response_dto.dart index 15ad2a170e..d9ac019ee2 100644 --- a/mobile/openapi/lib/model/activity_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/activity_statistics_response_dto.dart @@ -18,9 +18,15 @@ class ActivityStatisticsResponseDto { }); /// Number of comments + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int comments; /// Number of likes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int likes; @override diff --git a/mobile/openapi/lib/model/album_response_dto.dart b/mobile/openapi/lib/model/album_response_dto.dart index 43e686fbdc..ca0c087027 100644 --- a/mobile/openapi/lib/model/album_response_dto.dart +++ b/mobile/openapi/lib/model/album_response_dto.dart @@ -43,6 +43,9 @@ class AlbumResponseDto { List albumUsers; /// Number of assets + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; List assets; @@ -82,7 +85,6 @@ class AlbumResponseDto { /// DateTime? lastModifiedAssetTimestamp; - /// Asset sort order /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/album_statistics_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart index 127334e687..0f440d572d 100644 --- a/mobile/openapi/lib/model/album_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/album_statistics_response_dto.dart @@ -19,12 +19,21 @@ class AlbumStatisticsResponseDto { }); /// Number of non-shared albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int notShared; /// Number of owned albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int owned; /// Number of shared albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int shared; @override diff --git a/mobile/openapi/lib/model/album_user_add_dto.dart b/mobile/openapi/lib/model/album_user_add_dto.dart index c448a0b4b7..ee457905bd 100644 --- a/mobile/openapi/lib/model/album_user_add_dto.dart +++ b/mobile/openapi/lib/model/album_user_add_dto.dart @@ -13,12 +13,17 @@ part of openapi.api; class AlbumUserAddDto { /// Returns a new [AlbumUserAddDto] instance. AlbumUserAddDto({ - this.role = AlbumUserRole.editor, + this.role, required this.userId, }); - /// Album user role - AlbumUserRole role; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AlbumUserRole? role; /// User ID String userId; @@ -31,7 +36,7 @@ class AlbumUserAddDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (role.hashCode) + + (role == null ? 0 : role!.hashCode) + (userId.hashCode); @override @@ -39,7 +44,11 @@ class AlbumUserAddDto { Map toJson() { final json = {}; + if (this.role != null) { json[r'role'] = this.role; + } else { + // json[r'role'] = null; + } json[r'userId'] = this.userId; return json; } @@ -53,7 +62,7 @@ class AlbumUserAddDto { final json = value.cast(); return AlbumUserAddDto( - role: AlbumUserRole.fromJson(json[r'role']) ?? AlbumUserRole.editor, + role: AlbumUserRole.fromJson(json[r'role']), userId: mapValueOfType(json, r'userId')!, ); } diff --git a/mobile/openapi/lib/model/album_user_create_dto.dart b/mobile/openapi/lib/model/album_user_create_dto.dart index 8006748341..26aa35ae78 100644 --- a/mobile/openapi/lib/model/album_user_create_dto.dart +++ b/mobile/openapi/lib/model/album_user_create_dto.dart @@ -17,7 +17,6 @@ class AlbumUserCreateDto { required this.userId, }); - /// Album user role AlbumUserRole role; /// User ID diff --git a/mobile/openapi/lib/model/album_user_response_dto.dart b/mobile/openapi/lib/model/album_user_response_dto.dart index 8d0c01cfb8..bbae03fba7 100644 --- a/mobile/openapi/lib/model/album_user_response_dto.dart +++ b/mobile/openapi/lib/model/album_user_response_dto.dart @@ -17,7 +17,6 @@ class AlbumUserResponseDto { required this.user, }); - /// Album user role AlbumUserRole role; UserResponseDto user; diff --git a/mobile/openapi/lib/model/albums_add_assets_response_dto.dart b/mobile/openapi/lib/model/albums_add_assets_response_dto.dart index 743a9f0645..99e679222e 100644 --- a/mobile/openapi/lib/model/albums_add_assets_response_dto.dart +++ b/mobile/openapi/lib/model/albums_add_assets_response_dto.dart @@ -17,7 +17,6 @@ class AlbumsAddAssetsResponseDto { required this.success, }); - /// Error reason /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/albums_response.dart b/mobile/openapi/lib/model/albums_response.dart index 520ee171c1..def205de90 100644 --- a/mobile/openapi/lib/model/albums_response.dart +++ b/mobile/openapi/lib/model/albums_response.dart @@ -13,10 +13,9 @@ part of openapi.api; class AlbumsResponse { /// Returns a new [AlbumsResponse] instance. AlbumsResponse({ - this.defaultAssetOrder = AssetOrder.desc, + required this.defaultAssetOrder, }); - /// Default asset order for albums AssetOrder defaultAssetOrder; @override diff --git a/mobile/openapi/lib/model/albums_update.dart b/mobile/openapi/lib/model/albums_update.dart index 107c65dd1e..d61b5c1398 100644 --- a/mobile/openapi/lib/model/albums_update.dart +++ b/mobile/openapi/lib/model/albums_update.dart @@ -16,7 +16,6 @@ class AlbumsUpdate { this.defaultAssetOrder, }); - /// Default asset order for albums /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart index 32ba543342..d5b8bf8b41 100644 --- a/mobile/openapi/lib/model/api_key_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -57,11 +57,15 @@ class APIKeyResponseDto { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'name'] = this.name; json[r'permissions'] = this.permissions; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -74,11 +78,11 @@ class APIKeyResponseDto { final json = value.cast(); return APIKeyResponseDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, permissions: Permission.listFromJson(json[r'permissions']), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index 99bac7abfa..f97300b19f 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -70,6 +70,9 @@ class AssetBulkUpdateDto { /// Latitude coordinate /// + /// Minimum value: -90 + /// Maximum value: 90 + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated /// source code must fall back to having a nullable type. @@ -79,6 +82,9 @@ class AssetBulkUpdateDto { /// Longitude coordinate /// + /// Minimum value: -180 + /// Maximum value: 180 + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated /// source code must fall back to having a nullable type. @@ -90,7 +96,7 @@ class AssetBulkUpdateDto { /// /// Minimum value: -1 /// Maximum value: 5 - num? rating; + int? rating; /// Time zone (IANA timezone) /// @@ -101,7 +107,6 @@ class AssetBulkUpdateDto { /// String? timeZone; - /// Asset visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -217,9 +222,7 @@ class AssetBulkUpdateDto { isFavorite: mapValueOfType(json, r'isFavorite'), latitude: num.parse('${json[r'latitude']}'), longitude: num.parse('${json[r'longitude']}'), - rating: json[r'rating'] == null - ? null - : num.parse('${json[r'rating']}'), + rating: mapValueOfType(json, r'rating'), timeZone: mapValueOfType(json, r'timeZone'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); diff --git a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart index b56370f689..bf3ee8e244 100644 --- a/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart +++ b/mobile/openapi/lib/model/asset_bulk_upload_check_result.dart @@ -20,8 +20,7 @@ class AssetBulkUploadCheckResult { this.reason, }); - /// Upload action - AssetBulkUploadCheckResultActionEnum action; + AssetUploadAction action; /// Existing asset ID if duplicate /// @@ -44,8 +43,13 @@ class AssetBulkUploadCheckResult { /// bool? isTrashed; - /// Rejection reason if rejected - AssetBulkUploadCheckResultReasonEnum? reason; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AssetRejectReason? reason; @override bool operator ==(Object other) => identical(this, other) || other is AssetBulkUploadCheckResult && @@ -98,11 +102,11 @@ class AssetBulkUploadCheckResult { final json = value.cast(); return AssetBulkUploadCheckResult( - action: AssetBulkUploadCheckResultActionEnum.fromJson(json[r'action'])!, + action: AssetUploadAction.fromJson(json[r'action'])!, assetId: mapValueOfType(json, r'assetId'), id: mapValueOfType(json, r'id')!, isTrashed: mapValueOfType(json, r'isTrashed'), - reason: AssetBulkUploadCheckResultReasonEnum.fromJson(json[r'reason']), + reason: AssetRejectReason.fromJson(json[r'reason']), ); } return null; @@ -155,151 +159,3 @@ class AssetBulkUploadCheckResult { }; } -/// Upload action -class AssetBulkUploadCheckResultActionEnum { - /// Instantiate a new enum with the provided [value]. - const AssetBulkUploadCheckResultActionEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const accept = AssetBulkUploadCheckResultActionEnum._(r'accept'); - static const reject = AssetBulkUploadCheckResultActionEnum._(r'reject'); - - /// List of all possible values in this [enum][AssetBulkUploadCheckResultActionEnum]. - static const values = [ - accept, - reject, - ]; - - static AssetBulkUploadCheckResultActionEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultActionEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetBulkUploadCheckResultActionEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultActionEnum] to String, -/// and [decode] dynamic data back to [AssetBulkUploadCheckResultActionEnum]. -class AssetBulkUploadCheckResultActionEnumTypeTransformer { - factory AssetBulkUploadCheckResultActionEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultActionEnumTypeTransformer._(); - - const AssetBulkUploadCheckResultActionEnumTypeTransformer._(); - - String encode(AssetBulkUploadCheckResultActionEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultActionEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetBulkUploadCheckResultActionEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'accept': return AssetBulkUploadCheckResultActionEnum.accept; - case r'reject': return AssetBulkUploadCheckResultActionEnum.reject; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetBulkUploadCheckResultActionEnumTypeTransformer] instance. - static AssetBulkUploadCheckResultActionEnumTypeTransformer? _instance; -} - - -/// Rejection reason if rejected -class AssetBulkUploadCheckResultReasonEnum { - /// Instantiate a new enum with the provided [value]. - const AssetBulkUploadCheckResultReasonEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = AssetBulkUploadCheckResultReasonEnum._(r'duplicate'); - static const unsupportedFormat = AssetBulkUploadCheckResultReasonEnum._(r'unsupported-format'); - - /// List of all possible values in this [enum][AssetBulkUploadCheckResultReasonEnum]. - static const values = [ - duplicate, - unsupportedFormat, - ]; - - static AssetBulkUploadCheckResultReasonEnum? fromJson(dynamic value) => AssetBulkUploadCheckResultReasonEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetBulkUploadCheckResultReasonEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetBulkUploadCheckResultReasonEnum] to String, -/// and [decode] dynamic data back to [AssetBulkUploadCheckResultReasonEnum]. -class AssetBulkUploadCheckResultReasonEnumTypeTransformer { - factory AssetBulkUploadCheckResultReasonEnumTypeTransformer() => _instance ??= const AssetBulkUploadCheckResultReasonEnumTypeTransformer._(); - - const AssetBulkUploadCheckResultReasonEnumTypeTransformer._(); - - String encode(AssetBulkUploadCheckResultReasonEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetBulkUploadCheckResultReasonEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetBulkUploadCheckResultReasonEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return AssetBulkUploadCheckResultReasonEnum.duplicate; - case r'unsupported-format': return AssetBulkUploadCheckResultReasonEnum.unsupportedFormat; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetBulkUploadCheckResultReasonEnumTypeTransformer] instance. - static AssetBulkUploadCheckResultReasonEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/asset_delta_sync_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_dto.dart index 22c09752d2..f59cdc1a67 100644 --- a/mobile/openapi/lib/model/asset_delta_sync_dto.dart +++ b/mobile/openapi/lib/model/asset_delta_sync_dto.dart @@ -39,7 +39,9 @@ class AssetDeltaSyncDto { Map toJson() { final json = {}; - json[r'updatedAfter'] = this.updatedAfter.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter.millisecondsSinceEpoch + : this.updatedAfter.toUtc().toIso8601String(); json[r'userIds'] = this.userIds; return json; } @@ -53,7 +55,7 @@ class AssetDeltaSyncDto { final json = value.cast(); return AssetDeltaSyncDto( - updatedAfter: mapDateTime(json, r'updatedAfter', r'')!, + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, userIds: json[r'userIds'] is Iterable ? (json[r'userIds'] as Iterable).cast().toList(growable: false) : const [], diff --git a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart index 7351840b11..348bae370b 100644 --- a/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart +++ b/mobile/openapi/lib/model/asset_delta_sync_response_dto.dart @@ -24,7 +24,6 @@ class AssetDeltaSyncResponseDto { /// Whether full sync is needed bool needsFullSync; - /// Upserted assets List upserted; @override diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart index 7829de4bd5..2c7bb82c24 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_dto.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto.dart @@ -17,7 +17,6 @@ class AssetEditActionItemDto { required this.parameters, }); - /// Type of edit action to perform AssetEditAction action; AssetEditActionItemDtoParameters parameters; diff --git a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart index fc67aa022f..2086f72929 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_dto_parameters.dart @@ -44,7 +44,6 @@ class AssetEditActionItemDtoParameters { /// Rotation angle in degrees num angle; - /// Axis to mirror along MirrorAxis axis; @override diff --git a/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart index a23a1ef5f3..3315fe8579 100644 --- a/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart +++ b/mobile/openapi/lib/model/asset_edit_action_item_response_dto.dart @@ -18,9 +18,9 @@ class AssetEditActionItemResponseDto { required this.parameters, }); - /// Type of edit action to perform AssetEditAction action; + /// Asset edit ID String id; AssetEditActionItemDtoParameters parameters; diff --git a/mobile/openapi/lib/model/asset_face_create_dto.dart b/mobile/openapi/lib/model/asset_face_create_dto.dart index 3ecc20c699..29c28175cd 100644 --- a/mobile/openapi/lib/model/asset_face_create_dto.dart +++ b/mobile/openapi/lib/model/asset_face_create_dto.dart @@ -27,24 +27,42 @@ class AssetFaceCreateDto { String assetId; /// Face bounding box height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int height; /// Image height in pixels + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Person ID String personId; /// Face bounding box width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int width; /// Face bounding box X coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int x; /// Face bounding box Y coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int y; @override diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart index 61d972a0c4..21b86dfe4e 100644 --- a/mobile/openapi/lib/model/asset_face_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_response_dto.dart @@ -25,30 +25,46 @@ class AssetFaceResponseDto { }); /// Bounding box X1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; /// Bounding box X2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; /// Bounding box Y1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; /// Bounding box Y2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face ID String id; /// Image height in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageWidth; - /// Person associated with face PersonResponseDto? person; - /// Face detection source type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart index 1ae5cef07e..4a4a2a658e 100644 --- a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart +++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart @@ -24,27 +24,44 @@ class AssetFaceWithoutPersonResponseDto { }); /// Bounding box X1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; /// Bounding box X2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; /// Bounding box Y1 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; /// Bounding box Y2 coordinate + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face ID String id; /// Image height in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageHeight; /// Image width in pixels + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int imageWidth; - /// Face detection source type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/asset_full_sync_dto.dart b/mobile/openapi/lib/model/asset_full_sync_dto.dart index 3fabb1cac6..835e063e92 100644 --- a/mobile/openapi/lib/model/asset_full_sync_dto.dart +++ b/mobile/openapi/lib/model/asset_full_sync_dto.dart @@ -31,6 +31,7 @@ class AssetFullSyncDto { /// Maximum number of assets to return /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int limit; /// Sync assets updated until this date @@ -71,7 +72,9 @@ class AssetFullSyncDto { // json[r'lastId'] = null; } json[r'limit'] = this.limit; - json[r'updatedUntil'] = this.updatedUntil.toUtc().toIso8601String(); + json[r'updatedUntil'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedUntil.millisecondsSinceEpoch + : this.updatedUntil.toUtc().toIso8601String(); if (this.userId != null) { json[r'userId'] = this.userId; } else { @@ -91,7 +94,7 @@ class AssetFullSyncDto { return AssetFullSyncDto( lastId: mapValueOfType(json, r'lastId'), limit: mapValueOfType(json, r'limit')!, - updatedUntil: mapDateTime(json, r'updatedUntil', r'')!, + updatedUntil: mapDateTime(json, r'updatedUntil', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, userId: mapValueOfType(json, r'userId'), ); } diff --git a/mobile/openapi/lib/model/asset_id_error_reason.dart b/mobile/openapi/lib/model/asset_id_error_reason.dart new file mode 100644 index 0000000000..c51eab1692 --- /dev/null +++ b/mobile/openapi/lib/model/asset_id_error_reason.dart @@ -0,0 +1,88 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +/// Error reason if failed +class AssetIdErrorReason { + /// Instantiate a new enum with the provided [value]. + const AssetIdErrorReason._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const duplicate = AssetIdErrorReason._(r'duplicate'); + static const noPermission = AssetIdErrorReason._(r'no_permission'); + static const notFound = AssetIdErrorReason._(r'not_found'); + + /// List of all possible values in this [enum][AssetIdErrorReason]. + static const values = [ + duplicate, + noPermission, + notFound, + ]; + + static AssetIdErrorReason? fromJson(dynamic value) => AssetIdErrorReasonTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetIdErrorReason.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetIdErrorReason] to String, +/// and [decode] dynamic data back to [AssetIdErrorReason]. +class AssetIdErrorReasonTypeTransformer { + factory AssetIdErrorReasonTypeTransformer() => _instance ??= const AssetIdErrorReasonTypeTransformer._(); + + const AssetIdErrorReasonTypeTransformer._(); + + String encode(AssetIdErrorReason data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetIdErrorReason. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetIdErrorReason? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'duplicate': return AssetIdErrorReason.duplicate; + case r'no_permission': return AssetIdErrorReason.noPermission; + case r'not_found': return AssetIdErrorReason.notFound; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetIdErrorReasonTypeTransformer] instance. + static AssetIdErrorReasonTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/asset_ids_response_dto.dart b/mobile/openapi/lib/model/asset_ids_response_dto.dart index 9745283021..cafe1b21b9 100644 --- a/mobile/openapi/lib/model/asset_ids_response_dto.dart +++ b/mobile/openapi/lib/model/asset_ids_response_dto.dart @@ -21,8 +21,13 @@ class AssetIdsResponseDto { /// Asset ID String assetId; - /// Error reason if failed - AssetIdsResponseDtoErrorEnum? error; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AssetIdErrorReason? error; /// Whether operation succeeded bool success; @@ -65,7 +70,7 @@ class AssetIdsResponseDto { return AssetIdsResponseDto( assetId: mapValueOfType(json, r'assetId')!, - error: AssetIdsResponseDtoErrorEnum.fromJson(json[r'error']), + error: AssetIdErrorReason.fromJson(json[r'error']), success: mapValueOfType(json, r'success')!, ); } @@ -119,80 +124,3 @@ class AssetIdsResponseDto { }; } -/// Error reason if failed -class AssetIdsResponseDtoErrorEnum { - /// Instantiate a new enum with the provided [value]. - const AssetIdsResponseDtoErrorEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = AssetIdsResponseDtoErrorEnum._(r'duplicate'); - static const noPermission = AssetIdsResponseDtoErrorEnum._(r'no_permission'); - static const notFound = AssetIdsResponseDtoErrorEnum._(r'not_found'); - - /// List of all possible values in this [enum][AssetIdsResponseDtoErrorEnum]. - static const values = [ - duplicate, - noPermission, - notFound, - ]; - - static AssetIdsResponseDtoErrorEnum? fromJson(dynamic value) => AssetIdsResponseDtoErrorEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = AssetIdsResponseDtoErrorEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [AssetIdsResponseDtoErrorEnum] to String, -/// and [decode] dynamic data back to [AssetIdsResponseDtoErrorEnum]. -class AssetIdsResponseDtoErrorEnumTypeTransformer { - factory AssetIdsResponseDtoErrorEnumTypeTransformer() => _instance ??= const AssetIdsResponseDtoErrorEnumTypeTransformer._(); - - const AssetIdsResponseDtoErrorEnumTypeTransformer._(); - - String encode(AssetIdsResponseDtoErrorEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a AssetIdsResponseDtoErrorEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - AssetIdsResponseDtoErrorEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return AssetIdsResponseDtoErrorEnum.duplicate; - case r'no_permission': return AssetIdsResponseDtoErrorEnum.noPermission; - case r'not_found': return AssetIdsResponseDtoErrorEnum.notFound; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [AssetIdsResponseDtoErrorEnumTypeTransformer] instance. - static AssetIdsResponseDtoErrorEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/asset_jobs_dto.dart b/mobile/openapi/lib/model/asset_jobs_dto.dart index 0aa5544a3a..5085e3820c 100644 --- a/mobile/openapi/lib/model/asset_jobs_dto.dart +++ b/mobile/openapi/lib/model/asset_jobs_dto.dart @@ -20,7 +20,6 @@ class AssetJobsDto { /// Asset IDs List assetIds; - /// Job name AssetJobName name; @override diff --git a/mobile/openapi/lib/model/asset_media_response_dto.dart b/mobile/openapi/lib/model/asset_media_response_dto.dart index 905e738b6e..6dc5cd3c92 100644 --- a/mobile/openapi/lib/model/asset_media_response_dto.dart +++ b/mobile/openapi/lib/model/asset_media_response_dto.dart @@ -20,7 +20,6 @@ class AssetMediaResponseDto { /// Asset media ID String id; - /// Upload status AssetMediaStatus status; @override diff --git a/mobile/openapi/lib/model/asset_media_size.dart b/mobile/openapi/lib/model/asset_media_size.dart index 087d19da1f..ed7a72a613 100644 --- a/mobile/openapi/lib/model/asset_media_size.dart +++ b/mobile/openapi/lib/model/asset_media_size.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Asset media size class AssetMediaSize { /// Instantiate a new enum with the provided [value]. const AssetMediaSize._(this.value); diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart index b79a693726..3e16ed8721 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_response_dto.dart @@ -16,7 +16,7 @@ class AssetMetadataBulkResponseDto { required this.assetId, required this.key, required this.updatedAt, - required this.value, + this.value = const {}, }); /// Asset ID @@ -29,14 +29,14 @@ class AssetMetadataBulkResponseDto { DateTime updatedAt; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkResponseDto && other.assetId == assetId && other.key == key && other.updatedAt == updatedAt && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -53,7 +53,9 @@ class AssetMetadataBulkResponseDto { final json = {}; json[r'assetId'] = this.assetId; json[r'key'] = this.key; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); json[r'value'] = this.value; return json; } @@ -69,8 +71,8 @@ class AssetMetadataBulkResponseDto { return AssetMetadataBulkResponseDto( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, - value: mapValueOfType(json, r'value')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart index caaf379b30..e4eab08bf1 100644 --- a/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_bulk_upsert_item_dto.dart @@ -15,7 +15,7 @@ class AssetMetadataBulkUpsertItemDto { AssetMetadataBulkUpsertItemDto({ required this.assetId, required this.key, - required this.value, + this.value = const {}, }); /// Asset ID @@ -25,13 +25,13 @@ class AssetMetadataBulkUpsertItemDto { String key; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataBulkUpsertItemDto && other.assetId == assetId && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +62,7 @@ class AssetMetadataBulkUpsertItemDto { return AssetMetadataBulkUpsertItemDto( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_response_dto.dart b/mobile/openapi/lib/model/asset_metadata_response_dto.dart index 2c3faab178..d3562f5a48 100644 --- a/mobile/openapi/lib/model/asset_metadata_response_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_response_dto.dart @@ -15,7 +15,7 @@ class AssetMetadataResponseDto { AssetMetadataResponseDto({ required this.key, required this.updatedAt, - required this.value, + this.value = const {}, }); /// Metadata key @@ -25,13 +25,13 @@ class AssetMetadataResponseDto { DateTime updatedAt; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataResponseDto && other.key == key && other.updatedAt == updatedAt && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -46,7 +46,9 @@ class AssetMetadataResponseDto { Map toJson() { final json = {}; json[r'key'] = this.key; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); json[r'value'] = this.value; return json; } @@ -61,8 +63,8 @@ class AssetMetadataResponseDto { return AssetMetadataResponseDto( key: mapValueOfType(json, r'key')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, - value: mapValueOfType(json, r'value')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart index 8a6bcb9b01..70de1941f3 100644 --- a/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart +++ b/mobile/openapi/lib/model/asset_metadata_upsert_item_dto.dart @@ -14,19 +14,19 @@ class AssetMetadataUpsertItemDto { /// Returns a new [AssetMetadataUpsertItemDto] instance. AssetMetadataUpsertItemDto({ required this.key, - required this.value, + this.value = const {}, }); /// Metadata key String key; /// Metadata value (object) - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is AssetMetadataUpsertItemDto && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -54,7 +54,7 @@ class AssetMetadataUpsertItemDto { return AssetMetadataUpsertItemDto( key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/asset_reject_reason.dart b/mobile/openapi/lib/model/asset_reject_reason.dart new file mode 100644 index 0000000000..a31e1e6117 --- /dev/null +++ b/mobile/openapi/lib/model/asset_reject_reason.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +/// Rejection reason if rejected +class AssetRejectReason { + /// Instantiate a new enum with the provided [value]. + const AssetRejectReason._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const duplicate = AssetRejectReason._(r'duplicate'); + static const unsupportedFormat = AssetRejectReason._(r'unsupported-format'); + + /// List of all possible values in this [enum][AssetRejectReason]. + static const values = [ + duplicate, + unsupportedFormat, + ]; + + static AssetRejectReason? fromJson(dynamic value) => AssetRejectReasonTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetRejectReason.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetRejectReason] to String, +/// and [decode] dynamic data back to [AssetRejectReason]. +class AssetRejectReasonTypeTransformer { + factory AssetRejectReasonTypeTransformer() => _instance ??= const AssetRejectReasonTypeTransformer._(); + + const AssetRejectReasonTypeTransformer._(); + + String encode(AssetRejectReason data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetRejectReason. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetRejectReason? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'duplicate': return AssetRejectReason.duplicate; + case r'unsupported-format': return AssetRejectReason.unsupportedFormat; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetRejectReasonTypeTransformer] instance. + static AssetRejectReasonTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 078dd0bdaf..d185761f54 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -86,6 +86,8 @@ class AssetResponseDto { bool hasMetadata; /// Asset height + /// + /// Minimum value: 0 num? height; /// Asset ID @@ -159,7 +161,6 @@ class AssetResponseDto { /// Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. String? thumbhash; - /// Asset type AssetTypeEnum type; List unassignedFaces; @@ -167,10 +168,11 @@ class AssetResponseDto { /// The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. DateTime updatedAt; - /// Asset visibility AssetVisibility visibility; /// Asset width + /// + /// Minimum value: 0 num? width; @override diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart index 229e7aa710..96fd66a392 100644 --- a/mobile/openapi/lib/model/asset_stack_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart @@ -19,6 +19,9 @@ class AssetStackResponseDto { }); /// Number of assets in stack + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; /// Stack ID diff --git a/mobile/openapi/lib/model/asset_stats_response_dto.dart b/mobile/openapi/lib/model/asset_stats_response_dto.dart index 201550c87f..df2762a2f3 100644 --- a/mobile/openapi/lib/model/asset_stats_response_dto.dart +++ b/mobile/openapi/lib/model/asset_stats_response_dto.dart @@ -19,12 +19,21 @@ class AssetStatsResponseDto { }); /// Number of images + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int images; /// Total number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/asset_upload_action.dart b/mobile/openapi/lib/model/asset_upload_action.dart new file mode 100644 index 0000000000..b5cdbb0151 --- /dev/null +++ b/mobile/openapi/lib/model/asset_upload_action.dart @@ -0,0 +1,85 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +/// Upload action +class AssetUploadAction { + /// Instantiate a new enum with the provided [value]. + const AssetUploadAction._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const accept = AssetUploadAction._(r'accept'); + static const reject = AssetUploadAction._(r'reject'); + + /// List of all possible values in this [enum][AssetUploadAction]. + static const values = [ + accept, + reject, + ]; + + static AssetUploadAction? fromJson(dynamic value) => AssetUploadActionTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetUploadAction.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [AssetUploadAction] to String, +/// and [decode] dynamic data back to [AssetUploadAction]. +class AssetUploadActionTypeTransformer { + factory AssetUploadActionTypeTransformer() => _instance ??= const AssetUploadActionTypeTransformer._(); + + const AssetUploadActionTypeTransformer._(); + + String encode(AssetUploadAction data) => data.value; + + /// Decodes a [dynamic value][data] to a AssetUploadAction. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + AssetUploadAction? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'accept': return AssetUploadAction.accept; + case r'reject': return AssetUploadAction.reject; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [AssetUploadActionTypeTransformer] instance. + static AssetUploadActionTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/avatar_update.dart b/mobile/openapi/lib/model/avatar_update.dart index a817832dab..875eb138a8 100644 --- a/mobile/openapi/lib/model/avatar_update.dart +++ b/mobile/openapi/lib/model/avatar_update.dart @@ -16,7 +16,6 @@ class AvatarUpdate { this.color, }); - /// Avatar color /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/bulk_id_response_dto.dart b/mobile/openapi/lib/model/bulk_id_response_dto.dart index 1fa8536964..bb3f1d8856 100644 --- a/mobile/openapi/lib/model/bulk_id_response_dto.dart +++ b/mobile/openapi/lib/model/bulk_id_response_dto.dart @@ -19,8 +19,13 @@ class BulkIdResponseDto { required this.success, }); - /// Error reason if failed - BulkIdResponseDtoErrorEnum? error; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + BulkIdErrorReason? error; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -80,7 +85,7 @@ class BulkIdResponseDto { final json = value.cast(); return BulkIdResponseDto( - error: BulkIdResponseDtoErrorEnum.fromJson(json[r'error']), + error: BulkIdErrorReason.fromJson(json[r'error']), errorMessage: mapValueOfType(json, r'errorMessage'), id: mapValueOfType(json, r'id')!, success: mapValueOfType(json, r'success')!, @@ -136,86 +141,3 @@ class BulkIdResponseDto { }; } -/// Error reason if failed -class BulkIdResponseDtoErrorEnum { - /// Instantiate a new enum with the provided [value]. - const BulkIdResponseDtoErrorEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const duplicate = BulkIdResponseDtoErrorEnum._(r'duplicate'); - static const noPermission = BulkIdResponseDtoErrorEnum._(r'no_permission'); - static const notFound = BulkIdResponseDtoErrorEnum._(r'not_found'); - static const unknown = BulkIdResponseDtoErrorEnum._(r'unknown'); - static const validation = BulkIdResponseDtoErrorEnum._(r'validation'); - - /// List of all possible values in this [enum][BulkIdResponseDtoErrorEnum]. - static const values = [ - duplicate, - noPermission, - notFound, - unknown, - validation, - ]; - - static BulkIdResponseDtoErrorEnum? fromJson(dynamic value) => BulkIdResponseDtoErrorEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = BulkIdResponseDtoErrorEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [BulkIdResponseDtoErrorEnum] to String, -/// and [decode] dynamic data back to [BulkIdResponseDtoErrorEnum]. -class BulkIdResponseDtoErrorEnumTypeTransformer { - factory BulkIdResponseDtoErrorEnumTypeTransformer() => _instance ??= const BulkIdResponseDtoErrorEnumTypeTransformer._(); - - const BulkIdResponseDtoErrorEnumTypeTransformer._(); - - String encode(BulkIdResponseDtoErrorEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a BulkIdResponseDtoErrorEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - BulkIdResponseDtoErrorEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'duplicate': return BulkIdResponseDtoErrorEnum.duplicate; - case r'no_permission': return BulkIdResponseDtoErrorEnum.noPermission; - case r'not_found': return BulkIdResponseDtoErrorEnum.notFound; - case r'unknown': return BulkIdResponseDtoErrorEnum.unknown; - case r'validation': return BulkIdResponseDtoErrorEnum.validation; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [BulkIdResponseDtoErrorEnumTypeTransformer] instance. - static BulkIdResponseDtoErrorEnumTypeTransformer? _instance; -} - - diff --git a/mobile/openapi/lib/model/cast_response.dart b/mobile/openapi/lib/model/cast_response.dart index 0b7f0738fe..796138b0bf 100644 --- a/mobile/openapi/lib/model/cast_response.dart +++ b/mobile/openapi/lib/model/cast_response.dart @@ -13,7 +13,7 @@ part of openapi.api; class CastResponse { /// Returns a new [CastResponse] instance. CastResponse({ - this.gCastEnabled = false, + required this.gCastEnabled, }); /// Whether Google Cast is enabled diff --git a/mobile/openapi/lib/model/contributor_count_response_dto.dart b/mobile/openapi/lib/model/contributor_count_response_dto.dart index 1bef8f29d8..af5b2cbf68 100644 --- a/mobile/openapi/lib/model/contributor_count_response_dto.dart +++ b/mobile/openapi/lib/model/contributor_count_response_dto.dart @@ -18,6 +18,9 @@ class ContributorCountResponseDto { }); /// Number of assets contributed + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int assetCount; /// User ID diff --git a/mobile/openapi/lib/model/create_library_dto.dart b/mobile/openapi/lib/model/create_library_dto.dart index 69942fee5c..ba12c62d76 100644 --- a/mobile/openapi/lib/model/create_library_dto.dart +++ b/mobile/openapi/lib/model/create_library_dto.dart @@ -13,17 +13,17 @@ part of openapi.api; class CreateLibraryDto { /// Returns a new [CreateLibraryDto] instance. CreateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], this.name, required this.ownerId, }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths (max 128) - Set importPaths; + List importPaths; /// Library name /// @@ -57,8 +57,8 @@ class CreateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; if (this.name != null) { json[r'name'] = this.name; } else { @@ -78,11 +78,11 @@ class CreateLibraryDto { return CreateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], name: mapValueOfType(json, r'name'), ownerId: mapValueOfType(json, r'ownerId')!, ); diff --git a/mobile/openapi/lib/model/create_profile_image_response_dto.dart b/mobile/openapi/lib/model/create_profile_image_response_dto.dart index 20d7cbd5e7..c6ec0d94a0 100644 --- a/mobile/openapi/lib/model/create_profile_image_response_dto.dart +++ b/mobile/openapi/lib/model/create_profile_image_response_dto.dart @@ -45,7 +45,9 @@ class CreateProfileImageResponseDto { Map toJson() { final json = {}; - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); json[r'profileImagePath'] = this.profileImagePath; json[r'userId'] = this.userId; return json; @@ -60,7 +62,7 @@ class CreateProfileImageResponseDto { final json = value.cast(); return CreateProfileImageResponseDto( - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, profileImagePath: mapValueOfType(json, r'profileImagePath')!, userId: mapValueOfType(json, r'userId')!, ); diff --git a/mobile/openapi/lib/model/database_backup_delete_dto.dart b/mobile/openapi/lib/model/database_backup_delete_dto.dart index 8bc33a81dc..c336270b84 100644 --- a/mobile/openapi/lib/model/database_backup_delete_dto.dart +++ b/mobile/openapi/lib/model/database_backup_delete_dto.dart @@ -16,6 +16,7 @@ class DatabaseBackupDeleteDto { this.backups = const [], }); + /// Backup filenames to delete List backups; @override diff --git a/mobile/openapi/lib/model/database_backup_dto.dart b/mobile/openapi/lib/model/database_backup_dto.dart index 34912a55e0..abfa637157 100644 --- a/mobile/openapi/lib/model/database_backup_dto.dart +++ b/mobile/openapi/lib/model/database_backup_dto.dart @@ -18,10 +18,13 @@ class DatabaseBackupDto { required this.timezone, }); + /// Backup filename String filename; + /// Backup file size num filesize; + /// Backup timezone String timezone; @override diff --git a/mobile/openapi/lib/model/database_backup_list_response_dto.dart b/mobile/openapi/lib/model/database_backup_list_response_dto.dart index 16985dd605..de7bf78d5a 100644 --- a/mobile/openapi/lib/model/database_backup_list_response_dto.dart +++ b/mobile/openapi/lib/model/database_backup_list_response_dto.dart @@ -16,6 +16,7 @@ class DatabaseBackupListResponseDto { this.backups = const [], }); + /// List of backups List backups; @override diff --git a/mobile/openapi/lib/model/download_archive_info.dart b/mobile/openapi/lib/model/download_archive_info.dart index 97a3346a67..dcb1258457 100644 --- a/mobile/openapi/lib/model/download_archive_info.dart +++ b/mobile/openapi/lib/model/download_archive_info.dart @@ -21,6 +21,9 @@ class DownloadArchiveInfo { List assetIds; /// Archive size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int size; @override diff --git a/mobile/openapi/lib/model/download_info_dto.dart b/mobile/openapi/lib/model/download_info_dto.dart index a1ba44920e..8a0cebd945 100644 --- a/mobile/openapi/lib/model/download_info_dto.dart +++ b/mobile/openapi/lib/model/download_info_dto.dart @@ -31,6 +31,7 @@ class DownloadInfoDto { /// Archive size limit in bytes /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/download_response.dart b/mobile/openapi/lib/model/download_response.dart index 32e9487475..bc1d7b4047 100644 --- a/mobile/openapi/lib/model/download_response.dart +++ b/mobile/openapi/lib/model/download_response.dart @@ -14,10 +14,13 @@ class DownloadResponse { /// Returns a new [DownloadResponse] instance. DownloadResponse({ required this.archiveSize, - this.includeEmbeddedVideos = false, + required this.includeEmbeddedVideos, }); /// Maximum archive size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int archiveSize; /// Whether to include embedded videos in downloads diff --git a/mobile/openapi/lib/model/download_response_dto.dart b/mobile/openapi/lib/model/download_response_dto.dart index 81912e1d30..bfe32307fa 100644 --- a/mobile/openapi/lib/model/download_response_dto.dart +++ b/mobile/openapi/lib/model/download_response_dto.dart @@ -21,6 +21,9 @@ class DownloadResponseDto { List archives; /// Total size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int totalSize; @override diff --git a/mobile/openapi/lib/model/download_update.dart b/mobile/openapi/lib/model/download_update.dart index 4acc1c8bd3..c5feb9df43 100644 --- a/mobile/openapi/lib/model/download_update.dart +++ b/mobile/openapi/lib/model/download_update.dart @@ -20,6 +20,7 @@ class DownloadUpdate { /// Maximum archive size in bytes /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index 6bb58a8ab9..64a5a73bed 100644 --- a/mobile/openapi/lib/model/exif_response_dto.dart +++ b/mobile/openapi/lib/model/exif_response_dto.dart @@ -50,9 +50,13 @@ class ExifResponseDto { String? description; /// Image height in pixels + /// + /// Minimum value: 0 num? exifImageHeight; /// Image width in pixels + /// + /// Minimum value: 0 num? exifImageWidth; /// Exposure time @@ -62,6 +66,9 @@ class ExifResponseDto { num? fNumber; /// File size in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? fileSizeInByte; /// Focal length in mm diff --git a/mobile/openapi/lib/model/facial_recognition_config.dart b/mobile/openapi/lib/model/facial_recognition_config.dart index 4b9d7a6e9e..66cb542ccf 100644 --- a/mobile/openapi/lib/model/facial_recognition_config.dart +++ b/mobile/openapi/lib/model/facial_recognition_config.dart @@ -32,6 +32,7 @@ class FacialRecognitionConfig { /// Minimum number of faces required for recognition /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int minFaces; /// Minimum confidence score for face detection diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart index 906a95a83c..873404c786 100644 --- a/mobile/openapi/lib/model/folders_response.dart +++ b/mobile/openapi/lib/model/folders_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class FoldersResponse { /// Returns a new [FoldersResponse] instance. FoldersResponse({ - this.enabled = false, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether folders are enabled diff --git a/mobile/openapi/lib/model/job_create_dto.dart b/mobile/openapi/lib/model/job_create_dto.dart index 3a3412384e..fe6743cba0 100644 --- a/mobile/openapi/lib/model/job_create_dto.dart +++ b/mobile/openapi/lib/model/job_create_dto.dart @@ -16,7 +16,6 @@ class JobCreateDto { required this.name, }); - /// Job name ManualJobName name; @override diff --git a/mobile/openapi/lib/model/job_settings_dto.dart b/mobile/openapi/lib/model/job_settings_dto.dart index 73a0187ddd..98fe3d3536 100644 --- a/mobile/openapi/lib/model/job_settings_dto.dart +++ b/mobile/openapi/lib/model/job_settings_dto.dart @@ -19,6 +19,7 @@ class JobSettingsDto { /// Concurrency /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int concurrency; @override diff --git a/mobile/openapi/lib/model/library_response_dto.dart b/mobile/openapi/lib/model/library_response_dto.dart index aa9158e591..88ebceae24 100644 --- a/mobile/openapi/lib/model/library_response_dto.dart +++ b/mobile/openapi/lib/model/library_response_dto.dart @@ -25,6 +25,9 @@ class LibraryResponseDto { }); /// Number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int assetCount; /// Creation date @@ -82,18 +85,24 @@ class LibraryResponseDto { Map toJson() { final json = {}; json[r'assetCount'] = this.assetCount; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'exclusionPatterns'] = this.exclusionPatterns; json[r'id'] = this.id; json[r'importPaths'] = this.importPaths; json[r'name'] = this.name; json[r'ownerId'] = this.ownerId; if (this.refreshedAt != null) { - json[r'refreshedAt'] = this.refreshedAt!.toUtc().toIso8601String(); + json[r'refreshedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.refreshedAt!.millisecondsSinceEpoch + : this.refreshedAt!.toUtc().toIso8601String(); } else { // json[r'refreshedAt'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -107,7 +116,7 @@ class LibraryResponseDto { return LibraryResponseDto( assetCount: mapValueOfType(json, r'assetCount')!, - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, exclusionPatterns: json[r'exclusionPatterns'] is Iterable ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) : const [], @@ -117,8 +126,8 @@ class LibraryResponseDto { : const [], name: mapValueOfType(json, r'name')!, ownerId: mapValueOfType(json, r'ownerId')!, - refreshedAt: mapDateTime(json, r'refreshedAt', r''), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + refreshedAt: mapDateTime(json, r'refreshedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/library_stats_response_dto.dart b/mobile/openapi/lib/model/library_stats_response_dto.dart index 6eec3ae8d7..55adbc2b49 100644 --- a/mobile/openapi/lib/model/library_stats_response_dto.dart +++ b/mobile/openapi/lib/model/library_stats_response_dto.dart @@ -13,22 +13,34 @@ part of openapi.api; class LibraryStatsResponseDto { /// Returns a new [LibraryStatsResponseDto] instance. LibraryStatsResponseDto({ - this.photos = 0, - this.total = 0, - this.usage = 0, - this.videos = 0, + required this.photos, + required this.total, + required this.usage, + required this.videos, }); /// Number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// Total number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; /// Storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/license_key_dto.dart b/mobile/openapi/lib/model/license_key_dto.dart index ea1fee9d7a..d1818a2a43 100644 --- a/mobile/openapi/lib/model/license_key_dto.dart +++ b/mobile/openapi/lib/model/license_key_dto.dart @@ -20,7 +20,7 @@ class LicenseKeyDto { /// Activation key String activationKey; - /// License key (format: IM(SV|CL)(-XXXX){8}) + /// License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/) String licenseKey; @override diff --git a/mobile/openapi/lib/model/license_response_dto.dart b/mobile/openapi/lib/model/license_response_dto.dart deleted file mode 100644 index 84ff72c1eb..0000000000 --- a/mobile/openapi/lib/model/license_response_dto.dart +++ /dev/null @@ -1,118 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class LicenseResponseDto { - /// Returns a new [LicenseResponseDto] instance. - LicenseResponseDto({ - required this.activatedAt, - required this.activationKey, - required this.licenseKey, - }); - - /// Activation date - DateTime activatedAt; - - /// Activation key - String activationKey; - - /// License key (format: IM(SV|CL)(-XXXX){8}) - String licenseKey; - - @override - bool operator ==(Object other) => identical(this, other) || other is LicenseResponseDto && - other.activatedAt == activatedAt && - other.activationKey == activationKey && - other.licenseKey == licenseKey; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (activatedAt.hashCode) + - (activationKey.hashCode) + - (licenseKey.hashCode); - - @override - String toString() => 'LicenseResponseDto[activatedAt=$activatedAt, activationKey=$activationKey, licenseKey=$licenseKey]'; - - Map toJson() { - final json = {}; - json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String(); - json[r'activationKey'] = this.activationKey; - json[r'licenseKey'] = this.licenseKey; - return json; - } - - /// Returns a new [LicenseResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static LicenseResponseDto? fromJson(dynamic value) { - upgradeDto(value, "LicenseResponseDto"); - if (value is Map) { - final json = value.cast(); - - return LicenseResponseDto( - activatedAt: mapDateTime(json, r'activatedAt', r'')!, - activationKey: mapValueOfType(json, r'activationKey')!, - licenseKey: mapValueOfType(json, r'licenseKey')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = LicenseResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = LicenseResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of LicenseResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = LicenseResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'activatedAt', - 'activationKey', - 'licenseKey', - }; -} - diff --git a/mobile/openapi/lib/model/log_level.dart b/mobile/openapi/lib/model/log_level.dart index 2129096da2..edb6a1ddda 100644 --- a/mobile/openapi/lib/model/log_level.dart +++ b/mobile/openapi/lib/model/log_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Log level class LogLevel { /// Instantiate a new enum with the provided [value]. const LogLevel._(this.value); diff --git a/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart index ad524914b4..e3f8c0acbe 100644 --- a/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart +++ b/mobile/openapi/lib/model/maintenance_detect_install_storage_folder_dto.dart @@ -22,7 +22,6 @@ class MaintenanceDetectInstallStorageFolderDto { /// Number of files in the folder num files; - /// Storage folder StorageFolder folder; /// Whether the folder is readable diff --git a/mobile/openapi/lib/model/maintenance_status_response_dto.dart b/mobile/openapi/lib/model/maintenance_status_response_dto.dart index 52dbb5b95b..124fa674fd 100644 --- a/mobile/openapi/lib/model/maintenance_status_response_dto.dart +++ b/mobile/openapi/lib/model/maintenance_status_response_dto.dart @@ -20,7 +20,6 @@ class MaintenanceStatusResponseDto { this.task, }); - /// Maintenance action MaintenanceAction action; bool active; diff --git a/mobile/openapi/lib/model/manual_job_name.dart b/mobile/openapi/lib/model/manual_job_name.dart index d09790a81a..27753eb9dc 100644 --- a/mobile/openapi/lib/model/manual_job_name.dart +++ b/mobile/openapi/lib/model/manual_job_name.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Job name +/// Manual job name class ManualJobName { /// Instantiate a new enum with the provided [value]. const ManualJobName._(this.value); diff --git a/mobile/openapi/lib/model/memories_response.dart b/mobile/openapi/lib/model/memories_response.dart index 63d4094cd0..250e214a60 100644 --- a/mobile/openapi/lib/model/memories_response.dart +++ b/mobile/openapi/lib/model/memories_response.dart @@ -13,11 +13,14 @@ part of openapi.api; class MemoriesResponse { /// Returns a new [MemoriesResponse] instance. MemoriesResponse({ - this.duration = 5, - this.enabled = true, + required this.duration, + required this.enabled, }); /// Memory duration in seconds + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int duration; /// Whether memories are enabled diff --git a/mobile/openapi/lib/model/memories_update.dart b/mobile/openapi/lib/model/memories_update.dart index d27cef022d..ede9910d74 100644 --- a/mobile/openapi/lib/model/memories_update.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -20,6 +20,7 @@ class MemoriesUpdate { /// Memory duration in seconds /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/memory_create_dto.dart b/mobile/openapi/lib/model/memory_create_dto.dart index 5b8eeed8fb..b906f6dd1d 100644 --- a/mobile/openapi/lib/model/memory_create_dto.dart +++ b/mobile/openapi/lib/model/memory_create_dto.dart @@ -67,7 +67,6 @@ class MemoryCreateDto { /// DateTime? showAt; - /// Memory type MemoryType type; @override @@ -101,7 +100,9 @@ class MemoryCreateDto { json[r'assetIds'] = this.assetIds; json[r'data'] = this.data; if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } @@ -110,14 +111,20 @@ class MemoryCreateDto { } else { // json[r'isSaved'] = null; } - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } @@ -138,11 +145,11 @@ class MemoryCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], data: OnThisDayDto.fromJson(json[r'data'])!, - hideAt: mapDateTime(json, r'hideAt', r''), + hideAt: mapDateTime(json, r'hideAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), isSaved: mapValueOfType(json, r'isSaved'), - memoryAt: mapDateTime(json, r'memoryAt', r'')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + showAt: mapDateTime(json, r'showAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: MemoryType.fromJson(json[r'type'])!, ); } diff --git a/mobile/openapi/lib/model/memory_response_dto.dart b/mobile/openapi/lib/model/memory_response_dto.dart index 1835095cf7..e736667d57 100644 --- a/mobile/openapi/lib/model/memory_response_dto.dart +++ b/mobile/openapi/lib/model/memory_response_dto.dart @@ -83,7 +83,6 @@ class MemoryResponseDto { /// DateTime? showAt; - /// Memory type MemoryType type; /// Last update date @@ -128,34 +127,48 @@ class MemoryResponseDto { Map toJson() { final json = {}; json[r'assets'] = this.assets; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } json[r'id'] = this.id; json[r'isSaved'] = this.isSaved; - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); json[r'ownerId'] = this.ownerId; if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } json[r'type'] = this.type; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -169,18 +182,18 @@ class MemoryResponseDto { return MemoryResponseDto( assets: AssetResponseDto.listFromJson(json[r'assets']), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, data: OnThisDayDto.fromJson(json[r'data'])!, - deletedAt: mapDateTime(json, r'deletedAt', r''), - hideAt: mapDateTime(json, r'hideAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + hideAt: mapDateTime(json, r'hideAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, isSaved: mapValueOfType(json, r'isSaved')!, - memoryAt: mapDateTime(json, r'memoryAt', r'')!, + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ownerId: mapValueOfType(json, r'ownerId')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + showAt: mapDateTime(json, r'showAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: MemoryType.fromJson(json[r'type'])!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/memory_search_order.dart b/mobile/openapi/lib/model/memory_search_order.dart index bdf5b59894..67d0b69f46 100644 --- a/mobile/openapi/lib/model/memory_search_order.dart +++ b/mobile/openapi/lib/model/memory_search_order.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Sort order class MemorySearchOrder { /// Instantiate a new enum with the provided [value]. const MemorySearchOrder._(this.value); diff --git a/mobile/openapi/lib/model/memory_statistics_response_dto.dart b/mobile/openapi/lib/model/memory_statistics_response_dto.dart index bde78de481..ae542870d9 100644 --- a/mobile/openapi/lib/model/memory_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/memory_statistics_response_dto.dart @@ -17,6 +17,9 @@ class MemoryStatisticsResponseDto { }); /// Total number of memories + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/memory_type.dart b/mobile/openapi/lib/model/memory_type.dart index aee7bd1ba1..ecfc93edb0 100644 --- a/mobile/openapi/lib/model/memory_type.dart +++ b/mobile/openapi/lib/model/memory_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Memory type class MemoryType { /// Instantiate a new enum with the provided [value]. const MemoryType._(this.value); diff --git a/mobile/openapi/lib/model/memory_update_dto.dart b/mobile/openapi/lib/model/memory_update_dto.dart index 4905b161bf..d8d7e9643b 100644 --- a/mobile/openapi/lib/model/memory_update_dto.dart +++ b/mobile/openapi/lib/model/memory_update_dto.dart @@ -69,12 +69,16 @@ class MemoryUpdateDto { // json[r'isSaved'] = null; } if (this.memoryAt != null) { - json[r'memoryAt'] = this.memoryAt!.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt!.millisecondsSinceEpoch + : this.memoryAt!.toUtc().toIso8601String(); } else { // json[r'memoryAt'] = null; } if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } @@ -91,8 +95,8 @@ class MemoryUpdateDto { return MemoryUpdateDto( isSaved: mapValueOfType(json, r'isSaved'), - memoryAt: mapDateTime(json, r'memoryAt', r''), - seenAt: mapDateTime(json, r'seenAt', r''), + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 4dbc90d407..0e8d509a16 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -34,7 +34,7 @@ class MetadataSearchDto { this.make, this.model, this.ocr, - this.order = AssetOrder.desc, + this.order, this.originalFileName, this.originalPath, this.page, @@ -192,12 +192,6 @@ class MetadataSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -212,8 +206,13 @@ class MetadataSearchDto { /// String? ocr; - /// Sort order - AssetOrder order; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + AssetOrder? order; /// Filter by original file name /// @@ -325,7 +324,6 @@ class MetadataSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -352,7 +350,6 @@ class MetadataSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -468,7 +465,7 @@ class MetadataSearchDto { (make == null ? 0 : make!.hashCode) + (model == null ? 0 : model!.hashCode) + (ocr == null ? 0 : ocr!.hashCode) + - (order.hashCode) + + (order == null ? 0 : order!.hashCode) + (originalFileName == null ? 0 : originalFileName!.hashCode) + (originalPath == null ? 0 : originalPath!.hashCode) + (page == null ? 0 : page!.hashCode) + @@ -514,12 +511,16 @@ class MetadataSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } @@ -598,7 +599,11 @@ class MetadataSearchDto { } else { // json[r'ocr'] = null; } + if (this.order != null) { json[r'order'] = this.order; + } else { + // json[r'order'] = null; + } if (this.originalFileName != null) { json[r'originalFileName'] = this.originalFileName; } else { @@ -641,12 +646,16 @@ class MetadataSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } @@ -656,12 +665,16 @@ class MetadataSearchDto { // json[r'thumbnailPath'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -671,12 +684,16 @@ class MetadataSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -723,8 +740,8 @@ class MetadataSearchDto { checksum: mapValueOfType(json, r'checksum'), city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), description: mapValueOfType(json, r'description'), deviceAssetId: mapValueOfType(json, r'deviceAssetId'), deviceId: mapValueOfType(json, r'deviceId'), @@ -740,7 +757,7 @@ class MetadataSearchDto { make: mapValueOfType(json, r'make'), model: mapValueOfType(json, r'model'), ocr: mapValueOfType(json, r'ocr'), - order: AssetOrder.fromJson(json[r'order']) ?? AssetOrder.desc, + order: AssetOrder.fromJson(json[r'order']), originalFileName: mapValueOfType(json, r'originalFileName'), originalPath: mapValueOfType(json, r'originalPath'), page: num.parse('${json[r'page']}'), @@ -756,14 +773,14 @@ class MetadataSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), thumbnailPath: mapValueOfType(json, r'thumbnailPath'), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/mirror_parameters.dart b/mobile/openapi/lib/model/mirror_parameters.dart index e8b8db685b..78c3da786c 100644 --- a/mobile/openapi/lib/model/mirror_parameters.dart +++ b/mobile/openapi/lib/model/mirror_parameters.dart @@ -16,7 +16,6 @@ class MirrorParameters { required this.axis, }); - /// Axis to mirror along MirrorAxis axis; @override diff --git a/mobile/openapi/lib/model/notification_create_dto.dart b/mobile/openapi/lib/model/notification_create_dto.dart index 1288da8670..f9771246f9 100644 --- a/mobile/openapi/lib/model/notification_create_dto.dart +++ b/mobile/openapi/lib/model/notification_create_dto.dart @@ -13,7 +13,7 @@ part of openapi.api; class NotificationCreateDto { /// Returns a new [NotificationCreateDto] instance. NotificationCreateDto({ - this.data, + this.data = const {}, this.description, this.level, this.readAt, @@ -23,18 +23,11 @@ class NotificationCreateDto { }); /// Additional notification data - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - Object? data; + Map data; /// Notification description String? description; - /// Notification level /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -49,7 +42,6 @@ class NotificationCreateDto { /// Notification title String title; - /// Notification type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -63,7 +55,7 @@ class NotificationCreateDto { @override bool operator ==(Object other) => identical(this, other) || other is NotificationCreateDto && - other.data == data && + _deepEquality.equals(other.data, data) && other.description == description && other.level == level && other.readAt == readAt && @@ -74,7 +66,7 @@ class NotificationCreateDto { @override int get hashCode => // ignore: unnecessary_parenthesis - (data == null ? 0 : data!.hashCode) + + (data.hashCode) + (description == null ? 0 : description!.hashCode) + (level == null ? 0 : level!.hashCode) + (readAt == null ? 0 : readAt!.hashCode) + @@ -87,11 +79,7 @@ class NotificationCreateDto { Map toJson() { final json = {}; - if (this.data != null) { json[r'data'] = this.data; - } else { - // json[r'data'] = null; - } if (this.description != null) { json[r'description'] = this.description; } else { @@ -103,7 +91,9 @@ class NotificationCreateDto { // json[r'level'] = null; } if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -126,10 +116,10 @@ class NotificationCreateDto { final json = value.cast(); return NotificationCreateDto( - data: mapValueOfType(json, r'data'), + data: mapCastOfType(json, r'data') ?? const {}, description: mapValueOfType(json, r'description'), level: NotificationLevel.fromJson(json[r'level']), - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), title: mapValueOfType(json, r'title')!, type: NotificationType.fromJson(json[r'type']), userId: mapValueOfType(json, r'userId')!, diff --git a/mobile/openapi/lib/model/notification_dto.dart b/mobile/openapi/lib/model/notification_dto.dart index 30d43de115..ad0e79cb27 100644 --- a/mobile/openapi/lib/model/notification_dto.dart +++ b/mobile/openapi/lib/model/notification_dto.dart @@ -14,7 +14,7 @@ class NotificationDto { /// Returns a new [NotificationDto] instance. NotificationDto({ required this.createdAt, - this.data, + this.data = const {}, this.description, required this.id, required this.level, @@ -27,13 +27,7 @@ class NotificationDto { DateTime createdAt; /// Additional notification data - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - Object? data; + Map data; /// Notification description /// @@ -47,7 +41,6 @@ class NotificationDto { /// Notification ID String id; - /// Notification level NotificationLevel level; /// Date when notification was read @@ -62,13 +55,12 @@ class NotificationDto { /// Notification title String title; - /// Notification type NotificationType type; @override bool operator ==(Object other) => identical(this, other) || other is NotificationDto && other.createdAt == createdAt && - other.data == data && + _deepEquality.equals(other.data, data) && other.description == description && other.id == id && other.level == level && @@ -80,7 +72,7 @@ class NotificationDto { int get hashCode => // ignore: unnecessary_parenthesis (createdAt.hashCode) + - (data == null ? 0 : data!.hashCode) + + (data.hashCode) + (description == null ? 0 : description!.hashCode) + (id.hashCode) + (level.hashCode) + @@ -93,12 +85,10 @@ class NotificationDto { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); - if (this.data != null) { + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; - } else { - // json[r'data'] = null; - } if (this.description != null) { json[r'description'] = this.description; } else { @@ -107,7 +97,9 @@ class NotificationDto { json[r'id'] = this.id; json[r'level'] = this.level; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -125,12 +117,12 @@ class NotificationDto { final json = value.cast(); return NotificationDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, - data: mapValueOfType(json, r'data'), + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + data: mapCastOfType(json, r'data') ?? const {}, description: mapValueOfType(json, r'description'), id: mapValueOfType(json, r'id')!, level: NotificationLevel.fromJson(json[r'level'])!, - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), title: mapValueOfType(json, r'title')!, type: NotificationType.fromJson(json[r'type'])!, ); diff --git a/mobile/openapi/lib/model/notification_level.dart b/mobile/openapi/lib/model/notification_level.dart index 554863ae4f..4ca4e2bcc8 100644 --- a/mobile/openapi/lib/model/notification_level.dart +++ b/mobile/openapi/lib/model/notification_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Notification level class NotificationLevel { /// Instantiate a new enum with the provided [value]. const NotificationLevel._(this.value); diff --git a/mobile/openapi/lib/model/notification_type.dart b/mobile/openapi/lib/model/notification_type.dart index b5885aa441..dbc9c12f84 100644 --- a/mobile/openapi/lib/model/notification_type.dart +++ b/mobile/openapi/lib/model/notification_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Notification type class NotificationType { /// Instantiate a new enum with the provided [value]. const NotificationType._(this.value); diff --git a/mobile/openapi/lib/model/notification_update_all_dto.dart b/mobile/openapi/lib/model/notification_update_all_dto.dart index a157058324..5ac61ededc 100644 --- a/mobile/openapi/lib/model/notification_update_all_dto.dart +++ b/mobile/openapi/lib/model/notification_update_all_dto.dart @@ -41,7 +41,9 @@ class NotificationUpdateAllDto { final json = {}; json[r'ids'] = this.ids; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -60,7 +62,7 @@ class NotificationUpdateAllDto { ids: json[r'ids'] is Iterable ? (json[r'ids'] as Iterable).cast().toList(growable: false) : const [], - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/notification_update_dto.dart b/mobile/openapi/lib/model/notification_update_dto.dart index eddf9c7e12..c5d949d7b2 100644 --- a/mobile/openapi/lib/model/notification_update_dto.dart +++ b/mobile/openapi/lib/model/notification_update_dto.dart @@ -34,7 +34,9 @@ class NotificationUpdateDto { Map toJson() { final json = {}; if (this.readAt != null) { - json[r'readAt'] = this.readAt!.toUtc().toIso8601String(); + json[r'readAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.readAt!.millisecondsSinceEpoch + : this.readAt!.toUtc().toIso8601String(); } else { // json[r'readAt'] = null; } @@ -50,7 +52,7 @@ class NotificationUpdateDto { final json = value.cast(); return NotificationUpdateDto( - readAt: mapDateTime(json, r'readAt', r''), + readAt: mapDateTime(json, r'readAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), ); } return null; diff --git a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart index 77466d61d9..b63f027af7 100644 --- a/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart +++ b/mobile/openapi/lib/model/o_auth_token_endpoint_auth_method.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Token endpoint auth method +/// OAuth token endpoint auth method class OAuthTokenEndpointAuthMethod { /// Instantiate a new enum with the provided [value]. const OAuthTokenEndpointAuthMethod._(this.value); diff --git a/mobile/openapi/lib/model/ocr_config.dart b/mobile/openapi/lib/model/ocr_config.dart index d97cd5ffca..2ce5646731 100644 --- a/mobile/openapi/lib/model/ocr_config.dart +++ b/mobile/openapi/lib/model/ocr_config.dart @@ -26,6 +26,7 @@ class OcrConfig { /// Maximum resolution for OCR processing /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int maxResolution; /// Minimum confidence score for text detection diff --git a/mobile/openapi/lib/model/on_this_day_dto.dart b/mobile/openapi/lib/model/on_this_day_dto.dart index 93ec956f58..77ae96532f 100644 --- a/mobile/openapi/lib/model/on_this_day_dto.dart +++ b/mobile/openapi/lib/model/on_this_day_dto.dart @@ -18,8 +18,9 @@ class OnThisDayDto { /// Year for on this day memory /// - /// Minimum value: 1 - num year; + /// Minimum value: 1000 + /// Maximum value: 9999 + int year; @override bool operator ==(Object other) => identical(this, other) || other is OnThisDayDto && @@ -48,7 +49,7 @@ class OnThisDayDto { final json = value.cast(); return OnThisDayDto( - year: num.parse('${json[r'year']}'), + year: mapValueOfType(json, r'year')!, ); } return null; diff --git a/mobile/openapi/lib/model/partner_direction.dart b/mobile/openapi/lib/model/partner_direction.dart index c43c0df75d..c5e3b308ac 100644 --- a/mobile/openapi/lib/model/partner_direction.dart +++ b/mobile/openapi/lib/model/partner_direction.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Partner direction class PartnerDirection { /// Instantiate a new enum with the provided [value]. const PartnerDirection._(this.value); diff --git a/mobile/openapi/lib/model/partner_response_dto.dart b/mobile/openapi/lib/model/partner_response_dto.dart index 5789938d18..f4612cc98a 100644 --- a/mobile/openapi/lib/model/partner_response_dto.dart +++ b/mobile/openapi/lib/model/partner_response_dto.dart @@ -22,7 +22,6 @@ class PartnerResponseDto { required this.profileImagePath, }); - /// Avatar color UserAvatarColor avatarColor; /// User email diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart index c09560e08c..9d5d8ec18a 100644 --- a/mobile/openapi/lib/model/people_response.dart +++ b/mobile/openapi/lib/model/people_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class PeopleResponse { /// Returns a new [PeopleResponse] instance. PeopleResponse({ - this.enabled = true, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether people are enabled diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart index f345657e73..87edc6b4a7 100644 --- a/mobile/openapi/lib/model/people_response_dto.dart +++ b/mobile/openapi/lib/model/people_response_dto.dart @@ -29,12 +29,17 @@ class PeopleResponseDto { bool? hasNextPage; /// Number of hidden people + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int hidden; - /// List of people List people; /// Total number of people + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/person_statistics_response_dto.dart b/mobile/openapi/lib/model/person_statistics_response_dto.dart index d2b45c8ccb..aeac16cc8a 100644 --- a/mobile/openapi/lib/model/person_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/person_statistics_response_dto.dart @@ -17,6 +17,9 @@ class PersonStatisticsResponseDto { }); /// Number of assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int assets; @override diff --git a/mobile/openapi/lib/model/person_with_faces_response_dto.dart b/mobile/openapi/lib/model/person_with_faces_response_dto.dart index f31c04b69f..f710dff8b9 100644 --- a/mobile/openapi/lib/model/person_with_faces_response_dto.dart +++ b/mobile/openapi/lib/model/person_with_faces_response_dto.dart @@ -36,7 +36,6 @@ class PersonWithFacesResponseDto { /// String? color; - /// Face detections List faces; /// Person ID diff --git a/mobile/openapi/lib/model/plugin_action_response_dto.dart b/mobile/openapi/lib/model/plugin_action_response_dto.dart index 34fa314ba9..cff2dc92f7 100644 --- a/mobile/openapi/lib/model/plugin_action_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_action_response_dto.dart @@ -35,7 +35,7 @@ class PluginActionResponseDto { String pluginId; /// Action schema - Object? schema; + PluginJsonSchema? schema; /// Supported contexts List supportedContexts; @@ -96,7 +96,7 @@ class PluginActionResponseDto { id: mapValueOfType(json, r'id')!, methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, - schema: mapValueOfType(json, r'schema'), + schema: PluginJsonSchema.fromJson(json[r'schema']), supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); diff --git a/mobile/openapi/lib/model/plugin_context_type.dart b/mobile/openapi/lib/model/plugin_context_type.dart index 6f4ac91fdb..beda0b0f1a 100644 --- a/mobile/openapi/lib/model/plugin_context_type.dart +++ b/mobile/openapi/lib/model/plugin_context_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Context type +/// Plugin context class PluginContextType { /// Instantiate a new enum with the provided [value]. const PluginContextType._(this.value); diff --git a/mobile/openapi/lib/model/plugin_filter_response_dto.dart b/mobile/openapi/lib/model/plugin_filter_response_dto.dart index ea6411a9c1..d1ab867ff9 100644 --- a/mobile/openapi/lib/model/plugin_filter_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_filter_response_dto.dart @@ -35,7 +35,7 @@ class PluginFilterResponseDto { String pluginId; /// Filter schema - Object? schema; + PluginJsonSchema? schema; /// Supported contexts List supportedContexts; @@ -96,7 +96,7 @@ class PluginFilterResponseDto { id: mapValueOfType(json, r'id')!, methodName: mapValueOfType(json, r'methodName')!, pluginId: mapValueOfType(json, r'pluginId')!, - schema: mapValueOfType(json, r'schema'), + schema: PluginJsonSchema.fromJson(json[r'schema']), supportedContexts: PluginContextType.listFromJson(json[r'supportedContexts']), title: mapValueOfType(json, r'title')!, ); diff --git a/mobile/openapi/lib/model/plugin_json_schema.dart b/mobile/openapi/lib/model/plugin_json_schema.dart new file mode 100644 index 0000000000..f7a2d584d9 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema.dart @@ -0,0 +1,158 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PluginJsonSchema { + /// Returns a new [PluginJsonSchema] instance. + PluginJsonSchema({ + this.additionalProperties, + this.description, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? additionalProperties; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + Map properties; + + List required_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchema && + other.additionalProperties == additionalProperties && + other.description == description && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchema[additionalProperties=$additionalProperties, description=$description, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchema] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchema? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchema"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchema( + additionalProperties: mapValueOfType(json, r'additionalProperties'), + description: mapValueOfType(json, r'description'), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchema.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PluginJsonSchema.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchema-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PluginJsonSchema.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_property.dart b/mobile/openapi/lib/model/plugin_json_schema_property.dart new file mode 100644 index 0000000000..65951da0a3 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_property.dart @@ -0,0 +1,195 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PluginJsonSchemaProperty { + /// Returns a new [PluginJsonSchemaProperty] instance. + PluginJsonSchemaProperty({ + this.additionalProperties, + this.default_, + this.description, + this.enum_ = const [], + this.items, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaPropertyAdditionalProperties? additionalProperties; + + Object? default_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + List enum_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaProperty? items; + + Map properties; + + List required_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchemaProperty && + other.additionalProperties == additionalProperties && + other.default_ == default_ && + other.description == description && + _deepEquality.equals(other.enum_, enum_) && + other.items == items && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (default_ == null ? 0 : default_!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (enum_.hashCode) + + (items == null ? 0 : items!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchemaProperty[additionalProperties=$additionalProperties, default_=$default_, description=$description, enum_=$enum_, items=$items, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.default_ != null) { + json[r'default'] = this.default_; + } else { + // json[r'default'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'enum'] = this.enum_; + if (this.items != null) { + json[r'items'] = this.items; + } else { + // json[r'items'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchemaProperty] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchemaProperty? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchemaProperty"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchemaProperty( + additionalProperties: PluginJsonSchemaPropertyAdditionalProperties.fromJson(json[r'additionalProperties']), + default_: mapValueOfType(json, r'default'), + description: mapValueOfType(json, r'description'), + enum_: json[r'enum'] is Iterable + ? (json[r'enum'] as Iterable).cast().toList(growable: false) + : const [], + items: PluginJsonSchemaProperty.fromJson(json[r'items']), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchemaProperty.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PluginJsonSchemaProperty.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchemaProperty-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PluginJsonSchemaProperty.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart b/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart new file mode 100644 index 0000000000..169c6be772 --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_property_additional_properties.dart @@ -0,0 +1,195 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PluginJsonSchemaPropertyAdditionalProperties { + /// Returns a new [PluginJsonSchemaPropertyAdditionalProperties] instance. + PluginJsonSchemaPropertyAdditionalProperties({ + this.additionalProperties, + this.default_, + this.description, + this.enum_ = const [], + this.items, + this.properties = const {}, + this.required_ = const [], + this.type, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaPropertyAdditionalProperties? additionalProperties; + + Object? default_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? description; + + List enum_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaProperty? items; + + Map properties; + + List required_; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PluginJsonSchemaType? type; + + @override + bool operator ==(Object other) => identical(this, other) || other is PluginJsonSchemaPropertyAdditionalProperties && + other.additionalProperties == additionalProperties && + other.default_ == default_ && + other.description == description && + _deepEquality.equals(other.enum_, enum_) && + other.items == items && + _deepEquality.equals(other.properties, properties) && + _deepEquality.equals(other.required_, required_) && + other.type == type; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (additionalProperties == null ? 0 : additionalProperties!.hashCode) + + (default_ == null ? 0 : default_!.hashCode) + + (description == null ? 0 : description!.hashCode) + + (enum_.hashCode) + + (items == null ? 0 : items!.hashCode) + + (properties.hashCode) + + (required_.hashCode) + + (type == null ? 0 : type!.hashCode); + + @override + String toString() => 'PluginJsonSchemaPropertyAdditionalProperties[additionalProperties=$additionalProperties, default_=$default_, description=$description, enum_=$enum_, items=$items, properties=$properties, required_=$required_, type=$type]'; + + Map toJson() { + final json = {}; + if (this.additionalProperties != null) { + json[r'additionalProperties'] = this.additionalProperties; + } else { + // json[r'additionalProperties'] = null; + } + if (this.default_ != null) { + json[r'default'] = this.default_; + } else { + // json[r'default'] = null; + } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } + json[r'enum'] = this.enum_; + if (this.items != null) { + json[r'items'] = this.items; + } else { + // json[r'items'] = null; + } + json[r'properties'] = this.properties; + json[r'required'] = this.required_; + if (this.type != null) { + json[r'type'] = this.type; + } else { + // json[r'type'] = null; + } + return json; + } + + /// Returns a new [PluginJsonSchemaPropertyAdditionalProperties] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PluginJsonSchemaPropertyAdditionalProperties? fromJson(dynamic value) { + upgradeDto(value, "PluginJsonSchemaPropertyAdditionalProperties"); + if (value is Map) { + final json = value.cast(); + + return PluginJsonSchemaPropertyAdditionalProperties( + additionalProperties: PluginJsonSchemaPropertyAdditionalProperties.fromJson(json[r'additionalProperties']), + default_: mapValueOfType(json, r'default'), + description: mapValueOfType(json, r'description'), + enum_: json[r'enum'] is Iterable + ? (json[r'enum'] as Iterable).cast().toList(growable: false) + : const [], + items: PluginJsonSchemaProperty.fromJson(json[r'items']), + properties: PluginJsonSchemaProperty.mapFromJson(json[r'properties']), + required_: json[r'required'] is Iterable + ? (json[r'required'] as Iterable).cast().toList(growable: false) + : const [], + type: PluginJsonSchemaType.fromJson(json[r'type']), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchemaPropertyAdditionalProperties.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PluginJsonSchemaPropertyAdditionalProperties.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PluginJsonSchemaPropertyAdditionalProperties-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PluginJsonSchemaPropertyAdditionalProperties.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/plugin_json_schema_type.dart b/mobile/openapi/lib/model/plugin_json_schema_type.dart new file mode 100644 index 0000000000..cabac9b71b --- /dev/null +++ b/mobile/openapi/lib/model/plugin_json_schema_type.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class PluginJsonSchemaType { + /// Instantiate a new enum with the provided [value]. + const PluginJsonSchemaType._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const string = PluginJsonSchemaType._(r'string'); + static const number = PluginJsonSchemaType._(r'number'); + static const integer = PluginJsonSchemaType._(r'integer'); + static const boolean = PluginJsonSchemaType._(r'boolean'); + static const object = PluginJsonSchemaType._(r'object'); + static const array = PluginJsonSchemaType._(r'array'); + static const null_ = PluginJsonSchemaType._(r'null'); + + /// List of all possible values in this [enum][PluginJsonSchemaType]. + static const values = [ + string, + number, + integer, + boolean, + object, + array, + null_, + ]; + + static PluginJsonSchemaType? fromJson(dynamic value) => PluginJsonSchemaTypeTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PluginJsonSchemaType.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [PluginJsonSchemaType] to String, +/// and [decode] dynamic data back to [PluginJsonSchemaType]. +class PluginJsonSchemaTypeTypeTransformer { + factory PluginJsonSchemaTypeTypeTransformer() => _instance ??= const PluginJsonSchemaTypeTypeTransformer._(); + + const PluginJsonSchemaTypeTypeTransformer._(); + + String encode(PluginJsonSchemaType data) => data.value; + + /// Decodes a [dynamic value][data] to a PluginJsonSchemaType. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + PluginJsonSchemaType? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'string': return PluginJsonSchemaType.string; + case r'number': return PluginJsonSchemaType.number; + case r'integer': return PluginJsonSchemaType.integer; + case r'boolean': return PluginJsonSchemaType.boolean; + case r'object': return PluginJsonSchemaType.object; + case r'array': return PluginJsonSchemaType.array; + case r'null': return PluginJsonSchemaType.null_; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [PluginJsonSchemaTypeTypeTransformer] instance. + static PluginJsonSchemaTypeTypeTransformer? _instance; +} + diff --git a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart index 16a9604bcd..a6ee1c6b69 100644 --- a/mobile/openapi/lib/model/plugin_trigger_response_dto.dart +++ b/mobile/openapi/lib/model/plugin_trigger_response_dto.dart @@ -17,10 +17,8 @@ class PluginTriggerResponseDto { required this.type, }); - /// Context type PluginContextType contextType; - /// Trigger type PluginTriggerType type; @override diff --git a/mobile/openapi/lib/model/plugin_trigger_type.dart b/mobile/openapi/lib/model/plugin_trigger_type.dart index 9ae64acf6c..3ebcef7a95 100644 --- a/mobile/openapi/lib/model/plugin_trigger_type.dart +++ b/mobile/openapi/lib/model/plugin_trigger_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Trigger type +/// Plugin trigger type class PluginTriggerType { /// Instantiate a new enum with the provided [value]. const PluginTriggerType._(this.value); diff --git a/mobile/openapi/lib/model/queue_command_dto.dart b/mobile/openapi/lib/model/queue_command_dto.dart index 9e1eea15db..fb68d85583 100644 --- a/mobile/openapi/lib/model/queue_command_dto.dart +++ b/mobile/openapi/lib/model/queue_command_dto.dart @@ -17,7 +17,6 @@ class QueueCommandDto { this.force, }); - /// Queue command to execute QueueCommand command; /// Force the command execution (if applicable) diff --git a/mobile/openapi/lib/model/queue_job_response_dto.dart b/mobile/openapi/lib/model/queue_job_response_dto.dart index 2ce63784eb..06d433edad 100644 --- a/mobile/openapi/lib/model/queue_job_response_dto.dart +++ b/mobile/openapi/lib/model/queue_job_response_dto.dart @@ -13,14 +13,14 @@ part of openapi.api; class QueueJobResponseDto { /// Returns a new [QueueJobResponseDto] instance. QueueJobResponseDto({ - required this.data, + this.data = const {}, this.id, required this.name, required this.timestamp, }); /// Job data payload - Object data; + Map data; /// Job ID /// @@ -31,15 +31,17 @@ class QueueJobResponseDto { /// String? id; - /// Job name JobName name; /// Job creation timestamp + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int timestamp; @override bool operator ==(Object other) => identical(this, other) || other is QueueJobResponseDto && - other.data == data && + _deepEquality.equals(other.data, data) && other.id == id && other.name == name && other.timestamp == timestamp; @@ -77,7 +79,7 @@ class QueueJobResponseDto { final json = value.cast(); return QueueJobResponseDto( - data: mapValueOfType(json, r'data')!, + data: mapCastOfType(json, r'data')!, id: mapValueOfType(json, r'id'), name: JobName.fromJson(json[r'name'])!, timestamp: mapValueOfType(json, r'timestamp')!, diff --git a/mobile/openapi/lib/model/queue_job_status.dart b/mobile/openapi/lib/model/queue_job_status.dart index 03a1371cc5..cbd01b11ed 100644 --- a/mobile/openapi/lib/model/queue_job_status.dart +++ b/mobile/openapi/lib/model/queue_job_status.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Queue job status class QueueJobStatus { /// Instantiate a new enum with the provided [value]. const QueueJobStatus._(this.value); diff --git a/mobile/openapi/lib/model/queue_name.dart b/mobile/openapi/lib/model/queue_name.dart index d94304d0d3..eb19d8957f 100644 --- a/mobile/openapi/lib/model/queue_name.dart +++ b/mobile/openapi/lib/model/queue_name.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Queue name class QueueName { /// Instantiate a new enum with the provided [value]. const QueueName._(this.value); diff --git a/mobile/openapi/lib/model/queue_response_dto.dart b/mobile/openapi/lib/model/queue_response_dto.dart index ac9244514c..c88f9fc195 100644 --- a/mobile/openapi/lib/model/queue_response_dto.dart +++ b/mobile/openapi/lib/model/queue_response_dto.dart @@ -21,7 +21,6 @@ class QueueResponseDto { /// Whether the queue is paused bool isPaused; - /// Queue name QueueName name; QueueStatisticsDto statistics; diff --git a/mobile/openapi/lib/model/queue_statistics_dto.dart b/mobile/openapi/lib/model/queue_statistics_dto.dart index c9a37ee30a..86c75f8e7c 100644 --- a/mobile/openapi/lib/model/queue_statistics_dto.dart +++ b/mobile/openapi/lib/model/queue_statistics_dto.dart @@ -22,21 +22,39 @@ class QueueStatisticsDto { }); /// Number of active jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int active; /// Number of completed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int completed; /// Number of delayed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int delayed; /// Number of failed jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int failed; /// Number of paused jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int paused; /// Number of waiting jobs + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int waiting; @override diff --git a/mobile/openapi/lib/model/random_search_dto.dart b/mobile/openapi/lib/model/random_search_dto.dart index d5803c9cc7..904561a033 100644 --- a/mobile/openapi/lib/model/random_search_dto.dart +++ b/mobile/openapi/lib/model/random_search_dto.dart @@ -136,12 +136,6 @@ class RandomSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -219,7 +213,6 @@ class RandomSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -246,7 +239,6 @@ class RandomSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -381,12 +373,16 @@ class RandomSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } @@ -467,22 +463,30 @@ class RandomSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -492,12 +496,16 @@ class RandomSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -543,8 +551,8 @@ class RandomSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), deviceId: mapValueOfType(json, r'deviceId'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), @@ -567,13 +575,13 @@ class RandomSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/ratings_response.dart b/mobile/openapi/lib/model/ratings_response.dart index 4346fa5c58..7b067412bf 100644 --- a/mobile/openapi/lib/model/ratings_response.dart +++ b/mobile/openapi/lib/model/ratings_response.dart @@ -13,7 +13,7 @@ part of openapi.api; class RatingsResponse { /// Returns a new [RatingsResponse] instance. RatingsResponse({ - this.enabled = false, + required this.enabled, }); /// Whether ratings are enabled diff --git a/mobile/openapi/lib/model/reaction_level.dart b/mobile/openapi/lib/model/reaction_level.dart index 29568b9d11..6060f4c2b7 100644 --- a/mobile/openapi/lib/model/reaction_level.dart +++ b/mobile/openapi/lib/model/reaction_level.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Reaction level class ReactionLevel { /// Instantiate a new enum with the provided [value]. const ReactionLevel._(this.value); diff --git a/mobile/openapi/lib/model/reaction_type.dart b/mobile/openapi/lib/model/reaction_type.dart index 4c788138fb..c4daccad71 100644 --- a/mobile/openapi/lib/model/reaction_type.dart +++ b/mobile/openapi/lib/model/reaction_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Reaction type class ReactionType { /// Instantiate a new enum with the provided [value]. const ReactionType._(this.value); diff --git a/mobile/openapi/lib/model/search_album_response_dto.dart b/mobile/openapi/lib/model/search_album_response_dto.dart index 8841251e4a..c21113ee6d 100644 --- a/mobile/openapi/lib/model/search_album_response_dto.dart +++ b/mobile/openapi/lib/model/search_album_response_dto.dart @@ -20,6 +20,9 @@ class SearchAlbumResponseDto { }); /// Number of albums in this page + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; List facets; @@ -27,6 +30,9 @@ class SearchAlbumResponseDto { List items; /// Total number of matching albums + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_asset_response_dto.dart b/mobile/openapi/lib/model/search_asset_response_dto.dart index acb81f28e2..f4ffade26b 100644 --- a/mobile/openapi/lib/model/search_asset_response_dto.dart +++ b/mobile/openapi/lib/model/search_asset_response_dto.dart @@ -21,6 +21,9 @@ class SearchAssetResponseDto { }); /// Number of assets in this page + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; List facets; @@ -31,6 +34,9 @@ class SearchAssetResponseDto { String? nextPage; /// Total number of matching assets + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_facet_count_response_dto.dart b/mobile/openapi/lib/model/search_facet_count_response_dto.dart index 8318fbfb3b..62adfaa74a 100644 --- a/mobile/openapi/lib/model/search_facet_count_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_count_response_dto.dart @@ -18,6 +18,9 @@ class SearchFacetCountResponseDto { }); /// Number of assets with this facet value + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int count; /// Facet value diff --git a/mobile/openapi/lib/model/search_facet_response_dto.dart b/mobile/openapi/lib/model/search_facet_response_dto.dart index 43b5ac5c81..51124ef1cf 100644 --- a/mobile/openapi/lib/model/search_facet_response_dto.dart +++ b/mobile/openapi/lib/model/search_facet_response_dto.dart @@ -17,7 +17,6 @@ class SearchFacetResponseDto { required this.fieldName, }); - /// Facet counts List counts; /// Facet field name diff --git a/mobile/openapi/lib/model/search_statistics_response_dto.dart b/mobile/openapi/lib/model/search_statistics_response_dto.dart index 5aebe4d6a9..c4d893af05 100644 --- a/mobile/openapi/lib/model/search_statistics_response_dto.dart +++ b/mobile/openapi/lib/model/search_statistics_response_dto.dart @@ -17,6 +17,9 @@ class SearchStatisticsResponseDto { }); /// Total number of matching assets + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int total; @override diff --git a/mobile/openapi/lib/model/search_suggestion_type.dart b/mobile/openapi/lib/model/search_suggestion_type.dart index b18fe687c4..6d44b881bd 100644 --- a/mobile/openapi/lib/model/search_suggestion_type.dart +++ b/mobile/openapi/lib/model/search_suggestion_type.dart @@ -10,7 +10,7 @@ part of openapi.api; - +/// Suggestion type class SearchSuggestionType { /// Instantiate a new enum with the provided [value]. const SearchSuggestionType._(this.value); diff --git a/mobile/openapi/lib/model/server_config_dto.dart b/mobile/openapi/lib/model/server_config_dto.dart index fec096d51a..316edb609f 100644 --- a/mobile/openapi/lib/model/server_config_dto.dart +++ b/mobile/openapi/lib/model/server_config_dto.dart @@ -54,9 +54,15 @@ class ServerConfigDto { bool publicUsers; /// Number of days before trashed assets are permanently deleted + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int trashDays; /// Delay in days before deleted users are permanently removed + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int userDeleteDelay; @override diff --git a/mobile/openapi/lib/model/server_stats_response_dto.dart b/mobile/openapi/lib/model/server_stats_response_dto.dart index ef2fa458e2..605bd74f41 100644 --- a/mobile/openapi/lib/model/server_stats_response_dto.dart +++ b/mobile/openapi/lib/model/server_stats_response_dto.dart @@ -13,29 +13,45 @@ part of openapi.api; class ServerStatsResponseDto { /// Returns a new [ServerStatsResponseDto] instance. ServerStatsResponseDto({ - this.photos = 0, - this.usage = 0, + required this.photos, + required this.usage, this.usageByUser = const [], - this.usagePhotos = 0, - this.usageVideos = 0, - this.videos = 0, + required this.usagePhotos, + required this.usageVideos, + required this.videos, }); /// Total number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// Total storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; + /// Array of usage for each user List usageByUser; /// Storage usage for photos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usagePhotos; /// Storage usage for videos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usageVideos; /// Total number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/server_storage_response_dto.dart b/mobile/openapi/lib/model/server_storage_response_dto.dart index 476b048b4d..4a66d54e37 100644 --- a/mobile/openapi/lib/model/server_storage_response_dto.dart +++ b/mobile/openapi/lib/model/server_storage_response_dto.dart @@ -26,12 +26,18 @@ class ServerStorageResponseDto { String diskAvailable; /// Available disk space in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskAvailableRaw; /// Total disk size (human-readable format) String diskSize; /// Total disk size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskSizeRaw; /// Disk usage percentage (0-100) @@ -41,6 +47,9 @@ class ServerStorageResponseDto { String diskUse; /// Used disk space in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int diskUseRaw; @override diff --git a/mobile/openapi/lib/model/server_version_history_response_dto.dart b/mobile/openapi/lib/model/server_version_history_response_dto.dart index c3b7049016..ae5e060cff 100644 --- a/mobile/openapi/lib/model/server_version_history_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_history_response_dto.dart @@ -45,7 +45,9 @@ class ServerVersionHistoryResponseDto { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'version'] = this.version; return json; @@ -60,7 +62,7 @@ class ServerVersionHistoryResponseDto { final json = value.cast(); return ServerVersionHistoryResponseDto( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, version: mapValueOfType(json, r'version')!, ); diff --git a/mobile/openapi/lib/model/server_version_response_dto.dart b/mobile/openapi/lib/model/server_version_response_dto.dart index a13cd81ad7..60161a7458 100644 --- a/mobile/openapi/lib/model/server_version_response_dto.dart +++ b/mobile/openapi/lib/model/server_version_response_dto.dart @@ -19,12 +19,21 @@ class ServerVersionResponseDto { }); /// Major version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int major; /// Minor version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int minor; /// Patch version number + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int patch_; @override diff --git a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart index 14bf584bb9..e7c9dc0d63 100644 --- a/mobile/openapi/lib/model/set_maintenance_mode_dto.dart +++ b/mobile/openapi/lib/model/set_maintenance_mode_dto.dart @@ -17,7 +17,6 @@ class SetMaintenanceModeDto { this.restoreBackupFilename, }); - /// Maintenance action MaintenanceAction action; /// Restore backup filename diff --git a/mobile/openapi/lib/model/shared_link_create_dto.dart b/mobile/openapi/lib/model/shared_link_create_dto.dart index 2675ad4beb..a32714d556 100644 --- a/mobile/openapi/lib/model/shared_link_create_dto.dart +++ b/mobile/openapi/lib/model/shared_link_create_dto.dart @@ -64,7 +64,6 @@ class SharedLinkCreateDto { /// Custom URL slug String? slug; - /// Shared link type SharedLinkType type; @override @@ -117,7 +116,9 @@ class SharedLinkCreateDto { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -152,7 +153,7 @@ class SharedLinkCreateDto { ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) : const [], description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata') ?? true, slug: mapValueOfType(json, r'slug'), diff --git a/mobile/openapi/lib/model/shared_link_edit_dto.dart b/mobile/openapi/lib/model/shared_link_edit_dto.dart index b22232add6..11d6cdd52e 100644 --- a/mobile/openapi/lib/model/shared_link_edit_dto.dart +++ b/mobile/openapi/lib/model/shared_link_edit_dto.dart @@ -120,7 +120,9 @@ class SharedLinkEditDto { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -155,7 +157,7 @@ class SharedLinkEditDto { allowUpload: mapValueOfType(json, r'allowUpload'), changeExpiryTime: mapValueOfType(json, r'changeExpiryTime'), description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), password: mapValueOfType(json, r'password'), showMetadata: mapValueOfType(json, r'showMetadata'), slug: mapValueOfType(json, r'slug'), diff --git a/mobile/openapi/lib/model/shared_link_response_dto.dart b/mobile/openapi/lib/model/shared_link_response_dto.dart index d9aec48c39..3312651296 100644 --- a/mobile/openapi/lib/model/shared_link_response_dto.dart +++ b/mobile/openapi/lib/model/shared_link_response_dto.dart @@ -73,7 +73,6 @@ class SharedLinkResponseDto { /// Access token String? token; - /// Shared link type SharedLinkType type; /// Owner user ID @@ -129,14 +128,18 @@ class SharedLinkResponseDto { json[r'allowDownload'] = this.allowDownload; json[r'allowUpload'] = this.allowUpload; json[r'assets'] = this.assets; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.description != null) { json[r'description'] = this.description; } else { // json[r'description'] = null; } if (this.expiresAt != null) { - json[r'expiresAt'] = this.expiresAt!.toUtc().toIso8601String(); + json[r'expiresAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.expiresAt!.millisecondsSinceEpoch + : this.expiresAt!.toUtc().toIso8601String(); } else { // json[r'expiresAt'] = null; } @@ -176,9 +179,9 @@ class SharedLinkResponseDto { allowDownload: mapValueOfType(json, r'allowDownload')!, allowUpload: mapValueOfType(json, r'allowUpload')!, assets: AssetResponseDto.listFromJson(json[r'assets']), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, description: mapValueOfType(json, r'description'), - expiresAt: mapDateTime(json, r'expiresAt', r''), + expiresAt: mapDateTime(json, r'expiresAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, key: mapValueOfType(json, r'key')!, password: mapValueOfType(json, r'password'), diff --git a/mobile/openapi/lib/model/shared_links_response.dart b/mobile/openapi/lib/model/shared_links_response.dart index 510e94e43f..2b32a57540 100644 --- a/mobile/openapi/lib/model/shared_links_response.dart +++ b/mobile/openapi/lib/model/shared_links_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class SharedLinksResponse { /// Returns a new [SharedLinksResponse] instance. SharedLinksResponse({ - this.enabled = true, - this.sidebarWeb = false, + required this.enabled, + required this.sidebarWeb, }); /// Whether shared links are enabled diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 5f8214467f..9c1192ff34 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -147,12 +147,6 @@ class SmartSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -259,7 +253,6 @@ class SmartSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -286,7 +279,6 @@ class SmartSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -407,12 +399,16 @@ class SmartSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } @@ -513,22 +509,30 @@ class SmartSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -538,12 +542,16 @@ class SmartSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -579,8 +587,8 @@ class SmartSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), deviceId: mapValueOfType(json, r'deviceId'), isEncoded: mapValueOfType(json, r'isEncoded'), isFavorite: mapValueOfType(json, r'isFavorite'), @@ -607,13 +615,13 @@ class SmartSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart index 638dfb5255..326f83a03d 100644 --- a/mobile/openapi/lib/model/stack_response_dto.dart +++ b/mobile/openapi/lib/model/stack_response_dto.dart @@ -18,7 +18,6 @@ class StackResponseDto { required this.primaryAssetId, }); - /// Stack assets List assets; /// Stack ID diff --git a/mobile/openapi/lib/model/statistics_search_dto.dart b/mobile/openapi/lib/model/statistics_search_dto.dart index d5bbf448a3..729b7f127c 100644 --- a/mobile/openapi/lib/model/statistics_search_dto.dart +++ b/mobile/openapi/lib/model/statistics_search_dto.dart @@ -141,12 +141,6 @@ class StatisticsSearchDto { String? libraryId; /// Filter by camera make - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? make; /// Filter by camera model @@ -212,7 +206,6 @@ class StatisticsSearchDto { /// DateTime? trashedBefore; - /// Asset type filter /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -239,7 +232,6 @@ class StatisticsSearchDto { /// DateTime? updatedBefore; - /// Filter by visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -330,12 +322,16 @@ class StatisticsSearchDto { // json[r'country'] = null; } if (this.createdAfter != null) { - json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String(); + json[r'createdAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAfter!.millisecondsSinceEpoch + : this.createdAfter!.toUtc().toIso8601String(); } else { // json[r'createdAfter'] = null; } if (this.createdBefore != null) { - json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String(); + json[r'createdBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdBefore!.millisecondsSinceEpoch + : this.createdBefore!.toUtc().toIso8601String(); } else { // json[r'createdBefore'] = null; } @@ -416,22 +412,30 @@ class StatisticsSearchDto { // json[r'tagIds'] = null; } if (this.takenAfter != null) { - json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String(); + json[r'takenAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenAfter!.millisecondsSinceEpoch + : this.takenAfter!.toUtc().toIso8601String(); } else { // json[r'takenAfter'] = null; } if (this.takenBefore != null) { - json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String(); + json[r'takenBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.takenBefore!.millisecondsSinceEpoch + : this.takenBefore!.toUtc().toIso8601String(); } else { // json[r'takenBefore'] = null; } if (this.trashedAfter != null) { - json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String(); + json[r'trashedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedAfter!.millisecondsSinceEpoch + : this.trashedAfter!.toUtc().toIso8601String(); } else { // json[r'trashedAfter'] = null; } if (this.trashedBefore != null) { - json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String(); + json[r'trashedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.trashedBefore!.millisecondsSinceEpoch + : this.trashedBefore!.toUtc().toIso8601String(); } else { // json[r'trashedBefore'] = null; } @@ -441,12 +445,16 @@ class StatisticsSearchDto { // json[r'type'] = null; } if (this.updatedAfter != null) { - json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String(); + json[r'updatedAfter'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAfter!.millisecondsSinceEpoch + : this.updatedAfter!.toUtc().toIso8601String(); } else { // json[r'updatedAfter'] = null; } if (this.updatedBefore != null) { - json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String(); + json[r'updatedBefore'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedBefore!.millisecondsSinceEpoch + : this.updatedBefore!.toUtc().toIso8601String(); } else { // json[r'updatedBefore'] = null; } @@ -472,8 +480,8 @@ class StatisticsSearchDto { : const [], city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - createdAfter: mapDateTime(json, r'createdAfter', r''), - createdBefore: mapDateTime(json, r'createdBefore', r''), + createdAfter: mapDateTime(json, r'createdAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + createdBefore: mapDateTime(json, r'createdBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), description: mapValueOfType(json, r'description'), deviceId: mapValueOfType(json, r'deviceId'), isEncoded: mapValueOfType(json, r'isEncoded'), @@ -496,13 +504,13 @@ class StatisticsSearchDto { tagIds: json[r'tagIds'] is Iterable ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) : const [], - takenAfter: mapDateTime(json, r'takenAfter', r''), - takenBefore: mapDateTime(json, r'takenBefore', r''), - trashedAfter: mapDateTime(json, r'trashedAfter', r''), - trashedBefore: mapDateTime(json, r'trashedBefore', r''), + takenAfter: mapDateTime(json, r'takenAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + takenBefore: mapDateTime(json, r'takenBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedAfter: mapDateTime(json, r'trashedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + trashedBefore: mapDateTime(json, r'trashedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: AssetTypeEnum.fromJson(json[r'type']), - updatedAfter: mapDateTime(json, r'updatedAfter', r''), - updatedBefore: mapDateTime(json, r'updatedBefore', r''), + updatedAfter: mapDateTime(json, r'updatedAfter', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + updatedBefore: mapDateTime(json, r'updatedBefore', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); } diff --git a/mobile/openapi/lib/model/sync_ack_dto.dart b/mobile/openapi/lib/model/sync_ack_dto.dart index 747f671557..fa7e20a832 100644 --- a/mobile/openapi/lib/model/sync_ack_dto.dart +++ b/mobile/openapi/lib/model/sync_ack_dto.dart @@ -20,7 +20,6 @@ class SyncAckDto { /// Acknowledgment ID String ack; - /// Sync entity type SyncEntityType type; @override diff --git a/mobile/openapi/lib/model/sync_album_user_v1.dart b/mobile/openapi/lib/model/sync_album_user_v1.dart index 3fc8972069..1efe7da029 100644 --- a/mobile/openapi/lib/model/sync_album_user_v1.dart +++ b/mobile/openapi/lib/model/sync_album_user_v1.dart @@ -21,7 +21,6 @@ class SyncAlbumUserV1 { /// Album ID String albumId; - /// Album user role AlbumUserRole role; /// User ID diff --git a/mobile/openapi/lib/model/sync_album_v1.dart b/mobile/openapi/lib/model/sync_album_v1.dart index 6c89d93724..17b2bda02b 100644 --- a/mobile/openapi/lib/model/sync_album_v1.dart +++ b/mobile/openapi/lib/model/sync_album_v1.dart @@ -80,7 +80,9 @@ class SyncAlbumV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'description'] = this.description; json[r'id'] = this.id; json[r'isActivityEnabled'] = this.isActivityEnabled; @@ -92,7 +94,9 @@ class SyncAlbumV1 { } else { // json[r'thumbnailAssetId'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -105,7 +109,7 @@ class SyncAlbumV1 { final json = value.cast(); return SyncAlbumV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, description: mapValueOfType(json, r'description')!, id: mapValueOfType(json, r'id')!, isActivityEnabled: mapValueOfType(json, r'isActivityEnabled')!, @@ -113,7 +117,7 @@ class SyncAlbumV1 { order: AssetOrder.fromJson(json[r'order'])!, ownerId: mapValueOfType(json, r'ownerId')!, thumbnailAssetId: mapValueOfType(json, r'thumbnailAssetId'), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart index 68af280290..e0c98bfef3 100644 --- a/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_edit_delete_v1.dart @@ -16,6 +16,7 @@ class SyncAssetEditDeleteV1 { required this.editId, }); + /// Edit ID String editId; @override diff --git a/mobile/openapi/lib/model/sync_asset_edit_v1.dart b/mobile/openapi/lib/model/sync_asset_edit_v1.dart index 3cc2673bfc..8acfad5f6a 100644 --- a/mobile/openapi/lib/model/sync_asset_edit_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_edit_v1.dart @@ -16,18 +16,25 @@ class SyncAssetEditV1 { required this.action, required this.assetId, required this.id, - required this.parameters, + this.parameters = const {}, required this.sequence, }); AssetEditAction action; + /// Asset ID String assetId; + /// Edit ID String id; - Object parameters; + /// Edit parameters + Map parameters; + /// Edit sequence + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int sequence; @override @@ -35,7 +42,7 @@ class SyncAssetEditV1 { other.action == action && other.assetId == assetId && other.id == id && - other.parameters == parameters && + _deepEquality.equals(other.parameters, parameters) && other.sequence == sequence; @override @@ -72,7 +79,7 @@ class SyncAssetEditV1 { action: AssetEditAction.fromJson(json[r'action'])!, assetId: mapValueOfType(json, r'assetId')!, id: mapValueOfType(json, r'id')!, - parameters: mapValueOfType(json, r'parameters')!, + parameters: mapCastOfType(json, r'parameters')!, sequence: mapValueOfType(json, r'sequence')!, ); } diff --git a/mobile/openapi/lib/model/sync_asset_exif_v1.dart b/mobile/openapi/lib/model/sync_asset_exif_v1.dart index ff9efdfea3..caaeed7fb3 100644 --- a/mobile/openapi/lib/model/sync_asset_exif_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_exif_v1.dart @@ -56,9 +56,15 @@ class SyncAssetExifV1 { String? description; /// Exif image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? exifImageHeight; /// Exif image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? exifImageWidth; /// Exposure time @@ -68,6 +74,9 @@ class SyncAssetExifV1 { double? fNumber; /// File size in byte + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? fileSizeInByte; /// Focal length @@ -77,6 +86,9 @@ class SyncAssetExifV1 { double? fps; /// ISO + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? iso; /// Latitude @@ -107,6 +119,9 @@ class SyncAssetExifV1 { String? projectionType; /// Rating + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? rating; /// State @@ -189,7 +204,9 @@ class SyncAssetExifV1 { // json[r'country'] = null; } if (this.dateTimeOriginal != null) { - json[r'dateTimeOriginal'] = this.dateTimeOriginal!.toUtc().toIso8601String(); + json[r'dateTimeOriginal'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.dateTimeOriginal!.millisecondsSinceEpoch + : this.dateTimeOriginal!.toUtc().toIso8601String(); } else { // json[r'dateTimeOriginal'] = null; } @@ -264,7 +281,9 @@ class SyncAssetExifV1 { // json[r'model'] = null; } if (this.modifyDate != null) { - json[r'modifyDate'] = this.modifyDate!.toUtc().toIso8601String(); + json[r'modifyDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.modifyDate!.millisecondsSinceEpoch + : this.modifyDate!.toUtc().toIso8601String(); } else { // json[r'modifyDate'] = null; } @@ -313,7 +332,7 @@ class SyncAssetExifV1 { assetId: mapValueOfType(json, r'assetId')!, city: mapValueOfType(json, r'city'), country: mapValueOfType(json, r'country'), - dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r''), + dateTimeOriginal: mapDateTime(json, r'dateTimeOriginal', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), description: mapValueOfType(json, r'description'), exifImageHeight: mapValueOfType(json, r'exifImageHeight'), exifImageWidth: mapValueOfType(json, r'exifImageWidth'), @@ -328,7 +347,7 @@ class SyncAssetExifV1 { longitude: (mapValueOfType(json, r'longitude'))?.toDouble(), make: mapValueOfType(json, r'make'), model: mapValueOfType(json, r'model'), - modifyDate: mapDateTime(json, r'modifyDate', r''), + modifyDate: mapDateTime(json, r'modifyDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), orientation: mapValueOfType(json, r'orientation'), profileDescription: mapValueOfType(json, r'profileDescription'), projectionType: mapValueOfType(json, r'projectionType'), diff --git a/mobile/openapi/lib/model/sync_asset_face_v1.dart b/mobile/openapi/lib/model/sync_asset_face_v1.dart index 647a07d5eb..c3f74ff2cd 100644 --- a/mobile/openapi/lib/model/sync_asset_face_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_face_v1.dart @@ -28,19 +28,43 @@ class SyncAssetFaceV1 { /// Asset ID String assetId; + /// Bounding box X1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; + /// Bounding box X2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; + /// Bounding box Y1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; + /// Bounding box Y2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Asset face ID String id; + /// Image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; + /// Image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Person ID diff --git a/mobile/openapi/lib/model/sync_asset_face_v2.dart b/mobile/openapi/lib/model/sync_asset_face_v2.dart index 688d71229f..aeefc2ece9 100644 --- a/mobile/openapi/lib/model/sync_asset_face_v2.dart +++ b/mobile/openapi/lib/model/sync_asset_face_v2.dart @@ -30,12 +30,28 @@ class SyncAssetFaceV2 { /// Asset ID String assetId; + /// Bounding box X1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX1; + /// Bounding box X2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxX2; + /// Bounding box Y1 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY1; + /// Bounding box Y2 + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int boundingBoxY2; /// Face deleted at @@ -44,8 +60,16 @@ class SyncAssetFaceV2 { /// Asset face ID String id; + /// Image height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageHeight; + /// Image width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int imageWidth; /// Is the face visible in the asset @@ -99,7 +123,9 @@ class SyncAssetFaceV2 { json[r'boundingBoxY1'] = this.boundingBoxY1; json[r'boundingBoxY2'] = this.boundingBoxY2; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -130,7 +156,7 @@ class SyncAssetFaceV2 { boundingBoxX2: mapValueOfType(json, r'boundingBoxX2')!, boundingBoxY1: mapValueOfType(json, r'boundingBoxY1')!, boundingBoxY2: mapValueOfType(json, r'boundingBoxY2')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, imageHeight: mapValueOfType(json, r'imageHeight')!, imageWidth: mapValueOfType(json, r'imageWidth')!, diff --git a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart index 4a66623939..08d7eae49b 100644 --- a/mobile/openapi/lib/model/sync_asset_metadata_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_metadata_v1.dart @@ -15,7 +15,7 @@ class SyncAssetMetadataV1 { SyncAssetMetadataV1({ required this.assetId, required this.key, - required this.value, + this.value = const {}, }); /// Asset ID @@ -25,13 +25,13 @@ class SyncAssetMetadataV1 { String key; /// Value - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is SyncAssetMetadataV1 && other.assetId == assetId && other.key == key && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +62,7 @@ class SyncAssetMetadataV1 { return SyncAssetMetadataV1( assetId: mapValueOfType(json, r'assetId')!, key: mapValueOfType(json, r'key')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_asset_v1.dart b/mobile/openapi/lib/model/sync_asset_v1.dart index debde4488e..d08de6ab72 100644 --- a/mobile/openapi/lib/model/sync_asset_v1.dart +++ b/mobile/openapi/lib/model/sync_asset_v1.dart @@ -50,6 +50,9 @@ class SyncAssetV1 { DateTime? fileModifiedAt; /// Asset height + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? height; /// Asset ID @@ -82,13 +85,14 @@ class SyncAssetV1 { /// Thumbhash String? thumbhash; - /// Asset type AssetTypeEnum type; - /// Asset visibility AssetVisibility visibility; /// Asset width + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? width; @override @@ -143,7 +147,9 @@ class SyncAssetV1 { final json = {}; json[r'checksum'] = this.checksum; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -153,12 +159,16 @@ class SyncAssetV1 { // json[r'duration'] = null; } if (this.fileCreatedAt != null) { - json[r'fileCreatedAt'] = this.fileCreatedAt!.toUtc().toIso8601String(); + json[r'fileCreatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.fileCreatedAt!.millisecondsSinceEpoch + : this.fileCreatedAt!.toUtc().toIso8601String(); } else { // json[r'fileCreatedAt'] = null; } if (this.fileModifiedAt != null) { - json[r'fileModifiedAt'] = this.fileModifiedAt!.toUtc().toIso8601String(); + json[r'fileModifiedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.fileModifiedAt!.millisecondsSinceEpoch + : this.fileModifiedAt!.toUtc().toIso8601String(); } else { // json[r'fileModifiedAt'] = null; } @@ -181,7 +191,9 @@ class SyncAssetV1 { // json[r'livePhotoVideoId'] = null; } if (this.localDateTime != null) { - json[r'localDateTime'] = this.localDateTime!.toUtc().toIso8601String(); + json[r'localDateTime'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.localDateTime!.millisecondsSinceEpoch + : this.localDateTime!.toUtc().toIso8601String(); } else { // json[r'localDateTime'] = null; } @@ -217,17 +229,17 @@ class SyncAssetV1 { return SyncAssetV1( checksum: mapValueOfType(json, r'checksum')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), duration: mapValueOfType(json, r'duration'), - fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r''), - fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r''), + fileCreatedAt: mapDateTime(json, r'fileCreatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + fileModifiedAt: mapDateTime(json, r'fileModifiedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), height: mapValueOfType(json, r'height'), id: mapValueOfType(json, r'id')!, isEdited: mapValueOfType(json, r'isEdited')!, isFavorite: mapValueOfType(json, r'isFavorite')!, libraryId: mapValueOfType(json, r'libraryId'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), - localDateTime: mapDateTime(json, r'localDateTime', r''), + localDateTime: mapDateTime(json, r'localDateTime', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), originalFileName: mapValueOfType(json, r'originalFileName')!, ownerId: mapValueOfType(json, r'ownerId')!, stackId: mapValueOfType(json, r'stackId'), diff --git a/mobile/openapi/lib/model/sync_auth_user_v1.dart b/mobile/openapi/lib/model/sync_auth_user_v1.dart index 0edd804c6a..c64d82bfbd 100644 --- a/mobile/openapi/lib/model/sync_auth_user_v1.dart +++ b/mobile/openapi/lib/model/sync_auth_user_v1.dart @@ -13,7 +13,7 @@ part of openapi.api; class SyncAuthUserV1 { /// Returns a new [SyncAuthUserV1] instance. SyncAuthUserV1({ - required this.avatarColor, + this.avatarColor, required this.deletedAt, required this.email, required this.hasProfileImage, @@ -28,7 +28,6 @@ class SyncAuthUserV1 { required this.storageLabel, }); - /// User avatar color UserAvatarColor? avatarColor; /// User deleted at @@ -58,8 +57,16 @@ class SyncAuthUserV1 { /// User profile changed at DateTime profileChangedAt; + /// Quota size in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; + /// Quota usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int quotaUsageInBytes; /// User storage label @@ -109,7 +116,9 @@ class SyncAuthUserV1 { // json[r'avatarColor'] = null; } if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -124,7 +133,9 @@ class SyncAuthUserV1 { } else { // json[r'pinCode'] = null; } - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); if (this.quotaSizeInBytes != null) { json[r'quotaSizeInBytes'] = this.quotaSizeInBytes; } else { @@ -149,7 +160,7 @@ class SyncAuthUserV1 { return SyncAuthUserV1( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), email: mapValueOfType(json, r'email')!, hasProfileImage: mapValueOfType(json, r'hasProfileImage')!, id: mapValueOfType(json, r'id')!, @@ -157,7 +168,7 @@ class SyncAuthUserV1 { name: mapValueOfType(json, r'name')!, oauthId: mapValueOfType(json, r'oauthId')!, pinCode: mapValueOfType(json, r'pinCode'), - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, quotaSizeInBytes: mapValueOfType(json, r'quotaSizeInBytes'), quotaUsageInBytes: mapValueOfType(json, r'quotaUsageInBytes')!, storageLabel: mapValueOfType(json, r'storageLabel'), @@ -208,7 +219,6 @@ class SyncAuthUserV1 { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'avatarColor', 'deletedAt', 'email', 'hasProfileImage', diff --git a/mobile/openapi/lib/model/sync_memory_v1.dart b/mobile/openapi/lib/model/sync_memory_v1.dart index c506738d97..855340f4d7 100644 --- a/mobile/openapi/lib/model/sync_memory_v1.dart +++ b/mobile/openapi/lib/model/sync_memory_v1.dart @@ -14,7 +14,7 @@ class SyncMemoryV1 { /// Returns a new [SyncMemoryV1] instance. SyncMemoryV1({ required this.createdAt, - required this.data, + this.data = const {}, required this.deletedAt, required this.hideAt, required this.id, @@ -31,7 +31,7 @@ class SyncMemoryV1 { DateTime createdAt; /// Data - Object data; + Map data; /// Deleted at DateTime? deletedAt; @@ -57,7 +57,6 @@ class SyncMemoryV1 { /// Show at DateTime? showAt; - /// Memory type MemoryType type; /// Updated at @@ -66,7 +65,7 @@ class SyncMemoryV1 { @override bool operator ==(Object other) => identical(this, other) || other is SyncMemoryV1 && other.createdAt == createdAt && - other.data == data && + _deepEquality.equals(other.data, data) && other.deletedAt == deletedAt && other.hideAt == hideAt && other.id == id && @@ -99,34 +98,48 @@ class SyncMemoryV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'data'] = this.data; if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } if (this.hideAt != null) { - json[r'hideAt'] = this.hideAt!.toUtc().toIso8601String(); + json[r'hideAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.hideAt!.millisecondsSinceEpoch + : this.hideAt!.toUtc().toIso8601String(); } else { // json[r'hideAt'] = null; } json[r'id'] = this.id; json[r'isSaved'] = this.isSaved; - json[r'memoryAt'] = this.memoryAt.toUtc().toIso8601String(); + json[r'memoryAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.memoryAt.millisecondsSinceEpoch + : this.memoryAt.toUtc().toIso8601String(); json[r'ownerId'] = this.ownerId; if (this.seenAt != null) { - json[r'seenAt'] = this.seenAt!.toUtc().toIso8601String(); + json[r'seenAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.seenAt!.millisecondsSinceEpoch + : this.seenAt!.toUtc().toIso8601String(); } else { // json[r'seenAt'] = null; } if (this.showAt != null) { - json[r'showAt'] = this.showAt!.toUtc().toIso8601String(); + json[r'showAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.showAt!.millisecondsSinceEpoch + : this.showAt!.toUtc().toIso8601String(); } else { // json[r'showAt'] = null; } json[r'type'] = this.type; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -139,18 +152,18 @@ class SyncMemoryV1 { final json = value.cast(); return SyncMemoryV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, - data: mapValueOfType(json, r'data')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), - hideAt: mapDateTime(json, r'hideAt', r''), + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + data: mapCastOfType(json, r'data')!, + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + hideAt: mapDateTime(json, r'hideAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), id: mapValueOfType(json, r'id')!, isSaved: mapValueOfType(json, r'isSaved')!, - memoryAt: mapDateTime(json, r'memoryAt', r'')!, + memoryAt: mapDateTime(json, r'memoryAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ownerId: mapValueOfType(json, r'ownerId')!, - seenAt: mapDateTime(json, r'seenAt', r''), - showAt: mapDateTime(json, r'showAt', r''), + seenAt: mapDateTime(json, r'seenAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), + showAt: mapDateTime(json, r'showAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), type: MemoryType.fromJson(json[r'type'])!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_person_v1.dart b/mobile/openapi/lib/model/sync_person_v1.dart index fc2c36aa8c..1bd6f4a160 100644 --- a/mobile/openapi/lib/model/sync_person_v1.dart +++ b/mobile/openapi/lib/model/sync_person_v1.dart @@ -88,7 +88,9 @@ class SyncPersonV1 { Map toJson() { final json = {}; if (this.birthDate != null) { - json[r'birthDate'] = this.birthDate!.toUtc().toIso8601String(); + json[r'birthDate'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.birthDate!.millisecondsSinceEpoch + : this.birthDate!.toUtc().toIso8601String(); } else { // json[r'birthDate'] = null; } @@ -97,7 +99,9 @@ class SyncPersonV1 { } else { // json[r'color'] = null; } - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.faceAssetId != null) { json[r'faceAssetId'] = this.faceAssetId; } else { @@ -108,7 +112,9 @@ class SyncPersonV1 { json[r'isHidden'] = this.isHidden; json[r'name'] = this.name; json[r'ownerId'] = this.ownerId; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -121,16 +127,16 @@ class SyncPersonV1 { final json = value.cast(); return SyncPersonV1( - birthDate: mapDateTime(json, r'birthDate', r''), + birthDate: mapDateTime(json, r'birthDate', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), color: mapValueOfType(json, r'color'), - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, faceAssetId: mapValueOfType(json, r'faceAssetId'), id: mapValueOfType(json, r'id')!, isFavorite: mapValueOfType(json, r'isFavorite')!, isHidden: mapValueOfType(json, r'isHidden')!, name: mapValueOfType(json, r'name')!, ownerId: mapValueOfType(json, r'ownerId')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_request_type.dart b/mobile/openapi/lib/model/sync_request_type.dart index 671081c0a5..f51cc8bde9 100644 --- a/mobile/openapi/lib/model/sync_request_type.dart +++ b/mobile/openapi/lib/model/sync_request_type.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Sync request types +/// Sync request type class SyncRequestType { /// Instantiate a new enum with the provided [value]. const SyncRequestType._(this.value); diff --git a/mobile/openapi/lib/model/sync_stack_v1.dart b/mobile/openapi/lib/model/sync_stack_v1.dart index e4487ccfaf..3e79a55134 100644 --- a/mobile/openapi/lib/model/sync_stack_v1.dart +++ b/mobile/openapi/lib/model/sync_stack_v1.dart @@ -57,11 +57,15 @@ class SyncStackV1 { Map toJson() { final json = {}; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'ownerId'] = this.ownerId; json[r'primaryAssetId'] = this.primaryAssetId; - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -74,11 +78,11 @@ class SyncStackV1 { final json = value.cast(); return SyncStackV1( - createdAt: mapDateTime(json, r'createdAt', r'')!, + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, id: mapValueOfType(json, r'id')!, ownerId: mapValueOfType(json, r'ownerId')!, primaryAssetId: mapValueOfType(json, r'primaryAssetId')!, - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart index 61340a8f82..67976108e1 100644 --- a/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart +++ b/mobile/openapi/lib/model/sync_user_metadata_delete_v1.dart @@ -17,7 +17,6 @@ class SyncUserMetadataDeleteV1 { required this.userId, }); - /// User metadata key UserMetadataKey key; /// User ID diff --git a/mobile/openapi/lib/model/sync_user_metadata_v1.dart b/mobile/openapi/lib/model/sync_user_metadata_v1.dart index 23803d0be4..ddde7c0513 100644 --- a/mobile/openapi/lib/model/sync_user_metadata_v1.dart +++ b/mobile/openapi/lib/model/sync_user_metadata_v1.dart @@ -15,23 +15,22 @@ class SyncUserMetadataV1 { SyncUserMetadataV1({ required this.key, required this.userId, - required this.value, + this.value = const {}, }); - /// User metadata key UserMetadataKey key; /// User ID String userId; /// User metadata value - Object value; + Map value; @override bool operator ==(Object other) => identical(this, other) || other is SyncUserMetadataV1 && other.key == key && other.userId == userId && - other.value == value; + _deepEquality.equals(other.value, value); @override int get hashCode => @@ -62,7 +61,7 @@ class SyncUserMetadataV1 { return SyncUserMetadataV1( key: UserMetadataKey.fromJson(json[r'key'])!, userId: mapValueOfType(json, r'userId')!, - value: mapValueOfType(json, r'value')!, + value: mapCastOfType(json, r'value')!, ); } return null; diff --git a/mobile/openapi/lib/model/sync_user_v1.dart b/mobile/openapi/lib/model/sync_user_v1.dart index 6d425130a3..0a81593547 100644 --- a/mobile/openapi/lib/model/sync_user_v1.dart +++ b/mobile/openapi/lib/model/sync_user_v1.dart @@ -13,7 +13,7 @@ part of openapi.api; class SyncUserV1 { /// Returns a new [SyncUserV1] instance. SyncUserV1({ - required this.avatarColor, + this.avatarColor, required this.deletedAt, required this.email, required this.hasProfileImage, @@ -22,7 +22,6 @@ class SyncUserV1 { required this.profileChangedAt, }); - /// User avatar color UserAvatarColor? avatarColor; /// User deleted at @@ -75,7 +74,9 @@ class SyncUserV1 { // json[r'avatarColor'] = null; } if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -83,7 +84,9 @@ class SyncUserV1 { json[r'hasProfileImage'] = this.hasProfileImage; json[r'id'] = this.id; json[r'name'] = this.name; - json[r'profileChangedAt'] = this.profileChangedAt.toUtc().toIso8601String(); + json[r'profileChangedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.profileChangedAt.millisecondsSinceEpoch + : this.profileChangedAt.toUtc().toIso8601String(); return json; } @@ -97,12 +100,12 @@ class SyncUserV1 { return SyncUserV1( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor']), - deletedAt: mapDateTime(json, r'deletedAt', r''), + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), email: mapValueOfType(json, r'email')!, hasProfileImage: mapValueOfType(json, r'hasProfileImage')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, - profileChangedAt: mapDateTime(json, r'profileChangedAt', r'')!, + profileChangedAt: mapDateTime(json, r'profileChangedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; @@ -150,7 +153,6 @@ class SyncUserV1 { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'avatarColor', 'deletedAt', 'email', 'hasProfileImage', diff --git a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart index 6c7acbd218..ecf2e5da4a 100644 --- a/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart +++ b/mobile/openapi/lib/model/system_config_f_fmpeg_dto.dart @@ -36,7 +36,6 @@ class SystemConfigFFmpegDto { required this.twoPass, }); - /// Transcode hardware acceleration TranscodeHWAccel accel; /// Accelerated decode @@ -57,7 +56,6 @@ class SystemConfigFFmpegDto { /// Maximum value: 16 int bframes; - /// CQ mode CQMode cqMode; /// CRF @@ -69,6 +67,7 @@ class SystemConfigFFmpegDto { /// GOP size /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int gopSize; /// Max bitrate @@ -86,13 +85,11 @@ class SystemConfigFFmpegDto { /// Maximum value: 6 int refs; - /// Target audio codec AudioCodec targetAudioCodec; /// Target resolution String targetResolution; - /// Target video codec VideoCodec targetVideoCodec; /// Temporal AQ @@ -101,12 +98,11 @@ class SystemConfigFFmpegDto { /// Threads /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int threads; - /// Tone mapping ToneMapping tonemap; - /// Transcode policy TranscodePolicy transcode; /// Two pass diff --git a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart index b5640f82c8..d78f8fadd5 100644 --- a/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_generated_fullsize_image_dto.dart @@ -15,18 +15,23 @@ class SystemConfigGeneratedFullsizeImageDto { SystemConfigGeneratedFullsizeImageDto({ required this.enabled, required this.format, - this.progressive = false, + this.progressive, required this.quality, }); /// Enabled bool enabled; - /// Image format ImageFormat format; /// Progressive - bool progressive; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? progressive; /// Quality /// @@ -46,7 +51,7 @@ class SystemConfigGeneratedFullsizeImageDto { // ignore: unnecessary_parenthesis (enabled.hashCode) + (format.hashCode) + - (progressive.hashCode) + + (progressive == null ? 0 : progressive!.hashCode) + (quality.hashCode); @override @@ -56,7 +61,11 @@ class SystemConfigGeneratedFullsizeImageDto { final json = {}; json[r'enabled'] = this.enabled; json[r'format'] = this.format; + if (this.progressive != null) { json[r'progressive'] = this.progressive; + } else { + // json[r'progressive'] = null; + } json[r'quality'] = this.quality; return json; } @@ -72,7 +81,7 @@ class SystemConfigGeneratedFullsizeImageDto { return SystemConfigGeneratedFullsizeImageDto( enabled: mapValueOfType(json, r'enabled')!, format: ImageFormat.fromJson(json[r'format'])!, - progressive: mapValueOfType(json, r'progressive') ?? false, + progressive: mapValueOfType(json, r'progressive'), quality: mapValueOfType(json, r'quality')!, ); } diff --git a/mobile/openapi/lib/model/system_config_generated_image_dto.dart b/mobile/openapi/lib/model/system_config_generated_image_dto.dart index 3e8fed2c68..2571c0cab0 100644 --- a/mobile/openapi/lib/model/system_config_generated_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_generated_image_dto.dart @@ -14,15 +14,21 @@ class SystemConfigGeneratedImageDto { /// Returns a new [SystemConfigGeneratedImageDto] instance. SystemConfigGeneratedImageDto({ required this.format, - this.progressive = false, + this.progressive, required this.quality, required this.size, }); - /// Image format ImageFormat format; - bool progressive; + /// Progressive + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? progressive; /// Quality /// @@ -33,6 +39,7 @@ class SystemConfigGeneratedImageDto { /// Size /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int size; @override @@ -46,7 +53,7 @@ class SystemConfigGeneratedImageDto { int get hashCode => // ignore: unnecessary_parenthesis (format.hashCode) + - (progressive.hashCode) + + (progressive == null ? 0 : progressive!.hashCode) + (quality.hashCode) + (size.hashCode); @@ -56,7 +63,11 @@ class SystemConfigGeneratedImageDto { Map toJson() { final json = {}; json[r'format'] = this.format; + if (this.progressive != null) { json[r'progressive'] = this.progressive; + } else { + // json[r'progressive'] = null; + } json[r'quality'] = this.quality; json[r'size'] = this.size; return json; @@ -72,7 +83,7 @@ class SystemConfigGeneratedImageDto { return SystemConfigGeneratedImageDto( format: ImageFormat.fromJson(json[r'format'])!, - progressive: mapValueOfType(json, r'progressive') ?? false, + progressive: mapValueOfType(json, r'progressive'), quality: mapValueOfType(json, r'quality')!, size: mapValueOfType(json, r'size')!, ); diff --git a/mobile/openapi/lib/model/system_config_image_dto.dart b/mobile/openapi/lib/model/system_config_image_dto.dart index 217a666a67..668b740872 100644 --- a/mobile/openapi/lib/model/system_config_image_dto.dart +++ b/mobile/openapi/lib/model/system_config_image_dto.dart @@ -20,7 +20,6 @@ class SystemConfigImageDto { required this.thumbnail, }); - /// Colorspace Colorspace colorspace; /// Extract embedded diff --git a/mobile/openapi/lib/model/system_config_library_scan_dto.dart b/mobile/openapi/lib/model/system_config_library_scan_dto.dart index 28ea603c2a..003000d2ec 100644 --- a/mobile/openapi/lib/model/system_config_library_scan_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_scan_dto.dart @@ -17,6 +17,7 @@ class SystemConfigLibraryScanDto { required this.enabled, }); + /// Cron expression String cronExpression; /// Enabled diff --git a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart index 2a0f1ffbc6..6162e72b8f 100644 --- a/mobile/openapi/lib/model/system_config_machine_learning_dto.dart +++ b/mobile/openapi/lib/model/system_config_machine_learning_dto.dart @@ -35,6 +35,7 @@ class SystemConfigMachineLearningDto { OcrConfig ocr; + /// ML service URLs List urls; @override diff --git a/mobile/openapi/lib/model/system_config_map_dto.dart b/mobile/openapi/lib/model/system_config_map_dto.dart index 109babd374..7a2fbb516b 100644 --- a/mobile/openapi/lib/model/system_config_map_dto.dart +++ b/mobile/openapi/lib/model/system_config_map_dto.dart @@ -18,11 +18,13 @@ class SystemConfigMapDto { required this.lightStyle, }); + /// Dark map style URL String darkStyle; /// Enabled bool enabled; + /// Light map style URL String lightStyle; @override diff --git a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart index cfb18b181e..0db417427f 100644 --- a/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart +++ b/mobile/openapi/lib/model/system_config_nightly_tasks_dto.dart @@ -33,6 +33,7 @@ class SystemConfigNightlyTasksDto { /// Missing thumbnails bool missingThumbnails; + /// Start time String startTime; /// Sync quota usage diff --git a/mobile/openapi/lib/model/system_config_o_auth_dto.dart b/mobile/openapi/lib/model/system_config_o_auth_dto.dart index 82195e498b..88dddbb4d3 100644 --- a/mobile/openapi/lib/model/system_config_o_auth_dto.dart +++ b/mobile/openapi/lib/model/system_config_o_auth_dto.dart @@ -51,7 +51,7 @@ class SystemConfigOAuthDto { /// Default storage quota /// /// Minimum value: 0 - int? defaultStorageQuota; + num? defaultStorageQuota; /// Enabled bool enabled; @@ -62,7 +62,7 @@ class SystemConfigOAuthDto { /// Mobile override enabled bool mobileOverrideEnabled; - /// Mobile redirect URI + /// Mobile redirect URI (set to empty string to disable) String mobileRedirectUri; /// Profile signing algorithm @@ -74,6 +74,7 @@ class SystemConfigOAuthDto { /// Scope String scope; + /// Signing algorithm String signingAlgorithm; /// Storage label claim @@ -85,9 +86,9 @@ class SystemConfigOAuthDto { /// Timeout /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int timeout; - /// Token endpoint auth method OAuthTokenEndpointAuthMethod tokenEndpointAuthMethod; @override @@ -177,7 +178,9 @@ class SystemConfigOAuthDto { buttonText: mapValueOfType(json, r'buttonText')!, clientId: mapValueOfType(json, r'clientId')!, clientSecret: mapValueOfType(json, r'clientSecret')!, - defaultStorageQuota: mapValueOfType(json, r'defaultStorageQuota'), + defaultStorageQuota: json[r'defaultStorageQuota'] == null + ? null + : num.parse('${json[r'defaultStorageQuota']}'), enabled: mapValueOfType(json, r'enabled')!, issuerUrl: mapValueOfType(json, r'issuerUrl')!, mobileOverrideEnabled: mapValueOfType(json, r'mobileOverrideEnabled')!, diff --git a/mobile/openapi/lib/model/system_config_template_emails_dto.dart b/mobile/openapi/lib/model/system_config_template_emails_dto.dart index 9db85509f5..d29ca1fac3 100644 --- a/mobile/openapi/lib/model/system_config_template_emails_dto.dart +++ b/mobile/openapi/lib/model/system_config_template_emails_dto.dart @@ -18,10 +18,13 @@ class SystemConfigTemplateEmailsDto { required this.welcomeTemplate, }); + /// Album invite template String albumInviteTemplate; + /// Album update template String albumUpdateTemplate; + /// Welcome template String welcomeTemplate; @override diff --git a/mobile/openapi/lib/model/system_config_trash_dto.dart b/mobile/openapi/lib/model/system_config_trash_dto.dart index 9bdaef92d3..790710751f 100644 --- a/mobile/openapi/lib/model/system_config_trash_dto.dart +++ b/mobile/openapi/lib/model/system_config_trash_dto.dart @@ -20,6 +20,7 @@ class SystemConfigTrashDto { /// Days /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int days; /// Enabled diff --git a/mobile/openapi/lib/model/system_config_user_dto.dart b/mobile/openapi/lib/model/system_config_user_dto.dart index a7313560e6..dc553e7369 100644 --- a/mobile/openapi/lib/model/system_config_user_dto.dart +++ b/mobile/openapi/lib/model/system_config_user_dto.dart @@ -19,6 +19,7 @@ class SystemConfigUserDto { /// Delete delay /// /// Minimum value: 1 + /// Maximum value: 9007199254740991 int deleteDelay; @override diff --git a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart index 5566846e3c..4d689f01a1 100644 --- a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart +++ b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart @@ -17,6 +17,9 @@ class TagBulkAssetsResponseDto { }); /// Number of assets tagged + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; @override diff --git a/mobile/openapi/lib/model/tag_create_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart index fd6a10163c..e05b29f1ed 100644 --- a/mobile/openapi/lib/model/tag_create_dto.dart +++ b/mobile/openapi/lib/model/tag_create_dto.dart @@ -19,12 +19,6 @@ class TagCreateDto { }); /// Tag color (hex) - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// String? color; /// Tag name diff --git a/mobile/openapi/lib/model/tags_response.dart b/mobile/openapi/lib/model/tags_response.dart index 1e4a4bd109..8a3ac17474 100644 --- a/mobile/openapi/lib/model/tags_response.dart +++ b/mobile/openapi/lib/model/tags_response.dart @@ -13,8 +13,8 @@ part of openapi.api; class TagsResponse { /// Returns a new [TagsResponse] instance. TagsResponse({ - this.enabled = true, - this.sidebarWeb = true, + required this.enabled, + required this.sidebarWeb, }); /// Whether tags are enabled diff --git a/mobile/openapi/lib/model/time_buckets_response_dto.dart b/mobile/openapi/lib/model/time_buckets_response_dto.dart index 11faa815e2..8b8da1d37a 100644 --- a/mobile/openapi/lib/model/time_buckets_response_dto.dart +++ b/mobile/openapi/lib/model/time_buckets_response_dto.dart @@ -18,6 +18,9 @@ class TimeBucketsResponseDto { }); /// Number of assets in this time bucket + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; /// Time bucket identifier in YYYY-MM-DD format representing the start of the time period diff --git a/mobile/openapi/lib/model/trash_response_dto.dart b/mobile/openapi/lib/model/trash_response_dto.dart index 7edd5d032a..7b43d9ceb7 100644 --- a/mobile/openapi/lib/model/trash_response_dto.dart +++ b/mobile/openapi/lib/model/trash_response_dto.dart @@ -17,6 +17,9 @@ class TrashResponseDto { }); /// Number of items in trash + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int count; @override diff --git a/mobile/openapi/lib/model/update_album_dto.dart b/mobile/openapi/lib/model/update_album_dto.dart index 46ce8b0ecc..ae4a5c1f87 100644 --- a/mobile/openapi/lib/model/update_album_dto.dart +++ b/mobile/openapi/lib/model/update_album_dto.dart @@ -56,7 +56,6 @@ class UpdateAlbumDto { /// bool? isActivityEnabled; - /// Asset sort order /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/openapi/lib/model/update_album_user_dto.dart b/mobile/openapi/lib/model/update_album_user_dto.dart index 9d934eb465..43218cae6e 100644 --- a/mobile/openapi/lib/model/update_album_user_dto.dart +++ b/mobile/openapi/lib/model/update_album_user_dto.dart @@ -16,7 +16,6 @@ class UpdateAlbumUserDto { required this.role, }); - /// Album user role AlbumUserRole role; @override diff --git a/mobile/openapi/lib/model/update_asset_dto.dart b/mobile/openapi/lib/model/update_asset_dto.dart index 8526995934..2c4c3352ea 100644 --- a/mobile/openapi/lib/model/update_asset_dto.dart +++ b/mobile/openapi/lib/model/update_asset_dto.dart @@ -52,6 +52,9 @@ class UpdateAssetDto { /// Latitude coordinate /// + /// Minimum value: -90 + /// Maximum value: 90 + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated /// source code must fall back to having a nullable type. @@ -64,6 +67,9 @@ class UpdateAssetDto { /// Longitude coordinate /// + /// Minimum value: -180 + /// Maximum value: 180 + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated /// source code must fall back to having a nullable type. @@ -75,9 +81,8 @@ class UpdateAssetDto { /// /// Minimum value: -1 /// Maximum value: 5 - num? rating; + int? rating; - /// Asset visibility /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -172,9 +177,7 @@ class UpdateAssetDto { latitude: num.parse('${json[r'latitude']}'), livePhotoVideoId: mapValueOfType(json, r'livePhotoVideoId'), longitude: num.parse('${json[r'longitude']}'), - rating: json[r'rating'] == null - ? null - : num.parse('${json[r'rating']}'), + rating: mapValueOfType(json, r'rating'), visibility: AssetVisibility.fromJson(json[r'visibility']), ); } diff --git a/mobile/openapi/lib/model/update_library_dto.dart b/mobile/openapi/lib/model/update_library_dto.dart index 628bdc0055..276d43ecd9 100644 --- a/mobile/openapi/lib/model/update_library_dto.dart +++ b/mobile/openapi/lib/model/update_library_dto.dart @@ -13,16 +13,16 @@ part of openapi.api; class UpdateLibraryDto { /// Returns a new [UpdateLibraryDto] instance. UpdateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], this.name, }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths (max 128) - Set importPaths; + List importPaths; /// Library name /// @@ -51,8 +51,8 @@ class UpdateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; if (this.name != null) { json[r'name'] = this.name; } else { @@ -71,11 +71,11 @@ class UpdateLibraryDto { return UpdateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], name: mapValueOfType(json, r'name'), ); } diff --git a/mobile/openapi/lib/model/usage_by_user_dto.dart b/mobile/openapi/lib/model/usage_by_user_dto.dart index da1fe600a5..462b82c3e0 100644 --- a/mobile/openapi/lib/model/usage_by_user_dto.dart +++ b/mobile/openapi/lib/model/usage_by_user_dto.dart @@ -24,18 +24,33 @@ class UsageByUserDto { }); /// Number of photos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int photos; /// User quota size in bytes (null if unlimited) + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Total storage usage in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usage; /// Storage usage for photos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usagePhotos; /// Storage usage for videos in bytes + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int usageVideos; /// User ID @@ -45,6 +60,9 @@ class UsageByUserDto { String userName; /// Number of videos + /// + /// Minimum value: -9007199254740991 + /// Maximum value: 9007199254740991 int videos; @override diff --git a/mobile/openapi/lib/model/user_admin_create_dto.dart b/mobile/openapi/lib/model/user_admin_create_dto.dart index 485b2e00e5..54da0b0566 100644 --- a/mobile/openapi/lib/model/user_admin_create_dto.dart +++ b/mobile/openapi/lib/model/user_admin_create_dto.dart @@ -25,7 +25,6 @@ class UserAdminCreateDto { this.storageLabel, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email @@ -61,6 +60,7 @@ class UserAdminCreateDto { /// Storage quota in bytes /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Require password change on next login diff --git a/mobile/openapi/lib/model/user_admin_response_dto.dart b/mobile/openapi/lib/model/user_admin_response_dto.dart index 706f65cf35..09f8cedce4 100644 --- a/mobile/openapi/lib/model/user_admin_response_dto.dart +++ b/mobile/openapi/lib/model/user_admin_response_dto.dart @@ -32,7 +32,6 @@ class UserAdminResponseDto { required this.updatedAt, }); - /// Avatar color UserAvatarColor avatarColor; /// Creation date @@ -50,7 +49,6 @@ class UserAdminResponseDto { /// Is admin user bool isAdmin; - /// User license UserLicense? license; /// User name @@ -66,15 +64,20 @@ class UserAdminResponseDto { String profileImagePath; /// Storage quota in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Storage usage in bytes + /// + /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaUsageInBytes; /// Require password change on next login bool shouldChangePassword; - /// User status UserStatus status; /// Storage label @@ -130,9 +133,13 @@ class UserAdminResponseDto { Map toJson() { final json = {}; json[r'avatarColor'] = this.avatarColor; - json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); + json[r'createdAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.createdAt.millisecondsSinceEpoch + : this.createdAt.toUtc().toIso8601String(); if (this.deletedAt != null) { - json[r'deletedAt'] = this.deletedAt!.toUtc().toIso8601String(); + json[r'deletedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.deletedAt!.millisecondsSinceEpoch + : this.deletedAt!.toUtc().toIso8601String(); } else { // json[r'deletedAt'] = null; } @@ -165,7 +172,9 @@ class UserAdminResponseDto { } else { // json[r'storageLabel'] = null; } - json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'updatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.updatedAt.millisecondsSinceEpoch + : this.updatedAt.toUtc().toIso8601String(); return json; } @@ -179,8 +188,8 @@ class UserAdminResponseDto { return UserAdminResponseDto( avatarColor: UserAvatarColor.fromJson(json[r'avatarColor'])!, - createdAt: mapDateTime(json, r'createdAt', r'')!, - deletedAt: mapDateTime(json, r'deletedAt', r''), + createdAt: mapDateTime(json, r'createdAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, + deletedAt: mapDateTime(json, r'deletedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/'), email: mapValueOfType(json, r'email')!, id: mapValueOfType(json, r'id')!, isAdmin: mapValueOfType(json, r'isAdmin')!, @@ -194,7 +203,7 @@ class UserAdminResponseDto { shouldChangePassword: mapValueOfType(json, r'shouldChangePassword')!, status: UserStatus.fromJson(json[r'status'])!, storageLabel: mapValueOfType(json, r'storageLabel'), - updatedAt: mapDateTime(json, r'updatedAt', r'')!, + updatedAt: mapDateTime(json, r'updatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, ); } return null; diff --git a/mobile/openapi/lib/model/user_admin_update_dto.dart b/mobile/openapi/lib/model/user_admin_update_dto.dart index 3cce65745f..0c33a46139 100644 --- a/mobile/openapi/lib/model/user_admin_update_dto.dart +++ b/mobile/openapi/lib/model/user_admin_update_dto.dart @@ -24,7 +24,6 @@ class UserAdminUpdateDto { this.storageLabel, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email @@ -69,6 +68,7 @@ class UserAdminUpdateDto { /// Storage quota in bytes /// /// Minimum value: 0 + /// Maximum value: 9007199254740991 int? quotaSizeInBytes; /// Require password change on next login diff --git a/mobile/openapi/lib/model/user_avatar_color.dart b/mobile/openapi/lib/model/user_avatar_color.dart index 4fcf518550..719e366899 100644 --- a/mobile/openapi/lib/model/user_avatar_color.dart +++ b/mobile/openapi/lib/model/user_avatar_color.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Avatar color +/// User avatar color class UserAvatarColor { /// Instantiate a new enum with the provided [value]. const UserAvatarColor._(this.value); diff --git a/mobile/openapi/lib/model/user_license.dart b/mobile/openapi/lib/model/user_license.dart index f02dc73bef..8ef46a0bb5 100644 --- a/mobile/openapi/lib/model/user_license.dart +++ b/mobile/openapi/lib/model/user_license.dart @@ -24,7 +24,7 @@ class UserLicense { /// Activation key String activationKey; - /// License key + /// License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/) String licenseKey; @override @@ -45,7 +45,9 @@ class UserLicense { Map toJson() { final json = {}; - json[r'activatedAt'] = this.activatedAt.toUtc().toIso8601String(); + json[r'activatedAt'] = _isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/') + ? this.activatedAt.millisecondsSinceEpoch + : this.activatedAt.toUtc().toIso8601String(); json[r'activationKey'] = this.activationKey; json[r'licenseKey'] = this.licenseKey; return json; @@ -60,7 +62,7 @@ class UserLicense { final json = value.cast(); return UserLicense( - activatedAt: mapDateTime(json, r'activatedAt', r'')!, + activatedAt: mapDateTime(json, r'activatedAt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$/')!, activationKey: mapValueOfType(json, r'activationKey')!, licenseKey: mapValueOfType(json, r'licenseKey')!, ); diff --git a/mobile/openapi/lib/model/user_response_dto.dart b/mobile/openapi/lib/model/user_response_dto.dart index bf0e2cbf09..f671072c72 100644 --- a/mobile/openapi/lib/model/user_response_dto.dart +++ b/mobile/openapi/lib/model/user_response_dto.dart @@ -21,7 +21,6 @@ class UserResponseDto { required this.profileImagePath, }); - /// Avatar color UserAvatarColor avatarColor; /// User email diff --git a/mobile/openapi/lib/model/user_update_me_dto.dart b/mobile/openapi/lib/model/user_update_me_dto.dart index 066c435eb3..0751d4096b 100644 --- a/mobile/openapi/lib/model/user_update_me_dto.dart +++ b/mobile/openapi/lib/model/user_update_me_dto.dart @@ -19,7 +19,6 @@ class UserUpdateMeDto { this.password, }); - /// Avatar color UserAvatarColor? avatarColor; /// User email diff --git a/mobile/openapi/lib/model/validate_library_dto.dart b/mobile/openapi/lib/model/validate_library_dto.dart index 59c3680782..68fb0e9fe2 100644 --- a/mobile/openapi/lib/model/validate_library_dto.dart +++ b/mobile/openapi/lib/model/validate_library_dto.dart @@ -13,15 +13,15 @@ part of openapi.api; class ValidateLibraryDto { /// Returns a new [ValidateLibraryDto] instance. ValidateLibraryDto({ - this.exclusionPatterns = const {}, - this.importPaths = const {}, + this.exclusionPatterns = const [], + this.importPaths = const [], }); /// Exclusion patterns (max 128) - Set exclusionPatterns; + List exclusionPatterns; /// Import paths to validate (max 128) - Set importPaths; + List importPaths; @override bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryDto && @@ -39,8 +39,8 @@ class ValidateLibraryDto { Map toJson() { final json = {}; - json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false); - json[r'importPaths'] = this.importPaths.toList(growable: false); + json[r'exclusionPatterns'] = this.exclusionPatterns; + json[r'importPaths'] = this.importPaths; return json; } @@ -54,11 +54,11 @@ class ValidateLibraryDto { return ValidateLibraryDto( exclusionPatterns: json[r'exclusionPatterns'] is Iterable - ? (json[r'exclusionPatterns'] as Iterable).cast().toSet() - : const {}, + ? (json[r'exclusionPatterns'] as Iterable).cast().toList(growable: false) + : const [], importPaths: json[r'importPaths'] is Iterable - ? (json[r'importPaths'] as Iterable).cast().toSet() - : const {}, + ? (json[r'importPaths'] as Iterable).cast().toList(growable: false) + : const [], ); } return null; diff --git a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart index 78cc03dc94..ebcb881935 100644 --- a/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart +++ b/mobile/openapi/lib/model/validate_library_import_path_response_dto.dart @@ -14,7 +14,7 @@ class ValidateLibraryImportPathResponseDto { /// Returns a new [ValidateLibraryImportPathResponseDto] instance. ValidateLibraryImportPathResponseDto({ required this.importPath, - this.isValid = false, + required this.isValid, this.message, }); diff --git a/mobile/openapi/lib/model/video_container.dart b/mobile/openapi/lib/model/video_container.dart index b1a47c8721..a291fabf6e 100644 --- a/mobile/openapi/lib/model/video_container.dart +++ b/mobile/openapi/lib/model/video_container.dart @@ -10,7 +10,7 @@ part of openapi.api; -/// Accepted containers +/// Accepted video containers class VideoContainer { /// Instantiate a new enum with the provided [value]. const VideoContainer._(this.value); diff --git a/mobile/openapi/lib/model/workflow_action_item_dto.dart b/mobile/openapi/lib/model/workflow_action_item_dto.dart index 9222dd6ba7..1ad70238d8 100644 --- a/mobile/openapi/lib/model/workflow_action_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_item_dto.dart @@ -13,31 +13,24 @@ part of openapi.api; class WorkflowActionItemDto { /// Returns a new [WorkflowActionItemDto] instance. WorkflowActionItemDto({ - this.actionConfig, + this.actionConfig = const {}, required this.pluginActionId, }); - /// Action configuration - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - Object? actionConfig; + Map actionConfig; /// Plugin action ID String pluginActionId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowActionItemDto && - other.actionConfig == actionConfig && + _deepEquality.equals(other.actionConfig, actionConfig) && other.pluginActionId == pluginActionId; @override int get hashCode => // ignore: unnecessary_parenthesis - (actionConfig == null ? 0 : actionConfig!.hashCode) + + (actionConfig.hashCode) + (pluginActionId.hashCode); @override @@ -45,11 +38,7 @@ class WorkflowActionItemDto { Map toJson() { final json = {}; - if (this.actionConfig != null) { json[r'actionConfig'] = this.actionConfig; - } else { - // json[r'actionConfig'] = null; - } json[r'pluginActionId'] = this.pluginActionId; return json; } @@ -63,7 +52,7 @@ class WorkflowActionItemDto { final json = value.cast(); return WorkflowActionItemDto( - actionConfig: mapValueOfType(json, r'actionConfig'), + actionConfig: mapCastOfType(json, r'actionConfig') ?? const {}, pluginActionId: mapValueOfType(json, r'pluginActionId')!, ); } diff --git a/mobile/openapi/lib/model/workflow_action_response_dto.dart b/mobile/openapi/lib/model/workflow_action_response_dto.dart index 8f77e9cf2b..dcbb5ee8ef 100644 --- a/mobile/openapi/lib/model/workflow_action_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_action_response_dto.dart @@ -20,8 +20,7 @@ class WorkflowActionResponseDto { required this.workflowId, }); - /// Action configuration - Object? actionConfig; + Map? actionConfig; /// Action ID String id; @@ -37,7 +36,7 @@ class WorkflowActionResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is WorkflowActionResponseDto && - other.actionConfig == actionConfig && + _deepEquality.equals(other.actionConfig, actionConfig) && other.id == id && other.order == order && other.pluginActionId == pluginActionId && @@ -78,7 +77,7 @@ class WorkflowActionResponseDto { final json = value.cast(); return WorkflowActionResponseDto( - actionConfig: mapValueOfType(json, r'actionConfig'), + actionConfig: mapCastOfType(json, r'actionConfig'), id: mapValueOfType(json, r'id')!, order: num.parse('${json[r'order']}'), pluginActionId: mapValueOfType(json, r'pluginActionId')!, diff --git a/mobile/openapi/lib/model/workflow_create_dto.dart b/mobile/openapi/lib/model/workflow_create_dto.dart index 38665a1912..143af0ca6c 100644 --- a/mobile/openapi/lib/model/workflow_create_dto.dart +++ b/mobile/openapi/lib/model/workflow_create_dto.dart @@ -48,7 +48,6 @@ class WorkflowCreateDto { /// Workflow name String name; - /// Workflow trigger type PluginTriggerType triggerType; @override diff --git a/mobile/openapi/lib/model/workflow_filter_item_dto.dart b/mobile/openapi/lib/model/workflow_filter_item_dto.dart index 52e29c3e93..92224b9f16 100644 --- a/mobile/openapi/lib/model/workflow_filter_item_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_item_dto.dart @@ -13,31 +13,24 @@ part of openapi.api; class WorkflowFilterItemDto { /// Returns a new [WorkflowFilterItemDto] instance. WorkflowFilterItemDto({ - this.filterConfig, + this.filterConfig = const {}, required this.pluginFilterId, }); - /// Filter configuration - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - Object? filterConfig; + Map filterConfig; /// Plugin filter ID String pluginFilterId; @override bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterItemDto && - other.filterConfig == filterConfig && + _deepEquality.equals(other.filterConfig, filterConfig) && other.pluginFilterId == pluginFilterId; @override int get hashCode => // ignore: unnecessary_parenthesis - (filterConfig == null ? 0 : filterConfig!.hashCode) + + (filterConfig.hashCode) + (pluginFilterId.hashCode); @override @@ -45,11 +38,7 @@ class WorkflowFilterItemDto { Map toJson() { final json = {}; - if (this.filterConfig != null) { json[r'filterConfig'] = this.filterConfig; - } else { - // json[r'filterConfig'] = null; - } json[r'pluginFilterId'] = this.pluginFilterId; return json; } @@ -63,7 +52,7 @@ class WorkflowFilterItemDto { final json = value.cast(); return WorkflowFilterItemDto( - filterConfig: mapValueOfType(json, r'filterConfig'), + filterConfig: mapCastOfType(json, r'filterConfig') ?? const {}, pluginFilterId: mapValueOfType(json, r'pluginFilterId')!, ); } diff --git a/mobile/openapi/lib/model/workflow_filter_response_dto.dart b/mobile/openapi/lib/model/workflow_filter_response_dto.dart index 355378adac..932722f5a5 100644 --- a/mobile/openapi/lib/model/workflow_filter_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_filter_response_dto.dart @@ -20,8 +20,7 @@ class WorkflowFilterResponseDto { required this.workflowId, }); - /// Filter configuration - Object? filterConfig; + Map? filterConfig; /// Filter ID String id; @@ -37,7 +36,7 @@ class WorkflowFilterResponseDto { @override bool operator ==(Object other) => identical(this, other) || other is WorkflowFilterResponseDto && - other.filterConfig == filterConfig && + _deepEquality.equals(other.filterConfig, filterConfig) && other.id == id && other.order == order && other.pluginFilterId == pluginFilterId && @@ -78,7 +77,7 @@ class WorkflowFilterResponseDto { final json = value.cast(); return WorkflowFilterResponseDto( - filterConfig: mapValueOfType(json, r'filterConfig'), + filterConfig: mapCastOfType(json, r'filterConfig'), id: mapValueOfType(json, r'id')!, order: num.parse('${json[r'order']}'), pluginFilterId: mapValueOfType(json, r'pluginFilterId')!, diff --git a/mobile/openapi/lib/model/workflow_response_dto.dart b/mobile/openapi/lib/model/workflow_response_dto.dart index ae3e6510aa..6461b62508 100644 --- a/mobile/openapi/lib/model/workflow_response_dto.dart +++ b/mobile/openapi/lib/model/workflow_response_dto.dart @@ -48,7 +48,6 @@ class WorkflowResponseDto { /// Owner user ID String ownerId; - /// Workflow trigger type PluginTriggerType triggerType; @override diff --git a/mobile/openapi/lib/model/workflow_update_dto.dart b/mobile/openapi/lib/model/workflow_update_dto.dart index 9891fff079..9abb45ddd5 100644 --- a/mobile/openapi/lib/model/workflow_update_dto.dart +++ b/mobile/openapi/lib/model/workflow_update_dto.dart @@ -54,7 +54,6 @@ class WorkflowUpdateDto { /// String? name; - /// Workflow trigger type /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated diff --git a/mobile/test/modules/utils/openapi_patching_test.dart b/mobile/test/modules/utils/openapi_patching_test.dart index a577b0544f..18ab07b3a9 100644 --- a/mobile/test/modules/utils/openapi_patching_test.dart +++ b/mobile/test/modules/utils/openapi_patching_test.dart @@ -21,7 +21,7 @@ void main() { """); upgradeDto(value, targetType); - expect(value['tags'], TagsResponse().toJson()); + expect(value['tags'], TagsResponse(enabled: false, sidebarWeb: false).toJson()); expect(value['download']['includeEmbeddedVideos'], false); }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 90d151a2a3..f07898d4e7 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -11,8 +11,12 @@ "required": true, "in": "query", "description": "Album ID", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -21,8 +25,12 @@ "required": false, "in": "query", "description": "Asset ID (if activity is for an asset)", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -30,7 +38,9 @@ "name": "level", "required": false, "in": "query", - "description": "Filter by activity level", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "$ref": "#/components/schemas/ReactionLevel" } @@ -39,7 +49,9 @@ "name": "type", "required": false, "in": "query", - "description": "Filter by activity type", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "$ref": "#/components/schemas/ReactionType" } @@ -49,8 +61,12 @@ "required": false, "in": "query", "description": "Filter by user ID", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -171,8 +187,12 @@ "required": true, "in": "query", "description": "Album ID", + "x-nestjs_zod-parent-metadata": { + "description": "Activity" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -181,8 +201,12 @@ "required": false, "in": "query", "description": "Asset ID (if activity is for an asset)", + "x-nestjs_zod-parent-metadata": { + "description": "Activity" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -243,6 +267,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -512,7 +537,7 @@ "required": true, "in": "path", "schema": { - "format": "string", + "pattern": "^[a-zA-Z0-9_\\-.]+$", "type": "string" } } @@ -936,6 +961,7 @@ "description": "User ID filter", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -1068,6 +1094,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1137,6 +1164,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1196,6 +1224,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1267,6 +1296,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1326,6 +1356,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1397,6 +1428,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1458,6 +1490,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1522,6 +1555,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -1547,7 +1581,6 @@ "name": "visibility", "required": false, "in": "query", - "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -1611,6 +1644,7 @@ "description": "Filter albums containing this asset ID (ignores shared parameter)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -1868,6 +1902,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1919,6 +1954,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -2002,6 +2038,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2072,6 +2109,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2143,6 +2181,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -2232,6 +2271,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -2291,6 +2331,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -2362,6 +2403,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2592,6 +2634,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2643,6 +2686,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2701,6 +2745,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3441,7 +3486,6 @@ "name": "visibility", "required": false, "in": "query", - "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -3503,6 +3547,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -3577,6 +3622,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3647,6 +3693,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3694,6 +3741,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3748,6 +3796,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3814,6 +3863,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3875,6 +3925,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3949,6 +4000,7 @@ "description": "Asset ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4010,6 +4062,7 @@ "description": "Asset ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4079,6 +4132,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -4152,6 +4206,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4228,6 +4283,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4322,6 +4378,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4337,7 +4394,6 @@ "name": "size", "required": false, "in": "query", - "description": "Asset media size", "schema": { "$ref": "#/components/schemas/AssetMediaSize" } @@ -4408,6 +4464,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -5355,6 +5412,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5409,6 +5467,7 @@ "description": "Face ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5523,6 +5582,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5584,6 +5644,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5762,7 +5823,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -5953,6 +6013,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6005,6 +6066,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6064,6 +6126,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6135,6 +6198,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6189,6 +6253,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6250,6 +6315,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6321,6 +6387,8 @@ "description": "Filter assets created after this date", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6331,6 +6399,8 @@ "description": "Filter assets created before this date", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6505,6 +6575,8 @@ "description": "Filter by date", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6530,7 +6602,6 @@ "name": "order", "required": false, "in": "query", - "description": "Sort order", "schema": { "$ref": "#/components/schemas/MemorySearchOrder" } @@ -6542,6 +6613,7 @@ "description": "Number of memories to return", "schema": { "minimum": 1, + "maximum": 9007199254740991, "type": "integer" } }, @@ -6549,7 +6621,6 @@ "name": "type", "required": false, "in": "query", - "description": "Memory type", "schema": { "$ref": "#/components/schemas/MemoryType" } @@ -6673,6 +6744,8 @@ "description": "Filter by date", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6698,7 +6771,6 @@ "name": "order", "required": false, "in": "query", - "description": "Sort order", "schema": { "$ref": "#/components/schemas/MemorySearchOrder" } @@ -6710,6 +6782,7 @@ "description": "Number of memories to return", "schema": { "minimum": 1, + "maximum": 9007199254740991, "type": "integer" } }, @@ -6717,7 +6790,6 @@ "name": "type", "required": false, "in": "query", - "description": "Memory type", "schema": { "$ref": "#/components/schemas/MemoryType" } @@ -6779,6 +6851,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6830,6 +6903,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6888,6 +6962,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6958,6 +7033,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7029,6 +7105,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7154,6 +7231,7 @@ "description": "Filter by notification ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -7161,7 +7239,6 @@ "name": "level", "required": false, "in": "query", - "description": "Filter by notification level", "schema": { "$ref": "#/components/schemas/NotificationLevel" } @@ -7170,7 +7247,6 @@ "name": "type", "required": false, "in": "query", - "description": "Filter by notification type", "schema": { "$ref": "#/components/schemas/NotificationType" } @@ -7295,6 +7371,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7346,6 +7423,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7404,6 +7482,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7707,7 +7786,6 @@ "name": "direction", "required": true, "in": "query", - "description": "Partner direction", "schema": { "$ref": "#/components/schemas/PartnerDirection" } @@ -7830,6 +7908,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7882,6 +7961,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7938,6 +8018,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8060,6 +8141,7 @@ "description": "Closest asset ID for similarity search", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -8070,6 +8152,7 @@ "description": "Closest person ID for similarity search", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -8281,6 +8364,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8332,6 +8416,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8390,6 +8475,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8460,6 +8546,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8533,6 +8620,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8606,6 +8694,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8666,6 +8755,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8825,6 +8915,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8929,7 +9020,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8984,7 +9074,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -9051,7 +9140,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -9109,7 +9197,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -9292,7 +9379,8 @@ "type": "array", "items": { "type": "string", - "format": "uuid" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" } } }, @@ -9302,8 +9390,8 @@ "in": "query", "description": "Filter by city name", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9312,8 +9400,8 @@ "in": "query", "description": "Filter by country name", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9323,6 +9411,8 @@ "description": "Filter by creation date (after)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9333,6 +9423,8 @@ "description": "Filter by creation date (before)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9396,8 +9488,8 @@ "in": "query", "description": "Filter by lens model", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9406,9 +9498,10 @@ "in": "query", "description": "Library ID to filter by", "schema": { + "type": "string", "format": "uuid", - "nullable": true, - "type": "string" + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "nullable": true } }, { @@ -9417,7 +9510,8 @@ "in": "query", "description": "Filter by camera make", "schema": { - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9427,6 +9521,7 @@ "description": "Minimum file size in bytes", "schema": { "minimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, @@ -9436,8 +9531,8 @@ "in": "query", "description": "Filter by camera model", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9458,7 +9553,8 @@ "type": "array", "items": { "type": "string", - "format": "uuid" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" } } }, @@ -9484,10 +9580,10 @@ ], "x-immich-state": "Stable", "schema": { + "type": "number", "minimum": -1, "maximum": 5, - "nullable": true, - "type": "number" + "nullable": true } }, { @@ -9507,8 +9603,8 @@ "in": "query", "description": "Filter by state/province name", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9517,12 +9613,13 @@ "in": "query", "description": "Filter by tag IDs", "schema": { - "nullable": true, "type": "array", "items": { "type": "string", - "format": "uuid" - } + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" + }, + "nullable": true } }, { @@ -9532,6 +9629,8 @@ "description": "Filter by taken date (after)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9542,6 +9641,8 @@ "description": "Filter by taken date (before)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9552,6 +9653,8 @@ "description": "Filter by trash date (after)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9562,6 +9665,8 @@ "description": "Filter by trash date (before)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9569,7 +9674,6 @@ "name": "type", "required": false, "in": "query", - "description": "Asset type filter", "schema": { "$ref": "#/components/schemas/AssetTypeEnum" } @@ -9581,6 +9685,8 @@ "description": "Filter by update date (after)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9591,6 +9697,8 @@ "description": "Filter by update date (before)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9598,7 +9706,6 @@ "name": "visibility", "required": false, "in": "query", - "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -10122,7 +10229,6 @@ "name": "type", "required": true, "in": "query", - "description": "Suggestion type", "schema": { "$ref": "#/components/schemas/SearchSuggestionType" } @@ -11014,6 +11120,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11065,6 +11172,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11135,6 +11243,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11189,6 +11298,7 @@ "description": "Filter by album ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -11205,6 +11315,7 @@ ], "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11406,7 +11517,6 @@ "in": "query", "description": "Link password", "schema": { - "example": "password", "type": "string" } }, @@ -11483,6 +11593,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11534,6 +11645,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11592,6 +11704,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11662,6 +11775,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11733,6 +11847,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -11873,6 +11988,7 @@ "description": "Filter by primary asset ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11994,6 +12110,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -12045,6 +12162,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -12103,6 +12221,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -12173,6 +12292,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -12182,6 +12302,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13209,6 +13330,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13260,6 +13382,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13318,6 +13441,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13388,6 +13512,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13459,6 +13584,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13533,6 +13659,7 @@ "description": "Filter assets belonging to a specific album", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13588,6 +13715,7 @@ "description": "Filter assets containing a specific person (face recognition)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13606,6 +13734,7 @@ "description": "Filter assets with a specific tag", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13613,7 +13742,7 @@ "name": "timeBucket", "required": true, "in": "query", - "description": "Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024)", + "description": "Time bucket identifier in YYYY-MM-DD format", "schema": { "example": "2024-01-01", "type": "string" @@ -13626,6 +13755,7 @@ "description": "Filter assets by specific user ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13719,6 +13849,7 @@ "description": "Filter assets belonging to a specific album", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13774,6 +13905,7 @@ "description": "Filter assets containing a specific person (face recognition)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13792,6 +13924,7 @@ "description": "Filter assets with a specific tag", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13802,6 +13935,7 @@ "description": "Filter assets by specific user ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -14726,6 +14860,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -14786,6 +14921,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15065,6 +15201,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15112,6 +15249,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15166,6 +15304,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15443,7 +15582,9 @@ "properties": { "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -15463,7 +15604,9 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -15494,15 +15637,18 @@ "type": "object" }, "ActivityCreateDto": { + "description": "Activity create", "properties": { "albumId": { "description": "Album ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "assetId": { "description": "Asset ID (if activity is for an asset)", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "comment": { @@ -15510,12 +15656,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/ReactionType" - } - ], - "description": "Activity type (like or comment)" + "$ref": "#/components/schemas/ReactionType" } }, "required": [ @@ -15528,7 +15669,9 @@ "properties": { "assetId": { "description": "Asset ID (if activity is for an asset)", + "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "comment": { @@ -15538,20 +15681,19 @@ }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { "description": "Activity ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/ReactionType" - } - ], - "description": "Activity type" + "$ref": "#/components/schemas/ReactionType" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -15570,10 +15712,14 @@ "properties": { "comments": { "description": "Number of comments", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "likes": { "description": "Number of likes", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -15630,6 +15776,8 @@ }, "assetCount": { "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "assets": { @@ -15676,12 +15824,7 @@ "type": "string" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "description": "Asset sort order" + "$ref": "#/components/schemas/AssetOrder" }, "owner": { "$ref": "#/components/schemas/UserResponseDto" @@ -15727,14 +15870,20 @@ "properties": { "notShared": { "description": "Number of non-shared albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "owned": { "description": "Number of owned albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "shared": { "description": "Number of shared albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -15748,17 +15897,14 @@ "AlbumUserAddDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], + "$ref": "#/components/schemas/AlbumUserRole", "default": "editor", "description": "Album user role" }, "userId": { "description": "User ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -15770,16 +15916,12 @@ "AlbumUserCreateDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" }, "userId": { "description": "User ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -15792,12 +15934,7 @@ "AlbumUserResponseDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -15823,6 +15960,7 @@ "description": "Album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15831,6 +15969,7 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15845,12 +15984,7 @@ "AlbumsAddAssetsResponseDto": { "properties": { "error": { - "allOf": [ - { - "$ref": "#/components/schemas/BulkIdErrorReason" - } - ], - "description": "Error reason" + "$ref": "#/components/schemas/BulkIdErrorReason" }, "success": { "description": "Operation success", @@ -15865,13 +15999,7 @@ "AlbumsResponse": { "properties": { "defaultAssetOrder": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "default": "desc", - "description": "Default asset order for albums" + "$ref": "#/components/schemas/AssetOrder" } }, "required": [ @@ -15883,12 +16011,7 @@ "description": "Album preferences", "properties": { "defaultAssetOrder": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "description": "Default asset order for albums" + "$ref": "#/components/schemas/AssetOrder" } }, "type": "object" @@ -15903,6 +16026,7 @@ "description": "IDs to process", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15936,6 +16060,7 @@ "description": "Asset IDs to update", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15946,10 +16071,14 @@ }, "latitude": { "description": "Latitude coordinate", + "maximum": 90, + "minimum": -90, "type": "number" }, "longitude": { "description": "Longitude coordinate", + "maximum": 180, + "minimum": -180, "type": "number" }, "rating": { @@ -15957,7 +16086,7 @@ "maximum": 5, "minimum": -1, "nullable": true, - "type": "number", + "type": "integer", "x-immich-history": [ { "version": "v1", @@ -15980,12 +16109,7 @@ "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "required": [ @@ -16043,12 +16167,7 @@ "AssetBulkUploadCheckResult": { "properties": { "action": { - "description": "Upload action", - "enum": [ - "accept", - "reject" - ], - "type": "string" + "$ref": "#/components/schemas/AssetUploadAction" }, "assetId": { "description": "Existing asset ID if duplicate", @@ -16063,12 +16182,7 @@ "type": "boolean" }, "reason": { - "description": "Rejection reason if rejected", - "enum": [ - "duplicate", - "unsupported-format" - ], - "type": "string" + "$ref": "#/components/schemas/AssetRejectReason" } }, "required": [ @@ -16102,6 +16216,7 @@ "sourceId": { "description": "Source asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "stack": { @@ -16112,6 +16227,7 @@ "targetId": { "description": "Target asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -16125,13 +16241,16 @@ "properties": { "updatedAfter": { "description": "Sync assets updated after this date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "userIds": { "description": "User IDs to sync", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -16144,6 +16263,7 @@ "type": "object" }, "AssetDeltaSyncResponseDto": { + "description": "Asset delta sync response", "properties": { "deleted": { "description": "Deleted asset IDs", @@ -16157,7 +16277,6 @@ "type": "boolean" }, "upserted": { - "description": "Upserted assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, @@ -16183,12 +16302,7 @@ "AssetEditActionItemDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ], - "description": "Type of edit action to perform" + "$ref": "#/components/schemas/AssetEditAction" }, "parameters": { "anyOf": [ @@ -16214,15 +16328,12 @@ "AssetEditActionItemResponseDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ], - "description": "Type of edit action to perform" + "$ref": "#/components/schemas/AssetEditAction" }, "id": { + "description": "Asset edit ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "parameters": { @@ -16268,6 +16379,7 @@ "assetId": { "description": "Asset ID these edits belong to", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "edits": { @@ -16289,35 +16401,49 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "height": { "description": "Face bounding box height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "personId": { "description": "Person ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "width": { "description": "Face bounding box width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "x": { "description": "Face bounding box X coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "y": { "description": "Face bounding box Y coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -16349,31 +16475,44 @@ "properties": { "boundingBoxX1": { "description": "Bounding box X1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { "description": "Bounding box X2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { "description": "Bounding box Y1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { "description": "Bounding box Y2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "person": { @@ -16382,16 +16521,10 @@ "$ref": "#/components/schemas/PersonResponseDto" } ], - "description": "Person associated with face", "nullable": true }, "sourceType": { - "allOf": [ - { - "$ref": "#/components/schemas/SourceType" - } - ], - "description": "Face detection source type" + "$ref": "#/components/schemas/SourceType" } }, "required": [ @@ -16426,11 +16559,13 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "personId": { "description": "Person ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -16441,43 +16576,52 @@ "type": "object" }, "AssetFaceWithoutPersonResponseDto": { + "description": "Asset face without person", "properties": { "boundingBoxX1": { "description": "Bounding box X1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { "description": "Bounding box X2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { "description": "Bounding box Y1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { "description": "Bounding box Y2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "sourceType": { - "allOf": [ - { - "$ref": "#/components/schemas/SourceType" - } - ], - "description": "Face detection source type" + "$ref": "#/components/schemas/SourceType" } }, "required": [ @@ -16496,21 +16640,26 @@ "lastId": { "description": "Last asset ID (pagination)", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "limit": { "description": "Maximum number of assets to return", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, "updatedUntil": { "description": "Sync assets updated until this date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "userId": { "description": "Filter by user ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -16520,12 +16669,22 @@ ], "type": "object" }, + "AssetIdErrorReason": { + "description": "Error reason if failed", + "enum": [ + "duplicate", + "no_permission", + "not_found" + ], + "type": "string" + }, "AssetIdsDto": { "properties": { "assetIds": { "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -16543,13 +16702,7 @@ "type": "string" }, "error": { - "description": "Error reason if failed", - "enum": [ - "duplicate", - "no_permission", - "not_found" - ], - "type": "string" + "$ref": "#/components/schemas/AssetIdErrorReason" }, "success": { "description": "Whether operation succeeded", @@ -16578,17 +16731,13 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" }, "name": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetJobName" - } - ], - "description": "Job name" + "$ref": "#/components/schemas/AssetJobName" } }, "required": [ @@ -16618,12 +16767,16 @@ }, "fileCreatedAt": { "description": "File creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "fileModifiedAt": { "description": "File modification date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "filename": { @@ -16637,6 +16790,7 @@ "livePhotoVideoId": { "description": "Live photo video ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "metadata": { @@ -16652,12 +16806,7 @@ "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "required": [ @@ -16690,12 +16839,16 @@ }, "fileCreatedAt": { "description": "File creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "fileModifiedAt": { "description": "File modification date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "filename": { @@ -16719,12 +16872,7 @@ "type": "string" }, "status": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetMediaStatus" - } - ], - "description": "Upload status" + "$ref": "#/components/schemas/AssetMediaStatus" } }, "required": [ @@ -16734,6 +16882,7 @@ "type": "object" }, "AssetMediaSize": { + "description": "Asset media size", "enum": [ "original", "fullsize", @@ -16771,6 +16920,7 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "key": { @@ -16796,10 +16946,13 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16832,6 +16985,7 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "key": { @@ -16839,6 +16993,7 @@ "type": "string" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16858,10 +17013,13 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16895,6 +17053,7 @@ "type": "string" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16909,6 +17068,7 @@ "properties": { "assetId": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "boxScore": { @@ -16918,6 +17078,7 @@ }, "id": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "text": { @@ -16995,6 +17156,14 @@ ], "type": "string" }, + "AssetRejectReason": { + "description": "Rejection reason if rejected", + "enum": [ + "duplicate", + "unsupported-format" + ], + "type": "string" + }, "AssetResponseDto": { "properties": { "checksum": { @@ -17003,7 +17172,6 @@ }, "createdAt": { "description": "The UTC timestamp when the asset was originally uploaded to Immich.", - "example": "2024-01-15T20:30:00.000Z", "format": "date-time", "type": "string" }, @@ -17029,13 +17197,11 @@ }, "fileCreatedAt": { "description": "The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.", - "example": "2024-01-15T19:30:00.000Z", "format": "date-time", "type": "string" }, "fileModifiedAt": { "description": "The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.", - "example": "2024-01-16T10:15:00.000Z", "format": "date-time", "type": "string" }, @@ -17045,6 +17211,7 @@ }, "height": { "description": "Asset height", + "minimum": 0, "nullable": true, "type": "number" }, @@ -17084,10 +17251,10 @@ "type": "boolean" }, "libraryId": { - "deprecated": true, "description": "Library ID", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string", "x-immich-history": [ { @@ -17108,7 +17275,6 @@ }, "localDateTime": { "description": "The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by \"local\" days and months.", - "example": "2024-01-15T14:30:00.000Z", "format": "date-time", "type": "string" }, @@ -17138,7 +17304,6 @@ "type": "array" }, "resized": { - "deprecated": true, "description": "Is resized", "type": "boolean", "x-immich-history": [ @@ -17173,12 +17338,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type" + "$ref": "#/components/schemas/AssetTypeEnum" }, "unassignedFaces": { "items": { @@ -17188,20 +17348,15 @@ }, "updatedAt": { "description": "The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.", - "example": "2024-01-16T12:45:30.000Z", "format": "date-time", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "width": { "description": "Asset width", + "minimum": 0, "nullable": true, "type": "number" } @@ -17238,6 +17393,8 @@ "properties": { "assetCount": { "description": "Number of assets in stack", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "id": { @@ -17260,14 +17417,20 @@ "properties": { "images": { "description": "Number of images", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "total": { "description": "Total number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "videos": { "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -17288,6 +17451,14 @@ ], "type": "string" }, + "AssetUploadAction": { + "description": "Upload action", + "enum": [ + "accept", + "reject" + ], + "type": "string" + }, "AssetVisibility": { "description": "Asset visibility", "enum": [ @@ -17342,12 +17513,7 @@ "AvatarUpdate": { "properties": { "color": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" } }, "type": "object" @@ -17366,15 +17532,7 @@ "BulkIdResponseDto": { "properties": { "error": { - "description": "Error reason if failed", - "enum": [ - "duplicate", - "no_permission", - "not_found", - "unknown", - "validation" - ], - "type": "string" + "$ref": "#/components/schemas/BulkIdErrorReason" }, "errorMessage": { "type": "string" @@ -17400,6 +17558,7 @@ "description": "IDs to process", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17439,7 +17598,6 @@ "CastResponse": { "properties": { "gCastEnabled": { - "default": false, "description": "Whether Google Cast is enabled", "type": "boolean" } @@ -17531,6 +17689,8 @@ "properties": { "assetCount": { "description": "Number of assets contributed", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "userId": { @@ -17561,6 +17721,7 @@ "description": "Initial asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17583,8 +17744,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "importPaths": { "description": "Import paths (max 128)", @@ -17592,16 +17752,17 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "name": { "description": "Library name", + "minLength": 1, "type": "string" }, "ownerId": { "description": "Owner user ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -17627,7 +17788,9 @@ "properties": { "profileChangedAt": { "description": "Profile image change date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "profileImagePath": { @@ -17681,6 +17844,7 @@ "properties": { "cronExpression": { "description": "Cron expression", + "pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}", "type": "string" }, "enabled": { @@ -17703,6 +17867,7 @@ "DatabaseBackupDeleteDto": { "properties": { "backups": { + "description": "Backup filenames to delete", "items": { "type": "string" }, @@ -17717,12 +17882,15 @@ "DatabaseBackupDto": { "properties": { "filename": { + "description": "Backup filename", "type": "string" }, "filesize": { + "description": "Backup file size", "type": "number" }, "timezone": { + "description": "Backup timezone", "type": "string" } }, @@ -17736,6 +17904,7 @@ "DatabaseBackupListResponseDto": { "properties": { "backups": { + "description": "List of backups", "items": { "$ref": "#/components/schemas/DatabaseBackupDto" }, @@ -17750,6 +17919,7 @@ "DatabaseBackupUploadDto": { "properties": { "file": { + "description": "Database backup file", "format": "binary", "type": "string" } @@ -17762,6 +17932,7 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17787,6 +17958,8 @@ }, "size": { "description": "Archive size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -17801,10 +17974,12 @@ "albumId": { "description": "Album ID to download", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "archiveSize": { "description": "Archive size limit in bytes", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -17812,6 +17987,7 @@ "description": "Asset IDs to download", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17819,6 +17995,7 @@ "userId": { "description": "User ID to download assets from", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -17828,10 +18005,11 @@ "properties": { "archiveSize": { "description": "Maximum archive size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "includeEmbeddedVideos": { - "default": false, "description": "Whether to include embedded videos in downloads", "type": "boolean" } @@ -17853,6 +18031,8 @@ }, "totalSize": { "description": "Total size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -17866,6 +18046,7 @@ "properties": { "archiveSize": { "description": "Maximum archive size in bytes", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -17916,12 +18097,14 @@ "properties": { "duplicateId": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "keepAssetIds": { "description": "Asset IDs to keep", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17930,6 +18113,7 @@ "description": "Asset IDs to trash or delete", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17959,6 +18143,7 @@ "description": "Suggested asset IDs to keep based on file size and EXIF data", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -18011,6 +18196,7 @@ "type": "object" }, "ExifResponseDto": { + "description": "EXIF response", "properties": { "city": { "default": null, @@ -18040,12 +18226,14 @@ "exifImageHeight": { "default": null, "description": "Image height in pixels", + "minimum": 0, "nullable": true, "type": "number" }, "exifImageWidth": { "default": null, "description": "Image width in pixels", + "minimum": 0, "nullable": true, "type": "number" }, @@ -18064,7 +18252,8 @@ "fileSizeInByte": { "default": null, "description": "File size in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, @@ -18155,6 +18344,7 @@ "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -18178,6 +18368,7 @@ }, "minFaces": { "description": "Minimum number of faces required for recognition", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -18205,12 +18396,10 @@ "FoldersResponse": { "properties": { "enabled": { - "default": false, "description": "Whether folders are enabled", "type": "boolean" }, "sidebarWeb": { - "default": false, "description": "Whether folders appear in web sidebar", "type": "boolean" } @@ -18245,12 +18434,7 @@ "JobCreateDto": { "properties": { "name": { - "allOf": [ - { - "$ref": "#/components/schemas/ManualJobName" - } - ], - "description": "Job name" + "$ref": "#/components/schemas/ManualJobName" } }, "required": [ @@ -18324,6 +18508,7 @@ "properties": { "concurrency": { "description": "Concurrency", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" } @@ -18337,11 +18522,15 @@ "properties": { "assetCount": { "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "exclusionPatterns": { @@ -18372,13 +18561,17 @@ }, "refreshedAt": { "description": "Last refresh date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -18398,24 +18591,27 @@ "LibraryStatsResponseDto": { "properties": { "photos": { - "default": 0, "description": "Number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "total": { - "default": 0, "description": "Total number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usage": { - "default": 0, "description": "Storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "videos": { - "default": 0, "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -18434,8 +18630,8 @@ "type": "string" }, "licenseKey": { - "description": "License key (format: IM(SV|CL)(-XXXX){8})", - "pattern": "/IM(SV|CL)(-[\\dA-Za-z]{4}){8}/", + "description": "License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/)", + "pattern": "^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$", "type": "string" } }, @@ -18446,30 +18642,10 @@ "type": "object" }, "LicenseResponseDto": { - "properties": { - "activatedAt": { - "description": "Activation date", - "format": "date-time", - "type": "string" - }, - "activationKey": { - "description": "Activation key", - "type": "string" - }, - "licenseKey": { - "description": "License key (format: IM(SV|CL)(-XXXX){8})", - "pattern": "/IM(SV|CL)(-[\\dA-Za-z]{4}){8}/", - "type": "string" - } - }, - "required": [ - "activatedAt", - "activationKey", - "licenseKey" - ], - "type": "object" + "$ref": "#/components/schemas/UserLicense" }, "LogLevel": { + "description": "Log level", "enum": [ "verbose", "debug", @@ -18486,6 +18662,7 @@ "description": "User email", "example": "testuser@email.com", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "password": { @@ -18528,6 +18705,8 @@ }, "userEmail": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "userId": { @@ -18627,12 +18806,7 @@ "type": "number" }, "folder": { - "allOf": [ - { - "$ref": "#/components/schemas/StorageFolder" - } - ], - "description": "Storage folder" + "$ref": "#/components/schemas/StorageFolder" }, "readable": { "description": "Whether the folder is readable", @@ -18663,12 +18837,7 @@ "MaintenanceStatusResponseDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/MaintenanceAction" - } - ], - "description": "Maintenance action" + "$ref": "#/components/schemas/MaintenanceAction" }, "active": { "type": "boolean" @@ -18690,7 +18859,7 @@ "type": "object" }, "ManualJobName": { - "description": "Job name", + "description": "Manual job name", "enum": [ "person-cleanup", "tag-cleanup", @@ -18771,12 +18940,12 @@ "MemoriesResponse": { "properties": { "duration": { - "default": 5, "description": "Memory duration in seconds", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "enabled": { - "default": true, "description": "Whether memories are enabled", "type": "boolean" } @@ -18791,6 +18960,7 @@ "properties": { "duration": { "description": "Memory duration in seconds", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -18807,6 +18977,7 @@ "description": "Asset IDs to associate with memory", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -18816,7 +18987,9 @@ }, "hideAt": { "description": "Date when memory should be hidden", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string", "x-immich-history": [ { @@ -18836,17 +19009,23 @@ }, "memoryAt": { "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "seenAt": { "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "showAt": { "description": "Date when memory should be shown", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string", "x-immich-history": [ { @@ -18861,12 +19040,7 @@ "x-immich-state": "Stable" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } - ], - "description": "Memory type" + "$ref": "#/components/schemas/MemoryType" } }, "required": [ @@ -18886,7 +19060,9 @@ }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "data": { @@ -18894,12 +19070,16 @@ }, "deletedAt": { "description": "Deletion date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "hideAt": { "description": "Date when memory should be hidden", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -18912,7 +19092,9 @@ }, "memoryAt": { "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "ownerId": { @@ -18921,25 +19103,26 @@ }, "seenAt": { "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "showAt": { "description": "Date when memory should be shown", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } - ], - "description": "Memory type" + "$ref": "#/components/schemas/MemoryType" }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -18957,6 +19140,7 @@ "type": "object" }, "MemorySearchOrder": { + "description": "Sort order", "enum": [ "asc", "desc", @@ -18968,6 +19152,8 @@ "properties": { "total": { "description": "Total number of memories", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -18977,6 +19163,7 @@ "type": "object" }, "MemoryType": { + "description": "Memory type", "enum": [ "on_this_day" ], @@ -18990,12 +19177,16 @@ }, "memoryAt": { "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "seenAt": { "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -19007,6 +19198,7 @@ "description": "Person IDs to merge", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -19023,6 +19215,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -19043,12 +19236,16 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "description": { @@ -19070,6 +19267,7 @@ "id": { "description": "Filter by asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "isEncoded": { @@ -19101,10 +19299,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -19117,11 +19317,7 @@ "type": "string" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], + "$ref": "#/components/schemas/AssetOrder", "default": "desc", "description": "Sort order" }, @@ -19142,6 +19338,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -19188,6 +19385,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -19195,12 +19393,16 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "thumbnailPath": { @@ -19209,39 +19411,37 @@ }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "withDeleted": { "description": "Include deleted assets", @@ -19273,12 +19473,7 @@ "MirrorParameters": { "properties": { "axis": { - "allOf": [ - { - "$ref": "#/components/schemas/MirrorAxis" - } - ], - "description": "Axis to mirror along" + "$ref": "#/components/schemas/MirrorAxis" } }, "required": [ @@ -19289,6 +19484,7 @@ "NotificationCreateDto": { "properties": { "data": { + "additionalProperties": {}, "description": "Additional notification data", "type": "object" }, @@ -19298,17 +19494,14 @@ "type": "string" }, "level": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationLevel" - } - ], - "description": "Notification level" + "$ref": "#/components/schemas/NotificationLevel" }, "readAt": { "description": "Date when notification was read", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "title": { @@ -19316,16 +19509,12 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationType" - } - ], - "description": "Notification type" + "$ref": "#/components/schemas/NotificationType" }, "userId": { "description": "User ID to send notification to", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -19341,6 +19530,7 @@ "description": "Notification IDs to delete", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "minItems": 1, @@ -19356,10 +19546,13 @@ "properties": { "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "data": { + "additionalProperties": {}, "description": "Additional notification data", "type": "object" }, @@ -19372,16 +19565,13 @@ "type": "string" }, "level": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationLevel" - } - ], - "description": "Notification level" + "$ref": "#/components/schemas/NotificationLevel" }, "readAt": { "description": "Date when notification was read", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "title": { @@ -19389,12 +19579,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationType" - } - ], - "description": "Notification type" + "$ref": "#/components/schemas/NotificationType" } }, "required": [ @@ -19407,6 +19592,7 @@ "type": "object" }, "NotificationLevel": { + "description": "Notification level", "enum": [ "success", "error", @@ -19416,6 +19602,7 @@ "type": "string" }, "NotificationType": { + "description": "Notification type", "enum": [ "JobFailed", "BackupFailed", @@ -19432,6 +19619,7 @@ "description": "Notification IDs to update", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "minItems": 1, @@ -19439,8 +19627,10 @@ }, "readAt": { "description": "Date when notifications were read", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -19453,8 +19643,10 @@ "properties": { "readAt": { "description": "Date when notification was read", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -19484,6 +19676,7 @@ }, "url": { "description": "OAuth callback URL", + "minLength": 1, "type": "string" } }, @@ -19513,7 +19706,7 @@ "type": "object" }, "OAuthTokenEndpointAuthMethod": { - "description": "Token endpoint auth method", + "description": "OAuth token endpoint auth method", "enum": [ "client_secret_post", "client_secret_basic" @@ -19528,6 +19721,7 @@ }, "maxResolution": { "description": "Maximum resolution for OCR processing", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -19563,8 +19757,9 @@ "properties": { "year": { "description": "Year for on this day memory", - "minimum": 1, - "type": "number" + "maximum": 9999, + "minimum": 1000, + "type": "integer" } }, "required": [ @@ -19601,6 +19796,7 @@ "sharedWithId": { "description": "User ID to share with", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -19610,6 +19806,7 @@ "type": "object" }, "PartnerDirection": { + "description": "Partner direction", "enum": [ "shared-by", "shared-with" @@ -19617,21 +19814,21 @@ "type": "string" }, "PartnerResponseDto": { + "description": "Partner response", "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" }, "email": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "inTimeline": { @@ -19677,12 +19874,10 @@ "PeopleResponse": { "properties": { "enabled": { - "default": true, "description": "Whether people are enabled", "type": "boolean" }, "sidebarWeb": { - "default": false, "description": "Whether people appear in web sidebar", "type": "boolean" } @@ -19694,6 +19889,7 @@ "type": "object" }, "PeopleResponseDto": { + "description": "People response", "properties": { "hasNextPage": { "description": "Whether there are more pages", @@ -19712,10 +19908,11 @@ }, "hidden": { "description": "Number of hidden people", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "people": { - "description": "List of people", "items": { "$ref": "#/components/schemas/PersonResponseDto" }, @@ -19723,6 +19920,8 @@ }, "total": { "description": "Total number of people", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -19772,11 +19971,13 @@ "color": { "description": "Person color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "featureFaceAssetId": { "description": "Asset ID used for feature face thumbnail", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "id": { @@ -19974,6 +20175,7 @@ "color": { "description": "Person color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "isFavorite": { @@ -20075,6 +20277,8 @@ "properties": { "assets": { "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -20094,11 +20298,13 @@ "color": { "description": "Person color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "featureFaceAssetId": { "description": "Asset ID used for feature face thumbnail", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "isFavorite": { @@ -20140,7 +20346,6 @@ "x-immich-state": "Stable" }, "faces": { - "description": "Face detections", "items": { "$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto" }, @@ -20208,16 +20413,18 @@ "properties": { "newPinCode": { "description": "New PIN code (4-6 digits)", - "example": "123456", + "pattern": "^\\d{6}$", "type": "string" }, "password": { "description": "User password (required if PIN code is not provided)", + "example": "password", "type": "string" }, "pinCode": { "description": "New PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -20230,11 +20437,13 @@ "properties": { "password": { "description": "User password (required if PIN code is not provided)", + "example": "password", "type": "string" }, "pinCode": { "description": "New PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -20245,6 +20454,7 @@ "pinCode": { "description": "PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -20302,9 +20512,13 @@ "type": "string" }, "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginJsonSchema" + } + ], "description": "Action schema", - "nullable": true, - "type": "object" + "nullable": true }, "supportedContexts": { "description": "Supported contexts", @@ -20329,8 +20543,9 @@ ], "type": "object" }, + "PluginConfigValue": {}, "PluginContextType": { - "description": "Context type", + "description": "Plugin context", "enum": [ "asset", "album", @@ -20357,9 +20572,13 @@ "type": "string" }, "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginJsonSchema" + } + ], "description": "Filter schema", - "nullable": true, - "type": "object" + "nullable": true }, "supportedContexts": { "description": "Supported contexts", @@ -20384,6 +20603,87 @@ ], "type": "object" }, + "PluginJsonSchema": { + "properties": { + "additionalProperties": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "properties": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/PluginJsonSchemaType" + } + }, + "type": "object" + }, + "PluginJsonSchemaProperty": { + "properties": { + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + } + ] + }, + "default": {}, + "description": { + "type": "string" + }, + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "items": { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + }, + "properties": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/PluginJsonSchemaType" + } + }, + "type": "object" + }, + "PluginJsonSchemaType": { + "enum": [ + "string", + "number", + "integer", + "boolean", + "object", + "array", + "null" + ], + "type": "string" + }, "PluginResponseDto": { "properties": { "actions": { @@ -20450,20 +20750,10 @@ "PluginTriggerResponseDto": { "properties": { "contextType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginContextType" - } - ], - "description": "Context type" + "$ref": "#/components/schemas/PluginContextType" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "required": [ @@ -20473,7 +20763,7 @@ "type": "object" }, "PluginTriggerType": { - "description": "Trigger type", + "description": "Plugin trigger type", "enum": [ "AssetCreate", "PersonRecognized" @@ -20524,12 +20814,7 @@ "QueueCommandDto": { "properties": { "command": { - "allOf": [ - { - "$ref": "#/components/schemas/QueueCommand" - } - ], - "description": "Queue command to execute" + "$ref": "#/components/schemas/QueueCommand" }, "force": { "description": "Force the command execution (if applicable)", @@ -20564,6 +20849,7 @@ "QueueJobResponseDto": { "properties": { "data": { + "additionalProperties": {}, "description": "Job data payload", "type": "object" }, @@ -20572,15 +20858,12 @@ "type": "string" }, "name": { - "allOf": [ - { - "$ref": "#/components/schemas/JobName" - } - ], - "description": "Job name" + "$ref": "#/components/schemas/JobName" }, "timestamp": { "description": "Job creation timestamp", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -20592,6 +20875,7 @@ "type": "object" }, "QueueJobStatus": { + "description": "Queue job status", "enum": [ "active", "failed", @@ -20603,6 +20887,7 @@ "type": "string" }, "QueueName": { + "description": "Queue name", "enum": [ "thumbnailGeneration", "metadataExtraction", @@ -20632,12 +20917,7 @@ "type": "boolean" }, "name": { - "allOf": [ - { - "$ref": "#/components/schemas/QueueName" - } - ], - "description": "Queue name" + "$ref": "#/components/schemas/QueueName" }, "statistics": { "$ref": "#/components/schemas/QueueStatisticsDto" @@ -20669,26 +20949,38 @@ "properties": { "active": { "description": "Number of active jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "completed": { "description": "Number of completed jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "delayed": { "description": "Number of delayed jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "failed": { "description": "Number of failed jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "paused": { "description": "Number of paused jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "waiting": { "description": "Number of waiting jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -20813,6 +21105,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -20829,12 +21122,16 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "deviceId": { @@ -20870,10 +21167,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -20889,6 +21188,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -20931,6 +21231,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -20938,49 +21239,51 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "withDeleted": { "description": "Include deleted assets", @@ -21004,7 +21307,6 @@ "RatingsResponse": { "properties": { "enabled": { - "default": false, "description": "Whether ratings are enabled", "type": "boolean" } @@ -21024,6 +21326,7 @@ "type": "object" }, "ReactionLevel": { + "description": "Reaction level", "enum": [ "album", "asset" @@ -21031,6 +21334,7 @@ "type": "string" }, "ReactionType": { + "description": "Reaction type", "enum": [ "comment", "like" @@ -21072,6 +21376,8 @@ "properties": { "count": { "description": "Number of albums in this page", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "facets": { @@ -21088,6 +21394,8 @@ }, "total": { "description": "Total number of matching albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -21103,6 +21411,8 @@ "properties": { "count": { "description": "Number of assets in this page", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "facets": { @@ -21124,6 +21434,8 @@ }, "total": { "description": "Total number of matching assets", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -21175,6 +21487,8 @@ "properties": { "count": { "description": "Number of assets with this facet value", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "value": { @@ -21191,7 +21505,6 @@ "SearchFacetResponseDto": { "properties": { "counts": { - "description": "Facet counts", "items": { "$ref": "#/components/schemas/SearchFacetCountResponseDto" }, @@ -21227,6 +21540,8 @@ "properties": { "total": { "description": "Total number of matching assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21236,6 +21551,7 @@ "type": "object" }, "SearchSuggestionType": { + "description": "Suggestion type", "enum": [ "country", "state", @@ -21407,10 +21723,14 @@ }, "trashDays": { "description": "Number of days before trashed assets are permanently deleted", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "userDeleteDelay": { "description": "Delay in days before deleted users are permanently removed", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21546,7 +21866,6 @@ "properties": { "res": { "example": "pong", - "readOnly": true, "type": "string" } }, @@ -21558,48 +21877,40 @@ "ServerStatsResponseDto": { "properties": { "photos": { - "default": 0, "description": "Total number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usage": { - "default": 0, "description": "Total storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usageByUser": { - "default": [], - "example": [ - { - "photos": 1, - "videos": 1, - "diskUsageRaw": 2, - "usagePhotos": 1, - "usageVideos": 1 - } - ], + "description": "Array of usage for each user", "items": { "$ref": "#/components/schemas/UsageByUserDto" }, - "title": "Array of usage for each user", "type": "array" }, "usagePhotos": { - "default": 0, "description": "Storage usage for photos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usageVideos": { - "default": 0, "description": "Storage usage for videos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "videos": { - "default": 0, "description": "Total number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21621,7 +21932,8 @@ }, "diskAvailableRaw": { "description": "Available disk space in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "diskSize": { @@ -21630,7 +21942,8 @@ }, "diskSizeRaw": { "description": "Total disk size in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "diskUsagePercentage": { @@ -21644,7 +21957,8 @@ }, "diskUseRaw": { "description": "Used disk space in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21675,7 +21989,9 @@ "properties": { "createdAt": { "description": "When this version was first seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -21698,14 +22014,20 @@ "properties": { "major": { "description": "Major version number", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "minor": { "description": "Minor version number", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "patch": { "description": "Patch version number", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21847,11 +22169,13 @@ "properties": { "password": { "description": "User password (required if PIN code is not provided)", + "example": "password", "type": "string" }, "pinCode": { "description": "New PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -21869,12 +22193,7 @@ "SetMaintenanceModeDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/MaintenanceAction" - } - ], - "description": "Maintenance action" + "$ref": "#/components/schemas/MaintenanceAction" }, "restoreBackupFilename": { "description": "Restore backup filename", @@ -21891,6 +22210,7 @@ "albumId": { "description": "Album ID (for album sharing)", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "allowDownload": { @@ -21906,6 +22226,7 @@ "description": "Asset IDs (for individual assets)", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -21918,8 +22239,10 @@ "expiresAt": { "default": null, "description": "Expiration date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "password": { @@ -21938,12 +22261,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/SharedLinkType" - } - ], - "description": "Shared link type" + "$ref": "#/components/schemas/SharedLinkType" } }, "required": [ @@ -21972,8 +22290,10 @@ }, "expiresAt": { "description": "Expiration date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "password": { @@ -22007,6 +22327,7 @@ "type": "object" }, "SharedLinkResponseDto": { + "description": "Shared link response", "properties": { "album": { "$ref": "#/components/schemas/AlbumResponseDto" @@ -22027,7 +22348,9 @@ }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "description": { @@ -22037,8 +22360,10 @@ }, "expiresAt": { "description": "Expiration date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -22085,12 +22410,7 @@ "x-immich-state": "Deprecated" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/SharedLinkType" - } - ], - "description": "Shared link type" + "$ref": "#/components/schemas/SharedLinkType" }, "userId": { "description": "Owner user ID", @@ -22125,12 +22445,10 @@ "SharedLinksResponse": { "properties": { "enabled": { - "default": true, "description": "Whether shared links are enabled", "type": "boolean" }, "sidebarWeb": { - "default": false, "description": "Whether shared links appear in web sidebar", "type": "boolean" } @@ -22160,6 +22478,7 @@ "description": "User email", "example": "testuser@email.com", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "name": { @@ -22186,6 +22505,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22202,12 +22522,16 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "deviceId": { @@ -22247,10 +22571,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -22271,6 +22597,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22282,6 +22609,7 @@ "queryAssetId": { "description": "Asset ID to use as search reference", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "rating": { @@ -22322,6 +22650,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -22329,49 +22658,51 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "withDeleted": { "description": "Include deleted assets", @@ -22399,6 +22730,7 @@ "description": "Asset IDs (first becomes primary, min 2)", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "minItems": 2, @@ -22411,9 +22743,9 @@ "type": "object" }, "StackResponseDto": { + "description": "Stack response", "properties": { "assets": { - "description": "Stack assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, @@ -22440,6 +22772,7 @@ "primaryAssetId": { "description": "Primary asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -22451,6 +22784,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22467,12 +22801,16 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "description": { @@ -22512,10 +22850,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -22531,6 +22871,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22567,6 +22908,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -22574,49 +22916,51 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "type": "object" @@ -22652,12 +22996,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/SyncEntityType" - } - ], - "description": "Sync entity type" + "$ref": "#/components/schemas/SyncEntityType" } }, "required": [ @@ -22756,12 +23095,7 @@ "type": "string" }, "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" }, "userId": { "description": "User ID", @@ -22779,7 +23113,9 @@ "properties": { "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "description": { @@ -22799,11 +23135,7 @@ "type": "string" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ] + "$ref": "#/components/schemas/AssetOrder" }, "ownerId": { "description": "Owner ID", @@ -22816,7 +23148,9 @@ }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -22848,6 +23182,7 @@ "SyncAssetEditDeleteV1": { "properties": { "editId": { + "description": "Edit ID", "type": "string" } }, @@ -22859,22 +23194,25 @@ "SyncAssetEditV1": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ] + "$ref": "#/components/schemas/AssetEditAction" }, "assetId": { + "description": "Asset ID", "type": "string" }, "id": { + "description": "Edit ID", "type": "string" }, "parameters": { + "additionalProperties": {}, + "description": "Edit parameters", "type": "object" }, "sequence": { + "description": "Edit sequence", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -22905,8 +23243,10 @@ }, "dateTimeOriginal": { "description": "Date time original", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "description": { @@ -22916,11 +23256,15 @@ }, "exifImageHeight": { "description": "Exif image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, "exifImageWidth": { "description": "Exif image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -22937,6 +23281,8 @@ }, "fileSizeInByte": { "description": "File size in byte", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -22954,6 +23300,8 @@ }, "iso": { "description": "ISO", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -22986,8 +23334,10 @@ }, "modifyDate": { "description": "Modify date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "orientation": { @@ -23007,6 +23357,8 @@ }, "rating": { "description": "Rating", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -23069,15 +23421,27 @@ "type": "string" }, "boundingBoxX1": { + "description": "Bounding box X1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { + "description": "Bounding box X2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { + "description": "Bounding box Y1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { + "description": "Bounding box Y2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { @@ -23085,9 +23449,15 @@ "type": "string" }, "imageHeight": { + "description": "Image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageWidth": { + "description": "Image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "personId": { @@ -23121,21 +23491,35 @@ "type": "string" }, "boundingBoxX1": { + "description": "Bounding box X1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { + "description": "Bounding box X2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { + "description": "Bounding box Y1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { + "description": "Bounding box Y2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "deletedAt": { "description": "Face deleted at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -23143,9 +23527,15 @@ "type": "string" }, "imageHeight": { + "description": "Image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageWidth": { + "description": "Image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "isVisible": { @@ -23206,6 +23596,7 @@ "type": "string" }, "value": { + "additionalProperties": {}, "description": "Value", "type": "object" } @@ -23225,8 +23616,10 @@ }, "deletedAt": { "description": "Deleted at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "duration": { @@ -23236,18 +23629,24 @@ }, "fileCreatedAt": { "description": "File created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "fileModifiedAt": { "description": "File modified at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "height": { "description": "Asset height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -23275,8 +23674,10 @@ }, "localDateTime": { "description": "Local date time", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "originalFileName": { @@ -23298,23 +23699,15 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type" + "$ref": "#/components/schemas/AssetTypeEnum" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "width": { "description": "Asset width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" } @@ -23350,13 +23743,14 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "User avatar color", "nullable": true }, "deletedAt": { "description": "User deleted at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "email": { @@ -23390,14 +23784,22 @@ }, "profileChangedAt": { "description": "User profile changed at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "quotaSizeInBytes": { + "description": "Quota size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, "quotaUsageInBytes": { + "description": "Quota usage in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "storageLabel": { @@ -23407,7 +23809,6 @@ } }, "required": [ - "avatarColor", "deletedAt", "email", "hasProfileImage", @@ -23533,23 +23934,30 @@ "properties": { "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "data": { + "additionalProperties": {}, "description": "Data", "type": "object" }, "deletedAt": { "description": "Deleted at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "hideAt": { "description": "Hide at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -23562,7 +23970,9 @@ }, "memoryAt": { "description": "Memory at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "ownerId": { @@ -23571,27 +23981,28 @@ }, "seenAt": { "description": "Seen at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "showAt": { "description": "Show at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } - ], - "description": "Memory type" + "$ref": "#/components/schemas/MemoryType" }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -23666,8 +24077,10 @@ "properties": { "birthDate": { "description": "Birth date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "color": { @@ -23677,7 +24090,9 @@ }, "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "faceAssetId": { @@ -23707,7 +24122,9 @@ }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -23726,7 +24143,7 @@ "type": "object" }, "SyncRequestType": { - "description": "Sync request types", + "description": "Sync request type", "enum": [ "AlbumsV1", "AlbumUsersV1", @@ -23773,7 +24190,9 @@ "properties": { "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -23790,7 +24209,9 @@ }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -23837,12 +24258,7 @@ "SyncUserMetadataDeleteV1": { "properties": { "key": { - "allOf": [ - { - "$ref": "#/components/schemas/UserMetadataKey" - } - ], - "description": "User metadata key" + "$ref": "#/components/schemas/UserMetadataKey" }, "userId": { "description": "User ID", @@ -23858,18 +24274,14 @@ "SyncUserMetadataV1": { "properties": { "key": { - "allOf": [ - { - "$ref": "#/components/schemas/UserMetadataKey" - } - ], - "description": "User metadata key" + "$ref": "#/components/schemas/UserMetadataKey" }, "userId": { "description": "User ID", "type": "string" }, "value": { + "additionalProperties": {}, "description": "User metadata value", "type": "object" } @@ -23889,13 +24301,14 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "User avatar color", "nullable": true }, "deletedAt": { "description": "User deleted at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "email": { @@ -23916,12 +24329,13 @@ }, "profileChangedAt": { "description": "User profile changed at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, "required": [ - "avatarColor", "deletedAt", "email", "hasProfileImage", @@ -23943,6 +24357,7 @@ "type": "object" }, "SystemConfigDto": { + "description": "System configuration", "properties": { "backup": { "$ref": "#/components/schemas/SystemConfigBackupsDto" @@ -24036,12 +24451,7 @@ "SystemConfigFFmpegDto": { "properties": { "accel": { - "allOf": [ - { - "$ref": "#/components/schemas/TranscodeHWAccel" - } - ], - "description": "Transcode hardware acceleration" + "$ref": "#/components/schemas/TranscodeHWAccel" }, "accelDecode": { "description": "Accelerated decode", @@ -24075,12 +24485,7 @@ "type": "integer" }, "cqMode": { - "allOf": [ - { - "$ref": "#/components/schemas/CQMode" - } - ], - "description": "CQ mode" + "$ref": "#/components/schemas/CQMode" }, "crf": { "description": "CRF", @@ -24090,6 +24495,7 @@ }, "gopSize": { "description": "GOP size", + "maximum": 9007199254740991, "minimum": 0, "type": "integer" }, @@ -24112,24 +24518,14 @@ "type": "integer" }, "targetAudioCodec": { - "allOf": [ - { - "$ref": "#/components/schemas/AudioCodec" - } - ], - "description": "Target audio codec" + "$ref": "#/components/schemas/AudioCodec" }, "targetResolution": { "description": "Target resolution", "type": "string" }, "targetVideoCodec": { - "allOf": [ - { - "$ref": "#/components/schemas/VideoCodec" - } - ], - "description": "Target video codec" + "$ref": "#/components/schemas/VideoCodec" }, "temporalAQ": { "description": "Temporal AQ", @@ -24137,24 +24533,15 @@ }, "threads": { "description": "Threads", + "maximum": 9007199254740991, "minimum": 0, "type": "integer" }, "tonemap": { - "allOf": [ - { - "$ref": "#/components/schemas/ToneMapping" - } - ], - "description": "Tone mapping" + "$ref": "#/components/schemas/ToneMapping" }, "transcode": { - "allOf": [ - { - "$ref": "#/components/schemas/TranscodePolicy" - } - ], - "description": "Transcode policy" + "$ref": "#/components/schemas/TranscodePolicy" }, "twoPass": { "description": "Two pass", @@ -24205,15 +24592,9 @@ "type": "boolean" }, "format": { - "allOf": [ - { - "$ref": "#/components/schemas/ImageFormat" - } - ], - "description": "Image format" + "$ref": "#/components/schemas/ImageFormat" }, "progressive": { - "default": false, "description": "Progressive", "type": "boolean" }, @@ -24234,15 +24615,10 @@ "SystemConfigGeneratedImageDto": { "properties": { "format": { - "allOf": [ - { - "$ref": "#/components/schemas/ImageFormat" - } - ], - "description": "Image format" + "$ref": "#/components/schemas/ImageFormat" }, "progressive": { - "default": false, + "description": "Progressive", "type": "boolean" }, "quality": { @@ -24253,6 +24629,7 @@ }, "size": { "description": "Size", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" } @@ -24267,12 +24644,7 @@ "SystemConfigImageDto": { "properties": { "colorspace": { - "allOf": [ - { - "$ref": "#/components/schemas/Colorspace" - } - ], - "description": "Colorspace" + "$ref": "#/components/schemas/Colorspace" }, "extractEmbedded": { "description": "Extract embedded", @@ -24378,6 +24750,8 @@ "SystemConfigLibraryScanDto": { "properties": { "cronExpression": { + "description": "Cron expression", + "pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}", "type": "string" }, "enabled": { @@ -24410,11 +24784,7 @@ "type": "boolean" }, "level": { - "allOf": [ - { - "$ref": "#/components/schemas/LogLevel" - } - ] + "$ref": "#/components/schemas/LogLevel" } }, "required": [ @@ -24445,9 +24815,8 @@ "$ref": "#/components/schemas/OcrConfig" }, "urls": { - "format": "uri", + "description": "ML service URLs", "items": { - "format": "uri", "type": "string" }, "minItems": 1, @@ -24468,6 +24837,7 @@ "SystemConfigMapDto": { "properties": { "darkStyle": { + "description": "Dark map style URL", "format": "uri", "type": "string" }, @@ -24476,6 +24846,7 @@ "type": "boolean" }, "lightStyle": { + "description": "Light map style URL", "format": "uri", "type": "string" } @@ -24529,6 +24900,8 @@ "type": "boolean" }, "startTime": { + "description": "Start time", + "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$", "type": "string" }, "syncQuotaUsage": { @@ -24581,10 +24954,9 @@ }, "defaultStorageQuota": { "description": "Default storage quota", - "format": "int64", "minimum": 0, "nullable": true, - "type": "integer" + "type": "number" }, "enabled": { "description": "Enabled", @@ -24599,8 +24971,7 @@ "type": "boolean" }, "mobileRedirectUri": { - "description": "Mobile redirect URI", - "format": "uri", + "description": "Mobile redirect URI (set to empty string to disable)", "type": "string" }, "profileSigningAlgorithm": { @@ -24616,6 +24987,7 @@ "type": "string" }, "signingAlgorithm": { + "description": "Signing algorithm", "type": "string" }, "storageLabelClaim": { @@ -24628,16 +25000,12 @@ }, "timeout": { "description": "Timeout", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, "tokenEndpointAuthMethod": { - "allOf": [ - { - "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" - } - ], - "description": "Token endpoint auth method" + "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" } }, "required": [ @@ -24690,7 +25058,6 @@ "properties": { "externalDomain": { "description": "External domain", - "format": "uri", "type": "string" }, "loginPageMessage": { @@ -24799,12 +25166,15 @@ "SystemConfigTemplateEmailsDto": { "properties": { "albumInviteTemplate": { + "description": "Album invite template", "type": "string" }, "albumUpdateTemplate": { + "description": "Album update template", "type": "string" }, "welcomeTemplate": { + "description": "Welcome template", "type": "string" } }, @@ -24913,6 +25283,7 @@ "properties": { "days": { "description": "Days", + "maximum": 9007199254740991, "minimum": 0, "type": "integer" }, @@ -24931,6 +25302,7 @@ "properties": { "deleteDelay": { "description": "Delete delay", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" } @@ -24946,6 +25318,7 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -24954,6 +25327,7 @@ "description": "Tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -24969,6 +25343,8 @@ "properties": { "count": { "description": "Number of assets tagged", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -24981,7 +25357,8 @@ "properties": { "color": { "description": "Tag color (hex)", - "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$", + "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "name": { @@ -24992,6 +25369,7 @@ "description": "Parent tag ID", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -25047,6 +25425,7 @@ "color": { "description": "Tag color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" } }, @@ -25070,12 +25449,10 @@ "TagsResponse": { "properties": { "enabled": { - "default": true, "description": "Whether tags are enabled", "type": "boolean" }, "sidebarWeb": { - "default": true, "description": "Whether tags appear in web sidebar", "type": "boolean" } @@ -25307,6 +25684,8 @@ "count": { "description": "Number of assets in this time bucket", "example": 42, + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "timeBucket": { @@ -25357,6 +25736,8 @@ "properties": { "count": { "description": "Number of items in trash", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -25374,6 +25755,7 @@ "albumThumbnailAssetId": { "description": "Album thumbnail asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "description": { @@ -25385,12 +25767,7 @@ "type": "boolean" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "description": "Asset sort order" + "$ref": "#/components/schemas/AssetOrder" } }, "type": "object" @@ -25398,12 +25775,7 @@ "UpdateAlbumUserDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" } }, "required": [ @@ -25427,16 +25799,21 @@ }, "latitude": { "description": "Latitude coordinate", + "maximum": 90, + "minimum": -90, "type": "number" }, "livePhotoVideoId": { "description": "Live photo video ID", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "longitude": { "description": "Longitude coordinate", + "maximum": 180, + "minimum": -180, "type": "number" }, "rating": { @@ -25444,7 +25821,7 @@ "maximum": 5, "minimum": -1, "nullable": true, - "type": "number", + "type": "integer", "x-immich-history": [ { "version": "v1", @@ -25463,12 +25840,7 @@ "x-immich-state": "Stable" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "type": "object" @@ -25481,8 +25853,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "importPaths": { "description": "Import paths (max 128)", @@ -25490,11 +25861,11 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "name": { "description": "Library name", + "minLength": 1, "type": "string" } }, @@ -25504,27 +25875,33 @@ "properties": { "photos": { "description": "Number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "quotaSizeInBytes": { "description": "User quota size in bytes (null if unlimited)", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, "usage": { "description": "Total storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usagePhotos": { "description": "Storage usage for photos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usageVideos": { "description": "Storage usage for videos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "userId": { @@ -25537,6 +25914,8 @@ }, "videos": { "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -25560,12 +25939,12 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "Avatar color", "nullable": true }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "isAdmin": { @@ -25588,11 +25967,12 @@ "description": "PIN code", "example": "123456", "nullable": true, + "pattern": "^\\d{6}$", "type": "string" }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, "minimum": 0, "nullable": true, "type": "integer" @@ -25626,30 +26006,33 @@ "UserAdminResponseDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "deletedAt": { "description": "Deletion date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "email": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "isAdmin": { @@ -25662,7 +26045,6 @@ "$ref": "#/components/schemas/UserLicense" } ], - "description": "User license", "nullable": true }, "name": { @@ -25684,13 +26066,15 @@ }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, "quotaUsageInBytes": { "description": "Storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, @@ -25699,12 +26083,7 @@ "type": "boolean" }, "status": { - "allOf": [ - { - "$ref": "#/components/schemas/UserStatus" - } - ], - "description": "User status" + "$ref": "#/components/schemas/UserStatus" }, "storageLabel": { "description": "Storage label", @@ -25713,7 +26092,9 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -25746,12 +26127,12 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "Avatar color", "nullable": true }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "isAdmin": { @@ -25770,11 +26151,12 @@ "description": "PIN code", "example": "123456", "nullable": true, + "pattern": "^\\d{6}$", "type": "string" }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, "minimum": 0, "nullable": true, "type": "integer" @@ -25792,7 +26174,7 @@ "type": "object" }, "UserAvatarColor": { - "description": "Avatar color", + "description": "User avatar color", "enum": [ "primary", "pink", @@ -25811,7 +26193,9 @@ "properties": { "activatedAt": { "description": "Activation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "activationKey": { @@ -25819,7 +26203,8 @@ "type": "string" }, "licenseKey": { - "description": "License key", + "description": "License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/)", + "pattern": "^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$", "type": "string" } }, @@ -25934,19 +26319,18 @@ "UserResponseDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" }, "email": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "name": { @@ -25990,12 +26374,12 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "Avatar color", "nullable": true }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "name": { @@ -26003,6 +26387,7 @@ "type": "string" }, "password": { + "deprecated": true, "description": "User password (deprecated, use change password endpoint)", "type": "string" } @@ -26029,8 +26414,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "importPaths": { "description": "Import paths to validate (max 128)", @@ -26038,8 +26422,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" } }, "type": "object" @@ -26051,7 +26434,6 @@ "type": "string" }, "isValid": { - "default": false, "description": "Is valid", "type": "boolean" }, @@ -26108,7 +26490,7 @@ "type": "string" }, "VideoContainer": { - "description": "Accepted containers", + "description": "Accepted video containers", "enum": [ "mov", "mp4", @@ -26117,15 +26499,21 @@ ], "type": "string" }, + "WorkflowActionConfig": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginConfigValue" + }, + "type": "object" + }, "WorkflowActionItemDto": { "properties": { "actionConfig": { - "description": "Action configuration", - "type": "object" + "$ref": "#/components/schemas/WorkflowActionConfig" }, "pluginActionId": { "description": "Plugin action ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -26137,9 +26525,12 @@ "WorkflowActionResponseDto": { "properties": { "actionConfig": { - "description": "Action configuration", - "nullable": true, - "type": "object" + "allOf": [ + { + "$ref": "#/components/schemas/WorkflowActionConfig" + } + ], + "nullable": true }, "id": { "description": "Action ID", @@ -26196,12 +26587,7 @@ "type": "string" }, "triggerType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Workflow trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "required": [ @@ -26212,15 +26598,21 @@ ], "type": "object" }, + "WorkflowFilterConfig": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginConfigValue" + }, + "type": "object" + }, "WorkflowFilterItemDto": { "properties": { "filterConfig": { - "description": "Filter configuration", - "type": "object" + "$ref": "#/components/schemas/WorkflowFilterConfig" }, "pluginFilterId": { "description": "Plugin filter ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -26232,9 +26624,12 @@ "WorkflowFilterResponseDto": { "properties": { "filterConfig": { - "description": "Filter configuration", - "nullable": true, - "type": "object" + "allOf": [ + { + "$ref": "#/components/schemas/WorkflowFilterConfig" + } + ], + "nullable": true }, "id": { "description": "Filter ID", @@ -26304,12 +26699,7 @@ "type": "string" }, "triggerType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Workflow trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "required": [ @@ -26354,12 +26744,7 @@ "type": "string" }, "triggerType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Workflow trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index d74c2dd3e2..365187e6a7 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -15,7 +15,6 @@ export const servers = { server1: "/api" }; export type UserResponseDto = { - /** Avatar color */ avatarColor: UserAvatarColor; /** User email */ email: string; @@ -37,7 +36,6 @@ export type ActivityResponseDto = { createdAt: string; /** Activity ID */ id: string; - /** Activity type */ "type": ReactionType; user: UserResponseDto; }; @@ -48,7 +46,6 @@ export type ActivityCreateDto = { assetId?: string; /** Comment text (required if type is comment) */ comment?: string; - /** Activity type (like or comment) */ "type": ReactionType; }; export type ActivityStatisticsResponseDto = { @@ -58,21 +55,26 @@ export type ActivityStatisticsResponseDto = { likes: number; }; export type DatabaseBackupDeleteDto = { + /** Backup filenames to delete */ backups: string[]; }; export type DatabaseBackupDto = { + /** Backup filename */ filename: string; + /** Backup file size */ filesize: number; + /** Backup timezone */ timezone: string; }; export type DatabaseBackupListResponseDto = { + /** List of backups */ backups: DatabaseBackupDto[]; }; export type DatabaseBackupUploadDto = { + /** Database backup file */ file?: Blob; }; export type SetMaintenanceModeDto = { - /** Maintenance action */ action: MaintenanceAction; /** Restore backup filename */ restoreBackupFilename?: string; @@ -80,7 +82,6 @@ export type SetMaintenanceModeDto = { export type MaintenanceDetectInstallStorageFolderDto = { /** Number of files in the folder */ files: number; - /** Storage folder */ folder: StorageFolder; /** Whether the folder is readable */ readable: boolean; @@ -99,7 +100,6 @@ export type MaintenanceAuthDto = { username: string; }; export type MaintenanceStatusResponseDto = { - /** Maintenance action */ action: MaintenanceAction; active: boolean; error?: string; @@ -108,16 +108,16 @@ export type MaintenanceStatusResponseDto = { }; export type NotificationCreateDto = { /** Additional notification data */ - data?: object; + data?: { + [key: string]: any; + }; /** Notification description */ description?: string | null; - /** Notification level */ level?: NotificationLevel; /** Date when notification was read */ readAt?: string | null; /** Notification title */ title: string; - /** Notification type */ "type"?: NotificationType; /** User ID to send notification to */ userId: string; @@ -126,18 +126,18 @@ export type NotificationDto = { /** Creation date */ createdAt: string; /** Additional notification data */ - data?: object; + data?: { + [key: string]: any; + }; /** Notification description */ description?: string; /** Notification ID */ id: string; - /** Notification level */ level: NotificationLevel; /** Date when notification was read */ readAt?: string; /** Notification title */ title: string; - /** Notification type */ "type": NotificationType; }; export type TemplateDto = { @@ -182,11 +182,10 @@ export type UserLicense = { activatedAt: string; /** Activation key */ activationKey: string; - /** License key */ + /** License key (format: /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/) */ licenseKey: string; }; export type UserAdminResponseDto = { - /** Avatar color */ avatarColor: UserAvatarColor; /** Creation date */ createdAt: string; @@ -198,7 +197,6 @@ export type UserAdminResponseDto = { id: string; /** Is admin user */ isAdmin: boolean; - /** User license */ license: (UserLicense) | null; /** User name */ name: string; @@ -214,7 +212,6 @@ export type UserAdminResponseDto = { quotaUsageInBytes: number | null; /** Require password change on next login */ shouldChangePassword: boolean; - /** User status */ status: UserStatus; /** Storage label */ storageLabel: string | null; @@ -222,7 +219,6 @@ export type UserAdminResponseDto = { updatedAt: string; }; export type UserAdminCreateDto = { - /** Avatar color */ avatarColor?: (UserAvatarColor) | null; /** User email */ email: string; @@ -248,7 +244,6 @@ export type UserAdminDeleteDto = { force?: boolean; }; export type UserAdminUpdateDto = { - /** Avatar color */ avatarColor?: (UserAvatarColor) | null; /** User email */ email?: string; @@ -268,7 +263,6 @@ export type UserAdminUpdateDto = { storageLabel?: string | null; }; export type AlbumsResponse = { - /** Default asset order for albums */ defaultAssetOrder: AssetOrder; }; export type CastResponse = { @@ -343,11 +337,9 @@ export type UserPreferencesResponseDto = { tags: TagsResponse; }; export type AlbumsUpdate = { - /** Default asset order for albums */ defaultAssetOrder?: AssetOrder; }; export type AvatarUpdate = { - /** Avatar color */ color?: UserAvatarColor; }; export type CastUpdate = { @@ -451,7 +443,6 @@ export type AssetStatsResponseDto = { videos: number; }; export type AlbumUserResponseDto = { - /** Album user role */ role: AlbumUserRole; user: UserResponseDto; }; @@ -516,7 +507,6 @@ export type AssetFaceWithoutPersonResponseDto = { imageHeight: number; /** Image width in pixels */ imageWidth: number; - /** Face detection source type */ sourceType?: SourceType; }; export type PersonWithFacesResponseDto = { @@ -524,7 +514,6 @@ export type PersonWithFacesResponseDto = { birthDate: string | null; /** Person color (hex) */ color?: string; - /** Face detections */ faces: AssetFaceWithoutPersonResponseDto[]; /** Person ID */ id: string; @@ -619,12 +608,10 @@ export type AssetResponseDto = { tags?: TagResponseDto[]; /** Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting. */ thumbhash: string | null; - /** Asset type */ "type": AssetTypeEnum; unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; /** The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified. */ updatedAt: string; - /** Asset visibility */ visibility: AssetVisibility; /** Asset width */ width: number | null; @@ -659,7 +646,6 @@ export type AlbumResponseDto = { isActivityEnabled: boolean; /** Last modified asset timestamp */ lastModifiedAssetTimestamp?: string; - /** Asset sort order */ order?: AssetOrder; owner: UserResponseDto; /** Owner user ID */ @@ -672,7 +658,6 @@ export type AlbumResponseDto = { updatedAt: string; }; export type AlbumUserCreateDto = { - /** Album user role */ role: AlbumUserRole; /** User ID */ userId: string; @@ -694,7 +679,6 @@ export type AlbumsAddAssetsDto = { assetIds: string[]; }; export type AlbumsAddAssetsResponseDto = { - /** Error reason */ error?: BulkIdErrorReason; /** Operation success */ success: boolean; @@ -716,7 +700,6 @@ export type UpdateAlbumDto = { description?: string; /** Enable activity feed */ isActivityEnabled?: boolean; - /** Asset sort order */ order?: AssetOrder; }; export type BulkIdsDto = { @@ -724,8 +707,7 @@ export type BulkIdsDto = { ids: string[]; }; export type BulkIdResponseDto = { - /** Error reason if failed */ - error?: Error; + error?: BulkIdErrorReason; errorMessage?: string; /** ID */ id: string; @@ -733,7 +715,6 @@ export type BulkIdResponseDto = { success: boolean; }; export type UpdateAlbumUserDto = { - /** Album user role */ role: AlbumUserRole; }; export type AlbumUserAddDto = { @@ -785,7 +766,9 @@ export type AssetMetadataUpsertItemDto = { /** Metadata key */ key: string; /** Metadata value (object) */ - value: object; + value: { + [key: string]: any; + }; }; export type AssetMediaCreateDto = { /** Asset file data */ @@ -810,13 +793,11 @@ export type AssetMediaCreateDto = { metadata?: AssetMetadataUpsertItemDto[]; /** Sidecar file data */ sidecarData?: Blob; - /** Asset visibility */ visibility?: AssetVisibility; }; export type AssetMediaResponseDto = { /** Asset media ID */ id: string; - /** Upload status */ status: AssetMediaStatus; }; export type AssetBulkUpdateDto = { @@ -840,7 +821,6 @@ export type AssetBulkUpdateDto = { rating?: number | null; /** Time zone (IANA timezone) */ timeZone?: string; - /** Asset visibility */ visibility?: AssetVisibility; }; export type AssetBulkUploadCheckItem = { @@ -854,16 +834,14 @@ export type AssetBulkUploadCheckDto = { assets: AssetBulkUploadCheckItem[]; }; export type AssetBulkUploadCheckResult = { - /** Upload action */ - action: Action; + action: AssetUploadAction; /** Existing asset ID if duplicate */ assetId?: string; /** Asset ID */ id: string; /** Whether existing asset is trashed */ isTrashed?: boolean; - /** Rejection reason if rejected */ - reason?: Reason; + reason?: AssetRejectReason; }; export type AssetBulkUploadCheckResponseDto = { /** Upload check results */ @@ -898,7 +876,6 @@ export type CheckExistingAssetsResponseDto = { export type AssetJobsDto = { /** Asset IDs */ assetIds: string[]; - /** Job name */ name: AssetJobName; }; export type AssetMetadataBulkDeleteItemDto = { @@ -917,7 +894,9 @@ export type AssetMetadataBulkUpsertItemDto = { /** Metadata key */ key: string; /** Metadata value (object) */ - value: object; + value: { + [key: string]: any; + }; }; export type AssetMetadataBulkUpsertDto = { /** Metadata items to upsert */ @@ -931,7 +910,9 @@ export type AssetMetadataBulkResponseDto = { /** Last update date */ updatedAt: string; /** Metadata value (object) */ - value: object; + value: { + [key: string]: any; + }; }; export type UpdateAssetDto = { /** Original date and time */ @@ -948,7 +929,6 @@ export type UpdateAssetDto = { longitude?: number; /** Rating in range [1-5], or null for unrated */ rating?: number | null; - /** Asset visibility */ visibility?: AssetVisibility; }; export type CropParameters = { @@ -966,12 +946,11 @@ export type RotateParameters = { angle: number; }; export type MirrorParameters = { - /** Axis to mirror along */ axis: MirrorAxis; }; export type AssetEditActionItemResponseDto = { - /** Type of edit action to perform */ action: AssetEditAction; + /** Asset edit ID */ id: string; /** List of edit actions to apply (crop, rotate, or mirror) */ parameters: CropParameters | RotateParameters | MirrorParameters; @@ -983,7 +962,6 @@ export type AssetEditsResponseDto = { edits: AssetEditActionItemResponseDto[]; }; export type AssetEditActionItemDto = { - /** Type of edit action to perform */ action: AssetEditAction; /** List of edit actions to apply (crop, rotate, or mirror) */ parameters: CropParameters | RotateParameters | MirrorParameters; @@ -998,7 +976,9 @@ export type AssetMetadataResponseDto = { /** Last update date */ updatedAt: string; /** Metadata value (object) */ - value: object; + value: { + [key: string]: any; + }; }; export type AssetMetadataUpsertDto = { /** Metadata items to upsert */ @@ -1212,9 +1192,7 @@ export type AssetFaceResponseDto = { imageHeight: number; /** Image width in pixels */ imageWidth: number; - /** Person associated with face */ person: (PersonResponseDto) | null; - /** Face detection source type */ sourceType?: SourceType; }; export type AssetFaceCreateDto = { @@ -1288,11 +1266,9 @@ export type QueuesResponseLegacyDto = { workflow: QueueResponseLegacyDto; }; export type JobCreateDto = { - /** Job name */ name: ManualJobName; }; export type QueueCommandDto = { - /** Queue command to execute */ command: QueueCommand; /** Force the command execution (if applicable) */ force?: boolean; @@ -1410,7 +1386,6 @@ export type MemoryResponseDto = { seenAt?: string; /** Date when memory should be shown */ showAt?: string; - /** Memory type */ "type": MemoryType; /** Last update date */ updatedAt: string; @@ -1429,7 +1404,6 @@ export type MemoryCreateDto = { seenAt?: string; /** Date when memory should be shown */ showAt?: string; - /** Memory type */ "type": MemoryType; }; export type MemoryStatisticsResponseDto = { @@ -1479,7 +1453,6 @@ export type OAuthCallbackDto = { url: string; }; export type PartnerResponseDto = { - /** Avatar color */ avatarColor: UserAvatarColor; /** User email */ email: string; @@ -1507,7 +1480,6 @@ export type PeopleResponseDto = { hasNextPage?: boolean; /** Number of hidden people */ hidden: number; - /** List of people */ people: PersonResponseDto[]; /** Total number of people */ total: number; @@ -1576,6 +1548,27 @@ export type PersonStatisticsResponseDto = { /** Number of assets */ assets: number; }; +export type PluginJsonSchemaProperty = { + additionalProperties?: boolean | PluginJsonSchemaProperty; + "default"?: any; + description?: string; + "enum"?: string[]; + items?: PluginJsonSchemaProperty; + properties?: { + [key: string]: PluginJsonSchemaProperty; + }; + required?: string[]; + "type"?: PluginJsonSchemaType; +}; +export type PluginJsonSchema = { + additionalProperties?: boolean; + description?: string; + properties?: { + [key: string]: PluginJsonSchemaProperty; + }; + required?: string[]; + "type"?: PluginJsonSchemaType; +}; export type PluginActionResponseDto = { /** Action description */ description: string; @@ -1586,7 +1579,7 @@ export type PluginActionResponseDto = { /** Plugin ID */ pluginId: string; /** Action schema */ - schema: object | null; + schema: (PluginJsonSchema) | null; /** Supported contexts */ supportedContexts: PluginContextType[]; /** Action title */ @@ -1602,7 +1595,7 @@ export type PluginFilterResponseDto = { /** Plugin ID */ pluginId: string; /** Filter schema */ - schema: object | null; + schema: (PluginJsonSchema) | null; /** Supported contexts */ supportedContexts: PluginContextType[]; /** Filter title */ @@ -1631,15 +1624,12 @@ export type PluginResponseDto = { version: string; }; export type PluginTriggerResponseDto = { - /** Context type */ contextType: PluginContextType; - /** Trigger type */ "type": PluginTriggerType; }; export type QueueResponseDto = { /** Whether the queue is paused */ isPaused: boolean; - /** Queue name */ name: QueueName; statistics: QueueStatisticsDto; }; @@ -1653,10 +1643,11 @@ export type QueueDeleteDto = { }; export type QueueJobResponseDto = { /** Job data payload */ - data: object; + data: { + [key: string]: any; + }; /** Job ID */ id?: string; - /** Job name */ name: JobName; /** Job creation timestamp */ timestamp: number; @@ -1709,7 +1700,7 @@ export type MetadataSearchDto = { /** Library ID to filter by */ libraryId?: string | null; /** Filter by camera make */ - make?: string; + make?: string | null; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1744,13 +1735,11 @@ export type MetadataSearchDto = { trashedAfter?: string; /** Filter by trash date (before) */ trashedBefore?: string; - /** Asset type filter */ "type"?: AssetTypeEnum; /** Filter by update date (after) */ updatedAfter?: string; /** Filter by update date (before) */ updatedBefore?: string; - /** Filter by visibility */ visibility?: AssetVisibility; /** Include deleted assets */ withDeleted?: boolean; @@ -1768,7 +1757,6 @@ export type SearchFacetCountResponseDto = { value: string; }; export type SearchFacetResponseDto = { - /** Facet counts */ counts: SearchFacetCountResponseDto[]; /** Facet field name */ fieldName: string; @@ -1835,7 +1823,7 @@ export type RandomSearchDto = { /** Library ID to filter by */ libraryId?: string | null; /** Filter by camera make */ - make?: string; + make?: string | null; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1858,13 +1846,11 @@ export type RandomSearchDto = { trashedAfter?: string; /** Filter by trash date (before) */ trashedBefore?: string; - /** Asset type filter */ "type"?: AssetTypeEnum; /** Filter by update date (after) */ updatedAfter?: string; /** Filter by update date (before) */ updatedBefore?: string; - /** Filter by visibility */ visibility?: AssetVisibility; /** Include deleted assets */ withDeleted?: boolean; @@ -1905,7 +1891,7 @@ export type SmartSearchDto = { /** Library ID to filter by */ libraryId?: string | null; /** Filter by camera make */ - make?: string; + make?: string | null; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1934,13 +1920,11 @@ export type SmartSearchDto = { trashedAfter?: string; /** Filter by trash date (before) */ trashedBefore?: string; - /** Asset type filter */ "type"?: AssetTypeEnum; /** Filter by update date (after) */ updatedAfter?: string; /** Filter by update date (before) */ updatedBefore?: string; - /** Filter by visibility */ visibility?: AssetVisibility; /** Include deleted assets */ withDeleted?: boolean; @@ -1977,7 +1961,7 @@ export type StatisticsSearchDto = { /** Library ID to filter by */ libraryId?: string | null; /** Filter by camera make */ - make?: string; + make?: string | null; /** Filter by camera model */ model?: string | null; /** Filter by OCR text content */ @@ -1998,13 +1982,11 @@ export type StatisticsSearchDto = { trashedAfter?: string; /** Filter by trash date (before) */ trashedBefore?: string; - /** Asset type filter */ "type"?: AssetTypeEnum; /** Filter by update date (after) */ updatedAfter?: string; /** Filter by update date (before) */ updatedBefore?: string; - /** Filter by visibility */ visibility?: AssetVisibility; }; export type SearchStatisticsResponseDto = { @@ -2121,18 +2103,10 @@ export type ServerFeaturesDto = { /** Whether trash feature is enabled */ trash: boolean; }; -export type LicenseResponseDto = { - /** Activation date */ - activatedAt: string; - /** Activation key */ - activationKey: string; - /** License key (format: IM(SV|CL)(-XXXX){8}) */ - licenseKey: string; -}; export type LicenseKeyDto = { /** Activation key */ activationKey: string; - /** License key (format: IM(SV|CL)(-XXXX){8}) */ + /** License key (format: /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/) */ licenseKey: string; }; export type ServerMediaTypesResponseDto = { @@ -2143,8 +2117,7 @@ export type ServerMediaTypesResponseDto = { /** Supported video MIME types */ video: string[]; }; -export type ServerPingResponse = {}; -export type ServerPingResponseRead = { +export type ServerPingResponse = { res: string; }; export type UsageByUserDto = { @@ -2170,6 +2143,7 @@ export type ServerStatsResponseDto = { photos: number; /** Total storage usage in bytes */ usage: number; + /** Array of usage for each user */ usageByUser: UsageByUserDto[]; /** Storage usage for photos in bytes */ usagePhotos: number; @@ -2279,7 +2253,6 @@ export type SharedLinkResponseDto = { slug: string | null; /** Access token */ token?: string | null; - /** Shared link type */ "type": SharedLinkType; /** Owner user ID */ userId: string; @@ -2303,7 +2276,6 @@ export type SharedLinkCreateDto = { showMetadata?: boolean; /** Custom URL slug */ slug?: string | null; - /** Shared link type */ "type": SharedLinkType; }; export type SharedLinkLoginDto = { @@ -2335,13 +2307,11 @@ export type AssetIdsDto = { export type AssetIdsResponseDto = { /** Asset ID */ assetId: string; - /** Error reason if failed */ - error?: Error2; + error?: AssetIdErrorReason; /** Whether operation succeeded */ success: boolean; }; export type StackResponseDto = { - /** Stack assets */ assets: AssetResponseDto[]; /** Stack ID */ id: string; @@ -2363,7 +2333,6 @@ export type SyncAckDeleteDto = { export type SyncAckDto = { /** Acknowledgment ID */ ack: string; - /** Sync entity type */ "type": SyncEntityType; }; export type SyncAckSetDto = { @@ -2381,7 +2350,6 @@ export type AssetDeltaSyncResponseDto = { deleted: string[]; /** Whether full sync is needed */ needsFullSync: boolean; - /** Upserted assets */ upserted: AssetResponseDto[]; }; export type AssetFullSyncDto = { @@ -2412,7 +2380,6 @@ export type SystemConfigBackupsDto = { database: DatabaseBackupConfig; }; export type SystemConfigFFmpegDto = { - /** Transcode hardware acceleration */ accel: TranscodeHWAccel; /** Accelerated decode */ accelDecode: boolean; @@ -2424,7 +2391,6 @@ export type SystemConfigFFmpegDto = { acceptedVideoCodecs: VideoCodec[]; /** B-frames */ bframes: number; - /** CQ mode */ cqMode: CQMode; /** CRF */ crf: number; @@ -2438,19 +2404,15 @@ export type SystemConfigFFmpegDto = { preset: string; /** References */ refs: number; - /** Target audio codec */ targetAudioCodec: AudioCodec; /** Target resolution */ targetResolution: string; - /** Target video codec */ targetVideoCodec: VideoCodec; /** Temporal AQ */ temporalAQ: boolean; /** Threads */ threads: number; - /** Tone mapping */ tonemap: ToneMapping; - /** Transcode policy */ transcode: TranscodePolicy; /** Two pass */ twoPass: boolean; @@ -2458,7 +2420,6 @@ export type SystemConfigFFmpegDto = { export type SystemConfigGeneratedFullsizeImageDto = { /** Enabled */ enabled: boolean; - /** Image format */ format: ImageFormat; /** Progressive */ progressive?: boolean; @@ -2466,8 +2427,8 @@ export type SystemConfigGeneratedFullsizeImageDto = { quality: number; }; export type SystemConfigGeneratedImageDto = { - /** Image format */ format: ImageFormat; + /** Progressive */ progressive?: boolean; /** Quality */ quality: number; @@ -2475,7 +2436,6 @@ export type SystemConfigGeneratedImageDto = { size: number; }; export type SystemConfigImageDto = { - /** Colorspace */ colorspace: Colorspace; /** Extract embedded */ extractEmbedded: boolean; @@ -2504,6 +2464,7 @@ export type SystemConfigJobDto = { workflow: JobSettingsDto; }; export type SystemConfigLibraryScanDto = { + /** Cron expression */ cronExpression: string; /** Enabled */ enabled: boolean; @@ -2571,12 +2532,15 @@ export type SystemConfigMachineLearningDto = { enabled: boolean; facialRecognition: FacialRecognitionConfig; ocr: OcrConfig; + /** ML service URLs */ urls: string[]; }; export type SystemConfigMapDto = { + /** Dark map style URL */ darkStyle: string; /** Enabled */ enabled: boolean; + /** Light map style URL */ lightStyle: string; }; export type SystemConfigFacesDto = { @@ -2599,6 +2563,7 @@ export type SystemConfigNightlyTasksDto = { generateMemories: boolean; /** Missing thumbnails */ missingThumbnails: boolean; + /** Start time */ startTime: string; /** Sync quota usage */ syncQuotaUsage: boolean; @@ -2625,7 +2590,7 @@ export type SystemConfigOAuthDto = { issuerUrl: string; /** Mobile override enabled */ mobileOverrideEnabled: boolean; - /** Mobile redirect URI */ + /** Mobile redirect URI (set to empty string to disable) */ mobileRedirectUri: string; /** Profile signing algorithm */ profileSigningAlgorithm: string; @@ -2633,6 +2598,7 @@ export type SystemConfigOAuthDto = { roleClaim: string; /** Scope */ scope: string; + /** Signing algorithm */ signingAlgorithm: string; /** Storage label claim */ storageLabelClaim: string; @@ -2640,7 +2606,6 @@ export type SystemConfigOAuthDto = { storageQuotaClaim: string; /** Timeout */ timeout: number; - /** Token endpoint auth method */ tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethod; }; export type SystemConfigPasswordLoginDto = { @@ -2668,8 +2633,11 @@ export type SystemConfigStorageTemplateDto = { template: string; }; export type SystemConfigTemplateEmailsDto = { + /** Album invite template */ albumInviteTemplate: string; + /** Album update template */ albumUpdateTemplate: string; + /** Welcome template */ welcomeTemplate: string; }; export type SystemConfigTemplatesDto = { @@ -2742,7 +2710,7 @@ export type ReverseGeocodingStateResponseDto = { }; export type TagCreateDto = { /** Tag color (hex) */ - color?: string; + color?: string | null; /** Tag name */ name: string; /** Parent tag ID */ @@ -2815,7 +2783,6 @@ export type TrashResponseDto = { count: number; }; export type UserUpdateMeDto = { - /** Avatar color */ avatarColor?: (UserAvatarColor) | null; /** User email */ email?: string; @@ -2844,9 +2811,12 @@ export type CreateProfileImageResponseDto = { /** User ID */ userId: string; }; +export type PluginConfigValue = any; +export type WorkflowActionConfig = { + [key: string]: PluginConfigValue; +}; export type WorkflowActionResponseDto = { - /** Action configuration */ - actionConfig: object | null; + actionConfig: (WorkflowActionConfig) | null; /** Action ID */ id: string; /** Action order */ @@ -2856,9 +2826,11 @@ export type WorkflowActionResponseDto = { /** Workflow ID */ workflowId: string; }; +export type WorkflowFilterConfig = { + [key: string]: PluginConfigValue; +}; export type WorkflowFilterResponseDto = { - /** Filter configuration */ - filterConfig: object | null; + filterConfig: (WorkflowFilterConfig) | null; /** Filter ID */ id: string; /** Filter order */ @@ -2885,18 +2857,15 @@ export type WorkflowResponseDto = { name: string | null; /** Owner user ID */ ownerId: string; - /** Workflow trigger type */ triggerType: PluginTriggerType; }; export type WorkflowActionItemDto = { - /** Action configuration */ - actionConfig?: object; + actionConfig?: WorkflowActionConfig; /** Plugin action ID */ pluginActionId: string; }; export type WorkflowFilterItemDto = { - /** Filter configuration */ - filterConfig?: object; + filterConfig?: WorkflowFilterConfig; /** Plugin filter ID */ pluginFilterId: string; }; @@ -2911,7 +2880,6 @@ export type WorkflowCreateDto = { filters: WorkflowFilterItemDto[]; /** Workflow name */ name: string; - /** Workflow trigger type */ triggerType: PluginTriggerType; }; export type WorkflowUpdateDto = { @@ -2925,9 +2893,9 @@ export type WorkflowUpdateDto = { filters?: WorkflowFilterItemDto[]; /** Workflow name */ name?: string; - /** Workflow trigger type */ triggerType?: PluginTriggerType; }; +export type LicenseResponseDto = UserLicense; export type SyncAckV1 = {}; export type SyncAlbumDeleteV1 = { /** Album ID */ @@ -2954,7 +2922,6 @@ export type SyncAlbumUserDeleteV1 = { export type SyncAlbumUserV1 = { /** Album ID */ albumId: string; - /** Album user role */ role: AlbumUserRole; /** User ID */ userId: string; @@ -2983,13 +2950,20 @@ export type SyncAssetDeleteV1 = { assetId: string; }; export type SyncAssetEditDeleteV1 = { + /** Edit ID */ editId: string; }; export type SyncAssetEditV1 = { action: AssetEditAction; + /** Asset ID */ assetId: string; + /** Edit ID */ id: string; - parameters: object; + /** Edit parameters */ + parameters: { + [key: string]: any; + }; + /** Edit sequence */ sequence: number; }; export type SyncAssetExifV1 = { @@ -3051,13 +3025,19 @@ export type SyncAssetFaceDeleteV1 = { export type SyncAssetFaceV1 = { /** Asset ID */ assetId: string; + /** Bounding box X1 */ boundingBoxX1: number; + /** Bounding box X2 */ boundingBoxX2: number; + /** Bounding box Y1 */ boundingBoxY1: number; + /** Bounding box Y2 */ boundingBoxY2: number; /** Asset face ID */ id: string; + /** Image height */ imageHeight: number; + /** Image width */ imageWidth: number; /** Person ID */ personId: string | null; @@ -3067,15 +3047,21 @@ export type SyncAssetFaceV1 = { export type SyncAssetFaceV2 = { /** Asset ID */ assetId: string; + /** Bounding box X1 */ boundingBoxX1: number; + /** Bounding box X2 */ boundingBoxX2: number; + /** Bounding box Y1 */ boundingBoxY1: number; + /** Bounding box Y2 */ boundingBoxY2: number; /** Face deleted at */ deletedAt: string | null; /** Asset face ID */ id: string; + /** Image height */ imageHeight: number; + /** Image width */ imageWidth: number; /** Is the face visible in the asset */ isVisible: boolean; @@ -3096,7 +3082,9 @@ export type SyncAssetMetadataV1 = { /** Key */ key: string; /** Value */ - value: object; + value: { + [key: string]: any; + }; }; export type SyncAssetV1 = { /** Checksum */ @@ -3131,16 +3119,13 @@ export type SyncAssetV1 = { stackId: string | null; /** Thumbhash */ thumbhash: string | null; - /** Asset type */ "type": AssetTypeEnum; - /** Asset visibility */ visibility: AssetVisibility; /** Asset width */ width: number | null; }; export type SyncAuthUserV1 = { - /** User avatar color */ - avatarColor: (UserAvatarColor) | null; + avatarColor?: (UserAvatarColor) | null; /** User deleted at */ deletedAt: string | null; /** User email */ @@ -3159,7 +3144,9 @@ export type SyncAuthUserV1 = { pinCode: string | null; /** User profile changed at */ profileChangedAt: string; + /** Quota size in bytes */ quotaSizeInBytes: number | null; + /** Quota usage in bytes */ quotaUsageInBytes: number; /** User storage label */ storageLabel: string | null; @@ -3185,7 +3172,9 @@ export type SyncMemoryV1 = { /** Created at */ createdAt: string; /** Data */ - data: object; + data: { + [key: string]: any; + }; /** Deleted at */ deletedAt: string | null; /** Hide at */ @@ -3202,7 +3191,6 @@ export type SyncMemoryV1 = { seenAt: string | null; /** Show at */ showAt: string | null; - /** Memory type */ "type": MemoryType; /** Updated at */ updatedAt: string; @@ -3269,22 +3257,21 @@ export type SyncUserDeleteV1 = { userId: string; }; export type SyncUserMetadataDeleteV1 = { - /** User metadata key */ key: UserMetadataKey; /** User ID */ userId: string; }; export type SyncUserMetadataV1 = { - /** User metadata key */ key: UserMetadataKey; /** User ID */ userId: string; /** User metadata value */ - value: object; + value: { + [key: string]: any; + }; }; export type SyncUserV1 = { - /** User avatar color */ - avatarColor: (UserAvatarColor) | null; + avatarColor?: (UserAvatarColor) | null; /** User deleted at */ deletedAt: string | null; /** User email */ @@ -5479,7 +5466,7 @@ export function searchLargeAssets({ albumIds, city, country, createdAfter, creat isOffline?: boolean; lensModel?: string | null; libraryId?: string | null; - make?: string; + make?: string | null; minFileSize?: number; model?: string | null; ocr?: string; @@ -5718,7 +5705,7 @@ export function deleteServerLicense(opts?: Oazapfts.RequestOpts) { export function getServerLicense(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: LicenseResponseDto; + data: UserLicense; } | { status: 404; }>("/server/license", { @@ -5733,7 +5720,7 @@ export function setServerLicense({ licenseKeyDto }: { }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: LicenseResponseDto; + data: UserLicense; }>("/server/license", oazapfts.json({ ...opts, method: "PUT", @@ -5757,7 +5744,7 @@ export function getSupportedMediaTypes(opts?: Oazapfts.RequestOpts) { export function pingServer(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: ServerPingResponseRead; + data: ServerPingResponse; }>("/server/ping", { ...opts })); @@ -6618,7 +6605,7 @@ export function deleteUserLicense(opts?: Oazapfts.RequestOpts) { export function getUserLicense(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: LicenseResponseDto; + data: UserLicense; }>("/users/me/license", { ...opts })); @@ -6631,7 +6618,7 @@ export function setUserLicense({ licenseKeyDto }: { }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: LicenseResponseDto; + data: UserLicense; }>("/users/me/license", oazapfts.json({ ...opts, method: "PUT", @@ -6926,13 +6913,6 @@ export enum BulkIdErrorReason { Unknown = "unknown", Validation = "validation" } -export enum Error { - Duplicate = "duplicate", - NoPermission = "no_permission", - NotFound = "not_found", - Unknown = "unknown", - Validation = "validation" -} export enum Permission { All = "all", ActivityCreate = "activity.create", @@ -7096,11 +7076,11 @@ export enum AssetMediaStatus { Replaced = "replaced", Duplicate = "duplicate" } -export enum Action { +export enum AssetUploadAction { Accept = "accept", Reject = "reject" } -export enum Reason { +export enum AssetRejectReason { Duplicate = "duplicate", UnsupportedFormat = "unsupported-format" } @@ -7172,6 +7152,15 @@ export enum PartnerDirection { SharedBy = "shared-by", SharedWith = "shared-with" } +export enum PluginJsonSchemaType { + String = "string", + Number = "number", + Integer = "integer", + Boolean = "boolean", + Object = "object", + Array = "array", + Null = "null" +} export enum PluginContextType { Asset = "asset", Album = "album", @@ -7259,7 +7248,7 @@ export enum SharedLinkType { Album = "ALBUM", Individual = "INDIVIDUAL" } -export enum Error2 { +export enum AssetIdErrorReason { Duplicate = "duplicate", NoPermission = "no_permission", NotFound = "not_found" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9dd140fbc3..077f7a6785 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -433,12 +433,6 @@ importers: chokidar: specifier: ^4.0.3 version: 4.0.3 - class-transformer: - specifier: ^0.5.1 - version: 0.5.1 - class-validator: - specifier: ^0.15.0 - version: 0.15.1 compression: specifier: ^1.8.0 version: 1.8.1 @@ -517,6 +511,9 @@ importers: nestjs-otel: specifier: ^7.0.0 version: 7.0.1(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18) + nestjs-zod: + specifier: ^5.3.0 + version: 5.3.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6) nodemailer: specifier: ^8.0.0 version: 8.0.5 @@ -583,6 +580,9 @@ importers: validator: specifier: ^13.12.0 version: 13.15.26 + zod: + specifier: ^4.3.6 + version: 4.3.6 devDependencies: '@eslint/js': specifier: ^10.0.0 @@ -9390,6 +9390,17 @@ packages: '@nestjs/common': '>= 11 < 12' '@nestjs/core': '>= 11 < 12' + nestjs-zod@5.3.0: + resolution: {integrity: sha512-QY6imXm9heMOpWigjFHgMWPvc1ZQHeNQ7pdogo9Q5xj5F8HpqZ972vKlVdkaTyzYlOXJP/yVy3wlF1EjubDQPg==} + peerDependencies: + '@nestjs/common': ^10.0.0 || ^11.0.0 + '@nestjs/swagger': ^7.4.2 || ^8.0.0 || ^11.0.0 + rxjs: ^7.0.0 + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@nestjs/swagger': + optional: true + next-tick@1.1.0: resolution: {integrity: sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==} @@ -12532,8 +12543,8 @@ packages: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} - zod@4.2.1: - resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} zwitch@1.0.5: resolution: {integrity: sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==} @@ -12545,33 +12556,33 @@ snapshots: '@adobe/css-tools@4.4.4': {} - '@ai-sdk/gateway@2.0.21(zod@4.2.1)': + '@ai-sdk/gateway@2.0.21(zod@4.3.6)': dependencies: '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.19(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.19(zod@4.3.6) '@vercel/oidc': 3.0.5 - zod: 4.2.1 + zod: 4.3.6 - '@ai-sdk/provider-utils@3.0.19(zod@4.2.1)': + '@ai-sdk/provider-utils@3.0.19(zod@4.3.6)': dependencies: '@ai-sdk/provider': 2.0.0 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 - zod: 4.2.1 + zod: 4.3.6 '@ai-sdk/provider@2.0.0': dependencies: json-schema: 0.4.0 - '@ai-sdk/react@2.0.115(react@19.2.4)(zod@4.2.1)': + '@ai-sdk/react@2.0.115(react@19.2.4)(zod@4.3.6)': dependencies: - '@ai-sdk/provider-utils': 3.0.19(zod@4.2.1) - ai: 5.0.113(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.19(zod@4.3.6) + ai: 5.0.113(zod@4.3.6) react: 19.2.4 swr: 2.3.8(react@19.2.4) throttleit: 2.1.0 optionalDependencies: - zod: 4.2.1 + zod: 4.3.6 '@algolia/abtesting@1.12.0': dependencies: @@ -13914,14 +13925,14 @@ snapshots: '@docsearch/react@4.3.2(@algolia/client-search@5.46.0)(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(search-insights@2.17.3)': dependencies: - '@ai-sdk/react': 2.0.115(react@19.2.4)(zod@4.2.1) + '@ai-sdk/react': 2.0.115(react@19.2.4)(zod@4.3.6) '@algolia/autocomplete-core': 1.19.2(@algolia/client-search@5.46.0)(algoliasearch@5.46.0)(search-insights@2.17.3) '@docsearch/core': 4.3.1(@types/react@19.2.14)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) '@docsearch/css': 4.3.2 - ai: 5.0.113(zod@4.2.1) + ai: 5.0.113(zod@4.3.6) algoliasearch: 5.46.0 marked: 16.4.2 - zod: 4.2.1 + zod: 4.3.6 optionalDependencies: '@types/react': 19.2.14 react: 19.2.4 @@ -17935,13 +17946,13 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 - ai@5.0.113(zod@4.2.1): + ai@5.0.113(zod@4.3.6): dependencies: - '@ai-sdk/gateway': 2.0.21(zod@4.2.1) + '@ai-sdk/gateway': 2.0.21(zod@4.3.6) '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.19(zod@4.2.1) + '@ai-sdk/provider-utils': 3.0.19(zod@4.3.6) '@opentelemetry/api': 1.9.0 - zod: 4.2.1 + zod: 4.3.6 ajv-formats@2.1.1(ajv@8.18.0): optionalDependencies: @@ -18600,13 +18611,15 @@ snapshots: cjs-module-lexer@2.2.0: {} - class-transformer@0.5.1: {} + class-transformer@0.5.1: + optional: true class-validator@0.15.1: dependencies: '@types/validator': 13.15.10 libphonenumber-js: 1.12.38 validator: 13.15.26 + optional: true clean-css@5.3.3: dependencies: @@ -21483,7 +21496,8 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - libphonenumber-js@1.12.38: {} + libphonenumber-js@1.12.38: + optional: true lightningcss-android-arm64@1.32.0: optional: true @@ -22533,6 +22547,15 @@ snapshots: response-time: 2.3.4 tslib: 2.8.1 + nestjs-zod@5.3.0(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/swagger@11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2))(rxjs@7.8.2)(zod@4.3.6): + dependencies: + '@nestjs/common': 11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2) + deepmerge: 4.3.1 + rxjs: 7.8.2 + zod: 4.3.6 + optionalDependencies: + '@nestjs/swagger': 11.2.6(@nestjs/common@11.1.17(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.18)(class-transformer@0.5.1)(class-validator@0.15.1)(reflect-metadata@0.2.2) + next-tick@1.1.0: {} no-case@3.0.4: @@ -26252,7 +26275,7 @@ snapshots: compress-commons: 6.0.2 readable-stream: 4.7.0 - zod@4.2.1: {} + zod@4.3.6: {} zwitch@1.0.5: {} diff --git a/server/package.json b/server/package.json index bd3f5b0d69..73ea7f6f45 100644 --- a/server/package.json +++ b/server/package.json @@ -70,8 +70,6 @@ "body-parser": "^2.2.0", "bullmq": "^5.51.0", "chokidar": "^4.0.3", - "class-transformer": "^0.5.1", - "class-validator": "^0.15.0", "compression": "^1.8.0", "cookie": "^1.0.2", "cookie-parser": "^1.4.7", @@ -99,6 +97,7 @@ "nestjs-kysely": "3.1.2", "nestjs-otel": "^7.0.0", "nodemailer": "^8.0.0", + "nestjs-zod": "^5.3.0", "openid-client": "^6.3.3", "pg": "^8.11.3", "pg-connection-string": "^2.9.1", @@ -119,7 +118,8 @@ "transformation-matrix": "^3.1.0", "ua-parser-js": "^2.0.0", "uuid": "^11.1.0", - "validator": "^13.12.0" + "validator": "^13.12.0", + "zod": "^4.3.6" }, "devDependencies": { "@eslint/js": "^10.0.0", diff --git a/server/src/app.module.ts b/server/src/app.module.ts index f2b6a7e805..ae930762d0 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -1,10 +1,11 @@ import { BullModule } from '@nestjs/bullmq'; -import { Inject, Module, OnModuleDestroy, OnModuleInit, ValidationPipe } from '@nestjs/common'; +import { Inject, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { ClsModule } from 'nestjs-cls'; import { KyselyModule } from 'nestjs-kysely'; import { OpenTelemetryModule } from 'nestjs-otel'; +import { ZodSerializerInterceptor, ZodValidationPipe } from 'nestjs-zod'; import { commandsAndQuestions } from 'src/commands'; import { IWorker } from 'src/constants'; import { controllers } from 'src/controllers'; @@ -43,7 +44,8 @@ const common = [...repositories, ...services, GlobalExceptionFilter]; const commonMiddleware = [ { provide: APP_FILTER, useClass: GlobalExceptionFilter }, - { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, + { provide: APP_PIPE, useClass: ZodValidationPipe }, + { provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, ]; diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index b632332069..5be9ae29b9 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -3,7 +3,6 @@ import { INestApplication } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { SchedulerRegistry } from '@nestjs/schedule'; import { Test } from '@nestjs/testing'; -import { ClassConstructor } from 'class-transformer'; import { ClsModule } from 'nestjs-cls'; import { KyselyModule } from 'nestjs-kysely'; import { OpenTelemetryModule } from 'nestjs-otel'; @@ -44,7 +43,7 @@ export class SqlLogger { const reflector = new Reflector(); -type Repository = ClassConstructor; +type Repository = new (...args: any[]) => any; type SqlGeneratorOptions = { targetDir: string }; class SqlGenerator { diff --git a/server/src/controllers/activity.controller.spec.ts b/server/src/controllers/activity.controller.spec.ts index bf2038048f..7ac6e051f6 100644 --- a/server/src/controllers/activity.controller.spec.ts +++ b/server/src/controllers/activity.controller.spec.ts @@ -27,13 +27,15 @@ describe(ActivityController.name, () => { it('should require an albumId', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/activities'); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual( + factory.responses.badRequest(['[albumId] Invalid input: expected string, received undefined']), + ); }); it('should reject an invalid albumId', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/activities').query({ albumId: '123' }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(['[albumId] Invalid UUID'])); }); it('should reject an invalid assetId', async () => { @@ -41,7 +43,7 @@ describe(ActivityController.name, () => { .get('/activities') .query({ albumId: factory.uuid(), assetId: '123' }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['assetId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(['[assetId] Invalid UUID'])); }); }); @@ -52,9 +54,11 @@ describe(ActivityController.name, () => { }); it('should require an albumId', async () => { - const { status, body } = await request(ctx.getHttpServer()).post('/activities').send({ albumId: '123' }); + const { status, body } = await request(ctx.getHttpServer()) + .post('/activities') + .send({ albumId: '123', type: 'like' }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(['[albumId] Invalid UUID'])); }); it('should require a comment when type is comment', async () => { @@ -62,7 +66,7 @@ describe(ActivityController.name, () => { .post('/activities') .send({ albumId: factory.uuid(), type: 'comment', comment: null }); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(['comment must be a string', 'comment should not be empty'])); + expect(body).toEqual(factory.responses.badRequest(['[comment] Invalid input: expected string, received null'])); }); }); @@ -75,7 +79,7 @@ describe(ActivityController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/activities/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/album.controller.spec.ts b/server/src/controllers/album.controller.spec.ts index d13227555b..fadc5103eb 100644 --- a/server/src/controllers/album.controller.spec.ts +++ b/server/src/controllers/album.controller.spec.ts @@ -27,13 +27,13 @@ describe(AlbumController.name, () => { it('should reject an invalid shared param', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/albums?shared=invalid'); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(['shared must be a boolean value'])); + expect(body).toEqual(factory.responses.badRequest(['[shared] Invalid option: expected one of "true"|"false"'])); }); it('should reject an invalid assetId param', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/albums?assetId=invalid'); expect(status).toEqual(400); - expect(body).toEqual(factory.responses.badRequest(['assetId must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[assetId] Invalid UUID'])); }); }); diff --git a/server/src/controllers/api-key.controller.spec.ts b/server/src/controllers/api-key.controller.spec.ts index c6dab09a3c..23a1f8b98c 100644 --- a/server/src/controllers/api-key.controller.spec.ts +++ b/server/src/controllers/api-key.controller.spec.ts @@ -49,7 +49,7 @@ describe(ApiKeyController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/api-keys/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); @@ -64,7 +64,7 @@ describe(ApiKeyController.name, () => { .put(`/api-keys/123`) .send({ name: 'new name', permissions: [Permission.All] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); it('should allow updating just the name', async () => { @@ -84,7 +84,7 @@ describe(ApiKeyController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/api-keys/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/asset-media.controller.spec.ts b/server/src/controllers/asset-media.controller.spec.ts index c2f6aeacef..0bfb423898 100644 --- a/server/src/controllers/asset-media.controller.spec.ts +++ b/server/src/controllers/asset-media.controller.spec.ts @@ -82,7 +82,9 @@ describe(AssetMediaController.name, () => { }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['metadata must be valid JSON'])); + expect(body).toEqual( + factory.responses.badRequest(['[metadata] Invalid input: expected JSON string, received string']), + ); }); it('should require `deviceAssetId`', async () => { @@ -92,7 +94,7 @@ describe(AssetMediaController.name, () => { .field({ ...makeUploadDto({ omit: 'deviceAssetId' }) }); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(['deviceAssetId must be a string', 'deviceAssetId should not be empty']), + factory.responses.badRequest(['[deviceAssetId] Invalid input: expected string, received undefined']), ); }); @@ -102,7 +104,9 @@ describe(AssetMediaController.name, () => { .attach('assetData', assetData, filename) .field({ ...makeUploadDto({ omit: 'deviceId' }) }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['deviceId must be a string', 'deviceId should not be empty'])); + expect(body).toEqual( + factory.responses.badRequest(['[deviceId] Invalid input: expected string, received undefined']), + ); }); it('should require `fileCreatedAt`', async () => { @@ -112,7 +116,9 @@ describe(AssetMediaController.name, () => { .field({ ...makeUploadDto({ omit: 'fileCreatedAt' }) }); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(['fileCreatedAt must be a Date instance', 'fileCreatedAt should not be empty']), + factory.responses.badRequest([ + '[fileCreatedAt] Invalid input: expected ISO 8601 datetime string, received undefined', + ]), ); }); @@ -123,7 +129,9 @@ describe(AssetMediaController.name, () => { .field(makeUploadDto({ omit: 'fileModifiedAt' })); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(['fileModifiedAt must be a Date instance', 'fileModifiedAt should not be empty']), + factory.responses.badRequest([ + '[fileModifiedAt] Invalid input: expected ISO 8601 datetime string, received undefined', + ]), ); }); @@ -133,7 +141,9 @@ describe(AssetMediaController.name, () => { .attach('assetData', assetData, filename) .field({ ...makeUploadDto(), isFavorite: 'not-a-boolean' }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['isFavorite must be a boolean value'])); + expect(body).toEqual( + factory.responses.badRequest(['[isFavorite] Invalid option: expected one of "true"|"false"']), + ); }); it('should throw if `visibility` is not an enum', async () => { @@ -143,7 +153,7 @@ describe(AssetMediaController.name, () => { .field({ ...makeUploadDto(), visibility: 'not-an-option' }); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest([expect.stringContaining('visibility must be one of the following values:')]), + factory.responses.badRequest([expect.stringContaining('[visibility] Invalid option: expected one of')]), ); }); diff --git a/server/src/controllers/asset.controller.spec.ts b/server/src/controllers/asset.controller.spec.ts index 69bf1f6443..4a8d4b3582 100644 --- a/server/src/controllers/asset.controller.spec.ts +++ b/server/src/controllers/asset.controller.spec.ts @@ -31,7 +31,7 @@ describe(AssetController.name, () => { .send({ ids: ['123'] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['each value in ids must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[ids.0] Invalid UUID'])); }); it('should require duplicateId to be a string', async () => { @@ -41,7 +41,9 @@ describe(AssetController.name, () => { .send({ ids: [id], duplicateId: true }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['duplicateId must be a string'])); + expect(body).toEqual( + factory.responses.badRequest(['[duplicateId] Invalid input: expected string, received boolean']), + ); }); it('should accept a null duplicateId', async () => { @@ -68,7 +70,7 @@ describe(AssetController.name, () => { .send({ ids: ['123'] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['each value in ids must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[ids.0] Invalid UUID'])); }); }); @@ -81,7 +83,7 @@ describe(AssetController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); @@ -95,7 +97,12 @@ describe(AssetController.name, () => { const { status, body } = await request(ctx.getHttpServer()).put('/assets/copy').send({}); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(expect.arrayContaining(['sourceId must be a UUID', 'targetId must be a UUID'])), + factory.responses.badRequest( + expect.arrayContaining([ + '[sourceId] Invalid input: expected string, received undefined', + '[targetId] Invalid input: expected string, received undefined', + ]), + ), ); }); @@ -118,7 +125,7 @@ describe(AssetController.name, () => { .put('/assets/metadata') .send({ items: [{ assetId: '123', key: 'test', value: {} }] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['items.0.assetId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[items.0.assetId] Invalid UUID']))); }); it('should require a key', async () => { @@ -128,7 +135,7 @@ describe(AssetController.name, () => { expect(status).toBe(400); expect(body).toEqual( factory.responses.badRequest( - expect.arrayContaining(['items.0.key must be a string', 'items.0.key should not be empty']), + expect.arrayContaining(['[items.0.key] Invalid input: expected string, received undefined']), ), ); }); @@ -152,7 +159,7 @@ describe(AssetController.name, () => { .delete('/assets/metadata') .send({ items: [{ assetId: '123', key: 'test' }] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['items.0.assetId must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[items.0.assetId] Invalid UUID']))); }); it('should require a key', async () => { @@ -162,7 +169,7 @@ describe(AssetController.name, () => { expect(status).toBe(400); expect(body).toEqual( factory.responses.badRequest( - expect.arrayContaining(['items.0.key must be a string', 'items.0.key should not be empty']), + expect.arrayContaining(['[items.0.key] Invalid input: expected string, received undefined']), ), ); }); @@ -184,7 +191,7 @@ describe(AssetController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['Invalid input: expected object, received undefined'])); }); it('should reject invalid gps coordinates', async () => { @@ -247,9 +254,7 @@ describe(AssetController.name, () => { it('should not allow count to be a string', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/assets/random?count=ABC'); expect(status).toBe(400); - expect(body).toEqual( - factory.responses.badRequest(['count must be a positive number', 'count must be an integer number']), - ); + expect(body).toEqual(factory.responses.badRequest(['[count] Invalid input: expected number, received NaN'])); }); }); @@ -269,13 +274,13 @@ describe(AssetController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/123/metadata`).send({ items: [] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID']))); }); it('should require items to be an array', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/assets/${factory.uuid()}/metadata`).send({}); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['items must be an array'])); + expect(body).toEqual(factory.responses.badRequest(['[items] Invalid input: expected array, received undefined'])); }); it('should require each item to have a valid key', async () => { @@ -284,7 +289,7 @@ describe(AssetController.name, () => { .send({ items: [{ value: { some: 'value' } }] }); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(['items.0.key must be a string', 'items.0.key should not be empty']), + factory.responses.badRequest(['[items.0.key] Invalid input: expected string, received undefined']), ); }); @@ -294,7 +299,9 @@ describe(AssetController.name, () => { .send({ items: [{ key: 'mobile-app', value: null }] }); expect(status).toBe(400); expect(body).toEqual( - factory.responses.badRequest(expect.arrayContaining([expect.stringContaining('value must be an object')])), + factory.responses.badRequest( + expect.arrayContaining(['[items.0.value] Invalid input: expected record, received null']), + ), ); }); @@ -332,7 +339,7 @@ describe(AssetController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/assets/123/metadata/mobile-app`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID']))); }); }); @@ -382,7 +389,7 @@ describe(AssetController.name, () => { }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['id must be a UUID']))); + expect(body).toEqual(factory.responses.badRequest(expect.arrayContaining(['[id] Invalid UUID']))); }); it('should check the action and parameters discriminator', async () => { @@ -405,7 +412,11 @@ describe(AssetController.name, () => { expect(status).toBe(400); expect(body).toEqual( factory.responses.badRequest( - expect.arrayContaining([expect.stringContaining('parameters.angle must be one of the following values')]), + expect.arrayContaining([ + expect.stringContaining( + "[edits.0.parameters] Invalid parameters for action 'rotate', expecting keys: angle", + ), + ]), ), ); }); @@ -415,7 +426,7 @@ describe(AssetController.name, () => { .put(`/assets/${factory.uuid()}/edits`) .send({ edits: [] }); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['edits must contain at least 1 elements'])); + expect(body).toEqual(factory.responses.badRequest(['[edits] Too small: expected array to have >=1 items'])); }); }); @@ -428,7 +439,7 @@ describe(AssetController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/assets/123/metadata/mobile-app`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/auth.controller.spec.ts b/server/src/controllers/auth.controller.spec.ts index 7dd145ff5c..a61397e75c 100644 --- a/server/src/controllers/auth.controller.spec.ts +++ b/server/src/controllers/auth.controller.spec.ts @@ -74,10 +74,8 @@ describe(AuthController.name, () => { expect(status).toBe(400); expect(body).toEqual( errorDto.badRequest([ - 'email should not be empty', - 'email must be an email', - 'password should not be empty', - 'password must be a string', + '[email] Invalid input: expected email, received undefined', + '[password] Invalid input: expected string, received undefined', ]), ); }); @@ -87,7 +85,7 @@ describe(AuthController.name, () => { .post('/auth/login') .send({ name: 'admin', email: null, password: 'password' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['email should not be empty', 'email must be an email'])); + expect(body).toEqual(errorDto.badRequest(['[email] Invalid input: expected email, received object'])); }); it(`should not allow null password`, async () => { @@ -95,7 +93,7 @@ describe(AuthController.name, () => { .post('/auth/login') .send({ name: 'admin', email: 'admin@immich.cloud', password: null }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['password should not be empty', 'password must be a string'])); + expect(body).toEqual(errorDto.badRequest(['[password] Invalid input: expected string, received null'])); }); it('should reject an invalid email', async () => { @@ -106,7 +104,7 @@ describe(AuthController.name, () => { .send({ name: 'admin', email: [], password: 'password' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['email must be an email'])); + expect(body).toEqual(errorDto.badRequest(['[email] Invalid input: expected email, received object'])); }); it('should transform the email to all lowercase', async () => { @@ -197,19 +195,19 @@ describe(AuthController.name, () => { it('should reject 5 digits', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '12345' }); expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string'])); + expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`])); }); it('should reject 7 digits', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: '1234567' }); expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string'])); + expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`])); }); it('should reject non-numbers', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/auth/pin-code').send({ pinCode: 'A12345' }); expect(status).toEqual(400); - expect(body).toEqual(errorDto.badRequest(['pinCode must be a 6-digit numeric string'])); + expect(body).toEqual(errorDto.badRequest([String.raw`[pinCode] Invalid string: must match pattern /^\d{6}$/`])); }); }); diff --git a/server/src/controllers/duplicate.controller.spec.ts b/server/src/controllers/duplicate.controller.spec.ts index 66598b9920..3e11b628e3 100644 --- a/server/src/controllers/duplicate.controller.spec.ts +++ b/server/src/controllers/duplicate.controller.spec.ts @@ -41,7 +41,7 @@ describe(DuplicateController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/duplicates/123`); expect(status).toBe(400); - expect(body).toEqual(factory.responses.badRequest(['id must be a UUID'])); + expect(body).toEqual(factory.responses.badRequest(['[id] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/maintenance.controller.spec.ts b/server/src/controllers/maintenance.controller.spec.ts index 094028687e..07c0149463 100644 --- a/server/src/controllers/maintenance.controller.spec.ts +++ b/server/src/controllers/maintenance.controller.spec.ts @@ -31,7 +31,7 @@ describe(MaintenanceController.name, () => { }); expect(status).toBe(400); expect(body).toEqual( - errorDto.badRequest(['restoreBackupFilename must be a string', 'restoreBackupFilename should not be empty']), + errorDto.badRequest(['[restoreBackupFilename] Backup filename is required when action is restore_database']), ); expect(ctx.authenticate).toHaveBeenCalled(); }); diff --git a/server/src/controllers/memory.controller.spec.ts b/server/src/controllers/memory.controller.spec.ts index 820819ee6e..4ed32ee271 100644 --- a/server/src/controllers/memory.controller.spec.ts +++ b/server/src/controllers/memory.controller.spec.ts @@ -47,9 +47,7 @@ describe(MemoryController.name, () => { }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest(['data.year must be a positive number', 'data.year must be an integer number']), - ); + expect(body).toEqual(errorDto.badRequest(['[data.year] Invalid input: expected number, received undefined'])); }); it('should accept showAt and hideAt', async () => { @@ -83,7 +81,7 @@ describe(MemoryController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/memories/invalid`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); }); @@ -96,7 +94,7 @@ describe(MemoryController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['Invalid input: expected object, received undefined'])); }); }); @@ -116,7 +114,7 @@ describe(MemoryController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/memories/invalid/assets`).send({ ids: [] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should require a valid asset id', async () => { @@ -124,7 +122,7 @@ describe(MemoryController.name, () => { .put(`/memories/${factory.uuid()}/assets`) .send({ ids: ['invalid'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID'])); }); }); @@ -137,7 +135,7 @@ describe(MemoryController.name, () => { it('should require a valid id', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/memories/invalid/assets`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should require a valid asset id', async () => { @@ -145,7 +143,7 @@ describe(MemoryController.name, () => { .delete(`/memories/${factory.uuid()}/assets`) .send({ ids: ['invalid'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/notification.controller.spec.ts b/server/src/controllers/notification.controller.spec.ts index a64aee2912..e9886ebb07 100644 --- a/server/src/controllers/notification.controller.spec.ts +++ b/server/src/controllers/notification.controller.spec.ts @@ -31,7 +31,7 @@ describe(NotificationController.name, () => { .query({ level: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('level must be one of the following values')])); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[level] Invalid option: expected one of')])); }); }); @@ -45,7 +45,7 @@ describe(NotificationController.name, () => { it('should require a list', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/notifications`).send({ ids: true }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['ids must be an array']))); + expect(body).toEqual(errorDto.badRequest(['[ids] Invalid input: expected array, received boolean'])); }); it('should require uuids', async () => { @@ -53,7 +53,7 @@ describe(NotificationController.name, () => { .put(`/notifications`) .send({ ids: [true] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid input: expected string, received boolean'])); }); it('should accept valid uuids', async () => { @@ -75,7 +75,7 @@ describe(NotificationController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/notifications/123`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); }); diff --git a/server/src/controllers/partner.controller.spec.ts b/server/src/controllers/partner.controller.spec.ts index 2c507a634f..0661e9121b 100644 --- a/server/src/controllers/partner.controller.spec.ts +++ b/server/src/controllers/partner.controller.spec.ts @@ -33,10 +33,7 @@ describe(PartnerController.name, () => { const { status, body } = await request(ctx.getHttpServer()).get(`/partners`).set('Authorization', `Bearer token`); expect(status).toBe(400); expect(body).toEqual( - errorDto.badRequest([ - 'direction should not be empty', - expect.stringContaining('direction must be one of the following values:'), - ]), + errorDto.badRequest([expect.stringContaining('[direction] Invalid option: expected one of')]), ); }); @@ -47,7 +44,7 @@ describe(PartnerController.name, () => { .set('Authorization', `Bearer token`); expect(status).toBe(400); expect(body).toEqual( - errorDto.badRequest([expect.stringContaining('direction must be one of the following values:')]), + errorDto.badRequest([expect.stringContaining('[direction] Invalid option: expected one of')]), ); }); }); @@ -64,7 +61,7 @@ describe(PartnerController.name, () => { .send({ sharedWithId: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[sharedWithId] Invalid UUID'])); }); }); @@ -80,7 +77,7 @@ describe(PartnerController.name, () => { .send({ inTimeline: true }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); }); @@ -95,7 +92,7 @@ describe(PartnerController.name, () => { .delete(`/partners/invalid`) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); }); }); diff --git a/server/src/controllers/person.controller.spec.ts b/server/src/controllers/person.controller.spec.ts index a28ac9b659..c6c0a1c91f 100644 --- a/server/src/controllers/person.controller.spec.ts +++ b/server/src/controllers/person.controller.spec.ts @@ -35,7 +35,7 @@ describe(PersonController.name, () => { .query({ closestPersonId: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[closestPersonId] Invalid UUID'])); }); it(`should require closestAssetId to be a uuid`, async () => { @@ -44,7 +44,7 @@ describe(PersonController.name, () => { .query({ closestAssetId: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[closestAssetId] Invalid UUID'])); }); }); @@ -76,7 +76,7 @@ describe(PersonController.name, () => { .delete('/people') .send({ ids: ['invalid'] }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[ids.0] Invalid UUID'])); }); it('should respond with 204', async () => { @@ -104,7 +104,7 @@ describe(PersonController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).put(`/people/123`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['Invalid input: expected object, received undefined'])); }); it(`should not allow a null name`, async () => { @@ -113,7 +113,7 @@ describe(PersonController.name, () => { .send({ name: null }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['name must be a string'])); + expect(body).toEqual(errorDto.badRequest(['[name] Invalid input: expected string, received null'])); }); it(`should require featureFaceAssetId to be a uuid`, async () => { @@ -122,7 +122,7 @@ describe(PersonController.name, () => { .send({ featureFaceAssetId: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['featureFaceAssetId must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['[featureFaceAssetId] Invalid UUID'])); }); it(`should require isFavorite to be a boolean`, async () => { @@ -131,7 +131,7 @@ describe(PersonController.name, () => { .send({ isFavorite: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isFavorite must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isFavorite] Invalid input: expected boolean, received string'])); }); it(`should require isHidden to be a boolean`, async () => { @@ -140,7 +140,7 @@ describe(PersonController.name, () => { .send({ isHidden: 'invalid' }) .set('Authorization', `Bearer token`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isHidden must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isHidden] Invalid input: expected boolean, received string'])); }); it('should map an empty birthDate to null', async () => { @@ -154,12 +154,7 @@ describe(PersonController.name, () => { .put(`/people/${factory.uuid()}`) .send({ birthDate: false }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([ - 'birthDate must be a string in the format yyyy-MM-dd', - 'Birth date cannot be in the future', - ]), - ); + expect(body).toEqual(errorDto.badRequest(['[birthDate] Invalid input: expected string, received boolean'])); }); it('should not accept an invalid birth date (number)', async () => { @@ -167,12 +162,7 @@ describe(PersonController.name, () => { .put(`/people/${factory.uuid()}`) .send({ birthDate: 123_456 }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([ - 'birthDate must be a string in the format yyyy-MM-dd', - 'Birth date cannot be in the future', - ]), - ); + expect(body).toEqual(errorDto.badRequest(['[birthDate] Invalid input: expected string, received number'])); }); it('should not accept a birth date in the future)', async () => { @@ -180,7 +170,7 @@ describe(PersonController.name, () => { .put(`/people/${factory.uuid()}`) .send({ birthDate: '9999-01-01' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['Birth date cannot be in the future'])); + expect(body).toEqual(errorDto.badRequest(['[birthDate] Birth date cannot be in the future'])); }); }); @@ -193,7 +183,7 @@ describe(PersonController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).delete(`/people/invalid`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); it('should respond with 204', async () => { diff --git a/server/src/controllers/search.controller.spec.ts b/server/src/controllers/search.controller.spec.ts index adbc8be0f3..4df247031a 100644 --- a/server/src/controllers/search.controller.spec.ts +++ b/server/src/controllers/search.controller.spec.ts @@ -27,37 +27,31 @@ describe(SearchController.name, () => { it('should reject page as a string', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 'abc' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['page must not be less than 1', 'page must be an integer number'])); + expect(body).toEqual(errorDto.badRequest(['[page] Invalid input: expected number, received string'])); }); it('should reject page as a negative number', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: -10 }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['page must not be less than 1'])); + expect(body).toEqual(errorDto.badRequest(['[page] Too small: expected number to be >=1'])); }); it('should reject page as 0', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ page: 0 }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['page must not be less than 1'])); + expect(body).toEqual(errorDto.badRequest(['[page] Too small: expected number to be >=1'])); }); it('should reject size as a string', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: 'abc' }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([ - 'size must not be greater than 1000', - 'size must not be less than 1', - 'size must be an integer number', - ]), - ); + expect(body).toEqual(errorDto.badRequest(['[size] Invalid input: expected number, received string'])); }); it('should reject an invalid size', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ size: -1.5 }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['size must not be less than 1', 'size must be an integer number'])); + expect(body).toEqual(errorDto.badRequest(['[size] Too small: expected number to be >=1'])); }); it('should reject an visibility as not an enum', async () => { @@ -66,7 +60,7 @@ describe(SearchController.name, () => { .send({ visibility: 'immich' }); expect(status).toBe(400); expect(body).toEqual( - errorDto.badRequest(['visibility must be one of the following values: archive, timeline, hidden, locked']), + errorDto.badRequest([expect.stringContaining('[visibility] Invalid option: expected one of')]), ); }); @@ -75,7 +69,7 @@ describe(SearchController.name, () => { .post('/search/metadata') .send({ isFavorite: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isFavorite must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isFavorite] Invalid input: expected boolean, received string'])); }); it('should reject an isEncoded as not a boolean', async () => { @@ -83,7 +77,7 @@ describe(SearchController.name, () => { .post('/search/metadata') .send({ isEncoded: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isEncoded must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isEncoded] Invalid input: expected boolean, received string'])); }); it('should reject an isOffline as not a boolean', async () => { @@ -91,13 +85,13 @@ describe(SearchController.name, () => { .post('/search/metadata') .send({ isOffline: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isOffline must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isOffline] Invalid input: expected boolean, received string'])); }); it('should reject an isMotion as not a boolean', async () => { const { status, body } = await request(ctx.getHttpServer()).post('/search/metadata').send({ isMotion: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['isMotion must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[isMotion] Invalid input: expected boolean, received string'])); }); describe('POST /search/random', () => { @@ -111,7 +105,7 @@ describe(SearchController.name, () => { .post('/search/random') .send({ withStacked: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['withStacked must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[withStacked] Invalid input: expected boolean, received string'])); }); it('should reject if withPeople is not a boolean', async () => { @@ -119,7 +113,7 @@ describe(SearchController.name, () => { .post('/search/random') .send({ withPeople: 'immich' }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['withPeople must be a boolean value'])); + expect(body).toEqual(errorDto.badRequest(['[withPeople] Invalid input: expected boolean, received string'])); }); }); @@ -146,7 +140,7 @@ describe(SearchController.name, () => { it('should require a name', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/person').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); + expect(body).toEqual(errorDto.badRequest(['[name] Invalid input: expected string, received undefined'])); }); }); @@ -159,7 +153,7 @@ describe(SearchController.name, () => { it('should require a name', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/places').send({}); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['name should not be empty', 'name must be a string'])); + expect(body).toEqual(errorDto.badRequest(['[name] Invalid input: expected string, received undefined'])); }); }); @@ -179,12 +173,7 @@ describe(SearchController.name, () => { it('should require a type', async () => { const { status, body } = await request(ctx.getHttpServer()).get('/search/suggestions').send({}); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([ - 'type should not be empty', - expect.stringContaining('type must be one of the following values:'), - ]), - ); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[type] Invalid option: expected one of')])); }); }); }); diff --git a/server/src/controllers/sync.controller.spec.ts b/server/src/controllers/sync.controller.spec.ts index c1f19ddd66..07b0d7199f 100644 --- a/server/src/controllers/sync.controller.spec.ts +++ b/server/src/controllers/sync.controller.spec.ts @@ -35,9 +35,7 @@ describe(SyncController.name, () => { .post('/sync/stream') .send({ types: ['invalid'] }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([expect.stringContaining('each value in types must be one of the following values')]), - ); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[types.0] Invalid option: expected one of')])); expect(ctx.authenticate).toHaveBeenCalled(); }); }); @@ -59,7 +57,7 @@ describe(SyncController.name, () => { const acks = Array.from({ length: 1001 }, (_, i) => `ack-${i}`); const { status, body } = await request(ctx.getHttpServer()).post('/sync/ack').send({ acks }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['acks must contain no more than 1000 elements'])); + expect(body).toEqual(errorDto.badRequest(['[acks] Too big: expected array to have <=1000 items'])); expect(ctx.authenticate).toHaveBeenCalled(); }); }); @@ -75,9 +73,7 @@ describe(SyncController.name, () => { .delete('/sync/ack') .send({ types: ['invalid'] }); expect(status).toBe(400); - expect(body).toEqual( - errorDto.badRequest([expect.stringContaining('each value in types must be one of the following values')]), - ); + expect(body).toEqual(errorDto.badRequest([expect.stringContaining('[types.0] Invalid option: expected one of')])); expect(ctx.authenticate).toHaveBeenCalled(); }); }); diff --git a/server/src/controllers/system-config.controller.spec.ts b/server/src/controllers/system-config.controller.spec.ts index bbd1241dc5..a07dee64ad 100644 --- a/server/src/controllers/system-config.controller.spec.ts +++ b/server/src/controllers/system-config.controller.spec.ts @@ -7,6 +7,20 @@ import request from 'supertest'; import { errorDto } from 'test/medium/responses'; import { ControllerContext, controllerSetup, mockBaseService } from 'test/utils'; +/** Returns a full config that passes Zod validation (required URLs and min lengths). */ +function validConfig() { + const config = _.cloneDeep(defaults) as typeof defaults & { + oauth: { mobileRedirectUri: string }; + notifications: { smtp: { from: string; transport: { host: string } } }; + server: { externalDomain: string }; + }; + config.oauth.mobileRedirectUri = config.oauth.mobileRedirectUri || 'https://example.com'; + config.server.externalDomain = config.server.externalDomain || 'https://example.com'; + config.notifications.smtp.from = config.notifications.smtp.from || 'noreply@example.com'; + config.notifications.smtp.transport.host = config.notifications.smtp.transport.host || 'localhost'; + return config; +} + describe(SystemConfigController.name, () => { let ctx: ControllerContext; const systemConfigService = mockBaseService(SystemConfigService); @@ -48,32 +62,38 @@ describe(SystemConfigController.name, () => { describe('nightlyTasks', () => { it('should validate nightly jobs start time', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); config.nightlyTasks.startTime = 'invalid'; const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['nightlyTasks.startTime must be in HH:mm format'])); + expect(body).toEqual( + errorDto.badRequest([ + '[nightlyTasks.startTime] Invalid input: expected string in HH:mm format, received string', + ]), + ); }); it('should accept a valid time', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); config.nightlyTasks.startTime = '05:05'; const { status } = await request(ctx.getHttpServer()).put('/system-config').send(config); expect(status).toBe(200); }); it('should validate a boolean field', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); (config.nightlyTasks.databaseCleanup as any) = 'invalid'; const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['nightlyTasks.databaseCleanup must be a boolean value'])); + expect(body).toEqual( + errorDto.badRequest(['[nightlyTasks.databaseCleanup] Invalid input: expected boolean, received string']), + ); }); }); describe('image', () => { it('should accept config without optional progressive property', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); delete config.image.thumbnail.progressive; delete config.image.preview.progressive; delete config.image.fullsize.progressive; @@ -82,7 +102,7 @@ describe(SystemConfigController.name, () => { }); it('should accept config with progressive set to true', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); config.image.thumbnail.progressive = true; config.image.preview.progressive = true; config.image.fullsize.progressive = true; @@ -91,11 +111,13 @@ describe(SystemConfigController.name, () => { }); it('should reject invalid progressive value', async () => { - const config = _.cloneDeep(defaults); + const config = validConfig(); (config.image.thumbnail.progressive as any) = 'invalid'; const { status, body } = await request(ctx.getHttpServer()).put('/system-config').send(config); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['image.thumbnail.progressive must be a boolean value'])); + expect(body).toEqual( + errorDto.badRequest(['[image.thumbnail.progressive] Invalid input: expected boolean, received string']), + ); }); }); }); diff --git a/server/src/controllers/tag.controller.spec.ts b/server/src/controllers/tag.controller.spec.ts index 60fc3d65ae..edd0f27980 100644 --- a/server/src/controllers/tag.controller.spec.ts +++ b/server/src/controllers/tag.controller.spec.ts @@ -54,7 +54,7 @@ describe(TagController.name, () => { it('should require a valid uuid', async () => { const { status, body } = await request(ctx.getHttpServer()).get(`/tags/123`); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest([expect.stringContaining('id must be a UUID')])); + expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID'])); }); }); diff --git a/server/src/controllers/timeline.controller.spec.ts b/server/src/controllers/timeline.controller.spec.ts index 6d0276c6a3..f4c18235e4 100644 --- a/server/src/controllers/timeline.controller.spec.ts +++ b/server/src/controllers/timeline.controller.spec.ts @@ -23,6 +23,36 @@ describe(TimelineController.name, () => { await request(ctx.getHttpServer()).get('/timeline/buckets'); expect(ctx.authenticate).toHaveBeenCalled(); }); + + it('should parse bbox query string into an object', async () => { + const { status } = await request(ctx.getHttpServer()) + .get('/timeline/buckets') + .query({ bbox: '11.075683,49.416711,11.117589,49.454875' }); + + expect(status).toBe(200); + expect(service.getTimeBuckets).toHaveBeenCalledWith( + undefined, + expect.objectContaining({ + bbox: { west: 11.075_683, south: 49.416_711, east: 11.117_589, north: 49.454_875 }, + }), + ); + }); + + it('should reject incomplete bbox query string', async () => { + const { status, body } = await request(ctx.getHttpServer()).get('/timeline/buckets').query({ bbox: '1,2,3' }); + expect(status).toBe(400); + expect(body).toEqual( + errorDto.badRequest(['[bbox] bbox must have 4 comma-separated numbers: west,south,east,north'] as any), + ); + }); + + it('should reject invalid bbox query string', async () => { + const { status, body } = await request(ctx.getHttpServer()) + .get('/timeline/buckets') + .query({ bbox: '1,2,3,invalid' }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['[bbox] bbox parts must be valid numbers'] as any)); + }); }); describe('GET /timeline/bucket', () => { diff --git a/server/src/controllers/user-admin.controller.spec.ts b/server/src/controllers/user-admin.controller.spec.ts index edda974476..048f94df5a 100644 --- a/server/src/controllers/user-admin.controller.spec.ts +++ b/server/src/controllers/user-admin.controller.spec.ts @@ -77,7 +77,11 @@ describe(UserAdminController.name, () => { .set('Authorization', `Bearer token`) .send(dto); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + expect(body).toEqual( + errorDto.badRequest( + expect.arrayContaining(['[quotaSizeInBytes] Invalid input: expected int, received number']), + ), + ); }); it(`should not allow decimal quota`, async () => { @@ -93,7 +97,11 @@ describe(UserAdminController.name, () => { .set('Authorization', `Bearer token`) .send(dto); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + expect(body).toEqual( + errorDto.badRequest( + expect.arrayContaining(['[quotaSizeInBytes] Invalid input: expected int, received number']), + ), + ); }); }); @@ -116,7 +124,11 @@ describe(UserAdminController.name, () => { .set('Authorization', `Bearer token`) .send({ quotaSizeInBytes: 1.2 }); expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['quotaSizeInBytes must be an integer number']))); + expect(body).toEqual( + errorDto.badRequest( + expect.arrayContaining(['[quotaSizeInBytes] Invalid input: expected int, received number']), + ), + ); }); it('should allow a null pinCode', async () => { diff --git a/server/src/database.ts b/server/src/database.ts index 4f339624e6..f8065ffd2c 100644 --- a/server/src/database.ts +++ b/server/src/database.ts @@ -104,7 +104,7 @@ export type Memory = { showAt: Date | null; hideAt: Date | null; type: MemoryType; - data: object; + data: Record; ownerId: string; isSaved: boolean; assets: ShallowDehydrateObject[]; diff --git a/server/src/dtos/activity.dto.ts b/server/src/dtos/activity.dto.ts index 6464d88508..7b8ba34c91 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,76 +1,68 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Activity } from 'src/database'; -import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { ValidateEnum, ValidateUUID } from 'src/validation'; - -export enum ReactionType { - COMMENT = 'comment', - LIKE = 'like', -} +import { mapUser, UserResponseSchema } from 'src/dtos/user.dto'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; export enum ReactionLevel { ALBUM = 'album', ASSET = 'asset', } +const ReactionLevelSchema = z.enum(ReactionLevel).describe('Reaction level').meta({ id: 'ReactionLevel' }); + +export enum ReactionType { + COMMENT = 'comment', + LIKE = 'like', +} +const ReactionTypeSchema = z.enum(ReactionType).describe('Reaction type').meta({ id: 'ReactionType' }); export type MaybeDuplicate = { 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', 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; -} +const ActivityResponseSchema = z + .object({ + id: z.uuidv4().describe('Activity ID'), + createdAt: isoDatetimeToDate.describe('Creation date'), + user: UserResponseSchema, + assetId: z.uuidv4().nullable().describe('Asset ID (if activity is for an asset)'), + type: ReactionTypeSchema, + comment: z.string().nullish().describe('Comment text (for comment activities)'), + }) + .meta({ id: 'ActivityResponseDto' }); -export class ActivityStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of comments' }) - comments!: number; +const ActivityStatisticsResponseSchema = z + .object({ + comments: z.int().min(0).describe('Number of comments'), + likes: z.int().min(0).describe('Number of likes'), + }) + .meta({ id: 'ActivityStatisticsResponseDto' }); - @ApiProperty({ type: 'integer', description: 'Number of likes' }) - likes!: number; -} +const ActivitySchema = z + .object({ + albumId: z.uuidv4().describe('Album ID'), + assetId: z.uuidv4().optional().describe('Asset ID (if activity is for an asset)'), + }) + .describe('Activity'); -export class ActivityDto { - @ValidateUUID({ description: 'Album ID' }) - albumId!: string; +const ActivitySearchSchema = ActivitySchema.extend({ + type: ReactionTypeSchema.optional(), + level: ReactionLevelSchema.optional(), + userId: z.uuidv4().optional().describe('Filter by user ID'), +}).describe('Activity search'); - @ValidateUUID({ optional: true, description: 'Asset ID (if activity is for an asset)' }) - assetId?: string; -} - -export class ActivitySearchDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Filter by activity type', optional: true }) - type?: ReactionType; - - @ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', description: 'Filter by activity level', optional: true }) - level?: ReactionLevel; - - @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', description: 'Activity type (like or comment)' }) - type!: ReactionType; - - @ApiPropertyOptional({ description: 'Comment text (required if type is comment)' }) - @ValidateIf(isComment) - @IsNotEmpty() - @IsString() - comment?: string; -} +const ActivityCreateSchema = ActivitySchema.extend({ + type: ReactionTypeSchema, + assetId: z.uuidv4().optional().describe('Asset ID (if activity is for an asset)'), + comment: z.string().optional().describe('Comment text (required if type is comment)'), +}) + .refine((data) => data.type !== ReactionType.COMMENT || (data.comment && data.comment.trim() !== ''), { + error: 'Comment is required when type is COMMENT', + path: ['comment'], + }) + .refine((data) => data.type === ReactionType.COMMENT || !data.comment, { + error: 'Comment must not be provided when type is not COMMENT', + path: ['comment'], + }) + .describe('Activity create'); export const mapActivity = (activity: Activity): ActivityResponseDto => { return { @@ -82,3 +74,9 @@ export const mapActivity = (activity: Activity): ActivityResponseDto => { user: mapUser(activity.user), }; }; + +export class ActivityResponseDto extends createZodDto(ActivityResponseSchema) {} +export class ActivityCreateDto extends createZodDto(ActivityCreateSchema) {} +export class ActivityDto extends createZodDto(ActivitySchema) {} +export class ActivitySearchDto extends createZodDto(ActivitySearchSchema) {} +export class ActivityStatisticsResponseDto extends createZodDto(ActivityStatisticsResponseSchema) {} diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index b270125b36..1514809838 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,196 +1,158 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator'; import { ShallowDehydrateObject } from 'kysely'; import _ from 'lodash'; +import { createZodDto } from 'nestjs-zod'; import { AlbumUser, AuthSharedLink, User } from 'src/database'; -import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; +import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto'; +import { AssetResponseSchema, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { AlbumUserRole, AssetOrder } from 'src/enum'; +import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; +import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum'; import { MaybeDehydrated } from 'src/types'; import { asDateString } from 'src/utils/date'; -import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; +import { stringToBool } from 'src/validation'; +import z from 'zod'; -export class AlbumInfoDto { - @ValidateBoolean({ optional: true, description: 'Exclude assets from response' }) - withoutAssets?: boolean; -} - -export class AlbumUserAddDto { - @ValidateUUID({ description: 'User ID' }) - userId!: string; - - @ValidateEnum({ - enum: AlbumUserRole, - name: 'AlbumUserRole', - description: 'Album user role', - default: AlbumUserRole.Editor, +const AlbumInfoSchema = z + .object({ + withoutAssets: stringToBool.optional().describe('Exclude assets from response'), }) - role?: AlbumUserRole; -} + .meta({ id: 'AlbumInfoDto' }); -export class AddUsersDto { - @ApiProperty({ description: 'Album users to add' }) - @ArrayNotEmpty() - albumUsers!: AlbumUserAddDto[]; -} - -export class AlbumUserCreateDto { - @ValidateUUID({ description: 'User ID' }) - userId!: string; - - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} - -export class CreateAlbumDto { - @ApiProperty({ description: 'Album name' }) - @IsString() - 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, description: 'Initial asset IDs' }) - assetIds?: string[]; -} - -export class AlbumsAddAssetsDto { - @ValidateUUID({ each: true, description: 'Album IDs' }) - albumIds!: string[]; - - @ValidateUUID({ each: true, description: 'Asset IDs' }) - assetIds!: string[]; -} - -export class AlbumsAddAssetsResponseDto { - @ApiProperty({ description: 'Operation success' }) - success!: boolean; - @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, description: 'Album thumbnail asset ID' }) - albumThumbnailAssetId?: string; - - @ValidateBoolean({ optional: true, description: 'Enable activity feed' }) - isActivityEnabled?: boolean; - - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true }) - order?: AssetOrder; -} - -export class GetAlbumsDto { - @ValidateBoolean({ - optional: true, - description: 'Filter by shared status: true = only shared, false = not shared, undefined = all owned albums', +const AlbumUserAddSchema = z + .object({ + userId: z.uuidv4().describe('User ID'), + role: AlbumUserRoleSchema.default(AlbumUserRole.Editor).optional().describe('Album user role'), }) - shared?: boolean; + .meta({ id: 'AlbumUserAddDto' }); - @ValidateUUID({ optional: true, description: 'Filter albums containing this asset ID (ignores shared parameter)' }) - assetId?: string; -} +const AddUsersSchema = z + .object({ + albumUsers: z.array(AlbumUserAddSchema).min(1).describe('Album users to add'), + }) + .meta({ id: 'AddUsersDto' }); -export class AlbumStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of owned albums' }) - owned!: number; +const AlbumUserCreateSchema = z + .object({ + userId: z.uuidv4().describe('User ID'), + role: AlbumUserRoleSchema, + }) + .meta({ id: 'AlbumUserCreateDto' }); - @ApiProperty({ type: 'integer', description: 'Number of shared albums' }) - shared!: number; +const CreateAlbumSchema = z + .object({ + albumName: z.string().describe('Album name'), + description: z.string().optional().describe('Album description'), + albumUsers: z.array(AlbumUserCreateSchema).optional().describe('Album users'), + assetIds: z.array(z.uuidv4()).optional().describe('Initial asset IDs'), + }) + .meta({ id: 'CreateAlbumDto' }); - @ApiProperty({ type: 'integer', description: 'Number of non-shared albums' }) - notShared!: number; -} +const AlbumsAddAssetsSchema = z + .object({ + albumIds: z.array(z.uuidv4()).describe('Album IDs'), + assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + }) + .meta({ id: 'AlbumsAddAssetsDto' }); -export class UpdateAlbumUserDto { - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} +const AlbumsAddAssetsResponseSchema = z + .object({ + success: z.boolean().describe('Operation success'), + error: BulkIdErrorReasonSchema.optional(), + }) + .meta({ id: 'AlbumsAddAssetsResponseDto' }); -export class AlbumUserResponseDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - user!: UserResponseDto; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} +const UpdateAlbumSchema = z + .object({ + albumName: z.string().optional().describe('Album name'), + description: z.string().optional().describe('Album description'), + albumThumbnailAssetId: z.uuidv4().optional().describe('Album thumbnail asset ID'), + isActivityEnabled: z.boolean().optional().describe('Enable activity feed'), + order: AssetOrderSchema.optional(), + }) + .meta({ id: 'UpdateAlbumDto' }); -export class ContributorCountResponseDto { - @ApiProperty({ description: 'User ID' }) - userId!: string; +const GetAlbumsSchema = z + .object({ + shared: stringToBool + .optional() + .describe('Filter by shared status: true = only shared, false = not shared, undefined = all owned albums'), + assetId: z.uuidv4().optional().describe('Filter albums containing this asset ID (ignores shared parameter)'), + }) + .meta({ id: 'GetAlbumsDto' }); - @ApiProperty({ type: 'integer', description: 'Number of assets contributed' }) - assetCount!: number; -} +const AlbumStatisticsResponseSchema = z + .object({ + owned: z.int().min(0).describe('Number of owned albums'), + shared: z.int().min(0).describe('Number of shared albums'), + notShared: z.int().min(0).describe('Number of non-shared albums'), + }) + .meta({ id: 'AlbumStatisticsResponseDto' }); -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', format: 'date-time' }) - createdAt!: string; - @ApiProperty({ description: 'Last update date', format: 'date-time' }) - updatedAt!: string; - @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', description: 'Number of assets' }) - assetCount!: number; - @ApiPropertyOptional({ description: 'Last modified asset timestamp', format: 'date-time' }) - lastModifiedAssetTimestamp?: string; - @ApiPropertyOptional({ description: 'Start date (earliest asset)', format: 'date-time' }) - startDate?: string; - @ApiPropertyOptional({ description: 'End date (latest asset)', format: 'date-time' }) - endDate?: string; - @ApiProperty({ description: 'Activity feed enabled' }) - isActivityEnabled!: boolean; - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true }) - order?: AssetOrder; +const UpdateAlbumUserSchema = z + .object({ + role: AlbumUserRoleSchema, + }) + .meta({ id: 'UpdateAlbumUserDto' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Type(() => ContributorCountResponseDto) - contributorCounts?: ContributorCountResponseDto[]; -} +const AlbumUserResponseSchema = z + .object({ + user: UserResponseSchema, + role: AlbumUserRoleSchema, + }) + .meta({ id: 'AlbumUserResponseDto' }); + +const ContributorCountResponseSchema = z + .object({ + userId: z.string().describe('User ID'), + assetCount: z.int().min(0).describe('Number of assets contributed'), + }) + .meta({ id: 'ContributorCountResponseDto' }); + +export const AlbumResponseSchema = z + .object({ + id: z.string().describe('Album ID'), + ownerId: z.string().describe('Owner user ID'), + albumName: z.string().describe('Album name'), + description: z.string().describe('Album description'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'), + albumThumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'), + shared: z.boolean().describe('Is shared album'), + albumUsers: z.array(AlbumUserResponseSchema), + hasSharedLink: z.boolean().describe('Has shared link'), + assets: z.array(AssetResponseSchema), + owner: UserResponseSchema, + assetCount: z.int().min(0).describe('Number of assets'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + lastModifiedAssetTimestamp: z + .string() + .meta({ format: 'date-time' }) + .optional() + .describe('Last modified asset timestamp'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + startDate: z.string().meta({ format: 'date-time' }).optional().describe('Start date (earliest asset)'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + endDate: z.string().meta({ format: 'date-time' }).optional().describe('End date (latest asset)'), + isActivityEnabled: z.boolean().describe('Activity feed enabled'), + order: AssetOrderSchema.optional(), + contributorCounts: z.array(ContributorCountResponseSchema).optional(), + }) + .meta({ id: 'AlbumResponseDto' }); + +export class AlbumInfoDto extends createZodDto(AlbumInfoSchema) {} +export class AddUsersDto extends createZodDto(AddUsersSchema) {} +export class AlbumUserCreateDto extends createZodDto(AlbumUserCreateSchema) {} +export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {} +export class AlbumsAddAssetsDto extends createZodDto(AlbumsAddAssetsSchema) {} +export class AlbumsAddAssetsResponseDto extends createZodDto(AlbumsAddAssetsResponseSchema) {} +export class UpdateAlbumDto extends createZodDto(UpdateAlbumSchema) {} +export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {} +export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {} +export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {} +export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {} +class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {} export type MapAlbumDto = { albumUsers?: AlbumUser[]; diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index 273082c41b..8c1ffb53ca 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -1,55 +1,42 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ArrayMinSize, IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Permission } from 'src/enum'; -import { Optional, ValidateEnum } from 'src/validation'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class APIKeyCreateDto { - @ApiPropertyOptional({ description: 'API key name' }) - @IsString() - @IsNotEmpty() - @Optional() - name?: string; +const PermissionSchema = z.enum(Permission).describe('List of permissions').meta({ id: 'Permission' }); - @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', - description: 'List of permissions', - each: true, - optional: true, +const APIKeyCreateSchema = z + .object({ + name: z.string().optional().describe('API key name'), + permissions: z.array(PermissionSchema).min(1).describe('List of permissions'), }) - @ArrayMinSize(1) - permissions?: Permission[]; -} + .meta({ id: 'APIKeyCreateDto' }); -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, description: 'List of permissions' }) - permissions!: Permission[]; -} +const APIKeyUpdateSchema = z + .object({ + name: z.string().optional().describe('API key name'), + permissions: z.array(PermissionSchema).min(1).optional().describe('List of permissions'), + }) + .meta({ id: 'APIKeyUpdateDto' }); -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; -} +const APIKeyResponseSchema = z + .object({ + id: z.string().describe('API key ID'), + name: z.string().describe('API key name'), + createdAt: isoDatetimeToDate.describe('Creation date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + permissions: z.array(PermissionSchema).describe('List of permissions'), + }) + .meta({ id: 'APIKeyResponseDto' }); + +const APIKeyCreateResponseSchema = z + .object({ + secret: z.string().describe('API key secret (only shown once)'), + apiKey: APIKeyResponseSchema, + }) + .meta({ id: 'APIKeyCreateResponseDto' }); + +export class APIKeyCreateDto extends createZodDto(APIKeyCreateSchema) {} +export class APIKeyUpdateDto extends createZodDto(APIKeyUpdateSchema) {} +export class APIKeyResponseDto extends createZodDto(APIKeyResponseSchema) {} +export class APIKeyCreateResponseDto extends createZodDto(APIKeyCreateResponseSchema) {} diff --git a/server/src/dtos/asset-ids.response.dto.ts b/server/src/dtos/asset-ids.response.dto.ts index 1065d8485e..346829e644 100644 --- a/server/src/dtos/asset-ids.response.dto.ts +++ b/server/src/dtos/asset-ids.response.dto.ts @@ -1,5 +1,5 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; /** @deprecated Use `BulkIdResponseDto` instead */ export enum AssetIdErrorReason { @@ -8,15 +8,19 @@ export enum AssetIdErrorReason { NOT_FOUND = 'not_found', } +const AssetIdErrorReasonSchema = z + .enum(AssetIdErrorReason) + .describe('Error reason if failed') + .meta({ id: '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; -} +const AssetIdsResponseSchema = z + .object({ + assetId: z.string().describe('Asset ID'), + success: z.boolean().describe('Whether operation succeeded'), + error: AssetIdErrorReasonSchema.optional(), + }) + .meta({ id: 'AssetIdsResponseDto' }); export enum BulkIdErrorReason { DUPLICATE = 'duplicate', @@ -26,17 +30,27 @@ export enum BulkIdErrorReason { VALIDATION = 'validation', } -export class BulkIdsDto { - @ValidateUUID({ each: true, description: 'IDs to process' }) - ids!: string[]; -} +export const BulkIdErrorReasonSchema = z + .enum(BulkIdErrorReason) + .describe('Error reason') + .meta({ id: 'BulkIdErrorReason' }); -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; - errorMessage?: string; -} +export const BulkIdsSchema = z + .object({ + ids: z.array(z.uuidv4()).describe('IDs to process'), + }) + .meta({ id: 'BulkIdsDto' }); + +const BulkIdResponseSchema = z + .object({ + id: z.string().describe('ID'), + success: z.boolean().describe('Whether operation succeeded'), + error: BulkIdErrorReasonSchema.optional(), + errorMessage: z.string().optional(), + }) + .meta({ id: 'BulkIdResponseDto' }); + +/** @deprecated Use `BulkIdResponseDto` instead */ +export class AssetIdsResponseDto extends createZodDto(AssetIdsResponseSchema) {} +export class BulkIdsDto extends createZodDto(BulkIdsSchema) {} +export class BulkIdResponseDto extends createZodDto(BulkIdResponseSchema) {} diff --git a/server/src/dtos/asset-media-response.dto.ts b/server/src/dtos/asset-media-response.dto.ts index 345c1bf418..fa3c472765 100644 --- a/server/src/dtos/asset-media-response.dto.ts +++ b/server/src/dtos/asset-media-response.dto.ts @@ -1,47 +1,60 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ValidateEnum } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; export enum AssetMediaStatus { CREATED = 'created', REPLACED = 'replaced', DUPLICATE = 'duplicate', } -export class AssetMediaResponseDto { - @ValidateEnum({ enum: AssetMediaStatus, name: 'AssetMediaStatus', description: 'Upload status' }) - status!: AssetMediaStatus; - @ApiProperty({ description: 'Asset media ID' }) - id!: string; -} + +const AssetMediaStatusSchema = z.enum(AssetMediaStatus).describe('Upload status').meta({ id: 'AssetMediaStatus' }); + +const AssetMediaResponseSchema = z + .object({ + status: AssetMediaStatusSchema, + id: z.string().describe('Asset media ID'), + }) + .meta({ id: 'AssetMediaResponseDto' }); export enum AssetUploadAction { ACCEPT = 'accept', REJECT = 'reject', } +const AssetUploadActionSchema = z.enum(AssetUploadAction).describe('Upload action').meta({ id: 'AssetUploadAction' }); + export enum AssetRejectReason { DUPLICATE = 'duplicate', UNSUPPORTED_FORMAT = 'unsupported-format', } -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; -} +const AssetRejectReasonSchema = z + .enum(AssetRejectReason) + .describe('Rejection reason if rejected') + .meta({ id: 'AssetRejectReason' }); -export class AssetBulkUploadCheckResponseDto { - @ApiProperty({ description: 'Upload check results' }) - results!: AssetBulkUploadCheckResult[]; -} +const AssetBulkUploadCheckResultSchema = z + .object({ + id: z.string().describe('Asset ID'), + action: AssetUploadActionSchema, + reason: AssetRejectReasonSchema.optional(), + assetId: z.string().optional().describe('Existing asset ID if duplicate'), + isTrashed: z.boolean().optional().describe('Whether existing asset is trashed'), + }) + .meta({ id: 'AssetBulkUploadCheckResult' }); -export class CheckExistingAssetsResponseDto { - @ApiProperty({ description: 'Existing asset IDs' }) - existingIds!: string[]; -} +const AssetBulkUploadCheckResponseSchema = z + .object({ + results: z.array(AssetBulkUploadCheckResultSchema).describe('Upload check results'), + }) + .meta({ id: 'AssetBulkUploadCheckResponseDto' }); + +const CheckExistingAssetsResponseSchema = z + .object({ + existingIds: z.array(z.string()).describe('Existing asset IDs'), + }) + .meta({ id: 'CheckExistingAssetsResponseDto' }); + +export class AssetMediaResponseDto extends createZodDto(AssetMediaResponseSchema) {} +export class AssetBulkUploadCheckResponseDto extends createZodDto(AssetBulkUploadCheckResponseSchema) {} +export class CheckExistingAssetsResponseDto extends createZodDto(CheckExistingAssetsResponseSchema) {} diff --git a/server/src/dtos/asset-media.dto.ts b/server/src/dtos/asset-media.dto.ts index 4655850379..6a4c55c5aa 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,10 +1,8 @@ -import { BadRequestException } from '@nestjs/common'; -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'; -import { AssetVisibility } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { AssetMetadataUpsertItemSchema } from 'src/dtos/asset.dto'; +import { AssetVisibilitySchema } from 'src/enum'; +import { isoDatetimeToDate, JsonParsed, stringToBool } from 'src/validation'; +import z from 'zod'; export enum AssetMediaSize { Original = 'original', @@ -17,13 +15,14 @@ export enum AssetMediaSize { THUMBNAIL = 'thumbnail', } -export class AssetMediaOptionsDto { - @ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', description: 'Asset media size', optional: true }) - size?: AssetMediaSize; +const AssetMediaSizeSchema = z.enum(AssetMediaSize).describe('Asset media size').meta({ id: 'AssetMediaSize' }); - @ValidateBoolean({ optional: true, description: 'Return edited asset if available', default: false }) - edited?: boolean; -} +const AssetMediaOptionsSchema = z + .object({ + size: AssetMediaSizeSchema.optional(), + edited: stringToBool.default(false).optional().describe('Return edited asset if available'), + }) + .meta({ id: 'AssetMediaOptionsDto' }); export enum UploadFieldName { ASSET_DATA = 'assetData', @@ -31,98 +30,53 @@ export enum UploadFieldName { PROFILE_DATA = 'file', } -class AssetMediaBase { - @ApiProperty({ description: 'Device asset ID' }) - @IsNotEmpty() - @IsString() - deviceAssetId!: string; +const AssetMediaBaseSchema = z.object({ + deviceAssetId: z.string().describe('Device asset ID'), + deviceId: z.string().describe('Device ID'), + fileCreatedAt: isoDatetimeToDate.describe('File creation date'), + fileModifiedAt: isoDatetimeToDate.describe('File modification date'), + duration: z.string().optional().describe('Duration (for videos)'), + filename: z.string().optional().describe('Filename'), + /** The properties below are added to correctly generate the API docs and client SDKs. Validation should be handled in the controller. */ + [UploadFieldName.ASSET_DATA]: z.any().describe('Asset file data').meta({ type: 'string', format: 'binary' }), +}); - @ApiProperty({ description: 'Device ID' }) - @IsNotEmpty() - @IsString() - deviceId!: string; +const AssetMediaCreateSchema = AssetMediaBaseSchema.extend({ + isFavorite: stringToBool.optional().describe('Mark as favorite'), + visibility: AssetVisibilitySchema.optional(), + livePhotoVideoId: z.uuidv4().optional().describe('Live photo video ID'), + metadata: JsonParsed.pipe(z.array(AssetMetadataUpsertItemSchema)).optional().describe('Asset metadata items'), + [UploadFieldName.SIDECAR_DATA]: z + .any() + .optional() + .describe('Sidecar file data') + .meta({ type: 'string', format: 'binary' }), +}).meta({ id: 'AssetMediaCreateDto' }); - @ValidateDate({ description: 'File creation date' }) - fileCreatedAt!: Date; +const AssetMediaReplaceSchema = AssetMediaBaseSchema.meta({ id: 'AssetMediaReplaceDto' }); - @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', description: 'Asset file data' }) - [UploadFieldName.ASSET_DATA]!: any; -} - -export class AssetMediaCreateDto extends AssetMediaBase { - @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) - isFavorite?: boolean; - - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility', optional: true }) - visibility?: AssetVisibility; - - @ValidateUUID({ optional: true, description: 'Live photo video ID' }) - livePhotoVideoId?: string; - - @ApiPropertyOptional({ description: 'Asset metadata items' }) - @Transform(({ value }) => { - try { - const json = JSON.parse(value); - const items = Array.isArray(json) ? json : [json]; - return items.map((item) => plainToInstance(AssetMetadataUpsertItemDto, item)); - } catch { - throw new BadRequestException(['metadata must be valid JSON']); - } +const AssetBulkUploadCheckItemSchema = z + .object({ + id: z.string().describe('Asset ID'), + checksum: z.string().describe('Base64 or hex encoded SHA1 hash'), }) - @Optional() - @ValidateNested({ each: true }) - @IsArray() - metadata?: AssetMetadataUpsertItemDto[]; + .meta({ id: 'AssetBulkUploadCheckItem' }); - @ApiProperty({ type: 'string', format: 'binary', required: false, description: 'Sidecar file data' }) - [UploadFieldName.SIDECAR_DATA]?: any; -} +const AssetBulkUploadCheckSchema = z + .object({ + assets: z.array(AssetBulkUploadCheckItemSchema).describe('Assets to check'), + }) + .meta({ id: 'AssetBulkUploadCheckDto' }); -export class AssetMediaReplaceDto extends AssetMediaBase {} +const CheckExistingAssetsSchema = z + .object({ + deviceAssetIds: z.array(z.string()).min(1).describe('Device asset IDs to check'), + deviceId: z.string().describe('Device ID'), + }) + .meta({ id: 'CheckExistingAssetsDto' }); -export class AssetBulkUploadCheckItem { - @ApiProperty({ description: 'Asset ID' }) - @IsString() - @IsNotEmpty() - id!: string; - - @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) - assets!: AssetBulkUploadCheckItem[]; -} - -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; -} +export class AssetMediaOptionsDto extends createZodDto(AssetMediaOptionsSchema) {} +export class AssetMediaCreateDto extends createZodDto(AssetMediaCreateSchema) {} +export class AssetMediaReplaceDto extends createZodDto(AssetMediaReplaceSchema) {} +export class AssetBulkUploadCheckDto extends createZodDto(AssetBulkUploadCheckSchema) {} +export class CheckExistingAssetsDto extends createZodDto(CheckExistingAssetsSchema) {} diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 2c2f57bbb2..a95d2f3c1e 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -1,144 +1,132 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Selectable, ShallowDehydrateObject } from 'kysely'; +import { createZodDto } from 'nestjs-zod'; import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; +import { HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; -import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; +import { ExifResponseSchema, mapExif } from 'src/dtos/exif.dto'; import { - AssetFaceWithoutPersonResponseDto, + AssetFaceWithoutPersonResponseSchema, PersonWithFacesResponseDto, + PersonWithFacesResponseSchema, mapFacesWithoutPerson, mapPerson, } from 'src/dtos/person.dto'; -import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; -import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum'; +import { TagResponseSchema, mapTag } from 'src/dtos/tag.dto'; +import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; +import { + AssetStatus, + AssetType, + AssetTypeSchema, + AssetVisibility, + AssetVisibilitySchema, + ChecksumAlgorithm, +} from 'src/enum'; import { ImageDimensions, MaybeDehydrated } from 'src/types'; import { getDimensions } from 'src/utils/asset.util'; import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { asDateString } from 'src/utils/date'; import { mimeTypes } from 'src/utils/mime-types'; -import { ValidateEnum, ValidateUUID } from 'src/validation'; +import z from 'zod'; -export class SanitizedAssetResponseDto { - @ApiProperty({ description: 'Asset ID' }) - id!: string; - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' }) - type!: AssetType; - @ApiProperty({ - description: - 'Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.', +const SanitizedAssetResponseSchema = z + .object({ + id: z.string().describe('Asset ID'), + type: AssetTypeSchema, + thumbhash: z + .string() + .describe( + 'Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.', + ) + .nullable(), + originalMimeType: z.string().optional().describe('Original MIME type'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + localDateTime: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.', + ), + duration: z.string().describe('Video duration (for videos)'), + livePhotoVideoId: z.string().nullish().describe('Live photo video ID'), + hasMetadata: z.boolean().describe('Whether asset has metadata'), + width: z.number().min(0).nullable().describe('Asset width'), + height: z.number().min(0).nullable().describe('Asset height'), }) - thumbhash!: string | null; - @ApiPropertyOptional({ description: 'Original MIME type' }) - originalMimeType?: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - 'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.', - example: '2024-01-15T14:30:00.000Z', - }) - localDateTime!: string; - @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; -} + .meta({ id: 'SanitizedAssetResponseDto' }); -export class AssetResponseDto extends SanitizedAssetResponseDto { - @ApiProperty({ - type: 'string', - format: 'date-time', - description: 'The UTC timestamp when the asset was originally uploaded to Immich.', - example: '2024-01-15T20:30:00.000Z', - }) - createdAt!: string; - @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; - @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', - format: 'date-time', - description: - 'The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.', - example: '2024-01-15T19:30:00.000Z', - }) - fileCreatedAt!: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - 'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.', - example: '2024-01-16T10:15:00.000Z', - }) - fileModifiedAt!: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - 'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.', - example: '2024-01-16T12:45:30.000Z', - }) - updatedAt!: string; - @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', 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[]; - @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; +export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema) {} - @Property({ description: 'Is resized', history: new HistoryBuilder().added('v1').deprecated('v1.113.0') }) - resized?: boolean; - @Property({ description: 'Is edited', history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0') }) - isEdited!: boolean; -} +const AssetStackResponseSchema = z + .object({ + id: z.string().describe('Stack ID'), + primaryAssetId: z.string().describe('Primary asset ID'), + assetCount: z.int().min(0).describe('Number of assets in stack'), + }) + .meta({ id: 'AssetStackResponseDto' }); + +export const AssetResponseSchema = SanitizedAssetResponseSchema.extend( + z.object({ + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + createdAt: z + .string() + .meta({ format: 'date-time' }) + .describe('The UTC timestamp when the asset was originally uploaded to Immich.'), + deviceAssetId: z.string().describe('Device asset ID'), + deviceId: z.string().describe('Device ID'), + ownerId: z.string().describe('Owner user ID'), + owner: UserResponseSchema.optional(), + libraryId: z + .uuidv4() + .nullish() + .describe('Library ID') + .meta(new HistoryBuilder().added('v1').deprecated('v1').getExtensions()), + originalPath: z.string().describe('Original file path'), + originalFileName: z.string().describe('Original file name'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + fileCreatedAt: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.', + ), + fileModifiedAt: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.', + ), + updatedAt: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.', + ), + isFavorite: z.boolean().describe('Is favorite'), + isArchived: z.boolean().describe('Is archived'), + isTrashed: z.boolean().describe('Is trashed'), + isOffline: z.boolean().describe('Is offline'), + visibility: AssetVisibilitySchema, + exifInfo: ExifResponseSchema.optional(), + tags: z.array(TagResponseSchema).optional(), + people: z.array(PersonWithFacesResponseSchema).optional(), + unassignedFaces: z.array(AssetFaceWithoutPersonResponseSchema).optional(), + checksum: z.string().describe('Base64 encoded SHA1 hash'), + stack: AssetStackResponseSchema.nullish(), + duplicateId: z.string().nullish().describe('Duplicate group ID'), + resized: z + .boolean() + .optional() + .describe('Is resized') + .meta(new HistoryBuilder().added('v1').deprecated('v1.113.0').getExtensions()), + isEdited: z + .boolean() + .describe('Is edited') + .meta(new HistoryBuilder().added('v2.5.0').beta('v2.5.0').getExtensions()), + }).shape, +).meta({ id: 'AssetResponseDto' }); + +export class AssetResponseDto extends createZodDto(AssetResponseSchema) {} export type MapAsset = { createdAt: Date; @@ -180,17 +168,6 @@ export type MapAsset = { isEdited: boolean; }; -export class AssetStackResponseDto { - @ApiProperty({ description: 'Stack ID' }) - id!: string; - - @ApiProperty({ description: 'Primary asset ID' }) - primaryAssetId!: string; - - @ApiProperty({ type: 'integer', description: 'Number of assets in stack' }) - assetCount!: number; -} - export type AssetMapOptions = { stripMetadata?: boolean; withStack?: boolean; diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index b7bd7a18e8..3adb937475 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -1,125 +1,78 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { - IsArray, - IsDateString, - IsInt, - IsLatitude, - IsLongitude, - IsNotEmpty, - IsObject, - IsPositive, - IsString, - IsTimeZone, - Max, - Min, - ValidateIf, - ValidateNested, -} from 'class-validator'; -import { HistoryBuilder, Property } from 'src/decorators'; -import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetType, AssetVisibility } from 'src/enum'; +import { createZodDto } from 'nestjs-zod'; +import { HistoryBuilder } from 'src/decorators'; +import { BulkIdsSchema } from 'src/dtos/asset-ids.response.dto'; +import { AssetType, AssetVisibilitySchema } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; -import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { IsNotSiblingOf, isoDatetimeToDate, latitudeSchema, longitudeSchema, stringToBool } from 'src/validation'; +import z from 'zod'; -export class DeviceIdDto { - @ApiProperty({ description: 'Device ID' }) - @IsNotEmpty() - @IsString() - deviceId!: string; -} - -const hasGPS = (o: { latitude: undefined; longitude: undefined }) => - o.latitude !== undefined || o.longitude !== undefined; -const ValidateGPS = () => ValidateIf(hasGPS); - -export class UpdateAssetBase { - @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) - isFavorite?: boolean; - - @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; - - @Property({ - description: 'Rating in range [1-5], or null for unrated', - history: new HistoryBuilder() - .added('v1') - .stable('v2') - .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'), +const DeviceIdSchema = z + .object({ + deviceId: z.string().describe('Device ID'), }) - @Optional({ nullable: true }) - @IsInt() - @Max(5) - @Min(-1) - @Transform(({ value }) => (value === 0 ? null : value)) - rating?: number | null; + .meta({ id: 'DeviceIdDto' }); - @ApiProperty({ description: 'Asset description' }) - @Optional() - @IsString() - description?: string; -} +const UpdateAssetBaseSchema = z + .object({ + isFavorite: z.boolean().optional().describe('Mark as favorite'), + visibility: AssetVisibilitySchema.optional(), + dateTimeOriginal: z.string().optional().describe('Original date and time'), + latitude: latitudeSchema.optional().describe('Latitude coordinate'), + longitude: longitudeSchema.optional().describe('Longitude coordinate'), + rating: z + .number() + .int() + .min(-1) + .max(5) + .transform((value) => (value === 0 ? null : value)) + .nullish() + .describe('Rating in range [1-5], or null for unrated') + .meta({ + ...new HistoryBuilder() + .added('v1') + .stable('v2') + .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.') + .getExtensions(), + }), + description: z.string().optional().describe('Asset description'), + }) + .refine( + (data) => + (data.latitude === undefined && data.longitude === undefined) || + (data.latitude !== undefined && data.longitude !== undefined), + { message: 'Latitude and longitude must be provided together' }, + ); -export class AssetBulkUpdateDto extends UpdateAssetBase { - @ValidateUUID({ each: true, description: 'Asset IDs to update' }) - ids!: string[]; +const AssetBulkUpdateBaseSchema = UpdateAssetBaseSchema.extend({ + ids: z.array(z.uuidv4()).describe('Asset IDs to update'), + duplicateId: z.string().nullish().describe('Duplicate ID'), + dateTimeRelative: z.number().optional().describe('Relative time offset in seconds'), + timeZone: z.string().optional().describe('Time zone (IANA timezone)'), +}); - @ValidateString({ optional: true, nullable: true, description: 'Duplicate ID' }) - duplicateId?: string | null; +const AssetBulkUpdateSchema = AssetBulkUpdateBaseSchema.pipe( + IsNotSiblingOf(AssetBulkUpdateBaseSchema, 'dateTimeRelative', ['dateTimeOriginal']), +).meta({ id: 'AssetBulkUpdateDto' }); - @ApiProperty({ description: 'Relative time offset in seconds' }) - @IsNotSiblingOf(['dateTimeOriginal']) - @Optional() - @IsInt() - dateTimeRelative?: number; +const UpdateAssetSchema = UpdateAssetBaseSchema.extend({ + livePhotoVideoId: z.uuidv4().nullish().describe('Live photo video ID'), +}).meta({ id: 'UpdateAssetDto' }); - @ApiProperty({ description: 'Time zone (IANA timezone)' }) - @IsNotSiblingOf(['dateTimeOriginal']) - @IsTimeZone() - @Optional() - timeZone?: string; -} +const RandomAssetsSchema = z + .object({ + count: z.coerce.number().min(1).optional().describe('Number of random assets to return'), + }) + .meta({ id: 'RandomAssetsDto' }); -export class UpdateAssetDto extends UpdateAssetBase { - @ValidateUUID({ optional: true, nullable: true, description: 'Live photo video ID' }) - livePhotoVideoId?: string | null; -} +const AssetBulkDeleteSchema = BulkIdsSchema.extend({ + force: z.boolean().optional().describe('Force delete even if in use'), +}).meta({ id: 'AssetBulkDeleteDto' }); -export class RandomAssetsDto { - @ApiProperty({ description: 'Number of random assets to return' }) - @Optional() - @IsInt() - @IsPositive() - @Type(() => Number) - count?: number; -} - -export class AssetBulkDeleteDto extends BulkIdsDto { - @ValidateBoolean({ optional: true, description: 'Force delete even if in use' }) - force?: boolean; -} - -export class AssetIdsDto { - @ValidateUUID({ each: true, description: 'Asset IDs' }) - assetIds!: string[]; -} +export const AssetIdsSchema = z + .object({ + assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + }) + .meta({ id: 'AssetIdsDto' }); export enum AssetJobName { REFRESH_FACES = 'refresh-faces', @@ -128,137 +81,104 @@ export enum AssetJobName { TRANSCODE_VIDEO = 'transcode-video', } -export class AssetJobsDto extends AssetIdsDto { - @ValidateEnum({ enum: AssetJobName, name: 'AssetJobName', description: 'Job name' }) - name!: AssetJobName; -} +const AssetJobNameSchema = z.enum(AssetJobName).describe('Job name').meta({ id: 'AssetJobName' }); -export class AssetStatsDto { - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Filter by visibility', optional: true }) - visibility?: AssetVisibility; +const AssetJobsSchema = AssetIdsSchema.extend({ + name: AssetJobNameSchema, +}).meta({ id: 'AssetJobsDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; +const AssetStatsSchema = z + .object({ + visibility: AssetVisibilitySchema.optional(), + isFavorite: stringToBool.optional().describe('Filter by favorite status'), + isTrashed: stringToBool.optional().describe('Filter by trash status'), + }) + .meta({ id: 'AssetStatsDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by trash status' }) - isTrashed?: boolean; -} +const AssetStatsResponseSchema = z + .object({ + images: z.int().describe('Number of images'), + videos: z.int().describe('Number of videos'), + total: z.int().describe('Total number of assets'), + }) + .meta({ id: 'AssetStatsResponseDto' }); -export class AssetStatsResponseDto { - @ApiProperty({ description: 'Number of images', type: 'integer' }) - images!: number; +const AssetMetadataRouteParamsSchema = z + .object({ + id: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + }) + .meta({ id: 'AssetMetadataRouteParams' }); - @ApiProperty({ description: 'Number of videos', type: 'integer' }) - videos!: number; +export const AssetMetadataUpsertItemSchema = z + .object({ + key: z.string().describe('Metadata key'), + value: z.record(z.string(), z.unknown()).describe('Metadata value (object)'), + }) + .meta({ id: 'AssetMetadataUpsertItemDto' }); - @ApiProperty({ description: 'Total number of assets', type: 'integer' }) - total!: number; -} +const AssetMetadataUpsertSchema = z + .object({ + items: z.array(AssetMetadataUpsertItemSchema).describe('Metadata items to upsert'), + }) + .meta({ id: 'AssetMetadataUpsertDto' }); -export class AssetMetadataRouteParams { - @ValidateUUID({ description: 'Asset ID' }) - id!: string; +const AssetMetadataBulkUpsertItemSchema = z + .object({ + assetId: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + value: z.record(z.string(), z.unknown()).describe('Metadata value (object)'), + }) + .meta({ id: 'AssetMetadataBulkUpsertItemDto' }); - @ValidateString({ description: 'Metadata key' }) - key!: string; -} +const AssetMetadataBulkUpsertSchema = z + .object({ + items: z.array(AssetMetadataBulkUpsertItemSchema).describe('Metadata items to upsert'), + }) + .meta({ id: 'AssetMetadataBulkUpsertDto' }); -export class AssetMetadataUpsertDto { - @ApiProperty({ description: 'Metadata items to upsert' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetMetadataUpsertItemDto) - items!: AssetMetadataUpsertItemDto[]; -} +const AssetMetadataBulkDeleteItemSchema = z + .object({ + assetId: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + }) + .meta({ id: 'AssetMetadataBulkDeleteItemDto' }); -export class AssetMetadataUpsertItemDto { - @ValidateString({ description: 'Metadata key' }) - key!: string; +const AssetMetadataBulkDeleteSchema = z + .object({ + items: z.array(AssetMetadataBulkDeleteItemSchema).describe('Metadata items to delete'), + }) + .meta({ id: 'AssetMetadataBulkDeleteDto' }); - @ApiProperty({ description: 'Metadata value (object)' }) - @IsObject() - value!: object; -} +const AssetMetadataResponseSchema = z + .object({ + key: z.string().describe('Metadata key'), + value: z.record(z.string(), z.unknown()).describe('Metadata value (object)'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + }) + .meta({ id: 'AssetMetadataResponseDto' }); -export class AssetMetadataBulkUpsertDto { - @ApiProperty({ description: 'Metadata items to upsert' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetMetadataBulkUpsertItemDto) - items!: AssetMetadataBulkUpsertItemDto[]; -} +const AssetMetadataBulkResponseSchema = AssetMetadataResponseSchema.extend({ + assetId: z.string().describe('Asset ID'), +}).meta({ id: 'AssetMetadataBulkResponseDto' }); -export class AssetMetadataBulkUpsertItemDto { - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; +const AssetCopySchema = z + .object({ + sourceId: z.uuidv4().describe('Source asset ID'), + targetId: z.uuidv4().describe('Target asset ID'), + sharedLinks: z.boolean().default(true).optional().describe('Copy shared links'), + albums: z.boolean().default(true).optional().describe('Copy album associations'), + sidecar: z.boolean().default(true).optional().describe('Copy sidecar file'), + stack: z.boolean().default(true).optional().describe('Copy stack association'), + favorite: z.boolean().default(true).optional().describe('Copy favorite status'), + }) + .meta({ id: 'AssetCopyDto' }); - @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) - items!: AssetMetadataBulkDeleteItemDto[]; -} - -export class AssetMetadataBulkDeleteItemDto { - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; - - @ValidateString({ description: 'Metadata key' }) - key!: string; -} - -export class AssetMetadataResponseDto { - @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({ description: 'Source asset ID' }) - sourceId!: string; - - @ValidateUUID({ description: 'Target asset ID' }) - targetId!: string; - - @ValidateBoolean({ optional: true, description: 'Copy shared links', default: true }) - sharedLinks?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy album associations', default: true }) - albums?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy sidecar file', default: true }) - sidecar?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy stack association', default: true }) - stack?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy favorite status', default: true }) - favorite?: boolean; -} - -export class AssetDownloadOriginalDto { - @ValidateBoolean({ optional: true, description: 'Return edited asset if available', default: false }) - edited?: boolean; -} +const AssetDownloadOriginalSchema = z + .object({ + edited: stringToBool.default(false).optional().describe('Return edited asset if available'), + }) + .meta({ id: 'AssetDownloadOriginalDto' }); export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { return { @@ -267,3 +187,21 @@ export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { total: Object.values(stats).reduce((total, value) => total + value, 0), }; }; + +export class DeviceIdDto extends createZodDto(DeviceIdSchema) {} +export class AssetBulkUpdateDto extends createZodDto(AssetBulkUpdateSchema) {} +export class UpdateAssetDto extends createZodDto(UpdateAssetSchema) {} +export class RandomAssetsDto extends createZodDto(RandomAssetsSchema) {} +export class AssetBulkDeleteDto extends createZodDto(AssetBulkDeleteSchema) {} +export class AssetIdsDto extends createZodDto(AssetIdsSchema) {} +export class AssetJobsDto extends createZodDto(AssetJobsSchema) {} +export class AssetStatsDto extends createZodDto(AssetStatsSchema) {} +export class AssetStatsResponseDto extends createZodDto(AssetStatsResponseSchema) {} +export class AssetMetadataRouteParams extends createZodDto(AssetMetadataRouteParamsSchema) {} +export class AssetMetadataUpsertDto extends createZodDto(AssetMetadataUpsertSchema) {} +export class AssetMetadataBulkUpsertDto extends createZodDto(AssetMetadataBulkUpsertSchema) {} +export class AssetMetadataBulkDeleteDto extends createZodDto(AssetMetadataBulkDeleteSchema) {} +export class AssetMetadataResponseDto extends createZodDto(AssetMetadataResponseSchema) {} +export class AssetMetadataBulkResponseDto extends createZodDto(AssetMetadataBulkResponseSchema) {} +export class AssetCopyDto extends createZodDto(AssetCopySchema) {} +export class AssetDownloadOriginalDto extends createZodDto(AssetDownloadOriginalSchema) {} diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 3df82f4ef4..95d2bb126a 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -1,59 +1,43 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database'; import { ImmichCookie, UserMetadataKey } from 'src/enum'; import { UserMetadataItem } from 'src/types'; -import { Optional, PinCode, toEmail, ValidateBoolean } from 'src/validation'; +import { toEmail } from 'src/validation'; +import z from 'zod'; export type CookieResponse = { isSecure: boolean; values: Array<{ key: ImmichCookie; value: string | null }>; }; -export class AuthDto { - @ApiProperty({ description: 'Authenticated user' }) - user!: AuthUser; +export const pinCodeRegex = /^\d{6}$/; - @ApiPropertyOptional({ description: 'API key (if authenticated via API key)' }) +export type AuthDto = { + user: AuthUser; 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() - email!: string; +const LoginCredentialSchema = z + .object({ + email: toEmail.describe('User email').meta({ example: 'testuser@email.com' }), + password: z.string().describe('User password').meta({ example: 'password' }), + }) + .meta({ id: 'LoginCredentialDto' }); - @ApiProperty({ example: 'password', description: 'User password' }) - @IsString() - @IsNotEmpty() - 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; -} +const LoginResponseSchema = z + .object({ + accessToken: z.string().describe('Access token'), + userId: z.string().describe('User ID'), + userEmail: toEmail.describe('User email'), + name: z.string().describe('User name'), + profileImagePath: z.string().describe('Profile image path'), + isAdmin: z.boolean().describe('Is admin user'), + shouldChangePassword: z.boolean().describe('Should change password'), + isOnboarded: z.boolean().describe('Is onboarded'), + }) + .meta({ id: 'LoginResponseDto' }); export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto { const onboardingMetadata = entity.metadata.find( @@ -72,115 +56,95 @@ export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginR }; } -export class LogoutResponseDto { - @ApiProperty({ description: 'Logout successful' }) - successful!: boolean; - @ApiProperty({ description: 'Redirect URI' }) - redirectUri!: string; -} +const LogoutResponseSchema = z + .object({ + successful: z.boolean().describe('Logout successful'), + redirectUri: z.string().describe('Redirect URI'), + }) + .meta({ id: 'LogoutResponseDto' }); -export class SignUpDto extends LoginCredentialDto { - @ApiProperty({ example: 'Admin', description: 'User name' }) - @IsString() - @IsNotEmpty() - name!: string; -} +const SignUpSchema = LoginCredentialSchema.extend({ + name: z.string().describe('User name').meta({ example: 'Admin' }), +}).meta({ id: 'SignUpDto' }); -export class ChangePasswordDto { - @ApiProperty({ example: 'password', description: 'Current password' }) - @IsString() - @IsNotEmpty() - password!: string; +const ChangePasswordSchema = z + .object({ + password: z.string().describe('Current password').meta({ example: 'password' }), + newPassword: z.string().min(8).describe('New password (min 8 characters)').meta({ example: 'password' }), + invalidateSessions: z.boolean().default(false).optional().describe('Invalidate all other sessions'), + }) + .meta({ id: 'ChangePasswordDto' }); - @ApiProperty({ example: 'password', description: 'New password (min 8 characters)' }) - @IsString() - @IsNotEmpty() - @MinLength(8) - newPassword!: string; +const PinCodeSetupSchema = z + .object({ + pinCode: z.string().regex(pinCodeRegex).describe('PIN code (4-6 digits)').meta({ example: '123456' }), + }) + .meta({ id: 'PinCodeSetupDto' }); - @ValidateBoolean({ optional: true, default: false, description: 'Invalidate all other sessions' }) - invalidateSessions?: boolean; -} +const PinCodeResetSchema = z.object({ + pinCode: z.string().regex(pinCodeRegex).optional().describe('New PIN code (4-6 digits)').meta({ example: '123456' }), + password: z + .string() + .optional() + .describe('User password (required if PIN code is not provided)') + .meta({ example: 'password' }), +}); -export class PinCodeSetupDto { - @ApiProperty({ description: 'PIN code (4-6 digits)' }) - @PinCode() - pinCode!: string; -} +const SessionUnlockSchema = PinCodeResetSchema.meta({ id: 'SessionUnlockDto' }); -export class PinCodeResetDto { - @ApiPropertyOptional({ description: 'New PIN code (4-6 digits)' }) - @PinCode({ optional: true }) - pinCode?: string; +const PinCodeChangeSchema = PinCodeResetSchema.extend({ + newPinCode: z.string().regex(pinCodeRegex).describe('New PIN code (4-6 digits)'), +}).meta({ id: 'PinCodeChangeDto' }); - @ApiPropertyOptional({ description: 'User password (required if PIN code is not provided)' }) - @Optional() - @IsString() - @IsNotEmpty() - password?: string; -} +const ValidateAccessTokenResponseSchema = z + .object({ + authStatus: z.boolean().describe('Authentication status'), + }) + .meta({ id: 'ValidateAccessTokenResponseDto' }); -export class SessionUnlockDto extends PinCodeResetDto {} +const OAuthCallbackSchema = z + .object({ + url: z.string().min(1).describe('OAuth callback URL'), + state: z.string().optional().describe('OAuth state parameter'), + codeVerifier: z.string().optional().describe('OAuth code verifier (PKCE)'), + }) + .meta({ id: 'OAuthCallbackDto' }); -export class PinCodeChangeDto extends PinCodeResetDto { - @ApiProperty({ description: 'New PIN code (4-6 digits)' }) - @PinCode() - newPinCode!: string; -} +const OAuthConfigSchema = z + .object({ + redirectUri: z.string().describe('OAuth redirect URI'), + state: z.string().optional().describe('OAuth state parameter'), + codeChallenge: z.string().optional().describe('OAuth code challenge (PKCE)'), + }) + .meta({ id: 'OAuthConfigDto' }); -export class ValidateAccessTokenResponseDto { - @ApiProperty({ description: 'Authentication status' }) - authStatus!: boolean; -} +const OAuthAuthorizeResponseSchema = z + .object({ + url: z.string().describe('OAuth authorization URL'), + }) + .meta({ id: 'OAuthAuthorizeResponseDto' }); -export class OAuthCallbackDto { - @ApiProperty({ description: 'OAuth callback URL' }) - @IsNotEmpty() - @IsString() - url!: string; +const AuthStatusResponseSchema = z + .object({ + pinCode: z.boolean().describe('Has PIN code set'), + password: z.boolean().describe('Has password set'), + isElevated: z.boolean().describe('Is elevated session'), + expiresAt: z.string().optional().describe('Session expiration date'), + pinExpiresAt: z.string().optional().describe('PIN expiration date'), + }) + .meta({ id: 'AuthStatusResponseDto' }); - @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; -} +export class LoginCredentialDto extends createZodDto(LoginCredentialSchema) {} +export class LoginResponseDto extends createZodDto(LoginResponseSchema) {} +export class LogoutResponseDto extends createZodDto(LogoutResponseSchema) {} +export class SignUpDto extends createZodDto(SignUpSchema) {} +export class ChangePasswordDto extends createZodDto(ChangePasswordSchema) {} +export class PinCodeSetupDto extends createZodDto(PinCodeSetupSchema) {} +export class PinCodeResetDto extends createZodDto(PinCodeResetSchema) {} +export class SessionUnlockDto extends createZodDto(SessionUnlockSchema) {} +export class PinCodeChangeDto extends createZodDto(PinCodeChangeSchema) {} +export class ValidateAccessTokenResponseDto extends createZodDto(ValidateAccessTokenResponseSchema) {} +export class OAuthCallbackDto extends createZodDto(OAuthCallbackSchema) {} +export class OAuthConfigDto extends createZodDto(OAuthConfigSchema) {} +export class OAuthAuthorizeResponseDto extends createZodDto(OAuthAuthorizeResponseSchema) {} +export class AuthStatusResponseDto extends createZodDto(AuthStatusResponseSchema) {} diff --git a/server/src/dtos/bbox.dto.ts b/server/src/dtos/bbox.dto.ts index 1afe9f53ba..8c24173791 100644 --- a/server/src/dtos/bbox.dto.ts +++ b/server/src/dtos/bbox.dto.ts @@ -1,25 +1,17 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsLatitude, IsLongitude } from 'class-validator'; -import { IsGreaterThanOrEqualTo } from 'src/validation'; +import { latitudeSchema, longitudeSchema } from 'src/validation'; +import z from 'zod'; -export class BBoxDto { - @ApiProperty({ format: 'double', description: 'West longitude (-180 to 180)' }) - @IsLongitude() - west!: number; - - @ApiProperty({ format: 'double', description: 'South latitude (-90 to 90)' }) - @IsLatitude() - south!: number; - - @ApiProperty({ - format: 'double', - description: 'East longitude (-180 to 180). May be less than west when crossing the antimeridian.', +export const BBoxSchema = z + .object({ + west: longitudeSchema.describe('West longitude (-180 to 180)'), + south: latitudeSchema.describe('South latitude (-90 to 90)'), + east: longitudeSchema.describe( + 'East longitude (-180 to 180). May be less than west when crossing the antimeridian.', + ), + north: latitudeSchema.describe('North latitude (-90 to 90). Must be >= south.'), }) - @IsLongitude() - east!: number; - - @ApiProperty({ format: 'double', description: 'North latitude (-90 to 90). Must be >= south.' }) - @IsLatitude() - @IsGreaterThanOrEqualTo('south') - north!: number; -} + .refine(({ north, south }) => north >= south, { + path: ['north'], + error: 'North latitude must be greater than or equal to south latitude', + }) + .meta({ id: 'BBoxDto' }); diff --git a/server/src/dtos/database-backup.dto.ts b/server/src/dtos/database-backup.dto.ts index c0554f83b7..34dd8f2a62 100644 --- a/server/src/dtos/database-backup.dto.ts +++ b/server/src/dtos/database-backup.dto.ts @@ -1,22 +1,32 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class DatabaseBackupDto { - filename!: string; - filesize!: number; - timezone!: string; -} +const DatabaseBackupSchema = z + .object({ + filename: z.string().describe('Backup filename'), + filesize: z.number().describe('Backup file size'), + timezone: z.string().describe('Backup timezone'), + }) + .meta({ id: 'DatabaseBackupDto' }); -export class DatabaseBackupListResponseDto { - backups!: DatabaseBackupDto[]; -} +const DatabaseBackupListResponseSchema = z + .object({ + backups: z.array(DatabaseBackupSchema).describe('List of backups'), + }) + .meta({ id: 'DatabaseBackupListResponseDto' }); -export class DatabaseBackupUploadDto { - @ApiProperty({ type: 'string', format: 'binary', required: false }) - file?: any; -} +const DatabaseBackupUploadSchema = z + .object({ + file: z.file().optional().describe('Database backup file'), + }) + .meta({ id: 'DatabaseBackupUploadDto' }); -export class DatabaseBackupDeleteDto { - @IsString({ each: true }) - backups!: string[]; -} +const DatabaseBackupDeleteSchema = z + .object({ + backups: z.array(z.string()).describe('Backup filenames to delete'), + }) + .meta({ id: 'DatabaseBackupDeleteDto' }); + +export class DatabaseBackupListResponseDto extends createZodDto(DatabaseBackupListResponseSchema) {} +export class DatabaseBackupUploadDto extends createZodDto(DatabaseBackupUploadSchema) {} +export class DatabaseBackupDeleteDto extends createZodDto(DatabaseBackupDeleteSchema) {} diff --git a/server/src/dtos/download.dto.ts b/server/src/dtos/download.dto.ts index ef52a72bd0..b44a6a7afc 100644 --- a/server/src/dtos/download.dto.ts +++ b/server/src/dtos/download.dto.ts @@ -1,40 +1,35 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsInt, IsPositive } from 'class-validator'; -import { AssetIdsDto } from 'src/dtos/asset.dto'; -import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { AssetIdsSchema } from 'src/dtos/asset.dto'; +import z from 'zod'; -export class DownloadInfoDto { - @ValidateUUID({ each: true, optional: true, description: 'Asset IDs to download' }) - assetIds?: string[]; +const DownloadInfoSchema = z + .object({ + assetIds: z.array(z.uuidv4()).optional().describe('Asset IDs to download'), + albumId: z.uuidv4().optional().describe('Album ID to download'), + userId: z.uuidv4().optional().describe('User ID to download assets from'), + archiveSize: z.int().min(1).optional().describe('Archive size limit in bytes'), + }) + .meta({ id: 'DownloadInfoDto' }); - @ValidateUUID({ optional: true, description: 'Album ID to download' }) - albumId?: string; +const DownloadArchiveInfoSchema = z + .object({ + size: z.int().describe('Archive size in bytes'), + assetIds: z.array(z.string()).describe('Asset IDs in this archive'), + }) + .meta({ id: 'DownloadArchiveInfo' }); - @ValidateUUID({ optional: true, description: 'User ID to download assets from' }) - userId?: string; +const DownloadResponseSchema = z + .object({ + totalSize: z.int().describe('Total size in bytes'), + archives: z.array(DownloadArchiveInfoSchema).describe('Archive information'), + }) + .meta({ id: 'DownloadResponseDto' }); - @ApiPropertyOptional({ type: 'integer', description: 'Archive size limit in bytes' }) - @IsInt() - @IsPositive() - @Optional() - archiveSize?: number; -} +const DownloadArchiveSchema = AssetIdsSchema.extend({ + edited: z.boolean().optional().describe('Download edited asset if available'), +}).meta({ id: 'DownloadArchiveDto' }); -export class DownloadResponseDto { - @ApiProperty({ type: 'integer', description: 'Total size in bytes' }) - totalSize!: number; - @ApiProperty({ description: 'Archive information' }) - archives!: DownloadArchiveInfo[]; -} - -export class DownloadArchiveInfo { - @ApiProperty({ type: 'integer', description: 'Archive size in bytes' }) - size!: number; - @ApiProperty({ description: 'Asset IDs in this archive' }) - assetIds!: string[]; -} - -export class DownloadArchiveDto extends AssetIdsDto { - @ValidateBoolean({ optional: true, description: 'Download edited asset if available' }) - edited?: boolean; -} +export class DownloadInfoDto extends createZodDto(DownloadInfoSchema) {} +export class DownloadResponseDto extends createZodDto(DownloadResponseSchema) {} +export class DownloadArchiveInfo extends createZodDto(DownloadArchiveInfoSchema) {} +export class DownloadArchiveDto extends createZodDto(DownloadArchiveSchema) {} diff --git a/server/src/dtos/duplicate.dto.ts b/server/src/dtos/duplicate.dto.ts index 40b1b74c70..55427e36aa 100644 --- a/server/src/dtos/duplicate.dto.ts +++ b/server/src/dtos/duplicate.dto.ts @@ -1,35 +1,29 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayMinSize, IsArray, ValidateNested } from 'class-validator'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; +import z from 'zod'; -export class DuplicateResponseDto { - @ApiProperty({ description: 'Duplicate group ID' }) - duplicateId!: string; - @ApiProperty({ description: 'Duplicate assets' }) - assets!: AssetResponseDto[]; +const DuplicateResponseSchema = z + .object({ + duplicateId: z.string().describe('Duplicate group ID'), + assets: z.array(AssetResponseSchema).describe('Duplicate assets'), + suggestedKeepAssetIds: z.array(z.uuidv4()).describe('Suggested asset IDs to keep based on file size and EXIF data'), + }) + .meta({ id: 'DuplicateResponseDto' }); - @ValidateUUID({ each: true, description: 'Suggested asset IDs to keep based on file size and EXIF data' }) - suggestedKeepAssetIds!: string[]; -} +const DuplicateResolveGroupSchema = z + .object({ + duplicateId: z.uuidv4(), + keepAssetIds: z.array(z.uuidv4()).describe('Asset IDs to keep'), + trashAssetIds: z.array(z.uuidv4()).describe('Asset IDs to trash or delete'), + }) + .meta({ id: 'DuplicateResolveGroupDto' }); -export class DuplicateResolveGroupDto { - @ValidateUUID() - duplicateId!: string; +const DuplicateResolveSchema = z + .object({ + groups: z.array(DuplicateResolveGroupSchema).min(1).describe('List of duplicate groups to resolve'), + }) + .meta({ id: 'DuplicateResolveDto' }); - @ValidateUUID({ each: true, description: 'Asset IDs to keep' }) - keepAssetIds!: string[]; - - @ValidateUUID({ each: true, description: 'Asset IDs to trash or delete' }) - trashAssetIds!: string[]; -} - -export class DuplicateResolveDto { - @ApiProperty({ description: 'List of duplicate groups to resolve' }) - @ValidateNested({ each: true }) - @IsArray() - @Type(() => DuplicateResolveGroupDto) - @ArrayMinSize(1) - groups!: DuplicateResolveGroupDto[]; -} +export class DuplicateResponseDto extends createZodDto(DuplicateResponseSchema) {} +export class DuplicateResolveGroupDto extends createZodDto(DuplicateResolveGroupSchema) {} +export class DuplicateResolveDto extends createZodDto(DuplicateResolveSchema) {} diff --git a/server/src/dtos/editing.dto.ts b/server/src/dtos/editing.dto.ts index 8217fec41c..9f5b352195 100644 --- a/server/src/dtos/editing.dto.ts +++ b/server/src/dtos/editing.dto.ts @@ -1,7 +1,5 @@ -import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayMinSize, IsEnum, IsInt, Min, ValidateNested } from 'class-validator'; -import { IsAxisAlignedRotation, IsUniqueEditActions, ValidateEnum, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; export enum AssetEditAction { Crop = 'crop', @@ -9,103 +7,128 @@ export enum AssetEditAction { Mirror = 'mirror', } +export const AssetEditActionSchema = z + .enum(AssetEditAction) + .describe('Type of edit action to perform') + .meta({ id: 'AssetEditAction' }); + export enum MirrorAxis { Horizontal = 'horizontal', Vertical = 'vertical', } -export class CropParameters { - @IsInt() - @Min(0) - @ApiProperty({ description: 'Top-Left X coordinate of crop' }) - x!: number; +const MirrorAxisSchema = z.enum(['horizontal', 'vertical']).describe('Axis to mirror along').meta({ id: 'MirrorAxis' }); - @IsInt() - @Min(0) - @ApiProperty({ description: 'Top-Left Y coordinate of crop' }) - y!: number; - - @IsInt() - @Min(1) - @ApiProperty({ description: 'Width of the crop' }) - width!: number; - - @IsInt() - @Min(1) - @ApiProperty({ description: 'Height of the crop' }) - height!: number; -} - -export class RotateParameters { - @IsAxisAlignedRotation() - @ApiProperty({ description: 'Rotation angle in degrees' }) - angle!: number; -} - -export class MirrorParameters { - @IsEnum(MirrorAxis) - @ApiProperty({ enum: MirrorAxis, enumName: 'MirrorAxis', description: 'Axis to mirror along' }) - axis!: MirrorAxis; -} - -export type AssetEditParameters = CropParameters | RotateParameters | MirrorParameters; -export type AssetEditActionItem = - | { - action: AssetEditAction.Crop; - parameters: CropParameters; - } - | { - action: AssetEditAction.Rotate; - parameters: RotateParameters; - } - | { - action: AssetEditAction.Mirror; - parameters: MirrorParameters; - }; - -@ApiExtraModels(CropParameters, RotateParameters, MirrorParameters) -export class AssetEditActionItemDto { - @ValidateEnum({ name: 'AssetEditAction', enum: AssetEditAction, description: 'Type of edit action to perform' }) - action!: AssetEditAction; - - @ApiProperty({ - description: 'List of edit actions to apply (crop, rotate, or mirror)', - anyOf: [CropParameters, RotateParameters, MirrorParameters].map((type) => ({ - $ref: getSchemaPath(type), - })), +const CropParametersSchema = z + .object({ + x: z.number().min(0).describe('Top-Left X coordinate of crop'), + y: z.number().min(0).describe('Top-Left Y coordinate of crop'), + width: z.number().min(1).describe('Width of the crop'), + height: z.number().min(1).describe('Height of the crop'), }) - @ValidateNested() - @Type((options) => actionParameterMap[options?.object.action as keyof AssetEditActionParameter]) - parameters!: AssetEditActionItem['parameters']; -} + .meta({ id: 'CropParameters' }); -export class AssetEditActionItemResponseDto extends AssetEditActionItemDto { - @ValidateUUID() - id!: string; -} +const RotateParametersSchema = z + .object({ + angle: z + .number() + .refine((v) => [0, 90, 180, 270].includes(v), { + error: 'Angle must be one of the following values: 0, 90, 180, 270', + }) + .describe('Rotation angle in degrees'), + }) + .meta({ id: 'RotateParameters' }); + +const MirrorParametersSchema = z + .object({ + axis: MirrorAxisSchema, + }) + .meta({ id: 'MirrorParameters' }); + +// TODO: ideally we would use the discriminated union directly in the future not only for type support but also for validation and openapi generation +const __AssetEditActionItemSchema = z.discriminatedUnion('action', [ + z.object({ action: AssetEditActionSchema.extract(['Crop']), parameters: CropParametersSchema }), + z.object({ action: AssetEditActionSchema.extract(['Rotate']), parameters: RotateParametersSchema }), + z.object({ action: AssetEditActionSchema.extract(['Mirror']), parameters: MirrorParametersSchema }), +]); + +const AssetEditParametersSchema = z + .union([CropParametersSchema, RotateParametersSchema, MirrorParametersSchema], { + error: getExpectedKeysByActionMessage, + }) + .describe('List of edit actions to apply (crop, rotate, or mirror)'); -export type AssetEditActionParameter = typeof actionParameterMap; const actionParameterMap = { - [AssetEditAction.Crop]: CropParameters, - [AssetEditAction.Rotate]: RotateParameters, - [AssetEditAction.Mirror]: MirrorParameters, -}; + [AssetEditAction.Crop]: CropParametersSchema, + [AssetEditAction.Rotate]: RotateParametersSchema, + [AssetEditAction.Mirror]: MirrorParametersSchema, +} as const; -export class AssetEditsCreateDto { - @ArrayMinSize(1) - @IsUniqueEditActions() - @ValidateNested({ each: true }) - @Type(() => AssetEditActionItemDto) - @ApiProperty({ description: 'List of edit actions to apply (crop, rotate, or mirror)' }) - edits!: AssetEditActionItemDto[]; +function getExpectedKeysByActionMessage(): string { + const expectedByAction = Object.entries(actionParameterMap) + .map(([action, schema]) => `${action}: [${Object.keys(schema.shape).join(', ')}]`) + .join('; '); + + return `Invalid parameters for action, expected keys by action: ${expectedByAction}`; } -export class AssetEditsResponseDto { - @ValidateUUID({ description: 'Asset ID these edits belong to' }) - assetId!: string; +function isParametersValidForAction(edit: z.infer): boolean { + return actionParameterMap[edit.action].safeParse(edit.parameters).success; +} - @ApiProperty({ - description: 'List of edit actions applied to the asset', +const AssetEditActionItemSchema = z + .object({ + action: AssetEditActionSchema, + parameters: AssetEditParametersSchema, }) - edits!: AssetEditActionItemResponseDto[]; + .superRefine((edit, ctx) => { + if (!isParametersValidForAction(edit)) { + ctx.addIssue({ + code: 'custom', + path: ['parameters'], + message: `Invalid parameters for action '${edit.action}', expecting keys: ${Object.keys(actionParameterMap[edit.action].shape).join(', ')}`, + }); + } + }) + .meta({ id: 'AssetEditActionItemDto' }); + +export type AssetEditActionItem = z.infer; +export type AssetEditParameters = AssetEditActionItem['parameters']; + +function uniqueEditActions(edits: z.infer[]): boolean { + const keys = new Set(); + for (const edit of edits) { + const key = edit.action === 'mirror' ? `mirror-${JSON.stringify(edit.parameters)}` : edit.action; + if (keys.has(key)) { + return false; + } + keys.add(key); + } + return true; } + +const AssetEditsCreateSchema = z + .object({ + edits: z + .array(AssetEditActionItemSchema) + .min(1) + .describe('List of edit actions to apply (crop, rotate, or mirror)') + .refine(uniqueEditActions, { error: 'Duplicate edit actions are not allowed' }), + }) + .meta({ id: 'AssetEditsCreateDto' }); + +const AssetEditActionItemResponseSchema = AssetEditActionItemSchema.extend({ + id: z.uuidv4().describe('Asset edit ID'), +}).meta({ id: 'AssetEditActionItemResponseDto' }); + +const AssetEditsResponseSchema = z + .object({ + assetId: z.uuidv4().describe('Asset ID these edits belong to'), + edits: z.array(AssetEditActionItemResponseSchema).describe('List of edit actions applied to the asset'), + }) + .meta({ id: 'AssetEditsResponseDto' }); + +export class AssetEditActionItemResponseDto extends createZodDto(AssetEditActionItemResponseSchema) {} +export class AssetEditsCreateDto extends createZodDto(AssetEditsCreateSchema) {} +export class AssetEditsResponseDto extends createZodDto(AssetEditsResponseSchema) {} +export type CropParameters = z.infer; diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts index bdcf3614fd..fc30875b5a 100644 --- a/server/src/dtos/env.dto.ts +++ b/server/src/dtos/env.dto.ts @@ -1,7 +1,6 @@ -import { Transform, Type } from 'class-transformer'; -import { IsEnum, IsInt, IsString, Matches } from 'class-validator'; -import { ImmichEnvironment, LogFormat, LogLevel } from 'src/enum'; -import { IsIPRange, Optional, ValidateBoolean } from 'src/validation'; +import { ImmichEnvironmentSchema, LogFormatSchema, LogLevelSchema } from 'src/enum'; +import { IsIPRange } from 'src/validation'; +import z from 'zod'; // TODO import from sql-tools once the swagger plugin supports external enums enum DatabaseSslMode { @@ -12,214 +11,80 @@ enum DatabaseSslMode { VerifyFull = 'verify-full', } -export class EnvDto { - @IsInt() - @Optional() - @Type(() => Number) - IMMICH_API_METRICS_PORT?: number; +const DatabaseSslModeSchema = z.enum(DatabaseSslMode).describe('Database SSL mode').meta({ id: 'DatabaseSslMode' }); +const absolutePath = z.string().regex(/^\//, 'Must be an absolute path').optional(); +/** + * Treat certain strings as booleans and coerce them to boolean + * Ideal for environment variables that are strings but should be treated as booleans + * @docs https://zod.dev/api?id=stringbool + */ +const stringBool = z.stringbool(); - @IsString() - @Optional() - IMMICH_BUILD_DATA?: string; - - @IsString() - @Optional() - IMMICH_BUILD?: string; - - @IsString() - @Optional() - IMMICH_BUILD_URL?: string; - - @IsString() - @Optional() - IMMICH_BUILD_IMAGE?: string; - - @IsString() - @Optional() - IMMICH_BUILD_IMAGE_URL?: string; - - @IsString() - @Optional() - IMMICH_CONFIG_FILE?: string; - - @IsString() - @Optional() - IMMICH_HELMET_FILE?: string; - - @IsEnum(ImmichEnvironment) - @Optional() - IMMICH_ENV?: ImmichEnvironment; - - @IsString() - @Optional() - IMMICH_HOST?: string; - - @ValidateBoolean({ optional: true }) - IMMICH_IGNORE_MOUNT_CHECK_ERRORS?: boolean; - - @IsEnum(LogLevel) - @Optional() - IMMICH_LOG_LEVEL?: LogLevel; - - @IsEnum(LogFormat) - @Optional() - IMMICH_LOG_FORMAT?: LogFormat; - - @Optional() - @Matches(/^\//, { message: 'IMMICH_MEDIA_LOCATION must be an absolute path' }) - IMMICH_MEDIA_LOCATION?: string; - - @IsInt() - @Optional() - @Type(() => Number) - IMMICH_MICROSERVICES_METRICS_PORT?: number; - - @ValidateBoolean({ optional: true }) - IMMICH_ALLOW_EXTERNAL_PLUGINS?: boolean; - - @Optional() - @Matches(/^\//, { message: 'IMMICH_PLUGINS_INSTALL_FOLDER must be an absolute path' }) - IMMICH_PLUGINS_INSTALL_FOLDER?: string; - - @IsInt() - @Optional() - @Type(() => Number) - IMMICH_PORT?: number; - - @IsString() - @Optional() - IMMICH_REPOSITORY?: string; - - @IsString() - @Optional() - IMMICH_REPOSITORY_URL?: string; - - @IsString() - @Optional() - IMMICH_SOURCE_REF?: string; - - @IsString() - @Optional() - IMMICH_SOURCE_COMMIT?: string; - - @IsString() - @Optional() - IMMICH_SOURCE_URL?: string; - - @IsString() - @Optional() - IMMICH_TELEMETRY_INCLUDE?: string; - - @IsString() - @Optional() - IMMICH_TELEMETRY_EXCLUDE?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_SOURCE_URL?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_BUG_FEATURE_URL?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_DOCUMENTATION_URL?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_SUPPORT_URL?: string; - - @ValidateBoolean({ optional: true }) - IMMICH_ALLOW_SETUP?: boolean; - - @IsIPRange({ requireCIDR: false }, { each: true }) - @Transform(({ value }) => - value && typeof value === 'string' - ? value +const trustedProxiesSchema = z + .string() + .optional() + .transform((s) => + s + ? s .split(',') - .map((value) => value.trim()) + .map((x) => x.trim()) .filter(Boolean) - : value, + : undefined, ) - @Optional() - IMMICH_TRUSTED_PROXIES?: string[]; - @IsString() - @Optional() - IMMICH_WORKERS_INCLUDE?: string; + .pipe(z.union([z.undefined(), IsIPRange({ requireCIDR: false })])); - @IsString() - @Optional() - IMMICH_WORKERS_EXCLUDE?: string; - - @IsString() - @Optional() - DB_DATABASE_NAME?: string; - - @IsString() - @Optional() - DB_HOSTNAME?: string; - - @IsString() - @Optional() - DB_PASSWORD?: string; - - @IsInt() - @Optional() - @Type(() => Number) - DB_PORT?: number; - - @ValidateBoolean({ optional: true }) - DB_SKIP_MIGRATIONS?: boolean; - - @IsEnum(DatabaseSslMode) - @Optional() - DB_SSL_MODE?: DatabaseSslMode; - - @IsString() - @Optional() - DB_URL?: string; - - @IsString() - @Optional() - DB_USERNAME?: string; - - @IsEnum(['pgvector', 'pgvecto.rs', 'vectorchord']) - @Optional() - DB_VECTOR_EXTENSION?: 'pgvector' | 'pgvecto.rs' | 'vectorchord'; - - @IsString() - @Optional() - NO_COLOR?: string; - - @IsString() - @Optional() - REDIS_HOSTNAME?: string; - - @IsInt() - @Optional() - @Type(() => Number) - REDIS_PORT?: number; - - @IsInt() - @Optional() - @Type(() => Number) - REDIS_DBINDEX?: number; - - @IsString() - @Optional() - REDIS_USERNAME?: string; - - @IsString() - @Optional() - REDIS_PASSWORD?: string; - - @IsString() - @Optional() - REDIS_SOCKET?: string; - - @IsString() - @Optional() - REDIS_URL?: string; -} +export const EnvSchema = z + .object({ + IMMICH_API_METRICS_PORT: z.coerce.number().int().optional(), + IMMICH_BUILD_DATA: z.string().optional(), + IMMICH_BUILD: z.string().optional(), + IMMICH_BUILD_URL: z.string().optional(), + IMMICH_BUILD_IMAGE: z.string().optional(), + IMMICH_BUILD_IMAGE_URL: z.string().optional(), + IMMICH_CONFIG_FILE: z.string().optional(), + IMMICH_HELMET_FILE: z.string().optional(), + IMMICH_ENV: ImmichEnvironmentSchema.optional(), + IMMICH_HOST: z.string().optional(), + IMMICH_IGNORE_MOUNT_CHECK_ERRORS: stringBool.optional(), + IMMICH_LOG_LEVEL: LogLevelSchema.optional(), + IMMICH_LOG_FORMAT: LogFormatSchema.optional(), + IMMICH_MEDIA_LOCATION: absolutePath, + IMMICH_MICROSERVICES_METRICS_PORT: z.coerce.number().int().optional(), + IMMICH_ALLOW_EXTERNAL_PLUGINS: stringBool.optional(), + IMMICH_PLUGINS_INSTALL_FOLDER: absolutePath, + IMMICH_PORT: z.coerce.number().int().optional(), + IMMICH_REPOSITORY: z.string().optional(), + IMMICH_REPOSITORY_URL: z.string().optional(), + IMMICH_SOURCE_REF: z.string().optional(), + IMMICH_SOURCE_COMMIT: z.string().optional(), + IMMICH_SOURCE_URL: z.string().optional(), + IMMICH_TELEMETRY_INCLUDE: z.string().optional(), + IMMICH_TELEMETRY_EXCLUDE: z.string().optional(), + IMMICH_THIRD_PARTY_SOURCE_URL: z.string().optional(), + IMMICH_THIRD_PARTY_BUG_FEATURE_URL: z.string().optional(), + IMMICH_THIRD_PARTY_DOCUMENTATION_URL: z.string().optional(), + IMMICH_THIRD_PARTY_SUPPORT_URL: z.string().optional(), + IMMICH_ALLOW_SETUP: stringBool.optional(), + IMMICH_TRUSTED_PROXIES: trustedProxiesSchema, + IMMICH_WORKERS_INCLUDE: z.string().optional(), + IMMICH_WORKERS_EXCLUDE: z.string().optional(), + DB_DATABASE_NAME: z.string().optional(), + DB_HOSTNAME: z.string().optional(), + DB_PASSWORD: z.string().optional(), + DB_PORT: z.coerce.number().int().optional(), + DB_SKIP_MIGRATIONS: stringBool.optional(), + DB_SSL_MODE: DatabaseSslModeSchema.optional(), + DB_URL: z.string().optional(), + DB_USERNAME: z.string().optional(), + DB_VECTOR_EXTENSION: z.enum(['pgvector', 'pgvecto.rs', 'vectorchord']).optional(), + NO_COLOR: z.string().optional(), + REDIS_HOSTNAME: z.string().optional(), + REDIS_PORT: z.coerce.number().int().optional(), + REDIS_DBINDEX: z.coerce.number().int().optional(), + REDIS_USERNAME: z.string().optional(), + REDIS_PASSWORD: z.string().optional(), + REDIS_SOCKET: z.string().optional(), + REDIS_URL: z.string().optional(), + }) + .meta({ id: 'EnvDto' }); diff --git a/server/src/dtos/exif.dto.ts b/server/src/dtos/exif.dto.ts index 165ecde4db..c3e1ab36c8 100644 --- a/server/src/dtos/exif.dto.ts +++ b/server/src/dtos/exif.dto.ts @@ -1,55 +1,40 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; import { Exif } from 'src/database'; import { MaybeDehydrated } from 'src/types'; import { asDateString } from 'src/utils/date'; +import z from 'zod'; -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; +export const ExifResponseSchema = z + .object({ + make: z.string().nullish().default(null).describe('Camera make'), + model: z.string().nullish().default(null).describe('Camera model'), + exifImageWidth: z.number().min(0).nullish().default(null).describe('Image width in pixels'), + exifImageHeight: z.number().min(0).nullish().default(null).describe('Image height in pixels'), + fileSizeInByte: z.int().min(0).nullish().default(null).describe('File size in bytes'), + orientation: z.string().nullish().default(null).describe('Image orientation'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + dateTimeOriginal: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Original date/time'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + modifyDate: z.string().meta({ format: 'date-time' }).nullish().default(null).describe('Modification date/time'), + timeZone: z.string().nullish().default(null).describe('Time zone'), + lensModel: z.string().nullish().default(null).describe('Lens model'), + fNumber: z.number().nullish().default(null).describe('F-number (aperture)'), + focalLength: z.number().nullish().default(null).describe('Focal length in mm'), + iso: z.number().nullish().default(null).describe('ISO sensitivity'), + exposureTime: z.string().nullish().default(null).describe('Exposure time'), + latitude: z.number().nullish().default(null).describe('GPS latitude'), + longitude: z.number().nullish().default(null).describe('GPS longitude'), + city: z.string().nullish().default(null).describe('City name'), + state: z.string().nullish().default(null).describe('State/province name'), + country: z.string().nullish().default(null).describe('Country name'), + description: z.string().nullish().default(null).describe('Image description'), + projectionType: z.string().nullish().default(null).describe('Projection type'), + rating: z.number().nullish().default(null).describe('Rating'), + }) + .describe('EXIF response') + .meta({ id: 'ExifResponseDto' }); - @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?: string | null = null; - @ApiPropertyOptional({ description: 'Modification date/time', format: 'date-time' }) - modifyDate?: string | 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; -} +class ExifResponseDto extends createZodDto(ExifResponseSchema) {} export function mapExif(entity: MaybeDehydrated): ExifResponseDto { return { @@ -77,16 +62,3 @@ export function mapExif(entity: MaybeDehydrated): ExifResponseDto { rating: entity.rating, }; } - -export function mapSanitizedExif(entity: Exif): ExifResponseDto { - return { - fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null, - orientation: entity.orientation, - dateTimeOriginal: asDateString(entity.dateTimeOriginal), - timeZone: entity.timeZone, - projectionType: entity.projectionType, - exifImageWidth: entity.exifImageWidth, - exifImageHeight: entity.exifImageHeight, - rating: entity.rating, - }; -} diff --git a/server/src/dtos/job.dto.ts b/server/src/dtos/job.dto.ts index ef34a41720..325dae4d2e 100644 --- a/server/src/dtos/job.dto.ts +++ b/server/src/dtos/job.dto.ts @@ -1,7 +1,11 @@ -import { ManualJobName } from 'src/enum'; -import { ValidateEnum } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { ManualJobNameSchema } from 'src/enum'; +import z from 'zod'; -export class JobCreateDto { - @ValidateEnum({ enum: ManualJobName, name: 'ManualJobName', description: 'Job name' }) - name!: ManualJobName; -} +const JobCreateSchema = z + .object({ + name: ManualJobNameSchema, + }) + .meta({ id: 'JobCreateDto' }); + +export class JobCreateDto extends createZodDto(JobCreateSchema) {} diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index 3f71b8a0ed..aafdd9f793 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -1,58 +1,30 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Library } from 'src/database'; -import { Optional, ValidateUUID } from 'src/validation'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class CreateLibraryDto { - @ValidateUUID({ description: 'Owner user ID' }) - ownerId!: string; +const stringArrayMax128 = z + .array(z.string()) + .max(128) + .refine((arr) => arr.every((s) => s.trim() !== ''), 'Array items must not be empty') + .refine((arr) => new Set(arr).size === arr.length, 'Array must have unique items'); - @ApiPropertyOptional({ description: 'Library name' }) - @IsString() - @Optional() - @IsNotEmpty() - name?: string; +const CreateLibrarySchema = z + .object({ + ownerId: z.uuidv4().describe('Owner user ID'), + name: z.string().min(1).optional().describe('Library name'), + importPaths: stringArrayMax128.optional().describe('Import paths (max 128)'), + exclusionPatterns: stringArrayMax128.optional().describe('Exclusion patterns (max 128)'), + }) + .meta({ id: 'CreateLibraryDto' }); - @ApiPropertyOptional({ description: 'Import paths (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - importPaths?: string[]; - - @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - exclusionPatterns?: string[]; -} - -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 }) - @ArrayUnique() - @ArrayMaxSize(128) - importPaths?: string[]; - - @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) - @Optional() - @IsNotEmpty({ each: true }) - @IsString({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - exclusionPatterns?: string[]; -} +const UpdateLibrarySchema = z + .object({ + name: z.string().min(1).optional().describe('Library name'), + importPaths: stringArrayMax128.optional().describe('Import paths (max 128)'), + exclusionPatterns: stringArrayMax128.optional().describe('Exclusion patterns (max 128)'), + }) + .meta({ id: 'UpdateLibraryDto' }); export interface CrawlOptionsDto { pathsToCrawl: string[]; @@ -64,81 +36,60 @@ export interface WalkOptionsDto extends CrawlOptionsDto { take: number; } -export class ValidateLibraryDto { - @ApiPropertyOptional({ description: 'Import paths to validate (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - importPaths?: string[]; +const ValidateLibrarySchema = z + .object({ + importPaths: stringArrayMax128.optional().describe('Import paths to validate (max 128)'), + exclusionPatterns: stringArrayMax128.optional().describe('Exclusion patterns (max 128)'), + }) + .meta({ id: 'ValidateLibraryDto' }); - @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) - @Optional() - @IsNotEmpty({ each: true }) - @IsString({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - exclusionPatterns?: string[]; -} +const ValidateLibraryImportPathResponseSchema = z + .object({ + importPath: z.string().describe('Import path'), + isValid: z.boolean().describe('Is valid'), + message: z.string().optional().describe('Validation message'), + }) + .meta({ id: 'ValidateLibraryImportPathResponseDto' }); -export class ValidateLibraryResponseDto { - @ApiPropertyOptional({ description: 'Validation results for import paths' }) - importPaths?: ValidateLibraryImportPathResponseDto[]; -} +const ValidateLibraryResponseSchema = z + .object({ + importPaths: z + .array(ValidateLibraryImportPathResponseSchema) + .optional() + .describe('Validation results for import paths'), + }) + .meta({ id: 'ValidateLibraryResponseDto' }); -export class ValidateLibraryImportPathResponseDto { - @ApiProperty({ description: 'Import path' }) - importPath!: string; - @ApiProperty({ description: 'Is valid' }) - isValid: boolean = false; - @ApiPropertyOptional({ description: 'Validation message' }) - message?: string; -} +const LibraryResponseSchema = z + .object({ + id: z.string().describe('Library ID'), + ownerId: z.string().describe('Owner user ID'), + name: z.string().describe('Library name'), + assetCount: z.int().describe('Number of assets'), + importPaths: z.array(z.string()).describe('Import paths'), + exclusionPatterns: z.array(z.string()).describe('Exclusion patterns'), + createdAt: isoDatetimeToDate.describe('Creation date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + refreshedAt: isoDatetimeToDate.nullable().describe('Last refresh date'), + }) + .meta({ id: 'LibraryResponseDto' }); -export class LibrarySearchDto { - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} +const LibraryStatsResponseSchema = z + .object({ + photos: z.int().describe('Number of photos'), + videos: z.int().describe('Number of videos'), + total: z.int().describe('Total number of assets'), + usage: z.int().describe('Storage usage in bytes'), + }) + .meta({ id: 'LibraryStatsResponseDto' }); -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', 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', description: 'Number of photos' }) - photos = 0; - - @ApiProperty({ type: 'integer', description: 'Number of videos' }) - videos = 0; - - @ApiProperty({ type: 'integer', description: 'Total number of assets' }) - total = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' }) - usage = 0; -} +export class CreateLibraryDto extends createZodDto(CreateLibrarySchema) {} +export class UpdateLibraryDto extends createZodDto(UpdateLibrarySchema) {} +export class ValidateLibraryDto extends createZodDto(ValidateLibrarySchema) {} +export class ValidateLibraryResponseDto extends createZodDto(ValidateLibraryResponseSchema) {} +export class ValidateLibraryImportPathResponseDto extends createZodDto(ValidateLibraryImportPathResponseSchema) {} +export class LibraryResponseDto extends createZodDto(LibraryResponseSchema) {} +export class LibraryStatsResponseDto extends createZodDto(LibraryStatsResponseSchema) {} export function mapLibrary(entity: Library): LibraryResponseDto { let assetCount = 0; diff --git a/server/src/dtos/license.dto.ts b/server/src/dtos/license.dto.ts index 14232940b6..a68905fb47 100644 --- a/server/src/dtos/license.dto.ts +++ b/server/src/dtos/license.dto.ts @@ -1,20 +1,12 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, Matches } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; +import { UserLicenseSchema } from 'src/dtos/user.dto'; -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; +const LicenseKeySchema = UserLicenseSchema.pick({ + licenseKey: true, + activationKey: true, +}).meta({ id: 'LicenseKeyDto' }); - @ApiProperty({ description: 'Activation key' }) - @IsString() - @IsNotEmpty() - activationKey!: string; -} +const LicenseResponseSchema = UserLicenseSchema.meta({ id: 'LicenseResponseDto' }); -export class LicenseResponseDto extends LicenseKeyDto { - @ApiProperty({ description: 'Activation date' }) - activatedAt!: Date; -} +export class LicenseKeyDto extends createZodDto(LicenseKeySchema) {} +export class LicenseResponseDto extends createZodDto(LicenseResponseSchema) {} diff --git a/server/src/dtos/maintenance.dto.ts b/server/src/dtos/maintenance.dto.ts index f31d9ffa23..9b1c0b63c0 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -1,49 +1,57 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ValidateIf } from 'class-validator'; -import { MaintenanceAction, StorageFolder } from 'src/enum'; -import { ValidateBoolean, ValidateEnum, ValidateString } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { MaintenanceAction, MaintenanceActionSchema, StorageFolderSchema } from 'src/enum'; +import z from 'zod'; -export class SetMaintenanceModeDto { - @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' }) - action!: MaintenanceAction; +const SetMaintenanceModeSchema = z + .object({ + action: MaintenanceActionSchema, + restoreBackupFilename: z.string().optional().describe('Restore backup filename'), + }) + .refine( + (data) => data.action !== MaintenanceAction.RestoreDatabase || (data.restoreBackupFilename?.length ?? 0) > 0, + { error: 'Backup filename is required when action is restore_database', path: ['restoreBackupFilename'] }, + ) + .meta({ id: 'SetMaintenanceModeDto' }); - @ValidateIf((o) => o.action === MaintenanceAction.RestoreDatabase) - @ValidateString({ description: 'Restore backup filename' }) - restoreBackupFilename?: string; -} +const MaintenanceLoginSchema = z + .object({ + token: z.string().optional().describe('Maintenance token'), + }) + .meta({ id: 'MaintenanceLoginDto' }); -export class MaintenanceLoginDto { - @ValidateString({ optional: true, description: 'Maintenance token' }) - token?: string; -} +const MaintenanceAuthSchema = z + .object({ + username: z.string().describe('Maintenance username'), + }) + .meta({ id: 'MaintenanceAuthDto' }); -export class MaintenanceAuthDto { - @ApiProperty({ description: 'Maintenance username' }) - username!: string; -} +const MaintenanceStatusResponseSchema = z + .object({ + active: z.boolean(), + action: MaintenanceActionSchema, + progress: z.number().optional(), + task: z.string().optional(), + error: z.string().optional(), + }) + .meta({ id: 'MaintenanceStatusResponseDto' }); -export class MaintenanceStatusResponseDto { - active!: boolean; +const MaintenanceDetectInstallStorageFolderSchema = z + .object({ + folder: StorageFolderSchema, + readable: z.boolean().describe('Whether the folder is readable'), + writable: z.boolean().describe('Whether the folder is writable'), + files: z.number().describe('Number of files in the folder'), + }) + .meta({ id: 'MaintenanceDetectInstallStorageFolderDto' }); - @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' }) - action!: MaintenanceAction; +const MaintenanceDetectInstallResponseSchema = z + .object({ + storage: z.array(MaintenanceDetectInstallStorageFolderSchema), + }) + .meta({ id: 'MaintenanceDetectInstallResponseDto' }); - progress?: number; - task?: string; - error?: string; -} - -export class MaintenanceDetectInstallStorageFolderDto { - @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; -} - -export class MaintenanceDetectInstallResponseDto { - storage!: MaintenanceDetectInstallStorageFolderDto[]; -} +export class SetMaintenanceModeDto extends createZodDto(SetMaintenanceModeSchema) {} +export class MaintenanceLoginDto extends createZodDto(MaintenanceLoginSchema) {} +export class MaintenanceAuthDto extends createZodDto(MaintenanceAuthSchema) {} +export class MaintenanceStatusResponseDto extends createZodDto(MaintenanceStatusResponseSchema) {} +export class MaintenanceDetectInstallResponseDto extends createZodDto(MaintenanceDetectInstallResponseSchema) {} diff --git a/server/src/dtos/map.dto.ts b/server/src/dtos/map.dto.ts index d8db175c28..6a4776d49d 100644 --- a/server/src/dtos/map.dto.ts +++ b/server/src/dtos/map.dto.ts @@ -1,67 +1,45 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsLatitude, IsLongitude } from 'class-validator'; -import { ValidateBoolean, ValidateDate } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { isoDatetimeToDate, latitudeSchema, longitudeSchema, stringToBool } from 'src/validation'; +import z from 'zod'; -export class MapReverseGeocodeDto { - @ApiProperty({ format: 'double', description: 'Latitude (-90 to 90)' }) - @Type(() => Number) - @IsLatitude({ message: ({ property }) => `${property} must be a number between -90 and 90` }) - lat!: number; +const MapReverseGeocodeSchema = z + .object({ + lat: z.coerce.number().meta({ format: 'double' }).pipe(latitudeSchema).describe('Latitude (-90 to 90)'), + lon: z.coerce.number().meta({ format: 'double' }).pipe(longitudeSchema).describe('Longitude (-180 to 180)'), + }) + .meta({ id: 'MapReverseGeocodeDto' }); - @ApiProperty({ format: 'double', description: 'Longitude (-180 to 180)' }) - @Type(() => Number) - @IsLongitude({ message: ({ property }) => `${property} must be a number between -180 and 180` }) - lon!: number; -} +const MapReverseGeocodeResponseSchema = z + .object({ + city: z.string().nullable().describe('City name'), + state: z.string().nullable().describe('State/Province name'), + country: z.string().nullable().describe('Country name'), + }) + .meta({ id: 'MapReverseGeocodeResponseDto' }); -export class MapReverseGeocodeResponseDto { - @ApiProperty({ description: 'City name' }) - city!: string | null; +const MapMarkerSchema = z + .object({ + isArchived: stringToBool.optional().describe('Filter by archived status'), + isFavorite: stringToBool.optional().describe('Filter by favorite status'), + fileCreatedAfter: isoDatetimeToDate.optional().describe('Filter assets created after this date'), + fileCreatedBefore: isoDatetimeToDate.optional().describe('Filter assets created before this date'), + withPartners: stringToBool.optional().describe('Include partner assets'), + withSharedAlbums: stringToBool.optional().describe('Include shared album assets'), + }) + .meta({ id: 'MapMarkerDto' }); - @ApiProperty({ description: 'State/Province name' }) - state!: string | null; +const MapMarkerResponseSchema = z + .object({ + id: z.string().describe('Asset ID'), + lat: z.number().meta({ format: 'double' }).describe('Latitude'), + lon: z.number().meta({ format: 'double' }).describe('Longitude'), + city: z.string().nullable().describe('City name'), + state: z.string().nullable().describe('State/Province name'), + country: z.string().nullable().describe('Country name'), + }) + .meta({ id: 'MapMarkerResponseDto' }); - @ApiProperty({ description: 'Country name' }) - country!: string | null; -} - -export class MapMarkerDto { - @ValidateBoolean({ optional: true, description: 'Filter by archived status' }) - isArchived?: boolean; - - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; - - @ValidateDate({ optional: true, description: 'Filter assets created after this date' }) - fileCreatedAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter assets created before this date' }) - fileCreatedBefore?: Date; - - @ValidateBoolean({ optional: true, description: 'Include partner assets' }) - withPartners?: boolean; - - @ValidateBoolean({ optional: true, description: 'Include shared album assets' }) - withSharedAlbums?: boolean; -} - -export class MapMarkerResponseDto { - @ApiProperty({ description: 'Asset ID' }) - id!: string; - - @ApiProperty({ format: 'double', description: 'Latitude' }) - lat!: number; - - @ApiProperty({ format: 'double', description: 'Longitude' }) - lon!: number; - - @ApiProperty({ description: 'City name' }) - city!: string | null; - - @ApiProperty({ description: 'State/Province name' }) - state!: string | null; - - @ApiProperty({ description: 'Country name' }) - country!: string | null; -} +export class MapReverseGeocodeDto extends createZodDto(MapReverseGeocodeSchema) {} +export class MapReverseGeocodeResponseDto extends createZodDto(MapReverseGeocodeResponseSchema) {} +export class MapMarkerDto extends createZodDto(MapMarkerSchema) {} +export class MapMarkerResponseDto extends createZodDto(MapMarkerResponseSchema) {} diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index edf65ef583..334520dded 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -1,136 +1,87 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Memory } from 'src/database'; import { HistoryBuilder } from 'src/decorators'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetOrderWithRandom, MemoryType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { AssetOrderWithRandomSchema, MemoryType, MemoryTypeSchema } from 'src/enum'; +import { isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; -class MemoryBaseDto { - @ValidateBoolean({ optional: true, description: 'Is memory saved' }) - isSaved?: boolean; - - @ValidateDate({ optional: true, description: 'Date when memory was seen' }) - seenAt?: Date; -} - -export class MemorySearchDto { - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type', optional: true }) - type?: MemoryType; - - @ValidateDate({ optional: true, description: 'Filter by date' }) - for?: Date; - - @ValidateBoolean({ optional: true, description: 'Include trashed memories' }) - isTrashed?: boolean; - - @ValidateBoolean({ optional: true, description: 'Filter by saved status' }) - isSaved?: boolean; - - @IsInt() - @IsPositive() - @Type(() => Number) - @Optional() - @ApiProperty({ type: 'integer', description: 'Number of memories to return' }) - size?: number; - - @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; -} - -type MemoryData = OnThisDayDto; - -export class MemoryUpdateDto extends MemoryBaseDto { - @ValidateDate({ optional: true, description: 'Memory date' }) - memoryAt?: Date; -} - -export class MemoryCreateDto extends MemoryBaseDto { - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) - type!: MemoryType; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @IsObject() - @ValidateNested() - @Type((options) => { - switch (options?.object.type) { - case MemoryType.OnThisDay: { - return OnThisDayDto; - } - - default: { - return Object; - } - } +const MemorySearchSchema = z + .object({ + type: MemoryTypeSchema.optional(), + for: isoDatetimeToDate.optional().describe('Filter by date'), + isTrashed: stringToBool.optional().describe('Include trashed memories'), + isSaved: stringToBool.optional().describe('Filter by saved status'), + size: z.coerce.number().int().min(1).optional().describe('Number of memories to return'), + order: AssetOrderWithRandomSchema.optional(), }) - data!: MemoryData; + .meta({ id: 'MemorySearchDto' }); - @ValidateDate({ description: 'Memory date' }) - memoryAt!: Date; - - @ValidateDate({ - optional: true, - description: 'Date when memory should be shown', - history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), +const OnThisDaySchema = z + .object({ + year: z.int().min(1000).max(9999).describe('Year for on this day memory'), }) - showAt?: Date; + .meta({ id: 'OnThisDayDto' }); - @ValidateDate({ - optional: true, - description: 'Date when memory should be hidden', - history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), +type MemoryData = z.infer; + +const MemoryUpdateSchema = z + .object({ + isSaved: z.boolean().optional().describe('Is memory saved'), + seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'), + memoryAt: isoDatetimeToDate.optional().describe('Memory date'), }) - hideAt?: Date; + .meta({ id: 'MemoryUpdateDto' }); - @ValidateUUID({ optional: true, each: true, description: 'Asset IDs to associate with memory' }) - assetIds?: string[]; -} +const MemoryCreateSchema = z + .object({ + type: MemoryTypeSchema, + data: OnThisDaySchema, + memoryAt: isoDatetimeToDate.describe('Memory date'), + assetIds: z.array(z.uuidv4()).optional().describe('Asset IDs to associate with memory'), + isSaved: z.boolean().optional().describe('Is memory saved'), + seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'), + showAt: isoDatetimeToDate + .optional() + .describe('Date when memory should be shown') + .meta(new HistoryBuilder().added('v2.6.0').stable('v2.6.0').getExtensions()), + hideAt: isoDatetimeToDate + .optional() + .describe('Date when memory should be hidden') + .meta(new HistoryBuilder().added('v2.6.0').stable('v2.6.0').getExtensions()), + }) + .meta({ id: 'MemoryCreateDto' }); -export class MemoryStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of memories' }) - total!: number; -} +const MemoryStatisticsResponseSchema = z + .object({ + total: z.int().describe('Total number of memories'), + }) + .meta({ id: 'MemoryStatisticsResponseDto' }); -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', 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[]; -} +const MemoryResponseSchema = z + .object({ + id: z.string().describe('Memory ID'), + createdAt: isoDatetimeToDate.describe('Creation date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + deletedAt: isoDatetimeToDate.optional().describe('Deletion date'), + memoryAt: isoDatetimeToDate.describe('Memory date'), + seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'), + showAt: isoDatetimeToDate.optional().describe('Date when memory should be shown'), + hideAt: isoDatetimeToDate.optional().describe('Date when memory should be hidden'), + ownerId: z.string().describe('Owner user ID'), + type: MemoryTypeSchema, + data: OnThisDaySchema, + isSaved: z.boolean().describe('Is memory saved'), + assets: z.array(AssetResponseSchema), + }) + .meta({ id: 'MemoryResponseDto' }); + +export class MemorySearchDto extends createZodDto(MemorySearchSchema) {} +export class MemoryUpdateDto extends createZodDto(MemoryUpdateSchema) {} +export class MemoryCreateDto extends createZodDto(MemoryCreateSchema) {} +export class MemoryStatisticsResponseDto extends createZodDto(MemoryStatisticsResponseSchema) {} +export class MemoryResponseDto extends createZodDto(MemoryResponseSchema) {} export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => { return { diff --git a/server/src/dtos/model-config.dto.ts b/server/src/dtos/model-config.dto.ts index a75808f95a..2ba6f0c365 100644 --- a/server/src/dtos/model-config.dto.ts +++ b/server/src/dtos/model-config.dto.ts @@ -1,83 +1,57 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; -import { ValidateBoolean } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class TaskConfig { - @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; -} - -export class CLIPConfig extends ModelConfig {} - -export class DuplicateDetectionConfig extends TaskConfig { - @IsNumber() - @Min(0.001) - @Max(0.1) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Maximum distance threshold for duplicate detection', +const TaskConfigSchema = z + .object({ + enabled: z.boolean().describe('Whether the task is enabled'), }) - maxDistance!: number; -} + .meta({ id: 'TaskConfig' }); -export class FacialRecognitionConfig extends ModelConfig { - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for face detection' }) - minScore!: number; +const ModelConfigSchema = TaskConfigSchema.extend({ + modelName: z.string().describe('Name of the model to use'), +}); - @IsNumber() - @Min(0.1) - @Max(2) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Maximum distance threshold for face recognition', - }) - maxDistance!: number; +export const CLIPConfigSchema = ModelConfigSchema.meta({ id: 'CLIPConfig' }); - @IsNumber() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Minimum number of faces required for recognition' }) - minFaces!: number; -} +export const DuplicateDetectionConfigSchema = TaskConfigSchema.extend({ + maxDistance: z + .number() + .meta({ format: 'double' }) + .min(0.001) + .max(0.1) + .describe('Maximum distance threshold for duplicate detection'), +}).meta({ id: 'DuplicateDetectionConfig' }); -export class OcrConfig extends ModelConfig { - @IsNumber() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Maximum resolution for OCR processing' }) - maxResolution!: number; +export const FacialRecognitionConfigSchema = ModelConfigSchema.extend({ + minScore: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(1) + .describe('Minimum confidence score for face detection'), + maxDistance: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(2) + .describe('Maximum distance threshold for face recognition'), + minFaces: z.int().min(1).describe('Minimum number of faces required for recognition'), +}).meta({ id: 'FacialRecognitionConfig' }); - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for text detection' }) - minDetectionScore!: number; +export const OcrConfigSchema = ModelConfigSchema.extend({ + maxResolution: z.int().min(1).describe('Maximum resolution for OCR processing'), + minDetectionScore: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(1) + .describe('Minimum confidence score for text detection'), + minRecognitionScore: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(1) + .describe('Minimum confidence score for text recognition'), +}).meta({ id: 'OcrConfig' }); - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Minimum confidence score for text recognition', - }) - minRecognitionScore!: number; -} +export class CLIPConfig extends createZodDto(CLIPConfigSchema) {} diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts index 87a15f29e3..f474cfc0a1 100644 --- a/server/src/dtos/notification.dto.ts +++ b/server/src/dtos/notification.dto.ts @@ -1,118 +1,91 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ArrayMinSize, IsString } from 'class-validator'; -import { NotificationLevel, NotificationType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { NotificationLevel, NotificationLevelSchema, NotificationType, NotificationTypeSchema } from 'src/enum'; +import { isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; -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({ description: 'Creation date' }) - createdAt!: Date; - @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', description: 'Notification level' }) - level!: NotificationLevel; - @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, description: 'Filter by notification ID' }) - id?: string; - - @ValidateEnum({ - enum: NotificationLevel, - name: 'NotificationLevel', - optional: true, - description: 'Filter by notification level', +const TestEmailResponseSchema = z + .object({ + messageId: z.string().describe('Email message ID'), }) - level?: NotificationLevel; + .meta({ id: 'TestEmailResponseDto' }); - @ValidateEnum({ - enum: NotificationType, - name: 'NotificationType', - optional: true, - description: 'Filter by notification type', +const TemplateResponseSchema = z + .object({ + name: z.string().describe('Template name'), + html: z.string().describe('Template HTML content'), }) - type?: NotificationType; + .meta({ id: 'TemplateResponseDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by unread status' }) - unread?: boolean; -} - -export class NotificationCreateDto { - @ValidateEnum({ - enum: NotificationLevel, - name: 'NotificationLevel', - optional: true, - description: 'Notification level', +const TemplateSchema = z + .object({ + template: z.string().describe('Template name'), }) - level?: NotificationLevel; + .meta({ id: 'TemplateDto' }); - @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true, description: 'Notification type' }) - type?: NotificationType; +const NotificationSchema = z + .object({ + id: z.string().describe('Notification ID'), + createdAt: isoDatetimeToDate.describe('Creation date'), + level: NotificationLevelSchema, + type: NotificationTypeSchema, + title: z.string().describe('Notification title'), + description: z.string().optional().describe('Notification description'), + data: z.record(z.string(), z.unknown()).optional().describe('Additional notification data'), + readAt: isoDatetimeToDate.optional().describe('Date when notification was read'), + }) + .meta({ id: 'NotificationDto' }); - @ValidateString({ description: 'Notification title' }) - title!: string; +const NotificationSearchSchema = z + .object({ + id: z.uuidv4().optional().describe('Filter by notification ID'), + level: NotificationLevelSchema.optional(), + type: NotificationTypeSchema.optional(), + unread: stringToBool.optional().describe('Filter by unread status'), + }) + .meta({ id: 'NotificationSearchDto' }); - @ValidateString({ optional: true, nullable: true, description: 'Notification description' }) - description?: string | null; +const NotificationCreateSchema = z + .object({ + level: NotificationLevelSchema.optional(), + type: NotificationTypeSchema.optional(), + title: z.string().describe('Notification title'), + description: z.string().nullish().describe('Notification description'), + data: z.record(z.string(), z.unknown()).optional().describe('Additional notification data'), + readAt: isoDatetimeToDate.nullish().describe('Date when notification was read'), + userId: z.uuidv4().describe('User ID to send notification to'), + }) + .meta({ id: 'NotificationCreateDto' }); - @ApiPropertyOptional({ description: 'Additional notification data' }) - @Optional({ nullable: true }) - data?: any; +const NotificationUpdateSchema = z + .object({ + readAt: isoDatetimeToDate.nullish().describe('Date when notification was read'), + }) + .meta({ id: 'NotificationUpdateDto' }); - @ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' }) - readAt?: Date | null; +const NotificationUpdateAllSchema = z + .object({ + ids: z.array(z.uuidv4()).min(1).describe('Notification IDs to update'), + readAt: isoDatetimeToDate.nullish().describe('Date when notifications were read'), + }) + .meta({ id: 'NotificationUpdateAllDto' }); - @ValidateUUID({ description: 'User ID to send notification to' }) - userId!: string; -} +const NotificationDeleteAllSchema = z + .object({ + ids: z.array(z.uuidv4()).min(1).describe('Notification IDs to delete'), + }) + .meta({ id: 'NotificationDeleteAllDto' }); -export class NotificationUpdateDto { - @ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' }) - readAt?: Date | null; -} +export class TestEmailResponseDto extends createZodDto(TestEmailResponseSchema) {} +export class TemplateResponseDto extends createZodDto(TemplateResponseSchema) {} +export class TemplateDto extends createZodDto(TemplateSchema) {} +export class NotificationDto extends createZodDto(NotificationSchema) {} +export class NotificationSearchDto extends createZodDto(NotificationSearchSchema) {} +export class NotificationCreateDto extends createZodDto(NotificationCreateSchema) {} +export class NotificationUpdateDto extends createZodDto(NotificationUpdateSchema) {} +export class NotificationUpdateAllDto extends createZodDto(NotificationUpdateAllSchema) {} +export class NotificationDeleteAllDto extends createZodDto(NotificationDeleteAllSchema) {} -export class NotificationUpdateAllDto { - @ValidateUUID({ each: true, description: 'Notification IDs to update' }) - @ArrayMinSize(1) - ids!: string[]; - - @ValidateDate({ optional: true, nullable: true, description: 'Date when notifications were read' }) - readAt?: Date | null; -} - -export class NotificationDeleteAllDto { - @ValidateUUID({ each: true, description: 'Notification IDs to delete' }) - @ArrayMinSize(1) - ids!: string[]; -} - -export type MapNotification = { +type MapNotification = { id: string; createdAt: Date; updateId?: string; @@ -123,6 +96,7 @@ export type MapNotification = { description: string | null; readAt: Date | null; }; + export const mapNotification = (notification: MapNotification): NotificationDto => { return { id: notification.id, diff --git a/server/src/dtos/ocr.dto.ts b/server/src/dtos/ocr.dto.ts index 1e838d0ec0..62e32ed4af 100644 --- a/server/src/dtos/ocr.dto.ts +++ b/server/src/dtos/ocr.dto.ts @@ -1,42 +1,22 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class AssetOcrResponseDto { - @ApiProperty({ type: 'string', format: 'uuid' }) - id!: string; +const AssetOcrResponseSchema = z + .object({ + assetId: z.uuidv4(), + boxScore: z.number().meta({ format: 'double' }).describe('Confidence score for text detection box'), + id: z.uuidv4(), + text: z.string().describe('Recognized text'), + textScore: z.number().meta({ format: 'double' }).describe('Confidence score for text recognition'), + x1: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 1 (0-1)'), + x2: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 2 (0-1)'), + x3: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 3 (0-1)'), + x4: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 4 (0-1)'), + y1: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 1 (0-1)'), + y2: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 2 (0-1)'), + y3: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 3 (0-1)'), + y4: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 4 (0-1)'), + }) + .meta({ id: 'AssetOcrResponseDto' }); - @ApiProperty({ type: 'string', format: 'uuid' }) - assetId!: string; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 1 (0-1)' }) - x1!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 1 (0-1)' }) - y1!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 2 (0-1)' }) - x2!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 2 (0-1)' }) - y2!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 3 (0-1)' }) - x3!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 3 (0-1)' }) - y3!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 4 (0-1)' }) - x4!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 4 (0-1)' }) - y4!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Confidence score for text detection box' }) - boxScore!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Confidence score for text recognition' }) - textScore!: number; - - @ApiProperty({ type: 'string', description: 'Recognized text' }) - text!: string; -} +export class AssetOcrResponseDto extends createZodDto(AssetOcrResponseSchema) {} diff --git a/server/src/dtos/onboarding.dto.ts b/server/src/dtos/onboarding.dto.ts index d2781c6b90..ae26f5e88a 100644 --- a/server/src/dtos/onboarding.dto.ts +++ b/server/src/dtos/onboarding.dto.ts @@ -1,8 +1,10 @@ -import { ValidateBoolean } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class OnboardingDto { - @ValidateBoolean({ description: 'Is user onboarded' }) - isOnboarded!: boolean; -} +const OnboardingSchema = z.object({ + isOnboarded: z.boolean().describe('Is user onboarded'), +}); + +export class OnboardingDto extends createZodDto(OnboardingSchema) {} export class OnboardingResponseDto extends OnboardingDto {} diff --git a/server/src/dtos/partner.dto.ts b/server/src/dtos/partner.dto.ts index 5b949326a4..049cf2b25e 100644 --- a/server/src/dtos/partner.dto.ts +++ b/server/src/dtos/partner.dto.ts @@ -1,26 +1,35 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty } from 'class-validator'; -import { UserResponseDto } from 'src/dtos/user.dto'; +import { createZodDto } from 'nestjs-zod'; +import { UserResponseSchema } from 'src/dtos/user.dto'; import { PartnerDirection } from 'src/repositories/partner.repository'; -import { ValidateEnum, ValidateUUID } from 'src/validation'; +import z from 'zod'; -export class PartnerCreateDto { - @ValidateUUID({ description: 'User ID to share with' }) - sharedWithId!: string; -} +const PartnerDirectionSchema = z.enum(PartnerDirection).describe('Partner direction').meta({ id: 'PartnerDirection' }); -export class PartnerUpdateDto { - @ApiProperty({ description: 'Show partner assets in timeline' }) - @IsNotEmpty() - inTimeline!: boolean; -} +const PartnerCreateSchema = z + .object({ + sharedWithId: z.uuidv4().describe('User ID to share with'), + }) + .meta({ id: 'PartnerCreateDto' }); -export class PartnerSearchDto { - @ValidateEnum({ enum: PartnerDirection, name: 'PartnerDirection', description: 'Partner direction' }) - direction!: PartnerDirection; -} +const PartnerUpdateSchema = z + .object({ + inTimeline: z.boolean().describe('Show partner assets in timeline'), + }) + .meta({ id: 'PartnerUpdateDto' }); -export class PartnerResponseDto extends UserResponseDto { - @ApiPropertyOptional({ description: 'Show in timeline' }) - inTimeline?: boolean; -} +const PartnerSearchSchema = z + .object({ + direction: PartnerDirectionSchema, + }) + .meta({ id: 'PartnerSearchDto' }); + +const PartnerResponseSchema = UserResponseSchema.extend({ + inTimeline: z.boolean().optional().describe('Show in timeline'), +}) + .describe('Partner response') + .meta({ id: 'PartnerResponseDto' }); + +export class PartnerCreateDto extends createZodDto(PartnerCreateSchema) {} +export class PartnerUpdateDto extends createZodDto(PartnerUpdateSchema) {} +export class PartnerSearchDto extends createZodDto(PartnerSearchSchema) {} +export class PartnerResponseDto extends createZodDto(PartnerResponseSchema) {} diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 477166d3d5..1f8f080905 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -1,230 +1,184 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, IsInt, IsNotEmpty, IsNumber, IsString, Max, Min, ValidateNested } from 'class-validator'; import { Selectable } from 'kysely'; -import { DateTime } from 'luxon'; +import { createZodDto } from 'nestjs-zod'; import { AssetFace, Person } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; +import { HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; -import { SourceType } from 'src/enum'; +import { SourceTypeSchema } from 'src/enum'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { ImageDimensions, MaybeDehydrated } from 'src/types'; import { asBirthDateString, asDateString } from 'src/utils/date'; import { transformFaceBoundingBox } from 'src/utils/transform'; -import { - IsDateStringFormat, - MaxDateString, - Optional, - ValidateBoolean, - ValidateEnum, - ValidateHexColor, - ValidateUUID, -} from 'src/validation'; +import { emptyStringToNull, hexColor, stringToBool } from 'src/validation'; +import z from 'zod'; -export class PersonCreateDto { - @ApiPropertyOptional({ description: 'Person name' }) - @Optional() - @IsString() - name?: string; - - // 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?: string | null; - - @ValidateBoolean({ optional: true, description: 'Person visibility (hidden)' }) - isHidden?: boolean; - - @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 { - @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) - people!: PeopleUpdateItem[]; -} - -export class PeopleUpdateItem extends PersonUpdateDto { - @ApiProperty({ description: 'Person ID' }) - @IsString() - @IsNotEmpty() - id!: string; -} - -export class MergePersonDto { - @ValidateUUID({ each: true, description: 'Person IDs to merge' }) - ids!: string[]; -} - -export class PersonSearchDto { - @ValidateBoolean({ optional: true, description: 'Include hidden people' }) - withHidden?: boolean; - @ValidateUUID({ optional: true, description: 'Closest person ID for similarity search' }) - closestPersonId?: string; - @ValidateUUID({ optional: true, description: 'Closest asset ID for similarity search' }) - closestAssetId?: string; - - @ApiPropertyOptional({ description: 'Page number for pagination', default: 1 }) - @IsInt() - @Min(1) - @Type(() => Number) - page: number = 1; - - @ApiPropertyOptional({ description: 'Number of items per page', default: 500 }) - @IsInt() - @Min(1) - @Max(1000) - @Type(() => Number) - size: number = 500; -} - -export class PersonResponseDto { - @ApiProperty({ description: 'Person ID' }) - id!: string; - @ApiProperty({ description: 'Person name' }) - name!: string; - @ApiProperty({ format: 'date', description: 'Person date of birth' }) - birthDate!: string | null; - @ApiProperty({ description: 'Thumbnail path' }) - thumbnailPath!: string; - @ApiProperty({ description: 'Is hidden' }) - isHidden!: boolean; - @Property({ - description: 'Last update date', - format: 'date-time', - history: new HistoryBuilder().added('v1.107.0').stable('v2'), +const PersonCreateSchema = z + .object({ + name: z.string().optional().describe('Person name'), + // Note: the mobile app cannot currently set the birth date to null. + birthDate: emptyStringToNull(z.string().meta({ format: 'date' }).nullable()) + .optional() + .refine((val) => (val ? new Date(val) <= new Date() : true), { error: 'Birth date cannot be in the future' }) + .describe('Person date of birth'), + isHidden: z.boolean().optional().describe('Person visibility (hidden)'), + isFavorite: z.boolean().optional().describe('Mark as favorite'), + color: emptyStringToNull(hexColor.nullable()).optional().describe('Person color (hex)'), }) - updatedAt?: string; - @Property({ description: 'Is favorite', history: new HistoryBuilder().added('v1.126.0').stable('v2') }) - isFavorite?: boolean; - @Property({ description: 'Person color (hex)', history: new HistoryBuilder().added('v1.126.0').stable('v2') }) - color?: string; -} + .meta({ id: 'PersonCreateDto' }); -export class PersonWithFacesResponseDto extends PersonResponseDto { - @ApiProperty({ description: 'Face detections' }) - faces!: AssetFaceWithoutPersonResponseDto[]; -} +const PersonUpdateSchema = PersonCreateSchema.extend({ + featureFaceAssetId: z.uuidv4().optional().describe('Asset ID used for feature face thumbnail'), +}).meta({ id: 'PersonUpdateDto' }); -export class AssetFaceWithoutPersonResponseDto { - @ValidateUUID({ description: 'Face ID' }) - id!: string; - @ApiProperty({ type: 'integer', description: 'Image height in pixels' }) - imageHeight!: number; - @ApiProperty({ type: 'integer', description: 'Image width in pixels' }) - imageWidth!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box X1 coordinate' }) - boundingBoxX1!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box X2 coordinate' }) - boundingBoxX2!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box Y1 coordinate' }) - boundingBoxY1!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box Y2 coordinate' }) - boundingBoxY2!: number; - @ValidateEnum({ enum: SourceType, name: 'SourceType', optional: true, description: 'Face detection source type' }) - sourceType?: SourceType; -} +const PeopleUpdateItemSchema = PersonUpdateSchema.extend({ + id: z.string().describe('Person ID'), +}).meta({ id: 'PeopleUpdateItem' }); -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) - data!: AssetFaceUpdateItem[]; -} - -export class FaceDto { - @ValidateUUID({ description: 'Face ID' }) - id!: string; -} - -export class AssetFaceUpdateItem { - @ValidateUUID({ description: 'Person ID' }) - personId!: string; - - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; -} - -export class AssetFaceCreateDto extends AssetFaceUpdateItem { - @ApiProperty({ type: 'integer', description: 'Image width in pixels' }) - @IsNotEmpty() - @IsNumber() - imageWidth!: number; - - @ApiProperty({ type: 'integer', description: 'Image height in pixels' }) - @IsNotEmpty() - @IsNumber() - imageHeight!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box X coordinate' }) - @IsNotEmpty() - @IsNumber() - x!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box Y coordinate' }) - @IsNotEmpty() - @IsNumber() - y!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box width' }) - @IsNotEmpty() - @IsNumber() - width!: number; - - @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', description: 'Number of assets' }) - assets!: number; -} - -export class PeopleResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of people' }) - total!: number; - @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({ - description: 'Whether there are more pages', - history: new HistoryBuilder().added('v1.110.0').stable('v2'), +const PeopleUpdateSchema = z + .object({ + people: z.array(PeopleUpdateItemSchema).describe('People to update'), }) - hasNextPage?: boolean; -} + .meta({ id: 'PeopleUpdateDto' }); + +const MergePersonSchema = z + .object({ + ids: z.array(z.uuidv4()).describe('Person IDs to merge'), + }) + .meta({ id: 'MergePersonDto' }); + +const PersonSearchSchema = z + .object({ + withHidden: stringToBool.optional().describe('Include hidden people'), + closestPersonId: z.uuidv4().optional().describe('Closest person ID for similarity search'), + closestAssetId: z.uuidv4().optional().describe('Closest asset ID for similarity search'), + page: z.coerce.number().min(1).default(1).describe('Page number for pagination'), + size: z.coerce.number().min(1).max(1000).default(500).describe('Number of items per page'), + }) + .meta({ id: 'PersonSearchDto' }); + +const PersonResponseSchema = z + .object({ + id: z.string().describe('Person ID'), + name: z.string().describe('Person name'), + // TODO: use `isoDateToDate` when using `ZodSerializerDto` on the controllers. + birthDate: z.string().meta({ format: 'date' }).describe('Person date of birth').nullable(), + thumbnailPath: z.string().describe('Thumbnail path'), + isHidden: z.boolean().describe('Is hidden'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + updatedAt: z + .string() + .meta({ format: 'date-time' }) + .optional() + .describe('Last update date') + .meta(new HistoryBuilder().added('v1.107.0').stable('v2').getExtensions()), + isFavorite: z + .boolean() + .optional() + .describe('Is favorite') + .meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()), + color: z + .string() + .optional() + .describe('Person color (hex)') + .meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()), + }) + .meta({ id: 'PersonResponseDto' }); + +export class PersonCreateDto extends createZodDto(PersonCreateSchema) {} +export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {} +export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {} +export class MergePersonDto extends createZodDto(MergePersonSchema) {} +export class PersonSearchDto extends createZodDto(PersonSearchSchema) {} +export class PersonResponseDto extends createZodDto(PersonResponseSchema) {} + +export const AssetFaceWithoutPersonResponseSchema = z + .object({ + id: z.uuidv4().describe('Face ID'), + imageHeight: z.int().min(0).describe('Image height in pixels'), + imageWidth: z.int().min(0).describe('Image width in pixels'), + boundingBoxX1: z.int().describe('Bounding box X1 coordinate'), + boundingBoxX2: z.int().describe('Bounding box X2 coordinate'), + boundingBoxY1: z.int().describe('Bounding box Y1 coordinate'), + boundingBoxY2: z.int().describe('Bounding box Y2 coordinate'), + sourceType: SourceTypeSchema.optional(), + }) + .describe('Asset face without person') + .meta({ id: 'AssetFaceWithoutPersonResponseDto' }); + +class AssetFaceWithoutPersonResponseDto extends createZodDto(AssetFaceWithoutPersonResponseSchema) {} + +export const PersonWithFacesResponseSchema = PersonResponseSchema.extend({ + faces: z.array(AssetFaceWithoutPersonResponseSchema), +}).meta({ id: 'PersonWithFacesResponseDto' }); + +export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema) {} + +const AssetFaceResponseSchema = AssetFaceWithoutPersonResponseSchema.extend({ + person: PersonResponseSchema.nullable(), +}).meta({ id: 'AssetFaceResponseDto' }); + +export class AssetFaceResponseDto extends createZodDto(AssetFaceResponseSchema) {} + +const AssetFaceUpdateItemSchema = z + .object({ + personId: z.uuidv4().describe('Person ID'), + assetId: z.uuidv4().describe('Asset ID'), + }) + .meta({ id: 'AssetFaceUpdateItem' }); + +const AssetFaceUpdateSchema = z + .object({ + data: z.array(AssetFaceUpdateItemSchema).describe('Face update items'), + }) + .meta({ id: 'AssetFaceUpdateDto' }); + +const FaceSchema = z + .object({ + id: z.uuidv4().describe('Face ID'), + }) + .meta({ id: 'FaceDto' }); + +const AssetFaceCreateSchema = AssetFaceUpdateItemSchema.extend({ + imageWidth: z.int().describe('Image width in pixels'), + imageHeight: z.int().describe('Image height in pixels'), + x: z.int().describe('Face bounding box X coordinate'), + y: z.int().describe('Face bounding box Y coordinate'), + width: z.int().describe('Face bounding box width'), + height: z.int().describe('Face bounding box height'), +}).meta({ id: 'AssetFaceCreateDto' }); + +const AssetFaceDeleteSchema = z + .object({ + force: z.boolean().describe('Force delete even if person has other faces'), + }) + .meta({ id: 'AssetFaceDeleteDto' }); + +const PersonStatisticsResponseSchema = z + .object({ + assets: z.int().describe('Number of assets'), + }) + .meta({ id: 'PersonStatisticsResponseDto' }); + +export class AssetFaceUpdateDto extends createZodDto(AssetFaceUpdateSchema) {} +export class FaceDto extends createZodDto(FaceSchema) {} +export class AssetFaceCreateDto extends createZodDto(AssetFaceCreateSchema) {} +export class AssetFaceDeleteDto extends createZodDto(AssetFaceDeleteSchema) {} +export class PersonStatisticsResponseDto extends createZodDto(PersonStatisticsResponseSchema) {} + +const PeopleResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of people'), + hidden: z.int().min(0).describe('Number of hidden people'), + people: z.array(PersonResponseSchema), + // TODO: make required after a few versions + hasNextPage: z + .boolean() + .optional() + .describe('Whether there are more pages') + .meta(new HistoryBuilder().added('v1.110.0').stable('v2').getExtensions()), + }) + .describe('People response'); +export class PeopleResponseDto extends createZodDto(PeopleResponseSchema) {} export function mapPerson(person: MaybeDehydrated): PersonResponseDto { return { diff --git a/server/src/dtos/plugin-manifest.dto.ts b/server/src/dtos/plugin-manifest.dto.ts index d5d1c52997..30aa8c0a68 100644 --- a/server/src/dtos/plugin-manifest.dto.ts +++ b/server/src/dtos/plugin-manifest.dto.ts @@ -1,128 +1,56 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { - ArrayMinSize, - IsArray, - IsEnum, - IsNotEmpty, - IsObject, - IsOptional, - IsSemVer, - IsString, - Matches, - ValidateNested, -} from 'class-validator'; -import { PluginContext } from 'src/enum'; -import { JSONSchema } from 'src/types/plugin-schema.types'; -import { ValidateEnum } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { PluginContextSchema } from 'src/enum'; +import { JSONSchemaSchema } from 'src/types/plugin-schema.types'; +import z from 'zod'; -class PluginManifestWasmDto { - @ApiProperty({ description: 'WASM file path' }) - @IsString() - @IsNotEmpty() - path!: string; -} +const pluginNameRegex = /^[a-z0-9-]+[a-z0-9]$/; +const semverRegex = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; -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; - - @ArrayMinSize(1) - @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]$/, { - message: 'Plugin name must contain only lowercase letters, numbers, and hyphens, and cannot end with a hyphen', +const PluginManifestWasmSchema = z + .object({ + path: z.string().describe('WASM file path'), }) - name!: string; + .meta({ id: 'PluginManifestWasmDto' }); - @ApiProperty({ description: 'Plugin version (semver)' }) - @IsString() - @IsNotEmpty() - @IsSemVer() - version!: string; +const PluginManifestFilterSchema = z + .object({ + methodName: z.string().describe('Filter method name'), + title: z.string().describe('Filter title'), + description: z.string().describe('Filter description'), + supportedContexts: z.array(PluginContextSchema).min(1).describe('Supported contexts'), + schema: JSONSchemaSchema.optional(), + }) + .meta({ id: 'PluginManifestFilterDto' }); - @ApiProperty({ description: 'Plugin title' }) - @IsString() - @IsNotEmpty() - title!: string; +const PluginManifestActionSchema = z + .object({ + methodName: z.string().describe('Action method name'), + title: z.string().describe('Action title'), + description: z.string().describe('Action description'), + supportedContexts: z.array(PluginContextSchema).min(1).describe('Supported contexts'), + schema: JSONSchemaSchema.optional(), + }) + .meta({ id: 'PluginManifestActionDto' }); - @ApiProperty({ description: 'Plugin description' }) - @IsString() - @IsNotEmpty() - description!: string; +export const PluginManifestSchema = z + .object({ + name: z + .string() + .min(1) + .regex( + pluginNameRegex, + 'Plugin name must contain only lowercase letters, numbers, and hyphens, and cannot end with a hyphen', + ) + .describe('Plugin name (lowercase, numbers, hyphens only)'), + version: z.string().regex(semverRegex).describe('Plugin version (semver)'), + title: z.string().describe('Plugin title'), + description: z.string().describe('Plugin description'), + author: z.string().describe('Plugin author'), + wasm: PluginManifestWasmSchema, + filters: z.array(PluginManifestFilterSchema).optional().describe('Plugin filters'), + actions: z.array(PluginManifestActionSchema).optional().describe('Plugin actions'), + }) + .meta({ id: 'PluginManifestDto' }); - @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) - @IsOptional() - actions?: PluginManifestActionDto[]; -} +export class PluginManifestDto extends createZodDto(PluginManifestSchema) {} diff --git a/server/src/dtos/plugin.dto.ts b/server/src/dtos/plugin.dto.ts index de1f1b28d4..2f928841cb 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -1,84 +1,59 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { PluginAction, PluginFilter } from 'src/database'; -import { PluginContext as PluginContextType, PluginTriggerType } from 'src/enum'; -import type { JSONSchema } from 'src/types/plugin-schema.types'; -import { ValidateEnum } from 'src/validation'; +import { PluginContextSchema, PluginTriggerTypeSchema } from 'src/enum'; +import { JSONSchemaSchema } from 'src/types/plugin-schema.types'; +import z from 'zod'; -export class PluginTriggerResponseDto { - @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Trigger type' }) - type!: PluginTriggerType; - @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', description: 'Context type' }) - contextType!: PluginContextType; -} +const PluginTriggerResponseSchema = z + .object({ + type: PluginTriggerTypeSchema, + contextType: PluginContextSchema, + }) + .meta({ id: 'PluginTriggerResponseDto' }); -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[]; -} +const PluginFilterResponseSchema = z + .object({ + id: z.string().describe('Filter ID'), + pluginId: z.string().describe('Plugin ID'), + methodName: z.string().describe('Method name'), + title: z.string().describe('Filter title'), + description: z.string().describe('Filter description'), + supportedContexts: z.array(PluginContextSchema).describe('Supported contexts'), + schema: JSONSchemaSchema.nullable().describe('Filter schema'), + }) + .meta({ id: 'PluginFilterResponseDto' }); -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; +const PluginActionResponseSchema = z + .object({ + id: z.string().describe('Action ID'), + pluginId: z.string().describe('Plugin ID'), + methodName: z.string().describe('Method name'), + title: z.string().describe('Action title'), + description: z.string().describe('Action description'), + supportedContexts: z.array(PluginContextSchema).describe('Supported contexts'), + schema: JSONSchemaSchema.nullable().describe('Action schema'), + }) + .meta({ id: 'PluginActionResponseDto' }); - @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' }) - supportedContexts!: PluginContextType[]; - @ApiProperty({ description: 'Filter schema' }) - schema!: JSONSchema | null; -} +const PluginResponseSchema = z + .object({ + id: z.string().describe('Plugin ID'), + name: z.string().describe('Plugin name'), + title: z.string().describe('Plugin title'), + description: z.string().describe('Plugin description'), + author: z.string().describe('Plugin author'), + version: z.string().describe('Plugin version'), + createdAt: z.string().describe('Creation date'), + updatedAt: z.string().describe('Last update date'), + filters: z.array(PluginFilterResponseSchema).describe('Plugin filters'), + actions: z.array(PluginActionResponseSchema).describe('Plugin actions'), + }) + .meta({ id: 'PluginResponseDto' }); -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; +export class PluginTriggerResponseDto extends createZodDto(PluginTriggerResponseSchema) {} +export class PluginResponseDto extends createZodDto(PluginResponseSchema) {} - @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; -} - -export type MapPlugin = { +type MapPlugin = { id: string; name: string; title: string; diff --git a/server/src/dtos/queue-legacy.dto.ts b/server/src/dtos/queue-legacy.dto.ts index 993160a03b..dbbcec2da5 100644 --- a/server/src/dtos/queue-legacy.dto.ts +++ b/server/src/dtos/queue-legacy.dto.ts @@ -1,79 +1,47 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { QueueResponseDto, QueueStatisticsDto } from 'src/dtos/queue.dto'; +import { createZodDto } from 'nestjs-zod'; +import { QueueResponseDto, QueueStatisticsSchema } from 'src/dtos/queue.dto'; import { QueueName } from 'src/enum'; +import z from 'zod'; -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; -} +const QueueStatusLegacySchema = z + .object({ + isActive: z.boolean().describe('Whether the queue is currently active (has running jobs)'), + isPaused: z.boolean().describe('Whether the queue is paused'), + }) + .meta({ id: 'QueueStatusLegacyDto' }); -export class QueueResponseLegacyDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - queueStatus!: QueueStatusLegacyDto; +const QueueResponseLegacySchema = z + .object({ + queueStatus: QueueStatusLegacySchema, + jobCounts: QueueStatisticsSchema, + }) + .meta({ id: 'QueueResponseLegacyDto' }); - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - jobCounts!: QueueStatisticsDto; -} +const QueuesResponseLegacySchema = z + .object({ + [QueueName.ThumbnailGeneration]: QueueResponseLegacySchema, + [QueueName.MetadataExtraction]: QueueResponseLegacySchema, + [QueueName.VideoConversion]: QueueResponseLegacySchema, + [QueueName.SmartSearch]: QueueResponseLegacySchema, + [QueueName.StorageTemplateMigration]: QueueResponseLegacySchema, + [QueueName.Migration]: QueueResponseLegacySchema, + [QueueName.BackgroundTask]: QueueResponseLegacySchema, + [QueueName.Search]: QueueResponseLegacySchema, + [QueueName.DuplicateDetection]: QueueResponseLegacySchema, + [QueueName.FaceDetection]: QueueResponseLegacySchema, + [QueueName.FacialRecognition]: QueueResponseLegacySchema, + [QueueName.Sidecar]: QueueResponseLegacySchema, + [QueueName.Library]: QueueResponseLegacySchema, + [QueueName.Notification]: QueueResponseLegacySchema, + [QueueName.BackupDatabase]: QueueResponseLegacySchema, + [QueueName.Ocr]: QueueResponseLegacySchema, + [QueueName.Workflow]: QueueResponseLegacySchema, + [QueueName.Editor]: QueueResponseLegacySchema, + }) + .meta({ id: 'QueuesResponseLegacyDto' }); -export class QueuesResponseLegacyDto implements Record { - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.ThumbnailGeneration]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.MetadataExtraction]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.VideoConversion]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.SmartSearch]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.StorageTemplateMigration]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Migration]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.BackgroundTask]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Search]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.DuplicateDetection]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.FaceDetection]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.FacialRecognition]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Sidecar]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Library]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Notification]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.BackupDatabase]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Ocr]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Workflow]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Editor]!: QueueResponseLegacyDto; -} +export class QueueResponseLegacyDto extends createZodDto(QueueResponseLegacySchema) {} +export class QueuesResponseLegacyDto extends createZodDto(QueuesResponseLegacySchema) {} export const mapQueueLegacy = (response: QueueResponseDto): QueueResponseLegacyDto => { return { diff --git a/server/src/dtos/queue.dto.ts b/server/src/dtos/queue.dto.ts index 7893581444..2147f60bde 100644 --- a/server/src/dtos/queue.dto.ts +++ b/server/src/dtos/queue.dto.ts @@ -1,82 +1,76 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; import { HistoryBuilder } from 'src/decorators'; -import { JobName, QueueCommand, QueueJobStatus, QueueName } from 'src/enum'; -import { ValidateBoolean, ValidateEnum } from 'src/validation'; +import { JobNameSchema, QueueCommandSchema, QueueJobStatusSchema, QueueNameSchema } from 'src/enum'; +import z from 'zod'; -export class QueueNameParamDto { - @ValidateEnum({ enum: QueueName, name: 'QueueName', description: 'Queue name' }) - name!: QueueName; -} - -export class QueueCommandDto { - @ValidateEnum({ enum: QueueCommand, name: 'QueueCommand', description: 'Queue command to execute' }) - command!: QueueCommand; - - @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, description: 'Whether to pause the queue' }) - isPaused?: boolean; -} - -export class QueueDeleteDto { - @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'), +const QueueNameParamSchema = z + .object({ + name: QueueNameSchema, }) - failed?: boolean; -} + .meta({ id: 'QueueNameParamDto' }); -export class QueueJobSearchDto { - @ValidateEnum({ - enum: QueueJobStatus, - name: 'QueueJobStatus', - optional: true, - each: true, - description: 'Filter jobs by status', +const QueueCommandSchemaDto = z + .object({ + command: QueueCommandSchema, + force: z.boolean().optional().describe('Force the command execution (if applicable)'), }) - status?: QueueJobStatus[]; -} -export class QueueJobResponseDto { - @ApiPropertyOptional({ description: 'Job ID' }) - id?: string; + .meta({ id: 'QueueCommandDto' }); - @ValidateEnum({ enum: JobName, name: 'JobName', description: 'Job name' }) - name!: JobName; +const QueueUpdateSchema = z + .object({ + isPaused: z.boolean().optional().describe('Whether to pause the queue'), + }) + .meta({ id: 'QueueUpdateDto' }); - @ApiProperty({ description: 'Job data payload', type: Object }) - data!: object; +const QueueDeleteSchema = z + .object({ + failed: z + .boolean() + .optional() + .describe('If true, will also remove failed jobs from the queue.') + .meta(new HistoryBuilder().added('v2.4.0').alpha('v2.4.0').getExtensions()), + }) + .meta({ id: 'QueueDeleteDto' }); - @ApiProperty({ type: 'integer', description: 'Job creation timestamp' }) - timestamp!: number; -} +const QueueJobSearchSchema = z + .object({ + status: z.array(QueueJobStatusSchema).optional().describe('Filter jobs by status'), + }) + .meta({ id: 'QueueJobSearchDto' }); -export class QueueStatisticsDto { - @ApiProperty({ type: 'integer', description: 'Number of active jobs' }) - active!: number; - @ApiProperty({ type: 'integer', description: 'Number of completed jobs' }) - completed!: number; - @ApiProperty({ type: 'integer', description: 'Number of failed jobs' }) - failed!: number; - @ApiProperty({ type: 'integer', description: 'Number of delayed jobs' }) - delayed!: number; - @ApiProperty({ type: 'integer', description: 'Number of waiting jobs' }) - waiting!: number; - @ApiProperty({ type: 'integer', description: 'Number of paused jobs' }) - paused!: number; -} +const QueueJobResponseSchema = z + .object({ + id: z.string().optional().describe('Job ID'), + name: JobNameSchema, + data: z.record(z.string(), z.unknown()).describe('Job data payload'), + timestamp: z.int().describe('Job creation timestamp'), + }) + .meta({ id: 'QueueJobResponseDto' }); -export class QueueResponseDto { - @ValidateEnum({ enum: QueueName, name: 'QueueName', description: 'Queue name' }) - name!: QueueName; +export const QueueStatisticsSchema = z + .object({ + active: z.int().describe('Number of active jobs'), + completed: z.int().describe('Number of completed jobs'), + failed: z.int().describe('Number of failed jobs'), + delayed: z.int().describe('Number of delayed jobs'), + waiting: z.int().describe('Number of waiting jobs'), + paused: z.int().describe('Number of paused jobs'), + }) + .meta({ id: 'QueueStatisticsDto' }); - @ValidateBoolean({ description: 'Whether the queue is paused' }) - isPaused!: boolean; +const QueueResponseSchema = z + .object({ + name: QueueNameSchema, + isPaused: z.boolean().describe('Whether the queue is paused'), + statistics: QueueStatisticsSchema, + }) + .meta({ id: 'QueueResponseDto' }); - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - statistics!: QueueStatisticsDto; -} +export class QueueNameParamDto extends createZodDto(QueueNameParamSchema) {} +export class QueueCommandDto extends createZodDto(QueueCommandSchemaDto) {} +export class QueueUpdateDto extends createZodDto(QueueUpdateSchema) {} +export class QueueDeleteDto extends createZodDto(QueueDeleteSchema) {} +export class QueueJobSearchDto extends createZodDto(QueueJobSearchSchema) {} +export class QueueJobResponseDto extends createZodDto(QueueJobResponseSchema) {} +export class QueueStatisticsDto extends createZodDto(QueueStatisticsSchema) {} +export class QueueResponseDto extends createZodDto(QueueResponseSchema) {} diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index 196e72c37e..43da0b8709 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,282 +1,157 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Place } from 'src/database'; -import { HistoryBuilder, Property } 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'; +import { HistoryBuilder } from 'src/decorators'; +import { AlbumResponseSchema } from 'src/dtos/album.dto'; +import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; +import { AssetOrder, AssetOrderSchema, AssetTypeSchema, AssetVisibilitySchema } from 'src/enum'; +import { emptyStringToNull, isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; -class BaseSearchDto { - @ValidateUUID({ optional: true, nullable: true, description: 'Library ID to filter by' }) - libraryId?: string | null; +const BaseSearchSchema = z.object({ + libraryId: z.uuidv4().nullish().describe('Library ID to filter by'), + deviceId: z.string().optional().describe('Device ID to filter by'), + type: AssetTypeSchema.optional(), + isEncoded: z.boolean().optional().describe('Filter by encoded status'), + isFavorite: z.boolean().optional().describe('Filter by favorite status'), + isMotion: z.boolean().optional().describe('Filter by motion photo status'), + isOffline: z.boolean().optional().describe('Filter by offline status'), + visibility: AssetVisibilitySchema.optional(), + createdBefore: isoDatetimeToDate.optional().describe('Filter by creation date (before)'), + createdAfter: isoDatetimeToDate.optional().describe('Filter by creation date (after)'), + updatedBefore: isoDatetimeToDate.optional().describe('Filter by update date (before)'), + updatedAfter: isoDatetimeToDate.optional().describe('Filter by update date (after)'), + trashedBefore: isoDatetimeToDate.optional().describe('Filter by trash date (before)'), + trashedAfter: isoDatetimeToDate.optional().describe('Filter by trash date (after)'), + takenBefore: isoDatetimeToDate.optional().describe('Filter by taken date (before)'), + takenAfter: isoDatetimeToDate.optional().describe('Filter by taken date (after)'), + city: emptyStringToNull(z.string().nullable()).optional().describe('Filter by city name'), + state: emptyStringToNull(z.string().nullable()).optional().describe('Filter by state/province name'), + country: emptyStringToNull(z.string().nullable()).optional().describe('Filter by country name'), + make: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera make'), + model: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera model'), + lensModel: emptyStringToNull(z.string().nullable()).optional().describe('Filter by lens model'), + isNotInAlbum: z.boolean().optional().describe('Filter assets not in any album'), + personIds: z.array(z.uuidv4()).optional().describe('Filter by person IDs'), + tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs'), + albumIds: z.array(z.uuidv4()).optional().describe('Filter by album IDs'), + rating: z + .number() + .min(-1) + .max(5) + .nullish() + .describe('Filter by rating [1-5], or null for unrated') + .meta({ + ...new HistoryBuilder() + .added('v1') + .stable('v2') + .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.') + .getExtensions(), + }), + ocr: z.string().optional().describe('Filter by OCR text content'), +}); - @ApiPropertyOptional({ description: 'Device ID to filter by' }) - @IsString() - @IsNotEmpty() - @Optional() - deviceId?: string; +const BaseSearchWithResultsSchema = BaseSearchSchema.extend({ + withDeleted: z.boolean().optional().describe('Include deleted assets'), + withExif: z.boolean().optional().describe('Include EXIF data in response'), + size: z.number().min(1).max(1000).optional().describe('Number of results to return'), +}); - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true, description: 'Asset type filter' }) - type?: AssetType; +const RandomSearchSchema = BaseSearchWithResultsSchema.extend({ + withStacked: z.boolean().optional().describe('Include stacked assets'), + withPeople: z.boolean().optional().describe('Include people data in response'), +}).meta({ id: 'RandomSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by encoded status' }) - isEncoded?: boolean; +const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({ + minFileSize: z.coerce.number().int().min(0).optional().describe('Minimum file size in bytes'), + size: z.coerce.number().min(1).max(1000).optional().describe('Number of results to return'), +}).meta({ id: 'LargeAssetSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; +const MetadataSearchSchema = RandomSearchSchema.extend({ + id: z.uuidv4().optional().describe('Filter by asset ID'), + deviceAssetId: z.string().optional().describe('Filter by device asset ID'), + description: z.string().trim().optional().describe('Filter by description text'), + checksum: z.string().optional().describe('Filter by file checksum'), + originalFileName: z.string().trim().optional().describe('Filter by original file name'), + originalPath: z.string().optional().describe('Filter by original file path'), + previewPath: z.string().optional().describe('Filter by preview file path'), + thumbnailPath: z.string().optional().describe('Filter by thumbnail file path'), + encodedVideoPath: z.string().optional().describe('Filter by encoded video file path'), + order: AssetOrderSchema.default(AssetOrder.Desc).optional().describe('Sort order'), + page: z.number().min(1).optional().describe('Page number'), +}).meta({ id: 'MetadataSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by motion photo status' }) - isMotion?: boolean; +const StatisticsSearchSchema = BaseSearchSchema.extend({ + description: z.string().trim().optional().describe('Filter by description text'), +}).meta({ id: 'StatisticsSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by offline status' }) - isOffline?: boolean; +const SmartSearchSchema = BaseSearchWithResultsSchema.extend({ + query: z.string().trim().optional().describe('Natural language search query'), + queryAssetId: z.uuidv4().optional().describe('Asset ID to use as search reference'), + language: z.string().optional().describe('Search language code'), + page: z.number().min(1).optional().describe('Page number'), +}).meta({ id: 'SmartSearchDto' }); - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true, description: 'Filter by visibility' }) - visibility?: AssetVisibility; - - @ValidateDate({ optional: true, description: 'Filter by creation date (before)' }) - createdBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by creation date (after)' }) - createdAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter by update date (before)' }) - updatedBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by update date (after)' }) - updatedAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter by trash date (before)' }) - trashedBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by trash date (after)' }) - trashedAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter by taken date (before)' }) - takenBefore?: Date; - - @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, description: 'Filter assets not in any album' }) - isNotInAlbum?: boolean; - - @ValidateUUID({ each: true, optional: true, description: 'Filter by person IDs' }) - personIds?: string[]; - - @ValidateUUID({ each: true, optional: true, nullable: true, description: 'Filter by tag IDs' }) - tagIds?: string[] | null; - - @ValidateUUID({ each: true, optional: true, description: 'Filter by album IDs' }) - albumIds?: string[]; - - @Property({ - type: 'number', - description: 'Filter by rating [1-5], or null for unrated', - minimum: -1, - maximum: 5, - history: new HistoryBuilder() - .added('v1') - .stable('v2') - .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'), +const SearchPlacesSchema = z + .object({ + name: z.string().describe('Place name to search for'), }) - @Optional({ nullable: true }) - @IsInt() - @Max(5) - @Min(-1) - rating?: number | null; + .meta({ id: 'SearchPlacesDto' }); - @ApiPropertyOptional({ description: 'Filter by OCR text content' }) - @IsString() - @IsNotEmpty() - @Optional() - ocr?: string; -} - -class BaseSearchWithResultsDto extends BaseSearchDto { - @ValidateBoolean({ optional: true, description: 'Include deleted assets' }) - withDeleted?: boolean; - - @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) - @Type(() => Number) - @Optional() - size?: number; -} - -export class RandomSearchDto extends BaseSearchWithResultsDto { - @ValidateBoolean({ optional: true, description: 'Include stacked assets' }) - withStacked?: boolean; - - @ValidateBoolean({ optional: true, description: 'Include people data in response' }) - withPeople?: boolean; -} - -export class LargeAssetSearchDto extends BaseSearchWithResultsDto { - @ApiPropertyOptional({ type: 'integer', description: 'Minimum file size in bytes', minimum: 0 }) - @Optional() - @IsInt() - @Min(0) - @Type(() => Number) - minFileSize?: number; -} - -export class MetadataSearchDto extends RandomSearchDto { - @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, description: 'Filter by description text' }) - description?: string; - - @ApiPropertyOptional({ description: 'Filter by file checksum' }) - @IsString() - @IsNotEmpty() - @Optional() - checksum?: string; - - @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, - description: 'Sort order', +const SearchPeopleSchema = z + .object({ + name: z.string().describe('Person name to search for'), + withHidden: stringToBool.optional().describe('Include hidden people'), }) - order?: AssetOrder; + .meta({ id: 'SearchPeopleDto' }); - @ApiPropertyOptional({ type: 'number', description: 'Page number', minimum: 1 }) - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - page?: number; +const PlacesResponseSchema = z + .object({ + name: z.string().describe('Place name'), + latitude: z.number().describe('Latitude coordinate'), + longitude: z.number().describe('Longitude coordinate'), + admin1name: z.string().optional().describe('Administrative level 1 name (state/province)'), + admin2name: z.string().optional().describe('Administrative level 2 name (county/district)'), + }) + .meta({ id: 'PlacesResponseDto' }); + +export enum SearchSuggestionType { + COUNTRY = 'country', + STATE = 'state', + CITY = 'city', + CAMERA_MAKE = 'camera-make', + CAMERA_MODEL = 'camera-model', + CAMERA_LENS_MODEL = 'camera-lens-model', } -export class StatisticsSearchDto extends BaseSearchDto { - @ValidateString({ optional: true, trim: true, description: 'Filter by description text' }) - description?: string; -} +const SearchSuggestionTypeSchema = z + .enum(SearchSuggestionType) + .describe('Suggestion type') + .meta({ id: 'SearchSuggestionType' }); -export class SmartSearchDto extends BaseSearchWithResultsDto { - @ValidateString({ optional: true, trim: true, description: 'Natural language search query' }) - query?: string; +const SearchSuggestionRequestSchema = z + .object({ + type: SearchSuggestionTypeSchema, + country: z.string().optional().describe('Filter by country'), + state: z.string().optional().describe('Filter by state/province'), + make: z.string().optional().describe('Filter by camera make'), + model: z.string().optional().describe('Filter by camera model'), + lensModel: z.string().optional().describe('Filter by lens model'), + includeNull: stringToBool + .optional() + .describe('Include null values in suggestions') + .meta(new HistoryBuilder().added('v1.111.0').stable('v2').getExtensions()), + }) + .meta({ id: 'SearchSuggestionRequestDto' }); - @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) - @Optional() - page?: number; -} - -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, 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; -} +export class RandomSearchDto extends createZodDto(RandomSearchSchema) {} +export class LargeAssetSearchDto extends createZodDto(LargeAssetSearchSchema) {} +export class MetadataSearchDto extends createZodDto(MetadataSearchSchema) {} +export class StatisticsSearchDto extends createZodDto(StatisticsSearchSchema) {} +export class SmartSearchDto extends createZodDto(SmartSearchSchema) {} +export class SearchPlacesDto extends createZodDto(SearchPlacesSchema) {} +export class SearchPeopleDto extends createZodDto(SearchPeopleSchema) {} +export class PlacesResponseDto extends createZodDto(PlacesResponseSchema) {} +export class SearchSuggestionRequestDto extends createZodDto(SearchSuggestionRequestSchema) {} export function mapPlaces(place: Place): PlacesResponseDto { return { @@ -288,136 +163,68 @@ export function mapPlaces(place: Place): PlacesResponseDto { }; } -export enum SearchSuggestionType { - COUNTRY = 'country', - STATE = 'state', - CITY = 'city', - CAMERA_MAKE = 'camera-make', - CAMERA_MODEL = 'camera-model', - CAMERA_LENS_MODEL = 'camera-lens-model', -} - -export class SearchSuggestionRequestDto { - @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, - description: 'Include null values in suggestions', - history: new HistoryBuilder().added('v1.111.0').stable('v2'), +const SearchFacetCountResponseSchema = z + .object({ + count: z.int().min(0).describe('Number of assets with this facet value'), + value: z.string().describe('Facet value'), }) - includeNull?: boolean; -} + .meta({ id: 'SearchFacetCountResponseDto' }); -class SearchFacetCountResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of assets with this facet value' }) - count!: number; - @ApiProperty({ description: 'Facet value' }) - value!: string; -} +const SearchFacetResponseSchema = z + .object({ + fieldName: z.string().describe('Facet field name'), + counts: z.array(SearchFacetCountResponseSchema), + }) + .meta({ id: 'SearchFacetResponseDto' }); -class SearchFacetResponseDto { - @ApiProperty({ description: 'Facet field name' }) - fieldName!: string; - @ApiProperty({ description: 'Facet counts' }) - counts!: SearchFacetCountResponseDto[]; -} +const SearchAlbumResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of matching albums'), + count: z.int().min(0).describe('Number of albums in this page'), + items: z.array(AlbumResponseSchema), + facets: z.array(SearchFacetResponseSchema), + }) + .meta({ id: 'SearchAlbumResponseDto' }); -class SearchAlbumResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching albums' }) - total!: number; - @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[]; -} +const SearchAssetResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of matching assets'), + count: z.int().min(0).describe('Number of assets in this page'), + items: z.array(AssetResponseSchema), + facets: z.array(SearchFacetResponseSchema), + nextPage: z.string().nullable().describe('Next page token'), + }) + .meta({ id: 'SearchAssetResponseDto' }); -class SearchAssetResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching assets' }) - total!: number; - @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; -} +const SearchResponseSchema = z + .object({ + albums: SearchAlbumResponseSchema, + assets: SearchAssetResponseSchema, + }) + .meta({ id: 'SearchResponseDto' }); -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 SearchResponseDto extends createZodDto(SearchResponseSchema) {} -export class SearchStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching assets' }) - total!: number; -} +const SearchStatisticsResponseSchema = z + .object({ + total: z.int().describe('Total number of matching assets'), + }) + .meta({ id: 'SearchStatisticsResponseDto' }); -class SearchExploreItem { - @ApiProperty({ description: 'Explore value' }) - value!: string; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - data!: AssetResponseDto; -} +export class SearchStatisticsResponseDto extends createZodDto(SearchStatisticsResponseSchema) {} -export class SearchExploreResponseDto { - @ApiProperty({ description: 'Explore field name' }) - fieldName!: string; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - items!: SearchExploreItem[]; -} +const SearchExploreItemSchema = z + .object({ + value: z.string().describe('Explore value'), + data: AssetResponseSchema, + }) + .meta({ id: 'SearchExploreItem' }); -export class MemoryLaneDto { - @ApiProperty({ type: 'integer', description: 'Day of month' }) - @IsInt() - @Type(() => Number) - @Max(31) - @Min(1) - day!: number; +const SearchExploreResponseSchema = z + .object({ + fieldName: z.string().describe('Explore field name'), + items: z.array(SearchExploreItemSchema), + }) + .meta({ id: 'SearchExploreResponseDto' }); - @ApiProperty({ type: 'integer', description: 'Month' }) - @IsInt() - @Type(() => Number) - @Max(12) - @Min(1) - month!: number; -} +export class SearchExploreResponseDto extends createZodDto(SearchExploreResponseSchema) {} diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index 626c94e40a..bd42032771 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -1,242 +1,169 @@ -import { ApiProperty, ApiPropertyOptional, ApiResponseProperty } from '@nestjs/swagger'; -import { SemVer } from 'semver'; -import { SystemConfigThemeDto } from 'src/dtos/system-config.dto'; +import { createZodDto } from 'nestjs-zod'; +import type { SemVer } from 'semver'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class ServerPingResponse { - @ApiResponseProperty({ type: String, example: 'pong' }) - res!: string; -} +const ServerPingResponseSchema = z + .object({ + res: z.string().meta({ example: 'pong' }), + }) + .meta({ id: 'ServerPingResponse' }); -export class ServerAboutResponseDto { - @ApiProperty({ description: 'Server version' }) - version!: string; - @ApiProperty({ description: 'URL to version information' }) - versionUrl!: string; +const ServerAboutResponseSchema = z + .object({ + version: z.string().describe('Server version'), + versionUrl: z.string().describe('URL to version information'), + repository: z.string().optional().describe('Repository name'), + repositoryUrl: z.string().optional().describe('Repository URL'), + sourceRef: z.string().optional().describe('Source reference (branch/tag)'), + sourceCommit: z.string().optional().describe('Source commit hash'), + sourceUrl: z.string().optional().describe('Source URL'), + build: z.string().optional().describe('Build identifier'), + buildUrl: z.string().optional().describe('Build URL'), + buildImage: z.string().optional().describe('Build image name'), + buildImageUrl: z.string().optional().describe('Build image URL'), + nodejs: z.string().optional().describe('Node.js version'), + ffmpeg: z.string().optional().describe('FFmpeg version'), + imagemagick: z.string().optional().describe('ImageMagick version'), + libvips: z.string().optional().describe('libvips version'), + exiftool: z.string().optional().describe('ExifTool version'), + licensed: z.boolean().describe('Whether the server is licensed'), + thirdPartySourceUrl: z.string().optional().describe('Third-party source URL'), + thirdPartyBugFeatureUrl: z.string().optional().describe('Third-party bug/feature URL'), + thirdPartyDocumentationUrl: z.string().optional().describe('Third-party documentation URL'), + thirdPartySupportUrl: z.string().optional().describe('Third-party support URL'), + }) + .meta({ id: 'ServerAboutResponseDto' }); - @ApiPropertyOptional({ description: 'Repository name' }) - repository?: string; - @ApiPropertyOptional({ description: 'Repository URL' }) - repositoryUrl?: string; +const ServerApkLinksSchema = z + .object({ + arm64v8a: z.string().describe('APK download link for ARM64 v8a architecture'), + armeabiv7a: z.string().describe('APK download link for ARM EABI v7a architecture'), + universal: z.string().describe('APK download link for universal architecture'), + x86_64: z.string().describe('APK download link for x86_64 architecture'), + }) + .meta({ id: 'ServerApkLinksDto' }); - @ApiPropertyOptional({ description: 'Source reference (branch/tag)' }) - sourceRef?: string; - @ApiPropertyOptional({ description: 'Source commit hash' }) - sourceCommit?: string; - @ApiPropertyOptional({ description: 'Source URL' }) - sourceUrl?: string; +const ServerStorageResponseSchema = z + .object({ + diskSize: z.string().describe('Total disk size (human-readable format)'), + diskUse: z.string().describe('Used disk space (human-readable format)'), + diskAvailable: z.string().describe('Available disk space (human-readable format)'), + diskSizeRaw: z.int().describe('Total disk size in bytes'), + diskUseRaw: z.int().describe('Used disk space in bytes'), + diskAvailableRaw: z.int().describe('Available disk space in bytes'), + diskUsagePercentage: z.number().meta({ format: 'double' }).describe('Disk usage percentage (0-100)'), + }) + .meta({ id: 'ServerStorageResponseDto' }); - @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; +const ServerVersionResponseSchema = z + .object({ + major: z.int().describe('Major version number'), + minor: z.int().describe('Minor version number'), + patch: z.int().describe('Patch version number'), + }) + .meta({ id: 'ServerVersionResponseDto' }); - @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; +const ServerVersionHistoryResponseSchema = z + .object({ + id: z.string().describe('Version history entry ID'), + createdAt: isoDatetimeToDate.describe('When this version was first seen'), + version: z.string().describe('Version string'), + }) + .meta({ id: 'ServerVersionHistoryResponseDto' }); - @ApiProperty({ description: 'Whether the server is licensed' }) - licensed!: boolean; +const UsageByUserSchema = z + .object({ + userId: z.string().describe('User ID'), + userName: z.string().describe('User name'), + photos: z.int().describe('Number of photos'), + videos: z.int().describe('Number of videos'), + usage: z.int().describe('Total storage usage in bytes'), + usagePhotos: z.int().describe('Storage usage for photos in bytes'), + usageVideos: z.int().describe('Storage usage for videos in bytes'), + quotaSizeInBytes: z.int().nullable().describe('User quota size in bytes (null if unlimited)'), + }) + .meta({ id: 'UsageByUserDto' }); - @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; -} +const ServerStatsResponseSchema = z + .object({ + photos: z.int().describe('Total number of photos'), + videos: z.int().describe('Total number of videos'), + usage: z.int().describe('Total storage usage in bytes'), + usagePhotos: z.int().describe('Storage usage for photos in bytes'), + usageVideos: z.int().describe('Storage usage for videos in bytes'), + usageByUser: z.array(UsageByUserSchema).describe('Array of usage for each user'), + }) + .meta({ id: 'ServerStatsResponseDto' }); -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; -} +const ServerMediaTypesResponseSchema = z + .object({ + video: z.array(z.string()).describe('Supported video MIME types'), + image: z.array(z.string()).describe('Supported image MIME types'), + sidecar: z.array(z.string()).describe('Supported sidecar MIME types'), + }) + .meta({ id: 'ServerMediaTypesResponseDto' }); -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; +const ServerThemeSchema = z + .object({ + customCss: z.string().describe('Custom CSS for theming'), + }) + .meta({ id: 'ServerThemeDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'Total disk size in bytes' }) - diskSizeRaw!: number; +const ServerConfigSchema = z + .object({ + oauthButtonText: z.string().describe('OAuth button text'), + loginPageMessage: z.string().describe('Login page message'), + trashDays: z.int().describe('Number of days before trashed assets are permanently deleted'), + userDeleteDelay: z.int().describe('Delay in days before deleted users are permanently removed'), + isInitialized: z.boolean().describe('Whether the server has been initialized'), + isOnboarded: z.boolean().describe('Whether the admin has completed onboarding'), + externalDomain: z.string().describe('External domain URL'), + publicUsers: z.boolean().describe('Whether public user registration is enabled'), + mapDarkStyleUrl: z.string().describe('Map dark style URL'), + mapLightStyleUrl: z.string().describe('Map light style URL'), + maintenanceMode: z.boolean().describe('Whether maintenance mode is active'), + }) + .meta({ id: 'ServerConfigDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'Used disk space in bytes' }) - diskUseRaw!: number; +const ServerFeaturesSchema = z + .object({ + smartSearch: z.boolean().describe('Whether smart search is enabled'), + duplicateDetection: z.boolean().describe('Whether duplicate detection is enabled'), + configFile: z.boolean().describe('Whether config file is available'), + facialRecognition: z.boolean().describe('Whether facial recognition is enabled'), + map: z.boolean().describe('Whether map feature is enabled'), + trash: z.boolean().describe('Whether trash feature is enabled'), + reverseGeocoding: z.boolean().describe('Whether reverse geocoding is enabled'), + importFaces: z.boolean().describe('Whether face import is enabled'), + oauth: z.boolean().describe('Whether OAuth is enabled'), + oauthAutoLaunch: z.boolean().describe('Whether OAuth auto-launch is enabled'), + passwordLogin: z.boolean().describe('Whether password login is enabled'), + sidecar: z.boolean().describe('Whether sidecar files are supported'), + search: z.boolean().describe('Whether search is enabled'), + email: z.boolean().describe('Whether email notifications are enabled'), + ocr: z.boolean().describe('Whether OCR is enabled'), + }) + .meta({ id: 'ServerFeaturesDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'Available disk space in bytes' }) - diskAvailableRaw!: number; +export class ServerPingResponse extends createZodDto(ServerPingResponseSchema) {} +export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSchema) {} +export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {} +export class ServerStorageResponseDto extends createZodDto(ServerStorageResponseSchema) {} - @ApiProperty({ type: 'number', format: 'double', description: 'Disk usage percentage (0-100)' }) - diskUsagePercentage!: number; -} - -export class ServerVersionResponseDto { - @ApiProperty({ type: 'integer', description: 'Major version number' }) - major!: number; - @ApiProperty({ type: 'integer', description: 'Minor version number' }) - minor!: number; - @ApiProperty({ type: 'integer', description: 'Patch version number' }) - patch!: number; - - static fromSemVer(value: SemVer) { +export class ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) { + static fromSemVer(value: SemVer): z.infer { return { major: value.major, minor: value.minor, patch: value.patch }; } } -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', description: 'User ID' }) - userId!: string; - @ApiProperty({ type: 'string', description: 'User name' }) - userName!: string; - @ApiProperty({ type: 'integer', description: 'Number of photos' }) - photos!: number; - @ApiProperty({ type: 'integer', description: 'Number of videos' }) - videos!: number; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Total storage usage in bytes' }) - usage!: number; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for photos in bytes' }) - usagePhotos!: number; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for videos in bytes' }) - usageVideos!: number; - @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', description: 'Total number of photos' }) - photos = 0; - - @ApiProperty({ type: 'integer', description: 'Total number of videos' }) - videos = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Total storage usage in bytes' }) - usage = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for photos in bytes' }) - usagePhotos = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for videos in bytes' }) - usageVideos = 0; - - @ApiProperty({ - isArray: true, - type: UsageByUserDto, - title: 'Array of usage for each user', - example: [ - { - photos: 1, - videos: 1, - diskUsageRaw: 2, - usagePhotos: 1, - usageVideos: 1, - }, - ], - }) - usageByUser: UsageByUserDto[] = []; -} - -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', description: 'Number of days before trashed assets are permanently deleted' }) - trashDays!: number; - @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; -} +export class ServerVersionHistoryResponseDto extends createZodDto(ServerVersionHistoryResponseSchema) {} +export class UsageByUserDto extends createZodDto(UsageByUserSchema) {} +export class ServerStatsResponseDto extends createZodDto(ServerStatsResponseSchema) {} +export class ServerMediaTypesResponseDto extends createZodDto(ServerMediaTypesResponseSchema) {} +export class ServerThemeDto extends createZodDto(ServerThemeSchema) {} +export class ServerConfigDto extends createZodDto(ServerConfigSchema) {} +export class ServerFeaturesDto extends createZodDto(ServerFeaturesSchema) {} export interface ReleaseNotification { isAvailable: boolean; diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts index f918f0b3bb..179a1dfb76 100644 --- a/server/src/dtos/session.dto.ts +++ b/server/src/dtos/session.dto.ts @@ -1,57 +1,43 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Equals, IsInt, IsPositive, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Session } from 'src/database'; -import { Optional, ValidateBoolean } from 'src/validation'; +import z from 'zod'; -export class SessionCreateDto { - @ApiPropertyOptional({ type: 'number', description: 'Session duration in seconds' }) - @IsInt() - @IsPositive() - @Optional() - duration?: number; +const SessionCreateSchema = z + .object({ + duration: z.number().min(1).optional().describe('Session duration in seconds'), + deviceType: z.string().optional().describe('Device type'), + deviceOS: z.string().optional().describe('Device OS'), + }) + .meta({ id: 'SessionCreateDto' }); - @ApiPropertyOptional({ description: 'Device type' }) - @IsString() - @Optional() - deviceType?: string; +const SessionUpdateSchema = z + .object({ + isPendingSyncReset: z.boolean().optional().describe('Reset pending sync state'), + }) + .meta({ id: 'SessionUpdateDto' }); - @ApiPropertyOptional({ description: 'Device OS' }) - @IsString() - @Optional() - deviceOS?: string; -} +const SessionResponseSchema = z + .object({ + id: z.string().describe('Session ID'), + createdAt: z.string().describe('Creation date'), + updatedAt: z.string().describe('Last update date'), + expiresAt: z.string().optional().describe('Expiration date'), + current: z.boolean().describe('Is current session'), + deviceType: z.string().describe('Device type'), + deviceOS: z.string().describe('Device OS'), + appVersion: z.string().nullable().describe('App version'), + isPendingSyncReset: z.boolean().describe('Is pending sync reset'), + }) + .meta({ id: 'SessionResponseDto' }); -export class SessionUpdateDto { - @ValidateBoolean({ optional: true, description: 'Reset pending sync state' }) - @Equals(true) - isPendingSyncReset?: true; -} +const SessionCreateResponseSchema = SessionResponseSchema.extend({ + token: z.string().describe('Session token'), +}).meta({ id: 'SessionCreateResponseDto' }); -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; -} +export class SessionCreateDto extends createZodDto(SessionCreateSchema) {} +export class SessionUpdateDto extends createZodDto(SessionUpdateSchema) {} +export class SessionResponseDto extends createZodDto(SessionResponseSchema) {} +export class SessionCreateResponseDto extends createZodDto(SessionCreateResponseSchema) {} export const mapSession = (entity: Session, currentId?: string): SessionResponseDto => ({ id: entity.id, diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index b2ecc70a3a..7dcec034dc 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,155 +1,103 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { SharedLink } from 'src/database'; -import { HistoryBuilder, Property } 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, ValidateString, ValidateUUID } from 'src/validation'; +import { HistoryBuilder } from 'src/decorators'; +import { AlbumResponseSchema, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; +import { SharedLinkTypeSchema } from 'src/enum'; +import { emptyStringToNull, isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class SharedLinkSearchDto { - @ValidateUUID({ optional: true, description: 'Filter by album ID' }) - albumId?: string; - - @ValidateUUID({ - optional: true, - description: 'Filter by shared link ID', - history: new HistoryBuilder().added('v2.5.0'), +const SharedLinkSearchSchema = z + .object({ + albumId: z.uuidv4().optional().describe('Filter by album ID'), + id: z + .uuidv4() + .optional() + .describe('Filter by shared link ID') + .meta(new HistoryBuilder().added('v2.5.0').getExtensions()), }) - id?: string; -} + .meta({ id: 'SharedLinkSearchDto' }); -export class SharedLinkCreateDto { - @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType', description: 'Shared link type' }) - type!: SharedLinkType; - - @ValidateUUID({ each: true, optional: true, description: 'Asset IDs (for individual assets)' }) - assetIds?: string[]; - - @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, description: 'Expiration date' }) - expiresAt?: Date | null = null; - - @ValidateBoolean({ optional: true, description: 'Allow uploads' }) - allowUpload?: boolean; - - @ValidateBoolean({ optional: true, description: 'Allow downloads', default: true }) - allowDownload?: boolean = 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, description: 'Allow uploads' }) - allowUpload?: boolean; - - @ValidateBoolean({ optional: true, description: 'Allow downloads' }) - allowDownload?: boolean; - - @ValidateBoolean({ optional: true, description: 'Show metadata' }) - showMetadata?: boolean; - - @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.', +const SharedLinkCreateSchema = z + .object({ + type: SharedLinkTypeSchema, + assetIds: z.array(z.uuidv4()).optional().describe('Asset IDs (for individual assets)'), + albumId: z.uuidv4().optional().describe('Album ID (for album sharing)'), + description: emptyStringToNull(z.string().nullable()).optional().describe('Link description'), + password: emptyStringToNull(z.string().nullable()).optional().describe('Link password'), + slug: emptyStringToNull(z.string().nullable()).optional().describe('Custom URL slug'), + expiresAt: isoDatetimeToDate.nullable().describe('Expiration date').default(null).optional(), + allowUpload: z.boolean().optional().describe('Allow uploads'), + allowDownload: z.boolean().default(true).optional().describe('Allow downloads'), + showMetadata: z.boolean().default(true).optional().describe('Show metadata'), }) - changeExpiryTime?: boolean; -} + .meta({ id: 'SharedLinkCreateDto' }); -export class SharedLinkLoginDto { - @ValidateString({ description: 'Shared link password', example: 'password' }) - password!: string; -} - -export class SharedLinkPasswordDto { - @ApiPropertyOptional({ example: 'password', description: 'Link password' }) - @IsString() - @Optional() - 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; - @Property({ - description: 'Access token', - history: new HistoryBuilder().added('v1').stable('v2').deprecated('v2.6.0'), +const SharedLinkEditSchema = z + .object({ + description: emptyStringToNull(z.string().nullable()).optional().describe('Link description'), + password: emptyStringToNull(z.string().nullable()).optional().describe('Link password'), + slug: emptyStringToNull(z.string().nullable()).optional().describe('Custom URL slug'), + expiresAt: isoDatetimeToDate.nullish().describe('Expiration date'), + allowUpload: z.boolean().optional().describe('Allow uploads'), + allowDownload: z.boolean().optional().describe('Allow downloads'), + showMetadata: z.boolean().optional().describe('Show metadata'), + changeExpiryTime: z + .boolean() + .optional() + .describe( + '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.', + ), }) - token?: string | null; - @ApiProperty({ description: 'Owner user ID' }) - userId!: string; - @ApiProperty({ description: 'Encryption key (base64url)' }) - key!: string; + .meta({ id: 'SharedLinkEditDto' }); - @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; +const SharedLinkLoginSchema = z + .object({ + password: z.string().describe('Shared link password').meta({ example: 'password' }), + }) + .meta({ id: 'SharedLinkLoginDto' }); - @ApiProperty({ description: 'Allow downloads' }) - allowDownload!: boolean; - @ApiProperty({ description: 'Show metadata' }) - showMetadata!: boolean; +const SharedLinkPasswordSchema = z + .object({ + password: z.string().optional().describe('Link password'), + token: z.string().optional().describe('Access token'), + }) + .meta({ id: 'SharedLinkPasswordDto' }); - @ApiProperty({ description: 'Custom URL slug' }) - slug!: string | null; -} +const SharedLinkResponseSchema = z + .object({ + id: z.string().describe('Shared link ID'), + description: z.string().nullable().describe('Link description'), + password: z.string().nullable().describe('Has password'), + token: z + .string() + .nullish() + .describe('Access token') + .meta({ + ...new HistoryBuilder().added('v1').stable('v2').deprecated('v2.6.0').getExtensions(), + deprecated: true, + }), + userId: z.string().describe('Owner user ID'), + key: z.string().describe('Encryption key (base64url)'), + type: SharedLinkTypeSchema, + createdAt: isoDatetimeToDate.describe('Creation date'), + expiresAt: isoDatetimeToDate.nullable().describe('Expiration date'), + assets: z.array(AssetResponseSchema), + album: AlbumResponseSchema.optional(), + allowUpload: z.boolean().describe('Allow uploads'), + allowDownload: z.boolean().describe('Allow downloads'), + showMetadata: z.boolean().describe('Show metadata'), + slug: z.string().nullable().describe('Custom URL slug'), + }) + .describe('Shared link response') + .meta({ id: 'SharedLinkResponseDto' }); + +export class SharedLinkSearchDto extends createZodDto(SharedLinkSearchSchema) {} +export class SharedLinkCreateDto extends createZodDto(SharedLinkCreateSchema) {} +export class SharedLinkEditDto extends createZodDto(SharedLinkEditSchema) {} +export class SharedLinkLoginDto extends createZodDto(SharedLinkLoginSchema) {} +export class SharedLinkPasswordDto extends createZodDto(SharedLinkPasswordSchema) {} +export class SharedLinkResponseDto extends createZodDto(SharedLinkResponseSchema) {} export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetadata: boolean }): SharedLinkResponseDto { const assets = sharedLink.assets || []; diff --git a/server/src/dtos/stack.dto.ts b/server/src/dtos/stack.dto.ts index a76b35e08e..48354cec6b 100644 --- a/server/src/dtos/stack.dto.ts +++ b/server/src/dtos/stack.dto.ts @@ -1,34 +1,40 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMinSize } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Stack } from 'src/database'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ValidateUUID } from 'src/validation'; +import z from 'zod'; -export class StackCreateDto { - @ValidateUUID({ each: true, description: 'Asset IDs (first becomes primary, min 2)' }) - @ArrayMinSize(2) - assetIds!: string[]; -} +const StackSearchSchema = z + .object({ + primaryAssetId: z.uuidv4().optional().describe('Filter by primary asset ID'), + }) + .meta({ id: 'StackSearchDto' }); -export class StackSearchDto { - @ValidateUUID({ optional: true, description: 'Filter by primary asset ID' }) - primaryAssetId?: string; -} +const StackCreateSchema = z + .object({ + assetIds: z.array(z.uuidv4()).min(2).describe('Asset IDs (first becomes primary, min 2)'), + }) + .meta({ id: 'StackCreateDto' }); -export class StackUpdateDto { - @ValidateUUID({ optional: true, description: 'Primary asset ID' }) - primaryAssetId?: string; -} +const StackUpdateSchema = z + .object({ + primaryAssetId: z.uuidv4().optional().describe('Primary asset ID'), + }) + .meta({ id: 'StackUpdateDto' }); -export class StackResponseDto { - @ApiProperty({ description: 'Stack ID' }) - id!: string; - @ApiProperty({ description: 'Primary asset ID' }) - primaryAssetId!: string; - @ApiProperty({ description: 'Stack assets' }) - assets!: AssetResponseDto[]; -} +const StackResponseSchema = z + .object({ + id: z.string().describe('Stack ID'), + primaryAssetId: z.string().describe('Primary asset ID'), + assets: z.array(AssetResponseSchema), + }) + .describe('Stack response') + .meta({ id: 'StackResponseDto' }); + +export class StackSearchDto extends createZodDto(StackSearchSchema) {} +export class StackCreateDto extends createZodDto(StackCreateSchema) {} +export class StackUpdateDto extends createZodDto(StackUpdateSchema) {} +export class StackResponseDto extends createZodDto(StackResponseSchema) {} export const mapStack = (stack: Stack, { auth }: { auth?: AuthDto }) => { const primary = stack.assets.filter((asset) => asset.id === stack.primaryAssetId); diff --git a/server/src/dtos/sync.dto.ts b/server/src/dtos/sync.dto.ts index 9a1332d303..d7903ebb0c 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,492 +1,423 @@ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AssetEditAction } from 'src/dtos/editing.dto'; +import { createZodDto } from 'nestjs-zod'; +import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; +import { AssetEditActionSchema } from 'src/dtos/editing.dto'; import { - AlbumUserRole, - AssetOrder, - AssetType, - AssetVisibility, - MemoryType, + AlbumUserRoleSchema, + AssetOrderSchema, + AssetTypeSchema, + AssetVisibilitySchema, + MemoryTypeSchema, SyncEntityType, - SyncRequestType, - UserAvatarColor, - UserMetadataKey, + SyncEntityTypeSchema, + SyncRequestTypeSchema, + UserAvatarColorSchema, + UserMetadataKeySchema, } from 'src/enum'; -import { UserMetadata } from 'src/types'; -import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class AssetFullSyncDto { - @ValidateUUID({ optional: true, description: 'Last asset ID (pagination)' }) - lastId?: string; +const AssetFullSyncSchema = z + .object({ + lastId: z.uuidv4().optional().describe('Last asset ID (pagination)'), + updatedUntil: isoDatetimeToDate.describe('Sync assets updated until this date'), + limit: z.int().min(1).describe('Maximum number of assets to return'), + userId: z.uuidv4().optional().describe('Filter by user ID'), + }) + .meta({ id: 'AssetFullSyncDto' }); - @ValidateDate({ description: 'Sync assets updated until this date' }) - updatedUntil!: Date; +const AssetDeltaSyncSchema = z + .object({ + updatedAfter: isoDatetimeToDate.describe('Sync assets updated after this date'), + userIds: z.array(z.uuidv4()).describe('User IDs to sync'), + }) + .meta({ id: 'AssetDeltaSyncDto' }); - @ApiProperty({ type: 'integer', description: 'Maximum number of assets to return' }) - @IsInt() - @IsPositive() - limit!: number; +export class AssetFullSyncDto extends createZodDto(AssetFullSyncSchema) {} +export class AssetDeltaSyncDto extends createZodDto(AssetDeltaSyncSchema) {} - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} +const AssetDeltaSyncResponseSchema = z + .object({ + needsFullSync: z.boolean().describe('Whether full sync is needed'), + upserted: z.array(AssetResponseSchema), + deleted: z.array(z.string()).describe('Deleted asset IDs'), + }) + .describe('Asset delta sync response') + .meta({ id: 'AssetDeltaSyncResponseDto' }); -export class AssetDeltaSyncDto { - @ValidateDate({ description: 'Sync assets updated after this date' }) - updatedAfter!: Date; - - @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[]; -} +export class AssetDeltaSyncResponseDto extends createZodDto(AssetDeltaSyncResponseSchema) {} export const extraSyncModels: Function[] = []; -export const ExtraModel = (): ClassDecorator => { +const ExtraModel = (): ClassDecorator => { // eslint-disable-next-line unicorn/consistent-function-scoping return (object: Function) => { extraSyncModels.push(object); }; }; -@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', 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; -} +const SyncUserV1Schema = z + .object({ + id: z.string().describe('User ID'), + name: z.string().describe('User name'), + email: z.string().describe('User email'), + avatarColor: UserAvatarColorSchema.nullish(), + deletedAt: isoDatetimeToDate.nullable().describe('User deleted at'), + hasProfileImage: z.boolean().describe('User has profile image'), + profileChangedAt: isoDatetimeToDate.describe('User profile changed at'), + }) + .meta({ id: 'SyncUserV1' }); + +const SyncAuthUserV1Schema = SyncUserV1Schema.merge( + z.object({ + isAdmin: z.boolean().describe('User is admin'), + pinCode: z.string().nullable().describe('User pin code'), + oauthId: z.string().describe('User OAuth ID'), + storageLabel: z.string().nullable().describe('User storage label'), + quotaSizeInBytes: z.int().nullable().describe('Quota size in bytes'), + quotaUsageInBytes: z.int().describe('Quota usage in bytes'), + }), +).meta({ id: 'SyncAuthUserV1' }); + +const SyncUserDeleteV1Schema = z.object({ userId: z.string().describe('User ID') }).meta({ id: 'SyncUserDeleteV1' }); + +const SyncPartnerV1Schema = z + .object({ + sharedById: z.string().describe('Shared by ID'), + sharedWithId: z.string().describe('Shared with ID'), + inTimeline: z.boolean().describe('In timeline'), + }) + .meta({ id: 'SyncPartnerV1' }); + +const SyncPartnerDeleteV1Schema = z + .object({ + sharedById: z.string().describe('Shared by ID'), + sharedWithId: z.string().describe('Shared with ID'), + }) + .meta({ id: 'SyncPartnerDeleteV1' }); + +const SyncAssetV1Schema = z + .object({ + id: z.string().describe('Asset ID'), + ownerId: z.string().describe('Owner ID'), + originalFileName: z.string().describe('Original file name'), + thumbhash: z.string().nullable().describe('Thumbhash'), + checksum: z.string().describe('Checksum'), + fileCreatedAt: isoDatetimeToDate.nullable().describe('File created at'), + fileModifiedAt: isoDatetimeToDate.nullable().describe('File modified at'), + localDateTime: isoDatetimeToDate.nullable().describe('Local date time'), + duration: z.string().nullable().describe('Duration'), + type: AssetTypeSchema, + deletedAt: isoDatetimeToDate.nullable().describe('Deleted at'), + isFavorite: z.boolean().describe('Is favorite'), + visibility: AssetVisibilitySchema, + livePhotoVideoId: z.string().nullable().describe('Live photo video ID'), + stackId: z.string().nullable().describe('Stack ID'), + libraryId: z.string().nullable().describe('Library ID'), + width: z.int().nullable().describe('Asset width'), + height: z.int().nullable().describe('Asset height'), + isEdited: z.boolean().describe('Is edited'), + }) + .meta({ id: 'SyncAssetV1' }); @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; - @ApiProperty({ type: 'integer' }) - quotaUsageInBytes!: number; -} +class SyncUserV1 extends createZodDto(SyncUserV1Schema) {} +@ExtraModel() +class SyncAuthUserV1 extends createZodDto(SyncAuthUserV1Schema) {} +@ExtraModel() +class SyncUserDeleteV1 extends createZodDto(SyncUserDeleteV1Schema) {} +@ExtraModel() +class SyncPartnerV1 extends createZodDto(SyncPartnerV1Schema) {} +@ExtraModel() +class SyncPartnerDeleteV1 extends createZodDto(SyncPartnerDeleteV1Schema) {} +@ExtraModel() +export class SyncAssetV1 extends createZodDto(SyncAssetV1Schema) {} + +const SyncAssetDeleteV1Schema = z + .object({ assetId: z.string().describe('Asset ID') }) + .meta({ id: 'SyncAssetDeleteV1' }); + +const SyncAssetExifV1Schema = z + .object({ + assetId: z.string().describe('Asset ID'), + description: z.string().nullable().describe('Description'), + exifImageWidth: z.int().nullable().describe('Exif image width'), + exifImageHeight: z.int().nullable().describe('Exif image height'), + fileSizeInByte: z.int().nullable().describe('File size in byte'), + orientation: z.string().nullable().describe('Orientation'), + dateTimeOriginal: isoDatetimeToDate.nullable().describe('Date time original'), + modifyDate: isoDatetimeToDate.nullable().describe('Modify date'), + timeZone: z.string().nullable().describe('Time zone'), + latitude: z.number().meta({ format: 'double' }).nullable().describe('Latitude'), + longitude: z.number().meta({ format: 'double' }).nullable().describe('Longitude'), + projectionType: z.string().nullable().describe('Projection type'), + city: z.string().nullable().describe('City'), + state: z.string().nullable().describe('State'), + country: z.string().nullable().describe('Country'), + make: z.string().nullable().describe('Make'), + model: z.string().nullable().describe('Model'), + lensModel: z.string().nullable().describe('Lens model'), + fNumber: z.number().meta({ format: 'double' }).nullable().describe('F number'), + focalLength: z.number().meta({ format: 'double' }).nullable().describe('Focal length'), + iso: z.int().nullable().describe('ISO'), + exposureTime: z.string().nullable().describe('Exposure time'), + profileDescription: z.string().nullable().describe('Profile description'), + rating: z.int().nullable().describe('Rating'), + fps: z.number().meta({ format: 'double' }).nullable().describe('FPS'), + }) + .meta({ id: 'SyncAssetExifV1' }); + +const SyncAssetMetadataV1Schema = z + .object({ + assetId: z.string().describe('Asset ID'), + key: z.string().describe('Key'), + value: z.record(z.string(), z.unknown()).describe('Value'), + }) + .meta({ id: 'SyncAssetMetadataV1' }); + +const SyncAssetMetadataDeleteV1Schema = z + .object({ + assetId: z.string().describe('Asset ID'), + key: z.string().describe('Key'), + }) + .meta({ id: 'SyncAssetMetadataDeleteV1' }); + +const SyncAssetEditV1Schema = z + .object({ + id: z.string().describe('Edit ID'), + assetId: z.string().describe('Asset ID'), + action: AssetEditActionSchema, + parameters: z.record(z.string(), z.unknown()).describe('Edit parameters'), + sequence: z.int().describe('Edit sequence'), + }) + .meta({ id: 'SyncAssetEditV1' }); + +const SyncAssetEditDeleteV1Schema = z + .object({ editId: z.string().describe('Edit ID') }) + .meta({ id: 'SyncAssetEditDeleteV1' }); @ExtraModel() -export class SyncUserDeleteV1 { - @ApiProperty({ description: 'User ID' }) - userId!: string; -} +class SyncAssetDeleteV1 extends createZodDto(SyncAssetDeleteV1Schema) {} +@ExtraModel() +export class SyncAssetExifV1 extends createZodDto(SyncAssetExifV1Schema) {} +@ExtraModel() +class SyncAssetMetadataV1 extends createZodDto(SyncAssetMetadataV1Schema) {} +@ExtraModel() +class SyncAssetMetadataDeleteV1 extends createZodDto(SyncAssetMetadataDeleteV1Schema) {} +@ExtraModel() +export class SyncAssetEditV1 extends createZodDto(SyncAssetEditV1Schema) {} +@ExtraModel() +class SyncAssetEditDeleteV1 extends createZodDto(SyncAssetEditDeleteV1Schema) {} + +const SyncAlbumDeleteV1Schema = z + .object({ albumId: z.string().describe('Album ID') }) + .meta({ id: 'SyncAlbumDeleteV1' }); + +const SyncAlbumUserDeleteV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + userId: z.string().describe('User ID'), + }) + .meta({ id: 'SyncAlbumUserDeleteV1' }); + +const SyncAlbumUserV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + userId: z.string().describe('User ID'), + role: AlbumUserRoleSchema, + }) + .meta({ id: 'SyncAlbumUserV1' }); + +const SyncAlbumV1Schema = z + .object({ + id: z.string().describe('Album ID'), + ownerId: z.string().describe('Owner ID'), + name: z.string().describe('Album name'), + description: z.string().describe('Album description'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + thumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'), + isActivityEnabled: z.boolean().describe('Is activity enabled'), + order: AssetOrderSchema, + }) + .meta({ id: 'SyncAlbumV1' }); + +const SyncAlbumToAssetV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncAlbumToAssetV1' }); + +const SyncAlbumToAssetDeleteV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncAlbumToAssetDeleteV1' }); @ExtraModel() -export class SyncPartnerV1 { - @ApiProperty({ description: 'Shared by ID' }) - sharedById!: string; - @ApiProperty({ description: 'Shared with ID' }) - sharedWithId!: string; - @ApiProperty({ description: 'In timeline' }) - inTimeline!: boolean; -} +class SyncAlbumDeleteV1 extends createZodDto(SyncAlbumDeleteV1Schema) {} +@ExtraModel() +class SyncAlbumUserDeleteV1 extends createZodDto(SyncAlbumUserDeleteV1Schema) {} +@ExtraModel() +class SyncAlbumUserV1 extends createZodDto(SyncAlbumUserV1Schema) {} +@ExtraModel() +class SyncAlbumV1 extends createZodDto(SyncAlbumV1Schema) {} +@ExtraModel() +class SyncAlbumToAssetV1 extends createZodDto(SyncAlbumToAssetV1Schema) {} +@ExtraModel() +class SyncAlbumToAssetDeleteV1 extends createZodDto(SyncAlbumToAssetDeleteV1Schema) {} + +const SyncMemoryV1Schema = z + .object({ + id: z.string().describe('Memory ID'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + deletedAt: isoDatetimeToDate.nullable().describe('Deleted at'), + ownerId: z.string().describe('Owner ID'), + type: MemoryTypeSchema, + data: z.record(z.string(), z.unknown()).describe('Data'), + isSaved: z.boolean().describe('Is saved'), + memoryAt: isoDatetimeToDate.describe('Memory at'), + seenAt: isoDatetimeToDate.nullable().describe('Seen at'), + showAt: isoDatetimeToDate.nullable().describe('Show at'), + hideAt: isoDatetimeToDate.nullable().describe('Hide at'), + }) + .meta({ id: 'SyncMemoryV1' }); + +const SyncMemoryDeleteV1Schema = z + .object({ memoryId: z.string().describe('Memory ID') }) + .meta({ id: 'SyncMemoryDeleteV1' }); + +const SyncMemoryAssetV1Schema = z + .object({ + memoryId: z.string().describe('Memory ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncMemoryAssetV1' }); + +const SyncMemoryAssetDeleteV1Schema = z + .object({ + memoryId: z.string().describe('Memory ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncMemoryAssetDeleteV1' }); + +const SyncStackV1Schema = z + .object({ + id: z.string().describe('Stack ID'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + primaryAssetId: z.string().describe('Primary asset ID'), + ownerId: z.string().describe('Owner ID'), + }) + .meta({ id: 'SyncStackV1' }); + +const SyncStackDeleteV1Schema = z + .object({ stackId: z.string().describe('Stack ID') }) + .meta({ id: 'SyncStackDeleteV1' }); + +const SyncPersonV1Schema = z + .object({ + id: z.string().describe('Person ID'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + ownerId: z.string().describe('Owner ID'), + name: z.string().describe('Person name'), + birthDate: isoDatetimeToDate.nullable().describe('Birth date'), + isHidden: z.boolean().describe('Is hidden'), + isFavorite: z.boolean().describe('Is favorite'), + color: z.string().nullable().describe('Color'), + faceAssetId: z.string().nullable().describe('Face asset ID'), + }) + .meta({ id: 'SyncPersonV1' }); + +const SyncPersonDeleteV1Schema = z + .object({ personId: z.string().describe('Person ID') }) + .meta({ id: 'SyncPersonDeleteV1' }); + +const SyncAssetFaceV1Schema = z + .object({ + id: z.string().describe('Asset face ID'), + assetId: z.string().describe('Asset ID'), + personId: z.string().nullable().describe('Person ID'), + imageWidth: z.int().describe('Image width'), + imageHeight: z.int().describe('Image height'), + boundingBoxX1: z.int().describe('Bounding box X1'), + boundingBoxY1: z.int().describe('Bounding box Y1'), + boundingBoxX2: z.int().describe('Bounding box X2'), + boundingBoxY2: z.int().describe('Bounding box Y2'), + sourceType: z.string().describe('Source type'), + }) + .meta({ id: 'SyncAssetFaceV1' }); + +const SyncAssetFaceV2Schema = SyncAssetFaceV1Schema.extend({ + deletedAt: isoDatetimeToDate.nullable().describe('Face deleted at'), + isVisible: z.boolean().describe('Is the face visible in the asset'), +}).meta({ id: 'SyncAssetFaceV2' }); + +const SyncAssetFaceDeleteV1Schema = z + .object({ assetFaceId: z.string().describe('Asset face ID') }) + .meta({ id: 'SyncAssetFaceDeleteV1' }); + +const SyncUserMetadataV1Schema = z + .object({ + userId: z.string().describe('User ID'), + key: UserMetadataKeySchema, + value: z.record(z.string(), z.unknown()).describe('User metadata value'), + }) + .meta({ id: 'SyncUserMetadataV1' }); + +const SyncUserMetadataDeleteV1Schema = z + .object({ + userId: z.string().describe('User ID'), + key: UserMetadataKeySchema, + }) + .meta({ id: 'SyncUserMetadataDeleteV1' }); + +const SyncAckV1Schema = z.object({}).meta({ id: 'SyncAckV1' }); +const SyncResetV1Schema = z.object({}).meta({ id: 'SyncResetV1' }); +const SyncCompleteV1Schema = z.object({}).meta({ id: 'SyncCompleteV1' }); @ExtraModel() -export class SyncPartnerDeleteV1 { - @ApiProperty({ description: 'Shared by ID' }) - sharedById!: string; - @ApiProperty({ description: 'Shared with ID' }) - sharedWithId!: string; -} - +class SyncMemoryV1 extends createZodDto(SyncMemoryV1Schema) {} @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', description: 'Asset type' }) - type!: AssetType; - @ApiProperty({ description: 'Deleted at' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Is favorite' }) - isFavorite!: boolean; - @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', description: 'Asset width' }) - width!: number | null; - @ApiProperty({ type: 'integer', description: 'Asset height' }) - height!: number | null; - @ApiProperty({ description: 'Is edited' }) - isEdited!: boolean; -} - +class SyncMemoryDeleteV1 extends createZodDto(SyncMemoryDeleteV1Schema) {} @ExtraModel() -export class SyncAssetDeleteV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - +class SyncMemoryAssetV1 extends createZodDto(SyncMemoryAssetV1Schema) {} @ExtraModel() -export class SyncAssetExifV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Description' }) - description!: string | null; - @ApiProperty({ type: 'integer', description: 'Exif image width' }) - exifImageWidth!: number | null; - @ApiProperty({ type: 'integer', description: 'Exif image height' }) - exifImageHeight!: number | null; - @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', description: 'Latitude' }) - latitude!: number | null; - @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', description: 'F number' }) - fNumber!: number | null; - @ApiProperty({ type: 'number', format: 'double', description: 'Focal length' }) - focalLength!: number | null; - @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', description: 'Rating' }) - rating!: number | null; - @ApiProperty({ type: 'number', format: 'double', description: 'FPS' }) - fps!: number | null; -} - +class SyncMemoryAssetDeleteV1 extends createZodDto(SyncMemoryAssetDeleteV1Schema) {} @ExtraModel() -export class SyncAssetEditV1 { - id!: string; - assetId!: string; - - @ValidateEnum({ enum: AssetEditAction, name: 'AssetEditAction' }) - action!: AssetEditAction; - parameters!: object; - - @ApiProperty({ type: 'integer' }) - sequence!: number; -} - +class SyncStackV1 extends createZodDto(SyncStackV1Schema) {} @ExtraModel() -export class SyncAssetEditDeleteV1 { - editId!: string; -} - +class SyncStackDeleteV1 extends createZodDto(SyncStackDeleteV1Schema) {} @ExtraModel() -export class SyncAssetMetadataV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Key' }) - key!: string; - @ApiProperty({ description: 'Value' }) - value!: object; -} - +class SyncPersonV1 extends createZodDto(SyncPersonV1Schema) {} @ExtraModel() -export class SyncAssetMetadataDeleteV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Key' }) - key!: string; -} - +class SyncPersonDeleteV1 extends createZodDto(SyncPersonDeleteV1Schema) {} @ExtraModel() -export class SyncAlbumDeleteV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; -} - +class SyncAssetFaceV1 extends createZodDto(SyncAssetFaceV1Schema) {} @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', 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; -} - -@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', 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; - @ApiProperty({ type: 'integer' }) - imageHeight!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxX1!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxY1!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxX2!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxY2!: number; - @ApiProperty({ description: 'Source type' }) - sourceType!: string; -} - -@ExtraModel() -export class SyncAssetFaceV2 extends SyncAssetFaceV1 { - @ApiProperty({ description: 'Face deleted at' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Is the face visible in the asset' }) - isVisible!: boolean; -} +class SyncAssetFaceV2 extends createZodDto(SyncAssetFaceV2Schema) {} export function syncAssetFaceV2ToV1(faceV2: SyncAssetFaceV2): SyncAssetFaceV1 { const { deletedAt: _, isVisible: __, ...faceV1 } = faceV2; return faceV1; } - @ExtraModel() -export class SyncAssetFaceDeleteV1 { - @ApiProperty({ description: 'Asset face ID' }) - assetFaceId!: string; -} - +class SyncAssetFaceDeleteV1 extends createZodDto(SyncAssetFaceDeleteV1Schema) {} @ExtraModel() -export class SyncUserMetadataV1 { - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey', description: 'User metadata key' }) - key!: UserMetadataKey; - @ApiProperty({ description: 'User metadata value' }) - value!: UserMetadata[UserMetadataKey]; -} - +class SyncUserMetadataV1 extends createZodDto(SyncUserMetadataV1Schema) {} @ExtraModel() -export class SyncUserMetadataDeleteV1 { - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey', description: 'User metadata key' }) - key!: UserMetadataKey; -} - +class SyncUserMetadataDeleteV1 extends createZodDto(SyncUserMetadataDeleteV1Schema) {} @ExtraModel() -export class SyncAckV1 {} - +class SyncAckV1 extends createZodDto(SyncAckV1Schema) {} @ExtraModel() -export class SyncResetV1 {} - +class SyncResetV1 extends createZodDto(SyncResetV1Schema) {} @ExtraModel() -export class SyncCompleteV1 {} +class SyncCompleteV1 extends createZodDto(SyncCompleteV1Schema) {} export type SyncItem = { [SyncEntityType.AuthUserV1]: SyncAuthUserV1; @@ -541,35 +472,33 @@ export type SyncItem = { [SyncEntityType.SyncResetV1]: SyncResetV1; }; -export class SyncStreamDto { - @ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true, description: 'Sync request types' }) - types!: SyncRequestType[]; - - @ValidateBoolean({ optional: true, description: 'Reset sync state' }) - reset?: boolean; -} - -export class SyncAckDto { - @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, - description: 'Sync entity types to delete acks for', +const SyncStreamSchema = z + .object({ + types: z.array(SyncRequestTypeSchema).describe('Sync request types'), + reset: z.boolean().optional().describe('Reset sync state'), }) - types?: SyncEntityType[]; -} + .meta({ id: 'SyncStreamDto' }); + +const SyncAckSchema = z + .object({ + type: SyncEntityTypeSchema, + ack: z.string().describe('Acknowledgment ID'), + }) + .meta({ id: 'SyncAckDto' }); + +const SyncAckSetSchema = z + .object({ + acks: z.array(z.string()).max(1000).describe('Acknowledgment IDs (max 1000)'), + }) + .meta({ id: 'SyncAckSetDto' }); + +const SyncAckDeleteSchema = z + .object({ + types: z.array(SyncEntityTypeSchema).optional().describe('Sync entity types to delete acks for'), + }) + .meta({ id: 'SyncAckDeleteDto' }); + +export class SyncStreamDto extends createZodDto(SyncStreamSchema) {} +export class SyncAckDto extends createZodDto(SyncAckSchema) {} +export class SyncAckSetDto extends createZodDto(SyncAckSetSchema) {} +export class SyncAckDeleteDto extends createZodDto(SyncAckDeleteSchema) {} diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index a214dbc467..b5222fd883 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -1,863 +1,374 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { - ArrayMinSize, - IsInt, - IsNotEmpty, - IsNumber, - IsObject, - IsPositive, - IsString, - IsUrl, - Max, - Min, - ValidateIf, - ValidateNested, -} from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { SystemConfig } from 'src/config'; -import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig, OcrConfig } from 'src/dtos/model-config.dto'; +import { + CLIPConfigSchema, + DuplicateDetectionConfigSchema, + FacialRecognitionConfigSchema, + OcrConfigSchema, +} from 'src/dtos/model-config.dto'; import { AudioCodec, - CQMode, - Colorspace, - ImageFormat, - LogLevel, - OAuthTokenEndpointAuthMethod, - QueueName, - ToneMapping, - TranscodeHardwareAcceleration, - TranscodePolicy, - VideoCodec, - VideoContainer, + AudioCodecSchema, + ColorspaceSchema, + CQModeSchema, + ImageFormatSchema, + LogLevelSchema, + OAuthTokenEndpointAuthMethodSchema, + ToneMappingSchema, + TranscodeHardwareAccelerationSchema, + TranscodePolicySchema, + VideoCodecSchema, + VideoContainerSchema, } from 'src/enum'; -import { ConcurrentQueueName } from 'src/types'; -import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; +import { isValidTime } from 'src/validation'; +import z from 'zod'; -const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; -const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; -const isOAuthOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled; -const isEmailNotificationEnabled = (config: SystemConfigSmtpDto) => config.enabled; -const isDatabaseBackupEnabled = (config: DatabaseBackupConfig) => config.enabled; +/** Coerces 'true'/'false' strings to boolean, but also allows booleans. */ +const configBool = z + .preprocess((val) => { + if (val === 'true') { + return true; + } + if (val === 'false') { + return false; + } + return val; + }, z.boolean()) + .meta({ type: 'boolean' }); -export class DatabaseBackupConfig { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; +const JobSettingsSchema = z + .object({ + concurrency: z.int().min(1).describe('Concurrency'), + }) + .meta({ id: 'JobSettingsDto' }); - @ValidateIf(isDatabaseBackupEnabled) - @IsNotEmpty() - @IsCronExpression() - @IsString() - @ApiProperty({ description: 'Cron expression' }) - cronExpression!: string; +const cronExpressionSchema = z + .string() + .regex(/(((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7}/, 'Invalid cron expression') + .describe('Cron expression'); - @IsInt() - @IsPositive() - @IsNotEmpty() - @ApiProperty({ description: 'Keep last amount' }) - keepLastAmount!: number; -} +const DatabaseBackupSchema = z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + keepLastAmount: z.number().min(1).describe('Keep last amount'), + }) + .meta({ id: 'DatabaseBackupConfig' }); -export class SystemConfigBackupsDto { - @Type(() => DatabaseBackupConfig) - @ValidateNested() - @IsObject() - database!: DatabaseBackupConfig; -} +const SystemConfigBackupsSchema = z.object({ database: DatabaseBackupSchema }).meta({ id: 'SystemConfigBackupsDto' }); -export class SystemConfigFFmpegDto { - @IsInt() - @Min(0) - @Max(51) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'CRF' }) - crf!: number; +const SystemConfigFFmpegSchema = z + .object({ + crf: z.coerce.number().int().min(0).max(51).describe('CRF'), + threads: z.coerce.number().int().min(0).describe('Threads'), + preset: z.string().describe('Preset'), + targetVideoCodec: VideoCodecSchema, + acceptedVideoCodecs: z.array(VideoCodecSchema).describe('Accepted video codecs'), + targetAudioCodec: AudioCodecSchema, + acceptedAudioCodecs: z + .array(AudioCodecSchema) + .transform((value): AudioCodec[] => value.map((v) => (v === AudioCodec.Libopus ? AudioCodec.Opus : v))) + .describe('Accepted audio codecs'), + acceptedContainers: z.array(VideoContainerSchema).describe('Accepted containers'), + targetResolution: z.string().describe('Target resolution'), + maxBitrate: z.string().describe('Max bitrate'), + bframes: z.coerce.number().int().min(-1).max(16).describe('B-frames'), + refs: z.coerce.number().int().min(0).max(6).describe('References'), + gopSize: z.coerce.number().int().min(0).describe('GOP size'), + temporalAQ: configBool.describe('Temporal AQ'), + cqMode: CQModeSchema, + twoPass: configBool.describe('Two pass'), + preferredHwDevice: z.string().describe('Preferred hardware device'), + transcode: TranscodePolicySchema, + accel: TranscodeHardwareAccelerationSchema, + accelDecode: configBool.describe('Accelerated decode'), + tonemap: ToneMappingSchema, + }) + .meta({ id: 'SystemConfigFFmpegDto' }); - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Threads' }) - threads!: number; +const SystemConfigJobSchema = z + .object({ + thumbnailGeneration: JobSettingsSchema, + metadataExtraction: JobSettingsSchema, + videoConversion: JobSettingsSchema, + faceDetection: JobSettingsSchema, + smartSearch: JobSettingsSchema, + backgroundTask: JobSettingsSchema, + migration: JobSettingsSchema, + search: JobSettingsSchema, + sidecar: JobSettingsSchema, + library: JobSettingsSchema, + notifications: JobSettingsSchema, + ocr: JobSettingsSchema, + workflow: JobSettingsSchema, + editor: JobSettingsSchema, + }) + .meta({ id: 'SystemConfigJobDto' }); - @IsString() - @ApiProperty({ description: 'Preset' }) - preset!: string; +const SystemConfigLibraryScanSchema = z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + }) + .meta({ id: 'SystemConfigLibraryScanDto' }); - @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', description: 'Target video codec' }) - targetVideoCodec!: VideoCodec; +const SystemConfigLibraryWatchSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigLibraryWatchDto' }); - @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true, description: 'Accepted video codecs' }) - acceptedVideoCodecs!: VideoCodec[]; +const SystemConfigLibrarySchema = z + .object({ scan: SystemConfigLibraryScanSchema, watch: SystemConfigLibraryWatchSchema }) + .meta({ id: 'SystemConfigLibraryDto' }); - @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', description: 'Target audio codec' }) - targetAudioCodec!: AudioCodec; +const SystemConfigLoggingSchema = z + .object({ + enabled: configBool.describe('Enabled'), + level: LogLevelSchema, + }) + .meta({ id: 'SystemConfigLoggingDto' }); - @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true, description: 'Accepted audio codecs' }) - @Transform(({ value }) => { - if (Array.isArray(value)) { - const libopusIndex = value.indexOf('libopus'); - if (libopusIndex !== -1) { - value[libopusIndex] = 'opus'; - } +const MachineLearningAvailabilityChecksSchema = z + .object({ + enabled: configBool.describe('Enabled'), + timeout: z.number(), + interval: z.number(), + }) + .meta({ id: 'MachineLearningAvailabilityChecksDto' }); + +const SystemConfigMachineLearningSchema = z + .object({ + enabled: configBool.describe('Enabled'), + urls: z.array(z.string()).min(1).describe('ML service URLs'), + availabilityChecks: MachineLearningAvailabilityChecksSchema, + clip: CLIPConfigSchema, + duplicateDetection: DuplicateDetectionConfigSchema, + facialRecognition: FacialRecognitionConfigSchema, + ocr: OcrConfigSchema, + }) + .meta({ id: 'SystemConfigMachineLearningDto' }); + +const SystemConfigMapSchema = z + .object({ + enabled: configBool.describe('Enabled'), + lightStyle: z.url().describe('Light map style URL'), + darkStyle: z.url().describe('Dark map style URL'), + }) + .meta({ id: 'SystemConfigMapDto' }); + +const SystemConfigNewVersionCheckSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigNewVersionCheckDto' }); + +const SystemConfigNightlyTasksSchema = z + .object({ + startTime: isValidTime.describe('Start time'), + databaseCleanup: configBool.describe('Database cleanup'), + missingThumbnails: configBool.describe('Missing thumbnails'), + clusterNewFaces: configBool.describe('Cluster new faces'), + generateMemories: configBool.describe('Generate memories'), + syncQuotaUsage: configBool.describe('Sync quota usage'), + }) + .meta({ id: 'SystemConfigNightlyTasksDto' }); + +const SystemConfigOAuthSchema = z + .object({ + autoLaunch: configBool.describe('Auto launch'), + autoRegister: configBool.describe('Auto register'), + buttonText: z.string().describe('Button text'), + clientId: z.string().describe('Client ID'), + clientSecret: z.string().describe('Client secret'), + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema, + timeout: z.int().min(1).describe('Timeout'), + defaultStorageQuota: z.number().min(0).nullable().describe('Default storage quota'), + enabled: configBool.describe('Enabled'), + issuerUrl: z.string().describe('Issuer URL'), + scope: z.string().describe('Scope'), + signingAlgorithm: z.string().describe('Signing algorithm'), + profileSigningAlgorithm: z.string().describe('Profile signing algorithm'), + storageLabelClaim: z.string().describe('Storage label claim'), + storageQuotaClaim: z.string().describe('Storage quota claim'), + roleClaim: z.string().describe('Role claim'), + mobileOverrideEnabled: configBool.describe('Mobile override enabled'), + mobileRedirectUri: z.string().describe('Mobile redirect URI (set to empty string to disable)'), + }) + .transform((value, ctx) => { + if (!value.mobileOverrideEnabled || value.mobileRedirectUri === '') { + return value; + } + + if (!z.url().safeParse(value.mobileRedirectUri).success) { + ctx.issues.push({ + code: 'custom', + message: 'Mobile redirect URI must be an empty string or a valid URL', + input: value.mobileRedirectUri, + }); + return z.NEVER; } return value; }) - acceptedAudioCodecs!: AudioCodec[]; + .meta({ + id: 'SystemConfigOAuthDto', + }); - @ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true, description: 'Accepted containers' }) - acceptedContainers!: VideoContainer[]; +const SystemConfigPasswordLoginSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigPasswordLoginDto' }); - @IsString() - @ApiProperty({ description: 'Target resolution' }) - targetResolution!: string; +const SystemConfigReverseGeocodingSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigReverseGeocodingDto' }); - @IsString() - @ApiProperty({ description: 'Max bitrate' }) - maxBitrate!: string; +const SystemConfigFacesSchema = z + .object({ import: configBool.describe('Import') }) + .meta({ id: 'SystemConfigFacesDto' }); +const SystemConfigMetadataSchema = z.object({ faces: SystemConfigFacesSchema }).meta({ id: 'SystemConfigMetadataDto' }); - @IsInt() - @Min(-1) - @Max(16) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'B-frames' }) - bframes!: number; - - @IsInt() - @Min(0) - @Max(6) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'References' }) - refs!: number; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'GOP size' }) - gopSize!: number; - - @ValidateBoolean({ description: 'Temporal AQ' }) - temporalAQ!: boolean; - - @ValidateEnum({ enum: CQMode, name: 'CQMode', description: 'CQ mode' }) - cqMode!: CQMode; - - @ValidateBoolean({ description: 'Two pass' }) - twoPass!: boolean; - - @ApiProperty({ description: 'Preferred hardware device' }) - @IsString() - preferredHwDevice!: string; - - @ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy', description: 'Transcode policy' }) - transcode!: TranscodePolicy; - - @ValidateEnum({ - enum: TranscodeHardwareAcceleration, - name: 'TranscodeHWAccel', - description: 'Transcode hardware acceleration', +const SystemConfigServerSchema = z + .object({ + externalDomain: z + .string() + .refine((url) => url.length === 0 || z.url().safeParse(url).success, { + error: 'External domain must be an empty string or a valid URL', + }) + .describe('External domain'), + loginPageMessage: z.string().describe('Login page message'), + publicUsers: configBool.describe('Public users'), }) - accel!: TranscodeHardwareAcceleration; + .meta({ id: 'SystemConfigServerDto' }); - @ValidateBoolean({ description: 'Accelerated decode' }) - accelDecode!: boolean; - - @ValidateEnum({ enum: ToneMapping, name: 'ToneMapping', description: 'Tone mapping' }) - tonemap!: ToneMapping; -} - -class JobSettingsDto { - @IsInt() - @IsPositive() - @ApiProperty({ type: 'integer', description: 'Concurrency' }) - concurrency!: number; -} - -class SystemConfigJobDto implements Record { - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.ThumbnailGeneration]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.MetadataExtraction]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.VideoConversion]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.SmartSearch]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Migration]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.BackgroundTask]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Search]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.FaceDetection]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Ocr]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Sidecar]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Library]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Notification]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Workflow]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Editor]!: JobSettingsDto; -} - -class SystemConfigLibraryScanDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateIf(isLibraryScanEnabled) - @IsNotEmpty() - @IsCronExpression() - @IsString() - cronExpression!: string; -} - -class SystemConfigLibraryWatchDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigLibraryDto { - @Type(() => SystemConfigLibraryScanDto) - @ValidateNested() - @IsObject() - scan!: SystemConfigLibraryScanDto; - - @Type(() => SystemConfigLibraryWatchDto) - @ValidateNested() - @IsObject() - watch!: SystemConfigLibraryWatchDto; -} - -class SystemConfigLoggingDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateEnum({ enum: LogLevel, name: 'LogLevel' }) - level!: LogLevel; -} - -class MachineLearningAvailabilityChecksDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsInt() - timeout!: number; - - @IsInt() - interval!: number; -} - -class SystemConfigMachineLearningDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsUrl({ require_tld: false, allow_underscores: true }, { each: true }) - @ArrayMinSize(1) - @ValidateIf((dto) => dto.enabled) - @ApiProperty({ type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1 }) - urls!: string[]; - - @Type(() => MachineLearningAvailabilityChecksDto) - @ValidateNested() - @IsObject() - availabilityChecks!: MachineLearningAvailabilityChecksDto; - - @Type(() => CLIPConfig) - @ValidateNested() - @IsObject() - clip!: CLIPConfig; - - @Type(() => DuplicateDetectionConfig) - @ValidateNested() - @IsObject() - duplicateDetection!: DuplicateDetectionConfig; - - @Type(() => FacialRecognitionConfig) - @ValidateNested() - @IsObject() - facialRecognition!: FacialRecognitionConfig; - - @Type(() => OcrConfig) - @ValidateNested() - @IsObject() - ocr!: OcrConfig; -} - -enum MapTheme { - LIGHT = 'light', - DARK = 'dark', -} - -export class MapThemeDto { - @ValidateEnum({ enum: MapTheme, name: 'MapTheme' }) - theme!: MapTheme; -} - -class SystemConfigMapDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsNotEmpty() - @IsUrl() - lightStyle!: string; - - @IsNotEmpty() - @IsUrl() - darkStyle!: string; -} - -class SystemConfigNewVersionCheckDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigNightlyTasksDto { - @IsDateStringFormat('HH:mm', { message: 'startTime must be in HH:mm format' }) - startTime!: string; - - @ValidateBoolean({ description: 'Database cleanup' }) - databaseCleanup!: boolean; - - @ValidateBoolean({ description: 'Missing thumbnails' }) - missingThumbnails!: boolean; - - @ValidateBoolean({ description: 'Cluster new faces' }) - clusterNewFaces!: boolean; - - @ValidateBoolean({ description: 'Generate memories' }) - generateMemories!: boolean; - - @ValidateBoolean({ description: 'Sync quota usage' }) - syncQuotaUsage!: boolean; -} - -class SystemConfigOAuthDto { - @ValidateBoolean({ description: 'Auto launch' }) - autoLaunch!: boolean; - - @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', - description: 'Token endpoint auth method', +const SystemConfigSmtpTransportSchema = z + .object({ + ignoreCert: configBool.describe('Whether to ignore SSL certificate errors'), + host: z.string().describe('SMTP server hostname'), + port: z.number().min(0).max(65_535).describe('SMTP server port'), + secure: configBool.describe('Whether to use secure connection (TLS/SSL)'), + username: z.string().describe('SMTP username'), + password: z.string().describe('SMTP password'), }) - tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod; - - @IsInt() - @IsPositive() - @Optional() - @ApiProperty({ type: 'integer', description: 'Timeout' }) - timeout!: number; - - @IsNumber() - @Min(0) - @Optional({ nullable: true }) - @ApiProperty({ type: 'integer', format: 'int64', description: 'Default storage quota' }) - defaultStorageQuota!: number | null; - - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateIf(isOAuthEnabled) - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Issuer URL' }) - issuerUrl!: string; - - @ValidateBoolean({ description: 'Mobile override enabled' }) - mobileOverrideEnabled!: boolean; - - @ValidateIf(isOAuthOverrideEnabled) - @IsUrl() - @ApiProperty({ description: 'Mobile redirect URI' }) - mobileRedirectUri!: string; - - @IsString() - @ApiProperty({ description: 'Scope' }) - scope!: string; - - @IsString() - @IsNotEmpty() - signingAlgorithm!: string; - - @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({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigReverseGeocodingDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigFacesDto { - @ValidateBoolean({ description: 'Import' }) - import!: boolean; -} - -class SystemConfigMetadataDto { - @Type(() => SystemConfigFacesDto) - @ValidateNested() - @IsObject() - faces!: SystemConfigFacesDto; -} - -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({ description: 'Public users' }) - publicUsers!: boolean; -} - -class SystemConfigSmtpTransportDto { - @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({ 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({ 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() - @IsObject() - transport!: SystemConfigSmtpTransportDto; -} - -class SystemConfigNotificationsDto { - @Type(() => SystemConfigSmtpDto) - @ValidateNested() - @IsObject() - smtp!: SystemConfigSmtpDto; -} - -class SystemConfigTemplateEmailsDto { - @IsString() - albumInviteTemplate!: string; - - @IsString() - welcomeTemplate!: string; - - @IsString() - albumUpdateTemplate!: string; -} - -class SystemConfigTemplatesDto { - @Type(() => SystemConfigTemplateEmailsDto) - @ValidateNested() - @IsObject() - email!: SystemConfigTemplateEmailsDto; -} - -class SystemConfigStorageTemplateDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @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', description: 'Image format' }) - format!: ImageFormat; - - @IsInt() - @Min(1) - @Max(100) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Quality' }) - quality!: number; - - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Size' }) - size!: number; - - @ValidateBoolean({ optional: true, default: false }) - progressive?: boolean; -} - -class SystemConfigGeneratedFullsizeImageDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat', description: 'Image format' }) - format!: ImageFormat; - - @IsInt() - @Min(1) - @Max(100) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Quality' }) - quality!: number; - - @ValidateBoolean({ optional: true, default: false, description: 'Progressive' }) - progressive?: boolean; -} - -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', description: 'Colorspace' }) - colorspace!: Colorspace; - - @ValidateBoolean({ description: 'Extract embedded' }) - extractEmbedded!: boolean; -} - -class SystemConfigTrashDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Days' }) - days!: number; -} - -class SystemConfigUserDto { - @IsInt() - @Min(1) - @Type(() => Number) - @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() - user!: SystemConfigUserDto; -} + .meta({ id: 'SystemConfigSmtpTransportDto' }); + +const SystemConfigSmtpSchema = z + .object({ + enabled: configBool.describe('Whether SMTP email notifications are enabled'), + from: z.string().describe('Email address to send from'), + replyTo: z.string().describe('Email address for replies'), + transport: SystemConfigSmtpTransportSchema, + }) + .meta({ id: 'SystemConfigSmtpDto' }); + +const SystemConfigNotificationsSchema = z + .object({ smtp: SystemConfigSmtpSchema }) + .meta({ id: 'SystemConfigNotificationsDto' }); + +const SystemConfigTemplateEmailsSchema = z + .object({ + albumInviteTemplate: z.string().describe('Album invite template'), + welcomeTemplate: z.string().describe('Welcome template'), + albumUpdateTemplate: z.string().describe('Album update template'), + }) + .meta({ id: 'SystemConfigTemplateEmailsDto' }); +const SystemConfigTemplatesSchema = z + .object({ email: SystemConfigTemplateEmailsSchema }) + .meta({ id: 'SystemConfigTemplatesDto' }); + +const SystemConfigStorageTemplateSchema = z + .object({ + enabled: configBool.describe('Enabled'), + hashVerificationEnabled: configBool.describe('Hash verification enabled'), + template: z.string().describe('Template'), + }) + .meta({ id: 'SystemConfigStorageTemplateDto' }); + +const SystemConfigTemplateStorageOptionSchema = z + .object({ + yearOptions: z.array(z.string()).describe('Available year format options for storage template'), + monthOptions: z.array(z.string()).describe('Available month format options for storage template'), + weekOptions: z.array(z.string()).describe('Available week format options for storage template'), + dayOptions: z.array(z.string()).describe('Available day format options for storage template'), + hourOptions: z.array(z.string()).describe('Available hour format options for storage template'), + minuteOptions: z.array(z.string()).describe('Available minute format options for storage template'), + secondOptions: z.array(z.string()).describe('Available second format options for storage template'), + presetOptions: z.array(z.string()).describe('Available preset template options'), + }) + .meta({ id: 'SystemConfigTemplateStorageOptionDto' }); + +const SystemConfigThemeSchema = z + .object({ customCss: z.string().describe('Custom CSS for theming') }) + .meta({ id: 'SystemConfigThemeDto' }); + +const SystemConfigGeneratedImageSchema = z + .object({ + format: ImageFormatSchema, + quality: z.int().min(1).max(100).describe('Quality'), + size: z.int().min(1).describe('Size'), + progressive: configBool.default(false).optional().describe('Progressive'), + }) + .meta({ id: 'SystemConfigGeneratedImageDto' }); + +const SystemConfigGeneratedFullsizeImageSchema = z + .object({ + enabled: configBool.describe('Enabled'), + format: ImageFormatSchema, + quality: z.int().min(1).max(100).describe('Quality'), + progressive: configBool.default(false).optional().describe('Progressive'), + }) + .meta({ id: 'SystemConfigGeneratedFullsizeImageDto' }); + +const SystemConfigImageSchema = z + .object({ + thumbnail: SystemConfigGeneratedImageSchema, + preview: SystemConfigGeneratedImageSchema, + fullsize: SystemConfigGeneratedFullsizeImageSchema, + colorspace: ColorspaceSchema, + extractEmbedded: configBool.describe('Extract embedded'), + }) + .meta({ id: 'SystemConfigImageDto' }); + +const SystemConfigTrashSchema = z + .object({ + enabled: configBool.describe('Enabled'), + days: z.int().min(0).describe('Days'), + }) + .meta({ id: 'SystemConfigTrashDto' }); + +const SystemConfigUserSchema = z + .object({ + deleteDelay: z.int().min(1).describe('Delete delay'), + }) + .meta({ id: 'SystemConfigUserDto' }); + +export const SystemConfigSchema = z + .object({ + backup: SystemConfigBackupsSchema, + ffmpeg: SystemConfigFFmpegSchema, + logging: SystemConfigLoggingSchema, + machineLearning: SystemConfigMachineLearningSchema, + map: SystemConfigMapSchema, + newVersionCheck: SystemConfigNewVersionCheckSchema, + nightlyTasks: SystemConfigNightlyTasksSchema, + oauth: SystemConfigOAuthSchema, + passwordLogin: SystemConfigPasswordLoginSchema, + reverseGeocoding: SystemConfigReverseGeocodingSchema, + metadata: SystemConfigMetadataSchema, + storageTemplate: SystemConfigStorageTemplateSchema, + job: SystemConfigJobSchema, + image: SystemConfigImageSchema, + trash: SystemConfigTrashSchema, + theme: SystemConfigThemeSchema, + library: SystemConfigLibrarySchema, + notifications: SystemConfigNotificationsSchema, + templates: SystemConfigTemplatesSchema, + server: SystemConfigServerSchema, + user: SystemConfigUserSchema, + }) + .describe('System configuration') + .meta({ id: 'SystemConfigDto' }); + +export class SystemConfigFFmpegDto extends createZodDto(SystemConfigFFmpegSchema) {} +export class SystemConfigSmtpDto extends createZodDto(SystemConfigSmtpSchema) {} +export class SystemConfigTemplateStorageOptionDto extends createZodDto(SystemConfigTemplateStorageOptionSchema) {} +export class SystemConfigDto extends createZodDto(SystemConfigSchema) {} export function mapConfig(config: SystemConfig): SystemConfigDto { return config; diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts index 0a4d55c970..676a06f774 100644 --- a/server/src/dtos/system-metadata.dto.ts +++ b/server/src/dtos/system-metadata.dto.ts @@ -1,26 +1,33 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ValidateBoolean } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class AdminOnboardingUpdateDto { - @ValidateBoolean({ description: 'Is admin onboarded' }) - isOnboarded!: boolean; -} +const AdminOnboardingUpdateSchema = z + .object({ + isOnboarded: z.boolean().describe('Is admin onboarded'), + }) + .meta({ id: 'AdminOnboardingUpdateDto' }); -export class AdminOnboardingResponseDto { - @ValidateBoolean({ description: 'Is admin onboarded' }) - isOnboarded!: boolean; -} +const AdminOnboardingResponseSchema = z + .object({ + isOnboarded: z.boolean().describe('Is admin onboarded'), + }) + .meta({ id: 'AdminOnboardingResponseDto' }); -export class ReverseGeocodingStateResponseDto { - @ApiProperty({ description: 'Last update timestamp' }) - lastUpdate!: string | null; - @ApiProperty({ description: 'Last import file name' }) - lastImportFileName!: string | null; -} +const ReverseGeocodingStateResponseSchema = z + .object({ + lastUpdate: z.string().nullable().describe('Last update timestamp'), + lastImportFileName: z.string().nullable().describe('Last import file name'), + }) + .meta({ id: 'ReverseGeocodingStateResponseDto' }); -export class VersionCheckStateResponseDto { - @ApiProperty({ description: 'Last check timestamp' }) - checkedAt!: string | null; - @ApiProperty({ description: 'Release version' }) - releaseVersion!: string | null; -} +const VersionCheckStateResponseSchema = z + .object({ + checkedAt: z.string().nullable().describe('Last check timestamp'), + releaseVersion: z.string().nullable().describe('Release version'), + }) + .meta({ id: 'VersionCheckStateResponseDto' }); + +export class AdminOnboardingUpdateDto extends createZodDto(AdminOnboardingUpdateSchema) {} +export class AdminOnboardingResponseDto extends createZodDto(AdminOnboardingResponseSchema) {} +export class ReverseGeocodingStateResponseDto extends createZodDto(ReverseGeocodingStateResponseSchema) {} +export class VersionCheckStateResponseDto extends createZodDto(VersionCheckStateResponseSchema) {} diff --git a/server/src/dtos/tag.dto.ts b/server/src/dtos/tag.dto.ts index ea85ea71f3..67dbca9914 100644 --- a/server/src/dtos/tag.dto.ts +++ b/server/src/dtos/tag.dto.ts @@ -1,68 +1,63 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsHexColor, IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Tag } from 'src/database'; import { MaybeDehydrated } from 'src/types'; import { asDateString } from 'src/utils/date'; -import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation'; +import { emptyStringToNull, hexColor } from 'src/validation'; +import z from 'zod'; -export class TagCreateDto { - @ApiProperty({ description: 'Tag name' }) - @IsString() - @IsNotEmpty() - name!: string; +const TagCreateSchema = z + .object({ + name: z.string().describe('Tag name'), + parentId: z.uuidv4().nullish().describe('Parent tag ID'), + color: emptyStringToNull(hexColor.nullable()).optional().describe('Tag color (hex)'), + }) + .meta({ id: 'TagCreateDto' }); - @ValidateUUID({ nullable: true, optional: true, description: 'Parent tag ID' }) - parentId?: string | null; +const TagUpdateSchema = z + .object({ + color: emptyStringToNull(hexColor.nullable()).optional().describe('Tag color (hex)'), + }) + .meta({ id: 'TagUpdateDto' }); - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - @IsHexColor() - @Optional({ nullable: true, emptyToNull: true }) - color?: string; -} +const TagUpsertSchema = z + .object({ + tags: z.array(z.string()).describe('Tag names to upsert'), + }) + .meta({ id: 'TagUpsertDto' }); -export class TagUpdateDto { - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - @Optional({ nullable: true, emptyToNull: true }) - @ValidateHexColor() - color?: string | null; -} +const TagBulkAssetsSchema = z + .object({ + tagIds: z.array(z.uuidv4()).describe('Tag IDs'), + assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + }) + .meta({ id: 'TagBulkAssetsDto' }); -export class TagUpsertDto { - @ApiProperty({ description: 'Tag names to upsert' }) - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - tags!: string[]; -} +const TagBulkAssetsResponseSchema = z + .object({ + count: z.int().describe('Number of assets tagged'), + }) + .meta({ id: 'TagBulkAssetsResponseDto' }); -export class TagBulkAssetsDto { - @ValidateUUID({ each: true, description: 'Tag IDs' }) - tagIds!: string[]; +export const TagResponseSchema = z + .object({ + id: z.string().describe('Tag ID'), + parentId: z.string().optional().describe('Parent tag ID'), + name: z.string().describe('Tag name'), + value: z.string().describe('Tag value (full path)'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'), + color: z.string().optional().describe('Tag color (hex)'), + }) + .meta({ id: 'TagResponseDto' }); - @ValidateUUID({ each: true, description: 'Asset IDs' }) - assetIds!: string[]; -} - -export class TagBulkAssetsResponseDto { - @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', format: 'date-time' }) - createdAt!: string; - @ApiProperty({ description: 'Last update date', format: 'date-time' }) - updatedAt!: string; - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - color?: string; -} +export class TagCreateDto extends createZodDto(TagCreateSchema) {} +export class TagUpdateDto extends createZodDto(TagUpdateSchema) {} +export class TagUpsertDto extends createZodDto(TagUpsertSchema) {} +export class TagBulkAssetsDto extends createZodDto(TagBulkAssetsSchema) {} +export class TagBulkAssetsResponseDto extends createZodDto(TagBulkAssetsResponseSchema) {} +export class TagResponseDto extends createZodDto(TagResponseSchema) {} export function mapTag(entity: MaybeDehydrated): TagResponseDto { return { diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index 9ea9dc49ae..af820e6868 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,230 +1,128 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import type { BBoxDto } from 'src/dtos/bbox.dto'; -import { AssetOrder, AssetVisibility } from 'src/enum'; -import { ValidateBBox } from 'src/utils/bbox'; -import { ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { BBoxSchema } from 'src/dtos/bbox.dto'; +import { AssetOrderSchema, AssetVisibilitySchema } from 'src/enum'; +import { stringToBool } from 'src/validation'; +import z from 'zod'; -export class TimeBucketDto { - @ValidateUUID({ optional: true, description: 'Filter assets by specific user ID' }) - userId?: string; +const TimeBucketQueryBaseSchema = z + .object({ + userId: z.uuidv4().optional().describe('Filter assets by specific user ID'), + albumId: z.uuidv4().optional().describe('Filter assets belonging to a specific album'), + personId: z.uuidv4().optional().describe('Filter assets containing a specific person (face recognition)'), + tagId: z.uuidv4().optional().describe('Filter assets with a specific tag'), + isFavorite: stringToBool + .optional() + .describe('Filter by favorite status (true for favorites only, false for non-favorites only)'), + isTrashed: stringToBool + .optional() + .describe('Filter by trash status (true for trashed assets only, false for non-trashed only)'), + withStacked: stringToBool + .optional() + .describe('Include stacked assets in the response. When true, only primary assets from stacks are returned.'), + withPartners: stringToBool.optional().describe('Include assets shared by partners'), + order: AssetOrderSchema.optional().describe( + 'Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)', + ), + visibility: AssetVisibilitySchema.optional().describe( + 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)', + ), + withCoordinates: stringToBool.optional().describe('Include location data in the response'), + key: z.string().optional(), + slug: z.string().optional(), + bbox: z + .string() + .transform((value, ctx) => { + const parts = value.split(','); + if (parts.length !== 4) { + ctx.issues.push({ + code: 'custom', + message: 'bbox must have 4 comma-separated numbers: west,south,east,north', + input: value, + }); + return z.NEVER; + } - @ValidateUUID({ optional: true, description: 'Filter assets belonging to a specific album' }) - albumId?: string; + const [west, south, east, north] = parts.map(Number); + if ([west, south, east, north].some((part) => Number.isNaN(part))) { + ctx.issues.push({ + code: 'custom', + message: 'bbox parts must be valid numbers', + input: value, + }); + return z.NEVER; + } - @ValidateUUID({ optional: true, description: 'Filter assets containing a specific person (face recognition)' }) - personId?: string; - - @ValidateUUID({ optional: true, description: 'Filter assets with a specific tag' }) - tagId?: string; - - @ValidateBoolean({ - optional: true, - description: 'Filter by favorite status (true for favorites only, false for non-favorites only)', + return { west, south, east, north }; + }) + .pipe(BBoxSchema) + .optional() + .describe('Bounding box coordinates as west,south,east,north (WGS84)') + .meta({ example: '11.075683,49.416711,11.117589,49.454875' }), }) - isFavorite?: boolean; + .meta({ id: 'TimeBucketDto' }); - @ValidateBoolean({ - optional: true, - description: 'Filter by trash status (true for trashed assets only, false for non-trashed only)', +const TimeBucketSchema = TimeBucketQueryBaseSchema; +const TimeBucketAssetSchema = TimeBucketQueryBaseSchema.extend({ + timeBucket: z.string().describe('Time bucket identifier in YYYY-MM-DD format').meta({ example: '2024-01-01' }), +}).meta({ id: 'TimeBucketAssetDto' }); + +const stackTupleSchema = z.array(z.string()).length(2).nullable(); + +const TimeBucketAssetResponseSchema = z + .object({ + id: z.array(z.string()).describe('Array of asset IDs in the time bucket'), + ownerId: z.array(z.string()).describe('Array of owner IDs for each asset'), + ratio: z.array(z.number()).describe('Array of aspect ratios (width/height) for each asset'), + isFavorite: z.array(z.boolean()).describe('Array indicating whether each asset is favorited'), + visibility: z + .array(AssetVisibilitySchema) + .describe('Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)'), + isTrashed: z.array(z.boolean()).describe('Array indicating whether each asset is in the trash'), + isImage: z.array(z.boolean()).describe('Array indicating whether each asset is an image (false for videos)'), + thumbhash: z + .array(z.string().nullable()) + .describe('Array of BlurHash strings for generating asset previews (base64 encoded)'), + fileCreatedAt: z.array(z.string()).describe('Array of file creation timestamps in UTC'), + localOffsetHours: z + .array(z.number()) + .describe( + "Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.", + ), + duration: z.array(z.string().nullable()).describe('Array of video durations in HH:MM:SS format (null for images)'), + stack: z + .array(stackTupleSchema) + .optional() + .describe('Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets)'), + projectionType: z + .array(z.string().nullable()) + .describe('Array of projection types for 360° content (e.g., "EQUIRECTANGULAR", "CUBEFACE", "CYLINDRICAL")'), + livePhotoVideoId: z + .array(z.string().nullable()) + .describe('Array of live photo video asset IDs (null for non-live photos)'), + city: z.array(z.string().nullable()).describe('Array of city names extracted from EXIF GPS data'), + country: z.array(z.string().nullable()).describe('Array of country names extracted from EXIF GPS data'), + latitude: z + .array(z.number().nullable()) + .optional() + .describe('Array of latitude coordinates extracted from EXIF GPS data'), + longitude: z + .array(z.number().nullable()) + .optional() + .describe('Array of longitude coordinates extracted from EXIF GPS data'), }) - isTrashed?: boolean; + .meta({ id: 'TimeBucketAssetResponseDto' }); - @ValidateBoolean({ - optional: true, - description: 'Include stacked assets in the response. When true, only primary assets from stacks are returned.', +const TimeBucketsResponseSchema = z + .object({ + timeBucket: z + .string() + .describe('Time bucket identifier in YYYY-MM-DD format representing the start of the time period') + .meta({ example: '2024-01-01' }), + count: z.int().describe('Number of assets in this time bucket').meta({ example: 42 }), }) - withStacked?: boolean; + .meta({ id: 'TimeBucketsResponseDto' }); - @ValidateBoolean({ optional: true, description: 'Include assets shared by partners' }) - withPartners?: boolean; - - @ValidateEnum({ - enum: AssetOrder, - name: 'AssetOrder', - description: 'Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)', - optional: true, - }) - order?: AssetOrder; - - @ValidateEnum({ - enum: AssetVisibility, - name: 'AssetVisibility', - optional: true, - description: 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)', - }) - visibility?: AssetVisibility; - - @ValidateBoolean({ - optional: true, - description: 'Include location data in the response', - }) - withCoordinates?: boolean; - - @ValidateBBox({ optional: true }) - bbox?: BBoxDto; -} - -export class TimeBucketAssetDto extends TimeBucketDto { - @ApiProperty({ - type: 'string', - description: 'Time bucket identifier in YYYY-MM-DD format (e.g., "2024-01-01" for January 2024)', - example: '2024-01-01', - }) - @IsString() - timeBucket!: string; -} - -export class TimeBucketAssetResponseDto { - @ApiProperty({ - type: 'array', - items: { type: 'string' }, - description: 'Array of asset IDs in the time bucket', - }) - id!: string[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string' }, - description: 'Array of owner IDs for each asset', - }) - ownerId!: string[]; - - @ApiProperty({ - type: 'array', - items: { type: 'number' }, - description: 'Array of aspect ratios (width/height) for each asset', - }) - ratio!: number[]; - - @ApiProperty({ - type: 'array', - items: { type: 'boolean' }, - description: 'Array indicating whether each asset is favorited', - }) - isFavorite!: boolean[]; - - @ValidateEnum({ - enum: AssetVisibility, - name: 'AssetVisibility', - each: true, - description: 'Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)', - }) - visibility!: AssetVisibility[]; - - @ApiProperty({ - type: 'array', - items: { type: 'boolean' }, - description: 'Array indicating whether each asset is in the trash', - }) - isTrashed!: boolean[]; - - @ApiProperty({ - type: 'array', - items: { type: 'boolean' }, - description: 'Array indicating whether each asset is an image (false for videos)', - }) - isImage!: boolean[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of BlurHash strings for generating asset previews (base64 encoded)', - }) - thumbhash!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string' }, - description: 'Array of file creation timestamps in UTC', - }) - fileCreatedAt!: string[]; - - @ApiProperty({ - type: 'array', - items: { type: 'number' }, - description: - "Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.", - }) - localOffsetHours!: number[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of video durations in HH:MM:SS format (null for images)', - }) - duration!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { - type: 'array', - items: { type: 'string' }, - minItems: 2, - maxItems: 2, - nullable: true, - }, - description: 'Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets)', - }) - stack?: ([string, string] | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of projection types for 360° content (e.g., "EQUIRECTANGULAR", "CUBEFACE", "CYLINDRICAL")', - }) - projectionType!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of live photo video asset IDs (null for non-live photos)', - }) - livePhotoVideoId!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of city names extracted from EXIF GPS data', - }) - city!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of country names extracted from EXIF GPS data', - }) - country!: (string | null)[]; - - @ApiProperty({ - type: 'array', - required: false, - items: { type: 'number', nullable: true }, - description: 'Array of latitude coordinates extracted from EXIF GPS data', - }) - latitude!: number[]; - - @ApiProperty({ - type: 'array', - required: false, - items: { type: 'number', nullable: true }, - description: 'Array of longitude coordinates extracted from EXIF GPS data', - }) - longitude!: number[]; -} - -export class TimeBucketsResponseDto { - @ApiProperty({ - type: 'string', - description: 'Time bucket identifier in YYYY-MM-DD format representing the start of the time period', - example: '2024-01-01', - }) - timeBucket!: string; - - @ApiProperty({ - type: 'integer', - description: 'Number of assets in this time bucket', - example: 42, - }) - count!: number; -} +export class TimeBucketDto extends createZodDto(TimeBucketSchema) {} +export class TimeBucketAssetDto extends createZodDto(TimeBucketAssetSchema) {} +export class TimeBucketAssetResponseDto extends createZodDto(TimeBucketAssetResponseSchema) {} +export class TimeBucketsResponseDto extends createZodDto(TimeBucketsResponseSchema) {} diff --git a/server/src/dtos/trash.dto.ts b/server/src/dtos/trash.dto.ts index f1d1f109f6..9a725bc6c8 100644 --- a/server/src/dtos/trash.dto.ts +++ b/server/src/dtos/trash.dto.ts @@ -1,6 +1,10 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class TrashResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of items in trash' }) - count!: number; -} +const TrashResponseSchema = z + .object({ + count: z.int().describe('Number of items in trash'), + }) + .meta({ id: 'TrashResponseDto' }); + +export class TrashResponseDto extends createZodDto(TrashResponseSchema) {} diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index cce1994007..7a7c1d2558 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,302 +1,212 @@ -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'; +import { createZodDto } from 'nestjs-zod'; +import { AssetOrderSchema, UserAvatarColorSchema } from 'src/enum'; import { UserPreferences } from 'src/types'; -import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; +import z from 'zod'; -class AvatarUpdate { - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' }) - color?: UserAvatarColor; -} +const AlbumsUpdateSchema = z + .object({ + defaultAssetOrder: AssetOrderSchema.optional(), + }) + .optional() + .describe('Album preferences') + .meta({ id: 'AlbumsUpdate' }); -class MemoriesUpdate { - @ValidateBoolean({ optional: true, description: 'Whether memories are enabled' }) - enabled?: boolean; +const AvatarUpdateSchema = z + .object({ + color: UserAvatarColorSchema.optional(), + }) + .optional() + .meta({ id: 'AvatarUpdate' }); - @Optional() - @IsInt() - @IsPositive() - @ApiProperty({ type: 'integer', description: 'Memory duration in seconds' }) - duration?: number; -} +const MemoriesUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether memories are enabled'), + duration: z.int().min(1).optional().describe('Memory duration in seconds'), + }) + .optional() + .meta({ id: 'MemoriesUpdate' }); -class RatingsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether ratings are enabled' }) - enabled?: boolean; -} +const RatingsUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether ratings are enabled'), + }) + .optional() + .meta({ id: 'RatingsUpdate' }); -@ApiSchema({ description: 'Album preferences' }) -class AlbumsUpdate { - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, description: 'Default asset order for albums' }) - defaultAssetOrder?: AssetOrder; -} +const FoldersUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether folders are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether folders appear in web sidebar'), + }) + .optional() + .meta({ id: 'FoldersUpdate' }); -class FoldersUpdate { - @ValidateBoolean({ optional: true, description: 'Whether folders are enabled' }) - enabled?: boolean; +const PeopleUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether people are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether people appear in web sidebar'), + }) + .optional() + .meta({ id: 'PeopleUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether folders appear in web sidebar' }) - sidebarWeb?: boolean; -} +const SharedLinksUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether shared links are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether shared links appear in web sidebar'), + }) + .optional() + .meta({ id: 'SharedLinksUpdate' }); -class PeopleUpdate { - @ValidateBoolean({ optional: true, description: 'Whether people are enabled' }) - enabled?: boolean; +const TagsUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether tags are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether tags appear in web sidebar'), + }) + .optional() + .meta({ id: 'TagsUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether people appear in web sidebar' }) - sidebarWeb?: boolean; -} +const EmailNotificationsUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether email notifications are enabled'), + albumInvite: z.boolean().optional().describe('Whether to receive email notifications for album invites'), + albumUpdate: z.boolean().optional().describe('Whether to receive email notifications for album updates'), + }) + .optional() + .meta({ id: 'EmailNotificationsUpdate' }); -class SharedLinksUpdate { - @ValidateBoolean({ optional: true, description: 'Whether shared links are enabled' }) - enabled?: boolean; +const DownloadUpdateSchema = z + .object({ + archiveSize: z.int().min(1).optional().describe('Maximum archive size in bytes'), + includeEmbeddedVideos: z.boolean().optional().describe('Whether to include embedded videos in downloads'), + }) + .optional() + .meta({ id: 'DownloadUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether shared links appear in web sidebar' }) - sidebarWeb?: boolean; -} +const PurchaseUpdateSchema = z + .object({ + showSupportBadge: z.boolean().optional().describe('Whether to show support badge'), + hideBuyButtonUntil: z.string().optional().describe('Date until which to hide buy button'), + }) + .optional() + .meta({ id: 'PurchaseUpdate' }); -class TagsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether tags are enabled' }) - enabled?: boolean; +const CastUpdateSchema = z + .object({ + gCastEnabled: z.boolean().optional().describe('Whether Google Cast is enabled'), + }) + .optional() + .meta({ id: 'CastUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether tags appear in web sidebar' }) - sidebarWeb?: boolean; -} +const UserPreferencesUpdateSchema = z + .object({ + albums: AlbumsUpdateSchema, + avatar: AvatarUpdateSchema, + cast: CastUpdateSchema, + download: DownloadUpdateSchema, + emailNotifications: EmailNotificationsUpdateSchema, + folders: FoldersUpdateSchema, + memories: MemoriesUpdateSchema, + people: PeopleUpdateSchema, + purchase: PurchaseUpdateSchema, + ratings: RatingsUpdateSchema, + sharedLinks: SharedLinksUpdateSchema, + tags: TagsUpdateSchema, + }) + .meta({ id: 'UserPreferencesUpdateDto' }); -class EmailNotificationsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether email notifications are enabled' }) - enabled?: boolean; +const AlbumsResponseSchema = z + .object({ + defaultAssetOrder: AssetOrderSchema, + }) + .meta({ id: 'AlbumsResponse' }); - @ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album invites' }) - albumInvite?: boolean; +const FoldersResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether folders are enabled'), + sidebarWeb: z.boolean().describe('Whether folders appear in web sidebar'), + }) + .meta({ id: 'FoldersResponse' }); - @ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album updates' }) - albumUpdate?: boolean; -} +const MemoriesResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether memories are enabled'), + duration: z.int().describe('Memory duration in seconds'), + }) + .meta({ id: 'MemoriesResponse' }); -class DownloadUpdate implements Partial { - @Optional() - @IsInt() - @IsPositive() - @ApiPropertyOptional({ type: 'integer', description: 'Maximum archive size in bytes' }) - archiveSize?: number; +const PeopleResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether people are enabled'), + sidebarWeb: z.boolean().describe('Whether people appear in web sidebar'), + }) + .meta({ id: 'PeopleResponse' }); - @ValidateBoolean({ optional: true, description: 'Whether to include embedded videos in downloads' }) - includeEmbeddedVideos?: boolean; -} +const RatingsResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether ratings are enabled'), + }) + .meta({ id: 'RatingsResponse' }); -class PurchaseUpdate { - @ValidateBoolean({ optional: true, description: 'Whether to show support badge' }) - showSupportBadge?: boolean; +const SharedLinksResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether shared links are enabled'), + sidebarWeb: z.boolean().describe('Whether shared links appear in web sidebar'), + }) + .meta({ id: 'SharedLinksResponse' }); - @ApiPropertyOptional({ description: 'Date until which to hide buy button' }) - @IsDateString() - @Optional() - hideBuyButtonUntil?: string; -} +const TagsResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether tags are enabled'), + sidebarWeb: z.boolean().describe('Whether tags appear in web sidebar'), + }) + .meta({ id: 'TagsResponse' }); -class CastUpdate { - @ValidateBoolean({ optional: true, description: 'Whether Google Cast is enabled' }) - gCastEnabled?: boolean; -} +const EmailNotificationsResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether email notifications are enabled'), + albumInvite: z.boolean().describe('Whether to receive email notifications for album invites'), + albumUpdate: z.boolean().describe('Whether to receive email notifications for album updates'), + }) + .meta({ id: 'EmailNotificationsResponse' }); -export class UserPreferencesUpdateDto { - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => AlbumsUpdate) - albums?: AlbumsUpdate; +const DownloadResponseSchema = z + .object({ + archiveSize: z.int().describe('Maximum archive size in bytes'), + includeEmbeddedVideos: z.boolean().describe('Whether to include embedded videos in downloads'), + }) + .meta({ id: 'DownloadResponse' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => FoldersUpdate) - folders?: FoldersUpdate; +const PurchaseResponseSchema = z + .object({ + showSupportBadge: z.boolean().describe('Whether to show support badge'), + hideBuyButtonUntil: z.string().describe('Date until which to hide buy button'), + }) + .meta({ id: 'PurchaseResponse' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => MemoriesUpdate) - memories?: MemoriesUpdate; +const CastResponseSchema = z + .object({ + gCastEnabled: z.boolean().describe('Whether Google Cast is enabled'), + }) + .meta({ id: 'CastResponse' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => PeopleUpdate) - people?: PeopleUpdate; +const UserPreferencesResponseSchema = z + .object({ + albums: AlbumsResponseSchema, + folders: FoldersResponseSchema, + memories: MemoriesResponseSchema, + people: PeopleResponseSchema, + ratings: RatingsResponseSchema, + sharedLinks: SharedLinksResponseSchema, + tags: TagsResponseSchema, + emailNotifications: EmailNotificationsResponseSchema, + download: DownloadResponseSchema, + purchase: PurchaseResponseSchema, + cast: CastResponseSchema, + }) + .meta({ id: 'UserPreferencesResponseDto' }); - // 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) - cast?: CastUpdate; -} - -class AlbumsResponse { - @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', 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', 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; -} +export class UserPreferencesUpdateDto extends createZodDto(UserPreferencesUpdateSchema) {} +export class UserPreferencesResponseDto extends createZodDto(UserPreferencesResponseSchema) {} export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { return preferences; diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts index 6559dd052c..c3c91d3d95 100644 --- a/server/src/dtos/user-profile.dto.ts +++ b/server/src/dtos/user-profile.dto.ts @@ -1,16 +1,20 @@ import { ApiProperty } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; import { UploadFieldName } from 'src/dtos/asset-media.dto'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; export class CreateProfileImageDto { @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; -} +const CreateProfileImageResponseSchema = z + .object({ + userId: z.string().describe('User ID'), + profileChangedAt: isoDatetimeToDate.describe('Profile image change date'), + profileImagePath: z.string().describe('Profile image file path'), + }) + .meta({ id: 'CreateProfileImageResponseDto' }); + +export class CreateProfileImageResponseDto extends createZodDto(CreateProfileImageResponseSchema) {} diff --git a/server/src/dtos/user.dto.spec.ts b/server/src/dtos/user.dto.spec.ts index e6be3b17d1..6acc9554f9 100644 --- a/server/src/dtos/user.dto.spec.ts +++ b/server/src/dtos/user.dto.spec.ts @@ -1,69 +1,59 @@ -import { plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; -import { UserAdminCreateDto, UserUpdateMeDto } from 'src/dtos/user.dto'; +import { UserAdminCreateSchema, UserUpdateMeSchema } from 'src/dtos/user.dto'; describe('update user DTO', () => { - it('should allow emails without a tld', async () => { + it('should allow emails without a tld', () => { const someEmail = 'test@test'; - - const dto = plainToInstance(UserUpdateMeDto, { + const result = UserUpdateMeSchema.safeParse({ email: someEmail, id: '3fe388e4-2078-44d7-b36c-39d9dee3a657', }); - const errors = await validate(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); + expect(result.success).toBe(true); + expect(result.data?.email).toEqual(someEmail); }); }); describe('create user DTO', () => { - it('validates the email', async () => { - const params: Partial = { - email: undefined, + it('validates the email', () => { + expect(UserAdminCreateSchema.safeParse({ password: 'password', name: 'name' }).success).toBe(false); + + expect( + UserAdminCreateSchema.safeParse({ email: 'invalid email', password: 'password', name: 'name' }).success, + ).toBe(false); + + const result = UserAdminCreateSchema.safeParse({ + email: 'valid@email.com', password: 'password', name: 'name', - }; - let dto: UserAdminCreateDto = plainToInstance(UserAdminCreateDto, params); - let errors = await validate(dto); - expect(errors).toHaveLength(1); - - params.email = 'invalid email'; - dto = plainToInstance(UserAdminCreateDto, params); - errors = await validate(dto); - expect(errors).toHaveLength(1); - - params.email = 'valid@email.com'; - dto = plainToInstance(UserAdminCreateDto, params); - errors = await validate(dto); - expect(errors).toHaveLength(0); + }); + expect(result.success).toBe(true); }); - it('validates invalid email type', async () => { - let dto = plainToInstance(UserAdminCreateDto, { - email: [], - password: 'some password', - name: 'some name', - }); - expect(await validate(dto)).toHaveLength(1); + it('validates invalid email type', () => { + expect( + UserAdminCreateSchema.safeParse({ + email: [], + password: 'some password', + name: 'some name', + }).success, + ).toBe(false); - dto = plainToInstance(UserAdminCreateDto, { - email: {}, - password: 'some password', - name: 'some name', - }); - expect(await validate(dto)).toHaveLength(1); + expect( + UserAdminCreateSchema.safeParse({ + email: {}, + password: 'some password', + name: 'some name', + }).success, + ).toBe(false); }); - it('should allow emails without a tld', async () => { + it('should allow emails without a tld', () => { const someEmail = 'test@test'; - - const dto = plainToInstance(UserAdminCreateDto, { + const result = UserAdminCreateSchema.safeParse({ email: someEmail, password: 'some password', name: 'some name', }); - const errors = await validate(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); + expect(result.success).toBe(true); + expect(result.data?.email).toEqual(someEmail); }); }); diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index ebd0018bba..75256b9e1a 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,65 +1,50 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { User, UserAdmin } from 'src/database'; -import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; +import { pinCodeRegex } from 'src/dtos/auth.dto'; +import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum'; import { MaybeDehydrated, UserMetadataItem } from 'src/types'; import { asDateString } from 'src/utils/date'; -import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation'; +import { emptyStringToNull, isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation'; +import z from 'zod'; -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, - description: 'Avatar color', +export const UserUpdateMeSchema = z + .object({ + email: toEmail.optional().describe('User email'), + password: z + .string() + .optional() + .describe('User password (deprecated, use change password endpoint)') + .meta({ deprecated: true }), + name: z.string().optional().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), }) - avatarColor?: UserAvatarColor | null; -} + .meta({ id: 'UserUpdateMeDto' }); -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', description: 'Avatar color' }) - avatarColor!: UserAvatarColor; - @ApiProperty({ description: 'Profile change date', format: 'date-time' }) - profileChangedAt!: string; -} +export class UserUpdateMeDto extends createZodDto(UserUpdateMeSchema) {} -export class UserLicense { - @ApiProperty({ description: 'License key' }) - licenseKey!: string; - @ApiProperty({ description: 'Activation key' }) - activationKey!: string; - @ApiProperty({ description: 'Activation date' }) - activatedAt!: Date; -} +export const UserResponseSchema = z + .object({ + id: z.uuidv4().describe('User ID'), + name: z.string().describe('User name'), + email: toEmail.describe('User email'), + profileImagePath: z.string().describe('Profile image path'), + avatarColor: UserAvatarColorSchema, + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + profileChangedAt: z.string().meta({ format: 'date-time' }).describe('Profile change date'), + }) + .meta({ id: 'UserResponseDto' }); + +export class UserResponseDto extends createZodDto(UserResponseSchema) {} + +const licenseKeyRegex = /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/; + +export const UserLicenseSchema = z + .object({ + licenseKey: z.string().regex(licenseKeyRegex).describe(`License key (format: ${licenseKeyRegex.toString()})`), + activationKey: z.string().describe('Activation key'), + activatedAt: isoDatetimeToDate.describe('Activation date'), + }) + .meta({ id: 'UserLicense' }); const emailToAvatarColor = (email: string): UserAvatarColor => { const values = Object.values(UserAvatarColor); @@ -80,144 +65,77 @@ export const mapUser = (entity: MaybeDehydrated): UserResponse }; }; -export class UserAdminSearchDto { - @ValidateBoolean({ optional: true, description: 'Include deleted users' }) - withDeleted?: boolean; - - @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, - description: 'Avatar color', +const UserAdminSearchSchema = z + .object({ + withDeleted: stringToBool.optional().describe('Include deleted users'), + id: z.uuidv4().optional().describe('User ID filter'), }) - avatarColor?: UserAvatarColor | null; + .meta({ id: 'UserAdminSearchDto' }); - @ApiPropertyOptional({ description: 'PIN code' }) - @PinCode({ optional: true, nullable: true, emptyToNull: true }) - pinCode?: string | null; +export class UserAdminSearchDto extends createZodDto(UserAdminSearchSchema) {} - @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) - quotaSizeInBytes?: number | null; - - @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) - shouldChangePassword?: boolean; - - @ValidateBoolean({ optional: true, description: 'Send notification email' }) - notify?: boolean; - - @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; - - @ApiPropertyOptional({ description: 'PIN code' }) - @PinCode({ optional: true, nullable: true, emptyToNull: true }) - pinCode?: string | null; - - @ApiPropertyOptional({ description: 'User name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ValidateEnum({ - enum: UserAvatarColor, - name: 'UserAvatarColor', - optional: true, - nullable: true, - description: 'Avatar color', +export const UserAdminCreateSchema = z + .object({ + email: toEmail.describe('User email'), + password: z.string().describe('User password'), + name: z.string().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), + pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable()) + .optional() + .describe('PIN code') + .meta({ example: '123456' }), + storageLabel: z.string().pipe(sanitizeFilename).nullish().describe('Storage label'), + quotaSizeInBytes: z.int().min(0).nullish().describe('Storage quota in bytes'), + shouldChangePassword: z.boolean().optional().describe('Require password change on next login'), + notify: z.boolean().optional().describe('Send notification email'), + isAdmin: z.boolean().optional().describe('Grant admin privileges'), }) - avatarColor?: UserAvatarColor | null; + .meta({ id: 'UserAdminCreateDto' }); - @ApiPropertyOptional({ description: 'Storage label' }) - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; +export class UserAdminCreateDto extends createZodDto(UserAdminCreateSchema) {} - @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) - shouldChangePassword?: boolean; +const UserAdminUpdateSchema = z + .object({ + email: toEmail.optional().describe('User email'), + password: z.string().optional().describe('User password'), + pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable()) + .optional() + .describe('PIN code') + .meta({ example: '123456' }), + name: z.string().optional().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), + storageLabel: z.string().pipe(sanitizeFilename).nullish().describe('Storage label'), + shouldChangePassword: z.boolean().optional().describe('Require password change on next login'), + quotaSizeInBytes: z.int().min(0).nullish().describe('Storage quota in bytes'), + isAdmin: z.boolean().optional().describe('Grant admin privileges'), + }) + .meta({ id: 'UserAdminUpdateDto' }); - @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - @Optional({ nullable: true }) - @IsInt() - @Min(0) - quotaSizeInBytes?: number | null; +export class UserAdminUpdateDto extends createZodDto(UserAdminUpdateSchema) {} - @ValidateBoolean({ optional: true, description: 'Grant admin privileges' }) - isAdmin?: boolean; -} +const UserAdminDeleteSchema = z + .object({ + force: z.boolean().optional().describe('Force delete even if user has assets'), + }) + .meta({ id: 'UserAdminDeleteDto' }); -export class UserAdminDeleteDto { - @ValidateBoolean({ optional: true, description: 'Force delete even if user has assets' }) - force?: boolean; -} +export class UserAdminDeleteDto extends createZodDto(UserAdminDeleteSchema) {} -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', description: 'Storage quota in bytes' }) - quotaSizeInBytes!: number | null; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' }) - quotaUsageInBytes!: number | null; - @ValidateEnum({ enum: UserStatus, name: 'UserStatus', description: 'User status' }) - status!: string; - @ApiProperty({ description: 'User license' }) - license!: UserLicense | null; -} +const UserAdminResponseSchema = UserResponseSchema.extend({ + storageLabel: z.string().nullable().describe('Storage label'), + shouldChangePassword: z.boolean().describe('Require password change on next login'), + isAdmin: z.boolean().describe('Is admin user'), + createdAt: isoDatetimeToDate.describe('Creation date'), + deletedAt: isoDatetimeToDate.nullable().describe('Deletion date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + oauthId: z.string().describe('OAuth ID'), + quotaSizeInBytes: z.int().min(0).nullable().describe('Storage quota in bytes'), + quotaUsageInBytes: z.int().min(0).nullable().describe('Storage usage in bytes'), + status: UserStatusSchema, + license: UserLicenseSchema.nullable(), +}).meta({ id: 'UserAdminResponseDto' }); + +export class UserAdminResponseDto extends createZodDto(UserAdminResponseSchema) {} export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { const metadata = entity.metadata || []; @@ -237,6 +155,6 @@ export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { quotaSizeInBytes: entity.quotaSizeInBytes, quotaUsageInBytes: entity.quotaUsageInBytes, status: entity.status, - license: license ? { ...license, activatedAt: new Date(license?.activatedAt) } : null, + license: license ? { ...license, activatedAt: new Date(license.activatedAt) } : null, }; } diff --git a/server/src/dtos/workflow.dto.ts b/server/src/dtos/workflow.dto.ts index c4e5ac9c4c..0307c7f483 100644 --- a/server/src/dtos/workflow.dto.ts +++ b/server/src/dtos/workflow.dto.ts @@ -1,143 +1,84 @@ -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'; -import { PluginTriggerType } from 'src/enum'; -import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types'; -import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import type { WorkflowAction, WorkflowFilter } from 'src/database'; +import { PluginTriggerTypeSchema } from 'src/enum'; +import { ActionConfigSchema, FilterConfigSchema } from 'src/types/plugin-schema.types'; +import z from 'zod'; -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', 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, 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, - description: 'Workflow trigger type', +const WorkflowFilterItemSchema = z + .object({ + pluginFilterId: z.uuidv4().describe('Plugin filter ID'), + filterConfig: FilterConfigSchema.optional(), }) - triggerType?: PluginTriggerType; + .meta({ id: 'WorkflowFilterItemDto' }); - @ApiPropertyOptional({ description: 'Workflow name' }) - @IsString() - @IsNotEmpty() - @Optional() - name?: string; +const WorkflowActionItemSchema = z + .object({ + pluginActionId: z.uuidv4().describe('Plugin action ID'), + actionConfig: ActionConfigSchema.optional(), + }) + .meta({ id: 'WorkflowActionItemDto' }); - @ApiPropertyOptional({ description: 'Workflow description' }) - @IsString() - @Optional() - description?: string; +const WorkflowCreateSchema = z + .object({ + triggerType: PluginTriggerTypeSchema, + name: z.string().describe('Workflow name'), + description: z.string().optional().describe('Workflow description'), + enabled: z.boolean().optional().describe('Workflow enabled'), + filters: z.array(WorkflowFilterItemSchema).describe('Workflow filters'), + actions: z.array(WorkflowActionItemSchema).describe('Workflow actions'), + }) + .meta({ id: 'WorkflowCreateDto' }); - @ValidateBoolean({ optional: true, description: 'Workflow enabled' }) - enabled?: boolean; +const WorkflowUpdateSchema = z + .object({ + triggerType: PluginTriggerTypeSchema.optional(), + name: z.string().optional().describe('Workflow name'), + description: z.string().optional().describe('Workflow description'), + enabled: z.boolean().optional().describe('Workflow enabled'), + filters: z.array(WorkflowFilterItemSchema).optional().describe('Workflow filters'), + actions: z.array(WorkflowActionItemSchema).optional().describe('Workflow actions'), + }) + .meta({ id: 'WorkflowUpdateDto' }); - @ApiPropertyOptional({ description: 'Workflow filters' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowFilterItemDto) - @Optional() - filters?: WorkflowFilterItemDto[]; +const WorkflowFilterResponseSchema = z + .object({ + id: z.string().describe('Filter ID'), + workflowId: z.string().describe('Workflow ID'), + pluginFilterId: z.string().describe('Plugin filter ID'), + filterConfig: FilterConfigSchema.nullable(), + order: z.number().describe('Filter order'), + }) + .meta({ id: 'WorkflowFilterResponseDto' }); - @ApiPropertyOptional({ description: 'Workflow actions' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowActionItemDto) - @Optional() - actions?: WorkflowActionItemDto[]; -} +const WorkflowActionResponseSchema = z + .object({ + id: z.string().describe('Action ID'), + workflowId: z.string().describe('Workflow ID'), + pluginActionId: z.string().describe('Plugin action ID'), + actionConfig: ActionConfigSchema.nullable(), + order: z.number().describe('Action order'), + }) + .meta({ id: 'WorkflowActionResponseDto' }); -export class WorkflowResponseDto { - @ApiProperty({ description: 'Workflow ID' }) - id!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @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[]; -} +const WorkflowResponseSchema = z + .object({ + id: z.string().describe('Workflow ID'), + ownerId: z.string().describe('Owner user ID'), + triggerType: PluginTriggerTypeSchema, + name: z.string().nullable().describe('Workflow name'), + description: z.string().describe('Workflow description'), + createdAt: z.string().describe('Creation date'), + enabled: z.boolean().describe('Workflow enabled'), + filters: z.array(WorkflowFilterResponseSchema).describe('Workflow filters'), + actions: z.array(WorkflowActionResponseSchema).describe('Workflow actions'), + }) + .meta({ id: 'WorkflowResponseDto' }); -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; -} +export class WorkflowCreateDto extends createZodDto(WorkflowCreateSchema) {} +export class WorkflowUpdateDto extends createZodDto(WorkflowUpdateSchema) {} +export class WorkflowResponseDto extends createZodDto(WorkflowResponseSchema) {} +class WorkflowFilterResponseDto extends createZodDto(WorkflowFilterResponseSchema) {} +class WorkflowActionResponseDto extends createZodDto(WorkflowActionResponseSchema) {} export function mapWorkflowFilter(filter: WorkflowFilter): WorkflowFilterResponseDto { return { diff --git a/server/src/enum.ts b/server/src/enum.ts index de85d24db3..cb4835020f 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -1,8 +1,12 @@ +import z from 'zod'; + export enum AuthType { Password = 'password', OAuth = 'oauth', } +export const AuthTypeSchema = z.enum(AuthType).describe('Auth type').meta({ id: 'AuthType' }); + export enum ImmichCookie { AccessToken = 'immich_access_token', MaintenanceToken = 'immich_maintenance_token', @@ -13,6 +17,8 @@ export enum ImmichCookie { OAuthCodeVerifier = 'immich_oauth_code_verifier', } +export const ImmichCookieSchema = z.enum(ImmichCookie).describe('Immich cookie').meta({ id: 'ImmichCookie' }); + export enum ImmichHeader { ApiKey = 'x-api-key', UserToken = 'x-immich-user-token', @@ -23,6 +29,8 @@ export enum ImmichHeader { Cid = 'x-immich-cid', } +export const ImmichHeaderSchema = z.enum(ImmichHeader).describe('Immich header').meta({ id: 'ImmichHeader' }); + export enum ImmichQuery { SharedLinkKey = 'key', SharedLinkSlug = 'slug', @@ -30,6 +38,8 @@ export enum ImmichQuery { SessionKey = 'sessionKey', } +export const ImmichQuerySchema = z.enum(ImmichQuery).describe('Immich query').meta({ id: 'ImmichQuery' }); + export enum AssetType { Image = 'IMAGE', Video = 'VIDEO', @@ -37,11 +47,20 @@ export enum AssetType { Other = 'OTHER', } +export const AssetTypeSchema = z.enum(AssetType).describe('Asset type').meta({ id: 'AssetTypeEnum' }); + export enum ChecksumAlgorithm { - sha1File = 'sha1', // sha1 checksum of the whole file contents - sha1Path = 'sha1-path', // sha1 checksum of "path:" plus the file path, currently used in external libraries, deprecated + /** sha1 checksum of the whole file contents */ + sha1File = 'sha1', + /** sha1 checksum of "path:" plus the file path, currently used in external libraries, deprecated */ + sha1Path = 'sha1-path', } +export const ChecksumAlgorithmSchema = z + .enum(ChecksumAlgorithm) + .describe('Checksum algorithm') + .meta({ id: 'ChecksumAlgorithmEnum' }); + export enum AssetFileType { /** * An full/large-size image extracted/converted from RAW photos @@ -53,32 +72,44 @@ export enum AssetFileType { EncodedVideo = 'encoded_video', } +export const AssetFileTypeSchema = z.enum(AssetFileType).describe('Asset file type').meta({ id: 'AssetFileType' }); + export enum AlbumUserRole { Editor = 'editor', Viewer = 'viewer', } +export const AlbumUserRoleSchema = z.enum(AlbumUserRole).describe('Album user role').meta({ id: 'AlbumUserRole' }); + export enum AssetOrder { Asc = 'asc', Desc = 'desc', } +export const AssetOrderSchema = z.enum(AssetOrder).describe('Asset sort order').meta({ id: 'AssetOrder' }); + export enum DatabaseAction { Create = 'CREATE', Update = 'UPDATE', Delete = 'DELETE', } +export const DatabaseActionSchema = z.enum(DatabaseAction).describe('Database action').meta({ id: 'DatabaseAction' }); + export enum EntityType { Asset = 'ASSET', Album = 'ALBUM', } +export const EntityTypeSchema = z.enum(EntityType).describe('Entity type').meta({ id: 'EntityType' }); + export enum MemoryType { /** pictures taken on this day X years ago */ OnThisDay = 'on_this_day', } +export const MemoryTypeSchema = z.enum(MemoryType).describe('Memory type').meta({ id: 'MemoryType' }); + export enum AssetOrderWithRandom { // Include existing values Asc = AssetOrder.Asc, @@ -87,6 +118,11 @@ export enum AssetOrderWithRandom { Random = 'random', } +export const AssetOrderWithRandomSchema = z + .enum(AssetOrderWithRandom) + .describe('Sort order') + .meta({ id: 'MemorySearchOrder' }); + export enum Permission { All = 'all', @@ -293,6 +329,8 @@ export enum Permission { AdminAuthUnlinkAll = 'adminAuth.unlinkAll', } +export const PermissionSchema = z.enum(Permission).describe('Permission').meta({ id: 'Permission' }); + export enum SharedLinkType { Album = 'ALBUM', @@ -303,6 +341,8 @@ export enum SharedLinkType { Individual = 'INDIVIDUAL', } +export const SharedLinkTypeSchema = z.enum(SharedLinkType).describe('Shared link type').meta({ id: 'SharedLinkType' }); + export enum StorageFolder { EncodedVideo = 'encoded-video', Library = 'library', @@ -312,6 +352,8 @@ export enum StorageFolder { Backups = 'backups', } +export const StorageFolderSchema = z.enum(StorageFolder).describe('Storage folder').meta({ id: 'StorageFolder' }); + export enum SystemMetadataKey { MediaLocation = 'MediaLocation', ReverseGeocodingState = 'reverse-geocoding-state', @@ -325,16 +367,31 @@ export enum SystemMetadataKey { License = 'license', } +export const SystemMetadataKeySchema = z + .enum(SystemMetadataKey) + .describe('System metadata key') + .meta({ id: 'SystemMetadataKey' }); + export enum UserMetadataKey { Preferences = 'preferences', License = 'license', Onboarding = 'onboarding', } +export const UserMetadataKeySchema = z + .enum(UserMetadataKey) + .describe('User metadata key') + .meta({ id: 'UserMetadataKey' }); + export enum AssetMetadataKey { MobileApp = 'mobile-app', } +export const AssetMetadataKeySchema = z + .enum(AssetMetadataKey) + .describe('Asset metadata key') + .meta({ id: 'AssetMetadataKey' }); + export enum UserAvatarColor { Primary = 'primary', Pink = 'pink', @@ -348,24 +405,35 @@ export enum UserAvatarColor { Amber = 'amber', } +export const UserAvatarColorSchema = z + .enum(UserAvatarColor) + .describe('User avatar color') + .meta({ id: 'UserAvatarColor' }); + export enum UserStatus { Active = 'active', Removing = 'removing', Deleted = 'deleted', } +export const UserStatusSchema = z.enum(UserStatus).describe('User status').meta({ id: 'UserStatus' }); + export enum AssetStatus { Active = 'active', Trashed = 'trashed', Deleted = 'deleted', } +export const AssetStatusSchema = z.enum(AssetStatus).describe('Asset status').meta({ id: 'AssetStatus' }); + export enum SourceType { MachineLearning = 'machine-learning', Exif = 'exif', Manual = 'manual', } +export const SourceTypeSchema = z.enum(SourceType).describe('Face detection source type').meta({ id: 'SourceType' }); + export enum ManualJobName { PersonCleanup = 'person-cleanup', TagCleanup = 'tag-cleanup', @@ -375,19 +443,27 @@ export enum ManualJobName { BackupDatabase = 'backup-database', } +export const ManualJobNameSchema = z.enum(ManualJobName).describe('Manual job name').meta({ id: 'ManualJobName' }); + export enum AssetPathType { Original = 'original', EncodedVideo = 'encoded_video', } +export const AssetPathTypeSchema = z.enum(AssetPathType).describe('Asset path type').meta({ id: 'AssetPathType' }); + export enum PersonPathType { Face = 'face', } +export const PersonPathTypeSchema = z.enum(PersonPathType).describe('Person path type').meta({ id: 'PersonPathType' }); + export enum UserPathType { Profile = 'profile', } +export const UserPathTypeSchema = z.enum(UserPathType).describe('User path type').meta({ id: 'UserPathType' }); + export type PathType = AssetFileType | AssetPathType | PersonPathType | UserPathType; export enum TranscodePolicy { @@ -398,6 +474,11 @@ export enum TranscodePolicy { Disabled = 'disabled', } +export const TranscodePolicySchema = z + .enum(TranscodePolicy) + .describe('Transcode policy') + .meta({ id: 'TranscodePolicy' }); + export enum TranscodeTarget { None = 'NONE', Audio = 'AUDIO', @@ -405,6 +486,11 @@ export enum TranscodeTarget { All = 'ALL', } +export const TranscodeTargetSchema = z + .enum(TranscodeTarget) + .describe('Transcode target') + .meta({ id: 'TranscodeTarget' }); + export enum VideoCodec { H264 = 'h264', Hevc = 'hevc', @@ -412,6 +498,8 @@ export enum VideoCodec { Av1 = 'av1', } +export const VideoCodecSchema = z.enum(VideoCodec).describe('Target video codec').meta({ id: 'VideoCodec' }); + export enum AudioCodec { Mp3 = 'mp3', Aac = 'aac', @@ -421,6 +509,8 @@ export enum AudioCodec { PcmS16le = 'pcm_s16le', } +export const AudioCodecSchema = z.enum(AudioCodec).describe('Target audio codec').meta({ id: 'AudioCodec' }); + export enum VideoContainer { Mov = 'mov', Mp4 = 'mp4', @@ -428,6 +518,11 @@ export enum VideoContainer { Webm = 'webm', } +export const VideoContainerSchema = z + .enum(VideoContainer) + .describe('Accepted video containers') + .meta({ id: 'VideoContainer' }); + export enum TranscodeHardwareAcceleration { Nvenc = 'nvenc', Qsv = 'qsv', @@ -436,6 +531,11 @@ export enum TranscodeHardwareAcceleration { Disabled = 'disabled', } +export const TranscodeHardwareAccelerationSchema = z + .enum(TranscodeHardwareAcceleration) + .describe('Transcode hardware acceleration') + .meta({ id: 'TranscodeHWAccel' }); + export enum ToneMapping { Hable = 'hable', Mobius = 'mobius', @@ -443,27 +543,40 @@ export enum ToneMapping { Disabled = 'disabled', } +export const ToneMappingSchema = z.enum(ToneMapping).describe('Tone mapping').meta({ id: 'ToneMapping' }); + export enum CQMode { Auto = 'auto', Cqp = 'cqp', Icq = 'icq', } +export const CQModeSchema = z.enum(CQMode).describe('CQ mode').meta({ id: 'CQMode' }); + export enum Colorspace { Srgb = 'srgb', P3 = 'p3', } +export const ColorspaceSchema = z.enum(Colorspace).describe('Colorspace').meta({ id: 'Colorspace' }); + export enum ImageFormat { Jpeg = 'jpeg', Webp = 'webp', } +export const ImageFormatSchema = z.enum(ImageFormat).describe('Image format').meta({ id: 'ImageFormat' }); + export enum RawExtractedFormat { Jpeg = 'jpeg', Jxl = 'jxl', } +export const RawExtractedFormatSchema = z + .enum(RawExtractedFormat) + .describe('Raw extracted format') + .meta({ id: 'RawExtractedFormat' }); + export enum LogLevel { Verbose = 'verbose', Debug = 'debug', @@ -473,11 +586,15 @@ export enum LogLevel { Fatal = 'fatal', } +export const LogLevelSchema = z.enum(LogLevel).describe('Log level').meta({ id: 'LogLevel' }); + export enum LogFormat { Console = 'console', Json = 'json', } +export const LogFormatSchema = z.enum(LogFormat).describe('Log format').meta({ id: 'LogFormat' }); + export enum ApiCustomExtension { Permission = 'x-immich-permission', AdminOnly = 'x-immich-admin-only', @@ -485,6 +602,11 @@ export enum ApiCustomExtension { State = 'x-immich-state', } +export const ApiCustomExtensionSchema = z + .enum(ApiCustomExtension) + .describe('API custom extension') + .meta({ id: 'ApiCustomExtension' }); + export enum MetadataKey { AuthRoute = 'auth_route', AdminRoute = 'admin_route', @@ -495,29 +617,42 @@ export enum MetadataKey { TelemetryEnabled = 'telemetry_enabled', } +export const MetadataKeySchema = z.enum(MetadataKey).describe('Metadata key').meta({ id: 'MetadataKey' }); + export enum RouteKey { Asset = 'assets', User = 'users', } +export const RouteKeySchema = z.enum(RouteKey).describe('Route key').meta({ id: 'RouteKey' }); + export enum CacheControl { PrivateWithCache = 'private_with_cache', PrivateWithoutCache = 'private_without_cache', None = 'none', } +export const CacheControlSchema = z.enum(CacheControl).describe('Cache control').meta({ id: 'CacheControl' }); + export enum ImmichEnvironment { Development = 'development', Testing = 'testing', Production = 'production', } +export const ImmichEnvironmentSchema = z + .enum(ImmichEnvironment) + .describe('Immich environment') + .meta({ id: 'ImmichEnvironment' }); + export enum ImmichWorker { Api = 'api', Maintenance = 'maintenance', Microservices = 'microservices', } +export const ImmichWorkerSchema = z.enum(ImmichWorker).describe('Immich worker').meta({ id: 'ImmichWorker' }); + export enum ImmichTelemetry { Host = 'host', Api = 'api', @@ -526,6 +661,11 @@ export enum ImmichTelemetry { Job = 'job', } +export const ImmichTelemetrySchema = z + .enum(ImmichTelemetry) + .describe('Immich telemetry') + .meta({ id: 'ImmichTelemetry' }); + export enum ExifOrientation { Horizontal = 1, MirrorHorizontal = 2, @@ -537,6 +677,11 @@ export enum ExifOrientation { Rotate270CW = 8, } +export const ExifOrientationSchema = z + .enum(ExifOrientation) + .describe('EXIF orientation') + .meta({ id: 'ExifOrientation' }); + export enum DatabaseExtension { Cube = 'cube', EarthDistance = 'earthdistance', @@ -545,6 +690,11 @@ export enum DatabaseExtension { VectorChord = 'vchord', } +export const DatabaseExtensionSchema = z + .enum(DatabaseExtension) + .describe('Database extension') + .meta({ id: 'DatabaseExtension' }); + export enum BootstrapEventPriority { // Database service should be initialized before anything else, most other services need database access DatabaseService = -200, @@ -556,6 +706,11 @@ export enum BootstrapEventPriority { SystemConfig = 100, } +export const BootstrapEventPrioritySchema = z + .enum(BootstrapEventPriority) + .describe('Bootstrap event priority') + .meta({ id: 'BootstrapEventPriority' }); + export enum QueueName { ThumbnailGeneration = 'thumbnailGeneration', MetadataExtraction = 'metadataExtraction', @@ -577,6 +732,8 @@ export enum QueueName { Editor = 'editor', } +export const QueueNameSchema = z.enum(QueueName).describe('Queue name').meta({ id: 'QueueName' }); + export enum QueueJobStatus { Active = 'active', Failed = 'failed', @@ -586,6 +743,8 @@ export enum QueueJobStatus { Paused = 'paused', } +export const QueueJobStatusSchema = z.enum(QueueJobStatus).describe('Queue job status').meta({ id: 'QueueJobStatus' }); + export enum JobName { AssetDelete = 'AssetDelete', AssetDeleteCheck = 'AssetDeleteCheck', @@ -666,6 +825,8 @@ export enum JobName { WorkflowRun = 'WorkflowRun', } +export const JobNameSchema = z.enum(JobName).describe('Job name').meta({ id: 'JobName' }); + export enum QueueCommand { Start = 'start', /** @deprecated Use `updateQueue` instead */ @@ -678,21 +839,32 @@ export enum QueueCommand { ClearFailed = 'clear-failed', } +export const QueueCommandSchema = z + .enum(QueueCommand) + .describe('Queue command to execute') + .meta({ id: 'QueueCommand' }); + export enum JobStatus { Success = 'success', Failed = 'failed', Skipped = 'skipped', } +export const JobStatusSchema = z.enum(JobStatus).describe('Job status').meta({ id: 'JobStatus' }); + export enum QueueCleanType { Failed = 'failed', } +export const QueueCleanTypeSchema = z.enum(QueueCleanType).describe('Queue clean type').meta({ id: 'QueueCleanType' }); + export enum VectorIndex { Clip = 'clip_index', Face = 'face_index', } +export const VectorIndexSchema = z.enum(VectorIndex).describe('Vector index').meta({ id: 'VectorIndex' }); + export enum DatabaseLock { GeodataImport = 100, Migrations = 200, @@ -710,6 +882,8 @@ export enum DatabaseLock { VersionCheck = 800, } +export const DatabaseLockSchema = z.enum(DatabaseLock).describe('Database lock').meta({ id: 'DatabaseLock' }); + export enum MaintenanceAction { Start = 'start', End = 'end', @@ -717,10 +891,17 @@ export enum MaintenanceAction { RestoreDatabase = 'restore_database', } +export const MaintenanceActionSchema = z + .enum(MaintenanceAction) + .describe('Maintenance action') + .meta({ id: 'MaintenanceAction' }); + export enum ExitCode { AppRestart = 7, } +export const ExitCodeSchema = z.enum(ExitCode).describe('Exit code').meta({ id: 'ExitCode' }); + export enum SyncRequestType { AlbumsV1 = 'AlbumsV1', AlbumUsersV1 = 'AlbumUsersV1', @@ -746,6 +927,11 @@ export enum SyncRequestType { UserMetadataV1 = 'UserMetadataV1', } +export const SyncRequestTypeSchema = z + .enum(SyncRequestType) + .describe('Sync request type') + .meta({ id: 'SyncRequestType' }); + export enum SyncEntityType { AuthUserV1 = 'AuthUserV1', @@ -814,6 +1000,8 @@ export enum SyncEntityType { SyncCompleteV1 = 'SyncCompleteV1', } +export const SyncEntityTypeSchema = z.enum(SyncEntityType).describe('Sync entity type').meta({ id: 'SyncEntityType' }); + export enum NotificationLevel { Success = 'success', Error = 'error', @@ -821,6 +1009,11 @@ export enum NotificationLevel { Info = 'info', } +export const NotificationLevelSchema = z + .enum(NotificationLevel) + .describe('Notification level') + .meta({ id: 'NotificationLevel' }); + export enum NotificationType { JobFailed = 'JobFailed', BackupFailed = 'BackupFailed', @@ -830,11 +1023,21 @@ export enum NotificationType { Custom = 'Custom', } +export const NotificationTypeSchema = z + .enum(NotificationType) + .describe('Notification type') + .meta({ id: 'NotificationType' }); + export enum OAuthTokenEndpointAuthMethod { ClientSecretPost = 'client_secret_post', ClientSecretBasic = 'client_secret_basic', } +export const OAuthTokenEndpointAuthMethodSchema = z + .enum(OAuthTokenEndpointAuthMethod) + .describe('OAuth token endpoint auth method') + .meta({ id: 'OAuthTokenEndpointAuthMethod' }); + export enum AssetVisibility { Archive = 'archive', Timeline = 'timeline', @@ -846,12 +1049,19 @@ export enum AssetVisibility { Locked = 'locked', } +export const AssetVisibilitySchema = z + .enum(AssetVisibility) + .describe('Asset visibility') + .meta({ id: 'AssetVisibility' }); + export enum CronJob { LibraryScan = 'LibraryScan', NightlyJobs = 'NightlyJobs', VersionCheck = 'VersionCheck', } +export const CronJobSchema = z.enum(CronJob).describe('Cron job').meta({ id: 'CronJob' }); + export enum ApiTag { Activities = 'Activities', Albums = 'Albums', @@ -892,13 +1102,22 @@ export enum ApiTag { Workflows = 'Workflows', } +export const ApiTagSchema = z.enum(ApiTag).describe('API tag').meta({ id: 'ApiTag' }); + export enum PluginContext { Asset = 'asset', Album = 'album', Person = 'person', } +export const PluginContextSchema = z.enum(PluginContext).describe('Plugin context').meta({ id: 'PluginContextType' }); + export enum PluginTriggerType { AssetCreate = 'AssetCreate', PersonRecognized = 'PersonRecognized', } + +export const PluginTriggerTypeSchema = z + .enum(PluginTriggerType) + .describe('Plugin trigger type') + .meta({ id: 'PluginTriggerType' }); diff --git a/server/src/middleware/global-exception.filter.ts b/server/src/middleware/global-exception.filter.ts index a8afa91cbc..f91bb2b122 100644 --- a/server/src/middleware/global-exception.filter.ts +++ b/server/src/middleware/global-exception.filter.ts @@ -1,8 +1,10 @@ import { ArgumentsHost, Catch, ExceptionFilter, HttpException } from '@nestjs/common'; import { Response } from 'express'; import { ClsService } from 'nestjs-cls'; +import { ZodSerializationException, ZodValidationException } from 'nestjs-zod'; import { LoggingRepository } from 'src/repositories/logging.repository'; import { logGlobalError } from 'src/utils/logger'; +import { ZodError } from 'zod'; @Catch() export class GlobalExceptionFilter implements ExceptionFilter { @@ -41,6 +43,19 @@ export class GlobalExceptionFilter implements ExceptionFilter { body = { message: body }; } + // handle both request and response validation errors + if (error instanceof ZodValidationException || error instanceof ZodSerializationException) { + const zodError = error.getZodError(); + if (zodError instanceof ZodError && zodError.issues.length > 0) { + body = { + message: zodError.issues.map((issue) => + issue.path.length > 0 ? `[${issue.path.join('.')}] ${issue.message}` : issue.message, + ), + error: 'Bad Request', + }; + } + } + return { status, body }; } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 5876b934e5..2ff4d224cf 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -318,7 +318,7 @@ export class AssetRepository { .execute(); } - upsertMetadata(id: string, items: Array<{ key: string; value: object }>) { + upsertMetadata(id: string, items: Array<{ key: string; value: Record }>) { if (items.length === 0) { return []; } diff --git a/server/src/repositories/config.repository.spec.ts b/server/src/repositories/config.repository.spec.ts index a3dc8ba5cb..3c579a1a94 100644 --- a/server/src/repositories/config.repository.spec.ts +++ b/server/src/repositories/config.repository.spec.ts @@ -85,7 +85,7 @@ describe('getEnv', () => { describe('IMMICH_MEDIA_LOCATION', () => { it('should throw an error for relative paths', () => { process.env.IMMICH_MEDIA_LOCATION = './relative/path'; - expect(() => getEnv()).toThrowError('IMMICH_MEDIA_LOCATION must be an absolute path'); + expect(() => getEnv()).toThrowError('[IMMICH_MEDIA_LOCATION] Must be an absolute path'); }); }); @@ -98,7 +98,7 @@ describe('getEnv', () => { it('should throw an error for invalid value', () => { process.env.IMMICH_ALLOW_EXTERNAL_PLUGINS = 'invalid'; - expect(() => getEnv()).toThrowError('IMMICH_ALLOW_EXTERNAL_PLUGINS must be a boolean value'); + expect(() => getEnv()).toThrowError('[IMMICH_ALLOW_EXTERNAL_PLUGINS] Invalid option: expected one of'); }); }); @@ -111,7 +111,7 @@ describe('getEnv', () => { it('should throw an error for invalid value', () => { process.env.IMMICH_ALLOW_SETUP = 'invalid'; - expect(() => getEnv()).toThrowError('IMMICH_ALLOW_SETUP must be a boolean value'); + expect(() => getEnv()).toThrowError('[IMMICH_ALLOW_SETUP] Invalid option: expected one of'); }); }); @@ -134,7 +134,7 @@ describe('getEnv', () => { it('should validate DB_SSL_MODE', () => { process.env.DB_SSL_MODE = 'invalid'; - expect(() => getEnv()).toThrowError('DB_SSL_MODE must be one of the following values:'); + expect(() => getEnv()).toThrow(/\[DB_SSL_MODE\] Invalid option: expected one of/); }); it('should accept a valid DB_SSL_MODE', () => { @@ -278,7 +278,7 @@ describe('getEnv', () => { it('should reject invalid trusted proxies', () => { process.env.IMMICH_TRUSTED_PROXIES = '10.1'; - expect(() => getEnv()).toThrow('IMMICH_TRUSTED_PROXIES must be an ip address, or ip address range'); + expect(() => getEnv()).toThrow('[IMMICH_TRUSTED_PROXIES] Must be an ip address or ip address range'); }); }); diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts index fa4823362e..97ec3f1cdc 100644 --- a/server/src/repositories/config.repository.ts +++ b/server/src/repositories/config.repository.ts @@ -2,8 +2,6 @@ import { DatabaseConnectionParams } from '@immich/sql-tools'; import { RegisterQueueOptions } from '@nestjs/bullmq'; import { Inject, Injectable, Optional } from '@nestjs/common'; import { QueueOptions } from 'bullmq'; -import { plainToInstance } from 'class-transformer'; -import { validateSync } from 'class-validator'; import { Request, Response } from 'express'; import { HelmetOptions } from 'helmet'; import { RedisOptions } from 'ioredis'; @@ -13,7 +11,7 @@ import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { citiesFile, excludePaths, IWorker } from 'src/constants'; import { Telemetry } from 'src/decorators'; -import { EnvDto } from 'src/dtos/env.dto'; +import { EnvSchema } from 'src/dtos/env.dto'; import { DatabaseExtension, ImmichEnvironment, @@ -173,15 +171,16 @@ const resolveHelmetFile = (helmetFile: 'true' | 'false' | string | undefined) => }; const getEnv = (): EnvData => { - const dto = plainToInstance(EnvDto, process.env); - const errors = validateSync(dto); - if (errors.length > 0) { - const messages = [`Invalid environment variables: `]; - for (const error of errors) { - messages.push(` - ${error.property}=${error.value} (${Object.values(error.constraints || {}).join(', ')})`); + const parseResult = EnvSchema.safeParse(process.env); + if (!parseResult.success) { + const messages = ['Invalid environment variables: ']; + for (const issue of parseResult.error.issues) { + const path = issue.path.join('.'); + messages.push(` - [${path}] ${issue.message}`); } throw new Error(messages.join('\n')); } + const dto = parseResult.data; const includedWorkers = asSet(dto.IMMICH_WORKERS_INCLUDE, [ImmichWorker.Api, ImmichWorker.Microservices]); const excludedWorkers = asSet(dto.IMMICH_WORKERS_EXCLUDE, []); diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index fbc281ccb3..c505dd3fb3 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; import { ModuleRef, Reflector } from '@nestjs/core'; -import { ClassConstructor } from 'class-transformer'; import _ from 'lodash'; import { Socket } from 'socket.io'; import { SystemConfig } from 'src/config'; @@ -152,7 +151,7 @@ export class EventRepository { this.logger.setContext(EventRepository.name); } - setup({ services }: { services: ClassConstructor[] }) { + setup({ services }: { services: (new (...args: any[]) => unknown)[] }) { const reflector = this.moduleRef.get(Reflector, { strict: false }); const items: Item[] = []; const worker = this.configRepository.getWorker(); diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index 142d5e3252..a94e5aa9f6 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -2,7 +2,6 @@ import { getQueueToken } from '@nestjs/bullmq'; import { Injectable } from '@nestjs/common'; import { ModuleRef, Reflector } from '@nestjs/core'; import { JobsOptions, Queue, Worker } from 'bullmq'; -import { ClassConstructor } from 'class-transformer'; import { setTimeout } from 'node:timers/promises'; import { JobConfig } from 'src/decorators'; import { QueueJobResponseDto, QueueJobSearchDto } from 'src/dtos/queue.dto'; @@ -34,7 +33,7 @@ export class JobRepository { this.logger.setContext(JobRepository.name); } - setup(services: ClassConstructor[]) { + setup(services: (new (...args: any[]) => unknown)[]) { const reflector = this.moduleRef.get(Reflector, { strict: false }); // discovery diff --git a/server/src/repositories/telemetry.repository.ts b/server/src/repositories/telemetry.repository.ts index 5fbbb76cf7..d87c0acf5a 100644 --- a/server/src/repositories/telemetry.repository.ts +++ b/server/src/repositories/telemetry.repository.ts @@ -11,7 +11,6 @@ import { resourceFromAttributes } from '@opentelemetry/resources'; import { AggregationType } from '@opentelemetry/sdk-metrics'; import { NodeSDK, contextBase } from '@opentelemetry/sdk-node'; import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from '@opentelemetry/semantic-conventions'; -import { ClassConstructor } from 'class-transformer'; import { snakeCase, startCase } from 'lodash'; import { MetricService } from 'nestjs-otel'; import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils'; @@ -118,7 +117,7 @@ export class TelemetryRepository { this.repo = new MetricGroupRepository(metricService).configure({ enabled: metrics.has(ImmichTelemetry.Repo) }); } - setup({ repositories }: { repositories: ClassConstructor[] }) { + setup({ repositories }: { repositories: (new (...args: any[]) => unknown)[] }) { const { telemetry } = this.configRepository.getEnv(); const { metrics } = telemetry; if (!metrics.has(ImmichTelemetry.Repo)) { @@ -136,7 +135,7 @@ export class TelemetryRepository { } } - private wrap(Repository: ClassConstructor) { + private wrap(Repository: new (...args: any[]) => unknown) { const className = Repository.name; const descriptors = Object.getOwnPropertyDescriptors(Repository.prototype); const unit = 'ms'; diff --git a/server/src/schema/tables/asset-metadata.table.ts b/server/src/schema/tables/asset-metadata.table.ts index 53e3121a41..dc5b984160 100644 --- a/server/src/schema/tables/asset-metadata.table.ts +++ b/server/src/schema/tables/asset-metadata.table.ts @@ -35,7 +35,7 @@ export class AssetMetadataTable { key!: AssetMetadataKey | string; @Column({ type: 'jsonb' }) - value!: object; + value!: Record; @UpdateIdColumn({ index: true }) updateId!: Generated; diff --git a/server/src/schema/tables/memory.table.ts b/server/src/schema/tables/memory.table.ts index 8b9867b4cc..c132d42fee 100644 --- a/server/src/schema/tables/memory.table.ts +++ b/server/src/schema/tables/memory.table.ts @@ -43,7 +43,7 @@ export class MemoryTable { type!: MemoryType; @Column({ type: 'jsonb' }) - data!: object; + data!: Record; /** unless set to true, will be automatically deleted in the future */ @Column({ type: 'boolean', default: false }) diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 2c4b31c83a..94b8acd25e 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -13,6 +13,7 @@ import { SessionFactory } from 'test/factories/session.factory'; import { UserFactory } from 'test/factories/user.factory'; import { sharedLinkStub } from 'test/fixtures/shared-link.stub'; import { systemConfigStub } from 'test/fixtures/system-config.stub'; +import { userStub } from 'test/fixtures/user.stub'; import { newUuid } from 'test/small.factory'; import { newTestService, ServiceMocks } from 'test/utils'; @@ -209,11 +210,13 @@ describe(AuthService.name, () => { it('should sign up the admin', async () => { mocks.user.getAdmin.mockResolvedValue(void 0); mocks.user.create.mockResolvedValue({ + ...userStub.admin, ...dto, id: 'admin', + name: 'immich admin', createdAt: new Date('2021-01-01'), metadata: [] as UserMetadataItem[], - } as unknown as UserAdmin); + } as UserAdmin); await expect(sut.adminSignUp(dto)).resolves.toMatchObject({ avatarColor: expect.any(String), diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 5932855a21..498c165888 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; -import { isString } from 'class-validator'; import { parse } from 'cookie'; import { DateTime } from 'luxon'; import { IncomingHttpHeaders } from 'node:http'; @@ -312,7 +311,7 @@ export class AuthService extends BaseService { const storageLabel = this.getClaim(profile, { key: storageLabelClaim, default: '', - isValid: isString, + isValid: (value: unknown): value is string => typeof value === 'string', }); const storageQuota = this.getClaim(profile, { key: storageQuotaClaim, @@ -322,7 +321,7 @@ export class AuthService extends BaseService { const role = this.getClaim<'admin' | 'user'>(profile, { key: roleClaim, default: 'user', - isValid: (value: unknown) => isString(value) && ['admin', 'user'].includes(value), + isValid: (value: unknown) => typeof value === 'string' && ['admin', 'user'].includes(value), }); user = await this.createUser({ diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index ce3c9ee662..81e8c99d49 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -283,6 +283,7 @@ export class LibraryService extends BaseService { private async validateImportPath(importPath: string): Promise { const validation = new ValidateLibraryImportPathResponseDto(); validation.importPath = importPath; + validation.isValid = false; if (StorageCore.isImmichPath(importPath)) { validation.message = 'Cannot use media upload folder for external libraries'; diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index c7bea2b440..1eaa4f9a2c 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,4 +1,3 @@ -import { plainToInstance } from 'class-transformer'; import { defaults, SystemConfig } from 'src/config'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AssetFileType, JobName, JobStatus, UserMetadataKey } from 'src/enum'; @@ -102,7 +101,7 @@ describe(NotificationService.name, () => { it('skips smtp validation with DTO when there are no changes', async () => { const oldConfig = { ...configs.smtpEnabled }; - const newConfig = plainToInstance(SystemConfigDto, configs.smtpEnabled); + const newConfig = configs.smtpEnabled as SystemConfigDto; await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(mocks.email.verifySmtp).not.toHaveBeenCalled(); diff --git a/server/src/services/plugin.service.ts b/server/src/services/plugin.service.ts index d78b8940d3..7209a613fe 100644 --- a/server/src/services/plugin.service.ts +++ b/server/src/services/plugin.service.ts @@ -1,11 +1,9 @@ import { Plugin as ExtismPlugin, newPlugin } from '@extism/extism'; import { BadRequestException, Injectable } from '@nestjs/common'; -import { plainToInstance } from 'class-transformer'; -import { validateOrReject } from 'class-validator'; import { join } from 'node:path'; import { Asset, WorkflowAction, WorkflowFilter } from 'src/database'; import { OnEvent, OnJob } from 'src/decorators'; -import { PluginManifestDto } from 'src/dtos/plugin-manifest.dto'; +import { PluginManifestDto, PluginManifestSchema } from 'src/dtos/plugin-manifest.dto'; import { mapPlugin, PluginResponseDto, PluginTriggerResponseDto } from 'src/dtos/plugin.dto'; import { JobName, JobStatus, PluginTriggerType, QueueName } from 'src/enum'; import { pluginTriggers } from 'src/plugins'; @@ -138,14 +136,7 @@ export class PluginService extends BaseService { private async readAndValidateManifest(manifestPath: string): Promise { const content = await this.storageRepository.readTextFile(manifestPath); const manifestData = JSON.parse(content); - const manifest = plainToInstance(PluginManifestDto, manifestData); - - await validateOrReject(manifest, { - whitelist: true, - forbidNonWhitelisted: true, - }); - - return manifest; + return PluginManifestSchema.parse(manifestData); } /////////////////////////////////////////// diff --git a/server/src/services/queue.service.ts b/server/src/services/queue.service.ts index cdfa2ad2ed..662ccbe618 100644 --- a/server/src/services/queue.service.ts +++ b/server/src/services/queue.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { ClassConstructor } from 'class-transformer'; import { SystemConfig } from 'src/config'; import { OnEvent } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -39,7 +38,7 @@ const asNightlyTasksCron = (config: SystemConfig) => { @Injectable() export class QueueService extends BaseService { - private services: ClassConstructor[] = []; + private services: (new (...args: any[]) => unknown)[] = []; private nightlyJobsLock = false; @OnEvent({ name: 'ConfigInit' }) @@ -96,7 +95,7 @@ export class QueueService extends BaseService { } } - setServices(services: ClassConstructor[]) { + setServices(services: (new (...args: any[]) => unknown)[]) { this.services = services; } diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 30bc1f1f0d..77636acfd2 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -138,6 +138,12 @@ export class ServerService extends BaseService { async getStatistics(): Promise { const userStats: UserStatsQueryResponse[] = await this.userRepository.getUserStats(); const serverStats = new ServerStatsResponseDto(); + serverStats.photos ??= 0; + serverStats.videos ??= 0; + serverStats.usage ??= 0; + serverStats.usagePhotos ??= 0; + serverStats.usageVideos ??= 0; + serverStats.usageByUser ??= []; for (const user of userStats) { const usage = new UsageByUserDto(); diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index b346906fc8..bb68f70d13 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -311,9 +311,7 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify({ library: { scan: { cronExpression: 'foo' } } })); - await expect(sut.getSystemConfig()).rejects.toThrow( - 'library.scan.cronExpression has failed the following constraints: cronValidator', - ); + await expect(sut.getSystemConfig()).rejects.toThrow('[library.scan.cronExpression] Invalid cron expression'); }); it('should log errors with the config file', async () => { @@ -402,10 +400,26 @@ describe(SystemConfigService.name, () => { }); const tests = [ - { should: 'validate numbers', config: { ffmpeg: { crf: 'not-a-number' } } }, - { should: 'validate booleans', config: { oauth: { enabled: 'invalid' } } }, - { should: 'validate enums', config: { ffmpeg: { transcode: 'unknown' } } }, - { should: 'validate required oauth fields', config: { oauth: { enabled: true } } }, + { + should: 'validate numbers', + config: { ffmpeg: { crf: 'not-a-number' } }, + throws: '[ffmpeg.crf] Invalid input: expected number, received NaN', + }, + { + should: 'validate booleans', + config: { oauth: { enabled: 'invalid' } }, + throws: '[oauth.enabled] Invalid input: expected boolean, received string', + }, + { + should: 'validate enums', + config: { ffmpeg: { transcode: 'unknown' } }, + throws: '[ffmpeg.transcode] Invalid option: expected one of', + }, + { + should: 'validate required oauth fields', + config: { oauth: { enabled: true } }, + check: (c: SystemConfig) => expect(c.oauth.enabled).toBe(true), + }, { should: 'warn for top level unknown options', warn: true, config: { unknownOption: true } }, { should: 'warn for nested unknown options', warn: true, config: { ffmpeg: { unknownOption: true } } }, ]; @@ -415,11 +429,14 @@ describe(SystemConfigService.name, () => { mocks.config.getEnv.mockReturnValue(mockEnvData({ configFile: 'immich-config.json' })); mocks.systemMetadata.readFile.mockResolvedValue(JSON.stringify(test.config)); - if (test.warn) { + if (test.throws) { + await expect(sut.getSystemConfig()).rejects.toThrow(test.throws); + } else if (test.warn) { await sut.getSystemConfig(); expect(mocks.logger.warn).toHaveBeenCalled(); } else { - await expect(sut.getSystemConfig()).rejects.toBeInstanceOf(Error); + const config = await sut.getSystemConfig(); + test.check!(config); } }); } diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index ea95b4df24..981141b02e 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Injectable } from '@nestjs/common'; -import { instanceToPlain } from 'class-transformer'; import _ from 'lodash'; import { defaults } from 'src/config'; import { OnEvent } from 'src/decorators'; @@ -61,7 +60,7 @@ export class SystemConfigService extends BaseService { @OnEvent({ name: 'ConfigValidate' }) onConfigValidate({ newConfig, oldConfig }: ArgOf<'ConfigValidate'>) { const { logLevel } = this.configRepository.getEnv(); - if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && logLevel) { + if (!_.isEqual(toPlainObject(newConfig.logging), oldConfig.logging) && logLevel) { throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.'); } } diff --git a/server/src/types/plugin-schema.types.ts b/server/src/types/plugin-schema.types.ts index 793bb3c1ff..da1f6da935 100644 --- a/server/src/types/plugin-schema.types.ts +++ b/server/src/types/plugin-schema.types.ts @@ -3,33 +3,54 @@ * Based on JSON Schema Draft 7 */ -export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null'; +import z from 'zod'; -export interface JSONSchemaProperty { - type?: JSONSchemaType | JSONSchemaType[]; - description?: string; - default?: any; - enum?: any[]; - items?: JSONSchemaProperty; - properties?: Record; - required?: string[]; - additionalProperties?: boolean | JSONSchemaProperty; -} +const JSONSchemaTypeSchema = z + .enum(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']) + .meta({ id: 'PluginJsonSchemaType' }); -export interface JSONSchema { - type: 'object'; - properties?: Record; - required?: string[]; - additionalProperties?: boolean; - description?: string; -} +const JSONSchemaPropertySchema = z + .object({ + type: JSONSchemaTypeSchema.optional(), + description: z.string().optional(), + default: z.any().optional(), + enum: z.array(z.string()).optional(), -export type ConfigValue = string | number | boolean | null | ConfigValue[] | { [key: string]: ConfigValue }; + get items() { + return JSONSchemaPropertySchema.optional(); + }, -export interface FilterConfig { - [key: string]: ConfigValue; -} + get properties() { + return z.record(z.string(), JSONSchemaPropertySchema).optional(); + }, -export interface ActionConfig { - [key: string]: ConfigValue; -} + required: z.array(z.string()).optional(), + + get additionalProperties() { + return z.union([z.boolean(), JSONSchemaPropertySchema]).optional(); + }, + }) + .meta({ id: 'PluginJsonSchemaProperty' }); + +export type JSONSchemaProperty = z.infer; + +export const JSONSchemaSchema = z + .object({ + type: JSONSchemaTypeSchema.optional(), + properties: z.record(z.string(), JSONSchemaPropertySchema).optional(), + required: z.array(z.string()).optional(), + additionalProperties: z.boolean().optional(), + description: z.string().optional(), + }) + .meta({ id: 'PluginJsonSchema' }); +export type JSONSchema = z.infer; + +type ConfigValue = string | number | boolean | null | ConfigValue[] | { [key: string]: ConfigValue }; + +const ConfigValueSchema: z.ZodType = z.any().meta({ id: 'PluginConfigValue' }); + +export const FilterConfigSchema = z.record(z.string(), ConfigValueSchema).meta({ id: 'WorkflowFilterConfig' }); +export type FilterConfig = z.infer; + +export const ActionConfigSchema = z.record(z.string(), ConfigValueSchema).meta({ id: 'WorkflowActionConfig' }); +export type ActionConfig = z.infer; diff --git a/server/src/utils/bbox.ts b/server/src/utils/bbox.ts deleted file mode 100644 index ad02e8355e..0000000000 --- a/server/src/utils/bbox.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { applyDecorators } from '@nestjs/common'; -import { ApiPropertyOptions } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { IsNotEmpty, ValidateNested } from 'class-validator'; -import { Property } from 'src/decorators'; -import { BBoxDto } from 'src/dtos/bbox.dto'; -import { Optional } from 'src/validation'; - -type BBoxOptions = { optional?: boolean }; -export const ValidateBBox = (options: BBoxOptions & ApiPropertyOptions = {}) => { - const { optional, ...apiPropertyOptions } = options; - - return applyDecorators( - Transform(({ value }) => { - if (typeof value !== 'string') { - return value; - } - - const [west, south, east, north] = value.split(',', 4).map(Number); - return Object.assign(new BBoxDto(), { west, south, east, north }); - }), - Type(() => BBoxDto), - ValidateNested(), - Property({ - type: 'string', - description: 'Bounding box coordinates as west,south,east,north (WGS84)', - example: '11.075683,49.416711,11.117589,49.454875', - ...apiPropertyOptions, - }), - optional ? Optional({}) : IsNotEmpty(), - ); -}; diff --git a/server/src/utils/config.ts b/server/src/utils/config.ts index a669af31cf..df7d05978c 100644 --- a/server/src/utils/config.ts +++ b/server/src/utils/config.ts @@ -1,10 +1,8 @@ import AsyncLock from 'async-lock'; -import { instanceToPlain, plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; import { load as loadYaml } from 'js-yaml'; import * as _ from 'lodash'; import { SystemConfig, defaults } from 'src/config'; -import { SystemConfigDto } from 'src/dtos/system-config.dto'; +import { SystemConfigSchema } from 'src/dtos/system-config.dto'; import { DatabaseLock, SystemMetadataKey } from 'src/enum'; import { ConfigRepository } from 'src/repositories/config.repository'; import { LoggingRepository } from 'src/repositories/logging.repository'; @@ -101,19 +99,22 @@ const buildConfig = async (repos: RepoDeps) => { logger.warn(`Unknown keys found: ${JSON.stringify(unknownKeys, null, 2)}`); } - // validate full config - const instance = plainToInstance(SystemConfigDto, rawConfig); - const errors = await validate(instance); - if (errors.length > 0) { + // validate with Zod schema + const result = SystemConfigSchema.safeParse(rawConfig); + if (!result.success) { + const messages = ['Invalid system config: ']; + for (const issue of result.error.issues) { + const path = issue.path.join('.'); + messages.push(` - [${path}] ${issue.message}`); + } if (configFile) { - throw new Error(`Invalid value(s) in file: ${errors}`); + throw new Error(messages.join('\n')); } else { - logger.error('Validation error', errors); + logger.error('Validation error', messages); } } - // return config with class-transform changes - const config = instanceToPlain(instance) as SystemConfig; + const config = (result.success ? result.data : rawConfig) as SystemConfig; if (config.server.externalDomain.length > 0) { const domain = new URL(config.server.externalDomain); diff --git a/server/src/utils/date.ts b/server/src/utils/date.ts index 092a0e6619..d4de1eba86 100644 --- a/server/src/utils/date.ts +++ b/server/src/utils/date.ts @@ -1,9 +1,21 @@ import { DateTime } from 'luxon'; +/** + * Convert a date to a ISO 8601 datetime string. + * @param x - The date to convert. + * @returns The ISO 8601 datetime string. + * @deprecated Remove this and all references when using `ZodSerializerDto` on the controllers. Then the codec in `isoDatetimeToDate` in validation.ts will handle the conversion instead. + */ export const asDateString = (x: T) => { return x instanceof Date ? x.toISOString() : (x as Exclude); }; +/** + * Convert a date to a date string. + * @param x - The date to convert. + * @returns The date string. + * @deprecated Remove this and all references when using `ZodSerializerDto` on the controllers. Then the codec in `isoDateToDate` in validation.ts will handle the conversion instead. + */ export const asBirthDateString = (x: Date | string | null): string | null => { return x instanceof Date ? x.toISOString().split('T')[0] : x; }; diff --git a/server/src/utils/duplicate.spec.ts b/server/src/utils/duplicate.spec.ts index 4c5d5ddfc4..9c8822518b 100644 --- a/server/src/utils/duplicate.spec.ts +++ b/server/src/utils/duplicate.spec.ts @@ -1,12 +1,16 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto'; +import { ExifResponseSchema } from 'src/dtos/exif.dto'; import { AssetType, AssetVisibility } from 'src/enum'; import { getExifCount, suggestDuplicate, suggestDuplicateKeepAssetIds } from 'src/utils/duplicate'; import { describe, expect, it } from 'vitest'; +import type { z } from 'zod'; + +type ExifInfoInput = Partial>; const createAsset = ( id: string, fileSizeInByte: number | null = null, - exifFields: Record = {}, + exifFields: ExifInfoInput = {}, ): AssetResponseDto => ({ id, type: AssetType.Image, @@ -33,7 +37,9 @@ const createAsset = ( visibility: AssetVisibility.Timeline, checksum: 'checksum', exifInfo: - fileSizeInByte !== null || Object.keys(exifFields).length > 0 ? { fileSizeInByte, ...exifFields } : undefined, + fileSizeInByte !== null || Object.keys(exifFields).length > 0 + ? ExifResponseSchema.parse({ fileSizeInByte, ...exifFields }) + : undefined, }); describe('duplicate utils', () => { @@ -46,7 +52,7 @@ describe('duplicate utils', () => { it('should return 0 for empty exifInfo', () => { const asset = createAsset('asset-1'); - asset.exifInfo = {}; + asset.exifInfo = ExifResponseSchema.parse({}); expect(getExifCount(asset)).toBe(0); }); @@ -54,7 +60,7 @@ describe('duplicate utils', () => { const asset = createAsset('asset-1', 1000, { make: 'Canon', model: 'EOS 5D', - dateTimeOriginal: new Date(), + dateTimeOriginal: new Date().toISOString(), timeZone: 'UTC', latitude: 40.7128, longitude: -74.006, @@ -107,7 +113,7 @@ describe('duplicate utils', () => { const moreExif = createAsset('more-exif', 1000, { make: 'Canon', model: 'EOS 5D', - dateTimeOriginal: new Date(), + dateTimeOriginal: new Date().toISOString(), city: 'New York', }); @@ -125,7 +131,7 @@ describe('duplicate utils', () => { it('should handle assets with exifInfo but no fileSizeInByte', () => { const noFileSize = createAsset('no-file-size'); - noFileSize.exifInfo = { make: 'Canon', model: 'EOS 5D' }; + noFileSize.exifInfo = ExifResponseSchema.parse({ make: 'Canon', model: 'EOS 5D' }); const withFileSize = createAsset('with-file-size', 1000); expect(suggestDuplicate([noFileSize, withFileSize])?.id).toBe('with-file-size'); @@ -148,7 +154,7 @@ describe('duplicate utils', () => { const smallWithMoreExif = createAsset('small-more-exif', 1000, { make: 'Canon', model: 'EOS 5D', - dateTimeOriginal: new Date(), + dateTimeOriginal: new Date().toISOString(), city: 'New York', state: 'NY', country: 'USA', diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index 7d2e99a215..450563cf7e 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -12,6 +12,7 @@ import { SchemaObject, } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface'; import _ from 'lodash'; +import { cleanupOpenApiDoc } from 'nestjs-zod'; import { writeFileSync } from 'node:fs'; import path from 'node:path'; import picomatch from 'picomatch'; @@ -158,11 +159,38 @@ const isSchema = (schema: string | ReferenceObject | SchemaObject): schema is Sc }; const patchOpenAPI = (document: OpenAPIObject) => { + const removeOpenApi30IncompatibleKeys = (target: unknown) => { + if (!target || typeof target !== 'object') { + return; + } + + if (Array.isArray(target)) { + for (const item of target) { + removeOpenApi30IncompatibleKeys(item); + } + return; + } + + const object = target as Record; + delete object.propertyNames; + delete object.contentEncoding; + + for (const value of Object.values(object)) { + removeOpenApi30IncompatibleKeys(value); + } + }; + document.paths = sortKeys(document.paths); + // Allowed in OpenAPI v3.1 (JSON Schema 2020-12), but not in OpenAPI v3.0 (current spec). + removeOpenApi30IncompatibleKeys(document); if (document.components?.schemas) { const schemas = document.components.schemas as Record; + for (const schema of Object.values(schemas)) { + delete (schema as Record).id; + } + document.components.schemas = sortKeys(schemas); for (const [schemaName, schema] of Object.entries(schemas)) { @@ -265,6 +293,7 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean }) }; const specification = SwaggerModule.createDocument(app, config, options); + const openApiDoc = cleanupOpenApiDoc(specification); const customOptions: SwaggerCustomOptions = { swaggerOptions: { @@ -275,12 +304,12 @@ export const useSwagger = (app: INestApplication, { write }: { write: boolean }) customSiteTitle: 'Immich API Documentation', }; - SwaggerModule.setup('doc', app, specification, customOptions); + SwaggerModule.setup('doc', app, openApiDoc, customOptions); if (write) { // Generate API Documentation only in development mode const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json'); - writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' }); + writeFileSync(outputPath, JSON.stringify(patchOpenAPI(openApiDoc), null, 2), { encoding: 'utf8' }); } }; diff --git a/server/src/validation.spec.ts b/server/src/validation.spec.ts index 631ba60a60..434ac89cee 100644 --- a/server/src/validation.spec.ts +++ b/server/src/validation.spec.ts @@ -1,92 +1,45 @@ -import { plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; -import { DateTime } from 'luxon'; -import { IsDateStringFormat, IsNotSiblingOf, MaxDateString, Optional } from 'src/validation'; -import { describe } from 'vitest'; +import { IsNotSiblingOf } from 'src/validation'; +import { describe, expect, it } from 'vitest'; +import z from 'zod'; describe('Validation', () => { - describe('MaxDateString', () => { - const maxDate = DateTime.fromISO('2000-01-01', { zone: 'utc' }); - - class MyDto { - @MaxDateString(maxDate) - date!: string; - } - - it('passes when date is before maxDate', async () => { - const dto = plainToInstance(MyDto, { date: '1999-12-31' }); - await expect(validate(dto)).resolves.toHaveLength(0); - }); - - it('passes when date is equal to maxDate', async () => { - const dto = plainToInstance(MyDto, { date: '2000-01-01' }); - await expect(validate(dto)).resolves.toHaveLength(0); - }); - - it('fails when date is after maxDate', async () => { - const dto = plainToInstance(MyDto, { date: '2010-01-01' }); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - }); - - describe('IsDateStringFormat', () => { - class MyDto { - @IsDateStringFormat('yyyy-MM-dd') - date!: string; - } - - it('passes when date is valid', async () => { - const dto = plainToInstance(MyDto, { date: '1999-12-31' }); - await expect(validate(dto)).resolves.toHaveLength(0); - }); - - it('fails when date has invalid format', async () => { - const dto = plainToInstance(MyDto, { date: '2000-01-01T00:00:00Z' }); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - - it('fails when empty string', async () => { - const dto = plainToInstance(MyDto, { date: '' }); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - - it('fails when undefined', async () => { - const dto = plainToInstance(MyDto, {}); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - }); - describe('IsNotSiblingOf', () => { - class MyDto { - @IsNotSiblingOf(['attribute2']) - @Optional() - attribute1?: string; - - @IsNotSiblingOf(['attribute1', 'attribute3']) - @Optional() - attribute2?: string; - - @IsNotSiblingOf(['attribute2']) - @Optional() - attribute3?: string; - - @Optional() - unrelatedAttribute?: string; - } - - it('passes when only one attribute is present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', unrelatedAttribute: 'value2' }); - await expect(validate(dto)).resolves.toHaveLength(0); + const MySchemaBase = z.object({ + attribute1: z.string().optional(), + attribute2: z.string().optional(), + attribute3: z.string().optional(), + unrelatedAttribute: z.string().optional(), }); - it('fails when colliding attributes are present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', attribute2: 'value2' }); - await expect(validate(dto)).resolves.toHaveLength(2); + const MySchema = MySchemaBase.pipe(IsNotSiblingOf(MySchemaBase, 'attribute1', ['attribute2'])) + .pipe(IsNotSiblingOf(MySchemaBase, 'attribute2', ['attribute1', 'attribute3'])) + .pipe(IsNotSiblingOf(MySchemaBase, 'attribute3', ['attribute2'])); + + it('passes when only one attribute is present', () => { + const result = MySchema.safeParse({ + attribute1: 'value1', + unrelatedAttribute: 'value2', + }); + expect(result.success).toBe(true); }); - it('passes when no colliding attributes are present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', attribute3: 'value2' }); - await expect(validate(dto)).resolves.toHaveLength(0); + it('fails when colliding attributes are present', () => { + const result = MySchema.safeParse({ + attribute1: 'value1', + attribute2: 'value2', + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe('attribute1 cannot exist alongside attribute2'); + } + }); + + it('passes when no colliding attributes are present', () => { + const result = MySchema.safeParse({ + attribute1: 'value1', + attribute3: 'value2', + }); + expect(result.success).toBe(true); }); }); }); diff --git a/server/src/validation.ts b/server/src/validation.ts index b959de94b1..54e3b1820e 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -1,40 +1,62 @@ -import { - ArgumentMetadata, - BadRequestException, - FileValidator, - Injectable, - ParseUUIDPipe, - applyDecorators, -} from '@nestjs/common'; -import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { - IsArray, - IsBoolean, - IsDate, - IsEnum, - IsHexColor, - IsNotEmpty, - IsOptional, - IsString, - IsUUID, - Matches, - Validate, - ValidateBy, - ValidateIf, - ValidationArguments, - ValidationOptions, - ValidatorConstraint, - ValidatorConstraintInterface, - buildMessage, - isDateString, - isDefined, -} from 'class-validator'; -import { CronJob } from 'cron'; -import { DateTime } from 'luxon'; +import { ArgumentMetadata, FileValidator, Injectable, ParseUUIDPipe } from '@nestjs/common'; +import { createZodDto } from 'nestjs-zod'; import sanitize from 'sanitize-filename'; -import { Property, PropertyOptions } from 'src/decorators'; import { isIP, isIPRange } from 'validator'; +import z from 'zod'; + +export type IsIPRangeOptions = { requireCIDR?: boolean }; + +function isIPOrRange(value: string, options?: IsIPRangeOptions): boolean { + const { requireCIDR = true } = options ?? {}; + if (isIPRange(value)) { + return true; + } + if (!requireCIDR && isIP(value)) { + return true; + } + return false; +} + +/** + * Zod schema that validates an array of strings as IP addresses or IP/CIDR ranges. + * When requireCIDR is true (default), plain IPs are rejected; only CIDR ranges are allowed. + * + * @example + * z.string().optional().transform(...).pipe(IsIPRange()) + * @example + * z.string().optional().transform(...).pipe(IsIPRange({ requireCIDR: false })) + */ +export function IsIPRange(options?: IsIPRangeOptions) { + return z + .array(z.string()) + .refine((arr) => arr.every((item) => isIPOrRange(item, options)), 'Must be an ip address or ip address range'); +} + +/** + * Zod schema that validates sibling-exclusion for object schemas. + * Validation passes when the target property is missing, or when none of the sibling properties are present. + * Use with .pipe() like IsIPRange. + * + * @example + * const Schema = z.object({ a: z.string().optional(), b: z.string().optional() }); + * Schema.pipe(IsNotSiblingOf(Schema, 'a', ['b'])); + */ +export function IsNotSiblingOf< + TSchema extends z.ZodObject, + TKey extends z.infer> & keyof z.infer, +>(_schema: TSchema, property: TKey, siblings: TKey[]) { + type T = z.infer; + const message = `${String(property)} cannot exist alongside ${siblings.join(' or ')}`; + return z.custom().refine( + (data) => { + if (data[property] === undefined) { + return true; + } + return siblings.every((sibling) => data[sibling] === undefined); + }, + { message }, + ); +} @Injectable() export class ParseMeUUIDPipe extends ParseUUIDPipe { @@ -66,386 +88,163 @@ export class FileNotEmptyValidator extends FileValidator { } } -type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean }; -export const ValidateUUID = (options?: UUIDOptions & PropertyOptions) => { - const { optional, each, nullable, ...apiPropertyOptions } = { - optional: false, - each: false, - nullable: false, - ...options, - }; - return applyDecorators( - IsUUID('4', { each }), - Property({ format: 'uuid', ...apiPropertyOptions }), - optional ? Optional({ nullable }) : IsNotEmpty(), - each ? IsArray() : IsString(), - ); -}; +const UUIDParamSchema = z.object({ + id: z.uuidv4(), +}); -export function IsAxisAlignedRotation() { - return ValidateBy( - { - name: 'isAxisAlignedRotation', - validator: { - validate(value: any) { - return [0, 90, 180, 270].includes(value); - }, - defaultMessage: buildMessage( - (eachPrefix) => eachPrefix + '$property must be one of the following values: 0, 90, 180, 270', - {}, - ), - }, - }, - {}, - ); -} +export class UUIDParamDto extends createZodDto(UUIDParamSchema) {} -@ValidatorConstraint({ name: 'uniqueEditActions' }) -class UniqueEditActionsValidator implements ValidatorConstraintInterface { - validate(edits: { action: string; parameters?: unknown }[]): boolean { - if (!Array.isArray(edits)) { - return true; - } +const UUIDAssetIDParamSchema = z.object({ + id: z.uuidv4(), + assetId: z.uuidv4(), +}); - const actionSet = new Set(); - for (const edit of edits) { - const key = edit.action === 'mirror' ? `${edit.action}-${JSON.stringify(edit.parameters)}` : edit.action; - if (actionSet.has(key)) { - return false; - } - actionSet.add(key); - } - return true; - } +export class UUIDAssetIDParamDto extends createZodDto(UUIDAssetIDParamSchema) {} - defaultMessage(): string { - return 'Duplicate edit actions are not allowed'; - } -} +const FilenameParamSchema = z.object({ + filename: z.string().regex(/^[a-zA-Z0-9_\-.]+$/, { + error: 'Filename contains invalid characters', + }), +}); -export const IsUniqueEditActions = () => Validate(UniqueEditActionsValidator); - -export class UUIDParamDto { - @IsNotEmpty() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - id!: string; -} - -export class UUIDAssetIDParamDto { - @ValidateUUID() - id!: string; - - @ValidateUUID() - assetId!: string; -} - -export class FilenameParamDto { - @IsNotEmpty() - @IsString() - @ApiProperty({ format: 'string' }) - @Matches(/^[a-zA-Z0-9_\-.]+$/, { - message: 'Filename contains invalid characters', - }) - filename!: string; -} - -type PinCodeOptions = { optional?: boolean } & OptionalOptions; -export const PinCode = (options?: PinCodeOptions & ApiPropertyOptions) => { - const { optional, nullable, emptyToNull, ...apiPropertyOptions } = { - optional: false, - nullable: false, - emptyToNull: false, - ...options, - }; - const decorators = [ - IsString(), - IsNotEmpty(), - Matches(/^\d{6}$/, { message: ({ property }) => `${property} must be a 6-digit numeric string` }), - ApiProperty({ example: '123456', ...apiPropertyOptions }), - ]; - - if (optional) { - decorators.push(Optional({ nullable, emptyToNull })); - } - - return applyDecorators(...decorators); -}; - -export interface OptionalOptions { - nullable?: boolean; - /** convert empty strings to null */ - emptyToNull?: boolean; -} - -/** - * Checks if value is missing and if so, ignores all validators. - * - * @param validationOptions {@link OptionalOptions} - * - * @see IsOptional exported from `class-validator. - */ -// https://stackoverflow.com/a/71353929 -export function Optional({ nullable, emptyToNull, ...validationOptions }: OptionalOptions = {}) { - const decorators: PropertyDecorator[] = []; - - if (nullable === true) { - decorators.push(IsOptional(validationOptions)); - } else { - decorators.push(ValidateIf((object: any, v: any) => v !== undefined, validationOptions)); - } - - if (emptyToNull) { - decorators.push(Transform(({ value }) => (value === '' ? null : value))); - } - - return applyDecorators(...decorators); -} - -export function IsNotSiblingOf(siblings: string[], validationOptions?: ValidationOptions) { - return ValidateBy( - { - name: 'isNotSiblingOf', - constraints: siblings, - validator: { - validate(value: any, args: ValidationArguments) { - if (!isDefined(value)) { - return true; - } - return args.constraints.filter((prop) => isDefined((args.object as any)[prop])).length === 0; - }, - defaultMessage: (args: ValidationArguments) => { - return `${args.property} cannot exist alongside any of the following properties: ${args.constraints.join(', ')}`; - }, - }, - }, - validationOptions, - ); -} - -export const ValidateHexColor = () => { - const decorators = [ - IsHexColor(), - Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value)), - ]; - - return applyDecorators(...decorators); -}; - -type DateOptions = OptionalOptions & { optional?: boolean; format?: 'date' | 'date-time' }; -export const ValidateDate = (options?: DateOptions & PropertyOptions) => { - const { - optional, - nullable = false, - emptyToNull = false, - format = 'date-time', - ...apiPropertyOptions - } = options || {}; - - return applyDecorators( - Property({ format, ...apiPropertyOptions }), - IsDate(), - optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), - Transform(({ key, value }) => { - if (value === null || value === undefined) { - return value; - } - - if (!isDateString(value)) { - throw new BadRequestException(`${key} must be a date string`); - } - - return new Date(value as string); - }), - ); -}; - -type StringOptions = OptionalOptions & { optional?: boolean; trim?: boolean }; -export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => { - const { optional, nullable, emptyToNull, trim, ...apiPropertyOptions } = options || {}; - const decorators = [ - ApiProperty(apiPropertyOptions), - IsString(), - optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), - ]; - - if (trim) { - decorators.push(Transform(({ value }: { value: string }) => value?.trim())); - } - - return applyDecorators(...decorators); -}; - -type BooleanOptions = OptionalOptions & { optional?: boolean }; -export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => { - const { optional, nullable, emptyToNull, ...apiPropertyOptions } = options || {}; - const decorators = [ - Property(apiPropertyOptions), - IsBoolean(), - Transform(({ value }) => { - if (value == 'true') { - return true; - } else if (value == 'false') { - return false; - } - return value; - }), - optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), - ]; - - return applyDecorators(...decorators); -}; - -type EnumOptions = { - enum: T; - name: string; - each?: boolean; - optional?: boolean; - nullable?: boolean; - default?: T[keyof T]; - description?: string; -}; -export const ValidateEnum = ({ - name, - enum: value, - each, - optional, - nullable, - default: defaultValue, - description, -}: EnumOptions) => { - return applyDecorators( - optional ? Optional({ nullable }) : IsNotEmpty(), - IsEnum(value, { each }), - ApiProperty({ enumName: name, enum: value, isArray: each, default: defaultValue, description }), - ); -}; - -@ValidatorConstraint({ name: 'cronValidator' }) -class CronValidator implements ValidatorConstraintInterface { - validate(expression: string): boolean { - try { - new CronJob(expression, () => {}); - return true; - } catch { - return false; - } - } -} - -export const IsCronExpression = () => Validate(CronValidator, { message: 'Invalid cron expression' }); - -type IValue = { value: unknown }; - -export const toEmail = ({ value }: IValue) => (typeof value === 'string' ? value.toLowerCase() : value); - -export const toSanitized = ({ value }: IValue) => { - const input = typeof value === 'string' ? value : ''; - return sanitize(input.replaceAll('.', '')); -}; +export class FilenameParamDto extends createZodDto(FilenameParamSchema) {} export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => { const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; return Number.isInteger(value) && value >= min && value <= max; }; -export function isDateStringFormat(value: unknown, format: string) { - if (typeof value !== 'string') { - return false; - } - return DateTime.fromFormat(value, format, { zone: 'utc' }).isValid; -} +/** + * Unified email validation + * Converts email strings to lowercase and validates against HTML5 email regex + * @docs https://zod.dev/api?id=email + */ +export const toEmail = z + .email({ + pattern: z.regexes.html5Email, + error: (iss) => `Invalid input: expected email, received ${typeof iss.input}`, + }) + .transform((val) => val.toLowerCase()); -export function IsDateStringFormat(format: string, validationOptions?: ValidationOptions) { - return ValidateBy( +/** + * Parse ISO 8601 datetime strings to Date objects + * @docs https://zod.dev/api?id=codec + */ +export const isoDatetimeToDate = z + .codec( + z.iso.datetime({ + error: (iss) => `Invalid input: expected ISO 8601 datetime string, received ${typeof iss.input}`, + }), + z.date(), { - name: 'isDateStringFormat', - constraints: [format], - validator: { - validate(value: unknown) { - return isDateStringFormat(value, format); - }, - defaultMessage: () => `$property must be a string in the format ${format}`, - }, + decode: (isoString) => new Date(isoString), + encode: (date) => date.toISOString(), }, - validationOptions, - ); -} + ) + .meta({ example: '2024-01-01T00:00:00.000Z' }); -function maxDate(date: DateTime, maxDate: DateTime | (() => DateTime)) { - return date <= (maxDate instanceof DateTime ? maxDate : maxDate()); -} - -export function MaxDateString( - date: DateTime | (() => DateTime), - validationOptions?: ValidationOptions, -): PropertyDecorator { - return ValidateBy( +/** + * Parse ISO date strings to Date objects + * @docs https://zod.dev/api?id=codec + */ +export const isoDateToDate = z + .codec( + z.iso.date({ + error: (iss) => `Invalid input: expected ISO date string (YYYY-MM-DD), received ${typeof iss.input}`, + }), + z.date(), { - name: 'maxDateString', - constraints: [date], - validator: { - validate: (value, args) => { - const date = DateTime.fromISO(value, { zone: 'utc' }); - return maxDate(date, args?.constraints[0]); - }, - defaultMessage: buildMessage( - (eachPrefix) => 'maximal allowed date for ' + eachPrefix + '$property is $constraint1', - validationOptions, - ), - }, + decode: (isoString) => new Date(isoString), + encode: (date) => date.toISOString().slice(0, 10), }, - validationOptions, - ); -} + ) + .meta({ example: '2024-01-01' }); -type IsIPRangeOptions = { requireCIDR?: boolean }; -export function IsIPRange(options: IsIPRangeOptions, validationOptions?: ValidationOptions): PropertyDecorator { - const { requireCIDR } = { requireCIDR: true, ...options }; +export const isValidTime = z + .string() + .regex(/^([01]\d|2[0-3]):[0-5]\d$/, 'Invalid input: expected string in HH:mm format, received string'); - return ValidateBy( - { - name: 'isIPRange', - validator: { - validate: (value): boolean => { - if (isIPRange(value)) { - return true; - } +/** + * Latitude in range [-90, 90]. Reuse for body or query params. + * + * @example + * // Regular (body): optional coordinates + * latitudeSchema.optional().describe('Latitude coordinate') + * + * @example + * // Pipe (query): coerce string to number then validate range + * z.coerce.number().pipe(latitudeSchema).describe('Latitude (-90 to 90)') + */ +export const latitudeSchema = z.number().min(-90).max(90); - if (!requireCIDR && isIP(value)) { - return true; - } +/** + * Longitude in range [-180, 180]. Reuse for body or query params. + * + * @example + * // Regular (body): optional coordinates + * longitudeSchema.optional().describe('Longitude coordinate') + * + * @example + * // Pipe (query): coerce string to number then validate range + * z.coerce.number().pipe(longitudeSchema).describe('Longitude (-180 to 180)') + */ +export const longitudeSchema = z.number().min(-180).max(180); - return false; - }, - defaultMessage: buildMessage( - (eachPrefix) => eachPrefix + '$property must be an ip address, or ip address range', - validationOptions, - ), - }, - }, - validationOptions, - ); -} +/** + * Parse string to boolean + * This should be used for boolean query parameters and path parameters, but not for boolean request body parameters, as the first are always string. + * We don't use z.coerce.boolean() as any truthy value is considered true + * z.stringbool() is a more robust way to parse strings to booleans as it lets you specify the truthy and falsy values and the case sensitivity. + * @docs https://zod.dev/api?id=coercion + * @docs https://zod.dev/api?id=stringbool + */ +export const stringToBool = z + .stringbool({ truthy: ['true'], falsy: ['false'], case: 'sensitive' }) + .meta({ type: 'boolean' }); -@ValidatorConstraint({ name: 'isGreaterThanOrEqualTo' }) -export class IsGreaterThanOrEqualToConstraint implements ValidatorConstraintInterface { - validate(value: unknown, args: ValidationArguments) { - const relatedPropertyName = args.constraints?.[0] as string; - const relatedValue = (args.object as Record)[relatedPropertyName]; - if (!Number.isFinite(value) || !Number.isFinite(relatedValue)) { - return true; +/** + * Parse JSON strings from multipart/form-data + */ +export const JsonParsed = z.transform((val, ctx) => { + if (typeof val === 'string') { + try { + return JSON.parse(val); + } catch { + ctx.issues.push({ + code: 'custom', + message: `Invalid input: expected JSON string, received ${typeof val}`, + input: val, + }); + return z.NEVER; } - - return Number(value) >= Number(relatedValue); } + return val; +}); - defaultMessage(args: ValidationArguments) { - const relatedPropertyName = args.constraints?.[0] as string; - return `${args.property} must be greater than or equal to ${relatedPropertyName}`; - } -} +/** + * Hex color validation and normalization. + * Accepts formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA (with or without # prefix). + * Normalizes output to always include the # prefix. + * + * @example + * hexColor.optional() + */ +const hexColorRegex = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; +export const hexColor = z + .string() + .regex(hexColorRegex) + .transform((val) => (val.startsWith('#') ? val : `#${val}`)); -export const IsGreaterThanOrEqualTo = (property: string, validationOptions?: ValidationOptions) => { - return Validate(IsGreaterThanOrEqualToConstraint, [property], validationOptions); -}; +/** + * Transform empty strings to null. Inner schema passed to this function must accept null. + * @docs https://zod.dev/api?id=preprocess + * @example emptyStringToNull(z.string().nullable()).optional() // [encouraged] final schema is optional + * @example emptyStringToNull(z.string().nullable()) // [encouraged] same as the one above, but final schema is not optional + * @example emptyStringToNull(z.string().nullish()) // [discouraged] same as the one above, might be confusing + * @example emptyStringToNull(z.string().optional()) // fails: string schema rejects null + * @example emptyStringToNull(z.string().nullable()).nullish() // [discouraged] passes, null is duplicated. use the first example instead + */ +export const emptyStringToNull = (schema: T) => + z.preprocess((val) => (val === '' ? null : val), schema); + +export const sanitizeFilename = z.string().transform((val) => sanitize(val.replaceAll('.', ''))); diff --git a/server/test/utils.ts b/server/test/utils.ts index b3e47b2b7e..aa9a9735bf 100644 --- a/server/test/utils.ts +++ b/server/test/utils.ts @@ -1,12 +1,13 @@ import { createPostgres, DatabaseConnectionParams } from '@immich/sql-tools'; -import { CallHandler, ExecutionContext, Provider, ValidationPipe } from '@nestjs/common'; -import { APP_GUARD, APP_PIPE } from '@nestjs/core'; +import { CallHandler, ExecutionContext, Provider } from '@nestjs/common'; +import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core'; import { transformException } from '@nestjs/platform-express/multer/multer/multer.utils'; import { Test } from '@nestjs/testing'; -import { ClassConstructor } from 'class-transformer'; import { NextFunction } from 'express'; import { Kysely } from 'kysely'; import multer from 'multer'; +import { ClsService } from 'nestjs-cls'; +import { ZodSerializerInterceptor, ZodValidationPipe } from 'nestjs-zod'; import { ChildProcessWithoutNullStreams } from 'node:child_process'; import { Duplex, Readable, Writable } from 'node:stream'; import { PNG } from 'pngjs'; @@ -14,6 +15,7 @@ import { UploadFieldName } from 'src/dtos/asset-media.dto'; import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor'; import { AuthGuard } from 'src/middleware/auth.guard'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; +import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; import { AccessRepository } from 'src/repositories/access.repository'; import { ActivityRepository } from 'src/repositories/activity.repository'; import { AlbumUserRepository } from 'src/repositories/album-user.repository'; @@ -90,7 +92,7 @@ export type ControllerContext = { close: () => Promise; }; -export const controllerSetup = async (controller: ClassConstructor, providers: Provider[]) => { +export const controllerSetup = async (controller: new (...args: any[]) => unknown, providers: Provider[]) => { const noopInterceptor = { intercept: (ctx: never, next: CallHandler) => next.handle() }; const upload = multer({ storage: multer.memoryStorage() }); const memoryFileInterceptor = { @@ -113,9 +115,12 @@ export const controllerSetup = async (controller: ClassConstructor, pro const moduleRef = await Test.createTestingModule({ controllers: [controller], providers: [ - { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, + { provide: APP_FILTER, useClass: GlobalExceptionFilter }, + { provide: APP_PIPE, useClass: ZodValidationPipe }, + { provide: APP_INTERCEPTOR, useClass: ZodSerializerInterceptor }, { provide: APP_GUARD, useClass: AuthGuard }, { provide: LoggingRepository, useValue: LoggingRepository.create() }, + { provide: ClsService, useValue: { getId: vi.fn() } }, { provide: AuthService, useValue: { authenticate: vi.fn() } }, ...providers, ], @@ -158,14 +163,14 @@ const mockFn = (label: string, { strict }: { strict: boolean }) => { }); }; -export const mockBaseService = (service: ClassConstructor) => { +export const mockBaseService = (service: new (...args: any[]) => T) => { return automock(service, { args: [{ setContext: () => {} }], strict: false }); }; export const automock = ( - Dependency: ClassConstructor, + Dependency: new (...args: any[]) => T, options?: { - args?: ConstructorParameters>; + args?: ConstructorParameters T>; strict?: boolean; }, ): AutoMocked => { diff --git a/web/src/lib/utils/file-uploader.ts b/web/src/lib/utils/file-uploader.ts index 627cdded50..24bf3739e2 100644 --- a/web/src/lib/utils/file-uploader.ts +++ b/web/src/lib/utils/file-uploader.ts @@ -7,8 +7,8 @@ import { uploadRequest } from '$lib/utils'; import { ExecutorQueue } from '$lib/utils/executor-queue'; import { asQueryString } from '$lib/utils/shared-links'; import { - Action, AssetMediaStatus, + AssetUploadAction, AssetVisibility, checkBulkUpload, getBaseUrl, @@ -180,7 +180,7 @@ async function fileUploader({ const { results: [checkUploadResult], } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: [{ id: assetFile.name, checksum }] } }); - if (checkUploadResult.action === Action.Reject && checkUploadResult.assetId) { + if (checkUploadResult.action === AssetUploadAction.Reject && checkUploadResult.assetId) { responseData = { status: AssetMediaStatus.Duplicate, id: checkUploadResult.assetId,