diff --git a/cli/src/commands/asset.spec.ts b/cli/src/commands/asset.spec.ts index 21700ef963924..f179b350c9fc7 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 7d4b09b69d7e3..c3ad820547abf 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 11e825a7cd387..2d9a325289bd8 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 4d67a8464781d..719436a66dc4c 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 977638aa2491d..c280deb13415f 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 ae9064375f58f..a0ae1dc8194fc 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 d69536f3a3df3..7b5a2f16de56c 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 793c508a36c4c..6751b21e84d06 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 3f280dddf5205..ee13a29c1bf80 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 1494b4053136a..eff93a8d722f9 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 33a8eca94df97..32188b4838466 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 525f0906babf6..367c2447f23e8 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 090889ff3297d..38c805a42e75c 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 d01e743a3f5f3..b1df5f240c43f 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 6b554fb644ced..9403852ef0476 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 697598ac97f61..e0a393948cb7b 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 a026b99028239..831c19683d11d 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 fbd485f86fe32..768185db1edf4 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 33bcaf062cb07..94b7e2e73854d 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 41517f8144a36..9dda59a883b41 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 913205428e0be..0cd96ac44258a 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 d4e2b1d80fefa..ab0be3e8f38db 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 3b15b909090ba..7d18f6d867937 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 ecb556e434e10..1312cb5952106 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 085958de66412..46fc8594a8f10 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 f5b70a9ea4532..4e43ec28ebf29 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 f82c362ff7c3e..30a4c123f1166 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 59a4b60096172..5e165ffd5d95b 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 7ccae02c76da3..1d905b1e2281b 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 48e5f5874bcdd..3ed1f7529f280 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 830325a5b6a1e..3b36b23d6cbf8 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 fb4b6d084e33d..bc220e64ced41 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 dadb45d8ac655..1b0e279ab7b55 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 15ad2a170e331..d9ac019ee226c 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 43e686fbdc72c..ca0c08702758f 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 127334e687eff..0f440d572dd8c 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 c448a0b4b7bd3..ee457905bd3a8 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 8006748341e60..26aa35ae78a48 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 8d0c01cfb8a43..bbae03fba74c3 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 743a9f064511b..99e679222e51c 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 520ee171c10db..def205de90cf9 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 107c65dd1e226..d61b5c1398af7 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 32ba5433425d0..d5b8bf8b418ee 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 99bac7abfa988..f97300b19f8bb 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 b56370f689a9b..bf3ee8e244354 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 22c09752d2958..f59cdc1a67bc8 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 7351840b11c60..348bae370b5fa 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 7829de4bd57cb..2c7bb82c242f5 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 fc67aa022f53c..2086f72929a4f 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 a23a1ef5f37f3..3315fe8579a23 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 3ecc20c699e96..29c28175cd458 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 61d972a0c493f..21b86dfe4e7d0 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 1ae5cef07e9d4..4a4a2a658e16b 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 3fabb1cac65d4..835e063e92943 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 0000000000000..c51eab1692b06 --- /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 974528302119a..cafe1b21b99f7 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 0aa5544a3aebb..5085e3820c27a 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 905e738b6ee93..6dc5cd3c92a55 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 087d19da1fab6..ed7a72a613b39 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 b79a69372686f..3e16ed8721e7f 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 caaf379b30488..e4eab08bf1e15 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 2c3faab178019..d3562f5a483cd 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 8a6bcb9b01c0a..70de1941f3dd3 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 0000000000000..a31e1e61172f2 --- /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 078dd0bdaf550..d185761f54caa 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 229e7aa710023..96fd66a392ac2 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 201550c87fe22..df2762a2f3c75 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 0000000000000..b5cdbb0151f18 --- /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 a817832dab0e3..875eb138a8dbf 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 1fa8536964a03..bb3f1d88561bd 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 0b7f0738fe7b5..796138b0bfa13 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 1bef8f29d829b..af5b2cbf68c32 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 69942fee5c7ed..ba12c62d7672e 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 20d7cbd5e703c..c6ec0d94a05f2 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 8bc33a81dc483..c336270b849d0 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 34912a55e0472..abfa637157bf7 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 16985dd605d7d..de7bf78d5a4ee 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 97a3346a67ba2..dcb1258457dba 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 a1ba44920ede8..8a0cebd945c64 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 32e9487475ecc..bc1d7b4047042 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 81912e1d3044a..bfe32307fa913 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 4acc1c8bd3511..c5feb9df43ded 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 6bb58a8ab98e2..64a5a73bed778 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 4b9d7a6e9e504..66cb542ccf1f4 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 906a95a83c20d..873404c78672c 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 3a3412384e8ad..fe6743cba09d9 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 73a0187ddd3ba..98fe3d3536cb1 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 aa9158e591846..88ebceae24f07 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 6eec3ae8d77aa..55adbc2b4933a 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 ea1fee9d7abf4..d1818a2a43c4a 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 84ff72c1eb3f2..0000000000000 --- 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 2129096da2f55..edb6a1ddda4fa 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 ad524914b49e7..e3f8c0acbedbf 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 52dbb5b95bb38..124fa674fdd65 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 d09790a81a08e..27753eb9dc463 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 63d4094cd0d31..250e214a60df4 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 d27cef022d426..ede9910d74847 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 5b8eeed8fb968..b906f6dd1dd55 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 1835095cf7270..e736667d5759c 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 bdf5b59894439..67d0b69f4650a 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 bde78de481012..ae542870d9fd2 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 aee7bd1ba1d01..ecfc93edb0953 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 4905b161bfb48..d8d7e9643b621 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 4dbc90d407e02..0e8d509a16842 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 e8b8db685b20b..78c3da786c37c 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 1288da8670b81..f9771246f9897 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 30d43de1155c2..ad0e79cb27799 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 554863ae4fb18..4ca4e2bcc832f 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 b5885aa441463..dbc9c12f84578 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 a1570583245a6..5ac61ededc10e 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 eddf9c7e12ba0..c5d949d7b27de 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 77466d61d92cb..b63f027af7e66 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 d97cd5ffca6c0..2ce56467312ee 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 93ec956f5801c..77ae96532f614 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 c43c0df75daba..c5e3b308ac163 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 5789938d187b2..f4612cc98a228 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 c09560e08c67b..9d5d8ec18a61f 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 f345657e739ea..87edc6b4a71eb 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 d2b45c8ccbd93..aeac16cc8a3e3 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 f31c04b69ff90..f710dff8b9ae0 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 34fa314ba94c9..cff2dc92f7efc 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 6f4ac91fdbb0d..beda0b0f1a436 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 ea6411a9c1b01..d1ab867ff99fc 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 0000000000000..f7a2d584d9574 --- /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 0000000000000..65951da0a364f --- /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 0000000000000..169c6be772a43 --- /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 0000000000000..cabac9b71bdba --- /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 16a9604bcd3e5..a6ee1c6b69046 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 9ae64acf6c4a1..3ebcef7a95527 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 9e1eea15dbde1..fb68d855832b1 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 2ce63784ebe8f..06d433edad42b 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 03a1371cc55a0..cbd01b11ed2f7 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 d94304d0d3d97..eb19d8957f60f 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 ac9244514c3f3..c88f9fc195c22 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 c9a37ee30aab6..86c75f8e7cdba 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 d5803c9cc7872..904561a033532 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 4346fa5c583c0..7b067412bff25 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 29568b9d1103d..6060f4c2b78fe 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 4c788138fbcb6..c4daccad716d8 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 8841251e4adcb..c21113ee6d25e 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 acb81f28e28d8..f4ffade26b797 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 8318fbfb3bcc0..62adfaa74ac4e 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 43b5ac5c81219..51124ef1cfb16 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 5aebe4d6a9dcb..c4d893af051b8 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 b18fe687c443b..6d44b881bd6d7 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 fec096d51aad5..316edb609fcb7 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 ef2fa458e2545..605bd74f414e7 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 476b048b4dfe3..4a66d54e373f4 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 c3b7049016fbc..ae5e060cffe21 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 a13cd81ad7682..60161a7458662 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 14bf584bb90a4..e7c9dc0d6351e 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 2675ad4beb3f7..a32714d556d8b 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 b22232add6d79..11d6cdd52efc7 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 d9aec48c39391..331265129628f 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 510e94e43f898..2b32a57540213 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 5f8214467fe21..9c1192ff34e3f 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 638dfb52557cc..326f83a03df37 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 d5bbf448a362b..729b7f127cae8 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 747f671557756..fa7e20a8325c6 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 3fc8972069239..1efe7da029557 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 6c89d93724f4f..17b2bda02be0e 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 68af2802904ce..e0c98bfef3eec 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 3cc2673bfc7b6..8acfad5f6afac 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 ff9efdfea3f53..caaeed7fb36e5 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 647a07d5eb0bd..c3f74ff2cd320 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 688d71229fd35..aeefc2ece9994 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 4a66623939d20..08d7eae49b844 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 debde4488ebd2..d08de6ab72cb1 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 0edd804c6ab2d..c64d82bfbd0cf 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 c506738d97ceb..855340f4d777c 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 fc2c36aa8c469..1bd6f4a16048b 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 671081c0a5db6..f51cc8bde9819 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 e4487ccfaf04d..3e79a551344ab 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 61340a8f82ed0..67976108e1a30 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 23803d0be4b9a..ddde7c0513ad2 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 6d425130a3127..0a8159354719a 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 6c7acbd2189eb..ecf2e5da4af27 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 b5640f82c8d83..d78f8fadd5add 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 3e8fed2c6859c..2571c0cab02db 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 217a666a675eb..668b7408726f8 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 28ea603c2a0cf..003000d2ecd98 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 2a0f1ffbc61db..6162e72b8f965 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 109babd37463e..7a2fbb516bdd6 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 cfb18b181e83a..0db417427fac2 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 82195e498beab..88dddbb4d360f 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 9db85509f58eb..d29ca1fac3eec 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 9bdaef92d3511..790710751fe9c 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 a7313560e61eb..dc553e7369084 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 5566846e3cf99..4d689f01a115b 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 fd6a10163cdaf..e05b29f1edc47 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 1e4a4bd109b6f..8a3ac17474029 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 11faa815e27b6..8b8da1d37a701 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 7edd5d032af17..7b43d9ceb741d 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 46ce8b0ecc714..ae4a5c1f876db 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 9d934eb465de2..43218cae6e140 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 8526995934ede..2c4c3352eac8d 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 628bdc0055ebb..276d43ecd997c 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 da1fe600a5c8d..462b82c3e0f53 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 485b2e00e50d6..54da0b05662ff 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 706f65cf35a32..09f8cedce45f6 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 3cce65745f6af..0c33a4613937a 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 4fcf518550a13..719e366899496 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 f02dc73befcfd..8ef46a0bb55be 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 bf0e2cbf09cbc..f671072c72cab 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 066c435eb304c..0751d4096b846 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 59c368078252a..68fb0e9fe210a 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 78cc03dc94502..ebcb88193513f 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 b1a47c8721902..a291fabf6ed43 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 9222dd6ba7c87..1ad70238d86db 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 8f77e9cf2b05a..dcbb5ee8efc04 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 38665a19128d0..143af0ca6cf1a 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 52e29c3e93ab1..92224b9f16101 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 355378adacb97..932722f5a57ef 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 ae3e6510aae3d..6461b625081a0 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 9891fff079374..9abb45ddd5789 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 a577b0544f381..18ab07b3a9954 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 90d151a2a37e3..f07898d4e7b78 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": { - "description": "Action schema", - "nullable": true, - "type": "object" + "allOf": [ + { + "$ref": "#/components/schemas/PluginJsonSchema" + } + ], + "description": "Action schema", + "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 d74c2dd3e2c97..365187e6a7699 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 9dd140fbc3689..077f7a6785b60 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 bd3f5b0d69c81..73ea7f6f45098 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 f2b6a7e805024..ae930762d003a 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 b632332069968..5be9ae29b91d0 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 bf2038048fc1e..7ac6e051f6b82 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 d13227555b2f1..fadc5103ebb0f 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 c6dab09a3ce51..23a1f8b98c991 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 c2f6aeacefae0..0bfb4238987e6 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 69bf1f6443c30..4a8d4b35826c4 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 7dd145ff5cc53..a61397e75cd78 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 66598b992018d..3e11b628e3101 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 094028687e31a..07c0149463a9b 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 820819ee6ec48..4ed32ee271774 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 a64aee2912667..e9886ebb07c00 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 2c507a634fc2a..0661e9121bb32 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 a28ac9b659b15..c6c0a1c91fb3a 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 adbc8be0f3fcd..4df247031a90c 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 c1f19ddd6606e..07b0d7199f4b8 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 bbd1241dc5846..a07dee64ad2f2 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 60fc3d65aea68..edd0f27980d72 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 6d0276c6a37b5..f4c18235e4b9f 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 edda974476dc8..048f94df5abcb 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 4f339624e68fe..f8065ffd2cf3c 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 6464d88508cb6..7b8ba34c910f2 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 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; -} - -export class ActivityStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of comments' }) - comments!: number; - - @ApiProperty({ type: 'integer', description: 'Number of likes' }) - likes!: number; -} - -export class ActivityDto { - @ValidateUUID({ description: 'Album ID' }) - albumId!: string; - - @ValidateUUID({ optional: true, description: 'Asset ID (if activity is for an asset)' }) - assetId?: string; +export enum ReactionType { + COMMENT = 'comment', + LIKE = 'like', } +const ReactionTypeSchema = z.enum(ReactionType).describe('Reaction type').meta({ id: 'ReactionType' }); -export class ActivitySearchDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Filter by activity type', optional: true }) - type?: ReactionType; +export type MaybeDuplicate = { duplicate: boolean; value: T }; - @ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', description: 'Filter by activity level', optional: true }) - level?: ReactionLevel; +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' }); - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} +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' }); -const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT; +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 ActivityCreateDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type (like or comment)' }) - type!: ReactionType; +const ActivitySearchSchema = ActivitySchema.extend({ + type: ReactionTypeSchema.optional(), + level: ReactionLevelSchema.optional(), + userId: z.uuidv4().optional().describe('Filter by user ID'), +}).describe('Activity search'); - @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 b270125b36513..1514809838751 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; -} - -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; + .meta({ id: 'AlbumInfoDto' }); - @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; - - @ValidateUUID({ optional: true, description: 'Filter albums containing this asset ID (ignores shared parameter)' }) - assetId?: string; -} + .meta({ id: 'AlbumUserAddDto' }); -export class AlbumStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of owned albums' }) - owned!: number; - - @ApiProperty({ type: 'integer', description: 'Number of shared albums' }) - shared!: number; +const AddUsersSchema = z + .object({ + albumUsers: z.array(AlbumUserAddSchema).min(1).describe('Album users to add'), + }) + .meta({ id: 'AddUsersDto' }); - @ApiProperty({ type: 'integer', description: 'Number of non-shared albums' }) - notShared!: number; -} +const AlbumUserCreateSchema = z + .object({ + userId: z.uuidv4().describe('User ID'), + role: AlbumUserRoleSchema, + }) + .meta({ id: 'AlbumUserCreateDto' }); + +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' }); -export class UpdateAlbumUserDto { - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} +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 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 AlbumsAddAssetsResponseSchema = z + .object({ + success: z.boolean().describe('Operation success'), + error: BulkIdErrorReasonSchema.optional(), + }) + .meta({ id: 'AlbumsAddAssetsResponseDto' }); + +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' }); + +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' }); -export class ContributorCountResponseDto { - @ApiProperty({ description: 'User ID' }) - userId!: string; +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' }); - @ApiProperty({ type: 'integer', description: 'Number of assets contributed' }) - assetCount!: number; -} +const UpdateAlbumUserSchema = z + .object({ + role: AlbumUserRoleSchema, + }) + .meta({ id: 'UpdateAlbumUserDto' }); -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 AlbumUserResponseSchema = z + .object({ + user: UserResponseSchema, + role: AlbumUserRoleSchema, + }) + .meta({ id: 'AlbumUserResponseDto' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Type(() => ContributorCountResponseDto) - contributorCounts?: ContributorCountResponseDto[]; -} +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 273082c41b597..8c1ffb53ca50d 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[]; -} +const APIKeyCreateSchema = z + .object({ + name: z.string().optional().describe('API key name'), + permissions: z.array(PermissionSchema).min(1).describe('List of permissions'), + }) + .meta({ id: 'APIKeyCreateDto' }); -export class APIKeyUpdateDto { - @ApiPropertyOptional({ description: 'API key name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; +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' }); - @ValidateEnum({ - enum: Permission, - name: 'Permission', - description: 'List of permissions', - each: true, - optional: true, +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'), }) - @ArrayMinSize(1) - permissions?: Permission[]; -} + .meta({ id: 'APIKeyResponseDto' }); -export class APIKeyResponseDto { - @ApiProperty({ description: 'API key ID' }) - id!: string; - @ApiProperty({ description: 'API key name' }) - name!: string; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; - @ValidateEnum({ enum: Permission, name: 'Permission', each: true, description: 'List of permissions' }) - permissions!: Permission[]; -} +const APIKeyCreateResponseSchema = z + .object({ + secret: z.string().describe('API key secret (only shown once)'), + apiKey: APIKeyResponseSchema, + }) + .meta({ id: 'APIKeyCreateResponseDto' }); -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; -} +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 1065d8485e804..346829e644e54 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 345c1bf4180b2..fa3c4727657bf 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 46558503793c3..6a4c55c5aabc7 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; - - @ApiProperty({ description: 'Device ID' }) - @IsNotEmpty() - @IsString() - deviceId!: string; - - @ValidateDate({ description: 'File creation date' }) - fileCreatedAt!: Date; - - @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 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' }), +}); + +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' }); + +const AssetMediaReplaceSchema = AssetMediaBaseSchema.meta({ id: 'AssetMediaReplaceDto' }); + +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[]; - - @ApiProperty({ type: 'string', format: 'binary', required: false, description: 'Sidecar file data' }) - [UploadFieldName.SIDECAR_DATA]?: any; -} + .meta({ id: 'AssetBulkUploadCheckItem' }); -export class AssetMediaReplaceDto extends AssetMediaBase {} - -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[]; -} +const AssetBulkUploadCheckSchema = z + .object({ + assets: z.array(AssetBulkUploadCheckItemSchema).describe('Assets to check'), + }) + .meta({ id: 'AssetBulkUploadCheckDto' }); -export class CheckExistingAssetsDto { - @ApiProperty({ description: 'Device asset IDs to check' }) - @ArrayNotEmpty() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - deviceAssetIds!: string[]; +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' }); - @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 2c2f57bbb28ba..a95d2f3c1e578 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.', - }) - 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', +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'), }) - 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', +export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema) {} + +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'), }) - 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; + .meta({ id: 'AssetStackResponseDto' }); - @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; -} +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 b7bd7a18e8d78..3adb9374751dd 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; - - @ApiProperty({ description: 'Asset description' }) - @Optional() - @IsString() - description?: string; -} - -export class AssetBulkUpdateDto extends UpdateAssetBase { - @ValidateUUID({ each: true, description: 'Asset IDs to update' }) - ids!: string[]; - - @ValidateString({ optional: true, nullable: true, description: 'Duplicate ID' }) - duplicateId?: string | null; - - @ApiProperty({ description: 'Relative time offset in seconds' }) - @IsNotSiblingOf(['dateTimeOriginal']) - @Optional() - @IsInt() - dateTimeRelative?: number; - - @ApiProperty({ description: 'Time zone (IANA timezone)' }) - @IsNotSiblingOf(['dateTimeOriginal']) - @IsTimeZone() - @Optional() - timeZone?: string; -} - -export class UpdateAssetDto extends UpdateAssetBase { - @ValidateUUID({ optional: true, nullable: true, description: 'Live photo video ID' }) - livePhotoVideoId?: string | null; -} - -export class RandomAssetsDto { - @ApiProperty({ description: 'Number of random assets to return' }) - @Optional() - @IsInt() - @IsPositive() - @Type(() => Number) - count?: number; -} + .meta({ id: 'DeviceIdDto' }); + +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' }, + ); + +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)'), +}); + +const AssetBulkUpdateSchema = AssetBulkUpdateBaseSchema.pipe( + IsNotSiblingOf(AssetBulkUpdateBaseSchema, 'dateTimeRelative', ['dateTimeOriginal']), +).meta({ id: 'AssetBulkUpdateDto' }); + +const UpdateAssetSchema = UpdateAssetBaseSchema.extend({ + livePhotoVideoId: z.uuidv4().nullish().describe('Live photo video ID'), +}).meta({ id: 'UpdateAssetDto' }); + +const RandomAssetsSchema = z + .object({ + count: z.coerce.number().min(1).optional().describe('Number of random assets to return'), + }) + .meta({ id: 'RandomAssetsDto' }); -export class AssetBulkDeleteDto extends BulkIdsDto { - @ValidateBoolean({ optional: true, description: 'Force delete even if in use' }) - force?: boolean; -} +const AssetBulkDeleteSchema = BulkIdsSchema.extend({ + force: z.boolean().optional().describe('Force delete even if in use'), +}).meta({ id: 'AssetBulkDeleteDto' }); -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; -} - -export class AssetStatsDto { - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Filter by visibility', optional: true }) - visibility?: AssetVisibility; - - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; +const AssetJobNameSchema = z.enum(AssetJobName).describe('Job name').meta({ id: 'AssetJobName' }); - @ValidateBoolean({ optional: true, description: 'Filter by trash status' }) - isTrashed?: boolean; -} - -export class AssetStatsResponseDto { - @ApiProperty({ description: 'Number of images', type: 'integer' }) - images!: number; - - @ApiProperty({ description: 'Number of videos', type: 'integer' }) - videos!: number; - - @ApiProperty({ description: 'Total number of assets', type: 'integer' }) - total!: number; -} - -export class AssetMetadataRouteParams { - @ValidateUUID({ description: 'Asset ID' }) - id!: string; - - @ValidateString({ description: 'Metadata key' }) - key!: string; -} +const AssetJobsSchema = AssetIdsSchema.extend({ + name: AssetJobNameSchema, +}).meta({ id: 'AssetJobsDto' }); -export class AssetMetadataUpsertDto { - @ApiProperty({ description: 'Metadata items to upsert' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetMetadataUpsertItemDto) - items!: AssetMetadataUpsertItemDto[]; -} - -export class AssetMetadataUpsertItemDto { - @ValidateString({ description: 'Metadata key' }) - key!: string; - - @ApiProperty({ description: 'Metadata value (object)' }) - @IsObject() - value!: object; -} - -export class AssetMetadataBulkUpsertDto { - @ApiProperty({ description: 'Metadata items to upsert' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetMetadataBulkUpsertItemDto) - items!: AssetMetadataBulkUpsertItemDto[]; -} - -export class AssetMetadataBulkUpsertItemDto { - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; - - @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; +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' }); - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; -} +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 AssetMetadataBulkResponseDto extends AssetMetadataResponseDto { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} +const AssetMetadataRouteParamsSchema = z + .object({ + id: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + }) + .meta({ id: 'AssetMetadataRouteParams' }); -export class AssetCopyDto { - @ValidateUUID({ description: 'Source asset ID' }) - sourceId!: string; +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' }); - @ValidateUUID({ description: 'Target asset ID' }) - targetId!: string; +const AssetMetadataUpsertSchema = z + .object({ + items: z.array(AssetMetadataUpsertItemSchema).describe('Metadata items to upsert'), + }) + .meta({ id: 'AssetMetadataUpsertDto' }); - @ValidateBoolean({ optional: true, description: 'Copy shared links', default: true }) - sharedLinks?: boolean; +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' }); - @ValidateBoolean({ optional: true, description: 'Copy album associations', default: true }) - albums?: boolean; +const AssetMetadataBulkUpsertSchema = z + .object({ + items: z.array(AssetMetadataBulkUpsertItemSchema).describe('Metadata items to upsert'), + }) + .meta({ id: 'AssetMetadataBulkUpsertDto' }); - @ValidateBoolean({ optional: true, description: 'Copy sidecar file', default: true }) - sidecar?: boolean; +const AssetMetadataBulkDeleteItemSchema = z + .object({ + assetId: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + }) + .meta({ id: 'AssetMetadataBulkDeleteItemDto' }); - @ValidateBoolean({ optional: true, description: 'Copy stack association', default: true }) - stack?: boolean; +const AssetMetadataBulkDeleteSchema = z + .object({ + items: z.array(AssetMetadataBulkDeleteItemSchema).describe('Metadata items to delete'), + }) + .meta({ id: 'AssetMetadataBulkDeleteDto' }); - @ValidateBoolean({ optional: true, description: 'Copy favorite status', default: true }) - favorite?: boolean; -} +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' }); + +const AssetMetadataBulkResponseSchema = AssetMetadataResponseSchema.extend({ + assetId: z.string().describe('Asset ID'), +}).meta({ id: 'AssetMetadataBulkResponseDto' }); + +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' }); -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 3df82f4ef405e..95d2bb126a028 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; - - @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 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' }); + +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; -} - -export class SignUpDto extends LoginCredentialDto { - @ApiProperty({ example: 'Admin', description: 'User name' }) - @IsString() - @IsNotEmpty() - name!: string; -} - -export class ChangePasswordDto { - @ApiProperty({ example: 'password', description: 'Current password' }) - @IsString() - @IsNotEmpty() - password!: string; - - @ApiProperty({ example: 'password', description: 'New password (min 8 characters)' }) - @IsString() - @IsNotEmpty() - @MinLength(8) - newPassword!: string; - - @ValidateBoolean({ optional: true, default: false, description: 'Invalidate all other sessions' }) - invalidateSessions?: boolean; -} - -export class PinCodeSetupDto { - @ApiProperty({ description: 'PIN code (4-6 digits)' }) - @PinCode() - pinCode!: string; -} - -export class PinCodeResetDto { - @ApiPropertyOptional({ description: 'New PIN code (4-6 digits)' }) - @PinCode({ optional: true }) - pinCode?: string; - - @ApiPropertyOptional({ description: 'User password (required if PIN code is not provided)' }) - @Optional() - @IsString() - @IsNotEmpty() - password?: string; -} - -export class SessionUnlockDto extends PinCodeResetDto {} - -export class PinCodeChangeDto extends PinCodeResetDto { - @ApiProperty({ description: 'New PIN code (4-6 digits)' }) - @PinCode() - newPinCode!: string; -} - -export class ValidateAccessTokenResponseDto { - @ApiProperty({ description: 'Authentication status' }) - authStatus!: boolean; -} - -export class OAuthCallbackDto { - @ApiProperty({ description: 'OAuth callback URL' }) - @IsNotEmpty() - @IsString() - url!: string; - - @ApiPropertyOptional({ description: 'OAuth state parameter' }) - @Optional() - @IsString() - state?: string; - - @ApiPropertyOptional({ description: 'OAuth code verifier (PKCE)' }) - @Optional() - @IsString() - codeVerifier?: string; -} - -export class OAuthConfigDto { - @ApiProperty({ description: 'OAuth redirect URI' }) - @IsNotEmpty() - @IsString() - redirectUri!: string; - - @ApiPropertyOptional({ description: 'OAuth state parameter' }) - @Optional() - @IsString() - state?: string; - - @ApiPropertyOptional({ description: 'OAuth code challenge (PKCE)' }) - @Optional() - @IsString() - codeChallenge?: string; -} - -export class OAuthAuthorizeResponseDto { - @ApiProperty({ description: 'OAuth authorization URL' }) - url!: string; -} - -export class AuthStatusResponseDto { - @ApiProperty({ description: 'Has PIN code set' }) - pinCode!: boolean; - @ApiProperty({ description: 'Has password set' }) - password!: boolean; - @ApiProperty({ description: 'Is elevated session' }) - isElevated!: boolean; - @ApiPropertyOptional({ description: 'Session expiration date' }) - expiresAt?: string; - @ApiPropertyOptional({ description: 'PIN expiration date' }) - pinExpiresAt?: string; -} +const LogoutResponseSchema = z + .object({ + successful: z.boolean().describe('Logout successful'), + redirectUri: z.string().describe('Redirect URI'), + }) + .meta({ id: 'LogoutResponseDto' }); + +const SignUpSchema = LoginCredentialSchema.extend({ + name: z.string().describe('User name').meta({ example: 'Admin' }), +}).meta({ id: 'SignUpDto' }); + +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' }); + +const PinCodeSetupSchema = z + .object({ + pinCode: z.string().regex(pinCodeRegex).describe('PIN code (4-6 digits)').meta({ example: '123456' }), + }) + .meta({ id: 'PinCodeSetupDto' }); + +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' }), +}); + +const SessionUnlockSchema = PinCodeResetSchema.meta({ id: 'SessionUnlockDto' }); + +const PinCodeChangeSchema = PinCodeResetSchema.extend({ + newPinCode: z.string().regex(pinCodeRegex).describe('New PIN code (4-6 digits)'), +}).meta({ id: 'PinCodeChangeDto' }); + +const ValidateAccessTokenResponseSchema = z + .object({ + authStatus: z.boolean().describe('Authentication status'), + }) + .meta({ id: 'ValidateAccessTokenResponseDto' }); + +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' }); + +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' }); + +const OAuthAuthorizeResponseSchema = z + .object({ + url: z.string().describe('OAuth authorization URL'), + }) + .meta({ id: 'OAuthAuthorizeResponseDto' }); + +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' }); + +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 1afe9f53ba99b..8c24173791ae0 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 c0554f83b7084..34dd8f2a62ea6 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 ef52a72bd0d43..b44a6a7afcf6f 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 40b1b74c70ab6..55427e36aa928 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 8217fec41c574..9f5b3521952cb 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; - - @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; -} +const MirrorAxisSchema = z.enum(['horizontal', 'vertical']).describe('Axis to mirror along').meta({ id: 'MirrorAxis' }); + +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'), + }) + .meta({ id: 'CropParameters' }); + +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)'); + +const actionParameterMap = { + [AssetEditAction.Crop]: CropParametersSchema, + [AssetEditAction.Rotate]: RotateParametersSchema, + [AssetEditAction.Mirror]: MirrorParametersSchema, +} as const; + +function getExpectedKeysByActionMessage(): string { + const expectedByAction = Object.entries(actionParameterMap) + .map(([action, schema]) => `${action}: [${Object.keys(schema.shape).join(', ')}]`) + .join('; '); -export class RotateParameters { - @IsAxisAlignedRotation() - @ApiProperty({ description: 'Rotation angle in degrees' }) - angle!: number; + return `Invalid parameters for action, expected keys by action: ${expectedByAction}`; } -export class MirrorParameters { - @IsEnum(MirrorAxis) - @ApiProperty({ enum: MirrorAxis, enumName: 'MirrorAxis', description: 'Axis to mirror along' }) - axis!: MirrorAxis; +function isParametersValidForAction(edit: z.infer): boolean { + return actionParameterMap[edit.action].safeParse(edit.parameters).success; } -export type AssetEditParameters = CropParameters | RotateParameters | MirrorParameters; -export type AssetEditActionItem = - | { - action: AssetEditAction.Crop; - parameters: CropParameters; - } - | { - action: AssetEditAction.Rotate; - parameters: RotateParameters; +const AssetEditActionItemSchema = z + .object({ + action: AssetEditActionSchema, + parameters: AssetEditParametersSchema, + }) + .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(', ')}`, + }); } - | { - 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), - })), }) - @ValidateNested() - @Type((options) => actionParameterMap[options?.object.action as keyof AssetEditActionParameter]) - parameters!: AssetEditActionItem['parameters']; -} + .meta({ id: 'AssetEditActionItemDto' }); -export class AssetEditActionItemResponseDto extends AssetEditActionItemDto { - @ValidateUUID() - id!: string; -} +export type AssetEditActionItem = z.infer; +export type AssetEditParameters = AssetEditActionItem['parameters']; -export type AssetEditActionParameter = typeof actionParameterMap; -const actionParameterMap = { - [AssetEditAction.Crop]: CropParameters, - [AssetEditAction.Rotate]: RotateParameters, - [AssetEditAction.Mirror]: MirrorParameters, -}; - -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 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; } -export class AssetEditsResponseDto { - @ValidateUUID({ description: 'Asset ID these edits belong to' }) - assetId!: string; +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' }); - @ApiProperty({ - description: 'List of edit actions applied to the asset', +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'), }) - edits!: AssetEditActionItemResponseDto[]; -} + .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 bdcf3614fd63c..fc30875b5abd3 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; - - @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 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(); + +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; - - @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; -} + .pipe(z.union([z.undefined(), IsIPRange({ requireCIDR: false })])); + +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 165ecde4db548..c3e1ab36c8375 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 ef34a417203ca..325dae4d2e895 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 3f71b8a0ed692..aafdd9f793417 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'; - -export class CreateLibraryDto { - @ValidateUUID({ description: 'Owner user ID' }) - ownerId!: string; - - @ApiPropertyOptional({ description: 'Library name' }) - @IsString() - @Optional() - @IsNotEmpty() - name?: string; - - @ApiPropertyOptional({ description: 'Import paths (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @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[]; -} +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; + +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'); + +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' }); + +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[]; - - @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) - @Optional() - @IsNotEmpty({ each: true }) - @IsString({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - exclusionPatterns?: string[]; -} - -export class ValidateLibraryResponseDto { - @ApiPropertyOptional({ description: 'Validation results for import paths' }) - importPaths?: ValidateLibraryImportPathResponseDto[]; -} - -export class ValidateLibraryImportPathResponseDto { - @ApiProperty({ description: 'Import path' }) - importPath!: string; - @ApiProperty({ description: 'Is valid' }) - isValid: boolean = false; - @ApiPropertyOptional({ description: 'Validation message' }) - message?: string; -} - -export class LibrarySearchDto { - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} - -export class LibraryResponseDto { - @ApiProperty({ description: 'Library ID' }) - id!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ApiProperty({ description: 'Library name' }) - name!: string; - - @ApiProperty({ type: 'integer', 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; -} +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' }); + +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' }); + +const ValidateLibraryResponseSchema = z + .object({ + importPaths: z + .array(ValidateLibraryImportPathResponseSchema) + .optional() + .describe('Validation results for import paths'), + }) + .meta({ id: 'ValidateLibraryResponseDto' }); + +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' }); + +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 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 14232940b698c..a68905fb47997 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 f31d9ffa231b5..9b1c0b63c0722 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'; - -export class SetMaintenanceModeDto { - @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' }) - action!: MaintenanceAction; - - @ValidateIf((o) => o.action === MaintenanceAction.RestoreDatabase) - @ValidateString({ description: 'Restore backup filename' }) - restoreBackupFilename?: string; -} - -export class MaintenanceLoginDto { - @ValidateString({ optional: true, description: 'Maintenance token' }) - token?: string; -} - -export class MaintenanceAuthDto { - @ApiProperty({ description: 'Maintenance username' }) - username!: string; -} - -export class MaintenanceStatusResponseDto { - active!: boolean; - - @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' }) - action!: MaintenanceAction; - - 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[]; -} +import { createZodDto } from 'nestjs-zod'; +import { MaintenanceAction, MaintenanceActionSchema, StorageFolderSchema } from 'src/enum'; +import z from 'zod'; + +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' }); + +const MaintenanceLoginSchema = z + .object({ + token: z.string().optional().describe('Maintenance token'), + }) + .meta({ id: 'MaintenanceLoginDto' }); + +const MaintenanceAuthSchema = z + .object({ + username: z.string().describe('Maintenance username'), + }) + .meta({ id: 'MaintenanceAuthDto' }); + +const MaintenanceStatusResponseSchema = z + .object({ + active: z.boolean(), + action: MaintenanceActionSchema, + progress: z.number().optional(), + task: z.string().optional(), + error: z.string().optional(), + }) + .meta({ id: 'MaintenanceStatusResponseDto' }); + +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' }); + +const MaintenanceDetectInstallResponseSchema = z + .object({ + storage: z.array(MaintenanceDetectInstallStorageFolderSchema), + }) + .meta({ id: 'MaintenanceDetectInstallResponseDto' }); + +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 d8db175c289ca..6a4776d49d42e 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'; - -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; - - @ApiProperty({ format: 'double', description: 'Longitude (-180 to 180)' }) - @Type(() => Number) - @IsLongitude({ message: ({ property }) => `${property} must be a number between -180 and 180` }) - lon!: number; -} - -export class MapReverseGeocodeResponseDto { - @ApiProperty({ description: 'City name' }) - city!: string | null; - - @ApiProperty({ description: 'State/Province name' }) - state!: string | null; - - @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; -} +import { createZodDto } from 'nestjs-zod'; +import { isoDatetimeToDate, latitudeSchema, longitudeSchema, stringToBool } from 'src/validation'; +import z from 'zod'; + +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' }); + +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' }); + +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' }); + +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' }); + +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 edf65ef583dbf..334520dded7f1 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'; - -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; - } +import { AssetOrderWithRandomSchema, MemoryType, MemoryTypeSchema } from 'src/enum'; +import { isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; + +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(), + }) + .meta({ id: 'MemorySearchDto' }); - default: { - return Object; - } - } +const OnThisDaySchema = z + .object({ + year: z.int().min(1000).max(9999).describe('Year for on this day memory'), }) - data!: MemoryData; + .meta({ id: 'OnThisDayDto' }); - @ValidateDate({ description: 'Memory date' }) - memoryAt!: Date; +type MemoryData = z.infer; - @ValidateDate({ - optional: true, - description: 'Date when memory should be shown', - history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), +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'), }) - showAt?: Date; - - @ValidateDate({ - optional: true, - description: 'Date when memory should be hidden', - history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), + .meta({ id: 'MemoryUpdateDto' }); + +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()), }) - hideAt?: Date; - - @ValidateUUID({ optional: true, each: true, description: 'Asset IDs to associate with memory' }) - assetIds?: string[]; -} + .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' }); + +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 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[]; -} +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 a75808f95a59b..2ba6f0c365abc 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', - }) - maxDistance!: number; -} - -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; - - @IsNumber() - @Min(0.1) - @Max(2) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Maximum distance threshold for face recognition', - }) - maxDistance!: number; - - @IsNumber() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Minimum number of faces required for recognition' }) - minFaces!: number; -} - -export class OcrConfig extends ModelConfig { - @IsNumber() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Maximum resolution for OCR processing' }) - maxResolution!: number; - - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for text detection' }) - minDetectionScore!: number; - - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Minimum confidence score for text recognition', +const TaskConfigSchema = z + .object({ + enabled: z.boolean().describe('Whether the task is enabled'), }) - minRecognitionScore!: number; -} + .meta({ id: 'TaskConfig' }); + +const ModelConfigSchema = TaskConfigSchema.extend({ + modelName: z.string().describe('Name of the model to use'), +}); + +export const CLIPConfigSchema = ModelConfigSchema.meta({ id: 'CLIPConfig' }); + +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 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' }); + +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' }); + +export class CLIPConfig extends createZodDto(CLIPConfigSchema) {} diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts index 87a15f29e31ee..f474cfc0a1ae8 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'; - -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', +import { createZodDto } from 'nestjs-zod'; +import { NotificationLevel, NotificationLevelSchema, NotificationType, NotificationTypeSchema } from 'src/enum'; +import { isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; + +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; - - @ValidateBoolean({ optional: true, description: 'Filter by unread status' }) - unread?: boolean; -} + .meta({ id: 'TemplateResponseDto' }); -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; - - @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true, description: 'Notification type' }) - type?: NotificationType; - - @ValidateString({ description: 'Notification title' }) - title!: string; - - @ValidateString({ optional: true, nullable: true, description: 'Notification description' }) - description?: string | null; - - @ApiPropertyOptional({ description: 'Additional notification data' }) - @Optional({ nullable: true }) - data?: any; - - @ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' }) - readAt?: Date | null; - - @ValidateUUID({ description: 'User ID to send notification to' }) - userId!: string; -} - -export class NotificationUpdateDto { - @ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' }) - readAt?: Date | null; -} - -export class NotificationUpdateAllDto { - @ValidateUUID({ each: true, description: 'Notification IDs to update' }) - @ArrayMinSize(1) - ids!: string[]; + .meta({ id: 'TemplateDto' }); + +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' }); + +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' }); + +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' }); - @ValidateDate({ optional: true, nullable: true, description: 'Date when notifications were read' }) - readAt?: Date | null; -} +const NotificationUpdateSchema = z + .object({ + readAt: isoDatetimeToDate.nullish().describe('Date when notification was read'), + }) + .meta({ id: 'NotificationUpdateDto' }); -export class NotificationDeleteAllDto { - @ValidateUUID({ each: true, description: 'Notification IDs to delete' }) - @ArrayMinSize(1) - ids!: string[]; -} +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' }); -export type MapNotification = { +const NotificationDeleteAllSchema = z + .object({ + ids: z.array(z.uuidv4()).min(1).describe('Notification IDs to delete'), + }) + .meta({ id: 'NotificationDeleteAllDto' }); + +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) {} + +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 1e838d0ec0d35..62e32ed4afeeb 100644 --- a/server/src/dtos/ocr.dto.ts +++ b/server/src/dtos/ocr.dto.ts @@ -1,42 +1,22 @@ -import { ApiProperty } from '@nestjs/swagger'; - -export class AssetOcrResponseDto { - @ApiProperty({ type: 'string', format: 'uuid' }) - id!: string; - - @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; -} +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; + +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' }); + +export class AssetOcrResponseDto extends createZodDto(AssetOcrResponseSchema) {} diff --git a/server/src/dtos/onboarding.dto.ts b/server/src/dtos/onboarding.dto.ts index d2781c6b90a2f..ae26f5e88ab2f 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 5b949326a4a5e..049cf2b25ef91 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 477166d3d5243..1f8f080905ed5 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'; - -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'), +import { emptyStringToNull, hexColor, stringToBool } from 'src/validation'; +import z from 'zod'; + +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; -} - -export class PersonWithFacesResponseDto extends PersonResponseDto { - @ApiProperty({ description: 'Face detections' }) - faces!: AssetFaceWithoutPersonResponseDto[]; -} + .meta({ id: 'PersonCreateDto' }); -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; -} - -export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto { - @ApiProperty({ description: 'Person associated with face' }) - person!: PersonResponseDto | null; -} +const PersonUpdateSchema = PersonCreateSchema.extend({ + featureFaceAssetId: z.uuidv4().optional().describe('Asset ID used for feature face thumbnail'), +}).meta({ id: 'PersonUpdateDto' }); -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; -} +const PeopleUpdateItemSchema = PersonUpdateSchema.extend({ + id: z.string().describe('Person ID'), +}).meta({ id: 'PeopleUpdateItem' }); -export class AssetFaceUpdateItem { - @ValidateUUID({ description: 'Person ID' }) - personId!: string; - - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; -} +const PeopleUpdateSchema = z + .object({ + people: z.array(PeopleUpdateItemSchema).describe('People to update'), + }) + .meta({ id: 'PeopleUpdateDto' }); -export class AssetFaceCreateDto extends AssetFaceUpdateItem { - @ApiProperty({ type: 'integer', description: 'Image width in pixels' }) - @IsNotEmpty() - @IsNumber() - imageWidth!: number; +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' }); - @ApiProperty({ type: 'integer', description: 'Image height in pixels' }) - @IsNotEmpty() - @IsNumber() - imageHeight!: number; +class AssetFaceWithoutPersonResponseDto extends createZodDto(AssetFaceWithoutPersonResponseSchema) {} - @ApiProperty({ type: 'integer', description: 'Face bounding box X coordinate' }) - @IsNotEmpty() - @IsNumber() - x!: number; +export const PersonWithFacesResponseSchema = PersonResponseSchema.extend({ + faces: z.array(AssetFaceWithoutPersonResponseSchema), +}).meta({ id: 'PersonWithFacesResponseDto' }); - @ApiProperty({ type: 'integer', description: 'Face bounding box Y coordinate' }) - @IsNotEmpty() - @IsNumber() - y!: number; +export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema) {} - @ApiProperty({ type: 'integer', description: 'Face bounding box width' }) - @IsNotEmpty() - @IsNumber() - width!: number; +const AssetFaceResponseSchema = AssetFaceWithoutPersonResponseSchema.extend({ + person: PersonResponseSchema.nullable(), +}).meta({ id: 'AssetFaceResponseDto' }); - @ApiProperty({ type: 'integer', description: 'Face bounding box height' }) - @IsNotEmpty() - @IsNumber() - height!: number; -} +export class AssetFaceResponseDto extends createZodDto(AssetFaceResponseSchema) {} -export class AssetFaceDeleteDto { - @ApiProperty({ description: 'Force delete even if person has other faces' }) - @IsNotEmpty() - force!: boolean; -} +const AssetFaceUpdateItemSchema = z + .object({ + personId: z.uuidv4().describe('Person ID'), + assetId: z.uuidv4().describe('Asset ID'), + }) + .meta({ id: 'AssetFaceUpdateItem' }); -export class PersonStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of assets' }) - assets!: number; -} +const AssetFaceUpdateSchema = z + .object({ + data: z.array(AssetFaceUpdateItemSchema).describe('Face update items'), + }) + .meta({ id: 'AssetFaceUpdateDto' }); -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[]; +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' }); - // TODO: make required after a few versions - @Property({ - description: 'Whether there are more pages', - history: new HistoryBuilder().added('v1.110.0').stable('v2'), +const PersonStatisticsResponseSchema = z + .object({ + assets: z.int().describe('Number of assets'), }) - hasNextPage?: boolean; -} + .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 d5d1c529970e2..30aa8c0a68f40 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'; - -class PluginManifestWasmDto { - @ApiProperty({ description: 'WASM file path' }) - @IsString() - @IsNotEmpty() - path!: string; -} - -class PluginManifestFilterDto { - @ApiProperty({ description: 'Filter method name' }) - @IsString() - @IsNotEmpty() - methodName!: string; - - @ApiProperty({ description: 'Filter title' }) - @IsString() - @IsNotEmpty() - title!: string; - - @ApiProperty({ description: 'Filter description' }) - @IsString() - @IsNotEmpty() - description!: string; - - @ApiProperty({ description: 'Supported contexts', enum: PluginContext, isArray: true }) - @IsArray() - @ArrayMinSize(1) - @IsEnum(PluginContext, { each: true }) - supportedContexts!: PluginContext[]; - - @ApiPropertyOptional({ description: 'Filter schema' }) - @IsObject() - @IsOptional() - schema?: JSONSchema; -} - -class PluginManifestActionDto { - @ApiProperty({ description: 'Action method name' }) - @IsString() - @IsNotEmpty() - methodName!: string; - - @ApiProperty({ description: 'Action title' }) - @IsString() - @IsNotEmpty() - title!: string; - - @ApiProperty({ description: 'Action description' }) - @IsString() - @IsNotEmpty() - description!: string; - - @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', +import { createZodDto } from 'nestjs-zod'; +import { PluginContextSchema } from 'src/enum'; +import { JSONSchemaSchema } from 'src/types/plugin-schema.types'; +import z from 'zod'; + +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-]+)*))?$/; + +const PluginManifestWasmSchema = z + .object({ + path: z.string().describe('WASM file path'), }) - name!: string; - - @ApiProperty({ description: 'Plugin version (semver)' }) - @IsString() - @IsNotEmpty() - @IsSemVer() - version!: string; - - @ApiProperty({ description: 'Plugin title' }) - @IsString() - @IsNotEmpty() - title!: string; - - @ApiProperty({ description: 'Plugin description' }) - @IsString() - @IsNotEmpty() - description!: string; - - @ApiProperty({ description: 'Plugin author' }) - @IsString() - @IsNotEmpty() - author!: string; - - @ApiProperty({ description: 'WASM configuration' }) - @ValidateNested() - @Type(() => PluginManifestWasmDto) - wasm!: PluginManifestWasmDto; - - @ApiPropertyOptional({ description: 'Plugin filters' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => PluginManifestFilterDto) - @IsOptional() - filters?: PluginManifestFilterDto[]; + .meta({ id: 'PluginManifestWasmDto' }); + +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' }); + +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' }); + +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' }); - @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 de1f1b28d43af..2f928841cb419 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; -} - -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 PluginTriggerResponseSchema = z + .object({ + type: PluginTriggerTypeSchema, + contextType: PluginContextSchema, + }) + .meta({ id: 'PluginTriggerResponseDto' }); -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 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' }); - @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' }) - supportedContexts!: PluginContextType[]; - @ApiProperty({ description: 'Filter schema' }) - schema!: JSONSchema | null; -} +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' }); -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; +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' }); - @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 class PluginTriggerResponseDto extends createZodDto(PluginTriggerResponseSchema) {} +export class PluginResponseDto extends createZodDto(PluginResponseSchema) {} -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 993160a03b755..dbbcec2da5304 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'; - -export class QueueStatusLegacyDto { - @ApiProperty({ description: 'Whether the queue is currently active (has running jobs)' }) - isActive!: boolean; - @ApiProperty({ description: 'Whether the queue is paused' }) - isPaused!: boolean; -} - -export class QueueResponseLegacyDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - queueStatus!: QueueStatusLegacyDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - jobCounts!: QueueStatisticsDto; -} - -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; -} +import z from 'zod'; + +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' }); + +const QueueResponseLegacySchema = z + .object({ + queueStatus: QueueStatusLegacySchema, + jobCounts: QueueStatisticsSchema, + }) + .meta({ id: 'QueueResponseLegacyDto' }); + +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 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 789358144418d..2147f60bdeeab 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 196e72c37e2d5..43da0b8709224 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'; - -class BaseSearchDto { - @ValidateUUID({ optional: true, nullable: true, description: 'Library ID to filter by' }) - libraryId?: string | null; - - @ApiPropertyOptional({ description: 'Device ID to filter by' }) - @IsString() - @IsNotEmpty() - @Optional() - deviceId?: string; - - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true, description: 'Asset type filter' }) - type?: AssetType; - - @ValidateBoolean({ optional: true, description: 'Filter by encoded status' }) - isEncoded?: boolean; - - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; - - @ValidateBoolean({ optional: true, description: 'Filter by motion photo status' }) - isMotion?: boolean; - - @ValidateBoolean({ optional: true, description: 'Filter by offline status' }) - isOffline?: boolean; - - @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.'), +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'; + +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'), +}); + +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'), +}); + +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' }); + +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' }); + +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' }); + +const StatisticsSearchSchema = BaseSearchSchema.extend({ + description: z.string().trim().optional().describe('Filter by description text'), +}).meta({ id: 'StatisticsSearchDto' }); + +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' }); + +const SearchPlacesSchema = z + .object({ + name: z.string().describe('Place name to search for'), }) - @Optional({ nullable: true }) - @IsInt() - @Max(5) - @Min(-1) - rating?: number | null; - - @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; + .meta({ id: 'SearchPlacesDto' }); - @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; - - @ApiPropertyOptional({ type: 'number', description: 'Page number', minimum: 1 }) - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - page?: number; -} - -export class StatisticsSearchDto extends BaseSearchDto { - @ValidateString({ optional: true, trim: true, description: 'Filter by description text' }) - description?: string; -} - -export class SmartSearchDto extends BaseSearchWithResultsDto { - @ValidateString({ optional: true, trim: true, description: 'Natural language search query' }) - query?: string; - - @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; + .meta({ id: 'SearchPeopleDto' }); + +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' }); - @ValidateBoolean({ optional: true, description: 'Include hidden people' }) - withHidden?: boolean; +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 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; -} +const SearchSuggestionTypeSchema = z + .enum(SearchSuggestionType) + .describe('Suggestion type') + .meta({ id: 'SearchSuggestionType' }); + +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' }); + +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; -} - -class SearchFacetResponseDto { - @ApiProperty({ description: 'Facet field name' }) - fieldName!: string; - @ApiProperty({ description: 'Facet counts' }) - counts!: SearchFacetCountResponseDto[]; -} - -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 SearchFacetResponseSchema = z + .object({ + fieldName: z.string().describe('Facet field name'), + counts: z.array(SearchFacetCountResponseSchema), + }) + .meta({ id: 'SearchFacetResponseDto' }); + +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' }); + +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 626c94e40a1b3..bd420327713ef 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'; - -export class ServerPingResponse { - @ApiResponseProperty({ type: String, example: 'pong' }) - res!: string; -} - -export class ServerAboutResponseDto { - @ApiProperty({ description: 'Server version' }) - version!: string; - @ApiProperty({ description: 'URL to version information' }) - versionUrl!: string; - - @ApiPropertyOptional({ description: 'Repository name' }) - repository?: string; - @ApiPropertyOptional({ description: 'Repository URL' }) - repositoryUrl?: string; - - @ApiPropertyOptional({ description: 'Source reference (branch/tag)' }) - sourceRef?: string; - @ApiPropertyOptional({ description: 'Source commit hash' }) - sourceCommit?: string; - @ApiPropertyOptional({ description: 'Source URL' }) - sourceUrl?: string; - - @ApiPropertyOptional({ description: 'Build identifier' }) - build?: string; - @ApiPropertyOptional({ description: 'Build URL' }) - buildUrl?: string; - @ApiPropertyOptional({ description: 'Build image name' }) - buildImage?: string; - @ApiPropertyOptional({ description: 'Build image URL' }) - buildImageUrl?: string; - - @ApiPropertyOptional({ description: 'Node.js version' }) - nodejs?: string; - @ApiPropertyOptional({ description: 'FFmpeg version' }) - ffmpeg?: string; - @ApiPropertyOptional({ description: 'ImageMagick version' }) - imagemagick?: string; - @ApiPropertyOptional({ description: 'libvips version' }) - libvips?: string; - @ApiPropertyOptional({ description: 'ExifTool version' }) - exiftool?: string; - - @ApiProperty({ description: 'Whether the server is licensed' }) - licensed!: boolean; - - @ApiPropertyOptional({ description: 'Third-party source URL' }) - thirdPartySourceUrl?: string; - @ApiPropertyOptional({ description: 'Third-party bug/feature URL' }) - thirdPartyBugFeatureUrl?: string; - @ApiPropertyOptional({ description: 'Third-party documentation URL' }) - thirdPartyDocumentationUrl?: string; - @ApiPropertyOptional({ description: 'Third-party support URL' }) - thirdPartySupportUrl?: string; -} - -export class ServerApkLinksDto { - @ApiProperty({ description: 'APK download link for ARM64 v8a architecture' }) - arm64v8a!: string; - @ApiProperty({ description: 'APK download link for ARM EABI v7a architecture' }) - armeabiv7a!: string; - @ApiProperty({ description: 'APK download link for universal architecture' }) - universal!: string; - @ApiProperty({ description: 'APK download link for x86_64 architecture' }) - x86_64!: string; -} - -export class ServerStorageResponseDto { - @ApiProperty({ description: 'Total disk size (human-readable format)' }) - diskSize!: string; - @ApiProperty({ description: 'Used disk space (human-readable format)' }) - diskUse!: string; - @ApiProperty({ description: 'Available disk space (human-readable format)' }) - diskAvailable!: string; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Total disk size in bytes' }) - diskSizeRaw!: number; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Used disk space in bytes' }) - diskUseRaw!: number; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Available disk space in bytes' }) - diskAvailableRaw!: number; - - @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; +import { createZodDto } from 'nestjs-zod'; +import type { SemVer } from 'semver'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; + +const ServerPingResponseSchema = z + .object({ + res: z.string().meta({ example: 'pong' }), + }) + .meta({ id: 'ServerPingResponse' }); + +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' }); + +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' }); + +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' }); - static fromSemVer(value: SemVer) { - return { major: value.major, minor: value.minor, patch: value.patch }; - } -} +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' }); -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; -} +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' }); + +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' }); + +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 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)', +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'), }) - quotaSizeInBytes!: number | null; -} + .meta({ id: 'ServerMediaTypesResponseDto' }); -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, - }, - ], +const ServerThemeSchema = z + .object({ + customCss: z.string().describe('Custom CSS for theming'), }) - usageByUser: UsageByUserDto[] = []; -} + .meta({ id: 'ServerThemeDto' }); + +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' }); + +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' }); -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 ServerPingResponse extends createZodDto(ServerPingResponseSchema) {} +export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSchema) {} +export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {} +export class ServerStorageResponseDto extends createZodDto(ServerStorageResponseSchema) {} -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 ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) { + static fromSemVer(value: SemVer): z.infer { + return { major: value.major, minor: value.minor, patch: value.patch }; + } } -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 f918f0b3bb2c3..179a1dfb76a93 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 b2ecc70a3aecc..7dcec034dcbbf 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'; - -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'), +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'; + +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; -} - -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.', + .meta({ id: 'SharedLinkSearchDto' }); + +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; -} - -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'), + .meta({ id: 'SharedLinkCreateDto' }); + +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; - - @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; + .meta({ id: 'SharedLinkEditDto' }); - @ApiProperty({ description: 'Allow downloads' }) - allowDownload!: boolean; - @ApiProperty({ description: 'Show metadata' }) - showMetadata!: boolean; +const SharedLinkLoginSchema = z + .object({ + password: z.string().describe('Shared link password').meta({ example: 'password' }), + }) + .meta({ id: 'SharedLinkLoginDto' }); - @ApiProperty({ description: 'Custom URL slug' }) - slug!: string | null; -} +const SharedLinkPasswordSchema = z + .object({ + password: z.string().optional().describe('Link password'), + token: z.string().optional().describe('Access token'), + }) + .meta({ id: 'SharedLinkPasswordDto' }); + +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 a76b35e08e73d..48354cec6b5fb 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 9a1332d30312b..d7903ebb0cd16 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'; - -export class AssetFullSyncDto { - @ValidateUUID({ optional: true, description: 'Last asset ID (pagination)' }) - lastId?: string; - - @ValidateDate({ description: 'Sync assets updated until this date' }) - updatedUntil!: Date; - - @ApiProperty({ type: 'integer', description: 'Maximum number of assets to return' }) - @IsInt() - @IsPositive() - limit!: number; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; + +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' }); - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} +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' }); -export class AssetDeltaSyncDto { - @ValidateDate({ description: 'Sync assets updated after this date' }) - updatedAfter!: Date; +export class AssetFullSyncDto extends createZodDto(AssetFullSyncSchema) {} +export class AssetDeltaSyncDto extends createZodDto(AssetDeltaSyncSchema) {} - @ValidateUUID({ each: true, description: 'User IDs to sync' }) - userIds!: 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 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' }); -@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; -} +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() +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' }); -@ExtraModel() -export class SyncUserDeleteV1 { - @ApiProperty({ description: 'User ID' }) - userId!: string; -} +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' }); -@ExtraModel() -export class SyncPartnerV1 { - @ApiProperty({ description: 'Shared by ID' }) - sharedById!: string; - @ApiProperty({ description: 'Shared with ID' }) - sharedWithId!: string; - @ApiProperty({ description: 'In timeline' }) - inTimeline!: boolean; -} +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' }); -@ExtraModel() -export class SyncPartnerDeleteV1 { - @ApiProperty({ description: 'Shared by ID' }) - sharedById!: string; - @ApiProperty({ description: 'Shared with ID' }) - sharedWithId!: string; -} +const SyncAssetEditDeleteV1Schema = z + .object({ editId: z.string().describe('Edit ID') }) + .meta({ id: 'SyncAssetEditDeleteV1' }); @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 SyncAssetDeleteV1 extends createZodDto(SyncAssetDeleteV1Schema) {} @ExtraModel() -export class SyncAssetDeleteV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - +export class SyncAssetExifV1 extends createZodDto(SyncAssetExifV1Schema) {} @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 SyncAssetMetadataV1 extends createZodDto(SyncAssetMetadataV1Schema) {} +@ExtraModel() +class SyncAssetMetadataDeleteV1 extends createZodDto(SyncAssetMetadataDeleteV1Schema) {} +@ExtraModel() +export class SyncAssetEditV1 extends createZodDto(SyncAssetEditV1Schema) {} @ExtraModel() -export class SyncAssetEditV1 { - id!: string; - assetId!: string; +class SyncAssetEditDeleteV1 extends createZodDto(SyncAssetEditDeleteV1Schema) {} - @ValidateEnum({ enum: AssetEditAction, name: 'AssetEditAction' }) - action!: AssetEditAction; - parameters!: object; +const SyncAlbumDeleteV1Schema = z + .object({ albumId: z.string().describe('Album ID') }) + .meta({ id: 'SyncAlbumDeleteV1' }); - @ApiProperty({ type: 'integer' }) - sequence!: number; -} +const SyncAlbumUserDeleteV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + userId: z.string().describe('User ID'), + }) + .meta({ id: 'SyncAlbumUserDeleteV1' }); -@ExtraModel() -export class SyncAssetEditDeleteV1 { - editId!: string; -} +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' }); -@ExtraModel() -export class SyncAssetMetadataV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Key' }) - key!: string; - @ApiProperty({ description: 'Value' }) - value!: object; -} +const SyncAlbumToAssetV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncAlbumToAssetV1' }); -@ExtraModel() -export class SyncAssetMetadataDeleteV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Key' }) - key!: string; -} +const SyncAlbumToAssetDeleteV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncAlbumToAssetDeleteV1' }); @ExtraModel() -export class SyncAlbumDeleteV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; -} - +class SyncAlbumDeleteV1 extends createZodDto(SyncAlbumDeleteV1Schema) {} @ExtraModel() -export class SyncAlbumUserDeleteV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'User ID' }) - userId!: string; -} - +class SyncAlbumUserDeleteV1 extends createZodDto(SyncAlbumUserDeleteV1Schema) {} @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; -} - +class SyncAlbumUserV1 extends createZodDto(SyncAlbumUserV1Schema) {} @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; -} - +class SyncAlbumV1 extends createZodDto(SyncAlbumV1Schema) {} @ExtraModel() -export class SyncAlbumToAssetV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - +class SyncAlbumToAssetV1 extends createZodDto(SyncAlbumToAssetV1Schema) {} @ExtraModel() -export class SyncAlbumToAssetDeleteV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} +class SyncAlbumToAssetDeleteV1 extends createZodDto(SyncAlbumToAssetDeleteV1Schema) {} -@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; -} +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' }); -@ExtraModel() -export class SyncMemoryDeleteV1 { - @ApiProperty({ description: 'Memory ID' }) - memoryId!: string; -} +const SyncMemoryDeleteV1Schema = z + .object({ memoryId: z.string().describe('Memory ID') }) + .meta({ id: 'SyncMemoryDeleteV1' }); -@ExtraModel() -export class SyncMemoryAssetV1 { - @ApiProperty({ description: 'Memory ID' }) - memoryId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} +const SyncMemoryAssetV1Schema = z + .object({ + memoryId: z.string().describe('Memory ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncMemoryAssetV1' }); -@ExtraModel() -export class SyncMemoryAssetDeleteV1 { - @ApiProperty({ description: 'Memory ID' }) - memoryId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} +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' }); -@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; -} +const SyncUserMetadataDeleteV1Schema = z + .object({ + userId: z.string().describe('User ID'), + key: UserMetadataKeySchema, + }) + .meta({ id: 'SyncUserMetadataDeleteV1' }); -@ExtraModel() -export class SyncStackDeleteV1 { - @ApiProperty({ description: 'Stack ID' }) - stackId!: string; -} +const SyncAckV1Schema = z.object({}).meta({ id: 'SyncAckV1' }); +const SyncResetV1Schema = z.object({}).meta({ id: 'SyncResetV1' }); +const SyncCompleteV1Schema = z.object({}).meta({ id: 'SyncCompleteV1' }); @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; -} - +class SyncMemoryV1 extends createZodDto(SyncMemoryV1Schema) {} @ExtraModel() -export class SyncPersonDeleteV1 { - @ApiProperty({ description: 'Person ID' }) - personId!: string; -} - +class SyncMemoryDeleteV1 extends createZodDto(SyncMemoryDeleteV1Schema) {} @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; -} - +class SyncMemoryAssetV1 extends createZodDto(SyncMemoryAssetV1Schema) {} @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 SyncMemoryAssetDeleteV1 extends createZodDto(SyncMemoryAssetDeleteV1Schema) {} +@ExtraModel() +class SyncStackV1 extends createZodDto(SyncStackV1Schema) {} +@ExtraModel() +class SyncStackDeleteV1 extends createZodDto(SyncStackDeleteV1Schema) {} +@ExtraModel() +class SyncPersonV1 extends createZodDto(SyncPersonV1Schema) {} +@ExtraModel() +class SyncPersonDeleteV1 extends createZodDto(SyncPersonDeleteV1Schema) {} +@ExtraModel() +class SyncAssetFaceV1 extends createZodDto(SyncAssetFaceV1Schema) {} +@ExtraModel() +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; -} +const SyncStreamSchema = z + .object({ + types: z.array(SyncRequestTypeSchema).describe('Sync request types'), + reset: z.boolean().optional().describe('Reset sync state'), + }) + .meta({ id: 'SyncStreamDto' }); -export class SyncAckDto { - @ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', description: 'Sync entity type' }) - type!: SyncEntityType; - @ApiProperty({ description: 'Acknowledgment ID' }) - ack!: string; -} +const SyncAckSchema = z + .object({ + type: SyncEntityTypeSchema, + ack: z.string().describe('Acknowledgment ID'), + }) + .meta({ id: 'SyncAckDto' }); -export class SyncAckSetDto { - @ApiProperty({ description: 'Acknowledgment IDs (max 1000)' }) - @ArrayMaxSize(1000) - @IsString({ each: true }) - acks!: string[]; -} +const SyncAckSetSchema = z + .object({ + acks: z.array(z.string()).max(1000).describe('Acknowledgment IDs (max 1000)'), + }) + .meta({ id: 'SyncAckSetDto' }); -export class SyncAckDeleteDto { - @ValidateEnum({ - enum: SyncEntityType, - name: 'SyncEntityType', - optional: true, - each: true, - description: 'Sync entity types to delete acks for', +const SyncAckDeleteSchema = z + .object({ + types: z.array(SyncEntityTypeSchema).optional().describe('Sync entity types to delete acks for'), }) - types?: SyncEntityType[]; -} + .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 a214dbc467159..b5222fd883909 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'; - -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; - -export class DatabaseBackupConfig { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateIf(isDatabaseBackupEnabled) - @IsNotEmpty() - @IsCronExpression() - @IsString() - @ApiProperty({ description: 'Cron expression' }) - cronExpression!: string; - - @IsInt() - @IsPositive() - @IsNotEmpty() - @ApiProperty({ description: 'Keep last amount' }) - keepLastAmount!: number; -} - -export class SystemConfigBackupsDto { - @Type(() => DatabaseBackupConfig) - @ValidateNested() - @IsObject() - database!: DatabaseBackupConfig; -} - -export class SystemConfigFFmpegDto { - @IsInt() - @Min(0) - @Max(51) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'CRF' }) - crf!: number; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Threads' }) - threads!: number; - - @IsString() - @ApiProperty({ description: 'Preset' }) - preset!: string; - - @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', description: 'Target video codec' }) - targetVideoCodec!: VideoCodec; - - @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true, description: 'Accepted video codecs' }) - acceptedVideoCodecs!: VideoCodec[]; - - @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', description: 'Target audio codec' }) - targetAudioCodec!: AudioCodec; - - @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'; - } +import { isValidTime } from 'src/validation'; +import z from 'zod'; + +/** 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' }); - return value; +const JobSettingsSchema = z + .object({ + concurrency: z.int().min(1).describe('Concurrency'), }) - acceptedAudioCodecs!: AudioCodec[]; - - @ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true, description: 'Accepted containers' }) - acceptedContainers!: VideoContainer[]; - - @IsString() - @ApiProperty({ description: 'Target resolution' }) - targetResolution!: string; - - @IsString() - @ApiProperty({ description: 'Max bitrate' }) - maxBitrate!: string; - - @IsInt() - @Min(-1) - @Max(16) - @Type(() => Number) - @ApiProperty({ type: 'integer', 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', + .meta({ id: 'JobSettingsDto' }); + +const cronExpressionSchema = z + .string() + .regex(/(((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7}/, 'Invalid cron expression') + .describe('Cron expression'); + +const DatabaseBackupSchema = z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + keepLastAmount: z.number().min(1).describe('Keep last amount'), }) - accel!: TranscodeHardwareAcceleration; - - @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', + .meta({ id: 'DatabaseBackupConfig' }); + +const SystemConfigBackupsSchema = z.object({ database: DatabaseBackupSchema }).meta({ id: 'SystemConfigBackupsDto' }); + +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, }) - 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; + .meta({ id: 'SystemConfigFFmpegDto' }); + +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' }); - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Template' }) - template!: string; -} +const SystemConfigLibraryScanSchema = z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + }) + .meta({ id: 'SystemConfigLibraryScanDto' }); -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[]; -} +const SystemConfigLibraryWatchSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigLibraryWatchDto' }); -export class SystemConfigThemeDto { - @ApiProperty({ description: 'Custom CSS for theming' }) - @IsString() - customCss!: string; -} +const SystemConfigLibrarySchema = z + .object({ scan: SystemConfigLibraryScanSchema, watch: SystemConfigLibraryWatchSchema }) + .meta({ id: 'SystemConfigLibraryDto' }); -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; -} +const SystemConfigLoggingSchema = z + .object({ + enabled: configBool.describe('Enabled'), + level: LogLevelSchema, + }) + .meta({ id: 'SystemConfigLoggingDto' }); -class SystemConfigGeneratedFullsizeImageDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; +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' }); - @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat', description: 'Image format' }) - format!: ImageFormat; +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; + } - @IsInt() - @Min(1) - @Max(100) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Quality' }) - quality!: number; + 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; + } - @ValidateBoolean({ optional: true, default: false, description: 'Progressive' }) - progressive?: boolean; -} + return value; + }) + .meta({ + id: 'SystemConfigOAuthDto', + }); + +const SystemConfigPasswordLoginSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigPasswordLoginDto' }); + +const SystemConfigReverseGeocodingSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigReverseGeocodingDto' }); + +const SystemConfigFacesSchema = z + .object({ import: configBool.describe('Import') }) + .meta({ id: 'SystemConfigFacesDto' }); +const SystemConfigMetadataSchema = z.object({ faces: SystemConfigFacesSchema }).meta({ id: 'SystemConfigMetadataDto' }); + +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'), + }) + .meta({ id: 'SystemConfigServerDto' }); + +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'), + }) + .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' }); -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; -} +const SystemConfigNotificationsSchema = z + .object({ smtp: SystemConfigSmtpSchema }) + .meta({ id: 'SystemConfigNotificationsDto' }); -class SystemConfigTrashDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; +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' }); - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Days' }) - days!: number; -} +const SystemConfigTrashSchema = z + .object({ + enabled: configBool.describe('Enabled'), + days: z.int().min(0).describe('Days'), + }) + .meta({ id: 'SystemConfigTrashDto' }); -class SystemConfigUserDto { - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Delete delay' }) - deleteDelay!: number; -} +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 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; -} +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 0a4d55c9708cc..676a06f7748ce 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 ea85ea71f3366..67dbca9914fac 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; -} - -export class TagUpdateDto { - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - @Optional({ nullable: true, emptyToNull: true }) - @ValidateHexColor() - color?: string | null; -} - -export class TagUpsertDto { - @ApiProperty({ description: 'Tag names to upsert' }) - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - tags!: string[]; -} +const TagUpsertSchema = z + .object({ + tags: z.array(z.string()).describe('Tag names to upsert'), + }) + .meta({ id: 'TagUpsertDto' }); -export class TagBulkAssetsDto { - @ValidateUUID({ each: true, description: 'Tag IDs' }) - tagIds!: string[]; +const TagBulkAssetsSchema = z + .object({ + tagIds: z.array(z.uuidv4()).describe('Tag IDs'), + assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + }) + .meta({ id: 'TagBulkAssetsDto' }); - @ValidateUUID({ each: true, description: 'Asset IDs' }) - assetIds!: string[]; -} +const TagBulkAssetsResponseSchema = z + .object({ + count: z.int().describe('Number of assets tagged'), + }) + .meta({ id: 'TagBulkAssetsResponseDto' }); -export class TagBulkAssetsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of assets tagged' }) - count!: number; -} +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' }); -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 9ea9dc49ae222..af820e6868c9a 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'; - -export class TimeBucketDto { - @ValidateUUID({ optional: true, description: 'Filter assets by specific user ID' }) - userId?: string; - - @ValidateUUID({ optional: true, description: 'Filter assets belonging to a specific album' }) - albumId?: string; - - @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)', - }) - isFavorite?: boolean; - - @ValidateBoolean({ - optional: true, - description: 'Filter by trash status (true for trashed assets only, false for non-trashed only)', - }) - isTrashed?: boolean; - - @ValidateBoolean({ - optional: true, - description: 'Include stacked assets in the response. When true, only primary assets from stacks are returned.', - }) - withStacked?: boolean; - - @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; -} +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'; + +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; + } + + 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; + } + + 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' }), + }) + .meta({ id: 'TimeBucketDto' }); + +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'), + }) + .meta({ id: 'TimeBucketAssetResponseDto' }); + +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 }), + }) + .meta({ id: 'TimeBucketsResponseDto' }); + +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 f1d1f109f6142..9a725bc6c835a 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 cce19940074fc..7a7c1d2558316 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'; - -class AvatarUpdate { - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' }) - color?: UserAvatarColor; -} - -class MemoriesUpdate { - @ValidateBoolean({ optional: true, description: 'Whether memories are enabled' }) - enabled?: boolean; - - @Optional() - @IsInt() - @IsPositive() - @ApiProperty({ type: 'integer', description: 'Memory duration in seconds' }) - duration?: number; -} - -class RatingsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether ratings are enabled' }) - enabled?: boolean; -} - -@ApiSchema({ description: 'Album preferences' }) -class AlbumsUpdate { - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, description: 'Default asset order for albums' }) - defaultAssetOrder?: AssetOrder; -} - -class FoldersUpdate { - @ValidateBoolean({ optional: true, description: 'Whether folders are enabled' }) - enabled?: boolean; - - @ValidateBoolean({ optional: true, description: 'Whether folders appear in web sidebar' }) - sidebarWeb?: boolean; -} - -class PeopleUpdate { - @ValidateBoolean({ optional: true, description: 'Whether people are enabled' }) - enabled?: boolean; - - @ValidateBoolean({ optional: true, description: 'Whether people appear in web sidebar' }) - sidebarWeb?: boolean; -} - -class SharedLinksUpdate { - @ValidateBoolean({ optional: true, description: 'Whether shared links are enabled' }) - enabled?: boolean; - - @ValidateBoolean({ optional: true, description: 'Whether shared links appear in web sidebar' }) - sidebarWeb?: boolean; -} - -class TagsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether tags are enabled' }) - enabled?: boolean; - - @ValidateBoolean({ optional: true, description: 'Whether tags appear in web sidebar' }) - sidebarWeb?: boolean; -} - -class EmailNotificationsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether email notifications are enabled' }) - enabled?: boolean; - - @ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album invites' }) - albumInvite?: boolean; - - @ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album updates' }) - albumUpdate?: boolean; -} - -class DownloadUpdate implements Partial { - @Optional() - @IsInt() - @IsPositive() - @ApiPropertyOptional({ type: 'integer', description: 'Maximum archive size in bytes' }) - archiveSize?: number; - - @ValidateBoolean({ optional: true, description: 'Whether to include embedded videos in downloads' }) - includeEmbeddedVideos?: boolean; -} - -class PurchaseUpdate { - @ValidateBoolean({ optional: true, description: 'Whether to show support badge' }) - showSupportBadge?: boolean; - - @ApiPropertyOptional({ description: 'Date until which to hide buy button' }) - @IsDateString() - @Optional() - hideBuyButtonUntil?: string; -} - -class CastUpdate { - @ValidateBoolean({ optional: true, description: 'Whether Google Cast is enabled' }) - gCastEnabled?: boolean; -} - -export class UserPreferencesUpdateDto { - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => AlbumsUpdate) - albums?: AlbumsUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => FoldersUpdate) - folders?: FoldersUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => MemoriesUpdate) - memories?: MemoriesUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => PeopleUpdate) - people?: PeopleUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => RatingsUpdate) - ratings?: RatingsUpdate; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined, required: false }) - @Optional() - @ValidateNested() - @Type(() => SharedLinksUpdate) - sharedLinks?: SharedLinksUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => TagsUpdate) - tags?: TagsUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => AvatarUpdate) - avatar?: AvatarUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => EmailNotificationsUpdate) - emailNotifications?: EmailNotificationsUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => DownloadUpdate) - download?: DownloadUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => PurchaseUpdate) - purchase?: PurchaseUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => CastUpdate) - 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; -} +import z from 'zod'; + +const AlbumsUpdateSchema = z + .object({ + defaultAssetOrder: AssetOrderSchema.optional(), + }) + .optional() + .describe('Album preferences') + .meta({ id: 'AlbumsUpdate' }); + +const AvatarUpdateSchema = z + .object({ + color: UserAvatarColorSchema.optional(), + }) + .optional() + .meta({ id: 'AvatarUpdate' }); + +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' }); + +const RatingsUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether ratings are enabled'), + }) + .optional() + .meta({ id: 'RatingsUpdate' }); + +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' }); + +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' }); + +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' }); + +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' }); + +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' }); + +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' }); + +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' }); + +const CastUpdateSchema = z + .object({ + gCastEnabled: z.boolean().optional().describe('Whether Google Cast is enabled'), + }) + .optional() + .meta({ id: 'CastUpdate' }); + +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' }); + +const AlbumsResponseSchema = z + .object({ + defaultAssetOrder: AssetOrderSchema, + }) + .meta({ id: 'AlbumsResponse' }); + +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' }); + +const MemoriesResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether memories are enabled'), + duration: z.int().describe('Memory duration in seconds'), + }) + .meta({ id: 'MemoriesResponse' }); + +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' }); + +const RatingsResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether ratings are enabled'), + }) + .meta({ id: 'RatingsResponse' }); + +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' }); + +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' }); + +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' }); + +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' }); + +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' }); + +const CastResponseSchema = z + .object({ + gCastEnabled: z.boolean().describe('Whether Google Cast is enabled'), + }) + .meta({ id: 'CastResponse' }); + +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' }); + +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 6559dd052c031..c3c91d3d954ae 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 e6be3b17d1173..6acc9554f9a02 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, - password: 'password', - name: 'name', - }; - let dto: UserAdminCreateDto = plainToInstance(UserAdminCreateDto, params); - let errors = await validate(dto); - expect(errors).toHaveLength(1); + it('validates the email', () => { + expect(UserAdminCreateSchema.safeParse({ password: 'password', name: 'name' }).success).toBe(false); - params.email = 'invalid email'; - dto = plainToInstance(UserAdminCreateDto, params); - errors = await validate(dto); - expect(errors).toHaveLength(1); + expect( + UserAdminCreateSchema.safeParse({ email: 'invalid email', password: 'password', name: 'name' }).success, + ).toBe(false); - params.email = 'valid@email.com'; - dto = plainToInstance(UserAdminCreateDto, params); - errors = await validate(dto); - expect(errors).toHaveLength(0); - }); - - it('validates invalid email type', async () => { - let dto = plainToInstance(UserAdminCreateDto, { - email: [], - password: 'some password', - name: 'some name', + const result = UserAdminCreateSchema.safeParse({ + email: 'valid@email.com', + password: 'password', + name: 'name', }); - expect(await validate(dto)).toHaveLength(1); + expect(result.success).toBe(true); + }); - 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); + + 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 ebd0018bba9ff..75256b9e1a3dc 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'; - -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', +import { emptyStringToNull, isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation'; +import z from 'zod'; + +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 UserUpdateMeDto extends createZodDto(UserUpdateMeSchema) {} + +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 { - @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 UserResponseDto extends createZodDto(UserResponseSchema) {} -export class UserLicense { - @ApiProperty({ description: 'License key' }) - licenseKey!: string; - @ApiProperty({ description: 'Activation key' }) - activationKey!: string; - @ApiProperty({ description: 'Activation date' }) - activatedAt!: Date; -} +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; - - @ApiPropertyOptional({ description: 'PIN code' }) - @PinCode({ optional: true, nullable: true, emptyToNull: true }) - pinCode?: string | null; - - @ApiPropertyOptional({ description: 'Storage label' }) - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; - - @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - @Optional({ nullable: true }) - @IsInt() - @Min(0) - 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', + .meta({ id: 'UserAdminSearchDto' }); + +export class UserAdminSearchDto extends createZodDto(UserAdminSearchSchema) {} + +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; - - @ApiPropertyOptional({ description: 'Storage label' }) - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; - - @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) - shouldChangePassword?: boolean; - - @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - @Optional({ nullable: true }) - @IsInt() - @Min(0) - quotaSizeInBytes?: number | null; - - @ValidateBoolean({ optional: true, description: 'Grant admin privileges' }) - isAdmin?: boolean; -} + .meta({ id: 'UserAdminCreateDto' }); + +export class UserAdminCreateDto extends createZodDto(UserAdminCreateSchema) {} + +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' }); -export class UserAdminDeleteDto { - @ValidateBoolean({ optional: true, description: 'Force delete even if user has assets' }) - force?: boolean; -} +export class UserAdminUpdateDto extends createZodDto(UserAdminUpdateSchema) {} -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 UserAdminDeleteSchema = z + .object({ + force: z.boolean().optional().describe('Force delete even if user has assets'), + }) + .meta({ id: 'UserAdminDeleteDto' }); + +export class UserAdminDeleteDto extends createZodDto(UserAdminDeleteSchema) {} + +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 c4e5ac9c4c519..0307c7f483e97 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'; - -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', +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'; + +const WorkflowFilterItemSchema = z + .object({ + pluginFilterId: z.uuidv4().describe('Plugin filter ID'), + filterConfig: FilterConfigSchema.optional(), }) - triggerType?: PluginTriggerType; - - @ApiPropertyOptional({ description: 'Workflow name' }) - @IsString() - @IsNotEmpty() - @Optional() - name?: string; + .meta({ id: 'WorkflowFilterItemDto' }); - @ApiPropertyOptional({ description: 'Workflow description' }) - @IsString() - @Optional() - description?: string; - - @ValidateBoolean({ optional: true, description: 'Workflow enabled' }) - enabled?: boolean; - - @ApiPropertyOptional({ description: 'Workflow filters' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowFilterItemDto) - @Optional() - filters?: WorkflowFilterItemDto[]; - - @ApiPropertyOptional({ description: 'Workflow actions' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowActionItemDto) - @Optional() - actions?: WorkflowActionItemDto[]; -} - -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[]; -} - -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; -} +const WorkflowActionItemSchema = z + .object({ + pluginActionId: z.uuidv4().describe('Plugin action ID'), + actionConfig: ActionConfigSchema.optional(), + }) + .meta({ id: 'WorkflowActionItemDto' }); + +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' }); + +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' }); + +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' }); + +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' }); + +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 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 de85d24db3318..cb4835020fe02 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 a8afa91cbcadb..f91bb2b12292f 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 5876b934e544a..2ff4d224cf1b9 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 a3dc8ba5cbd4d..3c579a1a94c91 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 fa4823362e6a1..97ec3f1cdcab1 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 fbc281ccb323f..c505dd3fb39c8 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 142d5e3252364..a94e5aa9f6799 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 5fbbb76cf7321..d87c0acf5a74f 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 53e3121a415e4..dc5b984160163 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 8b9867b4cc4dd..c132d42feeaf0 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 2c4b31c83a919..94b8acd25e1bb 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 5932855a21a79..498c165888652 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 ce3c9ee662e12..81e8c99d49ba4 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 c7bea2b4409c3..1eaa4f9a2c9b4 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 d78b8940d3aae..7209a613fe827 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 cdfa2ad2ed224..662ccbe618469 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 30bc1f1f0def5..77636acfd26c4 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 b346906fc8ac6..bb68f70d13fc3 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 ea95b4df24cbe..981141b02e151 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 793bb3c1ff8f8..da1f6da935d8e 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'; - -export interface JSONSchemaProperty { - type?: JSONSchemaType | JSONSchemaType[]; - description?: string; - default?: any; - enum?: any[]; - items?: JSONSchemaProperty; - properties?: Record; - required?: string[]; - additionalProperties?: boolean | JSONSchemaProperty; -} - -export interface JSONSchema { - type: 'object'; - properties?: Record; - required?: string[]; - additionalProperties?: boolean; - description?: string; -} - -export type ConfigValue = string | number | boolean | null | ConfigValue[] | { [key: string]: ConfigValue }; - -export interface FilterConfig { - [key: string]: ConfigValue; -} - -export interface ActionConfig { - [key: string]: ConfigValue; -} +import z from 'zod'; + +const JSONSchemaTypeSchema = z + .enum(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']) + .meta({ id: 'PluginJsonSchemaType' }); + +const JSONSchemaPropertySchema = z + .object({ + type: JSONSchemaTypeSchema.optional(), + description: z.string().optional(), + default: z.any().optional(), + enum: z.array(z.string()).optional(), + + get items() { + return JSONSchemaPropertySchema.optional(); + }, + + get properties() { + return z.record(z.string(), JSONSchemaPropertySchema).optional(); + }, + + 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 ad02e8355ed7c..0000000000000 --- 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 a669af31cf9e4..df7d05978c51d 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 092a0e6619d0b..d4de1eba86f4c 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 4c5d5ddfc43ce..9c8822518b399 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 7d2e99a215214..450563cf7e377 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 631ba60a607a4..434ac89ceef31 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); - }); - - it('fails when colliding attributes are present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', attribute2: 'value2' }); - await expect(validate(dto)).resolves.toHaveLength(2); - }); - - it('passes when no colliding attributes are present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', attribute3: '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(), + }); + + 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('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 b959de94b127c..54e3b1820ea47 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(), - ); -}; - -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', - {}, - ), - }, - }, - {}, - ); -} - -@ValidatorConstraint({ name: 'uniqueEditActions' }) -class UniqueEditActionsValidator implements ValidatorConstraintInterface { - validate(edits: { action: string; parameters?: unknown }[]): boolean { - if (!Array.isArray(edits)) { - return true; - } - - 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; - } - - defaultMessage(): string { - return 'Duplicate edit actions are not allowed'; - } -} - -export const IsUniqueEditActions = () => Validate(UniqueEditActionsValidator); +const UUIDParamSchema = z.object({ + id: z.uuidv4(), +}); -export class UUIDParamDto { - @IsNotEmpty() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - id!: string; -} +export class UUIDParamDto extends createZodDto(UUIDParamSchema) {} -export class UUIDAssetIDParamDto { - @ValidateUUID() - id!: string; - - @ValidateUUID() - assetId!: string; -} +const UUIDAssetIDParamSchema = z.object({ + id: z.uuidv4(), + assetId: z.uuidv4(), +}); -export class FilenameParamDto { - @IsNotEmpty() - @IsString() - @ApiProperty({ format: 'string' }) - @Matches(/^[a-zA-Z0-9_\-.]+$/, { - message: 'Filename contains invalid characters', - }) - filename!: string; -} +export class UUIDAssetIDParamDto extends createZodDto(UUIDAssetIDParamSchema) {} -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 }), - ]; +const FilenameParamSchema = z.object({ + filename: z.string().regex(/^[a-zA-Z0-9_\-.]+$/, { + error: 'Filename contains invalid characters', + }), +}); - if (optional) { - decorators.push(Optional({ nullable, emptyToNull })); - } +export class FilenameParamDto extends createZodDto(FilenameParamSchema) {} - return applyDecorators(...decorators); +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 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. + * Unified email validation + * Converts email strings to lowercase and validates against HTML5 email regex + * @docs https://zod.dev/api?id=email */ -// 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 const toEmail = z + .email({ + pattern: z.regexes.html5Email, + error: (iss) => `Invalid input: expected email, received ${typeof iss.input}`, + }) + .transform((val) => val.toLowerCase()); -export function IsNotSiblingOf(siblings: 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: '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(', ')}`; - }, - }, + decode: (isoString) => new Date(isoString), + encode: (date) => date.toISOString(), }, - validationOptions, - ); -} - -export const ValidateHexColor = () => { - const decorators = [ - IsHexColor(), - Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value)), - ]; + ) + .meta({ example: '2024-01-01T00:00:00.000Z' }); - 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); +/** + * 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}`, }), - ); -}; - -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())); - } + z.date(), + { + decode: (isoString) => new Date(isoString), + encode: (date) => date.toISOString().slice(0, 10), + }, + ) + .meta({ example: '2024-01-01' }); - return applyDecorators(...decorators); -}; +export const isValidTime = z + .string() + .regex(/^([01]\d|2[0-3]):[0-5]\d$/, 'Invalid input: expected string in HH:mm format, received string'); -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(), - ]; +/** + * 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); - return applyDecorators(...decorators); -}; +/** + * 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); -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 }), - ); -}; +/** + * 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: 'cronValidator' }) -class CronValidator implements ValidatorConstraintInterface { - validate(expression: string): boolean { +/** + * Parse JSON strings from multipart/form-data + */ +export const JsonParsed = z.transform((val, ctx) => { + if (typeof val === 'string') { try { - new CronJob(expression, () => {}); - return true; + return JSON.parse(val); } catch { - return false; + ctx.issues.push({ + code: 'custom', + message: `Invalid input: expected JSON string, received ${typeof val}`, + input: val, + }); + return z.NEVER; } } -} - -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 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; -} - -export function IsDateStringFormat(format: string, validationOptions?: ValidationOptions) { - return ValidateBy( - { - name: 'isDateStringFormat', - constraints: [format], - validator: { - validate(value: unknown) { - return isDateStringFormat(value, format); - }, - defaultMessage: () => `$property must be a string in the format ${format}`, - }, - }, - validationOptions, - ); -} - -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( - { - 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, - ), - }, - }, - validationOptions, - ); -} - -type IsIPRangeOptions = { requireCIDR?: boolean }; -export function IsIPRange(options: IsIPRangeOptions, validationOptions?: ValidationOptions): PropertyDecorator { - const { requireCIDR } = { requireCIDR: true, ...options }; + return val; +}); - return ValidateBy( - { - name: 'isIPRange', - validator: { - validate: (value): boolean => { - if (isIPRange(value)) { - return true; - } - - if (!requireCIDR && isIP(value)) { - return true; - } - - return false; - }, - defaultMessage: buildMessage( - (eachPrefix) => eachPrefix + '$property must be an ip address, or ip address range', - validationOptions, - ), - }, - }, - validationOptions, - ); -} - -@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; - } - - return Number(value) >= Number(relatedValue); - } +/** + * 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}`)); - defaultMessage(args: ValidationArguments) { - const relatedPropertyName = args.constraints?.[0] as string; - return `${args.property} must be greater than or equal to ${relatedPropertyName}`; - } -} +/** + * 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 IsGreaterThanOrEqualTo = (property: string, validationOptions?: ValidationOptions) => { - return Validate(IsGreaterThanOrEqualToConstraint, [property], validationOptions); -}; +export const sanitizeFilename = z.string().transform((val) => sanitize(val.replaceAll('.', ''))); diff --git a/server/test/utils.ts b/server/test/utils.ts index b3e47b2b7e0a9..aa9a9735bfdc5 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 627cdded50b45..24bf3739e2a01 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,