diff --git a/cli/src/commands/asset.spec.ts b/cli/src/commands/asset.spec.ts index 21700ef96392..f179b350c9fc 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 7d4b09b69d7e..c3ad820547ab 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 11e825a7cd38..2d9a325289bd 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 4d67a8464781..719436a66dc4 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 977638aa2491..c280deb13415 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 ae9064375f58..a0ae1dc8194f 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 d69536f3a3df..7b5a2f16de56 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 793c508a36c4..6751b21e84d0 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 3f280dddf520..ee13a29c1bf8 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 1494b4053136..eff93a8d722f 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 33a8eca94df9..32188b483846 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 525f0906babf..367c2447f23e 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 090889ff3297..38c805a42e75 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 d01e743a3f5f..b1df5f240c43 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 6b554fb644ce..9403852ef047 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 697598ac97f6..e0a393948cb7 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 a026b9902823..831c19683d11 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 fbd485f86fe3..768185db1edf 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 33bcaf062cb0..94b7e2e73854 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 41517f8144a3..9dda59a883b4 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 913205428e0b..0cd96ac44258 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 d4e2b1d80fef..ab0be3e8f38d 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 3b15b909090b..7d18f6d86793 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 ecb556e434e1..1312cb595210 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 085958de6641..46fc8594a8f1 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 f5b70a9ea453..4e43ec28ebf2 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 f82c362ff7c3..30a4c123f116 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 59a4b6009617..5e165ffd5d95 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 7ccae02c76da..1d905b1e2281 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 48e5f5874bcd..3ed1f7529f28 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 830325a5b6a1..3b36b23d6cbf 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 fb4b6d084e33..bc220e64ced4 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 dadb45d8ac65..1b0e279ab7b5 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 15ad2a170e33..d9ac019ee226 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 43e686fbdc72..ca0c08702758 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 127334e687ef..0f440d572dd8 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 c448a0b4b7bd..ee457905bd3a 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 8006748341e6..26aa35ae78a4 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 8d0c01cfb8a4..bbae03fba74c 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 743a9f064511..99e679222e51 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 520ee171c10d..def205de90cf 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 107c65dd1e22..d61b5c1398af 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 32ba5433425d..d5b8bf8b418e 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 99bac7abfa98..f97300b19f8b 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 b56370f689a9..bf3ee8e24435 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 22c09752d295..f59cdc1a67bc 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 7351840b11c6..348bae370b5f 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 7829de4bd57c..2c7bb82c242f 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 fc67aa022f53..2086f72929a4 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 a23a1ef5f37f..3315fe8579a2 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 3ecc20c699e9..29c28175cd45 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 61d972a0c493..21b86dfe4e7d 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 1ae5cef07e9d..4a4a2a658e16 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 3fabb1cac65d..835e063e9294 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 000000000000..c51eab1692b0 --- /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 974528302119..cafe1b21b99f 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 0aa5544a3aeb..5085e3820c27 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 905e738b6ee9..6dc5cd3c92a5 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 087d19da1fab..ed7a72a613b3 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 b79a69372686..3e16ed8721e7 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 caaf379b3048..e4eab08bf1e1 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 2c3faab17801..d3562f5a483c 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 8a6bcb9b01c0..70de1941f3dd 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 000000000000..a31e1e61172f --- /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 078dd0bdaf55..d185761f54ca 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 229e7aa71002..96fd66a392ac 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 201550c87fe2..df2762a2f3c7 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 000000000000..b5cdbb0151f1 --- /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 a817832dab0e..875eb138a8db 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 1fa8536964a0..bb3f1d88561b 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 0b7f0738fe7b..796138b0bfa1 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 1bef8f29d829..af5b2cbf68c3 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 69942fee5c7e..ba12c62d7672 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 20d7cbd5e703..c6ec0d94a05f 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 8bc33a81dc48..c336270b849d 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 34912a55e047..abfa637157bf 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 16985dd605d7..de7bf78d5a4e 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 97a3346a67ba..dcb1258457db 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 a1ba44920ede..8a0cebd945c6 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 32e9487475ec..bc1d7b404704 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 81912e1d3044..bfe32307fa91 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 4acc1c8bd351..c5feb9df43de 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 6bb58a8ab98e..64a5a73bed77 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 4b9d7a6e9e50..66cb542ccf1f 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 906a95a83c20..873404c78672 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 3a3412384e8a..fe6743cba09d 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 73a0187ddd3b..98fe3d3536cb 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 aa9158e59184..88ebceae24f0 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 6eec3ae8d77a..55adbc2b4933 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 ea1fee9d7abf..d1818a2a43c4 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 84ff72c1eb3f..000000000000 --- 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 2129096da2f5..edb6a1ddda4f 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 ad524914b49e..e3f8c0acbedb 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 52dbb5b95bb3..124fa674fdd6 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 d09790a81a08..27753eb9dc46 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 63d4094cd0d3..250e214a60df 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 d27cef022d42..ede9910d7484 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 5b8eeed8fb96..b906f6dd1dd5 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 1835095cf727..e736667d5759 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 bdf5b5989443..67d0b69f4650 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 bde78de48101..ae542870d9fd 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 aee7bd1ba1d0..ecfc93edb095 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 4905b161bfb4..d8d7e9643b62 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 4dbc90d407e0..0e8d509a1684 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 e8b8db685b20..78c3da786c37 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 1288da8670b8..f9771246f989 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 30d43de1155c..ad0e79cb2779 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 554863ae4fb1..4ca4e2bcc832 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 b5885aa44146..dbc9c12f8457 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 a1570583245a..5ac61ededc10 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 eddf9c7e12ba..c5d949d7b27d 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 77466d61d92c..b63f027af7e6 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 d97cd5ffca6c..2ce56467312e 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 93ec956f5801..77ae96532f61 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 c43c0df75dab..c5e3b308ac16 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 5789938d187b..f4612cc98a22 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 c09560e08c67..9d5d8ec18a61 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 f345657e739e..87edc6b4a71e 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 d2b45c8ccbd9..aeac16cc8a3e 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 f31c04b69ff9..f710dff8b9ae 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 34fa314ba94c..cff2dc92f7ef 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 6f4ac91fdbb0..beda0b0f1a43 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 ea6411a9c1b0..d1ab867ff99f 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 000000000000..f7a2d584d957 --- /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 000000000000..65951da0a364 --- /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 000000000000..169c6be772a4 --- /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 000000000000..cabac9b71bdb --- /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 16a9604bcd3e..a6ee1c6b6904 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 9ae64acf6c4a..3ebcef7a9552 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 9e1eea15dbde..fb68d855832b 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 2ce63784ebe8..06d433edad42 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 03a1371cc55a..cbd01b11ed2f 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 d94304d0d3d9..eb19d8957f60 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 ac9244514c3f..c88f9fc195c2 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 c9a37ee30aab..86c75f8e7cdb 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 d5803c9cc787..904561a03353 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 4346fa5c583c..7b067412bff2 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 29568b9d1103..6060f4c2b78f 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 4c788138fbcb..c4daccad716d 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 8841251e4adc..c21113ee6d25 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 acb81f28e28d..f4ffade26b79 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 8318fbfb3bcc..62adfaa74ac4 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 43b5ac5c8121..51124ef1cfb1 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 5aebe4d6a9dc..c4d893af051b 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 b18fe687c443..6d44b881bd6d 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 fec096d51aad..316edb609fcb 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 ef2fa458e254..605bd74f414e 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 476b048b4dfe..4a66d54e373f 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 c3b7049016fb..ae5e060cffe2 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 a13cd81ad768..60161a745866 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 14bf584bb90a..e7c9dc0d6351 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 2675ad4beb3f..a32714d556d8 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 b22232add6d7..11d6cdd52efc 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 d9aec48c3939..331265129628 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 510e94e43f89..2b32a5754021 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 5f8214467fe2..9c1192ff34e3 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 638dfb52557c..326f83a03df3 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 d5bbf448a362..729b7f127cae 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 747f67155775..fa7e20a8325c 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 3fc897206923..1efe7da02955 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 6c89d93724f4..17b2bda02be0 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 68af2802904c..e0c98bfef3ee 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 3cc2673bfc7b..8acfad5f6afa 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 ff9efdfea3f5..caaeed7fb36e 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 647a07d5eb0b..c3f74ff2cd32 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 688d71229fd3..aeefc2ece999 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 4a66623939d2..08d7eae49b84 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 debde4488ebd..d08de6ab72cb 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 0edd804c6ab2..c64d82bfbd0c 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 c506738d97ce..855340f4d777 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 fc2c36aa8c46..1bd6f4a16048 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 671081c0a5db..f51cc8bde981 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 e4487ccfaf04..3e79a551344a 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 61340a8f82ed..67976108e1a3 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 23803d0be4b9..ddde7c0513ad 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 6d425130a312..0a8159354719 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 6c7acbd2189e..ecf2e5da4af2 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 b5640f82c8d8..d78f8fadd5ad 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 3e8fed2c6859..2571c0cab02d 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 217a666a675e..668b7408726f 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 28ea603c2a0c..003000d2ecd9 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 2a0f1ffbc61d..6162e72b8f96 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 109babd37463..7a2fbb516bdd 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 cfb18b181e83..0db417427fac 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 82195e498bea..88dddbb4d360 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 9db85509f58e..d29ca1fac3ee 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 9bdaef92d351..790710751fe9 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 a7313560e61e..dc553e736908 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 5566846e3cf9..4d689f01a115 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 fd6a10163cda..e05b29f1edc4 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 1e4a4bd109b6..8a3ac1747402 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 11faa815e27b..8b8da1d37a70 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 7edd5d032af1..7b43d9ceb741 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 46ce8b0ecc71..ae4a5c1f876d 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 9d934eb465de..43218cae6e14 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 8526995934ed..2c4c3352eac8 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 628bdc0055eb..276d43ecd997 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 da1fe600a5c8..462b82c3e0f5 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 485b2e00e50d..54da0b05662f 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 706f65cf35a3..09f8cedce45f 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 3cce65745f6a..0c33a4613937 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 4fcf518550a1..719e36689949 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 f02dc73befcf..8ef46a0bb55b 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 bf0e2cbf09cb..f671072c72ca 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 066c435eb304..0751d4096b84 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 59c368078252..68fb0e9fe210 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 78cc03dc9450..ebcb88193513 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 b1a47c872190..a291fabf6ed4 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 9222dd6ba7c8..1ad70238d86d 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 8f77e9cf2b05..dcbb5ee8efc0 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 38665a19128d..143af0ca6cf1 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 52e29c3e93ab..92224b9f1610 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 355378adacb9..932722f5a57e 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 ae3e6510aae3..6461b625081a 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 9891fff07937..9abb45ddd578 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 a577b0544f38..18ab07b3a995 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 90d151a2a37e..f07898d4e7b7 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -11,8 +11,12 @@ "required": true, "in": "query", "description": "Album ID", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -21,8 +25,12 @@ "required": false, "in": "query", "description": "Asset ID (if activity is for an asset)", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -30,7 +38,9 @@ "name": "level", "required": false, "in": "query", - "description": "Filter by activity level", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "$ref": "#/components/schemas/ReactionLevel" } @@ -39,7 +49,9 @@ "name": "type", "required": false, "in": "query", - "description": "Filter by activity type", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "$ref": "#/components/schemas/ReactionType" } @@ -49,8 +61,12 @@ "required": false, "in": "query", "description": "Filter by user ID", + "x-nestjs_zod-parent-metadata": { + "description": "Activity search" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -171,8 +187,12 @@ "required": true, "in": "query", "description": "Album ID", + "x-nestjs_zod-parent-metadata": { + "description": "Activity" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -181,8 +201,12 @@ "required": false, "in": "query", "description": "Asset ID (if activity is for an asset)", + "x-nestjs_zod-parent-metadata": { + "description": "Activity" + }, "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -243,6 +267,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -512,7 +537,7 @@ "required": true, "in": "path", "schema": { - "format": "string", + "pattern": "^[a-zA-Z0-9_\\-.]+$", "type": "string" } } @@ -936,6 +961,7 @@ "description": "User ID filter", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -1068,6 +1094,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1137,6 +1164,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1196,6 +1224,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1267,6 +1296,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1326,6 +1356,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1397,6 +1428,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1458,6 +1490,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1522,6 +1555,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -1547,7 +1581,6 @@ "name": "visibility", "required": false, "in": "query", - "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -1611,6 +1644,7 @@ "description": "Filter albums containing this asset ID (ignores shared parameter)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -1868,6 +1902,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -1919,6 +1954,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -2002,6 +2038,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2072,6 +2109,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2143,6 +2181,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -2232,6 +2271,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -2291,6 +2331,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -2362,6 +2403,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2592,6 +2634,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2643,6 +2686,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -2701,6 +2745,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3441,7 +3486,6 @@ "name": "visibility", "required": false, "in": "query", - "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -3503,6 +3547,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -3577,6 +3622,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3647,6 +3693,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3694,6 +3741,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3748,6 +3796,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3814,6 +3863,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3875,6 +3925,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -3949,6 +4000,7 @@ "description": "Asset ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4010,6 +4062,7 @@ "description": "Asset ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4079,6 +4132,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -4152,6 +4206,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4228,6 +4283,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4322,6 +4378,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -4337,7 +4394,6 @@ "name": "size", "required": false, "in": "query", - "description": "Asset media size", "schema": { "$ref": "#/components/schemas/AssetMediaSize" } @@ -4408,6 +4464,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -5355,6 +5412,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5409,6 +5467,7 @@ "description": "Face ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5523,6 +5582,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5584,6 +5644,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -5762,7 +5823,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -5953,6 +6013,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6005,6 +6066,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6064,6 +6126,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6135,6 +6198,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6189,6 +6253,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6250,6 +6315,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6321,6 +6387,8 @@ "description": "Filter assets created after this date", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6331,6 +6399,8 @@ "description": "Filter assets created before this date", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6505,6 +6575,8 @@ "description": "Filter by date", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6530,7 +6602,6 @@ "name": "order", "required": false, "in": "query", - "description": "Sort order", "schema": { "$ref": "#/components/schemas/MemorySearchOrder" } @@ -6542,6 +6613,7 @@ "description": "Number of memories to return", "schema": { "minimum": 1, + "maximum": 9007199254740991, "type": "integer" } }, @@ -6549,7 +6621,6 @@ "name": "type", "required": false, "in": "query", - "description": "Memory type", "schema": { "$ref": "#/components/schemas/MemoryType" } @@ -6673,6 +6744,8 @@ "description": "Filter by date", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -6698,7 +6771,6 @@ "name": "order", "required": false, "in": "query", - "description": "Sort order", "schema": { "$ref": "#/components/schemas/MemorySearchOrder" } @@ -6710,6 +6782,7 @@ "description": "Number of memories to return", "schema": { "minimum": 1, + "maximum": 9007199254740991, "type": "integer" } }, @@ -6717,7 +6790,6 @@ "name": "type", "required": false, "in": "query", - "description": "Memory type", "schema": { "$ref": "#/components/schemas/MemoryType" } @@ -6779,6 +6851,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6830,6 +6903,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6888,6 +6962,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -6958,6 +7033,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7029,6 +7105,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7154,6 +7231,7 @@ "description": "Filter by notification ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -7161,7 +7239,6 @@ "name": "level", "required": false, "in": "query", - "description": "Filter by notification level", "schema": { "$ref": "#/components/schemas/NotificationLevel" } @@ -7170,7 +7247,6 @@ "name": "type", "required": false, "in": "query", - "description": "Filter by notification type", "schema": { "$ref": "#/components/schemas/NotificationType" } @@ -7295,6 +7371,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7346,6 +7423,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7404,6 +7482,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7707,7 +7786,6 @@ "name": "direction", "required": true, "in": "query", - "description": "Partner direction", "schema": { "$ref": "#/components/schemas/PartnerDirection" } @@ -7830,6 +7908,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7882,6 +7961,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -7938,6 +8018,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8060,6 +8141,7 @@ "description": "Closest asset ID for similarity search", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -8070,6 +8152,7 @@ "description": "Closest person ID for similarity search", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -8281,6 +8364,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8332,6 +8416,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8390,6 +8475,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8460,6 +8546,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8533,6 +8620,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8606,6 +8694,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8666,6 +8755,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8825,6 +8915,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -8929,7 +9020,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -8984,7 +9074,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -9051,7 +9140,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -9109,7 +9197,6 @@ "name": "name", "required": true, "in": "path", - "description": "Queue name", "schema": { "$ref": "#/components/schemas/QueueName" } @@ -9292,7 +9379,8 @@ "type": "array", "items": { "type": "string", - "format": "uuid" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" } } }, @@ -9302,8 +9390,8 @@ "in": "query", "description": "Filter by city name", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9312,8 +9400,8 @@ "in": "query", "description": "Filter by country name", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9323,6 +9411,8 @@ "description": "Filter by creation date (after)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9333,6 +9423,8 @@ "description": "Filter by creation date (before)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9396,8 +9488,8 @@ "in": "query", "description": "Filter by lens model", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9406,9 +9498,10 @@ "in": "query", "description": "Library ID to filter by", "schema": { + "type": "string", "format": "uuid", - "nullable": true, - "type": "string" + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", + "nullable": true } }, { @@ -9417,7 +9510,8 @@ "in": "query", "description": "Filter by camera make", "schema": { - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9427,6 +9521,7 @@ "description": "Minimum file size in bytes", "schema": { "minimum": 0, + "maximum": 9007199254740991, "type": "integer" } }, @@ -9436,8 +9531,8 @@ "in": "query", "description": "Filter by camera model", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9458,7 +9553,8 @@ "type": "array", "items": { "type": "string", - "format": "uuid" + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" } } }, @@ -9484,10 +9580,10 @@ ], "x-immich-state": "Stable", "schema": { + "type": "number", "minimum": -1, "maximum": 5, - "nullable": true, - "type": "number" + "nullable": true } }, { @@ -9507,8 +9603,8 @@ "in": "query", "description": "Filter by state/province name", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true } }, { @@ -9517,12 +9613,13 @@ "in": "query", "description": "Filter by tag IDs", "schema": { - "nullable": true, "type": "array", "items": { "type": "string", - "format": "uuid" - } + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$" + }, + "nullable": true } }, { @@ -9532,6 +9629,8 @@ "description": "Filter by taken date (after)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9542,6 +9641,8 @@ "description": "Filter by taken date (before)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9552,6 +9653,8 @@ "description": "Filter by trash date (after)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9562,6 +9665,8 @@ "description": "Filter by trash date (before)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9569,7 +9674,6 @@ "name": "type", "required": false, "in": "query", - "description": "Asset type filter", "schema": { "$ref": "#/components/schemas/AssetTypeEnum" } @@ -9581,6 +9685,8 @@ "description": "Filter by update date (after)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9591,6 +9697,8 @@ "description": "Filter by update date (before)", "schema": { "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "example": "2024-01-01T00:00:00.000Z", "type": "string" } }, @@ -9598,7 +9706,6 @@ "name": "visibility", "required": false, "in": "query", - "description": "Filter by visibility", "schema": { "$ref": "#/components/schemas/AssetVisibility" } @@ -10122,7 +10229,6 @@ "name": "type", "required": true, "in": "query", - "description": "Suggestion type", "schema": { "$ref": "#/components/schemas/SearchSuggestionType" } @@ -11014,6 +11120,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11065,6 +11172,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11135,6 +11243,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11189,6 +11298,7 @@ "description": "Filter by album ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -11205,6 +11315,7 @@ ], "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11406,7 +11517,6 @@ "in": "query", "description": "Link password", "schema": { - "example": "password", "type": "string" } }, @@ -11483,6 +11593,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11534,6 +11645,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11592,6 +11704,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11662,6 +11775,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11733,6 +11847,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -11873,6 +11988,7 @@ "description": "Filter by primary asset ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -11994,6 +12110,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -12045,6 +12162,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -12103,6 +12221,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -12173,6 +12292,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -12182,6 +12302,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13209,6 +13330,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13260,6 +13382,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13318,6 +13441,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13388,6 +13512,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13459,6 +13584,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -13533,6 +13659,7 @@ "description": "Filter assets belonging to a specific album", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13588,6 +13715,7 @@ "description": "Filter assets containing a specific person (face recognition)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13606,6 +13734,7 @@ "description": "Filter assets with a specific tag", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13613,7 +13742,7 @@ "name": "timeBucket", "required": true, "in": "query", - "description": "Time bucket identifier in YYYY-MM-DD format (e.g., \"2024-01-01\" for January 2024)", + "description": "Time bucket identifier in YYYY-MM-DD format", "schema": { "example": "2024-01-01", "type": "string" @@ -13626,6 +13755,7 @@ "description": "Filter assets by specific user ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13719,6 +13849,7 @@ "description": "Filter assets belonging to a specific album", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13774,6 +13905,7 @@ "description": "Filter assets containing a specific person (face recognition)", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13792,6 +13924,7 @@ "description": "Filter assets with a specific tag", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -13802,6 +13935,7 @@ "description": "Filter assets by specific user ID", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -14726,6 +14860,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -14786,6 +14921,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15065,6 +15201,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15112,6 +15249,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15166,6 +15304,7 @@ "in": "path", "schema": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } } @@ -15443,7 +15582,9 @@ "properties": { "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -15463,7 +15604,9 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -15494,15 +15637,18 @@ "type": "object" }, "ActivityCreateDto": { + "description": "Activity create", "properties": { "albumId": { "description": "Album ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "assetId": { "description": "Asset ID (if activity is for an asset)", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "comment": { @@ -15510,12 +15656,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/ReactionType" - } - ], - "description": "Activity type (like or comment)" + "$ref": "#/components/schemas/ReactionType" } }, "required": [ @@ -15528,7 +15669,9 @@ "properties": { "assetId": { "description": "Asset ID (if activity is for an asset)", + "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "comment": { @@ -15538,20 +15681,19 @@ }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { "description": "Activity ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/ReactionType" - } - ], - "description": "Activity type" + "$ref": "#/components/schemas/ReactionType" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -15570,10 +15712,14 @@ "properties": { "comments": { "description": "Number of comments", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "likes": { "description": "Number of likes", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -15630,6 +15776,8 @@ }, "assetCount": { "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "assets": { @@ -15676,12 +15824,7 @@ "type": "string" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "description": "Asset sort order" + "$ref": "#/components/schemas/AssetOrder" }, "owner": { "$ref": "#/components/schemas/UserResponseDto" @@ -15727,14 +15870,20 @@ "properties": { "notShared": { "description": "Number of non-shared albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "owned": { "description": "Number of owned albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "shared": { "description": "Number of shared albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -15748,17 +15897,14 @@ "AlbumUserAddDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], + "$ref": "#/components/schemas/AlbumUserRole", "default": "editor", "description": "Album user role" }, "userId": { "description": "User ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -15770,16 +15916,12 @@ "AlbumUserCreateDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" }, "userId": { "description": "User ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -15792,12 +15934,7 @@ "AlbumUserResponseDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" }, "user": { "$ref": "#/components/schemas/UserResponseDto" @@ -15823,6 +15960,7 @@ "description": "Album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15831,6 +15969,7 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15845,12 +15984,7 @@ "AlbumsAddAssetsResponseDto": { "properties": { "error": { - "allOf": [ - { - "$ref": "#/components/schemas/BulkIdErrorReason" - } - ], - "description": "Error reason" + "$ref": "#/components/schemas/BulkIdErrorReason" }, "success": { "description": "Operation success", @@ -15865,13 +15999,7 @@ "AlbumsResponse": { "properties": { "defaultAssetOrder": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "default": "desc", - "description": "Default asset order for albums" + "$ref": "#/components/schemas/AssetOrder" } }, "required": [ @@ -15883,12 +16011,7 @@ "description": "Album preferences", "properties": { "defaultAssetOrder": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "description": "Default asset order for albums" + "$ref": "#/components/schemas/AssetOrder" } }, "type": "object" @@ -15903,6 +16026,7 @@ "description": "IDs to process", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15936,6 +16060,7 @@ "description": "Asset IDs to update", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -15946,10 +16071,14 @@ }, "latitude": { "description": "Latitude coordinate", + "maximum": 90, + "minimum": -90, "type": "number" }, "longitude": { "description": "Longitude coordinate", + "maximum": 180, + "minimum": -180, "type": "number" }, "rating": { @@ -15957,7 +16086,7 @@ "maximum": 5, "minimum": -1, "nullable": true, - "type": "number", + "type": "integer", "x-immich-history": [ { "version": "v1", @@ -15980,12 +16109,7 @@ "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "required": [ @@ -16043,12 +16167,7 @@ "AssetBulkUploadCheckResult": { "properties": { "action": { - "description": "Upload action", - "enum": [ - "accept", - "reject" - ], - "type": "string" + "$ref": "#/components/schemas/AssetUploadAction" }, "assetId": { "description": "Existing asset ID if duplicate", @@ -16063,12 +16182,7 @@ "type": "boolean" }, "reason": { - "description": "Rejection reason if rejected", - "enum": [ - "duplicate", - "unsupported-format" - ], - "type": "string" + "$ref": "#/components/schemas/AssetRejectReason" } }, "required": [ @@ -16102,6 +16216,7 @@ "sourceId": { "description": "Source asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "stack": { @@ -16112,6 +16227,7 @@ "targetId": { "description": "Target asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -16125,13 +16241,16 @@ "properties": { "updatedAfter": { "description": "Sync assets updated after this date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "userIds": { "description": "User IDs to sync", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -16144,6 +16263,7 @@ "type": "object" }, "AssetDeltaSyncResponseDto": { + "description": "Asset delta sync response", "properties": { "deleted": { "description": "Deleted asset IDs", @@ -16157,7 +16277,6 @@ "type": "boolean" }, "upserted": { - "description": "Upserted assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, @@ -16183,12 +16302,7 @@ "AssetEditActionItemDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ], - "description": "Type of edit action to perform" + "$ref": "#/components/schemas/AssetEditAction" }, "parameters": { "anyOf": [ @@ -16214,15 +16328,12 @@ "AssetEditActionItemResponseDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ], - "description": "Type of edit action to perform" + "$ref": "#/components/schemas/AssetEditAction" }, "id": { + "description": "Asset edit ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "parameters": { @@ -16268,6 +16379,7 @@ "assetId": { "description": "Asset ID these edits belong to", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "edits": { @@ -16289,35 +16401,49 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "height": { "description": "Face bounding box height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "personId": { "description": "Person ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "width": { "description": "Face bounding box width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "x": { "description": "Face bounding box X coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "y": { "description": "Face bounding box Y coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -16349,31 +16475,44 @@ "properties": { "boundingBoxX1": { "description": "Bounding box X1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { "description": "Bounding box X2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { "description": "Bounding box Y1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { "description": "Bounding box Y2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "person": { @@ -16382,16 +16521,10 @@ "$ref": "#/components/schemas/PersonResponseDto" } ], - "description": "Person associated with face", "nullable": true }, "sourceType": { - "allOf": [ - { - "$ref": "#/components/schemas/SourceType" - } - ], - "description": "Face detection source type" + "$ref": "#/components/schemas/SourceType" } }, "required": [ @@ -16426,11 +16559,13 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "personId": { "description": "Person ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -16441,43 +16576,52 @@ "type": "object" }, "AssetFaceWithoutPersonResponseDto": { + "description": "Asset face without person", "properties": { "boundingBoxX1": { "description": "Bounding box X1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { "description": "Bounding box X2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { "description": "Bounding box Y1 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { "description": "Bounding box Y2 coordinate", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "imageHeight": { "description": "Image height in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "imageWidth": { "description": "Image width in pixels", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "sourceType": { - "allOf": [ - { - "$ref": "#/components/schemas/SourceType" - } - ], - "description": "Face detection source type" + "$ref": "#/components/schemas/SourceType" } }, "required": [ @@ -16496,21 +16640,26 @@ "lastId": { "description": "Last asset ID (pagination)", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "limit": { "description": "Maximum number of assets to return", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, "updatedUntil": { "description": "Sync assets updated until this date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "userId": { "description": "Filter by user ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -16520,12 +16669,22 @@ ], "type": "object" }, + "AssetIdErrorReason": { + "description": "Error reason if failed", + "enum": [ + "duplicate", + "no_permission", + "not_found" + ], + "type": "string" + }, "AssetIdsDto": { "properties": { "assetIds": { "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -16543,13 +16702,7 @@ "type": "string" }, "error": { - "description": "Error reason if failed", - "enum": [ - "duplicate", - "no_permission", - "not_found" - ], - "type": "string" + "$ref": "#/components/schemas/AssetIdErrorReason" }, "success": { "description": "Whether operation succeeded", @@ -16578,17 +16731,13 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" }, "name": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetJobName" - } - ], - "description": "Job name" + "$ref": "#/components/schemas/AssetJobName" } }, "required": [ @@ -16618,12 +16767,16 @@ }, "fileCreatedAt": { "description": "File creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "fileModifiedAt": { "description": "File modification date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "filename": { @@ -16637,6 +16790,7 @@ "livePhotoVideoId": { "description": "Live photo video ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "metadata": { @@ -16652,12 +16806,7 @@ "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "required": [ @@ -16690,12 +16839,16 @@ }, "fileCreatedAt": { "description": "File creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "fileModifiedAt": { "description": "File modification date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "filename": { @@ -16719,12 +16872,7 @@ "type": "string" }, "status": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetMediaStatus" - } - ], - "description": "Upload status" + "$ref": "#/components/schemas/AssetMediaStatus" } }, "required": [ @@ -16734,6 +16882,7 @@ "type": "object" }, "AssetMediaSize": { + "description": "Asset media size", "enum": [ "original", "fullsize", @@ -16771,6 +16920,7 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "key": { @@ -16796,10 +16946,13 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16832,6 +16985,7 @@ "assetId": { "description": "Asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "key": { @@ -16839,6 +16993,7 @@ "type": "string" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16858,10 +17013,13 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16895,6 +17053,7 @@ "type": "string" }, "value": { + "additionalProperties": {}, "description": "Metadata value (object)", "type": "object" } @@ -16909,6 +17068,7 @@ "properties": { "assetId": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "boxScore": { @@ -16918,6 +17078,7 @@ }, "id": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "text": { @@ -16995,6 +17156,14 @@ ], "type": "string" }, + "AssetRejectReason": { + "description": "Rejection reason if rejected", + "enum": [ + "duplicate", + "unsupported-format" + ], + "type": "string" + }, "AssetResponseDto": { "properties": { "checksum": { @@ -17003,7 +17172,6 @@ }, "createdAt": { "description": "The UTC timestamp when the asset was originally uploaded to Immich.", - "example": "2024-01-15T20:30:00.000Z", "format": "date-time", "type": "string" }, @@ -17029,13 +17197,11 @@ }, "fileCreatedAt": { "description": "The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.", - "example": "2024-01-15T19:30:00.000Z", "format": "date-time", "type": "string" }, "fileModifiedAt": { "description": "The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.", - "example": "2024-01-16T10:15:00.000Z", "format": "date-time", "type": "string" }, @@ -17045,6 +17211,7 @@ }, "height": { "description": "Asset height", + "minimum": 0, "nullable": true, "type": "number" }, @@ -17084,10 +17251,10 @@ "type": "boolean" }, "libraryId": { - "deprecated": true, "description": "Library ID", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string", "x-immich-history": [ { @@ -17108,7 +17275,6 @@ }, "localDateTime": { "description": "The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer's local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by \"local\" days and months.", - "example": "2024-01-15T14:30:00.000Z", "format": "date-time", "type": "string" }, @@ -17138,7 +17304,6 @@ "type": "array" }, "resized": { - "deprecated": true, "description": "Is resized", "type": "boolean", "x-immich-history": [ @@ -17173,12 +17338,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type" + "$ref": "#/components/schemas/AssetTypeEnum" }, "unassignedFaces": { "items": { @@ -17188,20 +17348,15 @@ }, "updatedAt": { "description": "The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.", - "example": "2024-01-16T12:45:30.000Z", "format": "date-time", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "width": { "description": "Asset width", + "minimum": 0, "nullable": true, "type": "number" } @@ -17238,6 +17393,8 @@ "properties": { "assetCount": { "description": "Number of assets in stack", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "id": { @@ -17260,14 +17417,20 @@ "properties": { "images": { "description": "Number of images", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "total": { "description": "Total number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "videos": { "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -17288,6 +17451,14 @@ ], "type": "string" }, + "AssetUploadAction": { + "description": "Upload action", + "enum": [ + "accept", + "reject" + ], + "type": "string" + }, "AssetVisibility": { "description": "Asset visibility", "enum": [ @@ -17342,12 +17513,7 @@ "AvatarUpdate": { "properties": { "color": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" } }, "type": "object" @@ -17366,15 +17532,7 @@ "BulkIdResponseDto": { "properties": { "error": { - "description": "Error reason if failed", - "enum": [ - "duplicate", - "no_permission", - "not_found", - "unknown", - "validation" - ], - "type": "string" + "$ref": "#/components/schemas/BulkIdErrorReason" }, "errorMessage": { "type": "string" @@ -17400,6 +17558,7 @@ "description": "IDs to process", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17439,7 +17598,6 @@ "CastResponse": { "properties": { "gCastEnabled": { - "default": false, "description": "Whether Google Cast is enabled", "type": "boolean" } @@ -17531,6 +17689,8 @@ "properties": { "assetCount": { "description": "Number of assets contributed", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "userId": { @@ -17561,6 +17721,7 @@ "description": "Initial asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17583,8 +17744,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "importPaths": { "description": "Import paths (max 128)", @@ -17592,16 +17752,17 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "name": { "description": "Library name", + "minLength": 1, "type": "string" }, "ownerId": { "description": "Owner user ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -17627,7 +17788,9 @@ "properties": { "profileChangedAt": { "description": "Profile image change date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "profileImagePath": { @@ -17681,6 +17844,7 @@ "properties": { "cronExpression": { "description": "Cron expression", + "pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}", "type": "string" }, "enabled": { @@ -17703,6 +17867,7 @@ "DatabaseBackupDeleteDto": { "properties": { "backups": { + "description": "Backup filenames to delete", "items": { "type": "string" }, @@ -17717,12 +17882,15 @@ "DatabaseBackupDto": { "properties": { "filename": { + "description": "Backup filename", "type": "string" }, "filesize": { + "description": "Backup file size", "type": "number" }, "timezone": { + "description": "Backup timezone", "type": "string" } }, @@ -17736,6 +17904,7 @@ "DatabaseBackupListResponseDto": { "properties": { "backups": { + "description": "List of backups", "items": { "$ref": "#/components/schemas/DatabaseBackupDto" }, @@ -17750,6 +17919,7 @@ "DatabaseBackupUploadDto": { "properties": { "file": { + "description": "Database backup file", "format": "binary", "type": "string" } @@ -17762,6 +17932,7 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17787,6 +17958,8 @@ }, "size": { "description": "Archive size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -17801,10 +17974,12 @@ "albumId": { "description": "Album ID to download", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "archiveSize": { "description": "Archive size limit in bytes", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -17812,6 +17987,7 @@ "description": "Asset IDs to download", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17819,6 +17995,7 @@ "userId": { "description": "User ID to download assets from", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -17828,10 +18005,11 @@ "properties": { "archiveSize": { "description": "Maximum archive size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "includeEmbeddedVideos": { - "default": false, "description": "Whether to include embedded videos in downloads", "type": "boolean" } @@ -17853,6 +18031,8 @@ }, "totalSize": { "description": "Total size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -17866,6 +18046,7 @@ "properties": { "archiveSize": { "description": "Maximum archive size in bytes", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -17916,12 +18097,14 @@ "properties": { "duplicateId": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "keepAssetIds": { "description": "Asset IDs to keep", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17930,6 +18113,7 @@ "description": "Asset IDs to trash or delete", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -17959,6 +18143,7 @@ "description": "Suggested asset IDs to keep based on file size and EXIF data", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -18011,6 +18196,7 @@ "type": "object" }, "ExifResponseDto": { + "description": "EXIF response", "properties": { "city": { "default": null, @@ -18040,12 +18226,14 @@ "exifImageHeight": { "default": null, "description": "Image height in pixels", + "minimum": 0, "nullable": true, "type": "number" }, "exifImageWidth": { "default": null, "description": "Image width in pixels", + "minimum": 0, "nullable": true, "type": "number" }, @@ -18064,7 +18252,8 @@ "fileSizeInByte": { "default": null, "description": "File size in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, @@ -18155,6 +18344,7 @@ "id": { "description": "Face ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -18178,6 +18368,7 @@ }, "minFaces": { "description": "Minimum number of faces required for recognition", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -18205,12 +18396,10 @@ "FoldersResponse": { "properties": { "enabled": { - "default": false, "description": "Whether folders are enabled", "type": "boolean" }, "sidebarWeb": { - "default": false, "description": "Whether folders appear in web sidebar", "type": "boolean" } @@ -18245,12 +18434,7 @@ "JobCreateDto": { "properties": { "name": { - "allOf": [ - { - "$ref": "#/components/schemas/ManualJobName" - } - ], - "description": "Job name" + "$ref": "#/components/schemas/ManualJobName" } }, "required": [ @@ -18324,6 +18508,7 @@ "properties": { "concurrency": { "description": "Concurrency", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" } @@ -18337,11 +18522,15 @@ "properties": { "assetCount": { "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "exclusionPatterns": { @@ -18372,13 +18561,17 @@ }, "refreshedAt": { "description": "Last refresh date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -18398,24 +18591,27 @@ "LibraryStatsResponseDto": { "properties": { "photos": { - "default": 0, "description": "Number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "total": { - "default": 0, "description": "Total number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usage": { - "default": 0, "description": "Storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "videos": { - "default": 0, "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -18434,8 +18630,8 @@ "type": "string" }, "licenseKey": { - "description": "License key (format: IM(SV|CL)(-XXXX){8})", - "pattern": "/IM(SV|CL)(-[\\dA-Za-z]{4}){8}/", + "description": "License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/)", + "pattern": "^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$", "type": "string" } }, @@ -18446,30 +18642,10 @@ "type": "object" }, "LicenseResponseDto": { - "properties": { - "activatedAt": { - "description": "Activation date", - "format": "date-time", - "type": "string" - }, - "activationKey": { - "description": "Activation key", - "type": "string" - }, - "licenseKey": { - "description": "License key (format: IM(SV|CL)(-XXXX){8})", - "pattern": "/IM(SV|CL)(-[\\dA-Za-z]{4}){8}/", - "type": "string" - } - }, - "required": [ - "activatedAt", - "activationKey", - "licenseKey" - ], - "type": "object" + "$ref": "#/components/schemas/UserLicense" }, "LogLevel": { + "description": "Log level", "enum": [ "verbose", "debug", @@ -18486,6 +18662,7 @@ "description": "User email", "example": "testuser@email.com", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "password": { @@ -18528,6 +18705,8 @@ }, "userEmail": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "userId": { @@ -18627,12 +18806,7 @@ "type": "number" }, "folder": { - "allOf": [ - { - "$ref": "#/components/schemas/StorageFolder" - } - ], - "description": "Storage folder" + "$ref": "#/components/schemas/StorageFolder" }, "readable": { "description": "Whether the folder is readable", @@ -18663,12 +18837,7 @@ "MaintenanceStatusResponseDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/MaintenanceAction" - } - ], - "description": "Maintenance action" + "$ref": "#/components/schemas/MaintenanceAction" }, "active": { "type": "boolean" @@ -18690,7 +18859,7 @@ "type": "object" }, "ManualJobName": { - "description": "Job name", + "description": "Manual job name", "enum": [ "person-cleanup", "tag-cleanup", @@ -18771,12 +18940,12 @@ "MemoriesResponse": { "properties": { "duration": { - "default": 5, "description": "Memory duration in seconds", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "enabled": { - "default": true, "description": "Whether memories are enabled", "type": "boolean" } @@ -18791,6 +18960,7 @@ "properties": { "duration": { "description": "Memory duration in seconds", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -18807,6 +18977,7 @@ "description": "Asset IDs to associate with memory", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -18816,7 +18987,9 @@ }, "hideAt": { "description": "Date when memory should be hidden", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string", "x-immich-history": [ { @@ -18836,17 +19009,23 @@ }, "memoryAt": { "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "seenAt": { "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "showAt": { "description": "Date when memory should be shown", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string", "x-immich-history": [ { @@ -18861,12 +19040,7 @@ "x-immich-state": "Stable" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } - ], - "description": "Memory type" + "$ref": "#/components/schemas/MemoryType" } }, "required": [ @@ -18886,7 +19060,9 @@ }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "data": { @@ -18894,12 +19070,16 @@ }, "deletedAt": { "description": "Deletion date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "hideAt": { "description": "Date when memory should be hidden", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -18912,7 +19092,9 @@ }, "memoryAt": { "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "ownerId": { @@ -18921,25 +19103,26 @@ }, "seenAt": { "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "showAt": { "description": "Date when memory should be shown", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } - ], - "description": "Memory type" + "$ref": "#/components/schemas/MemoryType" }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -18957,6 +19140,7 @@ "type": "object" }, "MemorySearchOrder": { + "description": "Sort order", "enum": [ "asc", "desc", @@ -18968,6 +19152,8 @@ "properties": { "total": { "description": "Total number of memories", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -18977,6 +19163,7 @@ "type": "object" }, "MemoryType": { + "description": "Memory type", "enum": [ "on_this_day" ], @@ -18990,12 +19177,16 @@ }, "memoryAt": { "description": "Memory date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "seenAt": { "description": "Date when memory was seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -19007,6 +19198,7 @@ "description": "Person IDs to merge", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -19023,6 +19215,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -19043,12 +19236,16 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "description": { @@ -19070,6 +19267,7 @@ "id": { "description": "Filter by asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "isEncoded": { @@ -19101,10 +19299,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -19117,11 +19317,7 @@ "type": "string" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], + "$ref": "#/components/schemas/AssetOrder", "default": "desc", "description": "Sort order" }, @@ -19142,6 +19338,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -19188,6 +19385,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -19195,12 +19393,16 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "thumbnailPath": { @@ -19209,39 +19411,37 @@ }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "withDeleted": { "description": "Include deleted assets", @@ -19273,12 +19473,7 @@ "MirrorParameters": { "properties": { "axis": { - "allOf": [ - { - "$ref": "#/components/schemas/MirrorAxis" - } - ], - "description": "Axis to mirror along" + "$ref": "#/components/schemas/MirrorAxis" } }, "required": [ @@ -19289,6 +19484,7 @@ "NotificationCreateDto": { "properties": { "data": { + "additionalProperties": {}, "description": "Additional notification data", "type": "object" }, @@ -19298,17 +19494,14 @@ "type": "string" }, "level": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationLevel" - } - ], - "description": "Notification level" + "$ref": "#/components/schemas/NotificationLevel" }, "readAt": { "description": "Date when notification was read", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "title": { @@ -19316,16 +19509,12 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationType" - } - ], - "description": "Notification type" + "$ref": "#/components/schemas/NotificationType" }, "userId": { "description": "User ID to send notification to", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -19341,6 +19530,7 @@ "description": "Notification IDs to delete", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "minItems": 1, @@ -19356,10 +19546,13 @@ "properties": { "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "data": { + "additionalProperties": {}, "description": "Additional notification data", "type": "object" }, @@ -19372,16 +19565,13 @@ "type": "string" }, "level": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationLevel" - } - ], - "description": "Notification level" + "$ref": "#/components/schemas/NotificationLevel" }, "readAt": { "description": "Date when notification was read", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "title": { @@ -19389,12 +19579,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/NotificationType" - } - ], - "description": "Notification type" + "$ref": "#/components/schemas/NotificationType" } }, "required": [ @@ -19407,6 +19592,7 @@ "type": "object" }, "NotificationLevel": { + "description": "Notification level", "enum": [ "success", "error", @@ -19416,6 +19602,7 @@ "type": "string" }, "NotificationType": { + "description": "Notification type", "enum": [ "JobFailed", "BackupFailed", @@ -19432,6 +19619,7 @@ "description": "Notification IDs to update", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "minItems": 1, @@ -19439,8 +19627,10 @@ }, "readAt": { "description": "Date when notifications were read", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -19453,8 +19643,10 @@ "properties": { "readAt": { "description": "Date when notification was read", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -19484,6 +19676,7 @@ }, "url": { "description": "OAuth callback URL", + "minLength": 1, "type": "string" } }, @@ -19513,7 +19706,7 @@ "type": "object" }, "OAuthTokenEndpointAuthMethod": { - "description": "Token endpoint auth method", + "description": "OAuth token endpoint auth method", "enum": [ "client_secret_post", "client_secret_basic" @@ -19528,6 +19721,7 @@ }, "maxResolution": { "description": "Maximum resolution for OCR processing", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, @@ -19563,8 +19757,9 @@ "properties": { "year": { "description": "Year for on this day memory", - "minimum": 1, - "type": "number" + "maximum": 9999, + "minimum": 1000, + "type": "integer" } }, "required": [ @@ -19601,6 +19796,7 @@ "sharedWithId": { "description": "User ID to share with", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -19610,6 +19806,7 @@ "type": "object" }, "PartnerDirection": { + "description": "Partner direction", "enum": [ "shared-by", "shared-with" @@ -19617,21 +19814,21 @@ "type": "string" }, "PartnerResponseDto": { + "description": "Partner response", "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" }, "email": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "inTimeline": { @@ -19677,12 +19874,10 @@ "PeopleResponse": { "properties": { "enabled": { - "default": true, "description": "Whether people are enabled", "type": "boolean" }, "sidebarWeb": { - "default": false, "description": "Whether people appear in web sidebar", "type": "boolean" } @@ -19694,6 +19889,7 @@ "type": "object" }, "PeopleResponseDto": { + "description": "People response", "properties": { "hasNextPage": { "description": "Whether there are more pages", @@ -19712,10 +19908,11 @@ }, "hidden": { "description": "Number of hidden people", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "people": { - "description": "List of people", "items": { "$ref": "#/components/schemas/PersonResponseDto" }, @@ -19723,6 +19920,8 @@ }, "total": { "description": "Total number of people", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -19772,11 +19971,13 @@ "color": { "description": "Person color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "featureFaceAssetId": { "description": "Asset ID used for feature face thumbnail", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "id": { @@ -19974,6 +20175,7 @@ "color": { "description": "Person color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "isFavorite": { @@ -20075,6 +20277,8 @@ "properties": { "assets": { "description": "Number of assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -20094,11 +20298,13 @@ "color": { "description": "Person color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "featureFaceAssetId": { "description": "Asset ID used for feature face thumbnail", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "isFavorite": { @@ -20140,7 +20346,6 @@ "x-immich-state": "Stable" }, "faces": { - "description": "Face detections", "items": { "$ref": "#/components/schemas/AssetFaceWithoutPersonResponseDto" }, @@ -20208,16 +20413,18 @@ "properties": { "newPinCode": { "description": "New PIN code (4-6 digits)", - "example": "123456", + "pattern": "^\\d{6}$", "type": "string" }, "password": { "description": "User password (required if PIN code is not provided)", + "example": "password", "type": "string" }, "pinCode": { "description": "New PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -20230,11 +20437,13 @@ "properties": { "password": { "description": "User password (required if PIN code is not provided)", + "example": "password", "type": "string" }, "pinCode": { "description": "New PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -20245,6 +20454,7 @@ "pinCode": { "description": "PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -20302,9 +20512,13 @@ "type": "string" }, "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginJsonSchema" + } + ], "description": "Action schema", - "nullable": true, - "type": "object" + "nullable": true }, "supportedContexts": { "description": "Supported contexts", @@ -20329,8 +20543,9 @@ ], "type": "object" }, + "PluginConfigValue": {}, "PluginContextType": { - "description": "Context type", + "description": "Plugin context", "enum": [ "asset", "album", @@ -20357,9 +20572,13 @@ "type": "string" }, "schema": { + "allOf": [ + { + "$ref": "#/components/schemas/PluginJsonSchema" + } + ], "description": "Filter schema", - "nullable": true, - "type": "object" + "nullable": true }, "supportedContexts": { "description": "Supported contexts", @@ -20384,6 +20603,87 @@ ], "type": "object" }, + "PluginJsonSchema": { + "properties": { + "additionalProperties": { + "type": "boolean" + }, + "description": { + "type": "string" + }, + "properties": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/PluginJsonSchemaType" + } + }, + "type": "object" + }, + "PluginJsonSchemaProperty": { + "properties": { + "additionalProperties": { + "anyOf": [ + { + "type": "boolean" + }, + { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + } + ] + }, + "default": {}, + "description": { + "type": "string" + }, + "enum": { + "items": { + "type": "string" + }, + "type": "array" + }, + "items": { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + }, + "properties": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginJsonSchemaProperty" + }, + "type": "object" + }, + "required": { + "items": { + "type": "string" + }, + "type": "array" + }, + "type": { + "$ref": "#/components/schemas/PluginJsonSchemaType" + } + }, + "type": "object" + }, + "PluginJsonSchemaType": { + "enum": [ + "string", + "number", + "integer", + "boolean", + "object", + "array", + "null" + ], + "type": "string" + }, "PluginResponseDto": { "properties": { "actions": { @@ -20450,20 +20750,10 @@ "PluginTriggerResponseDto": { "properties": { "contextType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginContextType" - } - ], - "description": "Context type" + "$ref": "#/components/schemas/PluginContextType" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "required": [ @@ -20473,7 +20763,7 @@ "type": "object" }, "PluginTriggerType": { - "description": "Trigger type", + "description": "Plugin trigger type", "enum": [ "AssetCreate", "PersonRecognized" @@ -20524,12 +20814,7 @@ "QueueCommandDto": { "properties": { "command": { - "allOf": [ - { - "$ref": "#/components/schemas/QueueCommand" - } - ], - "description": "Queue command to execute" + "$ref": "#/components/schemas/QueueCommand" }, "force": { "description": "Force the command execution (if applicable)", @@ -20564,6 +20849,7 @@ "QueueJobResponseDto": { "properties": { "data": { + "additionalProperties": {}, "description": "Job data payload", "type": "object" }, @@ -20572,15 +20858,12 @@ "type": "string" }, "name": { - "allOf": [ - { - "$ref": "#/components/schemas/JobName" - } - ], - "description": "Job name" + "$ref": "#/components/schemas/JobName" }, "timestamp": { "description": "Job creation timestamp", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -20592,6 +20875,7 @@ "type": "object" }, "QueueJobStatus": { + "description": "Queue job status", "enum": [ "active", "failed", @@ -20603,6 +20887,7 @@ "type": "string" }, "QueueName": { + "description": "Queue name", "enum": [ "thumbnailGeneration", "metadataExtraction", @@ -20632,12 +20917,7 @@ "type": "boolean" }, "name": { - "allOf": [ - { - "$ref": "#/components/schemas/QueueName" - } - ], - "description": "Queue name" + "$ref": "#/components/schemas/QueueName" }, "statistics": { "$ref": "#/components/schemas/QueueStatisticsDto" @@ -20669,26 +20949,38 @@ "properties": { "active": { "description": "Number of active jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "completed": { "description": "Number of completed jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "delayed": { "description": "Number of delayed jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "failed": { "description": "Number of failed jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "paused": { "description": "Number of paused jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "waiting": { "description": "Number of waiting jobs", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -20813,6 +21105,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -20829,12 +21122,16 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "deviceId": { @@ -20870,10 +21167,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -20889,6 +21188,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -20931,6 +21231,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -20938,49 +21239,51 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "withDeleted": { "description": "Include deleted assets", @@ -21004,7 +21307,6 @@ "RatingsResponse": { "properties": { "enabled": { - "default": false, "description": "Whether ratings are enabled", "type": "boolean" } @@ -21024,6 +21326,7 @@ "type": "object" }, "ReactionLevel": { + "description": "Reaction level", "enum": [ "album", "asset" @@ -21031,6 +21334,7 @@ "type": "string" }, "ReactionType": { + "description": "Reaction type", "enum": [ "comment", "like" @@ -21072,6 +21376,8 @@ "properties": { "count": { "description": "Number of albums in this page", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "facets": { @@ -21088,6 +21394,8 @@ }, "total": { "description": "Total number of matching albums", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -21103,6 +21411,8 @@ "properties": { "count": { "description": "Number of assets in this page", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "facets": { @@ -21124,6 +21434,8 @@ }, "total": { "description": "Total number of matching assets", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" } }, @@ -21175,6 +21487,8 @@ "properties": { "count": { "description": "Number of assets with this facet value", + "maximum": 9007199254740991, + "minimum": 0, "type": "integer" }, "value": { @@ -21191,7 +21505,6 @@ "SearchFacetResponseDto": { "properties": { "counts": { - "description": "Facet counts", "items": { "$ref": "#/components/schemas/SearchFacetCountResponseDto" }, @@ -21227,6 +21540,8 @@ "properties": { "total": { "description": "Total number of matching assets", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21236,6 +21551,7 @@ "type": "object" }, "SearchSuggestionType": { + "description": "Suggestion type", "enum": [ "country", "state", @@ -21407,10 +21723,14 @@ }, "trashDays": { "description": "Number of days before trashed assets are permanently deleted", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "userDeleteDelay": { "description": "Delay in days before deleted users are permanently removed", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21546,7 +21866,6 @@ "properties": { "res": { "example": "pong", - "readOnly": true, "type": "string" } }, @@ -21558,48 +21877,40 @@ "ServerStatsResponseDto": { "properties": { "photos": { - "default": 0, "description": "Total number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usage": { - "default": 0, "description": "Total storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usageByUser": { - "default": [], - "example": [ - { - "photos": 1, - "videos": 1, - "diskUsageRaw": 2, - "usagePhotos": 1, - "usageVideos": 1 - } - ], + "description": "Array of usage for each user", "items": { "$ref": "#/components/schemas/UsageByUserDto" }, - "title": "Array of usage for each user", "type": "array" }, "usagePhotos": { - "default": 0, "description": "Storage usage for photos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usageVideos": { - "default": 0, "description": "Storage usage for videos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "videos": { - "default": 0, "description": "Total number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21621,7 +21932,8 @@ }, "diskAvailableRaw": { "description": "Available disk space in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "diskSize": { @@ -21630,7 +21942,8 @@ }, "diskSizeRaw": { "description": "Total disk size in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "diskUsagePercentage": { @@ -21644,7 +21957,8 @@ }, "diskUseRaw": { "description": "Used disk space in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21675,7 +21989,9 @@ "properties": { "createdAt": { "description": "When this version was first seen", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -21698,14 +22014,20 @@ "properties": { "major": { "description": "Major version number", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "minor": { "description": "Minor version number", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "patch": { "description": "Patch version number", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -21847,11 +22169,13 @@ "properties": { "password": { "description": "User password (required if PIN code is not provided)", + "example": "password", "type": "string" }, "pinCode": { "description": "New PIN code (4-6 digits)", "example": "123456", + "pattern": "^\\d{6}$", "type": "string" } }, @@ -21869,12 +22193,7 @@ "SetMaintenanceModeDto": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/MaintenanceAction" - } - ], - "description": "Maintenance action" + "$ref": "#/components/schemas/MaintenanceAction" }, "restoreBackupFilename": { "description": "Restore backup filename", @@ -21891,6 +22210,7 @@ "albumId": { "description": "Album ID (for album sharing)", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "allowDownload": { @@ -21906,6 +22226,7 @@ "description": "Asset IDs (for individual assets)", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -21918,8 +22239,10 @@ "expiresAt": { "default": null, "description": "Expiration date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "password": { @@ -21938,12 +22261,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/SharedLinkType" - } - ], - "description": "Shared link type" + "$ref": "#/components/schemas/SharedLinkType" } }, "required": [ @@ -21972,8 +22290,10 @@ }, "expiresAt": { "description": "Expiration date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "password": { @@ -22007,6 +22327,7 @@ "type": "object" }, "SharedLinkResponseDto": { + "description": "Shared link response", "properties": { "album": { "$ref": "#/components/schemas/AlbumResponseDto" @@ -22027,7 +22348,9 @@ }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "description": { @@ -22037,8 +22360,10 @@ }, "expiresAt": { "description": "Expiration date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -22085,12 +22410,7 @@ "x-immich-state": "Deprecated" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/SharedLinkType" - } - ], - "description": "Shared link type" + "$ref": "#/components/schemas/SharedLinkType" }, "userId": { "description": "Owner user ID", @@ -22125,12 +22445,10 @@ "SharedLinksResponse": { "properties": { "enabled": { - "default": true, "description": "Whether shared links are enabled", "type": "boolean" }, "sidebarWeb": { - "default": false, "description": "Whether shared links appear in web sidebar", "type": "boolean" } @@ -22160,6 +22478,7 @@ "description": "User email", "example": "testuser@email.com", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "name": { @@ -22186,6 +22505,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22202,12 +22522,16 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "deviceId": { @@ -22247,10 +22571,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -22271,6 +22597,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22282,6 +22609,7 @@ "queryAssetId": { "description": "Asset ID to use as search reference", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "rating": { @@ -22322,6 +22650,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -22329,49 +22658,51 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "withDeleted": { "description": "Include deleted assets", @@ -22399,6 +22730,7 @@ "description": "Asset IDs (first becomes primary, min 2)", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "minItems": 2, @@ -22411,9 +22743,9 @@ "type": "object" }, "StackResponseDto": { + "description": "Stack response", "properties": { "assets": { - "description": "Stack assets", "items": { "$ref": "#/components/schemas/AssetResponseDto" }, @@ -22440,6 +22772,7 @@ "primaryAssetId": { "description": "Primary asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -22451,6 +22784,7 @@ "description": "Filter by album IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22467,12 +22801,16 @@ }, "createdAfter": { "description": "Filter by creation date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "createdBefore": { "description": "Filter by creation date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "description": { @@ -22512,10 +22850,12 @@ "description": "Library ID to filter by", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "make": { "description": "Filter by camera make", + "nullable": true, "type": "string" }, "model": { @@ -22531,6 +22871,7 @@ "description": "Filter by person IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -22567,6 +22908,7 @@ "description": "Filter by tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "nullable": true, @@ -22574,49 +22916,51 @@ }, "takenAfter": { "description": "Filter by taken date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "takenBefore": { "description": "Filter by taken date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedAfter": { "description": "Filter by trash date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "trashedBefore": { "description": "Filter by trash date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type filter" + "$ref": "#/components/schemas/AssetTypeEnum" }, "updatedAfter": { "description": "Filter by update date (after)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "updatedBefore": { "description": "Filter by update date (before)", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Filter by visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "type": "object" @@ -22652,12 +22996,7 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/SyncEntityType" - } - ], - "description": "Sync entity type" + "$ref": "#/components/schemas/SyncEntityType" } }, "required": [ @@ -22756,12 +23095,7 @@ "type": "string" }, "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" }, "userId": { "description": "User ID", @@ -22779,7 +23113,9 @@ "properties": { "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "description": { @@ -22799,11 +23135,7 @@ "type": "string" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ] + "$ref": "#/components/schemas/AssetOrder" }, "ownerId": { "description": "Owner ID", @@ -22816,7 +23148,9 @@ }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -22848,6 +23182,7 @@ "SyncAssetEditDeleteV1": { "properties": { "editId": { + "description": "Edit ID", "type": "string" } }, @@ -22859,22 +23194,25 @@ "SyncAssetEditV1": { "properties": { "action": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetEditAction" - } - ] + "$ref": "#/components/schemas/AssetEditAction" }, "assetId": { + "description": "Asset ID", "type": "string" }, "id": { + "description": "Edit ID", "type": "string" }, "parameters": { + "additionalProperties": {}, + "description": "Edit parameters", "type": "object" }, "sequence": { + "description": "Edit sequence", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -22905,8 +23243,10 @@ }, "dateTimeOriginal": { "description": "Date time original", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "description": { @@ -22916,11 +23256,15 @@ }, "exifImageHeight": { "description": "Exif image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, "exifImageWidth": { "description": "Exif image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -22937,6 +23281,8 @@ }, "fileSizeInByte": { "description": "File size in byte", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -22954,6 +23300,8 @@ }, "iso": { "description": "ISO", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -22986,8 +23334,10 @@ }, "modifyDate": { "description": "Modify date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "orientation": { @@ -23007,6 +23357,8 @@ }, "rating": { "description": "Rating", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -23069,15 +23421,27 @@ "type": "string" }, "boundingBoxX1": { + "description": "Bounding box X1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { + "description": "Bounding box X2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { + "description": "Bounding box Y1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { + "description": "Bounding box Y2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "id": { @@ -23085,9 +23449,15 @@ "type": "string" }, "imageHeight": { + "description": "Image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageWidth": { + "description": "Image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "personId": { @@ -23121,21 +23491,35 @@ "type": "string" }, "boundingBoxX1": { + "description": "Bounding box X1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxX2": { + "description": "Bounding box X2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY1": { + "description": "Bounding box Y1", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "boundingBoxY2": { + "description": "Bounding box Y2", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "deletedAt": { "description": "Face deleted at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -23143,9 +23527,15 @@ "type": "string" }, "imageHeight": { + "description": "Image height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "imageWidth": { + "description": "Image width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "isVisible": { @@ -23206,6 +23596,7 @@ "type": "string" }, "value": { + "additionalProperties": {}, "description": "Value", "type": "object" } @@ -23225,8 +23616,10 @@ }, "deletedAt": { "description": "Deleted at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "duration": { @@ -23236,18 +23629,24 @@ }, "fileCreatedAt": { "description": "File created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "fileModifiedAt": { "description": "File modified at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "height": { "description": "Asset height", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, @@ -23275,8 +23674,10 @@ }, "localDateTime": { "description": "Local date time", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "originalFileName": { @@ -23298,23 +23699,15 @@ "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetTypeEnum" - } - ], - "description": "Asset type" + "$ref": "#/components/schemas/AssetTypeEnum" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" }, "width": { "description": "Asset width", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" } @@ -23350,13 +23743,14 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "User avatar color", "nullable": true }, "deletedAt": { "description": "User deleted at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "email": { @@ -23390,14 +23784,22 @@ }, "profileChangedAt": { "description": "User profile changed at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "quotaSizeInBytes": { + "description": "Quota size in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, "quotaUsageInBytes": { + "description": "Quota usage in bytes", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "storageLabel": { @@ -23407,7 +23809,6 @@ } }, "required": [ - "avatarColor", "deletedAt", "email", "hasProfileImage", @@ -23533,23 +23934,30 @@ "properties": { "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "data": { + "additionalProperties": {}, "description": "Data", "type": "object" }, "deletedAt": { "description": "Deleted at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "hideAt": { "description": "Hide at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -23562,7 +23970,9 @@ }, "memoryAt": { "description": "Memory at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "ownerId": { @@ -23571,27 +23981,28 @@ }, "seenAt": { "description": "Seen at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "showAt": { "description": "Show at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "type": { - "allOf": [ - { - "$ref": "#/components/schemas/MemoryType" - } - ], - "description": "Memory type" + "$ref": "#/components/schemas/MemoryType" }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -23666,8 +24077,10 @@ "properties": { "birthDate": { "description": "Birth date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "color": { @@ -23677,7 +24090,9 @@ }, "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "faceAssetId": { @@ -23707,7 +24122,9 @@ }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -23726,7 +24143,7 @@ "type": "object" }, "SyncRequestType": { - "description": "Sync request types", + "description": "Sync request type", "enum": [ "AlbumsV1", "AlbumUsersV1", @@ -23773,7 +24190,9 @@ "properties": { "createdAt": { "description": "Created at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "id": { @@ -23790,7 +24209,9 @@ }, "updatedAt": { "description": "Updated at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -23837,12 +24258,7 @@ "SyncUserMetadataDeleteV1": { "properties": { "key": { - "allOf": [ - { - "$ref": "#/components/schemas/UserMetadataKey" - } - ], - "description": "User metadata key" + "$ref": "#/components/schemas/UserMetadataKey" }, "userId": { "description": "User ID", @@ -23858,18 +24274,14 @@ "SyncUserMetadataV1": { "properties": { "key": { - "allOf": [ - { - "$ref": "#/components/schemas/UserMetadataKey" - } - ], - "description": "User metadata key" + "$ref": "#/components/schemas/UserMetadataKey" }, "userId": { "description": "User ID", "type": "string" }, "value": { + "additionalProperties": {}, "description": "User metadata value", "type": "object" } @@ -23889,13 +24301,14 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "User avatar color", "nullable": true }, "deletedAt": { "description": "User deleted at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "email": { @@ -23916,12 +24329,13 @@ }, "profileChangedAt": { "description": "User profile changed at", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, "required": [ - "avatarColor", "deletedAt", "email", "hasProfileImage", @@ -23943,6 +24357,7 @@ "type": "object" }, "SystemConfigDto": { + "description": "System configuration", "properties": { "backup": { "$ref": "#/components/schemas/SystemConfigBackupsDto" @@ -24036,12 +24451,7 @@ "SystemConfigFFmpegDto": { "properties": { "accel": { - "allOf": [ - { - "$ref": "#/components/schemas/TranscodeHWAccel" - } - ], - "description": "Transcode hardware acceleration" + "$ref": "#/components/schemas/TranscodeHWAccel" }, "accelDecode": { "description": "Accelerated decode", @@ -24075,12 +24485,7 @@ "type": "integer" }, "cqMode": { - "allOf": [ - { - "$ref": "#/components/schemas/CQMode" - } - ], - "description": "CQ mode" + "$ref": "#/components/schemas/CQMode" }, "crf": { "description": "CRF", @@ -24090,6 +24495,7 @@ }, "gopSize": { "description": "GOP size", + "maximum": 9007199254740991, "minimum": 0, "type": "integer" }, @@ -24112,24 +24518,14 @@ "type": "integer" }, "targetAudioCodec": { - "allOf": [ - { - "$ref": "#/components/schemas/AudioCodec" - } - ], - "description": "Target audio codec" + "$ref": "#/components/schemas/AudioCodec" }, "targetResolution": { "description": "Target resolution", "type": "string" }, "targetVideoCodec": { - "allOf": [ - { - "$ref": "#/components/schemas/VideoCodec" - } - ], - "description": "Target video codec" + "$ref": "#/components/schemas/VideoCodec" }, "temporalAQ": { "description": "Temporal AQ", @@ -24137,24 +24533,15 @@ }, "threads": { "description": "Threads", + "maximum": 9007199254740991, "minimum": 0, "type": "integer" }, "tonemap": { - "allOf": [ - { - "$ref": "#/components/schemas/ToneMapping" - } - ], - "description": "Tone mapping" + "$ref": "#/components/schemas/ToneMapping" }, "transcode": { - "allOf": [ - { - "$ref": "#/components/schemas/TranscodePolicy" - } - ], - "description": "Transcode policy" + "$ref": "#/components/schemas/TranscodePolicy" }, "twoPass": { "description": "Two pass", @@ -24205,15 +24592,9 @@ "type": "boolean" }, "format": { - "allOf": [ - { - "$ref": "#/components/schemas/ImageFormat" - } - ], - "description": "Image format" + "$ref": "#/components/schemas/ImageFormat" }, "progressive": { - "default": false, "description": "Progressive", "type": "boolean" }, @@ -24234,15 +24615,10 @@ "SystemConfigGeneratedImageDto": { "properties": { "format": { - "allOf": [ - { - "$ref": "#/components/schemas/ImageFormat" - } - ], - "description": "Image format" + "$ref": "#/components/schemas/ImageFormat" }, "progressive": { - "default": false, + "description": "Progressive", "type": "boolean" }, "quality": { @@ -24253,6 +24629,7 @@ }, "size": { "description": "Size", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" } @@ -24267,12 +24644,7 @@ "SystemConfigImageDto": { "properties": { "colorspace": { - "allOf": [ - { - "$ref": "#/components/schemas/Colorspace" - } - ], - "description": "Colorspace" + "$ref": "#/components/schemas/Colorspace" }, "extractEmbedded": { "description": "Extract embedded", @@ -24378,6 +24750,8 @@ "SystemConfigLibraryScanDto": { "properties": { "cronExpression": { + "description": "Cron expression", + "pattern": "(((\\d+,)+\\d+|(\\d+(\\/|-)\\d+)|\\d+|\\*) ?){5,7}", "type": "string" }, "enabled": { @@ -24410,11 +24784,7 @@ "type": "boolean" }, "level": { - "allOf": [ - { - "$ref": "#/components/schemas/LogLevel" - } - ] + "$ref": "#/components/schemas/LogLevel" } }, "required": [ @@ -24445,9 +24815,8 @@ "$ref": "#/components/schemas/OcrConfig" }, "urls": { - "format": "uri", + "description": "ML service URLs", "items": { - "format": "uri", "type": "string" }, "minItems": 1, @@ -24468,6 +24837,7 @@ "SystemConfigMapDto": { "properties": { "darkStyle": { + "description": "Dark map style URL", "format": "uri", "type": "string" }, @@ -24476,6 +24846,7 @@ "type": "boolean" }, "lightStyle": { + "description": "Light map style URL", "format": "uri", "type": "string" } @@ -24529,6 +24900,8 @@ "type": "boolean" }, "startTime": { + "description": "Start time", + "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$", "type": "string" }, "syncQuotaUsage": { @@ -24581,10 +24954,9 @@ }, "defaultStorageQuota": { "description": "Default storage quota", - "format": "int64", "minimum": 0, "nullable": true, - "type": "integer" + "type": "number" }, "enabled": { "description": "Enabled", @@ -24599,8 +24971,7 @@ "type": "boolean" }, "mobileRedirectUri": { - "description": "Mobile redirect URI", - "format": "uri", + "description": "Mobile redirect URI (set to empty string to disable)", "type": "string" }, "profileSigningAlgorithm": { @@ -24616,6 +24987,7 @@ "type": "string" }, "signingAlgorithm": { + "description": "Signing algorithm", "type": "string" }, "storageLabelClaim": { @@ -24628,16 +25000,12 @@ }, "timeout": { "description": "Timeout", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" }, "tokenEndpointAuthMethod": { - "allOf": [ - { - "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" - } - ], - "description": "Token endpoint auth method" + "$ref": "#/components/schemas/OAuthTokenEndpointAuthMethod" } }, "required": [ @@ -24690,7 +25058,6 @@ "properties": { "externalDomain": { "description": "External domain", - "format": "uri", "type": "string" }, "loginPageMessage": { @@ -24799,12 +25166,15 @@ "SystemConfigTemplateEmailsDto": { "properties": { "albumInviteTemplate": { + "description": "Album invite template", "type": "string" }, "albumUpdateTemplate": { + "description": "Album update template", "type": "string" }, "welcomeTemplate": { + "description": "Welcome template", "type": "string" } }, @@ -24913,6 +25283,7 @@ "properties": { "days": { "description": "Days", + "maximum": 9007199254740991, "minimum": 0, "type": "integer" }, @@ -24931,6 +25302,7 @@ "properties": { "deleteDelay": { "description": "Delete delay", + "maximum": 9007199254740991, "minimum": 1, "type": "integer" } @@ -24946,6 +25318,7 @@ "description": "Asset IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -24954,6 +25327,7 @@ "description": "Tag IDs", "items": { "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "type": "array" @@ -24969,6 +25343,8 @@ "properties": { "count": { "description": "Number of assets tagged", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -24981,7 +25357,8 @@ "properties": { "color": { "description": "Tag color (hex)", - "pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$", + "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" }, "name": { @@ -24992,6 +25369,7 @@ "description": "Parent tag ID", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -25047,6 +25425,7 @@ "color": { "description": "Tag color (hex)", "nullable": true, + "pattern": "^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$", "type": "string" } }, @@ -25070,12 +25449,10 @@ "TagsResponse": { "properties": { "enabled": { - "default": true, "description": "Whether tags are enabled", "type": "boolean" }, "sidebarWeb": { - "default": true, "description": "Whether tags appear in web sidebar", "type": "boolean" } @@ -25307,6 +25684,8 @@ "count": { "description": "Number of assets in this time bucket", "example": 42, + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "timeBucket": { @@ -25357,6 +25736,8 @@ "properties": { "count": { "description": "Number of items in trash", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -25374,6 +25755,7 @@ "albumThumbnailAssetId": { "description": "Album thumbnail asset ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "description": { @@ -25385,12 +25767,7 @@ "type": "boolean" }, "order": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetOrder" - } - ], - "description": "Asset sort order" + "$ref": "#/components/schemas/AssetOrder" } }, "type": "object" @@ -25398,12 +25775,7 @@ "UpdateAlbumUserDto": { "properties": { "role": { - "allOf": [ - { - "$ref": "#/components/schemas/AlbumUserRole" - } - ], - "description": "Album user role" + "$ref": "#/components/schemas/AlbumUserRole" } }, "required": [ @@ -25427,16 +25799,21 @@ }, "latitude": { "description": "Latitude coordinate", + "maximum": 90, + "minimum": -90, "type": "number" }, "livePhotoVideoId": { "description": "Live photo video ID", "format": "uuid", "nullable": true, + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "longitude": { "description": "Longitude coordinate", + "maximum": 180, + "minimum": -180, "type": "number" }, "rating": { @@ -25444,7 +25821,7 @@ "maximum": 5, "minimum": -1, "nullable": true, - "type": "number", + "type": "integer", "x-immich-history": [ { "version": "v1", @@ -25463,12 +25840,7 @@ "x-immich-state": "Stable" }, "visibility": { - "allOf": [ - { - "$ref": "#/components/schemas/AssetVisibility" - } - ], - "description": "Asset visibility" + "$ref": "#/components/schemas/AssetVisibility" } }, "type": "object" @@ -25481,8 +25853,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "importPaths": { "description": "Import paths (max 128)", @@ -25490,11 +25861,11 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "name": { "description": "Library name", + "minLength": 1, "type": "string" } }, @@ -25504,27 +25875,33 @@ "properties": { "photos": { "description": "Number of photos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "quotaSizeInBytes": { "description": "User quota size in bytes (null if unlimited)", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "nullable": true, "type": "integer" }, "usage": { "description": "Total storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usagePhotos": { "description": "Storage usage for photos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "usageVideos": { "description": "Storage usage for videos in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" }, "userId": { @@ -25537,6 +25914,8 @@ }, "videos": { "description": "Number of videos", + "maximum": 9007199254740991, + "minimum": -9007199254740991, "type": "integer" } }, @@ -25560,12 +25939,12 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "Avatar color", "nullable": true }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "isAdmin": { @@ -25588,11 +25967,12 @@ "description": "PIN code", "example": "123456", "nullable": true, + "pattern": "^\\d{6}$", "type": "string" }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, "minimum": 0, "nullable": true, "type": "integer" @@ -25626,30 +26006,33 @@ "UserAdminResponseDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" }, "createdAt": { "description": "Creation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "deletedAt": { "description": "Deletion date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", "nullable": true, + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "email": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "isAdmin": { @@ -25662,7 +26045,6 @@ "$ref": "#/components/schemas/UserLicense" } ], - "description": "User license", "nullable": true }, "name": { @@ -25684,13 +26066,15 @@ }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, "quotaUsageInBytes": { "description": "Storage usage in bytes", - "format": "int64", + "maximum": 9007199254740991, + "minimum": 0, "nullable": true, "type": "integer" }, @@ -25699,12 +26083,7 @@ "type": "boolean" }, "status": { - "allOf": [ - { - "$ref": "#/components/schemas/UserStatus" - } - ], - "description": "User status" + "$ref": "#/components/schemas/UserStatus" }, "storageLabel": { "description": "Storage label", @@ -25713,7 +26092,9 @@ }, "updatedAt": { "description": "Last update date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" } }, @@ -25746,12 +26127,12 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "Avatar color", "nullable": true }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "isAdmin": { @@ -25770,11 +26151,12 @@ "description": "PIN code", "example": "123456", "nullable": true, + "pattern": "^\\d{6}$", "type": "string" }, "quotaSizeInBytes": { "description": "Storage quota in bytes", - "format": "int64", + "maximum": 9007199254740991, "minimum": 0, "nullable": true, "type": "integer" @@ -25792,7 +26174,7 @@ "type": "object" }, "UserAvatarColor": { - "description": "Avatar color", + "description": "User avatar color", "enum": [ "primary", "pink", @@ -25811,7 +26193,9 @@ "properties": { "activatedAt": { "description": "Activation date", + "example": "2024-01-01T00:00:00.000Z", "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", "type": "string" }, "activationKey": { @@ -25819,7 +26203,8 @@ "type": "string" }, "licenseKey": { - "description": "License key", + "description": "License key (format: /^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$/)", + "pattern": "^IM(SV|CL)(-[\\dA-Za-z]{4}){8}$", "type": "string" } }, @@ -25934,19 +26319,18 @@ "UserResponseDto": { "properties": { "avatarColor": { - "allOf": [ - { - "$ref": "#/components/schemas/UserAvatarColor" - } - ], - "description": "Avatar color" + "$ref": "#/components/schemas/UserAvatarColor" }, "email": { "description": "User email", + "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "id": { "description": "User ID", + "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" }, "name": { @@ -25990,12 +26374,12 @@ "$ref": "#/components/schemas/UserAvatarColor" } ], - "description": "Avatar color", "nullable": true }, "email": { "description": "User email", "format": "email", + "pattern": "^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$", "type": "string" }, "name": { @@ -26003,6 +26387,7 @@ "type": "string" }, "password": { + "deprecated": true, "description": "User password (deprecated, use change password endpoint)", "type": "string" } @@ -26029,8 +26414,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" }, "importPaths": { "description": "Import paths to validate (max 128)", @@ -26038,8 +26422,7 @@ "type": "string" }, "maxItems": 128, - "type": "array", - "uniqueItems": true + "type": "array" } }, "type": "object" @@ -26051,7 +26434,6 @@ "type": "string" }, "isValid": { - "default": false, "description": "Is valid", "type": "boolean" }, @@ -26108,7 +26490,7 @@ "type": "string" }, "VideoContainer": { - "description": "Accepted containers", + "description": "Accepted video containers", "enum": [ "mov", "mp4", @@ -26117,15 +26499,21 @@ ], "type": "string" }, + "WorkflowActionConfig": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginConfigValue" + }, + "type": "object" + }, "WorkflowActionItemDto": { "properties": { "actionConfig": { - "description": "Action configuration", - "type": "object" + "$ref": "#/components/schemas/WorkflowActionConfig" }, "pluginActionId": { "description": "Plugin action ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -26137,9 +26525,12 @@ "WorkflowActionResponseDto": { "properties": { "actionConfig": { - "description": "Action configuration", - "nullable": true, - "type": "object" + "allOf": [ + { + "$ref": "#/components/schemas/WorkflowActionConfig" + } + ], + "nullable": true }, "id": { "description": "Action ID", @@ -26196,12 +26587,7 @@ "type": "string" }, "triggerType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Workflow trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "required": [ @@ -26212,15 +26598,21 @@ ], "type": "object" }, + "WorkflowFilterConfig": { + "additionalProperties": { + "$ref": "#/components/schemas/PluginConfigValue" + }, + "type": "object" + }, "WorkflowFilterItemDto": { "properties": { "filterConfig": { - "description": "Filter configuration", - "type": "object" + "$ref": "#/components/schemas/WorkflowFilterConfig" }, "pluginFilterId": { "description": "Plugin filter ID", "format": "uuid", + "pattern": "^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12})$", "type": "string" } }, @@ -26232,9 +26624,12 @@ "WorkflowFilterResponseDto": { "properties": { "filterConfig": { - "description": "Filter configuration", - "nullable": true, - "type": "object" + "allOf": [ + { + "$ref": "#/components/schemas/WorkflowFilterConfig" + } + ], + "nullable": true }, "id": { "description": "Filter ID", @@ -26304,12 +26699,7 @@ "type": "string" }, "triggerType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Workflow trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "required": [ @@ -26354,12 +26744,7 @@ "type": "string" }, "triggerType": { - "allOf": [ - { - "$ref": "#/components/schemas/PluginTriggerType" - } - ], - "description": "Workflow trigger type" + "$ref": "#/components/schemas/PluginTriggerType" } }, "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index d74c2dd3e2c9..365187e6a769 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 9dd140fbc368..077f7a6785b6 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 bd3f5b0d69c8..73ea7f6f4509 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 f2b6a7e80502..ae930762d003 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 b63233206996..5be9ae29b91d 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 bf2038048fc1..7ac6e051f6b8 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 d13227555b2f..fadc5103ebb0 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 c6dab09a3ce5..23a1f8b98c99 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 c2f6aeacefae..0bfb4238987e 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 69bf1f6443c3..4a8d4b35826c 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 7dd145ff5cc5..a61397e75cd7 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 66598b992018..3e11b628e310 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 094028687e31..07c0149463a9 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 820819ee6ec4..4ed32ee27177 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 a64aee291266..e9886ebb07c0 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 2c507a634fc2..0661e9121bb3 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 a28ac9b659b1..c6c0a1c91fb3 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 adbc8be0f3fc..4df247031a90 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 c1f19ddd6606..07b0d7199f4b 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 bbd1241dc584..a07dee64ad2f 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 60fc3d65aea6..edd0f27980d7 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 6d0276c6a37b..f4c18235e4b9 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 edda974476dc..048f94df5abc 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 4f339624e68f..f8065ffd2cf3 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 6464d88508cb..7b8ba34c910f 100644 --- a/server/src/dtos/activity.dto.ts +++ b/server/src/dtos/activity.dto.ts @@ -1,76 +1,68 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsNotEmpty, IsString, ValidateIf } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Activity } from 'src/database'; -import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { ValidateEnum, ValidateUUID } from 'src/validation'; - -export enum ReactionType { - COMMENT = 'comment', - LIKE = 'like', -} +import { mapUser, UserResponseSchema } from 'src/dtos/user.dto'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; export enum ReactionLevel { ALBUM = 'album', ASSET = 'asset', } +const ReactionLevelSchema = z.enum(ReactionLevel).describe('Reaction level').meta({ id: 'ReactionLevel' }); + +export enum ReactionType { + COMMENT = 'comment', + LIKE = 'like', +} +const ReactionTypeSchema = z.enum(ReactionType).describe('Reaction type').meta({ id: 'ReactionType' }); export type MaybeDuplicate = { duplicate: boolean; value: T }; -export class ActivityResponseDto { - @ApiProperty({ description: 'Activity ID' }) - id!: string; - @ApiProperty({ description: 'Creation date', format: 'date-time' }) - createdAt!: Date; - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type' }) - type!: ReactionType; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - user!: UserResponseDto; - @ApiProperty({ description: 'Asset ID (if activity is for an asset)' }) - assetId!: string | null; - @ApiPropertyOptional({ description: 'Comment text (for comment activities)' }) - comment?: string | null; -} +const ActivityResponseSchema = z + .object({ + id: z.uuidv4().describe('Activity ID'), + createdAt: isoDatetimeToDate.describe('Creation date'), + user: UserResponseSchema, + assetId: z.uuidv4().nullable().describe('Asset ID (if activity is for an asset)'), + type: ReactionTypeSchema, + comment: z.string().nullish().describe('Comment text (for comment activities)'), + }) + .meta({ id: 'ActivityResponseDto' }); -export class ActivityStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of comments' }) - comments!: number; +const ActivityStatisticsResponseSchema = z + .object({ + comments: z.int().min(0).describe('Number of comments'), + likes: z.int().min(0).describe('Number of likes'), + }) + .meta({ id: 'ActivityStatisticsResponseDto' }); - @ApiProperty({ type: 'integer', description: 'Number of likes' }) - likes!: number; -} +const ActivitySchema = z + .object({ + albumId: z.uuidv4().describe('Album ID'), + assetId: z.uuidv4().optional().describe('Asset ID (if activity is for an asset)'), + }) + .describe('Activity'); -export class ActivityDto { - @ValidateUUID({ description: 'Album ID' }) - albumId!: string; +const ActivitySearchSchema = ActivitySchema.extend({ + type: ReactionTypeSchema.optional(), + level: ReactionLevelSchema.optional(), + userId: z.uuidv4().optional().describe('Filter by user ID'), +}).describe('Activity search'); - @ValidateUUID({ optional: true, description: 'Asset ID (if activity is for an asset)' }) - assetId?: string; -} - -export class ActivitySearchDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Filter by activity type', optional: true }) - type?: ReactionType; - - @ValidateEnum({ enum: ReactionLevel, name: 'ReactionLevel', description: 'Filter by activity level', optional: true }) - level?: ReactionLevel; - - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} - -const isComment = (dto: ActivityCreateDto) => dto.type === ReactionType.COMMENT; - -export class ActivityCreateDto extends ActivityDto { - @ValidateEnum({ enum: ReactionType, name: 'ReactionType', description: 'Activity type (like or comment)' }) - type!: ReactionType; - - @ApiPropertyOptional({ description: 'Comment text (required if type is comment)' }) - @ValidateIf(isComment) - @IsNotEmpty() - @IsString() - comment?: string; -} +const ActivityCreateSchema = ActivitySchema.extend({ + type: ReactionTypeSchema, + assetId: z.uuidv4().optional().describe('Asset ID (if activity is for an asset)'), + comment: z.string().optional().describe('Comment text (required if type is comment)'), +}) + .refine((data) => data.type !== ReactionType.COMMENT || (data.comment && data.comment.trim() !== ''), { + error: 'Comment is required when type is COMMENT', + path: ['comment'], + }) + .refine((data) => data.type === ReactionType.COMMENT || !data.comment, { + error: 'Comment must not be provided when type is not COMMENT', + path: ['comment'], + }) + .describe('Activity create'); export const mapActivity = (activity: Activity): ActivityResponseDto => { return { @@ -82,3 +74,9 @@ export const mapActivity = (activity: Activity): ActivityResponseDto => { user: mapUser(activity.user), }; }; + +export class ActivityResponseDto extends createZodDto(ActivityResponseSchema) {} +export class ActivityCreateDto extends createZodDto(ActivityCreateSchema) {} +export class ActivityDto extends createZodDto(ActivitySchema) {} +export class ActivitySearchDto extends createZodDto(ActivitySearchSchema) {} +export class ActivityStatisticsResponseDto extends createZodDto(ActivityStatisticsResponseSchema) {} diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index b270125b3651..151480983875 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -1,196 +1,158 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, IsString, ValidateNested } from 'class-validator'; import { ShallowDehydrateObject } from 'kysely'; import _ from 'lodash'; +import { createZodDto } from 'nestjs-zod'; import { AlbumUser, AuthSharedLink, User } from 'src/database'; -import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; +import { BulkIdErrorReasonSchema } from 'src/dtos/asset-ids.response.dto'; +import { AssetResponseSchema, MapAsset, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { mapUser, UserResponseDto } from 'src/dtos/user.dto'; -import { AlbumUserRole, AssetOrder } from 'src/enum'; +import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; +import { AlbumUserRole, AlbumUserRoleSchema, AssetOrder, AssetOrderSchema } from 'src/enum'; import { MaybeDehydrated } from 'src/types'; import { asDateString } from 'src/utils/date'; -import { Optional, ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; +import { stringToBool } from 'src/validation'; +import z from 'zod'; -export class AlbumInfoDto { - @ValidateBoolean({ optional: true, description: 'Exclude assets from response' }) - withoutAssets?: boolean; -} - -export class AlbumUserAddDto { - @ValidateUUID({ description: 'User ID' }) - userId!: string; - - @ValidateEnum({ - enum: AlbumUserRole, - name: 'AlbumUserRole', - description: 'Album user role', - default: AlbumUserRole.Editor, +const AlbumInfoSchema = z + .object({ + withoutAssets: stringToBool.optional().describe('Exclude assets from response'), }) - role?: AlbumUserRole; -} + .meta({ id: 'AlbumInfoDto' }); -export class AddUsersDto { - @ApiProperty({ description: 'Album users to add' }) - @ArrayNotEmpty() - albumUsers!: AlbumUserAddDto[]; -} - -export class AlbumUserCreateDto { - @ValidateUUID({ description: 'User ID' }) - userId!: string; - - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} - -export class CreateAlbumDto { - @ApiProperty({ description: 'Album name' }) - @IsString() - albumName!: string; - - @ApiPropertyOptional({ description: 'Album description' }) - @IsString() - @Optional() - description?: string; - - @ApiPropertyOptional({ description: 'Album users' }) - @Optional() - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AlbumUserCreateDto) - albumUsers?: AlbumUserCreateDto[]; - - @ValidateUUID({ optional: true, each: true, description: 'Initial asset IDs' }) - assetIds?: string[]; -} - -export class AlbumsAddAssetsDto { - @ValidateUUID({ each: true, description: 'Album IDs' }) - albumIds!: string[]; - - @ValidateUUID({ each: true, description: 'Asset IDs' }) - assetIds!: string[]; -} - -export class AlbumsAddAssetsResponseDto { - @ApiProperty({ description: 'Operation success' }) - success!: boolean; - @ValidateEnum({ enum: BulkIdErrorReason, name: 'BulkIdErrorReason', description: 'Error reason', optional: true }) - error?: BulkIdErrorReason; -} - -export class UpdateAlbumDto { - @ApiPropertyOptional({ description: 'Album name' }) - @Optional() - @IsString() - albumName?: string; - - @ApiPropertyOptional({ description: 'Album description' }) - @Optional() - @IsString() - description?: string; - - @ValidateUUID({ optional: true, description: 'Album thumbnail asset ID' }) - albumThumbnailAssetId?: string; - - @ValidateBoolean({ optional: true, description: 'Enable activity feed' }) - isActivityEnabled?: boolean; - - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true }) - order?: AssetOrder; -} - -export class GetAlbumsDto { - @ValidateBoolean({ - optional: true, - description: 'Filter by shared status: true = only shared, false = not shared, undefined = all owned albums', +const AlbumUserAddSchema = z + .object({ + userId: z.uuidv4().describe('User ID'), + role: AlbumUserRoleSchema.default(AlbumUserRole.Editor).optional().describe('Album user role'), }) - shared?: boolean; + .meta({ id: 'AlbumUserAddDto' }); - @ValidateUUID({ optional: true, description: 'Filter albums containing this asset ID (ignores shared parameter)' }) - assetId?: string; -} +const AddUsersSchema = z + .object({ + albumUsers: z.array(AlbumUserAddSchema).min(1).describe('Album users to add'), + }) + .meta({ id: 'AddUsersDto' }); -export class AlbumStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of owned albums' }) - owned!: number; +const AlbumUserCreateSchema = z + .object({ + userId: z.uuidv4().describe('User ID'), + role: AlbumUserRoleSchema, + }) + .meta({ id: 'AlbumUserCreateDto' }); - @ApiProperty({ type: 'integer', description: 'Number of shared albums' }) - shared!: number; +const CreateAlbumSchema = z + .object({ + albumName: z.string().describe('Album name'), + description: z.string().optional().describe('Album description'), + albumUsers: z.array(AlbumUserCreateSchema).optional().describe('Album users'), + assetIds: z.array(z.uuidv4()).optional().describe('Initial asset IDs'), + }) + .meta({ id: 'CreateAlbumDto' }); - @ApiProperty({ type: 'integer', description: 'Number of non-shared albums' }) - notShared!: number; -} +const AlbumsAddAssetsSchema = z + .object({ + albumIds: z.array(z.uuidv4()).describe('Album IDs'), + assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + }) + .meta({ id: 'AlbumsAddAssetsDto' }); -export class UpdateAlbumUserDto { - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} +const AlbumsAddAssetsResponseSchema = z + .object({ + success: z.boolean().describe('Operation success'), + error: BulkIdErrorReasonSchema.optional(), + }) + .meta({ id: 'AlbumsAddAssetsResponseDto' }); -export class AlbumUserResponseDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - user!: UserResponseDto; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} +const UpdateAlbumSchema = z + .object({ + albumName: z.string().optional().describe('Album name'), + description: z.string().optional().describe('Album description'), + albumThumbnailAssetId: z.uuidv4().optional().describe('Album thumbnail asset ID'), + isActivityEnabled: z.boolean().optional().describe('Enable activity feed'), + order: AssetOrderSchema.optional(), + }) + .meta({ id: 'UpdateAlbumDto' }); -export class ContributorCountResponseDto { - @ApiProperty({ description: 'User ID' }) - userId!: string; +const GetAlbumsSchema = z + .object({ + shared: stringToBool + .optional() + .describe('Filter by shared status: true = only shared, false = not shared, undefined = all owned albums'), + assetId: z.uuidv4().optional().describe('Filter albums containing this asset ID (ignores shared parameter)'), + }) + .meta({ id: 'GetAlbumsDto' }); - @ApiProperty({ type: 'integer', description: 'Number of assets contributed' }) - assetCount!: number; -} +const AlbumStatisticsResponseSchema = z + .object({ + owned: z.int().min(0).describe('Number of owned albums'), + shared: z.int().min(0).describe('Number of shared albums'), + notShared: z.int().min(0).describe('Number of non-shared albums'), + }) + .meta({ id: 'AlbumStatisticsResponseDto' }); -export class AlbumResponseDto { - @ApiProperty({ description: 'Album ID' }) - id!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ApiProperty({ description: 'Album name' }) - albumName!: string; - @ApiProperty({ description: 'Album description' }) - description!: string; - @ApiProperty({ description: 'Creation date', format: 'date-time' }) - createdAt!: string; - @ApiProperty({ description: 'Last update date', format: 'date-time' }) - updatedAt!: string; - @ApiProperty({ description: 'Thumbnail asset ID' }) - albumThumbnailAssetId!: string | null; - @ApiProperty({ description: 'Is shared album' }) - shared!: boolean; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - albumUsers!: AlbumUserResponseDto[]; - @ApiProperty({ description: 'Has shared link' }) - hasSharedLink!: boolean; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: AssetResponseDto[]; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - owner!: UserResponseDto; - @ApiProperty({ type: 'integer', description: 'Number of assets' }) - assetCount!: number; - @ApiPropertyOptional({ description: 'Last modified asset timestamp', format: 'date-time' }) - lastModifiedAssetTimestamp?: string; - @ApiPropertyOptional({ description: 'Start date (earliest asset)', format: 'date-time' }) - startDate?: string; - @ApiPropertyOptional({ description: 'End date (latest asset)', format: 'date-time' }) - endDate?: string; - @ApiProperty({ description: 'Activity feed enabled' }) - isActivityEnabled!: boolean; - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Asset sort order', optional: true }) - order?: AssetOrder; +const UpdateAlbumUserSchema = z + .object({ + role: AlbumUserRoleSchema, + }) + .meta({ id: 'UpdateAlbumUserDto' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Type(() => ContributorCountResponseDto) - contributorCounts?: ContributorCountResponseDto[]; -} +const AlbumUserResponseSchema = z + .object({ + user: UserResponseSchema, + role: AlbumUserRoleSchema, + }) + .meta({ id: 'AlbumUserResponseDto' }); + +const ContributorCountResponseSchema = z + .object({ + userId: z.string().describe('User ID'), + assetCount: z.int().min(0).describe('Number of assets contributed'), + }) + .meta({ id: 'ContributorCountResponseDto' }); + +export const AlbumResponseSchema = z + .object({ + id: z.string().describe('Album ID'), + ownerId: z.string().describe('Owner user ID'), + albumName: z.string().describe('Album name'), + description: z.string().describe('Album description'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'), + albumThumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'), + shared: z.boolean().describe('Is shared album'), + albumUsers: z.array(AlbumUserResponseSchema), + hasSharedLink: z.boolean().describe('Has shared link'), + assets: z.array(AssetResponseSchema), + owner: UserResponseSchema, + assetCount: z.int().min(0).describe('Number of assets'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + lastModifiedAssetTimestamp: z + .string() + .meta({ format: 'date-time' }) + .optional() + .describe('Last modified asset timestamp'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + startDate: z.string().meta({ format: 'date-time' }).optional().describe('Start date (earliest asset)'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + endDate: z.string().meta({ format: 'date-time' }).optional().describe('End date (latest asset)'), + isActivityEnabled: z.boolean().describe('Activity feed enabled'), + order: AssetOrderSchema.optional(), + contributorCounts: z.array(ContributorCountResponseSchema).optional(), + }) + .meta({ id: 'AlbumResponseDto' }); + +export class AlbumInfoDto extends createZodDto(AlbumInfoSchema) {} +export class AddUsersDto extends createZodDto(AddUsersSchema) {} +export class AlbumUserCreateDto extends createZodDto(AlbumUserCreateSchema) {} +export class CreateAlbumDto extends createZodDto(CreateAlbumSchema) {} +export class AlbumsAddAssetsDto extends createZodDto(AlbumsAddAssetsSchema) {} +export class AlbumsAddAssetsResponseDto extends createZodDto(AlbumsAddAssetsResponseSchema) {} +export class UpdateAlbumDto extends createZodDto(UpdateAlbumSchema) {} +export class GetAlbumsDto extends createZodDto(GetAlbumsSchema) {} +export class AlbumStatisticsResponseDto extends createZodDto(AlbumStatisticsResponseSchema) {} +export class UpdateAlbumUserDto extends createZodDto(UpdateAlbumUserSchema) {} +export class AlbumResponseDto extends createZodDto(AlbumResponseSchema) {} +class AlbumUserResponseDto extends createZodDto(AlbumUserResponseSchema) {} export type MapAlbumDto = { albumUsers?: AlbumUser[]; diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index 273082c41b59..8c1ffb53ca50 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -1,55 +1,42 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ArrayMinSize, IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Permission } from 'src/enum'; -import { Optional, ValidateEnum } from 'src/validation'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class APIKeyCreateDto { - @ApiPropertyOptional({ description: 'API key name' }) - @IsString() - @IsNotEmpty() - @Optional() - name?: string; +const PermissionSchema = z.enum(Permission).describe('List of permissions').meta({ id: 'Permission' }); - @ValidateEnum({ enum: Permission, name: 'Permission', each: true, description: 'List of permissions' }) - @ArrayMinSize(1) - permissions!: Permission[]; -} - -export class APIKeyUpdateDto { - @ApiPropertyOptional({ description: 'API key name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ValidateEnum({ - enum: Permission, - name: 'Permission', - description: 'List of permissions', - each: true, - optional: true, +const APIKeyCreateSchema = z + .object({ + name: z.string().optional().describe('API key name'), + permissions: z.array(PermissionSchema).min(1).describe('List of permissions'), }) - @ArrayMinSize(1) - permissions?: Permission[]; -} + .meta({ id: 'APIKeyCreateDto' }); -export class APIKeyResponseDto { - @ApiProperty({ description: 'API key ID' }) - id!: string; - @ApiProperty({ description: 'API key name' }) - name!: string; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; - @ValidateEnum({ enum: Permission, name: 'Permission', each: true, description: 'List of permissions' }) - permissions!: Permission[]; -} +const APIKeyUpdateSchema = z + .object({ + name: z.string().optional().describe('API key name'), + permissions: z.array(PermissionSchema).min(1).optional().describe('List of permissions'), + }) + .meta({ id: 'APIKeyUpdateDto' }); -export class APIKeyCreateResponseDto { - @ApiProperty({ description: 'API key secret (only shown once)' }) - secret!: string; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - apiKey!: APIKeyResponseDto; -} +const APIKeyResponseSchema = z + .object({ + id: z.string().describe('API key ID'), + name: z.string().describe('API key name'), + createdAt: isoDatetimeToDate.describe('Creation date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + permissions: z.array(PermissionSchema).describe('List of permissions'), + }) + .meta({ id: 'APIKeyResponseDto' }); + +const APIKeyCreateResponseSchema = z + .object({ + secret: z.string().describe('API key secret (only shown once)'), + apiKey: APIKeyResponseSchema, + }) + .meta({ id: 'APIKeyCreateResponseDto' }); + +export class APIKeyCreateDto extends createZodDto(APIKeyCreateSchema) {} +export class APIKeyUpdateDto extends createZodDto(APIKeyUpdateSchema) {} +export class APIKeyResponseDto extends createZodDto(APIKeyResponseSchema) {} +export class APIKeyCreateResponseDto extends createZodDto(APIKeyCreateResponseSchema) {} diff --git a/server/src/dtos/asset-ids.response.dto.ts b/server/src/dtos/asset-ids.response.dto.ts index 1065d8485e80..346829e644e5 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 345c1bf4180b..fa3c4727657b 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 46558503793c..6a4c55c5aabc 100644 --- a/server/src/dtos/asset-media.dto.ts +++ b/server/src/dtos/asset-media.dto.ts @@ -1,10 +1,8 @@ -import { BadRequestException } from '@nestjs/common'; -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { plainToInstance, Transform, Type } from 'class-transformer'; -import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator'; -import { AssetMetadataUpsertItemDto } from 'src/dtos/asset.dto'; -import { AssetVisibility } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { AssetMetadataUpsertItemSchema } from 'src/dtos/asset.dto'; +import { AssetVisibilitySchema } from 'src/enum'; +import { isoDatetimeToDate, JsonParsed, stringToBool } from 'src/validation'; +import z from 'zod'; export enum AssetMediaSize { Original = 'original', @@ -17,13 +15,14 @@ export enum AssetMediaSize { THUMBNAIL = 'thumbnail', } -export class AssetMediaOptionsDto { - @ValidateEnum({ enum: AssetMediaSize, name: 'AssetMediaSize', description: 'Asset media size', optional: true }) - size?: AssetMediaSize; +const AssetMediaSizeSchema = z.enum(AssetMediaSize).describe('Asset media size').meta({ id: 'AssetMediaSize' }); - @ValidateBoolean({ optional: true, description: 'Return edited asset if available', default: false }) - edited?: boolean; -} +const AssetMediaOptionsSchema = z + .object({ + size: AssetMediaSizeSchema.optional(), + edited: stringToBool.default(false).optional().describe('Return edited asset if available'), + }) + .meta({ id: 'AssetMediaOptionsDto' }); export enum UploadFieldName { ASSET_DATA = 'assetData', @@ -31,98 +30,53 @@ export enum UploadFieldName { PROFILE_DATA = 'file', } -class AssetMediaBase { - @ApiProperty({ description: 'Device asset ID' }) - @IsNotEmpty() - @IsString() - deviceAssetId!: string; +const AssetMediaBaseSchema = z.object({ + deviceAssetId: z.string().describe('Device asset ID'), + deviceId: z.string().describe('Device ID'), + fileCreatedAt: isoDatetimeToDate.describe('File creation date'), + fileModifiedAt: isoDatetimeToDate.describe('File modification date'), + duration: z.string().optional().describe('Duration (for videos)'), + filename: z.string().optional().describe('Filename'), + /** The properties below are added to correctly generate the API docs and client SDKs. Validation should be handled in the controller. */ + [UploadFieldName.ASSET_DATA]: z.any().describe('Asset file data').meta({ type: 'string', format: 'binary' }), +}); - @ApiProperty({ description: 'Device ID' }) - @IsNotEmpty() - @IsString() - deviceId!: string; +const AssetMediaCreateSchema = AssetMediaBaseSchema.extend({ + isFavorite: stringToBool.optional().describe('Mark as favorite'), + visibility: AssetVisibilitySchema.optional(), + livePhotoVideoId: z.uuidv4().optional().describe('Live photo video ID'), + metadata: JsonParsed.pipe(z.array(AssetMetadataUpsertItemSchema)).optional().describe('Asset metadata items'), + [UploadFieldName.SIDECAR_DATA]: z + .any() + .optional() + .describe('Sidecar file data') + .meta({ type: 'string', format: 'binary' }), +}).meta({ id: 'AssetMediaCreateDto' }); - @ValidateDate({ description: 'File creation date' }) - fileCreatedAt!: Date; +const AssetMediaReplaceSchema = AssetMediaBaseSchema.meta({ id: 'AssetMediaReplaceDto' }); - @ValidateDate({ description: 'File modification date' }) - fileModifiedAt!: Date; - - @ApiPropertyOptional({ description: 'Duration (for videos)' }) - @Optional() - @IsString() - duration?: string; - - @ApiPropertyOptional({ description: 'Filename' }) - @Optional() - @IsString() - filename?: string; - - // The properties below are added to correctly generate the API docs - // and client SDKs. Validation should be handled in the controller. - @ApiProperty({ type: 'string', format: 'binary', description: 'Asset file data' }) - [UploadFieldName.ASSET_DATA]!: any; -} - -export class AssetMediaCreateDto extends AssetMediaBase { - @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) - isFavorite?: boolean; - - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility', optional: true }) - visibility?: AssetVisibility; - - @ValidateUUID({ optional: true, description: 'Live photo video ID' }) - livePhotoVideoId?: string; - - @ApiPropertyOptional({ description: 'Asset metadata items' }) - @Transform(({ value }) => { - try { - const json = JSON.parse(value); - const items = Array.isArray(json) ? json : [json]; - return items.map((item) => plainToInstance(AssetMetadataUpsertItemDto, item)); - } catch { - throw new BadRequestException(['metadata must be valid JSON']); - } +const AssetBulkUploadCheckItemSchema = z + .object({ + id: z.string().describe('Asset ID'), + checksum: z.string().describe('Base64 or hex encoded SHA1 hash'), }) - @Optional() - @ValidateNested({ each: true }) - @IsArray() - metadata?: AssetMetadataUpsertItemDto[]; + .meta({ id: 'AssetBulkUploadCheckItem' }); - @ApiProperty({ type: 'string', format: 'binary', required: false, description: 'Sidecar file data' }) - [UploadFieldName.SIDECAR_DATA]?: any; -} +const AssetBulkUploadCheckSchema = z + .object({ + assets: z.array(AssetBulkUploadCheckItemSchema).describe('Assets to check'), + }) + .meta({ id: 'AssetBulkUploadCheckDto' }); -export class AssetMediaReplaceDto extends AssetMediaBase {} +const CheckExistingAssetsSchema = z + .object({ + deviceAssetIds: z.array(z.string()).min(1).describe('Device asset IDs to check'), + deviceId: z.string().describe('Device ID'), + }) + .meta({ id: 'CheckExistingAssetsDto' }); -export class AssetBulkUploadCheckItem { - @ApiProperty({ description: 'Asset ID' }) - @IsString() - @IsNotEmpty() - id!: string; - - @ApiProperty({ description: 'Base64 or hex encoded SHA1 hash' }) - @IsString() - @IsNotEmpty() - checksum!: string; -} - -export class AssetBulkUploadCheckDto { - @ApiProperty({ description: 'Assets to check' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetBulkUploadCheckItem) - assets!: AssetBulkUploadCheckItem[]; -} - -export class CheckExistingAssetsDto { - @ApiProperty({ description: 'Device asset IDs to check' }) - @ArrayNotEmpty() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - deviceAssetIds!: string[]; - - @ApiProperty({ description: 'Device ID' }) - @IsNotEmpty() - deviceId!: string; -} +export class AssetMediaOptionsDto extends createZodDto(AssetMediaOptionsSchema) {} +export class AssetMediaCreateDto extends createZodDto(AssetMediaCreateSchema) {} +export class AssetMediaReplaceDto extends createZodDto(AssetMediaReplaceSchema) {} +export class AssetBulkUploadCheckDto extends createZodDto(AssetBulkUploadCheckSchema) {} +export class CheckExistingAssetsDto extends createZodDto(CheckExistingAssetsSchema) {} diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 2c2f57bbb28b..a95d2f3c1e57 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -1,144 +1,132 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Selectable, ShallowDehydrateObject } from 'kysely'; +import { createZodDto } from 'nestjs-zod'; import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; +import { HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; -import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto'; +import { ExifResponseSchema, mapExif } from 'src/dtos/exif.dto'; import { - AssetFaceWithoutPersonResponseDto, + AssetFaceWithoutPersonResponseSchema, PersonWithFacesResponseDto, + PersonWithFacesResponseSchema, mapFacesWithoutPerson, mapPerson, } from 'src/dtos/person.dto'; -import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; -import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { AssetStatus, AssetType, AssetVisibility, ChecksumAlgorithm } from 'src/enum'; +import { TagResponseSchema, mapTag } from 'src/dtos/tag.dto'; +import { UserResponseSchema, mapUser } from 'src/dtos/user.dto'; +import { + AssetStatus, + AssetType, + AssetTypeSchema, + AssetVisibility, + AssetVisibilitySchema, + ChecksumAlgorithm, +} from 'src/enum'; import { ImageDimensions, MaybeDehydrated } from 'src/types'; import { getDimensions } from 'src/utils/asset.util'; import { hexOrBufferToBase64 } from 'src/utils/bytes'; import { asDateString } from 'src/utils/date'; import { mimeTypes } from 'src/utils/mime-types'; -import { ValidateEnum, ValidateUUID } from 'src/validation'; +import z from 'zod'; -export class SanitizedAssetResponseDto { - @ApiProperty({ description: 'Asset ID' }) - id!: string; - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' }) - type!: AssetType; - @ApiProperty({ - description: - 'Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.', +const SanitizedAssetResponseSchema = z + .object({ + id: z.string().describe('Asset ID'), + type: AssetTypeSchema, + thumbhash: z + .string() + .describe( + 'Thumbhash for thumbnail generation (base64) also used as the c query param for thumbnail cache busting.', + ) + .nullable(), + originalMimeType: z.string().optional().describe('Original MIME type'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + localDateTime: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.', + ), + duration: z.string().describe('Video duration (for videos)'), + livePhotoVideoId: z.string().nullish().describe('Live photo video ID'), + hasMetadata: z.boolean().describe('Whether asset has metadata'), + width: z.number().min(0).nullable().describe('Asset width'), + height: z.number().min(0).nullable().describe('Asset height'), }) - thumbhash!: string | null; - @ApiPropertyOptional({ description: 'Original MIME type' }) - originalMimeType?: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - 'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.', - example: '2024-01-15T14:30:00.000Z', - }) - localDateTime!: string; - @ApiProperty({ description: 'Video duration (for videos)' }) - duration!: string; - @ApiPropertyOptional({ description: 'Live photo video ID' }) - livePhotoVideoId?: string | null; - @ApiProperty({ description: 'Whether asset has metadata' }) - hasMetadata!: boolean; - @ApiProperty({ description: 'Asset width' }) - width!: number | null; - @ApiProperty({ description: 'Asset height' }) - height!: number | null; -} + .meta({ id: 'SanitizedAssetResponseDto' }); -export class AssetResponseDto extends SanitizedAssetResponseDto { - @ApiProperty({ - type: 'string', - format: 'date-time', - description: 'The UTC timestamp when the asset was originally uploaded to Immich.', - example: '2024-01-15T20:30:00.000Z', - }) - createdAt!: string; - @ApiProperty({ description: 'Device asset ID' }) - deviceAssetId!: string; - @ApiProperty({ description: 'Device ID' }) - deviceId!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - owner?: UserResponseDto; - @ValidateUUID({ - nullable: true, - description: 'Library ID', - history: new HistoryBuilder().added('v1').deprecated('v1'), - }) - libraryId?: string | null; - @ApiProperty({ description: 'Original file path' }) - originalPath!: string; - @ApiProperty({ description: 'Original file name' }) - originalFileName!: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - 'The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.', - example: '2024-01-15T19:30:00.000Z', - }) - fileCreatedAt!: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - 'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.', - example: '2024-01-16T10:15:00.000Z', - }) - fileModifiedAt!: string; - @ApiProperty({ - type: 'string', - format: 'date-time', - description: - 'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.', - example: '2024-01-16T12:45:30.000Z', - }) - updatedAt!: string; - @ApiProperty({ description: 'Is favorite' }) - isFavorite!: boolean; - @ApiProperty({ description: 'Is archived' }) - isArchived!: boolean; - @ApiProperty({ description: 'Is trashed' }) - isTrashed!: boolean; - @ApiProperty({ description: 'Is offline' }) - isOffline!: boolean; - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility' }) - visibility!: AssetVisibility; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - exifInfo?: ExifResponseDto; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - tags?: TagResponseDto[]; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - people?: PersonWithFacesResponseDto[]; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; - @ApiProperty({ description: 'Base64 encoded SHA1 hash' }) - checksum!: string; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - stack?: AssetStackResponseDto | null; - @ApiPropertyOptional({ description: 'Duplicate group ID' }) - duplicateId?: string | null; +export class SanitizedAssetResponseDto extends createZodDto(SanitizedAssetResponseSchema) {} - @Property({ description: 'Is resized', history: new HistoryBuilder().added('v1').deprecated('v1.113.0') }) - resized?: boolean; - @Property({ description: 'Is edited', history: new HistoryBuilder().added('v2.5.0').beta('v2.5.0') }) - isEdited!: boolean; -} +const AssetStackResponseSchema = z + .object({ + id: z.string().describe('Stack ID'), + primaryAssetId: z.string().describe('Primary asset ID'), + assetCount: z.int().min(0).describe('Number of assets in stack'), + }) + .meta({ id: 'AssetStackResponseDto' }); + +export const AssetResponseSchema = SanitizedAssetResponseSchema.extend( + z.object({ + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + createdAt: z + .string() + .meta({ format: 'date-time' }) + .describe('The UTC timestamp when the asset was originally uploaded to Immich.'), + deviceAssetId: z.string().describe('Device asset ID'), + deviceId: z.string().describe('Device ID'), + ownerId: z.string().describe('Owner user ID'), + owner: UserResponseSchema.optional(), + libraryId: z + .uuidv4() + .nullish() + .describe('Library ID') + .meta(new HistoryBuilder().added('v1').deprecated('v1').getExtensions()), + originalPath: z.string().describe('Original file path'), + originalFileName: z.string().describe('Original file name'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + fileCreatedAt: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The actual UTC timestamp when the file was created/captured, preserving timezone information. This is the authoritative timestamp for chronological sorting within timeline groups. Combined with timezone data, this can be used to determine the exact moment the photo was taken.', + ), + fileModifiedAt: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The UTC timestamp when the file was last modified on the filesystem. This reflects the last time the physical file was changed, which may be different from when the photo was originally taken.', + ), + updatedAt: z + .string() + .meta({ format: 'date-time' }) + .describe( + 'The UTC timestamp when the asset record was last updated in the database. This is automatically maintained by the database and reflects when any field in the asset was last modified.', + ), + isFavorite: z.boolean().describe('Is favorite'), + isArchived: z.boolean().describe('Is archived'), + isTrashed: z.boolean().describe('Is trashed'), + isOffline: z.boolean().describe('Is offline'), + visibility: AssetVisibilitySchema, + exifInfo: ExifResponseSchema.optional(), + tags: z.array(TagResponseSchema).optional(), + people: z.array(PersonWithFacesResponseSchema).optional(), + unassignedFaces: z.array(AssetFaceWithoutPersonResponseSchema).optional(), + checksum: z.string().describe('Base64 encoded SHA1 hash'), + stack: AssetStackResponseSchema.nullish(), + duplicateId: z.string().nullish().describe('Duplicate group ID'), + resized: z + .boolean() + .optional() + .describe('Is resized') + .meta(new HistoryBuilder().added('v1').deprecated('v1.113.0').getExtensions()), + isEdited: z + .boolean() + .describe('Is edited') + .meta(new HistoryBuilder().added('v2.5.0').beta('v2.5.0').getExtensions()), + }).shape, +).meta({ id: 'AssetResponseDto' }); + +export class AssetResponseDto extends createZodDto(AssetResponseSchema) {} export type MapAsset = { createdAt: Date; @@ -180,17 +168,6 @@ export type MapAsset = { isEdited: boolean; }; -export class AssetStackResponseDto { - @ApiProperty({ description: 'Stack ID' }) - id!: string; - - @ApiProperty({ description: 'Primary asset ID' }) - primaryAssetId!: string; - - @ApiProperty({ type: 'integer', description: 'Number of assets in stack' }) - assetCount!: number; -} - export type AssetMapOptions = { stripMetadata?: boolean; withStack?: boolean; diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index b7bd7a18e8d7..3adb9374751d 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -1,125 +1,78 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { - IsArray, - IsDateString, - IsInt, - IsLatitude, - IsLongitude, - IsNotEmpty, - IsObject, - IsPositive, - IsString, - IsTimeZone, - Max, - Min, - ValidateIf, - ValidateNested, -} from 'class-validator'; -import { HistoryBuilder, Property } from 'src/decorators'; -import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetType, AssetVisibility } from 'src/enum'; +import { createZodDto } from 'nestjs-zod'; +import { HistoryBuilder } from 'src/decorators'; +import { BulkIdsSchema } from 'src/dtos/asset-ids.response.dto'; +import { AssetType, AssetVisibilitySchema } from 'src/enum'; import { AssetStats } from 'src/repositories/asset.repository'; -import { IsNotSiblingOf, Optional, ValidateBoolean, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { IsNotSiblingOf, isoDatetimeToDate, latitudeSchema, longitudeSchema, stringToBool } from 'src/validation'; +import z from 'zod'; -export class DeviceIdDto { - @ApiProperty({ description: 'Device ID' }) - @IsNotEmpty() - @IsString() - deviceId!: string; -} - -const hasGPS = (o: { latitude: undefined; longitude: undefined }) => - o.latitude !== undefined || o.longitude !== undefined; -const ValidateGPS = () => ValidateIf(hasGPS); - -export class UpdateAssetBase { - @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) - isFavorite?: boolean; - - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true, description: 'Asset visibility' }) - visibility?: AssetVisibility; - - @ApiProperty({ description: 'Original date and time' }) - @Optional() - @IsDateString() - dateTimeOriginal?: string; - - @ApiProperty({ description: 'Latitude coordinate' }) - @ValidateGPS() - @IsLatitude() - @IsNotEmpty() - latitude?: number; - - @ApiProperty({ description: 'Longitude coordinate' }) - @ValidateGPS() - @IsLongitude() - @IsNotEmpty() - longitude?: number; - - @Property({ - description: 'Rating in range [1-5], or null for unrated', - history: new HistoryBuilder() - .added('v1') - .stable('v2') - .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'), +const DeviceIdSchema = z + .object({ + deviceId: z.string().describe('Device ID'), }) - @Optional({ nullable: true }) - @IsInt() - @Max(5) - @Min(-1) - @Transform(({ value }) => (value === 0 ? null : value)) - rating?: number | null; + .meta({ id: 'DeviceIdDto' }); - @ApiProperty({ description: 'Asset description' }) - @Optional() - @IsString() - description?: string; -} +const UpdateAssetBaseSchema = z + .object({ + isFavorite: z.boolean().optional().describe('Mark as favorite'), + visibility: AssetVisibilitySchema.optional(), + dateTimeOriginal: z.string().optional().describe('Original date and time'), + latitude: latitudeSchema.optional().describe('Latitude coordinate'), + longitude: longitudeSchema.optional().describe('Longitude coordinate'), + rating: z + .number() + .int() + .min(-1) + .max(5) + .transform((value) => (value === 0 ? null : value)) + .nullish() + .describe('Rating in range [1-5], or null for unrated') + .meta({ + ...new HistoryBuilder() + .added('v1') + .stable('v2') + .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.') + .getExtensions(), + }), + description: z.string().optional().describe('Asset description'), + }) + .refine( + (data) => + (data.latitude === undefined && data.longitude === undefined) || + (data.latitude !== undefined && data.longitude !== undefined), + { message: 'Latitude and longitude must be provided together' }, + ); -export class AssetBulkUpdateDto extends UpdateAssetBase { - @ValidateUUID({ each: true, description: 'Asset IDs to update' }) - ids!: string[]; +const AssetBulkUpdateBaseSchema = UpdateAssetBaseSchema.extend({ + ids: z.array(z.uuidv4()).describe('Asset IDs to update'), + duplicateId: z.string().nullish().describe('Duplicate ID'), + dateTimeRelative: z.number().optional().describe('Relative time offset in seconds'), + timeZone: z.string().optional().describe('Time zone (IANA timezone)'), +}); - @ValidateString({ optional: true, nullable: true, description: 'Duplicate ID' }) - duplicateId?: string | null; +const AssetBulkUpdateSchema = AssetBulkUpdateBaseSchema.pipe( + IsNotSiblingOf(AssetBulkUpdateBaseSchema, 'dateTimeRelative', ['dateTimeOriginal']), +).meta({ id: 'AssetBulkUpdateDto' }); - @ApiProperty({ description: 'Relative time offset in seconds' }) - @IsNotSiblingOf(['dateTimeOriginal']) - @Optional() - @IsInt() - dateTimeRelative?: number; +const UpdateAssetSchema = UpdateAssetBaseSchema.extend({ + livePhotoVideoId: z.uuidv4().nullish().describe('Live photo video ID'), +}).meta({ id: 'UpdateAssetDto' }); - @ApiProperty({ description: 'Time zone (IANA timezone)' }) - @IsNotSiblingOf(['dateTimeOriginal']) - @IsTimeZone() - @Optional() - timeZone?: string; -} +const RandomAssetsSchema = z + .object({ + count: z.coerce.number().min(1).optional().describe('Number of random assets to return'), + }) + .meta({ id: 'RandomAssetsDto' }); -export class UpdateAssetDto extends UpdateAssetBase { - @ValidateUUID({ optional: true, nullable: true, description: 'Live photo video ID' }) - livePhotoVideoId?: string | null; -} +const AssetBulkDeleteSchema = BulkIdsSchema.extend({ + force: z.boolean().optional().describe('Force delete even if in use'), +}).meta({ id: 'AssetBulkDeleteDto' }); -export class RandomAssetsDto { - @ApiProperty({ description: 'Number of random assets to return' }) - @Optional() - @IsInt() - @IsPositive() - @Type(() => Number) - count?: number; -} - -export class AssetBulkDeleteDto extends BulkIdsDto { - @ValidateBoolean({ optional: true, description: 'Force delete even if in use' }) - force?: boolean; -} - -export class AssetIdsDto { - @ValidateUUID({ each: true, description: 'Asset IDs' }) - assetIds!: string[]; -} +export const AssetIdsSchema = z + .object({ + assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + }) + .meta({ id: 'AssetIdsDto' }); export enum AssetJobName { REFRESH_FACES = 'refresh-faces', @@ -128,137 +81,104 @@ export enum AssetJobName { TRANSCODE_VIDEO = 'transcode-video', } -export class AssetJobsDto extends AssetIdsDto { - @ValidateEnum({ enum: AssetJobName, name: 'AssetJobName', description: 'Job name' }) - name!: AssetJobName; -} +const AssetJobNameSchema = z.enum(AssetJobName).describe('Job name').meta({ id: 'AssetJobName' }); -export class AssetStatsDto { - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Filter by visibility', optional: true }) - visibility?: AssetVisibility; +const AssetJobsSchema = AssetIdsSchema.extend({ + name: AssetJobNameSchema, +}).meta({ id: 'AssetJobsDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; +const AssetStatsSchema = z + .object({ + visibility: AssetVisibilitySchema.optional(), + isFavorite: stringToBool.optional().describe('Filter by favorite status'), + isTrashed: stringToBool.optional().describe('Filter by trash status'), + }) + .meta({ id: 'AssetStatsDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by trash status' }) - isTrashed?: boolean; -} +const AssetStatsResponseSchema = z + .object({ + images: z.int().describe('Number of images'), + videos: z.int().describe('Number of videos'), + total: z.int().describe('Total number of assets'), + }) + .meta({ id: 'AssetStatsResponseDto' }); -export class AssetStatsResponseDto { - @ApiProperty({ description: 'Number of images', type: 'integer' }) - images!: number; +const AssetMetadataRouteParamsSchema = z + .object({ + id: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + }) + .meta({ id: 'AssetMetadataRouteParams' }); - @ApiProperty({ description: 'Number of videos', type: 'integer' }) - videos!: number; +export const AssetMetadataUpsertItemSchema = z + .object({ + key: z.string().describe('Metadata key'), + value: z.record(z.string(), z.unknown()).describe('Metadata value (object)'), + }) + .meta({ id: 'AssetMetadataUpsertItemDto' }); - @ApiProperty({ description: 'Total number of assets', type: 'integer' }) - total!: number; -} +const AssetMetadataUpsertSchema = z + .object({ + items: z.array(AssetMetadataUpsertItemSchema).describe('Metadata items to upsert'), + }) + .meta({ id: 'AssetMetadataUpsertDto' }); -export class AssetMetadataRouteParams { - @ValidateUUID({ description: 'Asset ID' }) - id!: string; +const AssetMetadataBulkUpsertItemSchema = z + .object({ + assetId: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + value: z.record(z.string(), z.unknown()).describe('Metadata value (object)'), + }) + .meta({ id: 'AssetMetadataBulkUpsertItemDto' }); - @ValidateString({ description: 'Metadata key' }) - key!: string; -} +const AssetMetadataBulkUpsertSchema = z + .object({ + items: z.array(AssetMetadataBulkUpsertItemSchema).describe('Metadata items to upsert'), + }) + .meta({ id: 'AssetMetadataBulkUpsertDto' }); -export class AssetMetadataUpsertDto { - @ApiProperty({ description: 'Metadata items to upsert' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetMetadataUpsertItemDto) - items!: AssetMetadataUpsertItemDto[]; -} +const AssetMetadataBulkDeleteItemSchema = z + .object({ + assetId: z.uuidv4().describe('Asset ID'), + key: z.string().describe('Metadata key'), + }) + .meta({ id: 'AssetMetadataBulkDeleteItemDto' }); -export class AssetMetadataUpsertItemDto { - @ValidateString({ description: 'Metadata key' }) - key!: string; +const AssetMetadataBulkDeleteSchema = z + .object({ + items: z.array(AssetMetadataBulkDeleteItemSchema).describe('Metadata items to delete'), + }) + .meta({ id: 'AssetMetadataBulkDeleteDto' }); - @ApiProperty({ description: 'Metadata value (object)' }) - @IsObject() - value!: object; -} +const AssetMetadataResponseSchema = z + .object({ + key: z.string().describe('Metadata key'), + value: z.record(z.string(), z.unknown()).describe('Metadata value (object)'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + }) + .meta({ id: 'AssetMetadataResponseDto' }); -export class AssetMetadataBulkUpsertDto { - @ApiProperty({ description: 'Metadata items to upsert' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetMetadataBulkUpsertItemDto) - items!: AssetMetadataBulkUpsertItemDto[]; -} +const AssetMetadataBulkResponseSchema = AssetMetadataResponseSchema.extend({ + assetId: z.string().describe('Asset ID'), +}).meta({ id: 'AssetMetadataBulkResponseDto' }); -export class AssetMetadataBulkUpsertItemDto { - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; +const AssetCopySchema = z + .object({ + sourceId: z.uuidv4().describe('Source asset ID'), + targetId: z.uuidv4().describe('Target asset ID'), + sharedLinks: z.boolean().default(true).optional().describe('Copy shared links'), + albums: z.boolean().default(true).optional().describe('Copy album associations'), + sidecar: z.boolean().default(true).optional().describe('Copy sidecar file'), + stack: z.boolean().default(true).optional().describe('Copy stack association'), + favorite: z.boolean().default(true).optional().describe('Copy favorite status'), + }) + .meta({ id: 'AssetCopyDto' }); - @ValidateString({ description: 'Metadata key' }) - key!: string; - - @ApiProperty({ description: 'Metadata value (object)' }) - @IsObject() - value!: object; -} - -export class AssetMetadataBulkDeleteDto { - @ApiProperty({ description: 'Metadata items to delete' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetMetadataBulkDeleteItemDto) - items!: AssetMetadataBulkDeleteItemDto[]; -} - -export class AssetMetadataBulkDeleteItemDto { - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; - - @ValidateString({ description: 'Metadata key' }) - key!: string; -} - -export class AssetMetadataResponseDto { - @ValidateString({ description: 'Metadata key' }) - key!: string; - - @ApiProperty({ description: 'Metadata value (object)' }) - value!: object; - - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; -} - -export class AssetMetadataBulkResponseDto extends AssetMetadataResponseDto { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - -export class AssetCopyDto { - @ValidateUUID({ description: 'Source asset ID' }) - sourceId!: string; - - @ValidateUUID({ description: 'Target asset ID' }) - targetId!: string; - - @ValidateBoolean({ optional: true, description: 'Copy shared links', default: true }) - sharedLinks?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy album associations', default: true }) - albums?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy sidecar file', default: true }) - sidecar?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy stack association', default: true }) - stack?: boolean; - - @ValidateBoolean({ optional: true, description: 'Copy favorite status', default: true }) - favorite?: boolean; -} - -export class AssetDownloadOriginalDto { - @ValidateBoolean({ optional: true, description: 'Return edited asset if available', default: false }) - edited?: boolean; -} +const AssetDownloadOriginalSchema = z + .object({ + edited: stringToBool.default(false).optional().describe('Return edited asset if available'), + }) + .meta({ id: 'AssetDownloadOriginalDto' }); export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { return { @@ -267,3 +187,21 @@ export const mapStats = (stats: AssetStats): AssetStatsResponseDto => { total: Object.values(stats).reduce((total, value) => total + value, 0), }; }; + +export class DeviceIdDto extends createZodDto(DeviceIdSchema) {} +export class AssetBulkUpdateDto extends createZodDto(AssetBulkUpdateSchema) {} +export class UpdateAssetDto extends createZodDto(UpdateAssetSchema) {} +export class RandomAssetsDto extends createZodDto(RandomAssetsSchema) {} +export class AssetBulkDeleteDto extends createZodDto(AssetBulkDeleteSchema) {} +export class AssetIdsDto extends createZodDto(AssetIdsSchema) {} +export class AssetJobsDto extends createZodDto(AssetJobsSchema) {} +export class AssetStatsDto extends createZodDto(AssetStatsSchema) {} +export class AssetStatsResponseDto extends createZodDto(AssetStatsResponseSchema) {} +export class AssetMetadataRouteParams extends createZodDto(AssetMetadataRouteParamsSchema) {} +export class AssetMetadataUpsertDto extends createZodDto(AssetMetadataUpsertSchema) {} +export class AssetMetadataBulkUpsertDto extends createZodDto(AssetMetadataBulkUpsertSchema) {} +export class AssetMetadataBulkDeleteDto extends createZodDto(AssetMetadataBulkDeleteSchema) {} +export class AssetMetadataResponseDto extends createZodDto(AssetMetadataResponseSchema) {} +export class AssetMetadataBulkResponseDto extends createZodDto(AssetMetadataBulkResponseSchema) {} +export class AssetCopyDto extends createZodDto(AssetCopySchema) {} +export class AssetDownloadOriginalDto extends createZodDto(AssetDownloadOriginalSchema) {} diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 3df82f4ef405..95d2bb126a02 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -1,59 +1,43 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { AuthApiKey, AuthSession, AuthSharedLink, AuthUser, UserAdmin } from 'src/database'; import { ImmichCookie, UserMetadataKey } from 'src/enum'; import { UserMetadataItem } from 'src/types'; -import { Optional, PinCode, toEmail, ValidateBoolean } from 'src/validation'; +import { toEmail } from 'src/validation'; +import z from 'zod'; export type CookieResponse = { isSecure: boolean; values: Array<{ key: ImmichCookie; value: string | null }>; }; -export class AuthDto { - @ApiProperty({ description: 'Authenticated user' }) - user!: AuthUser; +export const pinCodeRegex = /^\d{6}$/; - @ApiPropertyOptional({ description: 'API key (if authenticated via API key)' }) +export type AuthDto = { + user: AuthUser; apiKey?: AuthApiKey; - @ApiPropertyOptional({ description: 'Shared link (if authenticated via shared link)' }) sharedLink?: AuthSharedLink; - @ApiPropertyOptional({ description: 'Session (if authenticated via session)' }) session?: AuthSession; -} +}; -export class LoginCredentialDto { - @ApiProperty({ example: 'testuser@email.com', description: 'User email' }) - @IsEmail({ require_tld: false }) - @Transform(toEmail) - @IsNotEmpty() - email!: string; +const LoginCredentialSchema = z + .object({ + email: toEmail.describe('User email').meta({ example: 'testuser@email.com' }), + password: z.string().describe('User password').meta({ example: 'password' }), + }) + .meta({ id: 'LoginCredentialDto' }); - @ApiProperty({ example: 'password', description: 'User password' }) - @IsString() - @IsNotEmpty() - password!: string; -} - -export class LoginResponseDto { - @ApiProperty({ description: 'Access token' }) - accessToken!: string; - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ApiProperty({ description: 'User email' }) - userEmail!: string; - @ApiProperty({ description: 'User name' }) - name!: string; - @ApiProperty({ description: 'Profile image path' }) - profileImagePath!: string; - @ApiProperty({ description: 'Is admin user' }) - isAdmin!: boolean; - @ApiProperty({ description: 'Should change password' }) - shouldChangePassword!: boolean; - @ApiProperty({ description: 'Is onboarded' }) - isOnboarded!: boolean; -} +const LoginResponseSchema = z + .object({ + accessToken: z.string().describe('Access token'), + userId: z.string().describe('User ID'), + userEmail: toEmail.describe('User email'), + name: z.string().describe('User name'), + profileImagePath: z.string().describe('Profile image path'), + isAdmin: z.boolean().describe('Is admin user'), + shouldChangePassword: z.boolean().describe('Should change password'), + isOnboarded: z.boolean().describe('Is onboarded'), + }) + .meta({ id: 'LoginResponseDto' }); export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginResponseDto { const onboardingMetadata = entity.metadata.find( @@ -72,115 +56,95 @@ export function mapLoginResponse(entity: UserAdmin, accessToken: string): LoginR }; } -export class LogoutResponseDto { - @ApiProperty({ description: 'Logout successful' }) - successful!: boolean; - @ApiProperty({ description: 'Redirect URI' }) - redirectUri!: string; -} +const LogoutResponseSchema = z + .object({ + successful: z.boolean().describe('Logout successful'), + redirectUri: z.string().describe('Redirect URI'), + }) + .meta({ id: 'LogoutResponseDto' }); -export class SignUpDto extends LoginCredentialDto { - @ApiProperty({ example: 'Admin', description: 'User name' }) - @IsString() - @IsNotEmpty() - name!: string; -} +const SignUpSchema = LoginCredentialSchema.extend({ + name: z.string().describe('User name').meta({ example: 'Admin' }), +}).meta({ id: 'SignUpDto' }); -export class ChangePasswordDto { - @ApiProperty({ example: 'password', description: 'Current password' }) - @IsString() - @IsNotEmpty() - password!: string; +const ChangePasswordSchema = z + .object({ + password: z.string().describe('Current password').meta({ example: 'password' }), + newPassword: z.string().min(8).describe('New password (min 8 characters)').meta({ example: 'password' }), + invalidateSessions: z.boolean().default(false).optional().describe('Invalidate all other sessions'), + }) + .meta({ id: 'ChangePasswordDto' }); - @ApiProperty({ example: 'password', description: 'New password (min 8 characters)' }) - @IsString() - @IsNotEmpty() - @MinLength(8) - newPassword!: string; +const PinCodeSetupSchema = z + .object({ + pinCode: z.string().regex(pinCodeRegex).describe('PIN code (4-6 digits)').meta({ example: '123456' }), + }) + .meta({ id: 'PinCodeSetupDto' }); - @ValidateBoolean({ optional: true, default: false, description: 'Invalidate all other sessions' }) - invalidateSessions?: boolean; -} +const PinCodeResetSchema = z.object({ + pinCode: z.string().regex(pinCodeRegex).optional().describe('New PIN code (4-6 digits)').meta({ example: '123456' }), + password: z + .string() + .optional() + .describe('User password (required if PIN code is not provided)') + .meta({ example: 'password' }), +}); -export class PinCodeSetupDto { - @ApiProperty({ description: 'PIN code (4-6 digits)' }) - @PinCode() - pinCode!: string; -} +const SessionUnlockSchema = PinCodeResetSchema.meta({ id: 'SessionUnlockDto' }); -export class PinCodeResetDto { - @ApiPropertyOptional({ description: 'New PIN code (4-6 digits)' }) - @PinCode({ optional: true }) - pinCode?: string; +const PinCodeChangeSchema = PinCodeResetSchema.extend({ + newPinCode: z.string().regex(pinCodeRegex).describe('New PIN code (4-6 digits)'), +}).meta({ id: 'PinCodeChangeDto' }); - @ApiPropertyOptional({ description: 'User password (required if PIN code is not provided)' }) - @Optional() - @IsString() - @IsNotEmpty() - password?: string; -} +const ValidateAccessTokenResponseSchema = z + .object({ + authStatus: z.boolean().describe('Authentication status'), + }) + .meta({ id: 'ValidateAccessTokenResponseDto' }); -export class SessionUnlockDto extends PinCodeResetDto {} +const OAuthCallbackSchema = z + .object({ + url: z.string().min(1).describe('OAuth callback URL'), + state: z.string().optional().describe('OAuth state parameter'), + codeVerifier: z.string().optional().describe('OAuth code verifier (PKCE)'), + }) + .meta({ id: 'OAuthCallbackDto' }); -export class PinCodeChangeDto extends PinCodeResetDto { - @ApiProperty({ description: 'New PIN code (4-6 digits)' }) - @PinCode() - newPinCode!: string; -} +const OAuthConfigSchema = z + .object({ + redirectUri: z.string().describe('OAuth redirect URI'), + state: z.string().optional().describe('OAuth state parameter'), + codeChallenge: z.string().optional().describe('OAuth code challenge (PKCE)'), + }) + .meta({ id: 'OAuthConfigDto' }); -export class ValidateAccessTokenResponseDto { - @ApiProperty({ description: 'Authentication status' }) - authStatus!: boolean; -} +const OAuthAuthorizeResponseSchema = z + .object({ + url: z.string().describe('OAuth authorization URL'), + }) + .meta({ id: 'OAuthAuthorizeResponseDto' }); -export class OAuthCallbackDto { - @ApiProperty({ description: 'OAuth callback URL' }) - @IsNotEmpty() - @IsString() - url!: string; +const AuthStatusResponseSchema = z + .object({ + pinCode: z.boolean().describe('Has PIN code set'), + password: z.boolean().describe('Has password set'), + isElevated: z.boolean().describe('Is elevated session'), + expiresAt: z.string().optional().describe('Session expiration date'), + pinExpiresAt: z.string().optional().describe('PIN expiration date'), + }) + .meta({ id: 'AuthStatusResponseDto' }); - @ApiPropertyOptional({ description: 'OAuth state parameter' }) - @Optional() - @IsString() - state?: string; - - @ApiPropertyOptional({ description: 'OAuth code verifier (PKCE)' }) - @Optional() - @IsString() - codeVerifier?: string; -} - -export class OAuthConfigDto { - @ApiProperty({ description: 'OAuth redirect URI' }) - @IsNotEmpty() - @IsString() - redirectUri!: string; - - @ApiPropertyOptional({ description: 'OAuth state parameter' }) - @Optional() - @IsString() - state?: string; - - @ApiPropertyOptional({ description: 'OAuth code challenge (PKCE)' }) - @Optional() - @IsString() - codeChallenge?: string; -} - -export class OAuthAuthorizeResponseDto { - @ApiProperty({ description: 'OAuth authorization URL' }) - url!: string; -} - -export class AuthStatusResponseDto { - @ApiProperty({ description: 'Has PIN code set' }) - pinCode!: boolean; - @ApiProperty({ description: 'Has password set' }) - password!: boolean; - @ApiProperty({ description: 'Is elevated session' }) - isElevated!: boolean; - @ApiPropertyOptional({ description: 'Session expiration date' }) - expiresAt?: string; - @ApiPropertyOptional({ description: 'PIN expiration date' }) - pinExpiresAt?: string; -} +export class LoginCredentialDto extends createZodDto(LoginCredentialSchema) {} +export class LoginResponseDto extends createZodDto(LoginResponseSchema) {} +export class LogoutResponseDto extends createZodDto(LogoutResponseSchema) {} +export class SignUpDto extends createZodDto(SignUpSchema) {} +export class ChangePasswordDto extends createZodDto(ChangePasswordSchema) {} +export class PinCodeSetupDto extends createZodDto(PinCodeSetupSchema) {} +export class PinCodeResetDto extends createZodDto(PinCodeResetSchema) {} +export class SessionUnlockDto extends createZodDto(SessionUnlockSchema) {} +export class PinCodeChangeDto extends createZodDto(PinCodeChangeSchema) {} +export class ValidateAccessTokenResponseDto extends createZodDto(ValidateAccessTokenResponseSchema) {} +export class OAuthCallbackDto extends createZodDto(OAuthCallbackSchema) {} +export class OAuthConfigDto extends createZodDto(OAuthConfigSchema) {} +export class OAuthAuthorizeResponseDto extends createZodDto(OAuthAuthorizeResponseSchema) {} +export class AuthStatusResponseDto extends createZodDto(AuthStatusResponseSchema) {} diff --git a/server/src/dtos/bbox.dto.ts b/server/src/dtos/bbox.dto.ts index 1afe9f53ba99..8c24173791ae 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 c0554f83b708..34dd8f2a62ea 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 ef52a72bd0d4..b44a6a7afcf6 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 40b1b74c70ab..55427e36aa92 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 8217fec41c57..9f5b3521952c 100644 --- a/server/src/dtos/editing.dto.ts +++ b/server/src/dtos/editing.dto.ts @@ -1,7 +1,5 @@ -import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { ArrayMinSize, IsEnum, IsInt, Min, ValidateNested } from 'class-validator'; -import { IsAxisAlignedRotation, IsUniqueEditActions, ValidateEnum, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; export enum AssetEditAction { Crop = 'crop', @@ -9,103 +7,128 @@ export enum AssetEditAction { Mirror = 'mirror', } +export const AssetEditActionSchema = z + .enum(AssetEditAction) + .describe('Type of edit action to perform') + .meta({ id: 'AssetEditAction' }); + export enum MirrorAxis { Horizontal = 'horizontal', Vertical = 'vertical', } -export class CropParameters { - @IsInt() - @Min(0) - @ApiProperty({ description: 'Top-Left X coordinate of crop' }) - x!: number; +const MirrorAxisSchema = z.enum(['horizontal', 'vertical']).describe('Axis to mirror along').meta({ id: 'MirrorAxis' }); - @IsInt() - @Min(0) - @ApiProperty({ description: 'Top-Left Y coordinate of crop' }) - y!: number; - - @IsInt() - @Min(1) - @ApiProperty({ description: 'Width of the crop' }) - width!: number; - - @IsInt() - @Min(1) - @ApiProperty({ description: 'Height of the crop' }) - height!: number; -} - -export class RotateParameters { - @IsAxisAlignedRotation() - @ApiProperty({ description: 'Rotation angle in degrees' }) - angle!: number; -} - -export class MirrorParameters { - @IsEnum(MirrorAxis) - @ApiProperty({ enum: MirrorAxis, enumName: 'MirrorAxis', description: 'Axis to mirror along' }) - axis!: MirrorAxis; -} - -export type AssetEditParameters = CropParameters | RotateParameters | MirrorParameters; -export type AssetEditActionItem = - | { - action: AssetEditAction.Crop; - parameters: CropParameters; - } - | { - action: AssetEditAction.Rotate; - parameters: RotateParameters; - } - | { - action: AssetEditAction.Mirror; - parameters: MirrorParameters; - }; - -@ApiExtraModels(CropParameters, RotateParameters, MirrorParameters) -export class AssetEditActionItemDto { - @ValidateEnum({ name: 'AssetEditAction', enum: AssetEditAction, description: 'Type of edit action to perform' }) - action!: AssetEditAction; - - @ApiProperty({ - description: 'List of edit actions to apply (crop, rotate, or mirror)', - anyOf: [CropParameters, RotateParameters, MirrorParameters].map((type) => ({ - $ref: getSchemaPath(type), - })), +const CropParametersSchema = z + .object({ + x: z.number().min(0).describe('Top-Left X coordinate of crop'), + y: z.number().min(0).describe('Top-Left Y coordinate of crop'), + width: z.number().min(1).describe('Width of the crop'), + height: z.number().min(1).describe('Height of the crop'), }) - @ValidateNested() - @Type((options) => actionParameterMap[options?.object.action as keyof AssetEditActionParameter]) - parameters!: AssetEditActionItem['parameters']; -} + .meta({ id: 'CropParameters' }); -export class AssetEditActionItemResponseDto extends AssetEditActionItemDto { - @ValidateUUID() - id!: string; -} +const RotateParametersSchema = z + .object({ + angle: z + .number() + .refine((v) => [0, 90, 180, 270].includes(v), { + error: 'Angle must be one of the following values: 0, 90, 180, 270', + }) + .describe('Rotation angle in degrees'), + }) + .meta({ id: 'RotateParameters' }); + +const MirrorParametersSchema = z + .object({ + axis: MirrorAxisSchema, + }) + .meta({ id: 'MirrorParameters' }); + +// TODO: ideally we would use the discriminated union directly in the future not only for type support but also for validation and openapi generation +const __AssetEditActionItemSchema = z.discriminatedUnion('action', [ + z.object({ action: AssetEditActionSchema.extract(['Crop']), parameters: CropParametersSchema }), + z.object({ action: AssetEditActionSchema.extract(['Rotate']), parameters: RotateParametersSchema }), + z.object({ action: AssetEditActionSchema.extract(['Mirror']), parameters: MirrorParametersSchema }), +]); + +const AssetEditParametersSchema = z + .union([CropParametersSchema, RotateParametersSchema, MirrorParametersSchema], { + error: getExpectedKeysByActionMessage, + }) + .describe('List of edit actions to apply (crop, rotate, or mirror)'); -export type AssetEditActionParameter = typeof actionParameterMap; const actionParameterMap = { - [AssetEditAction.Crop]: CropParameters, - [AssetEditAction.Rotate]: RotateParameters, - [AssetEditAction.Mirror]: MirrorParameters, -}; + [AssetEditAction.Crop]: CropParametersSchema, + [AssetEditAction.Rotate]: RotateParametersSchema, + [AssetEditAction.Mirror]: MirrorParametersSchema, +} as const; -export class AssetEditsCreateDto { - @ArrayMinSize(1) - @IsUniqueEditActions() - @ValidateNested({ each: true }) - @Type(() => AssetEditActionItemDto) - @ApiProperty({ description: 'List of edit actions to apply (crop, rotate, or mirror)' }) - edits!: AssetEditActionItemDto[]; +function getExpectedKeysByActionMessage(): string { + const expectedByAction = Object.entries(actionParameterMap) + .map(([action, schema]) => `${action}: [${Object.keys(schema.shape).join(', ')}]`) + .join('; '); + + return `Invalid parameters for action, expected keys by action: ${expectedByAction}`; } -export class AssetEditsResponseDto { - @ValidateUUID({ description: 'Asset ID these edits belong to' }) - assetId!: string; +function isParametersValidForAction(edit: z.infer): boolean { + return actionParameterMap[edit.action].safeParse(edit.parameters).success; +} - @ApiProperty({ - description: 'List of edit actions applied to the asset', +const AssetEditActionItemSchema = z + .object({ + action: AssetEditActionSchema, + parameters: AssetEditParametersSchema, }) - edits!: AssetEditActionItemResponseDto[]; + .superRefine((edit, ctx) => { + if (!isParametersValidForAction(edit)) { + ctx.addIssue({ + code: 'custom', + path: ['parameters'], + message: `Invalid parameters for action '${edit.action}', expecting keys: ${Object.keys(actionParameterMap[edit.action].shape).join(', ')}`, + }); + } + }) + .meta({ id: 'AssetEditActionItemDto' }); + +export type AssetEditActionItem = z.infer; +export type AssetEditParameters = AssetEditActionItem['parameters']; + +function uniqueEditActions(edits: z.infer[]): boolean { + const keys = new Set(); + for (const edit of edits) { + const key = edit.action === 'mirror' ? `mirror-${JSON.stringify(edit.parameters)}` : edit.action; + if (keys.has(key)) { + return false; + } + keys.add(key); + } + return true; } + +const AssetEditsCreateSchema = z + .object({ + edits: z + .array(AssetEditActionItemSchema) + .min(1) + .describe('List of edit actions to apply (crop, rotate, or mirror)') + .refine(uniqueEditActions, { error: 'Duplicate edit actions are not allowed' }), + }) + .meta({ id: 'AssetEditsCreateDto' }); + +const AssetEditActionItemResponseSchema = AssetEditActionItemSchema.extend({ + id: z.uuidv4().describe('Asset edit ID'), +}).meta({ id: 'AssetEditActionItemResponseDto' }); + +const AssetEditsResponseSchema = z + .object({ + assetId: z.uuidv4().describe('Asset ID these edits belong to'), + edits: z.array(AssetEditActionItemResponseSchema).describe('List of edit actions applied to the asset'), + }) + .meta({ id: 'AssetEditsResponseDto' }); + +export class AssetEditActionItemResponseDto extends createZodDto(AssetEditActionItemResponseSchema) {} +export class AssetEditsCreateDto extends createZodDto(AssetEditsCreateSchema) {} +export class AssetEditsResponseDto extends createZodDto(AssetEditsResponseSchema) {} +export type CropParameters = z.infer; diff --git a/server/src/dtos/env.dto.ts b/server/src/dtos/env.dto.ts index bdcf3614fd63..fc30875b5abd 100644 --- a/server/src/dtos/env.dto.ts +++ b/server/src/dtos/env.dto.ts @@ -1,7 +1,6 @@ -import { Transform, Type } from 'class-transformer'; -import { IsEnum, IsInt, IsString, Matches } from 'class-validator'; -import { ImmichEnvironment, LogFormat, LogLevel } from 'src/enum'; -import { IsIPRange, Optional, ValidateBoolean } from 'src/validation'; +import { ImmichEnvironmentSchema, LogFormatSchema, LogLevelSchema } from 'src/enum'; +import { IsIPRange } from 'src/validation'; +import z from 'zod'; // TODO import from sql-tools once the swagger plugin supports external enums enum DatabaseSslMode { @@ -12,214 +11,80 @@ enum DatabaseSslMode { VerifyFull = 'verify-full', } -export class EnvDto { - @IsInt() - @Optional() - @Type(() => Number) - IMMICH_API_METRICS_PORT?: number; +const DatabaseSslModeSchema = z.enum(DatabaseSslMode).describe('Database SSL mode').meta({ id: 'DatabaseSslMode' }); +const absolutePath = z.string().regex(/^\//, 'Must be an absolute path').optional(); +/** + * Treat certain strings as booleans and coerce them to boolean + * Ideal for environment variables that are strings but should be treated as booleans + * @docs https://zod.dev/api?id=stringbool + */ +const stringBool = z.stringbool(); - @IsString() - @Optional() - IMMICH_BUILD_DATA?: string; - - @IsString() - @Optional() - IMMICH_BUILD?: string; - - @IsString() - @Optional() - IMMICH_BUILD_URL?: string; - - @IsString() - @Optional() - IMMICH_BUILD_IMAGE?: string; - - @IsString() - @Optional() - IMMICH_BUILD_IMAGE_URL?: string; - - @IsString() - @Optional() - IMMICH_CONFIG_FILE?: string; - - @IsString() - @Optional() - IMMICH_HELMET_FILE?: string; - - @IsEnum(ImmichEnvironment) - @Optional() - IMMICH_ENV?: ImmichEnvironment; - - @IsString() - @Optional() - IMMICH_HOST?: string; - - @ValidateBoolean({ optional: true }) - IMMICH_IGNORE_MOUNT_CHECK_ERRORS?: boolean; - - @IsEnum(LogLevel) - @Optional() - IMMICH_LOG_LEVEL?: LogLevel; - - @IsEnum(LogFormat) - @Optional() - IMMICH_LOG_FORMAT?: LogFormat; - - @Optional() - @Matches(/^\//, { message: 'IMMICH_MEDIA_LOCATION must be an absolute path' }) - IMMICH_MEDIA_LOCATION?: string; - - @IsInt() - @Optional() - @Type(() => Number) - IMMICH_MICROSERVICES_METRICS_PORT?: number; - - @ValidateBoolean({ optional: true }) - IMMICH_ALLOW_EXTERNAL_PLUGINS?: boolean; - - @Optional() - @Matches(/^\//, { message: 'IMMICH_PLUGINS_INSTALL_FOLDER must be an absolute path' }) - IMMICH_PLUGINS_INSTALL_FOLDER?: string; - - @IsInt() - @Optional() - @Type(() => Number) - IMMICH_PORT?: number; - - @IsString() - @Optional() - IMMICH_REPOSITORY?: string; - - @IsString() - @Optional() - IMMICH_REPOSITORY_URL?: string; - - @IsString() - @Optional() - IMMICH_SOURCE_REF?: string; - - @IsString() - @Optional() - IMMICH_SOURCE_COMMIT?: string; - - @IsString() - @Optional() - IMMICH_SOURCE_URL?: string; - - @IsString() - @Optional() - IMMICH_TELEMETRY_INCLUDE?: string; - - @IsString() - @Optional() - IMMICH_TELEMETRY_EXCLUDE?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_SOURCE_URL?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_BUG_FEATURE_URL?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_DOCUMENTATION_URL?: string; - - @IsString() - @Optional() - IMMICH_THIRD_PARTY_SUPPORT_URL?: string; - - @ValidateBoolean({ optional: true }) - IMMICH_ALLOW_SETUP?: boolean; - - @IsIPRange({ requireCIDR: false }, { each: true }) - @Transform(({ value }) => - value && typeof value === 'string' - ? value +const trustedProxiesSchema = z + .string() + .optional() + .transform((s) => + s + ? s .split(',') - .map((value) => value.trim()) + .map((x) => x.trim()) .filter(Boolean) - : value, + : undefined, ) - @Optional() - IMMICH_TRUSTED_PROXIES?: string[]; - @IsString() - @Optional() - IMMICH_WORKERS_INCLUDE?: string; + .pipe(z.union([z.undefined(), IsIPRange({ requireCIDR: false })])); - @IsString() - @Optional() - IMMICH_WORKERS_EXCLUDE?: string; - - @IsString() - @Optional() - DB_DATABASE_NAME?: string; - - @IsString() - @Optional() - DB_HOSTNAME?: string; - - @IsString() - @Optional() - DB_PASSWORD?: string; - - @IsInt() - @Optional() - @Type(() => Number) - DB_PORT?: number; - - @ValidateBoolean({ optional: true }) - DB_SKIP_MIGRATIONS?: boolean; - - @IsEnum(DatabaseSslMode) - @Optional() - DB_SSL_MODE?: DatabaseSslMode; - - @IsString() - @Optional() - DB_URL?: string; - - @IsString() - @Optional() - DB_USERNAME?: string; - - @IsEnum(['pgvector', 'pgvecto.rs', 'vectorchord']) - @Optional() - DB_VECTOR_EXTENSION?: 'pgvector' | 'pgvecto.rs' | 'vectorchord'; - - @IsString() - @Optional() - NO_COLOR?: string; - - @IsString() - @Optional() - REDIS_HOSTNAME?: string; - - @IsInt() - @Optional() - @Type(() => Number) - REDIS_PORT?: number; - - @IsInt() - @Optional() - @Type(() => Number) - REDIS_DBINDEX?: number; - - @IsString() - @Optional() - REDIS_USERNAME?: string; - - @IsString() - @Optional() - REDIS_PASSWORD?: string; - - @IsString() - @Optional() - REDIS_SOCKET?: string; - - @IsString() - @Optional() - REDIS_URL?: string; -} +export const EnvSchema = z + .object({ + IMMICH_API_METRICS_PORT: z.coerce.number().int().optional(), + IMMICH_BUILD_DATA: z.string().optional(), + IMMICH_BUILD: z.string().optional(), + IMMICH_BUILD_URL: z.string().optional(), + IMMICH_BUILD_IMAGE: z.string().optional(), + IMMICH_BUILD_IMAGE_URL: z.string().optional(), + IMMICH_CONFIG_FILE: z.string().optional(), + IMMICH_HELMET_FILE: z.string().optional(), + IMMICH_ENV: ImmichEnvironmentSchema.optional(), + IMMICH_HOST: z.string().optional(), + IMMICH_IGNORE_MOUNT_CHECK_ERRORS: stringBool.optional(), + IMMICH_LOG_LEVEL: LogLevelSchema.optional(), + IMMICH_LOG_FORMAT: LogFormatSchema.optional(), + IMMICH_MEDIA_LOCATION: absolutePath, + IMMICH_MICROSERVICES_METRICS_PORT: z.coerce.number().int().optional(), + IMMICH_ALLOW_EXTERNAL_PLUGINS: stringBool.optional(), + IMMICH_PLUGINS_INSTALL_FOLDER: absolutePath, + IMMICH_PORT: z.coerce.number().int().optional(), + IMMICH_REPOSITORY: z.string().optional(), + IMMICH_REPOSITORY_URL: z.string().optional(), + IMMICH_SOURCE_REF: z.string().optional(), + IMMICH_SOURCE_COMMIT: z.string().optional(), + IMMICH_SOURCE_URL: z.string().optional(), + IMMICH_TELEMETRY_INCLUDE: z.string().optional(), + IMMICH_TELEMETRY_EXCLUDE: z.string().optional(), + IMMICH_THIRD_PARTY_SOURCE_URL: z.string().optional(), + IMMICH_THIRD_PARTY_BUG_FEATURE_URL: z.string().optional(), + IMMICH_THIRD_PARTY_DOCUMENTATION_URL: z.string().optional(), + IMMICH_THIRD_PARTY_SUPPORT_URL: z.string().optional(), + IMMICH_ALLOW_SETUP: stringBool.optional(), + IMMICH_TRUSTED_PROXIES: trustedProxiesSchema, + IMMICH_WORKERS_INCLUDE: z.string().optional(), + IMMICH_WORKERS_EXCLUDE: z.string().optional(), + DB_DATABASE_NAME: z.string().optional(), + DB_HOSTNAME: z.string().optional(), + DB_PASSWORD: z.string().optional(), + DB_PORT: z.coerce.number().int().optional(), + DB_SKIP_MIGRATIONS: stringBool.optional(), + DB_SSL_MODE: DatabaseSslModeSchema.optional(), + DB_URL: z.string().optional(), + DB_USERNAME: z.string().optional(), + DB_VECTOR_EXTENSION: z.enum(['pgvector', 'pgvecto.rs', 'vectorchord']).optional(), + NO_COLOR: z.string().optional(), + REDIS_HOSTNAME: z.string().optional(), + REDIS_PORT: z.coerce.number().int().optional(), + REDIS_DBINDEX: z.coerce.number().int().optional(), + REDIS_USERNAME: z.string().optional(), + REDIS_PASSWORD: z.string().optional(), + REDIS_SOCKET: z.string().optional(), + REDIS_URL: z.string().optional(), + }) + .meta({ id: 'EnvDto' }); diff --git a/server/src/dtos/exif.dto.ts b/server/src/dtos/exif.dto.ts index 165ecde4db54..c3e1ab36c837 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 ef34a417203c..325dae4d2e89 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 3f71b8a0ed69..aafdd9f79341 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -1,58 +1,30 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Library } from 'src/database'; -import { Optional, ValidateUUID } from 'src/validation'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class CreateLibraryDto { - @ValidateUUID({ description: 'Owner user ID' }) - ownerId!: string; +const stringArrayMax128 = z + .array(z.string()) + .max(128) + .refine((arr) => arr.every((s) => s.trim() !== ''), 'Array items must not be empty') + .refine((arr) => new Set(arr).size === arr.length, 'Array must have unique items'); - @ApiPropertyOptional({ description: 'Library name' }) - @IsString() - @Optional() - @IsNotEmpty() - name?: string; +const CreateLibrarySchema = z + .object({ + ownerId: z.uuidv4().describe('Owner user ID'), + name: z.string().min(1).optional().describe('Library name'), + importPaths: stringArrayMax128.optional().describe('Import paths (max 128)'), + exclusionPatterns: stringArrayMax128.optional().describe('Exclusion patterns (max 128)'), + }) + .meta({ id: 'CreateLibraryDto' }); - @ApiPropertyOptional({ description: 'Import paths (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - importPaths?: string[]; - - @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - exclusionPatterns?: string[]; -} - -export class UpdateLibraryDto { - @ApiPropertyOptional({ description: 'Library name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ApiPropertyOptional({ description: 'Import paths (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - importPaths?: string[]; - - @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) - @Optional() - @IsNotEmpty({ each: true }) - @IsString({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - exclusionPatterns?: string[]; -} +const UpdateLibrarySchema = z + .object({ + name: z.string().min(1).optional().describe('Library name'), + importPaths: stringArrayMax128.optional().describe('Import paths (max 128)'), + exclusionPatterns: stringArrayMax128.optional().describe('Exclusion patterns (max 128)'), + }) + .meta({ id: 'UpdateLibraryDto' }); export interface CrawlOptionsDto { pathsToCrawl: string[]; @@ -64,81 +36,60 @@ export interface WalkOptionsDto extends CrawlOptionsDto { take: number; } -export class ValidateLibraryDto { - @ApiPropertyOptional({ description: 'Import paths to validate (max 128)' }) - @Optional() - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - importPaths?: string[]; +const ValidateLibrarySchema = z + .object({ + importPaths: stringArrayMax128.optional().describe('Import paths to validate (max 128)'), + exclusionPatterns: stringArrayMax128.optional().describe('Exclusion patterns (max 128)'), + }) + .meta({ id: 'ValidateLibraryDto' }); - @ApiPropertyOptional({ description: 'Exclusion patterns (max 128)' }) - @Optional() - @IsNotEmpty({ each: true }) - @IsString({ each: true }) - @ArrayUnique() - @ArrayMaxSize(128) - exclusionPatterns?: string[]; -} +const ValidateLibraryImportPathResponseSchema = z + .object({ + importPath: z.string().describe('Import path'), + isValid: z.boolean().describe('Is valid'), + message: z.string().optional().describe('Validation message'), + }) + .meta({ id: 'ValidateLibraryImportPathResponseDto' }); -export class ValidateLibraryResponseDto { - @ApiPropertyOptional({ description: 'Validation results for import paths' }) - importPaths?: ValidateLibraryImportPathResponseDto[]; -} +const ValidateLibraryResponseSchema = z + .object({ + importPaths: z + .array(ValidateLibraryImportPathResponseSchema) + .optional() + .describe('Validation results for import paths'), + }) + .meta({ id: 'ValidateLibraryResponseDto' }); -export class ValidateLibraryImportPathResponseDto { - @ApiProperty({ description: 'Import path' }) - importPath!: string; - @ApiProperty({ description: 'Is valid' }) - isValid: boolean = false; - @ApiPropertyOptional({ description: 'Validation message' }) - message?: string; -} +const LibraryResponseSchema = z + .object({ + id: z.string().describe('Library ID'), + ownerId: z.string().describe('Owner user ID'), + name: z.string().describe('Library name'), + assetCount: z.int().describe('Number of assets'), + importPaths: z.array(z.string()).describe('Import paths'), + exclusionPatterns: z.array(z.string()).describe('Exclusion patterns'), + createdAt: isoDatetimeToDate.describe('Creation date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + refreshedAt: isoDatetimeToDate.nullable().describe('Last refresh date'), + }) + .meta({ id: 'LibraryResponseDto' }); -export class LibrarySearchDto { - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} +const LibraryStatsResponseSchema = z + .object({ + photos: z.int().describe('Number of photos'), + videos: z.int().describe('Number of videos'), + total: z.int().describe('Total number of assets'), + usage: z.int().describe('Storage usage in bytes'), + }) + .meta({ id: 'LibraryStatsResponseDto' }); -export class LibraryResponseDto { - @ApiProperty({ description: 'Library ID' }) - id!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ApiProperty({ description: 'Library name' }) - name!: string; - - @ApiProperty({ type: 'integer', description: 'Number of assets' }) - assetCount!: number; - - @ApiProperty({ description: 'Import paths' }) - importPaths!: string[]; - - @ApiProperty({ description: 'Exclusion patterns' }) - exclusionPatterns!: string[]; - - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; - @ApiProperty({ description: 'Last refresh date' }) - refreshedAt!: Date | null; -} - -export class LibraryStatsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of photos' }) - photos = 0; - - @ApiProperty({ type: 'integer', description: 'Number of videos' }) - videos = 0; - - @ApiProperty({ type: 'integer', description: 'Total number of assets' }) - total = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' }) - usage = 0; -} +export class CreateLibraryDto extends createZodDto(CreateLibrarySchema) {} +export class UpdateLibraryDto extends createZodDto(UpdateLibrarySchema) {} +export class ValidateLibraryDto extends createZodDto(ValidateLibrarySchema) {} +export class ValidateLibraryResponseDto extends createZodDto(ValidateLibraryResponseSchema) {} +export class ValidateLibraryImportPathResponseDto extends createZodDto(ValidateLibraryImportPathResponseSchema) {} +export class LibraryResponseDto extends createZodDto(LibraryResponseSchema) {} +export class LibraryStatsResponseDto extends createZodDto(LibraryStatsResponseSchema) {} export function mapLibrary(entity: Library): LibraryResponseDto { let assetCount = 0; diff --git a/server/src/dtos/license.dto.ts b/server/src/dtos/license.dto.ts index 14232940b698..a68905fb4799 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 f31d9ffa231b..9b1c0b63c072 100644 --- a/server/src/dtos/maintenance.dto.ts +++ b/server/src/dtos/maintenance.dto.ts @@ -1,49 +1,57 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { ValidateIf } from 'class-validator'; -import { MaintenanceAction, StorageFolder } from 'src/enum'; -import { ValidateBoolean, ValidateEnum, ValidateString } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { MaintenanceAction, MaintenanceActionSchema, StorageFolderSchema } from 'src/enum'; +import z from 'zod'; -export class SetMaintenanceModeDto { - @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' }) - action!: MaintenanceAction; +const SetMaintenanceModeSchema = z + .object({ + action: MaintenanceActionSchema, + restoreBackupFilename: z.string().optional().describe('Restore backup filename'), + }) + .refine( + (data) => data.action !== MaintenanceAction.RestoreDatabase || (data.restoreBackupFilename?.length ?? 0) > 0, + { error: 'Backup filename is required when action is restore_database', path: ['restoreBackupFilename'] }, + ) + .meta({ id: 'SetMaintenanceModeDto' }); - @ValidateIf((o) => o.action === MaintenanceAction.RestoreDatabase) - @ValidateString({ description: 'Restore backup filename' }) - restoreBackupFilename?: string; -} +const MaintenanceLoginSchema = z + .object({ + token: z.string().optional().describe('Maintenance token'), + }) + .meta({ id: 'MaintenanceLoginDto' }); -export class MaintenanceLoginDto { - @ValidateString({ optional: true, description: 'Maintenance token' }) - token?: string; -} +const MaintenanceAuthSchema = z + .object({ + username: z.string().describe('Maintenance username'), + }) + .meta({ id: 'MaintenanceAuthDto' }); -export class MaintenanceAuthDto { - @ApiProperty({ description: 'Maintenance username' }) - username!: string; -} +const MaintenanceStatusResponseSchema = z + .object({ + active: z.boolean(), + action: MaintenanceActionSchema, + progress: z.number().optional(), + task: z.string().optional(), + error: z.string().optional(), + }) + .meta({ id: 'MaintenanceStatusResponseDto' }); -export class MaintenanceStatusResponseDto { - active!: boolean; +const MaintenanceDetectInstallStorageFolderSchema = z + .object({ + folder: StorageFolderSchema, + readable: z.boolean().describe('Whether the folder is readable'), + writable: z.boolean().describe('Whether the folder is writable'), + files: z.number().describe('Number of files in the folder'), + }) + .meta({ id: 'MaintenanceDetectInstallStorageFolderDto' }); - @ValidateEnum({ enum: MaintenanceAction, name: 'MaintenanceAction', description: 'Maintenance action' }) - action!: MaintenanceAction; +const MaintenanceDetectInstallResponseSchema = z + .object({ + storage: z.array(MaintenanceDetectInstallStorageFolderSchema), + }) + .meta({ id: 'MaintenanceDetectInstallResponseDto' }); - progress?: number; - task?: string; - error?: string; -} - -export class MaintenanceDetectInstallStorageFolderDto { - @ValidateEnum({ enum: StorageFolder, name: 'StorageFolder', description: 'Storage folder' }) - folder!: StorageFolder; - @ValidateBoolean({ description: 'Whether the folder is readable' }) - readable!: boolean; - @ValidateBoolean({ description: 'Whether the folder is writable' }) - writable!: boolean; - @ApiProperty({ description: 'Number of files in the folder' }) - files!: number; -} - -export class MaintenanceDetectInstallResponseDto { - storage!: MaintenanceDetectInstallStorageFolderDto[]; -} +export class SetMaintenanceModeDto extends createZodDto(SetMaintenanceModeSchema) {} +export class MaintenanceLoginDto extends createZodDto(MaintenanceLoginSchema) {} +export class MaintenanceAuthDto extends createZodDto(MaintenanceAuthSchema) {} +export class MaintenanceStatusResponseDto extends createZodDto(MaintenanceStatusResponseSchema) {} +export class MaintenanceDetectInstallResponseDto extends createZodDto(MaintenanceDetectInstallResponseSchema) {} diff --git a/server/src/dtos/map.dto.ts b/server/src/dtos/map.dto.ts index d8db175c289c..6a4776d49d42 100644 --- a/server/src/dtos/map.dto.ts +++ b/server/src/dtos/map.dto.ts @@ -1,67 +1,45 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsLatitude, IsLongitude } from 'class-validator'; -import { ValidateBoolean, ValidateDate } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { isoDatetimeToDate, latitudeSchema, longitudeSchema, stringToBool } from 'src/validation'; +import z from 'zod'; -export class MapReverseGeocodeDto { - @ApiProperty({ format: 'double', description: 'Latitude (-90 to 90)' }) - @Type(() => Number) - @IsLatitude({ message: ({ property }) => `${property} must be a number between -90 and 90` }) - lat!: number; +const MapReverseGeocodeSchema = z + .object({ + lat: z.coerce.number().meta({ format: 'double' }).pipe(latitudeSchema).describe('Latitude (-90 to 90)'), + lon: z.coerce.number().meta({ format: 'double' }).pipe(longitudeSchema).describe('Longitude (-180 to 180)'), + }) + .meta({ id: 'MapReverseGeocodeDto' }); - @ApiProperty({ format: 'double', description: 'Longitude (-180 to 180)' }) - @Type(() => Number) - @IsLongitude({ message: ({ property }) => `${property} must be a number between -180 and 180` }) - lon!: number; -} +const MapReverseGeocodeResponseSchema = z + .object({ + city: z.string().nullable().describe('City name'), + state: z.string().nullable().describe('State/Province name'), + country: z.string().nullable().describe('Country name'), + }) + .meta({ id: 'MapReverseGeocodeResponseDto' }); -export class MapReverseGeocodeResponseDto { - @ApiProperty({ description: 'City name' }) - city!: string | null; +const MapMarkerSchema = z + .object({ + isArchived: stringToBool.optional().describe('Filter by archived status'), + isFavorite: stringToBool.optional().describe('Filter by favorite status'), + fileCreatedAfter: isoDatetimeToDate.optional().describe('Filter assets created after this date'), + fileCreatedBefore: isoDatetimeToDate.optional().describe('Filter assets created before this date'), + withPartners: stringToBool.optional().describe('Include partner assets'), + withSharedAlbums: stringToBool.optional().describe('Include shared album assets'), + }) + .meta({ id: 'MapMarkerDto' }); - @ApiProperty({ description: 'State/Province name' }) - state!: string | null; +const MapMarkerResponseSchema = z + .object({ + id: z.string().describe('Asset ID'), + lat: z.number().meta({ format: 'double' }).describe('Latitude'), + lon: z.number().meta({ format: 'double' }).describe('Longitude'), + city: z.string().nullable().describe('City name'), + state: z.string().nullable().describe('State/Province name'), + country: z.string().nullable().describe('Country name'), + }) + .meta({ id: 'MapMarkerResponseDto' }); - @ApiProperty({ description: 'Country name' }) - country!: string | null; -} - -export class MapMarkerDto { - @ValidateBoolean({ optional: true, description: 'Filter by archived status' }) - isArchived?: boolean; - - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; - - @ValidateDate({ optional: true, description: 'Filter assets created after this date' }) - fileCreatedAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter assets created before this date' }) - fileCreatedBefore?: Date; - - @ValidateBoolean({ optional: true, description: 'Include partner assets' }) - withPartners?: boolean; - - @ValidateBoolean({ optional: true, description: 'Include shared album assets' }) - withSharedAlbums?: boolean; -} - -export class MapMarkerResponseDto { - @ApiProperty({ description: 'Asset ID' }) - id!: string; - - @ApiProperty({ format: 'double', description: 'Latitude' }) - lat!: number; - - @ApiProperty({ format: 'double', description: 'Longitude' }) - lon!: number; - - @ApiProperty({ description: 'City name' }) - city!: string | null; - - @ApiProperty({ description: 'State/Province name' }) - state!: string | null; - - @ApiProperty({ description: 'Country name' }) - country!: string | null; -} +export class MapReverseGeocodeDto extends createZodDto(MapReverseGeocodeSchema) {} +export class MapReverseGeocodeResponseDto extends createZodDto(MapReverseGeocodeResponseSchema) {} +export class MapMarkerDto extends createZodDto(MapMarkerSchema) {} +export class MapMarkerResponseDto extends createZodDto(MapMarkerResponseSchema) {} diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index edf65ef583db..334520dded7f 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -1,136 +1,87 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Memory } from 'src/database'; import { HistoryBuilder } from 'src/decorators'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AssetOrderWithRandom, MemoryType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { AssetOrderWithRandomSchema, MemoryType, MemoryTypeSchema } from 'src/enum'; +import { isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; -class MemoryBaseDto { - @ValidateBoolean({ optional: true, description: 'Is memory saved' }) - isSaved?: boolean; - - @ValidateDate({ optional: true, description: 'Date when memory was seen' }) - seenAt?: Date; -} - -export class MemorySearchDto { - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type', optional: true }) - type?: MemoryType; - - @ValidateDate({ optional: true, description: 'Filter by date' }) - for?: Date; - - @ValidateBoolean({ optional: true, description: 'Include trashed memories' }) - isTrashed?: boolean; - - @ValidateBoolean({ optional: true, description: 'Filter by saved status' }) - isSaved?: boolean; - - @IsInt() - @IsPositive() - @Type(() => Number) - @Optional() - @ApiProperty({ type: 'integer', description: 'Number of memories to return' }) - size?: number; - - @ValidateEnum({ enum: AssetOrderWithRandom, name: 'MemorySearchOrder', description: 'Sort order', optional: true }) - order?: AssetOrderWithRandom; -} - -class OnThisDayDto { - @ApiProperty({ type: 'number', description: 'Year for on this day memory', minimum: 1 }) - @IsInt() - @IsPositive() - year!: number; -} - -type MemoryData = OnThisDayDto; - -export class MemoryUpdateDto extends MemoryBaseDto { - @ValidateDate({ optional: true, description: 'Memory date' }) - memoryAt?: Date; -} - -export class MemoryCreateDto extends MemoryBaseDto { - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) - type!: MemoryType; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @IsObject() - @ValidateNested() - @Type((options) => { - switch (options?.object.type) { - case MemoryType.OnThisDay: { - return OnThisDayDto; - } - - default: { - return Object; - } - } +const MemorySearchSchema = z + .object({ + type: MemoryTypeSchema.optional(), + for: isoDatetimeToDate.optional().describe('Filter by date'), + isTrashed: stringToBool.optional().describe('Include trashed memories'), + isSaved: stringToBool.optional().describe('Filter by saved status'), + size: z.coerce.number().int().min(1).optional().describe('Number of memories to return'), + order: AssetOrderWithRandomSchema.optional(), }) - data!: MemoryData; + .meta({ id: 'MemorySearchDto' }); - @ValidateDate({ description: 'Memory date' }) - memoryAt!: Date; - - @ValidateDate({ - optional: true, - description: 'Date when memory should be shown', - history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), +const OnThisDaySchema = z + .object({ + year: z.int().min(1000).max(9999).describe('Year for on this day memory'), }) - showAt?: Date; + .meta({ id: 'OnThisDayDto' }); - @ValidateDate({ - optional: true, - description: 'Date when memory should be hidden', - history: new HistoryBuilder().added('v2.6.0').stable('v2.6.0'), +type MemoryData = z.infer; + +const MemoryUpdateSchema = z + .object({ + isSaved: z.boolean().optional().describe('Is memory saved'), + seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'), + memoryAt: isoDatetimeToDate.optional().describe('Memory date'), }) - hideAt?: Date; + .meta({ id: 'MemoryUpdateDto' }); - @ValidateUUID({ optional: true, each: true, description: 'Asset IDs to associate with memory' }) - assetIds?: string[]; -} +const MemoryCreateSchema = z + .object({ + type: MemoryTypeSchema, + data: OnThisDaySchema, + memoryAt: isoDatetimeToDate.describe('Memory date'), + assetIds: z.array(z.uuidv4()).optional().describe('Asset IDs to associate with memory'), + isSaved: z.boolean().optional().describe('Is memory saved'), + seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'), + showAt: isoDatetimeToDate + .optional() + .describe('Date when memory should be shown') + .meta(new HistoryBuilder().added('v2.6.0').stable('v2.6.0').getExtensions()), + hideAt: isoDatetimeToDate + .optional() + .describe('Date when memory should be hidden') + .meta(new HistoryBuilder().added('v2.6.0').stable('v2.6.0').getExtensions()), + }) + .meta({ id: 'MemoryCreateDto' }); -export class MemoryStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of memories' }) - total!: number; -} +const MemoryStatisticsResponseSchema = z + .object({ + total: z.int().describe('Total number of memories'), + }) + .meta({ id: 'MemoryStatisticsResponseDto' }); -export class MemoryResponseDto { - @ApiProperty({ description: 'Memory ID' }) - id!: string; - @ValidateDate({ description: 'Creation date' }) - createdAt!: Date; - @ValidateDate({ description: 'Last update date' }) - updatedAt!: Date; - @ValidateDate({ optional: true, description: 'Deletion date' }) - deletedAt?: Date; - @ValidateDate({ description: 'Memory date' }) - memoryAt!: Date; - @ValidateDate({ optional: true, description: 'Date when memory was seen' }) - seenAt?: Date; - @ValidateDate({ optional: true, description: 'Date when memory should be shown' }) - showAt?: Date; - @ValidateDate({ optional: true, description: 'Date when memory should be hidden' }) - hideAt?: Date; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) - type!: MemoryType; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - data!: MemoryData; - @ApiProperty({ description: 'Is memory saved' }) - isSaved!: boolean; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: AssetResponseDto[]; -} +const MemoryResponseSchema = z + .object({ + id: z.string().describe('Memory ID'), + createdAt: isoDatetimeToDate.describe('Creation date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + deletedAt: isoDatetimeToDate.optional().describe('Deletion date'), + memoryAt: isoDatetimeToDate.describe('Memory date'), + seenAt: isoDatetimeToDate.optional().describe('Date when memory was seen'), + showAt: isoDatetimeToDate.optional().describe('Date when memory should be shown'), + hideAt: isoDatetimeToDate.optional().describe('Date when memory should be hidden'), + ownerId: z.string().describe('Owner user ID'), + type: MemoryTypeSchema, + data: OnThisDaySchema, + isSaved: z.boolean().describe('Is memory saved'), + assets: z.array(AssetResponseSchema), + }) + .meta({ id: 'MemoryResponseDto' }); + +export class MemorySearchDto extends createZodDto(MemorySearchSchema) {} +export class MemoryUpdateDto extends createZodDto(MemoryUpdateSchema) {} +export class MemoryCreateDto extends createZodDto(MemoryCreateSchema) {} +export class MemoryStatisticsResponseDto extends createZodDto(MemoryStatisticsResponseSchema) {} +export class MemoryResponseDto extends createZodDto(MemoryResponseSchema) {} export const mapMemory = (entity: Memory, auth: AuthDto): MemoryResponseDto => { return { diff --git a/server/src/dtos/model-config.dto.ts b/server/src/dtos/model-config.dto.ts index a75808f95a59..2ba6f0c365ab 100644 --- a/server/src/dtos/model-config.dto.ts +++ b/server/src/dtos/model-config.dto.ts @@ -1,83 +1,57 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator'; -import { ValidateBoolean } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class TaskConfig { - @ValidateBoolean({ description: 'Whether the task is enabled' }) - enabled!: boolean; -} - -export class ModelConfig extends TaskConfig { - @ApiProperty({ description: 'Name of the model to use' }) - @IsString() - @IsNotEmpty() - modelName!: string; -} - -export class CLIPConfig extends ModelConfig {} - -export class DuplicateDetectionConfig extends TaskConfig { - @IsNumber() - @Min(0.001) - @Max(0.1) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Maximum distance threshold for duplicate detection', +const TaskConfigSchema = z + .object({ + enabled: z.boolean().describe('Whether the task is enabled'), }) - maxDistance!: number; -} + .meta({ id: 'TaskConfig' }); -export class FacialRecognitionConfig extends ModelConfig { - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for face detection' }) - minScore!: number; +const ModelConfigSchema = TaskConfigSchema.extend({ + modelName: z.string().describe('Name of the model to use'), +}); - @IsNumber() - @Min(0.1) - @Max(2) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Maximum distance threshold for face recognition', - }) - maxDistance!: number; +export const CLIPConfigSchema = ModelConfigSchema.meta({ id: 'CLIPConfig' }); - @IsNumber() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Minimum number of faces required for recognition' }) - minFaces!: number; -} +export const DuplicateDetectionConfigSchema = TaskConfigSchema.extend({ + maxDistance: z + .number() + .meta({ format: 'double' }) + .min(0.001) + .max(0.1) + .describe('Maximum distance threshold for duplicate detection'), +}).meta({ id: 'DuplicateDetectionConfig' }); -export class OcrConfig extends ModelConfig { - @IsNumber() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Maximum resolution for OCR processing' }) - maxResolution!: number; +export const FacialRecognitionConfigSchema = ModelConfigSchema.extend({ + minScore: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(1) + .describe('Minimum confidence score for face detection'), + maxDistance: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(2) + .describe('Maximum distance threshold for face recognition'), + minFaces: z.int().min(1).describe('Minimum number of faces required for recognition'), +}).meta({ id: 'FacialRecognitionConfig' }); - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ type: 'number', format: 'double', description: 'Minimum confidence score for text detection' }) - minDetectionScore!: number; +export const OcrConfigSchema = ModelConfigSchema.extend({ + maxResolution: z.int().min(1).describe('Maximum resolution for OCR processing'), + minDetectionScore: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(1) + .describe('Minimum confidence score for text detection'), + minRecognitionScore: z + .number() + .meta({ format: 'double' }) + .min(0.1) + .max(1) + .describe('Minimum confidence score for text recognition'), +}).meta({ id: 'OcrConfig' }); - @IsNumber() - @Min(0.1) - @Max(1) - @Type(() => Number) - @ApiProperty({ - type: 'number', - format: 'double', - description: 'Minimum confidence score for text recognition', - }) - minRecognitionScore!: number; -} +export class CLIPConfig extends createZodDto(CLIPConfigSchema) {} diff --git a/server/src/dtos/notification.dto.ts b/server/src/dtos/notification.dto.ts index 87a15f29e31e..f474cfc0a1ae 100644 --- a/server/src/dtos/notification.dto.ts +++ b/server/src/dtos/notification.dto.ts @@ -1,118 +1,91 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { ArrayMinSize, IsString } from 'class-validator'; -import { NotificationLevel, NotificationType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { NotificationLevel, NotificationLevelSchema, NotificationType, NotificationTypeSchema } from 'src/enum'; +import { isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; -export class TestEmailResponseDto { - @ApiProperty({ description: 'Email message ID' }) - messageId!: string; -} -export class TemplateResponseDto { - @ApiProperty({ description: 'Template name' }) - name!: string; - @ApiProperty({ description: 'Template HTML content' }) - html!: string; -} - -export class TemplateDto { - @ApiProperty({ description: 'Template name' }) - @IsString() - template!: string; -} - -export class NotificationDto { - @ApiProperty({ description: 'Notification ID' }) - id!: string; - @ValidateDate({ description: 'Creation date' }) - createdAt!: Date; - @ValidateEnum({ enum: NotificationLevel, name: 'NotificationLevel', description: 'Notification level' }) - level!: NotificationLevel; - @ValidateEnum({ enum: NotificationType, name: 'NotificationType', description: 'Notification type' }) - type!: NotificationType; - @ApiProperty({ description: 'Notification title' }) - title!: string; - @ApiPropertyOptional({ description: 'Notification description' }) - description?: string; - @ApiPropertyOptional({ description: 'Additional notification data' }) - data?: any; - @ApiPropertyOptional({ description: 'Date when notification was read', format: 'date-time' }) - readAt?: Date; -} - -export class NotificationSearchDto { - @ValidateUUID({ optional: true, description: 'Filter by notification ID' }) - id?: string; - - @ValidateEnum({ - enum: NotificationLevel, - name: 'NotificationLevel', - optional: true, - description: 'Filter by notification level', +const TestEmailResponseSchema = z + .object({ + messageId: z.string().describe('Email message ID'), }) - level?: NotificationLevel; + .meta({ id: 'TestEmailResponseDto' }); - @ValidateEnum({ - enum: NotificationType, - name: 'NotificationType', - optional: true, - description: 'Filter by notification type', +const TemplateResponseSchema = z + .object({ + name: z.string().describe('Template name'), + html: z.string().describe('Template HTML content'), }) - type?: NotificationType; + .meta({ id: 'TemplateResponseDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by unread status' }) - unread?: boolean; -} - -export class NotificationCreateDto { - @ValidateEnum({ - enum: NotificationLevel, - name: 'NotificationLevel', - optional: true, - description: 'Notification level', +const TemplateSchema = z + .object({ + template: z.string().describe('Template name'), }) - level?: NotificationLevel; + .meta({ id: 'TemplateDto' }); - @ValidateEnum({ enum: NotificationType, name: 'NotificationType', optional: true, description: 'Notification type' }) - type?: NotificationType; +const NotificationSchema = z + .object({ + id: z.string().describe('Notification ID'), + createdAt: isoDatetimeToDate.describe('Creation date'), + level: NotificationLevelSchema, + type: NotificationTypeSchema, + title: z.string().describe('Notification title'), + description: z.string().optional().describe('Notification description'), + data: z.record(z.string(), z.unknown()).optional().describe('Additional notification data'), + readAt: isoDatetimeToDate.optional().describe('Date when notification was read'), + }) + .meta({ id: 'NotificationDto' }); - @ValidateString({ description: 'Notification title' }) - title!: string; +const NotificationSearchSchema = z + .object({ + id: z.uuidv4().optional().describe('Filter by notification ID'), + level: NotificationLevelSchema.optional(), + type: NotificationTypeSchema.optional(), + unread: stringToBool.optional().describe('Filter by unread status'), + }) + .meta({ id: 'NotificationSearchDto' }); - @ValidateString({ optional: true, nullable: true, description: 'Notification description' }) - description?: string | null; +const NotificationCreateSchema = z + .object({ + level: NotificationLevelSchema.optional(), + type: NotificationTypeSchema.optional(), + title: z.string().describe('Notification title'), + description: z.string().nullish().describe('Notification description'), + data: z.record(z.string(), z.unknown()).optional().describe('Additional notification data'), + readAt: isoDatetimeToDate.nullish().describe('Date when notification was read'), + userId: z.uuidv4().describe('User ID to send notification to'), + }) + .meta({ id: 'NotificationCreateDto' }); - @ApiPropertyOptional({ description: 'Additional notification data' }) - @Optional({ nullable: true }) - data?: any; +const NotificationUpdateSchema = z + .object({ + readAt: isoDatetimeToDate.nullish().describe('Date when notification was read'), + }) + .meta({ id: 'NotificationUpdateDto' }); - @ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' }) - readAt?: Date | null; +const NotificationUpdateAllSchema = z + .object({ + ids: z.array(z.uuidv4()).min(1).describe('Notification IDs to update'), + readAt: isoDatetimeToDate.nullish().describe('Date when notifications were read'), + }) + .meta({ id: 'NotificationUpdateAllDto' }); - @ValidateUUID({ description: 'User ID to send notification to' }) - userId!: string; -} +const NotificationDeleteAllSchema = z + .object({ + ids: z.array(z.uuidv4()).min(1).describe('Notification IDs to delete'), + }) + .meta({ id: 'NotificationDeleteAllDto' }); -export class NotificationUpdateDto { - @ValidateDate({ optional: true, nullable: true, description: 'Date when notification was read' }) - readAt?: Date | null; -} +export class TestEmailResponseDto extends createZodDto(TestEmailResponseSchema) {} +export class TemplateResponseDto extends createZodDto(TemplateResponseSchema) {} +export class TemplateDto extends createZodDto(TemplateSchema) {} +export class NotificationDto extends createZodDto(NotificationSchema) {} +export class NotificationSearchDto extends createZodDto(NotificationSearchSchema) {} +export class NotificationCreateDto extends createZodDto(NotificationCreateSchema) {} +export class NotificationUpdateDto extends createZodDto(NotificationUpdateSchema) {} +export class NotificationUpdateAllDto extends createZodDto(NotificationUpdateAllSchema) {} +export class NotificationDeleteAllDto extends createZodDto(NotificationDeleteAllSchema) {} -export class NotificationUpdateAllDto { - @ValidateUUID({ each: true, description: 'Notification IDs to update' }) - @ArrayMinSize(1) - ids!: string[]; - - @ValidateDate({ optional: true, nullable: true, description: 'Date when notifications were read' }) - readAt?: Date | null; -} - -export class NotificationDeleteAllDto { - @ValidateUUID({ each: true, description: 'Notification IDs to delete' }) - @ArrayMinSize(1) - ids!: string[]; -} - -export type MapNotification = { +type MapNotification = { id: string; createdAt: Date; updateId?: string; @@ -123,6 +96,7 @@ export type MapNotification = { description: string | null; readAt: Date | null; }; + export const mapNotification = (notification: MapNotification): NotificationDto => { return { id: notification.id, diff --git a/server/src/dtos/ocr.dto.ts b/server/src/dtos/ocr.dto.ts index 1e838d0ec0d3..62e32ed4afee 100644 --- a/server/src/dtos/ocr.dto.ts +++ b/server/src/dtos/ocr.dto.ts @@ -1,42 +1,22 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { createZodDto } from 'nestjs-zod'; +import z from 'zod'; -export class AssetOcrResponseDto { - @ApiProperty({ type: 'string', format: 'uuid' }) - id!: string; +const AssetOcrResponseSchema = z + .object({ + assetId: z.uuidv4(), + boxScore: z.number().meta({ format: 'double' }).describe('Confidence score for text detection box'), + id: z.uuidv4(), + text: z.string().describe('Recognized text'), + textScore: z.number().meta({ format: 'double' }).describe('Confidence score for text recognition'), + x1: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 1 (0-1)'), + x2: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 2 (0-1)'), + x3: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 3 (0-1)'), + x4: z.number().meta({ format: 'double' }).describe('Normalized x coordinate of box corner 4 (0-1)'), + y1: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 1 (0-1)'), + y2: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 2 (0-1)'), + y3: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 3 (0-1)'), + y4: z.number().meta({ format: 'double' }).describe('Normalized y coordinate of box corner 4 (0-1)'), + }) + .meta({ id: 'AssetOcrResponseDto' }); - @ApiProperty({ type: 'string', format: 'uuid' }) - assetId!: string; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 1 (0-1)' }) - x1!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 1 (0-1)' }) - y1!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 2 (0-1)' }) - x2!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 2 (0-1)' }) - y2!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 3 (0-1)' }) - x3!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 3 (0-1)' }) - y3!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized x coordinate of box corner 4 (0-1)' }) - x4!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Normalized y coordinate of box corner 4 (0-1)' }) - y4!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Confidence score for text detection box' }) - boxScore!: number; - - @ApiProperty({ type: 'number', format: 'double', description: 'Confidence score for text recognition' }) - textScore!: number; - - @ApiProperty({ type: 'string', description: 'Recognized text' }) - text!: string; -} +export class AssetOcrResponseDto extends createZodDto(AssetOcrResponseSchema) {} diff --git a/server/src/dtos/onboarding.dto.ts b/server/src/dtos/onboarding.dto.ts index d2781c6b90a2..ae26f5e88ab2 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 5b949326a4a5..049cf2b25ef9 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 477166d3d524..1f8f080905ed 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -1,230 +1,184 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsArray, IsInt, IsNotEmpty, IsNumber, IsString, Max, Min, ValidateNested } from 'class-validator'; import { Selectable } from 'kysely'; -import { DateTime } from 'luxon'; +import { createZodDto } from 'nestjs-zod'; import { AssetFace, Person } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; +import { HistoryBuilder } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetEditActionItem } from 'src/dtos/editing.dto'; -import { SourceType } from 'src/enum'; +import { SourceTypeSchema } from 'src/enum'; import { AssetFaceTable } from 'src/schema/tables/asset-face.table'; import { ImageDimensions, MaybeDehydrated } from 'src/types'; import { asBirthDateString, asDateString } from 'src/utils/date'; import { transformFaceBoundingBox } from 'src/utils/transform'; -import { - IsDateStringFormat, - MaxDateString, - Optional, - ValidateBoolean, - ValidateEnum, - ValidateHexColor, - ValidateUUID, -} from 'src/validation'; +import { emptyStringToNull, hexColor, stringToBool } from 'src/validation'; +import z from 'zod'; -export class PersonCreateDto { - @ApiPropertyOptional({ description: 'Person name' }) - @Optional() - @IsString() - name?: string; - - // Note: the mobile app cannot currently set the birth date to null. - @ApiProperty({ format: 'date', description: 'Person date of birth', required: false }) - @MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' }) - @IsDateStringFormat('yyyy-MM-dd') - @Optional({ nullable: true, emptyToNull: true }) - birthDate?: string | null; - - @ValidateBoolean({ optional: true, description: 'Person visibility (hidden)' }) - isHidden?: boolean; - - @ValidateBoolean({ optional: true, description: 'Mark as favorite' }) - isFavorite?: boolean; - - @ApiPropertyOptional({ description: 'Person color (hex)' }) - @Optional({ emptyToNull: true, nullable: true }) - @ValidateHexColor() - color?: string | null; -} - -export class PersonUpdateDto extends PersonCreateDto { - @ValidateUUID({ optional: true, description: 'Asset ID used for feature face thumbnail' }) - featureFaceAssetId?: string; -} - -export class PeopleUpdateDto { - @ApiProperty({ description: 'People to update' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => PeopleUpdateItem) - people!: PeopleUpdateItem[]; -} - -export class PeopleUpdateItem extends PersonUpdateDto { - @ApiProperty({ description: 'Person ID' }) - @IsString() - @IsNotEmpty() - id!: string; -} - -export class MergePersonDto { - @ValidateUUID({ each: true, description: 'Person IDs to merge' }) - ids!: string[]; -} - -export class PersonSearchDto { - @ValidateBoolean({ optional: true, description: 'Include hidden people' }) - withHidden?: boolean; - @ValidateUUID({ optional: true, description: 'Closest person ID for similarity search' }) - closestPersonId?: string; - @ValidateUUID({ optional: true, description: 'Closest asset ID for similarity search' }) - closestAssetId?: string; - - @ApiPropertyOptional({ description: 'Page number for pagination', default: 1 }) - @IsInt() - @Min(1) - @Type(() => Number) - page: number = 1; - - @ApiPropertyOptional({ description: 'Number of items per page', default: 500 }) - @IsInt() - @Min(1) - @Max(1000) - @Type(() => Number) - size: number = 500; -} - -export class PersonResponseDto { - @ApiProperty({ description: 'Person ID' }) - id!: string; - @ApiProperty({ description: 'Person name' }) - name!: string; - @ApiProperty({ format: 'date', description: 'Person date of birth' }) - birthDate!: string | null; - @ApiProperty({ description: 'Thumbnail path' }) - thumbnailPath!: string; - @ApiProperty({ description: 'Is hidden' }) - isHidden!: boolean; - @Property({ - description: 'Last update date', - format: 'date-time', - history: new HistoryBuilder().added('v1.107.0').stable('v2'), +const PersonCreateSchema = z + .object({ + name: z.string().optional().describe('Person name'), + // Note: the mobile app cannot currently set the birth date to null. + birthDate: emptyStringToNull(z.string().meta({ format: 'date' }).nullable()) + .optional() + .refine((val) => (val ? new Date(val) <= new Date() : true), { error: 'Birth date cannot be in the future' }) + .describe('Person date of birth'), + isHidden: z.boolean().optional().describe('Person visibility (hidden)'), + isFavorite: z.boolean().optional().describe('Mark as favorite'), + color: emptyStringToNull(hexColor.nullable()).optional().describe('Person color (hex)'), }) - updatedAt?: string; - @Property({ description: 'Is favorite', history: new HistoryBuilder().added('v1.126.0').stable('v2') }) - isFavorite?: boolean; - @Property({ description: 'Person color (hex)', history: new HistoryBuilder().added('v1.126.0').stable('v2') }) - color?: string; -} + .meta({ id: 'PersonCreateDto' }); -export class PersonWithFacesResponseDto extends PersonResponseDto { - @ApiProperty({ description: 'Face detections' }) - faces!: AssetFaceWithoutPersonResponseDto[]; -} +const PersonUpdateSchema = PersonCreateSchema.extend({ + featureFaceAssetId: z.uuidv4().optional().describe('Asset ID used for feature face thumbnail'), +}).meta({ id: 'PersonUpdateDto' }); -export class AssetFaceWithoutPersonResponseDto { - @ValidateUUID({ description: 'Face ID' }) - id!: string; - @ApiProperty({ type: 'integer', description: 'Image height in pixels' }) - imageHeight!: number; - @ApiProperty({ type: 'integer', description: 'Image width in pixels' }) - imageWidth!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box X1 coordinate' }) - boundingBoxX1!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box X2 coordinate' }) - boundingBoxX2!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box Y1 coordinate' }) - boundingBoxY1!: number; - @ApiProperty({ type: 'integer', description: 'Bounding box Y2 coordinate' }) - boundingBoxY2!: number; - @ValidateEnum({ enum: SourceType, name: 'SourceType', optional: true, description: 'Face detection source type' }) - sourceType?: SourceType; -} +const PeopleUpdateItemSchema = PersonUpdateSchema.extend({ + id: z.string().describe('Person ID'), +}).meta({ id: 'PeopleUpdateItem' }); -export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto { - @ApiProperty({ description: 'Person associated with face' }) - person!: PersonResponseDto | null; -} - -export class AssetFaceUpdateDto { - @ApiProperty({ description: 'Face update items' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => AssetFaceUpdateItem) - data!: AssetFaceUpdateItem[]; -} - -export class FaceDto { - @ValidateUUID({ description: 'Face ID' }) - id!: string; -} - -export class AssetFaceUpdateItem { - @ValidateUUID({ description: 'Person ID' }) - personId!: string; - - @ValidateUUID({ description: 'Asset ID' }) - assetId!: string; -} - -export class AssetFaceCreateDto extends AssetFaceUpdateItem { - @ApiProperty({ type: 'integer', description: 'Image width in pixels' }) - @IsNotEmpty() - @IsNumber() - imageWidth!: number; - - @ApiProperty({ type: 'integer', description: 'Image height in pixels' }) - @IsNotEmpty() - @IsNumber() - imageHeight!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box X coordinate' }) - @IsNotEmpty() - @IsNumber() - x!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box Y coordinate' }) - @IsNotEmpty() - @IsNumber() - y!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box width' }) - @IsNotEmpty() - @IsNumber() - width!: number; - - @ApiProperty({ type: 'integer', description: 'Face bounding box height' }) - @IsNotEmpty() - @IsNumber() - height!: number; -} - -export class AssetFaceDeleteDto { - @ApiProperty({ description: 'Force delete even if person has other faces' }) - @IsNotEmpty() - force!: boolean; -} - -export class PersonStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of assets' }) - assets!: number; -} - -export class PeopleResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of people' }) - total!: number; - @ApiProperty({ type: 'integer', description: 'Number of hidden people' }) - hidden!: number; - @ApiProperty({ description: 'List of people' }) - people!: PersonResponseDto[]; - - // TODO: make required after a few versions - @Property({ - description: 'Whether there are more pages', - history: new HistoryBuilder().added('v1.110.0').stable('v2'), +const PeopleUpdateSchema = z + .object({ + people: z.array(PeopleUpdateItemSchema).describe('People to update'), }) - hasNextPage?: boolean; -} + .meta({ id: 'PeopleUpdateDto' }); + +const MergePersonSchema = z + .object({ + ids: z.array(z.uuidv4()).describe('Person IDs to merge'), + }) + .meta({ id: 'MergePersonDto' }); + +const PersonSearchSchema = z + .object({ + withHidden: stringToBool.optional().describe('Include hidden people'), + closestPersonId: z.uuidv4().optional().describe('Closest person ID for similarity search'), + closestAssetId: z.uuidv4().optional().describe('Closest asset ID for similarity search'), + page: z.coerce.number().min(1).default(1).describe('Page number for pagination'), + size: z.coerce.number().min(1).max(1000).default(500).describe('Number of items per page'), + }) + .meta({ id: 'PersonSearchDto' }); + +const PersonResponseSchema = z + .object({ + id: z.string().describe('Person ID'), + name: z.string().describe('Person name'), + // TODO: use `isoDateToDate` when using `ZodSerializerDto` on the controllers. + birthDate: z.string().meta({ format: 'date' }).describe('Person date of birth').nullable(), + thumbnailPath: z.string().describe('Thumbnail path'), + isHidden: z.boolean().describe('Is hidden'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + updatedAt: z + .string() + .meta({ format: 'date-time' }) + .optional() + .describe('Last update date') + .meta(new HistoryBuilder().added('v1.107.0').stable('v2').getExtensions()), + isFavorite: z + .boolean() + .optional() + .describe('Is favorite') + .meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()), + color: z + .string() + .optional() + .describe('Person color (hex)') + .meta(new HistoryBuilder().added('v1.126.0').stable('v2').getExtensions()), + }) + .meta({ id: 'PersonResponseDto' }); + +export class PersonCreateDto extends createZodDto(PersonCreateSchema) {} +export class PersonUpdateDto extends createZodDto(PersonUpdateSchema) {} +export class PeopleUpdateDto extends createZodDto(PeopleUpdateSchema) {} +export class MergePersonDto extends createZodDto(MergePersonSchema) {} +export class PersonSearchDto extends createZodDto(PersonSearchSchema) {} +export class PersonResponseDto extends createZodDto(PersonResponseSchema) {} + +export const AssetFaceWithoutPersonResponseSchema = z + .object({ + id: z.uuidv4().describe('Face ID'), + imageHeight: z.int().min(0).describe('Image height in pixels'), + imageWidth: z.int().min(0).describe('Image width in pixels'), + boundingBoxX1: z.int().describe('Bounding box X1 coordinate'), + boundingBoxX2: z.int().describe('Bounding box X2 coordinate'), + boundingBoxY1: z.int().describe('Bounding box Y1 coordinate'), + boundingBoxY2: z.int().describe('Bounding box Y2 coordinate'), + sourceType: SourceTypeSchema.optional(), + }) + .describe('Asset face without person') + .meta({ id: 'AssetFaceWithoutPersonResponseDto' }); + +class AssetFaceWithoutPersonResponseDto extends createZodDto(AssetFaceWithoutPersonResponseSchema) {} + +export const PersonWithFacesResponseSchema = PersonResponseSchema.extend({ + faces: z.array(AssetFaceWithoutPersonResponseSchema), +}).meta({ id: 'PersonWithFacesResponseDto' }); + +export class PersonWithFacesResponseDto extends createZodDto(PersonWithFacesResponseSchema) {} + +const AssetFaceResponseSchema = AssetFaceWithoutPersonResponseSchema.extend({ + person: PersonResponseSchema.nullable(), +}).meta({ id: 'AssetFaceResponseDto' }); + +export class AssetFaceResponseDto extends createZodDto(AssetFaceResponseSchema) {} + +const AssetFaceUpdateItemSchema = z + .object({ + personId: z.uuidv4().describe('Person ID'), + assetId: z.uuidv4().describe('Asset ID'), + }) + .meta({ id: 'AssetFaceUpdateItem' }); + +const AssetFaceUpdateSchema = z + .object({ + data: z.array(AssetFaceUpdateItemSchema).describe('Face update items'), + }) + .meta({ id: 'AssetFaceUpdateDto' }); + +const FaceSchema = z + .object({ + id: z.uuidv4().describe('Face ID'), + }) + .meta({ id: 'FaceDto' }); + +const AssetFaceCreateSchema = AssetFaceUpdateItemSchema.extend({ + imageWidth: z.int().describe('Image width in pixels'), + imageHeight: z.int().describe('Image height in pixels'), + x: z.int().describe('Face bounding box X coordinate'), + y: z.int().describe('Face bounding box Y coordinate'), + width: z.int().describe('Face bounding box width'), + height: z.int().describe('Face bounding box height'), +}).meta({ id: 'AssetFaceCreateDto' }); + +const AssetFaceDeleteSchema = z + .object({ + force: z.boolean().describe('Force delete even if person has other faces'), + }) + .meta({ id: 'AssetFaceDeleteDto' }); + +const PersonStatisticsResponseSchema = z + .object({ + assets: z.int().describe('Number of assets'), + }) + .meta({ id: 'PersonStatisticsResponseDto' }); + +export class AssetFaceUpdateDto extends createZodDto(AssetFaceUpdateSchema) {} +export class FaceDto extends createZodDto(FaceSchema) {} +export class AssetFaceCreateDto extends createZodDto(AssetFaceCreateSchema) {} +export class AssetFaceDeleteDto extends createZodDto(AssetFaceDeleteSchema) {} +export class PersonStatisticsResponseDto extends createZodDto(PersonStatisticsResponseSchema) {} + +const PeopleResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of people'), + hidden: z.int().min(0).describe('Number of hidden people'), + people: z.array(PersonResponseSchema), + // TODO: make required after a few versions + hasNextPage: z + .boolean() + .optional() + .describe('Whether there are more pages') + .meta(new HistoryBuilder().added('v1.110.0').stable('v2').getExtensions()), + }) + .describe('People response'); +export class PeopleResponseDto extends createZodDto(PeopleResponseSchema) {} export function mapPerson(person: MaybeDehydrated): PersonResponseDto { return { diff --git a/server/src/dtos/plugin-manifest.dto.ts b/server/src/dtos/plugin-manifest.dto.ts index d5d1c529970e..30aa8c0a68f4 100644 --- a/server/src/dtos/plugin-manifest.dto.ts +++ b/server/src/dtos/plugin-manifest.dto.ts @@ -1,128 +1,56 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { - ArrayMinSize, - IsArray, - IsEnum, - IsNotEmpty, - IsObject, - IsOptional, - IsSemVer, - IsString, - Matches, - ValidateNested, -} from 'class-validator'; -import { PluginContext } from 'src/enum'; -import { JSONSchema } from 'src/types/plugin-schema.types'; -import { ValidateEnum } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { PluginContextSchema } from 'src/enum'; +import { JSONSchemaSchema } from 'src/types/plugin-schema.types'; +import z from 'zod'; -class PluginManifestWasmDto { - @ApiProperty({ description: 'WASM file path' }) - @IsString() - @IsNotEmpty() - path!: string; -} +const pluginNameRegex = /^[a-z0-9-]+[a-z0-9]$/; +const semverRegex = + /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/; -class PluginManifestFilterDto { - @ApiProperty({ description: 'Filter method name' }) - @IsString() - @IsNotEmpty() - methodName!: string; - - @ApiProperty({ description: 'Filter title' }) - @IsString() - @IsNotEmpty() - title!: string; - - @ApiProperty({ description: 'Filter description' }) - @IsString() - @IsNotEmpty() - description!: string; - - @ApiProperty({ description: 'Supported contexts', enum: PluginContext, isArray: true }) - @IsArray() - @ArrayMinSize(1) - @IsEnum(PluginContext, { each: true }) - supportedContexts!: PluginContext[]; - - @ApiPropertyOptional({ description: 'Filter schema' }) - @IsObject() - @IsOptional() - schema?: JSONSchema; -} - -class PluginManifestActionDto { - @ApiProperty({ description: 'Action method name' }) - @IsString() - @IsNotEmpty() - methodName!: string; - - @ApiProperty({ description: 'Action title' }) - @IsString() - @IsNotEmpty() - title!: string; - - @ApiProperty({ description: 'Action description' }) - @IsString() - @IsNotEmpty() - description!: string; - - @ArrayMinSize(1) - @ValidateEnum({ enum: PluginContext, name: 'PluginContext', each: true, description: 'Supported contexts' }) - supportedContexts!: PluginContext[]; - - @ApiPropertyOptional({ description: 'Action schema' }) - @IsObject() - @IsOptional() - schema?: JSONSchema; -} - -export class PluginManifestDto { - @ApiProperty({ description: 'Plugin name (lowercase, numbers, hyphens only)' }) - @IsString() - @IsNotEmpty() - @Matches(/^[a-z0-9-]+[a-z0-9]$/, { - message: 'Plugin name must contain only lowercase letters, numbers, and hyphens, and cannot end with a hyphen', +const PluginManifestWasmSchema = z + .object({ + path: z.string().describe('WASM file path'), }) - name!: string; + .meta({ id: 'PluginManifestWasmDto' }); - @ApiProperty({ description: 'Plugin version (semver)' }) - @IsString() - @IsNotEmpty() - @IsSemVer() - version!: string; +const PluginManifestFilterSchema = z + .object({ + methodName: z.string().describe('Filter method name'), + title: z.string().describe('Filter title'), + description: z.string().describe('Filter description'), + supportedContexts: z.array(PluginContextSchema).min(1).describe('Supported contexts'), + schema: JSONSchemaSchema.optional(), + }) + .meta({ id: 'PluginManifestFilterDto' }); - @ApiProperty({ description: 'Plugin title' }) - @IsString() - @IsNotEmpty() - title!: string; +const PluginManifestActionSchema = z + .object({ + methodName: z.string().describe('Action method name'), + title: z.string().describe('Action title'), + description: z.string().describe('Action description'), + supportedContexts: z.array(PluginContextSchema).min(1).describe('Supported contexts'), + schema: JSONSchemaSchema.optional(), + }) + .meta({ id: 'PluginManifestActionDto' }); - @ApiProperty({ description: 'Plugin description' }) - @IsString() - @IsNotEmpty() - description!: string; +export const PluginManifestSchema = z + .object({ + name: z + .string() + .min(1) + .regex( + pluginNameRegex, + 'Plugin name must contain only lowercase letters, numbers, and hyphens, and cannot end with a hyphen', + ) + .describe('Plugin name (lowercase, numbers, hyphens only)'), + version: z.string().regex(semverRegex).describe('Plugin version (semver)'), + title: z.string().describe('Plugin title'), + description: z.string().describe('Plugin description'), + author: z.string().describe('Plugin author'), + wasm: PluginManifestWasmSchema, + filters: z.array(PluginManifestFilterSchema).optional().describe('Plugin filters'), + actions: z.array(PluginManifestActionSchema).optional().describe('Plugin actions'), + }) + .meta({ id: 'PluginManifestDto' }); - @ApiProperty({ description: 'Plugin author' }) - @IsString() - @IsNotEmpty() - author!: string; - - @ApiProperty({ description: 'WASM configuration' }) - @ValidateNested() - @Type(() => PluginManifestWasmDto) - wasm!: PluginManifestWasmDto; - - @ApiPropertyOptional({ description: 'Plugin filters' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => PluginManifestFilterDto) - @IsOptional() - filters?: PluginManifestFilterDto[]; - - @ApiPropertyOptional({ description: 'Plugin actions' }) - @IsArray() - @ValidateNested({ each: true }) - @Type(() => PluginManifestActionDto) - @IsOptional() - actions?: PluginManifestActionDto[]; -} +export class PluginManifestDto extends createZodDto(PluginManifestSchema) {} diff --git a/server/src/dtos/plugin.dto.ts b/server/src/dtos/plugin.dto.ts index de1f1b28d43a..2f928841cb41 100644 --- a/server/src/dtos/plugin.dto.ts +++ b/server/src/dtos/plugin.dto.ts @@ -1,84 +1,59 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { PluginAction, PluginFilter } from 'src/database'; -import { PluginContext as PluginContextType, PluginTriggerType } from 'src/enum'; -import type { JSONSchema } from 'src/types/plugin-schema.types'; -import { ValidateEnum } from 'src/validation'; +import { PluginContextSchema, PluginTriggerTypeSchema } from 'src/enum'; +import { JSONSchemaSchema } from 'src/types/plugin-schema.types'; +import z from 'zod'; -export class PluginTriggerResponseDto { - @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Trigger type' }) - type!: PluginTriggerType; - @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', description: 'Context type' }) - contextType!: PluginContextType; -} +const PluginTriggerResponseSchema = z + .object({ + type: PluginTriggerTypeSchema, + contextType: PluginContextSchema, + }) + .meta({ id: 'PluginTriggerResponseDto' }); -export class PluginResponseDto { - @ApiProperty({ description: 'Plugin ID' }) - id!: string; - @ApiProperty({ description: 'Plugin name' }) - name!: string; - @ApiProperty({ description: 'Plugin title' }) - title!: string; - @ApiProperty({ description: 'Plugin description' }) - description!: string; - @ApiProperty({ description: 'Plugin author' }) - author!: string; - @ApiProperty({ description: 'Plugin version' }) - version!: string; - @ApiProperty({ description: 'Creation date' }) - createdAt!: string; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: string; - @ApiProperty({ description: 'Plugin filters' }) - filters!: PluginFilterResponseDto[]; - @ApiProperty({ description: 'Plugin actions' }) - actions!: PluginActionResponseDto[]; -} +const PluginFilterResponseSchema = z + .object({ + id: z.string().describe('Filter ID'), + pluginId: z.string().describe('Plugin ID'), + methodName: z.string().describe('Method name'), + title: z.string().describe('Filter title'), + description: z.string().describe('Filter description'), + supportedContexts: z.array(PluginContextSchema).describe('Supported contexts'), + schema: JSONSchemaSchema.nullable().describe('Filter schema'), + }) + .meta({ id: 'PluginFilterResponseDto' }); -export class PluginFilterResponseDto { - @ApiProperty({ description: 'Filter ID' }) - id!: string; - @ApiProperty({ description: 'Plugin ID' }) - pluginId!: string; - @ApiProperty({ description: 'Method name' }) - methodName!: string; - @ApiProperty({ description: 'Filter title' }) - title!: string; - @ApiProperty({ description: 'Filter description' }) - description!: string; +const PluginActionResponseSchema = z + .object({ + id: z.string().describe('Action ID'), + pluginId: z.string().describe('Plugin ID'), + methodName: z.string().describe('Method name'), + title: z.string().describe('Action title'), + description: z.string().describe('Action description'), + supportedContexts: z.array(PluginContextSchema).describe('Supported contexts'), + schema: JSONSchemaSchema.nullable().describe('Action schema'), + }) + .meta({ id: 'PluginActionResponseDto' }); - @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' }) - supportedContexts!: PluginContextType[]; - @ApiProperty({ description: 'Filter schema' }) - schema!: JSONSchema | null; -} +const PluginResponseSchema = z + .object({ + id: z.string().describe('Plugin ID'), + name: z.string().describe('Plugin name'), + title: z.string().describe('Plugin title'), + description: z.string().describe('Plugin description'), + author: z.string().describe('Plugin author'), + version: z.string().describe('Plugin version'), + createdAt: z.string().describe('Creation date'), + updatedAt: z.string().describe('Last update date'), + filters: z.array(PluginFilterResponseSchema).describe('Plugin filters'), + actions: z.array(PluginActionResponseSchema).describe('Plugin actions'), + }) + .meta({ id: 'PluginResponseDto' }); -export class PluginActionResponseDto { - @ApiProperty({ description: 'Action ID' }) - id!: string; - @ApiProperty({ description: 'Plugin ID' }) - pluginId!: string; - @ApiProperty({ description: 'Method name' }) - methodName!: string; - @ApiProperty({ description: 'Action title' }) - title!: string; - @ApiProperty({ description: 'Action description' }) - description!: string; +export class PluginTriggerResponseDto extends createZodDto(PluginTriggerResponseSchema) {} +export class PluginResponseDto extends createZodDto(PluginResponseSchema) {} - @ValidateEnum({ enum: PluginContextType, name: 'PluginContextType', each: true, description: 'Supported contexts' }) - supportedContexts!: PluginContextType[]; - @ApiProperty({ description: 'Action schema' }) - schema!: JSONSchema | null; -} - -export class PluginInstallDto { - @ApiProperty({ description: 'Path to plugin manifest file' }) - @IsString() - @IsNotEmpty() - manifestPath!: string; -} - -export type MapPlugin = { +type MapPlugin = { id: string; name: string; title: string; diff --git a/server/src/dtos/queue-legacy.dto.ts b/server/src/dtos/queue-legacy.dto.ts index 993160a03b75..dbbcec2da530 100644 --- a/server/src/dtos/queue-legacy.dto.ts +++ b/server/src/dtos/queue-legacy.dto.ts @@ -1,79 +1,47 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { QueueResponseDto, QueueStatisticsDto } from 'src/dtos/queue.dto'; +import { createZodDto } from 'nestjs-zod'; +import { QueueResponseDto, QueueStatisticsSchema } from 'src/dtos/queue.dto'; import { QueueName } from 'src/enum'; +import z from 'zod'; -export class QueueStatusLegacyDto { - @ApiProperty({ description: 'Whether the queue is currently active (has running jobs)' }) - isActive!: boolean; - @ApiProperty({ description: 'Whether the queue is paused' }) - isPaused!: boolean; -} +const QueueStatusLegacySchema = z + .object({ + isActive: z.boolean().describe('Whether the queue is currently active (has running jobs)'), + isPaused: z.boolean().describe('Whether the queue is paused'), + }) + .meta({ id: 'QueueStatusLegacyDto' }); -export class QueueResponseLegacyDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - queueStatus!: QueueStatusLegacyDto; +const QueueResponseLegacySchema = z + .object({ + queueStatus: QueueStatusLegacySchema, + jobCounts: QueueStatisticsSchema, + }) + .meta({ id: 'QueueResponseLegacyDto' }); - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - jobCounts!: QueueStatisticsDto; -} +const QueuesResponseLegacySchema = z + .object({ + [QueueName.ThumbnailGeneration]: QueueResponseLegacySchema, + [QueueName.MetadataExtraction]: QueueResponseLegacySchema, + [QueueName.VideoConversion]: QueueResponseLegacySchema, + [QueueName.SmartSearch]: QueueResponseLegacySchema, + [QueueName.StorageTemplateMigration]: QueueResponseLegacySchema, + [QueueName.Migration]: QueueResponseLegacySchema, + [QueueName.BackgroundTask]: QueueResponseLegacySchema, + [QueueName.Search]: QueueResponseLegacySchema, + [QueueName.DuplicateDetection]: QueueResponseLegacySchema, + [QueueName.FaceDetection]: QueueResponseLegacySchema, + [QueueName.FacialRecognition]: QueueResponseLegacySchema, + [QueueName.Sidecar]: QueueResponseLegacySchema, + [QueueName.Library]: QueueResponseLegacySchema, + [QueueName.Notification]: QueueResponseLegacySchema, + [QueueName.BackupDatabase]: QueueResponseLegacySchema, + [QueueName.Ocr]: QueueResponseLegacySchema, + [QueueName.Workflow]: QueueResponseLegacySchema, + [QueueName.Editor]: QueueResponseLegacySchema, + }) + .meta({ id: 'QueuesResponseLegacyDto' }); -export class QueuesResponseLegacyDto implements Record { - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.ThumbnailGeneration]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.MetadataExtraction]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.VideoConversion]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.SmartSearch]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.StorageTemplateMigration]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Migration]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.BackgroundTask]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Search]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.DuplicateDetection]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.FaceDetection]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.FacialRecognition]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Sidecar]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Library]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Notification]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.BackupDatabase]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Ocr]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Workflow]!: QueueResponseLegacyDto; - - @ApiProperty({ type: QueueResponseLegacyDto }) - [QueueName.Editor]!: QueueResponseLegacyDto; -} +export class QueueResponseLegacyDto extends createZodDto(QueueResponseLegacySchema) {} +export class QueuesResponseLegacyDto extends createZodDto(QueuesResponseLegacySchema) {} export const mapQueueLegacy = (response: QueueResponseDto): QueueResponseLegacyDto => { return { diff --git a/server/src/dtos/queue.dto.ts b/server/src/dtos/queue.dto.ts index 789358144418..2147f60bdeea 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 196e72c37e2d..43da0b870922 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -1,282 +1,157 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Place } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; -import { AlbumResponseDto } from 'src/dtos/album.dto'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AssetOrder, AssetType, AssetVisibility } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { HistoryBuilder } from 'src/decorators'; +import { AlbumResponseSchema } from 'src/dtos/album.dto'; +import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; +import { AssetOrder, AssetOrderSchema, AssetTypeSchema, AssetVisibilitySchema } from 'src/enum'; +import { emptyStringToNull, isoDatetimeToDate, stringToBool } from 'src/validation'; +import z from 'zod'; -class BaseSearchDto { - @ValidateUUID({ optional: true, nullable: true, description: 'Library ID to filter by' }) - libraryId?: string | null; +const BaseSearchSchema = z.object({ + libraryId: z.uuidv4().nullish().describe('Library ID to filter by'), + deviceId: z.string().optional().describe('Device ID to filter by'), + type: AssetTypeSchema.optional(), + isEncoded: z.boolean().optional().describe('Filter by encoded status'), + isFavorite: z.boolean().optional().describe('Filter by favorite status'), + isMotion: z.boolean().optional().describe('Filter by motion photo status'), + isOffline: z.boolean().optional().describe('Filter by offline status'), + visibility: AssetVisibilitySchema.optional(), + createdBefore: isoDatetimeToDate.optional().describe('Filter by creation date (before)'), + createdAfter: isoDatetimeToDate.optional().describe('Filter by creation date (after)'), + updatedBefore: isoDatetimeToDate.optional().describe('Filter by update date (before)'), + updatedAfter: isoDatetimeToDate.optional().describe('Filter by update date (after)'), + trashedBefore: isoDatetimeToDate.optional().describe('Filter by trash date (before)'), + trashedAfter: isoDatetimeToDate.optional().describe('Filter by trash date (after)'), + takenBefore: isoDatetimeToDate.optional().describe('Filter by taken date (before)'), + takenAfter: isoDatetimeToDate.optional().describe('Filter by taken date (after)'), + city: emptyStringToNull(z.string().nullable()).optional().describe('Filter by city name'), + state: emptyStringToNull(z.string().nullable()).optional().describe('Filter by state/province name'), + country: emptyStringToNull(z.string().nullable()).optional().describe('Filter by country name'), + make: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera make'), + model: emptyStringToNull(z.string().nullable()).optional().describe('Filter by camera model'), + lensModel: emptyStringToNull(z.string().nullable()).optional().describe('Filter by lens model'), + isNotInAlbum: z.boolean().optional().describe('Filter assets not in any album'), + personIds: z.array(z.uuidv4()).optional().describe('Filter by person IDs'), + tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs'), + albumIds: z.array(z.uuidv4()).optional().describe('Filter by album IDs'), + rating: z + .number() + .min(-1) + .max(5) + .nullish() + .describe('Filter by rating [1-5], or null for unrated') + .meta({ + ...new HistoryBuilder() + .added('v1') + .stable('v2') + .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.') + .getExtensions(), + }), + ocr: z.string().optional().describe('Filter by OCR text content'), +}); - @ApiPropertyOptional({ description: 'Device ID to filter by' }) - @IsString() - @IsNotEmpty() - @Optional() - deviceId?: string; +const BaseSearchWithResultsSchema = BaseSearchSchema.extend({ + withDeleted: z.boolean().optional().describe('Include deleted assets'), + withExif: z.boolean().optional().describe('Include EXIF data in response'), + size: z.number().min(1).max(1000).optional().describe('Number of results to return'), +}); - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', optional: true, description: 'Asset type filter' }) - type?: AssetType; +const RandomSearchSchema = BaseSearchWithResultsSchema.extend({ + withStacked: z.boolean().optional().describe('Include stacked assets'), + withPeople: z.boolean().optional().describe('Include people data in response'), +}).meta({ id: 'RandomSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by encoded status' }) - isEncoded?: boolean; +const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({ + minFileSize: z.coerce.number().int().min(0).optional().describe('Minimum file size in bytes'), + size: z.coerce.number().min(1).max(1000).optional().describe('Number of results to return'), +}).meta({ id: 'LargeAssetSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by favorite status' }) - isFavorite?: boolean; +const MetadataSearchSchema = RandomSearchSchema.extend({ + id: z.uuidv4().optional().describe('Filter by asset ID'), + deviceAssetId: z.string().optional().describe('Filter by device asset ID'), + description: z.string().trim().optional().describe('Filter by description text'), + checksum: z.string().optional().describe('Filter by file checksum'), + originalFileName: z.string().trim().optional().describe('Filter by original file name'), + originalPath: z.string().optional().describe('Filter by original file path'), + previewPath: z.string().optional().describe('Filter by preview file path'), + thumbnailPath: z.string().optional().describe('Filter by thumbnail file path'), + encodedVideoPath: z.string().optional().describe('Filter by encoded video file path'), + order: AssetOrderSchema.default(AssetOrder.Desc).optional().describe('Sort order'), + page: z.number().min(1).optional().describe('Page number'), +}).meta({ id: 'MetadataSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by motion photo status' }) - isMotion?: boolean; +const StatisticsSearchSchema = BaseSearchSchema.extend({ + description: z.string().trim().optional().describe('Filter by description text'), +}).meta({ id: 'StatisticsSearchDto' }); - @ValidateBoolean({ optional: true, description: 'Filter by offline status' }) - isOffline?: boolean; +const SmartSearchSchema = BaseSearchWithResultsSchema.extend({ + query: z.string().trim().optional().describe('Natural language search query'), + queryAssetId: z.uuidv4().optional().describe('Asset ID to use as search reference'), + language: z.string().optional().describe('Search language code'), + page: z.number().min(1).optional().describe('Page number'), +}).meta({ id: 'SmartSearchDto' }); - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', optional: true, description: 'Filter by visibility' }) - visibility?: AssetVisibility; - - @ValidateDate({ optional: true, description: 'Filter by creation date (before)' }) - createdBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by creation date (after)' }) - createdAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter by update date (before)' }) - updatedBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by update date (after)' }) - updatedAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter by trash date (before)' }) - trashedBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by trash date (after)' }) - trashedAfter?: Date; - - @ValidateDate({ optional: true, description: 'Filter by taken date (before)' }) - takenBefore?: Date; - - @ValidateDate({ optional: true, description: 'Filter by taken date (after)' }) - takenAfter?: Date; - - @ApiPropertyOptional({ description: 'Filter by city name' }) - @IsString() - @Optional({ nullable: true, emptyToNull: true }) - city?: string | null; - - @ApiPropertyOptional({ description: 'Filter by state/province name' }) - @IsString() - @Optional({ nullable: true, emptyToNull: true }) - state?: string | null; - - @ApiPropertyOptional({ description: 'Filter by country name' }) - @IsString() - @IsNotEmpty() - @Optional({ nullable: true, emptyToNull: true }) - country?: string | null; - - @ApiPropertyOptional({ description: 'Filter by camera make' }) - @IsString() - @Optional({ nullable: true, emptyToNull: true }) - make?: string; - - @ApiPropertyOptional({ description: 'Filter by camera model' }) - @IsString() - @Optional({ nullable: true, emptyToNull: true }) - model?: string | null; - - @ApiPropertyOptional({ description: 'Filter by lens model' }) - @IsString() - @Optional({ nullable: true, emptyToNull: true }) - lensModel?: string | null; - - @ValidateBoolean({ optional: true, description: 'Filter assets not in any album' }) - isNotInAlbum?: boolean; - - @ValidateUUID({ each: true, optional: true, description: 'Filter by person IDs' }) - personIds?: string[]; - - @ValidateUUID({ each: true, optional: true, nullable: true, description: 'Filter by tag IDs' }) - tagIds?: string[] | null; - - @ValidateUUID({ each: true, optional: true, description: 'Filter by album IDs' }) - albumIds?: string[]; - - @Property({ - type: 'number', - description: 'Filter by rating [1-5], or null for unrated', - minimum: -1, - maximum: 5, - history: new HistoryBuilder() - .added('v1') - .stable('v2') - .updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.'), +const SearchPlacesSchema = z + .object({ + name: z.string().describe('Place name to search for'), }) - @Optional({ nullable: true }) - @IsInt() - @Max(5) - @Min(-1) - rating?: number | null; + .meta({ id: 'SearchPlacesDto' }); - @ApiPropertyOptional({ description: 'Filter by OCR text content' }) - @IsString() - @IsNotEmpty() - @Optional() - ocr?: string; -} - -class BaseSearchWithResultsDto extends BaseSearchDto { - @ValidateBoolean({ optional: true, description: 'Include deleted assets' }) - withDeleted?: boolean; - - @ValidateBoolean({ optional: true, description: 'Include EXIF data in response' }) - withExif?: boolean; - - @ApiPropertyOptional({ type: 'number', description: 'Number of results to return', minimum: 1, maximum: 1000 }) - @IsInt() - @Min(1) - @Max(1000) - @Type(() => Number) - @Optional() - size?: number; -} - -export class RandomSearchDto extends BaseSearchWithResultsDto { - @ValidateBoolean({ optional: true, description: 'Include stacked assets' }) - withStacked?: boolean; - - @ValidateBoolean({ optional: true, description: 'Include people data in response' }) - withPeople?: boolean; -} - -export class LargeAssetSearchDto extends BaseSearchWithResultsDto { - @ApiPropertyOptional({ type: 'integer', description: 'Minimum file size in bytes', minimum: 0 }) - @Optional() - @IsInt() - @Min(0) - @Type(() => Number) - minFileSize?: number; -} - -export class MetadataSearchDto extends RandomSearchDto { - @ValidateUUID({ optional: true, description: 'Filter by asset ID' }) - id?: string; - - @ApiPropertyOptional({ description: 'Filter by device asset ID' }) - @IsString() - @IsNotEmpty() - @Optional() - deviceAssetId?: string; - - @ValidateString({ optional: true, trim: true, description: 'Filter by description text' }) - description?: string; - - @ApiPropertyOptional({ description: 'Filter by file checksum' }) - @IsString() - @IsNotEmpty() - @Optional() - checksum?: string; - - @ValidateString({ optional: true, trim: true, description: 'Filter by original file name' }) - originalFileName?: string; - - @ApiPropertyOptional({ description: 'Filter by original file path' }) - @IsString() - @IsNotEmpty() - @Optional() - originalPath?: string; - - @ApiPropertyOptional({ description: 'Filter by preview file path' }) - @IsString() - @IsNotEmpty() - @Optional() - previewPath?: string; - - @ApiPropertyOptional({ description: 'Filter by thumbnail file path' }) - @IsString() - @IsNotEmpty() - @Optional() - thumbnailPath?: string; - - @ApiPropertyOptional({ description: 'Filter by encoded video file path' }) - @IsString() - @IsNotEmpty() - @Optional() - encodedVideoPath?: string; - - @ValidateEnum({ - enum: AssetOrder, - name: 'AssetOrder', - optional: true, - default: AssetOrder.Desc, - description: 'Sort order', +const SearchPeopleSchema = z + .object({ + name: z.string().describe('Person name to search for'), + withHidden: stringToBool.optional().describe('Include hidden people'), }) - order?: AssetOrder; + .meta({ id: 'SearchPeopleDto' }); - @ApiPropertyOptional({ type: 'number', description: 'Page number', minimum: 1 }) - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - page?: number; +const PlacesResponseSchema = z + .object({ + name: z.string().describe('Place name'), + latitude: z.number().describe('Latitude coordinate'), + longitude: z.number().describe('Longitude coordinate'), + admin1name: z.string().optional().describe('Administrative level 1 name (state/province)'), + admin2name: z.string().optional().describe('Administrative level 2 name (county/district)'), + }) + .meta({ id: 'PlacesResponseDto' }); + +export enum SearchSuggestionType { + COUNTRY = 'country', + STATE = 'state', + CITY = 'city', + CAMERA_MAKE = 'camera-make', + CAMERA_MODEL = 'camera-model', + CAMERA_LENS_MODEL = 'camera-lens-model', } -export class StatisticsSearchDto extends BaseSearchDto { - @ValidateString({ optional: true, trim: true, description: 'Filter by description text' }) - description?: string; -} +const SearchSuggestionTypeSchema = z + .enum(SearchSuggestionType) + .describe('Suggestion type') + .meta({ id: 'SearchSuggestionType' }); -export class SmartSearchDto extends BaseSearchWithResultsDto { - @ValidateString({ optional: true, trim: true, description: 'Natural language search query' }) - query?: string; +const SearchSuggestionRequestSchema = z + .object({ + type: SearchSuggestionTypeSchema, + country: z.string().optional().describe('Filter by country'), + state: z.string().optional().describe('Filter by state/province'), + make: z.string().optional().describe('Filter by camera make'), + model: z.string().optional().describe('Filter by camera model'), + lensModel: z.string().optional().describe('Filter by lens model'), + includeNull: stringToBool + .optional() + .describe('Include null values in suggestions') + .meta(new HistoryBuilder().added('v1.111.0').stable('v2').getExtensions()), + }) + .meta({ id: 'SearchSuggestionRequestDto' }); - @ValidateUUID({ optional: true, description: 'Asset ID to use as search reference' }) - queryAssetId?: string; - - @ApiPropertyOptional({ description: 'Search language code' }) - @IsString() - @IsNotEmpty() - @Optional() - language?: string; - - @ApiPropertyOptional({ type: 'number', description: 'Page number', minimum: 1 }) - @IsInt() - @Min(1) - @Type(() => Number) - @Optional() - page?: number; -} - -export class SearchPlacesDto { - @ApiProperty({ description: 'Place name to search for' }) - @IsString() - @IsNotEmpty() - name!: string; -} - -export class SearchPeopleDto { - @ApiProperty({ description: 'Person name to search for' }) - @IsString() - @IsNotEmpty() - name!: string; - - @ValidateBoolean({ optional: true, description: 'Include hidden people' }) - withHidden?: boolean; -} - -export class PlacesResponseDto { - @ApiProperty({ description: 'Place name' }) - name!: string; - @ApiProperty({ type: 'number', description: 'Latitude coordinate' }) - latitude!: number; - @ApiProperty({ type: 'number', description: 'Longitude coordinate' }) - longitude!: number; - @ApiPropertyOptional({ description: 'Administrative level 1 name (state/province)' }) - admin1name?: string; - @ApiPropertyOptional({ description: 'Administrative level 2 name (county/district)' }) - admin2name?: string; -} +export class RandomSearchDto extends createZodDto(RandomSearchSchema) {} +export class LargeAssetSearchDto extends createZodDto(LargeAssetSearchSchema) {} +export class MetadataSearchDto extends createZodDto(MetadataSearchSchema) {} +export class StatisticsSearchDto extends createZodDto(StatisticsSearchSchema) {} +export class SmartSearchDto extends createZodDto(SmartSearchSchema) {} +export class SearchPlacesDto extends createZodDto(SearchPlacesSchema) {} +export class SearchPeopleDto extends createZodDto(SearchPeopleSchema) {} +export class PlacesResponseDto extends createZodDto(PlacesResponseSchema) {} +export class SearchSuggestionRequestDto extends createZodDto(SearchSuggestionRequestSchema) {} export function mapPlaces(place: Place): PlacesResponseDto { return { @@ -288,136 +163,68 @@ export function mapPlaces(place: Place): PlacesResponseDto { }; } -export enum SearchSuggestionType { - COUNTRY = 'country', - STATE = 'state', - CITY = 'city', - CAMERA_MAKE = 'camera-make', - CAMERA_MODEL = 'camera-model', - CAMERA_LENS_MODEL = 'camera-lens-model', -} - -export class SearchSuggestionRequestDto { - @ValidateEnum({ enum: SearchSuggestionType, name: 'SearchSuggestionType', description: 'Suggestion type' }) - type!: SearchSuggestionType; - - @ApiPropertyOptional({ description: 'Filter by country' }) - @IsString() - @Optional() - country?: string; - - @ApiPropertyOptional({ description: 'Filter by state/province' }) - @IsString() - @Optional() - state?: string; - - @ApiPropertyOptional({ description: 'Filter by camera make' }) - @IsString() - @Optional() - make?: string; - - @ApiPropertyOptional({ description: 'Filter by camera model' }) - @IsString() - @Optional() - model?: string; - - @ApiPropertyOptional({ description: 'Filter by lens model' }) - @IsString() - @Optional() - lensModel?: string; - - @ValidateBoolean({ - optional: true, - description: 'Include null values in suggestions', - history: new HistoryBuilder().added('v1.111.0').stable('v2'), +const SearchFacetCountResponseSchema = z + .object({ + count: z.int().min(0).describe('Number of assets with this facet value'), + value: z.string().describe('Facet value'), }) - includeNull?: boolean; -} + .meta({ id: 'SearchFacetCountResponseDto' }); -class SearchFacetCountResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of assets with this facet value' }) - count!: number; - @ApiProperty({ description: 'Facet value' }) - value!: string; -} +const SearchFacetResponseSchema = z + .object({ + fieldName: z.string().describe('Facet field name'), + counts: z.array(SearchFacetCountResponseSchema), + }) + .meta({ id: 'SearchFacetResponseDto' }); -class SearchFacetResponseDto { - @ApiProperty({ description: 'Facet field name' }) - fieldName!: string; - @ApiProperty({ description: 'Facet counts' }) - counts!: SearchFacetCountResponseDto[]; -} +const SearchAlbumResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of matching albums'), + count: z.int().min(0).describe('Number of albums in this page'), + items: z.array(AlbumResponseSchema), + facets: z.array(SearchFacetResponseSchema), + }) + .meta({ id: 'SearchAlbumResponseDto' }); -class SearchAlbumResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching albums' }) - total!: number; - @ApiProperty({ type: 'integer', description: 'Number of albums in this page' }) - count!: number; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - items!: AlbumResponseDto[]; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - facets!: SearchFacetResponseDto[]; -} +const SearchAssetResponseSchema = z + .object({ + total: z.int().min(0).describe('Total number of matching assets'), + count: z.int().min(0).describe('Number of assets in this page'), + items: z.array(AssetResponseSchema), + facets: z.array(SearchFacetResponseSchema), + nextPage: z.string().nullable().describe('Next page token'), + }) + .meta({ id: 'SearchAssetResponseDto' }); -class SearchAssetResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching assets' }) - total!: number; - @ApiProperty({ type: 'integer', description: 'Number of assets in this page' }) - count!: number; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - items!: AssetResponseDto[]; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - facets!: SearchFacetResponseDto[]; - @ApiProperty({ description: 'Next page token' }) - nextPage!: string | null; -} +const SearchResponseSchema = z + .object({ + albums: SearchAlbumResponseSchema, + assets: SearchAssetResponseSchema, + }) + .meta({ id: 'SearchResponseDto' }); -export class SearchResponseDto { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - albums!: SearchAlbumResponseDto; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: SearchAssetResponseDto; -} +export class SearchResponseDto extends createZodDto(SearchResponseSchema) {} -export class SearchStatisticsResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of matching assets' }) - total!: number; -} +const SearchStatisticsResponseSchema = z + .object({ + total: z.int().describe('Total number of matching assets'), + }) + .meta({ id: 'SearchStatisticsResponseDto' }); -class SearchExploreItem { - @ApiProperty({ description: 'Explore value' }) - value!: string; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - data!: AssetResponseDto; -} +export class SearchStatisticsResponseDto extends createZodDto(SearchStatisticsResponseSchema) {} -export class SearchExploreResponseDto { - @ApiProperty({ description: 'Explore field name' }) - fieldName!: string; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - items!: SearchExploreItem[]; -} +const SearchExploreItemSchema = z + .object({ + value: z.string().describe('Explore value'), + data: AssetResponseSchema, + }) + .meta({ id: 'SearchExploreItem' }); -export class MemoryLaneDto { - @ApiProperty({ type: 'integer', description: 'Day of month' }) - @IsInt() - @Type(() => Number) - @Max(31) - @Min(1) - day!: number; +const SearchExploreResponseSchema = z + .object({ + fieldName: z.string().describe('Explore field name'), + items: z.array(SearchExploreItemSchema), + }) + .meta({ id: 'SearchExploreResponseDto' }); - @ApiProperty({ type: 'integer', description: 'Month' }) - @IsInt() - @Type(() => Number) - @Max(12) - @Min(1) - month!: number; -} +export class SearchExploreResponseDto extends createZodDto(SearchExploreResponseSchema) {} diff --git a/server/src/dtos/server.dto.ts b/server/src/dtos/server.dto.ts index 626c94e40a1b..bd420327713e 100644 --- a/server/src/dtos/server.dto.ts +++ b/server/src/dtos/server.dto.ts @@ -1,242 +1,169 @@ -import { ApiProperty, ApiPropertyOptional, ApiResponseProperty } from '@nestjs/swagger'; -import { SemVer } from 'semver'; -import { SystemConfigThemeDto } from 'src/dtos/system-config.dto'; +import { createZodDto } from 'nestjs-zod'; +import type { SemVer } from 'semver'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class ServerPingResponse { - @ApiResponseProperty({ type: String, example: 'pong' }) - res!: string; -} +const ServerPingResponseSchema = z + .object({ + res: z.string().meta({ example: 'pong' }), + }) + .meta({ id: 'ServerPingResponse' }); -export class ServerAboutResponseDto { - @ApiProperty({ description: 'Server version' }) - version!: string; - @ApiProperty({ description: 'URL to version information' }) - versionUrl!: string; +const ServerAboutResponseSchema = z + .object({ + version: z.string().describe('Server version'), + versionUrl: z.string().describe('URL to version information'), + repository: z.string().optional().describe('Repository name'), + repositoryUrl: z.string().optional().describe('Repository URL'), + sourceRef: z.string().optional().describe('Source reference (branch/tag)'), + sourceCommit: z.string().optional().describe('Source commit hash'), + sourceUrl: z.string().optional().describe('Source URL'), + build: z.string().optional().describe('Build identifier'), + buildUrl: z.string().optional().describe('Build URL'), + buildImage: z.string().optional().describe('Build image name'), + buildImageUrl: z.string().optional().describe('Build image URL'), + nodejs: z.string().optional().describe('Node.js version'), + ffmpeg: z.string().optional().describe('FFmpeg version'), + imagemagick: z.string().optional().describe('ImageMagick version'), + libvips: z.string().optional().describe('libvips version'), + exiftool: z.string().optional().describe('ExifTool version'), + licensed: z.boolean().describe('Whether the server is licensed'), + thirdPartySourceUrl: z.string().optional().describe('Third-party source URL'), + thirdPartyBugFeatureUrl: z.string().optional().describe('Third-party bug/feature URL'), + thirdPartyDocumentationUrl: z.string().optional().describe('Third-party documentation URL'), + thirdPartySupportUrl: z.string().optional().describe('Third-party support URL'), + }) + .meta({ id: 'ServerAboutResponseDto' }); - @ApiPropertyOptional({ description: 'Repository name' }) - repository?: string; - @ApiPropertyOptional({ description: 'Repository URL' }) - repositoryUrl?: string; +const ServerApkLinksSchema = z + .object({ + arm64v8a: z.string().describe('APK download link for ARM64 v8a architecture'), + armeabiv7a: z.string().describe('APK download link for ARM EABI v7a architecture'), + universal: z.string().describe('APK download link for universal architecture'), + x86_64: z.string().describe('APK download link for x86_64 architecture'), + }) + .meta({ id: 'ServerApkLinksDto' }); - @ApiPropertyOptional({ description: 'Source reference (branch/tag)' }) - sourceRef?: string; - @ApiPropertyOptional({ description: 'Source commit hash' }) - sourceCommit?: string; - @ApiPropertyOptional({ description: 'Source URL' }) - sourceUrl?: string; +const ServerStorageResponseSchema = z + .object({ + diskSize: z.string().describe('Total disk size (human-readable format)'), + diskUse: z.string().describe('Used disk space (human-readable format)'), + diskAvailable: z.string().describe('Available disk space (human-readable format)'), + diskSizeRaw: z.int().describe('Total disk size in bytes'), + diskUseRaw: z.int().describe('Used disk space in bytes'), + diskAvailableRaw: z.int().describe('Available disk space in bytes'), + diskUsagePercentage: z.number().meta({ format: 'double' }).describe('Disk usage percentage (0-100)'), + }) + .meta({ id: 'ServerStorageResponseDto' }); - @ApiPropertyOptional({ description: 'Build identifier' }) - build?: string; - @ApiPropertyOptional({ description: 'Build URL' }) - buildUrl?: string; - @ApiPropertyOptional({ description: 'Build image name' }) - buildImage?: string; - @ApiPropertyOptional({ description: 'Build image URL' }) - buildImageUrl?: string; +const ServerVersionResponseSchema = z + .object({ + major: z.int().describe('Major version number'), + minor: z.int().describe('Minor version number'), + patch: z.int().describe('Patch version number'), + }) + .meta({ id: 'ServerVersionResponseDto' }); - @ApiPropertyOptional({ description: 'Node.js version' }) - nodejs?: string; - @ApiPropertyOptional({ description: 'FFmpeg version' }) - ffmpeg?: string; - @ApiPropertyOptional({ description: 'ImageMagick version' }) - imagemagick?: string; - @ApiPropertyOptional({ description: 'libvips version' }) - libvips?: string; - @ApiPropertyOptional({ description: 'ExifTool version' }) - exiftool?: string; +const ServerVersionHistoryResponseSchema = z + .object({ + id: z.string().describe('Version history entry ID'), + createdAt: isoDatetimeToDate.describe('When this version was first seen'), + version: z.string().describe('Version string'), + }) + .meta({ id: 'ServerVersionHistoryResponseDto' }); - @ApiProperty({ description: 'Whether the server is licensed' }) - licensed!: boolean; +const UsageByUserSchema = z + .object({ + userId: z.string().describe('User ID'), + userName: z.string().describe('User name'), + photos: z.int().describe('Number of photos'), + videos: z.int().describe('Number of videos'), + usage: z.int().describe('Total storage usage in bytes'), + usagePhotos: z.int().describe('Storage usage for photos in bytes'), + usageVideos: z.int().describe('Storage usage for videos in bytes'), + quotaSizeInBytes: z.int().nullable().describe('User quota size in bytes (null if unlimited)'), + }) + .meta({ id: 'UsageByUserDto' }); - @ApiPropertyOptional({ description: 'Third-party source URL' }) - thirdPartySourceUrl?: string; - @ApiPropertyOptional({ description: 'Third-party bug/feature URL' }) - thirdPartyBugFeatureUrl?: string; - @ApiPropertyOptional({ description: 'Third-party documentation URL' }) - thirdPartyDocumentationUrl?: string; - @ApiPropertyOptional({ description: 'Third-party support URL' }) - thirdPartySupportUrl?: string; -} +const ServerStatsResponseSchema = z + .object({ + photos: z.int().describe('Total number of photos'), + videos: z.int().describe('Total number of videos'), + usage: z.int().describe('Total storage usage in bytes'), + usagePhotos: z.int().describe('Storage usage for photos in bytes'), + usageVideos: z.int().describe('Storage usage for videos in bytes'), + usageByUser: z.array(UsageByUserSchema).describe('Array of usage for each user'), + }) + .meta({ id: 'ServerStatsResponseDto' }); -export class ServerApkLinksDto { - @ApiProperty({ description: 'APK download link for ARM64 v8a architecture' }) - arm64v8a!: string; - @ApiProperty({ description: 'APK download link for ARM EABI v7a architecture' }) - armeabiv7a!: string; - @ApiProperty({ description: 'APK download link for universal architecture' }) - universal!: string; - @ApiProperty({ description: 'APK download link for x86_64 architecture' }) - x86_64!: string; -} +const ServerMediaTypesResponseSchema = z + .object({ + video: z.array(z.string()).describe('Supported video MIME types'), + image: z.array(z.string()).describe('Supported image MIME types'), + sidecar: z.array(z.string()).describe('Supported sidecar MIME types'), + }) + .meta({ id: 'ServerMediaTypesResponseDto' }); -export class ServerStorageResponseDto { - @ApiProperty({ description: 'Total disk size (human-readable format)' }) - diskSize!: string; - @ApiProperty({ description: 'Used disk space (human-readable format)' }) - diskUse!: string; - @ApiProperty({ description: 'Available disk space (human-readable format)' }) - diskAvailable!: string; +const ServerThemeSchema = z + .object({ + customCss: z.string().describe('Custom CSS for theming'), + }) + .meta({ id: 'ServerThemeDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'Total disk size in bytes' }) - diskSizeRaw!: number; +const ServerConfigSchema = z + .object({ + oauthButtonText: z.string().describe('OAuth button text'), + loginPageMessage: z.string().describe('Login page message'), + trashDays: z.int().describe('Number of days before trashed assets are permanently deleted'), + userDeleteDelay: z.int().describe('Delay in days before deleted users are permanently removed'), + isInitialized: z.boolean().describe('Whether the server has been initialized'), + isOnboarded: z.boolean().describe('Whether the admin has completed onboarding'), + externalDomain: z.string().describe('External domain URL'), + publicUsers: z.boolean().describe('Whether public user registration is enabled'), + mapDarkStyleUrl: z.string().describe('Map dark style URL'), + mapLightStyleUrl: z.string().describe('Map light style URL'), + maintenanceMode: z.boolean().describe('Whether maintenance mode is active'), + }) + .meta({ id: 'ServerConfigDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'Used disk space in bytes' }) - diskUseRaw!: number; +const ServerFeaturesSchema = z + .object({ + smartSearch: z.boolean().describe('Whether smart search is enabled'), + duplicateDetection: z.boolean().describe('Whether duplicate detection is enabled'), + configFile: z.boolean().describe('Whether config file is available'), + facialRecognition: z.boolean().describe('Whether facial recognition is enabled'), + map: z.boolean().describe('Whether map feature is enabled'), + trash: z.boolean().describe('Whether trash feature is enabled'), + reverseGeocoding: z.boolean().describe('Whether reverse geocoding is enabled'), + importFaces: z.boolean().describe('Whether face import is enabled'), + oauth: z.boolean().describe('Whether OAuth is enabled'), + oauthAutoLaunch: z.boolean().describe('Whether OAuth auto-launch is enabled'), + passwordLogin: z.boolean().describe('Whether password login is enabled'), + sidecar: z.boolean().describe('Whether sidecar files are supported'), + search: z.boolean().describe('Whether search is enabled'), + email: z.boolean().describe('Whether email notifications are enabled'), + ocr: z.boolean().describe('Whether OCR is enabled'), + }) + .meta({ id: 'ServerFeaturesDto' }); - @ApiProperty({ type: 'integer', format: 'int64', description: 'Available disk space in bytes' }) - diskAvailableRaw!: number; +export class ServerPingResponse extends createZodDto(ServerPingResponseSchema) {} +export class ServerAboutResponseDto extends createZodDto(ServerAboutResponseSchema) {} +export class ServerApkLinksDto extends createZodDto(ServerApkLinksSchema) {} +export class ServerStorageResponseDto extends createZodDto(ServerStorageResponseSchema) {} - @ApiProperty({ type: 'number', format: 'double', description: 'Disk usage percentage (0-100)' }) - diskUsagePercentage!: number; -} - -export class ServerVersionResponseDto { - @ApiProperty({ type: 'integer', description: 'Major version number' }) - major!: number; - @ApiProperty({ type: 'integer', description: 'Minor version number' }) - minor!: number; - @ApiProperty({ type: 'integer', description: 'Patch version number' }) - patch!: number; - - static fromSemVer(value: SemVer) { +export class ServerVersionResponseDto extends createZodDto(ServerVersionResponseSchema) { + static fromSemVer(value: SemVer): z.infer { return { major: value.major, minor: value.minor, patch: value.patch }; } } -export class ServerVersionHistoryResponseDto { - @ApiProperty({ description: 'Version history entry ID' }) - id!: string; - @ApiProperty({ description: 'When this version was first seen', format: 'date-time' }) - createdAt!: Date; - @ApiProperty({ description: 'Version string' }) - version!: string; -} - -export class UsageByUserDto { - @ApiProperty({ type: 'string', description: 'User ID' }) - userId!: string; - @ApiProperty({ type: 'string', description: 'User name' }) - userName!: string; - @ApiProperty({ type: 'integer', description: 'Number of photos' }) - photos!: number; - @ApiProperty({ type: 'integer', description: 'Number of videos' }) - videos!: number; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Total storage usage in bytes' }) - usage!: number; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for photos in bytes' }) - usagePhotos!: number; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for videos in bytes' }) - usageVideos!: number; - @ApiProperty({ - type: 'integer', - format: 'int64', - nullable: true, - description: 'User quota size in bytes (null if unlimited)', - }) - quotaSizeInBytes!: number | null; -} - -export class ServerStatsResponseDto { - @ApiProperty({ type: 'integer', description: 'Total number of photos' }) - photos = 0; - - @ApiProperty({ type: 'integer', description: 'Total number of videos' }) - videos = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Total storage usage in bytes' }) - usage = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for photos in bytes' }) - usagePhotos = 0; - - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage for videos in bytes' }) - usageVideos = 0; - - @ApiProperty({ - isArray: true, - type: UsageByUserDto, - title: 'Array of usage for each user', - example: [ - { - photos: 1, - videos: 1, - diskUsageRaw: 2, - usagePhotos: 1, - usageVideos: 1, - }, - ], - }) - usageByUser: UsageByUserDto[] = []; -} - -export class ServerMediaTypesResponseDto { - @ApiProperty({ description: 'Supported video MIME types' }) - video!: string[]; - @ApiProperty({ description: 'Supported image MIME types' }) - image!: string[]; - @ApiProperty({ description: 'Supported sidecar MIME types' }) - sidecar!: string[]; -} - -export class ServerThemeDto extends SystemConfigThemeDto {} - -export class ServerConfigDto { - @ApiProperty({ description: 'OAuth button text' }) - oauthButtonText!: string; - @ApiProperty({ description: 'Login page message' }) - loginPageMessage!: string; - @ApiProperty({ type: 'integer', description: 'Number of days before trashed assets are permanently deleted' }) - trashDays!: number; - @ApiProperty({ type: 'integer', description: 'Delay in days before deleted users are permanently removed' }) - userDeleteDelay!: number; - @ApiProperty({ description: 'Whether the server has been initialized' }) - isInitialized!: boolean; - @ApiProperty({ description: 'Whether the admin has completed onboarding' }) - isOnboarded!: boolean; - @ApiProperty({ description: 'External domain URL' }) - externalDomain!: string; - @ApiProperty({ description: 'Whether public user registration is enabled' }) - publicUsers!: boolean; - @ApiProperty({ description: 'Map dark style URL' }) - mapDarkStyleUrl!: string; - @ApiProperty({ description: 'Map light style URL' }) - mapLightStyleUrl!: string; - @ApiProperty({ description: 'Whether maintenance mode is active' }) - maintenanceMode!: boolean; -} - -export class ServerFeaturesDto { - @ApiProperty({ description: 'Whether smart search is enabled' }) - smartSearch!: boolean; - @ApiProperty({ description: 'Whether duplicate detection is enabled' }) - duplicateDetection!: boolean; - @ApiProperty({ description: 'Whether config file is available' }) - configFile!: boolean; - @ApiProperty({ description: 'Whether facial recognition is enabled' }) - facialRecognition!: boolean; - @ApiProperty({ description: 'Whether map feature is enabled' }) - map!: boolean; - @ApiProperty({ description: 'Whether trash feature is enabled' }) - trash!: boolean; - @ApiProperty({ description: 'Whether reverse geocoding is enabled' }) - reverseGeocoding!: boolean; - @ApiProperty({ description: 'Whether face import is enabled' }) - importFaces!: boolean; - @ApiProperty({ description: 'Whether OAuth is enabled' }) - oauth!: boolean; - @ApiProperty({ description: 'Whether OAuth auto-launch is enabled' }) - oauthAutoLaunch!: boolean; - @ApiProperty({ description: 'Whether password login is enabled' }) - passwordLogin!: boolean; - @ApiProperty({ description: 'Whether sidecar files are supported' }) - sidecar!: boolean; - @ApiProperty({ description: 'Whether search is enabled' }) - search!: boolean; - @ApiProperty({ description: 'Whether email notifications are enabled' }) - email!: boolean; - @ApiProperty({ description: 'Whether OCR is enabled' }) - ocr!: boolean; -} +export class ServerVersionHistoryResponseDto extends createZodDto(ServerVersionHistoryResponseSchema) {} +export class UsageByUserDto extends createZodDto(UsageByUserSchema) {} +export class ServerStatsResponseDto extends createZodDto(ServerStatsResponseSchema) {} +export class ServerMediaTypesResponseDto extends createZodDto(ServerMediaTypesResponseSchema) {} +export class ServerThemeDto extends createZodDto(ServerThemeSchema) {} +export class ServerConfigDto extends createZodDto(ServerConfigSchema) {} +export class ServerFeaturesDto extends createZodDto(ServerFeaturesSchema) {} export interface ReleaseNotification { isAvailable: boolean; diff --git a/server/src/dtos/session.dto.ts b/server/src/dtos/session.dto.ts index f918f0b3bb2c..179a1dfb76a9 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 b2ecc70a3aec..7dcec034dcbb 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -1,155 +1,103 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { SharedLink } from 'src/database'; -import { HistoryBuilder, Property } from 'src/decorators'; -import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { SharedLinkType } from 'src/enum'; -import { Optional, ValidateBoolean, ValidateDate, ValidateEnum, ValidateString, ValidateUUID } from 'src/validation'; +import { HistoryBuilder } from 'src/decorators'; +import { AlbumResponseSchema, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; +import { AssetResponseSchema, mapAsset } from 'src/dtos/asset-response.dto'; +import { SharedLinkTypeSchema } from 'src/enum'; +import { emptyStringToNull, isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class SharedLinkSearchDto { - @ValidateUUID({ optional: true, description: 'Filter by album ID' }) - albumId?: string; - - @ValidateUUID({ - optional: true, - description: 'Filter by shared link ID', - history: new HistoryBuilder().added('v2.5.0'), +const SharedLinkSearchSchema = z + .object({ + albumId: z.uuidv4().optional().describe('Filter by album ID'), + id: z + .uuidv4() + .optional() + .describe('Filter by shared link ID') + .meta(new HistoryBuilder().added('v2.5.0').getExtensions()), }) - id?: string; -} + .meta({ id: 'SharedLinkSearchDto' }); -export class SharedLinkCreateDto { - @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType', description: 'Shared link type' }) - type!: SharedLinkType; - - @ValidateUUID({ each: true, optional: true, description: 'Asset IDs (for individual assets)' }) - assetIds?: string[]; - - @ValidateUUID({ optional: true, description: 'Album ID (for album sharing)' }) - albumId?: string; - - @ApiPropertyOptional({ description: 'Link description' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - description?: string | null; - - @ApiPropertyOptional({ description: 'Link password' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - password?: string | null; - - @ApiPropertyOptional({ description: 'Custom URL slug' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - slug?: string | null; - - @ValidateDate({ optional: true, nullable: true, description: 'Expiration date' }) - expiresAt?: Date | null = null; - - @ValidateBoolean({ optional: true, description: 'Allow uploads' }) - allowUpload?: boolean; - - @ValidateBoolean({ optional: true, description: 'Allow downloads', default: true }) - allowDownload?: boolean = true; - - @ValidateBoolean({ optional: true, description: 'Show metadata', default: true }) - showMetadata?: boolean = true; -} - -export class SharedLinkEditDto { - @ApiPropertyOptional({ description: 'Link description' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - description?: string | null; - - @ApiPropertyOptional({ description: 'Link password' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - password?: string | null; - - @ApiPropertyOptional({ description: 'Custom URL slug' }) - @Optional({ nullable: true, emptyToNull: true }) - @IsString() - slug?: string | null; - - @ApiPropertyOptional({ description: 'Expiration date' }) - @Optional({ nullable: true }) - expiresAt?: Date | null; - - @ValidateBoolean({ optional: true, description: 'Allow uploads' }) - allowUpload?: boolean; - - @ValidateBoolean({ optional: true, description: 'Allow downloads' }) - allowDownload?: boolean; - - @ValidateBoolean({ optional: true, description: 'Show metadata' }) - showMetadata?: boolean; - - @ValidateBoolean({ - optional: true, - description: - 'Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.', +const SharedLinkCreateSchema = z + .object({ + type: SharedLinkTypeSchema, + assetIds: z.array(z.uuidv4()).optional().describe('Asset IDs (for individual assets)'), + albumId: z.uuidv4().optional().describe('Album ID (for album sharing)'), + description: emptyStringToNull(z.string().nullable()).optional().describe('Link description'), + password: emptyStringToNull(z.string().nullable()).optional().describe('Link password'), + slug: emptyStringToNull(z.string().nullable()).optional().describe('Custom URL slug'), + expiresAt: isoDatetimeToDate.nullable().describe('Expiration date').default(null).optional(), + allowUpload: z.boolean().optional().describe('Allow uploads'), + allowDownload: z.boolean().default(true).optional().describe('Allow downloads'), + showMetadata: z.boolean().default(true).optional().describe('Show metadata'), }) - changeExpiryTime?: boolean; -} + .meta({ id: 'SharedLinkCreateDto' }); -export class SharedLinkLoginDto { - @ValidateString({ description: 'Shared link password', example: 'password' }) - password!: string; -} - -export class SharedLinkPasswordDto { - @ApiPropertyOptional({ example: 'password', description: 'Link password' }) - @IsString() - @Optional() - password?: string; - - @ApiPropertyOptional({ description: 'Access token' }) - @IsString() - @Optional() - token?: string; -} -export class SharedLinkResponseDto { - @ApiProperty({ description: 'Shared link ID' }) - id!: string; - @ApiProperty({ description: 'Link description' }) - description!: string | null; - @ApiProperty({ description: 'Has password' }) - password!: string | null; - @Property({ - description: 'Access token', - history: new HistoryBuilder().added('v1').stable('v2').deprecated('v2.6.0'), +const SharedLinkEditSchema = z + .object({ + description: emptyStringToNull(z.string().nullable()).optional().describe('Link description'), + password: emptyStringToNull(z.string().nullable()).optional().describe('Link password'), + slug: emptyStringToNull(z.string().nullable()).optional().describe('Custom URL slug'), + expiresAt: isoDatetimeToDate.nullish().describe('Expiration date'), + allowUpload: z.boolean().optional().describe('Allow uploads'), + allowDownload: z.boolean().optional().describe('Allow downloads'), + showMetadata: z.boolean().optional().describe('Show metadata'), + changeExpiryTime: z + .boolean() + .optional() + .describe( + 'Whether to change the expiry time. Few clients cannot send null to set the expiryTime to never. Setting this flag and not sending expiryAt is considered as null instead. Clients that can send null values can ignore this.', + ), }) - token?: string | null; - @ApiProperty({ description: 'Owner user ID' }) - userId!: string; - @ApiProperty({ description: 'Encryption key (base64url)' }) - key!: string; + .meta({ id: 'SharedLinkEditDto' }); - @ValidateEnum({ enum: SharedLinkType, name: 'SharedLinkType', description: 'Shared link type' }) - type!: SharedLinkType; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Expiration date' }) - expiresAt!: Date | null; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - assets!: AssetResponseDto[]; - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - album?: AlbumResponseDto; - @ApiProperty({ description: 'Allow uploads' }) - allowUpload!: boolean; +const SharedLinkLoginSchema = z + .object({ + password: z.string().describe('Shared link password').meta({ example: 'password' }), + }) + .meta({ id: 'SharedLinkLoginDto' }); - @ApiProperty({ description: 'Allow downloads' }) - allowDownload!: boolean; - @ApiProperty({ description: 'Show metadata' }) - showMetadata!: boolean; +const SharedLinkPasswordSchema = z + .object({ + password: z.string().optional().describe('Link password'), + token: z.string().optional().describe('Access token'), + }) + .meta({ id: 'SharedLinkPasswordDto' }); - @ApiProperty({ description: 'Custom URL slug' }) - slug!: string | null; -} +const SharedLinkResponseSchema = z + .object({ + id: z.string().describe('Shared link ID'), + description: z.string().nullable().describe('Link description'), + password: z.string().nullable().describe('Has password'), + token: z + .string() + .nullish() + .describe('Access token') + .meta({ + ...new HistoryBuilder().added('v1').stable('v2').deprecated('v2.6.0').getExtensions(), + deprecated: true, + }), + userId: z.string().describe('Owner user ID'), + key: z.string().describe('Encryption key (base64url)'), + type: SharedLinkTypeSchema, + createdAt: isoDatetimeToDate.describe('Creation date'), + expiresAt: isoDatetimeToDate.nullable().describe('Expiration date'), + assets: z.array(AssetResponseSchema), + album: AlbumResponseSchema.optional(), + allowUpload: z.boolean().describe('Allow uploads'), + allowDownload: z.boolean().describe('Allow downloads'), + showMetadata: z.boolean().describe('Show metadata'), + slug: z.string().nullable().describe('Custom URL slug'), + }) + .describe('Shared link response') + .meta({ id: 'SharedLinkResponseDto' }); + +export class SharedLinkSearchDto extends createZodDto(SharedLinkSearchSchema) {} +export class SharedLinkCreateDto extends createZodDto(SharedLinkCreateSchema) {} +export class SharedLinkEditDto extends createZodDto(SharedLinkEditSchema) {} +export class SharedLinkLoginDto extends createZodDto(SharedLinkLoginSchema) {} +export class SharedLinkPasswordDto extends createZodDto(SharedLinkPasswordSchema) {} +export class SharedLinkResponseDto extends createZodDto(SharedLinkResponseSchema) {} export function mapSharedLink(sharedLink: SharedLink, options: { stripAssetMetadata: boolean }): SharedLinkResponseDto { const assets = sharedLink.assets || []; diff --git a/server/src/dtos/stack.dto.ts b/server/src/dtos/stack.dto.ts index a76b35e08e73..48354cec6b5f 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 9a1332d30312..d7903ebb0cd1 100644 --- a/server/src/dtos/sync.dto.ts +++ b/server/src/dtos/sync.dto.ts @@ -1,492 +1,423 @@ /* eslint-disable @typescript-eslint/no-unsafe-function-type */ -import { ApiProperty } from '@nestjs/swagger'; -import { ArrayMaxSize, IsInt, IsPositive, IsString } from 'class-validator'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AssetEditAction } from 'src/dtos/editing.dto'; +import { createZodDto } from 'nestjs-zod'; +import { AssetResponseSchema } from 'src/dtos/asset-response.dto'; +import { AssetEditActionSchema } from 'src/dtos/editing.dto'; import { - AlbumUserRole, - AssetOrder, - AssetType, - AssetVisibility, - MemoryType, + AlbumUserRoleSchema, + AssetOrderSchema, + AssetTypeSchema, + AssetVisibilitySchema, + MemoryTypeSchema, SyncEntityType, - SyncRequestType, - UserAvatarColor, - UserMetadataKey, + SyncEntityTypeSchema, + SyncRequestTypeSchema, + UserAvatarColorSchema, + UserMetadataKeySchema, } from 'src/enum'; -import { UserMetadata } from 'src/types'; -import { ValidateBoolean, ValidateDate, ValidateEnum, ValidateUUID } from 'src/validation'; +import { isoDatetimeToDate } from 'src/validation'; +import z from 'zod'; -export class AssetFullSyncDto { - @ValidateUUID({ optional: true, description: 'Last asset ID (pagination)' }) - lastId?: string; +const AssetFullSyncSchema = z + .object({ + lastId: z.uuidv4().optional().describe('Last asset ID (pagination)'), + updatedUntil: isoDatetimeToDate.describe('Sync assets updated until this date'), + limit: z.int().min(1).describe('Maximum number of assets to return'), + userId: z.uuidv4().optional().describe('Filter by user ID'), + }) + .meta({ id: 'AssetFullSyncDto' }); - @ValidateDate({ description: 'Sync assets updated until this date' }) - updatedUntil!: Date; +const AssetDeltaSyncSchema = z + .object({ + updatedAfter: isoDatetimeToDate.describe('Sync assets updated after this date'), + userIds: z.array(z.uuidv4()).describe('User IDs to sync'), + }) + .meta({ id: 'AssetDeltaSyncDto' }); - @ApiProperty({ type: 'integer', description: 'Maximum number of assets to return' }) - @IsInt() - @IsPositive() - limit!: number; +export class AssetFullSyncDto extends createZodDto(AssetFullSyncSchema) {} +export class AssetDeltaSyncDto extends createZodDto(AssetDeltaSyncSchema) {} - @ValidateUUID({ optional: true, description: 'Filter by user ID' }) - userId?: string; -} +const AssetDeltaSyncResponseSchema = z + .object({ + needsFullSync: z.boolean().describe('Whether full sync is needed'), + upserted: z.array(AssetResponseSchema), + deleted: z.array(z.string()).describe('Deleted asset IDs'), + }) + .describe('Asset delta sync response') + .meta({ id: 'AssetDeltaSyncResponseDto' }); -export class AssetDeltaSyncDto { - @ValidateDate({ description: 'Sync assets updated after this date' }) - updatedAfter!: Date; - - @ValidateUUID({ each: true, description: 'User IDs to sync' }) - userIds!: string[]; -} - -export class AssetDeltaSyncResponseDto { - @ApiProperty({ description: 'Whether full sync is needed' }) - needsFullSync!: boolean; - @ApiProperty({ description: 'Upserted assets' }) - upserted!: AssetResponseDto[]; - @ApiProperty({ description: 'Deleted asset IDs' }) - deleted!: string[]; -} +export class AssetDeltaSyncResponseDto extends createZodDto(AssetDeltaSyncResponseSchema) {} export const extraSyncModels: Function[] = []; -export const ExtraModel = (): ClassDecorator => { +const ExtraModel = (): ClassDecorator => { // eslint-disable-next-line unicorn/consistent-function-scoping return (object: Function) => { extraSyncModels.push(object); }; }; -@ExtraModel() -export class SyncUserV1 { - @ApiProperty({ description: 'User ID' }) - id!: string; - @ApiProperty({ description: 'User name' }) - name!: string; - @ApiProperty({ description: 'User email' }) - email!: string; - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'User avatar color' }) - avatarColor!: UserAvatarColor | null; - @ApiProperty({ description: 'User deleted at' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'User has profile image' }) - hasProfileImage!: boolean; - @ApiProperty({ description: 'User profile changed at' }) - profileChangedAt!: Date; -} +const SyncUserV1Schema = z + .object({ + id: z.string().describe('User ID'), + name: z.string().describe('User name'), + email: z.string().describe('User email'), + avatarColor: UserAvatarColorSchema.nullish(), + deletedAt: isoDatetimeToDate.nullable().describe('User deleted at'), + hasProfileImage: z.boolean().describe('User has profile image'), + profileChangedAt: isoDatetimeToDate.describe('User profile changed at'), + }) + .meta({ id: 'SyncUserV1' }); + +const SyncAuthUserV1Schema = SyncUserV1Schema.merge( + z.object({ + isAdmin: z.boolean().describe('User is admin'), + pinCode: z.string().nullable().describe('User pin code'), + oauthId: z.string().describe('User OAuth ID'), + storageLabel: z.string().nullable().describe('User storage label'), + quotaSizeInBytes: z.int().nullable().describe('Quota size in bytes'), + quotaUsageInBytes: z.int().describe('Quota usage in bytes'), + }), +).meta({ id: 'SyncAuthUserV1' }); + +const SyncUserDeleteV1Schema = z.object({ userId: z.string().describe('User ID') }).meta({ id: 'SyncUserDeleteV1' }); + +const SyncPartnerV1Schema = z + .object({ + sharedById: z.string().describe('Shared by ID'), + sharedWithId: z.string().describe('Shared with ID'), + inTimeline: z.boolean().describe('In timeline'), + }) + .meta({ id: 'SyncPartnerV1' }); + +const SyncPartnerDeleteV1Schema = z + .object({ + sharedById: z.string().describe('Shared by ID'), + sharedWithId: z.string().describe('Shared with ID'), + }) + .meta({ id: 'SyncPartnerDeleteV1' }); + +const SyncAssetV1Schema = z + .object({ + id: z.string().describe('Asset ID'), + ownerId: z.string().describe('Owner ID'), + originalFileName: z.string().describe('Original file name'), + thumbhash: z.string().nullable().describe('Thumbhash'), + checksum: z.string().describe('Checksum'), + fileCreatedAt: isoDatetimeToDate.nullable().describe('File created at'), + fileModifiedAt: isoDatetimeToDate.nullable().describe('File modified at'), + localDateTime: isoDatetimeToDate.nullable().describe('Local date time'), + duration: z.string().nullable().describe('Duration'), + type: AssetTypeSchema, + deletedAt: isoDatetimeToDate.nullable().describe('Deleted at'), + isFavorite: z.boolean().describe('Is favorite'), + visibility: AssetVisibilitySchema, + livePhotoVideoId: z.string().nullable().describe('Live photo video ID'), + stackId: z.string().nullable().describe('Stack ID'), + libraryId: z.string().nullable().describe('Library ID'), + width: z.int().nullable().describe('Asset width'), + height: z.int().nullable().describe('Asset height'), + isEdited: z.boolean().describe('Is edited'), + }) + .meta({ id: 'SyncAssetV1' }); @ExtraModel() -export class SyncAuthUserV1 extends SyncUserV1 { - @ApiProperty({ description: 'User is admin' }) - isAdmin!: boolean; - @ApiProperty({ description: 'User pin code' }) - pinCode!: string | null; - @ApiProperty({ description: 'User OAuth ID' }) - oauthId!: string; - @ApiProperty({ description: 'User storage label' }) - storageLabel!: string | null; - @ApiProperty({ type: 'integer' }) - quotaSizeInBytes!: number | null; - @ApiProperty({ type: 'integer' }) - quotaUsageInBytes!: number; -} +class SyncUserV1 extends createZodDto(SyncUserV1Schema) {} +@ExtraModel() +class SyncAuthUserV1 extends createZodDto(SyncAuthUserV1Schema) {} +@ExtraModel() +class SyncUserDeleteV1 extends createZodDto(SyncUserDeleteV1Schema) {} +@ExtraModel() +class SyncPartnerV1 extends createZodDto(SyncPartnerV1Schema) {} +@ExtraModel() +class SyncPartnerDeleteV1 extends createZodDto(SyncPartnerDeleteV1Schema) {} +@ExtraModel() +export class SyncAssetV1 extends createZodDto(SyncAssetV1Schema) {} + +const SyncAssetDeleteV1Schema = z + .object({ assetId: z.string().describe('Asset ID') }) + .meta({ id: 'SyncAssetDeleteV1' }); + +const SyncAssetExifV1Schema = z + .object({ + assetId: z.string().describe('Asset ID'), + description: z.string().nullable().describe('Description'), + exifImageWidth: z.int().nullable().describe('Exif image width'), + exifImageHeight: z.int().nullable().describe('Exif image height'), + fileSizeInByte: z.int().nullable().describe('File size in byte'), + orientation: z.string().nullable().describe('Orientation'), + dateTimeOriginal: isoDatetimeToDate.nullable().describe('Date time original'), + modifyDate: isoDatetimeToDate.nullable().describe('Modify date'), + timeZone: z.string().nullable().describe('Time zone'), + latitude: z.number().meta({ format: 'double' }).nullable().describe('Latitude'), + longitude: z.number().meta({ format: 'double' }).nullable().describe('Longitude'), + projectionType: z.string().nullable().describe('Projection type'), + city: z.string().nullable().describe('City'), + state: z.string().nullable().describe('State'), + country: z.string().nullable().describe('Country'), + make: z.string().nullable().describe('Make'), + model: z.string().nullable().describe('Model'), + lensModel: z.string().nullable().describe('Lens model'), + fNumber: z.number().meta({ format: 'double' }).nullable().describe('F number'), + focalLength: z.number().meta({ format: 'double' }).nullable().describe('Focal length'), + iso: z.int().nullable().describe('ISO'), + exposureTime: z.string().nullable().describe('Exposure time'), + profileDescription: z.string().nullable().describe('Profile description'), + rating: z.int().nullable().describe('Rating'), + fps: z.number().meta({ format: 'double' }).nullable().describe('FPS'), + }) + .meta({ id: 'SyncAssetExifV1' }); + +const SyncAssetMetadataV1Schema = z + .object({ + assetId: z.string().describe('Asset ID'), + key: z.string().describe('Key'), + value: z.record(z.string(), z.unknown()).describe('Value'), + }) + .meta({ id: 'SyncAssetMetadataV1' }); + +const SyncAssetMetadataDeleteV1Schema = z + .object({ + assetId: z.string().describe('Asset ID'), + key: z.string().describe('Key'), + }) + .meta({ id: 'SyncAssetMetadataDeleteV1' }); + +const SyncAssetEditV1Schema = z + .object({ + id: z.string().describe('Edit ID'), + assetId: z.string().describe('Asset ID'), + action: AssetEditActionSchema, + parameters: z.record(z.string(), z.unknown()).describe('Edit parameters'), + sequence: z.int().describe('Edit sequence'), + }) + .meta({ id: 'SyncAssetEditV1' }); + +const SyncAssetEditDeleteV1Schema = z + .object({ editId: z.string().describe('Edit ID') }) + .meta({ id: 'SyncAssetEditDeleteV1' }); @ExtraModel() -export class SyncUserDeleteV1 { - @ApiProperty({ description: 'User ID' }) - userId!: string; -} +class SyncAssetDeleteV1 extends createZodDto(SyncAssetDeleteV1Schema) {} +@ExtraModel() +export class SyncAssetExifV1 extends createZodDto(SyncAssetExifV1Schema) {} +@ExtraModel() +class SyncAssetMetadataV1 extends createZodDto(SyncAssetMetadataV1Schema) {} +@ExtraModel() +class SyncAssetMetadataDeleteV1 extends createZodDto(SyncAssetMetadataDeleteV1Schema) {} +@ExtraModel() +export class SyncAssetEditV1 extends createZodDto(SyncAssetEditV1Schema) {} +@ExtraModel() +class SyncAssetEditDeleteV1 extends createZodDto(SyncAssetEditDeleteV1Schema) {} + +const SyncAlbumDeleteV1Schema = z + .object({ albumId: z.string().describe('Album ID') }) + .meta({ id: 'SyncAlbumDeleteV1' }); + +const SyncAlbumUserDeleteV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + userId: z.string().describe('User ID'), + }) + .meta({ id: 'SyncAlbumUserDeleteV1' }); + +const SyncAlbumUserV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + userId: z.string().describe('User ID'), + role: AlbumUserRoleSchema, + }) + .meta({ id: 'SyncAlbumUserV1' }); + +const SyncAlbumV1Schema = z + .object({ + id: z.string().describe('Album ID'), + ownerId: z.string().describe('Owner ID'), + name: z.string().describe('Album name'), + description: z.string().describe('Album description'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + thumbnailAssetId: z.string().nullable().describe('Thumbnail asset ID'), + isActivityEnabled: z.boolean().describe('Is activity enabled'), + order: AssetOrderSchema, + }) + .meta({ id: 'SyncAlbumV1' }); + +const SyncAlbumToAssetV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncAlbumToAssetV1' }); + +const SyncAlbumToAssetDeleteV1Schema = z + .object({ + albumId: z.string().describe('Album ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncAlbumToAssetDeleteV1' }); @ExtraModel() -export class SyncPartnerV1 { - @ApiProperty({ description: 'Shared by ID' }) - sharedById!: string; - @ApiProperty({ description: 'Shared with ID' }) - sharedWithId!: string; - @ApiProperty({ description: 'In timeline' }) - inTimeline!: boolean; -} +class SyncAlbumDeleteV1 extends createZodDto(SyncAlbumDeleteV1Schema) {} +@ExtraModel() +class SyncAlbumUserDeleteV1 extends createZodDto(SyncAlbumUserDeleteV1Schema) {} +@ExtraModel() +class SyncAlbumUserV1 extends createZodDto(SyncAlbumUserV1Schema) {} +@ExtraModel() +class SyncAlbumV1 extends createZodDto(SyncAlbumV1Schema) {} +@ExtraModel() +class SyncAlbumToAssetV1 extends createZodDto(SyncAlbumToAssetV1Schema) {} +@ExtraModel() +class SyncAlbumToAssetDeleteV1 extends createZodDto(SyncAlbumToAssetDeleteV1Schema) {} + +const SyncMemoryV1Schema = z + .object({ + id: z.string().describe('Memory ID'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + deletedAt: isoDatetimeToDate.nullable().describe('Deleted at'), + ownerId: z.string().describe('Owner ID'), + type: MemoryTypeSchema, + data: z.record(z.string(), z.unknown()).describe('Data'), + isSaved: z.boolean().describe('Is saved'), + memoryAt: isoDatetimeToDate.describe('Memory at'), + seenAt: isoDatetimeToDate.nullable().describe('Seen at'), + showAt: isoDatetimeToDate.nullable().describe('Show at'), + hideAt: isoDatetimeToDate.nullable().describe('Hide at'), + }) + .meta({ id: 'SyncMemoryV1' }); + +const SyncMemoryDeleteV1Schema = z + .object({ memoryId: z.string().describe('Memory ID') }) + .meta({ id: 'SyncMemoryDeleteV1' }); + +const SyncMemoryAssetV1Schema = z + .object({ + memoryId: z.string().describe('Memory ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncMemoryAssetV1' }); + +const SyncMemoryAssetDeleteV1Schema = z + .object({ + memoryId: z.string().describe('Memory ID'), + assetId: z.string().describe('Asset ID'), + }) + .meta({ id: 'SyncMemoryAssetDeleteV1' }); + +const SyncStackV1Schema = z + .object({ + id: z.string().describe('Stack ID'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + primaryAssetId: z.string().describe('Primary asset ID'), + ownerId: z.string().describe('Owner ID'), + }) + .meta({ id: 'SyncStackV1' }); + +const SyncStackDeleteV1Schema = z + .object({ stackId: z.string().describe('Stack ID') }) + .meta({ id: 'SyncStackDeleteV1' }); + +const SyncPersonV1Schema = z + .object({ + id: z.string().describe('Person ID'), + createdAt: isoDatetimeToDate.describe('Created at'), + updatedAt: isoDatetimeToDate.describe('Updated at'), + ownerId: z.string().describe('Owner ID'), + name: z.string().describe('Person name'), + birthDate: isoDatetimeToDate.nullable().describe('Birth date'), + isHidden: z.boolean().describe('Is hidden'), + isFavorite: z.boolean().describe('Is favorite'), + color: z.string().nullable().describe('Color'), + faceAssetId: z.string().nullable().describe('Face asset ID'), + }) + .meta({ id: 'SyncPersonV1' }); + +const SyncPersonDeleteV1Schema = z + .object({ personId: z.string().describe('Person ID') }) + .meta({ id: 'SyncPersonDeleteV1' }); + +const SyncAssetFaceV1Schema = z + .object({ + id: z.string().describe('Asset face ID'), + assetId: z.string().describe('Asset ID'), + personId: z.string().nullable().describe('Person ID'), + imageWidth: z.int().describe('Image width'), + imageHeight: z.int().describe('Image height'), + boundingBoxX1: z.int().describe('Bounding box X1'), + boundingBoxY1: z.int().describe('Bounding box Y1'), + boundingBoxX2: z.int().describe('Bounding box X2'), + boundingBoxY2: z.int().describe('Bounding box Y2'), + sourceType: z.string().describe('Source type'), + }) + .meta({ id: 'SyncAssetFaceV1' }); + +const SyncAssetFaceV2Schema = SyncAssetFaceV1Schema.extend({ + deletedAt: isoDatetimeToDate.nullable().describe('Face deleted at'), + isVisible: z.boolean().describe('Is the face visible in the asset'), +}).meta({ id: 'SyncAssetFaceV2' }); + +const SyncAssetFaceDeleteV1Schema = z + .object({ assetFaceId: z.string().describe('Asset face ID') }) + .meta({ id: 'SyncAssetFaceDeleteV1' }); + +const SyncUserMetadataV1Schema = z + .object({ + userId: z.string().describe('User ID'), + key: UserMetadataKeySchema, + value: z.record(z.string(), z.unknown()).describe('User metadata value'), + }) + .meta({ id: 'SyncUserMetadataV1' }); + +const SyncUserMetadataDeleteV1Schema = z + .object({ + userId: z.string().describe('User ID'), + key: UserMetadataKeySchema, + }) + .meta({ id: 'SyncUserMetadataDeleteV1' }); + +const SyncAckV1Schema = z.object({}).meta({ id: 'SyncAckV1' }); +const SyncResetV1Schema = z.object({}).meta({ id: 'SyncResetV1' }); +const SyncCompleteV1Schema = z.object({}).meta({ id: 'SyncCompleteV1' }); @ExtraModel() -export class SyncPartnerDeleteV1 { - @ApiProperty({ description: 'Shared by ID' }) - sharedById!: string; - @ApiProperty({ description: 'Shared with ID' }) - sharedWithId!: string; -} - +class SyncMemoryV1 extends createZodDto(SyncMemoryV1Schema) {} @ExtraModel() -export class SyncAssetV1 { - @ApiProperty({ description: 'Asset ID' }) - id!: string; - @ApiProperty({ description: 'Owner ID' }) - ownerId!: string; - @ApiProperty({ description: 'Original file name' }) - originalFileName!: string; - @ApiProperty({ description: 'Thumbhash' }) - thumbhash!: string | null; - @ApiProperty({ description: 'Checksum' }) - checksum!: string; - @ApiProperty({ description: 'File created at' }) - fileCreatedAt!: Date | null; - @ApiProperty({ description: 'File modified at' }) - fileModifiedAt!: Date | null; - @ApiProperty({ description: 'Local date time' }) - localDateTime!: Date | null; - @ApiProperty({ description: 'Duration' }) - duration!: string | null; - @ValidateEnum({ enum: AssetType, name: 'AssetTypeEnum', description: 'Asset type' }) - type!: AssetType; - @ApiProperty({ description: 'Deleted at' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Is favorite' }) - isFavorite!: boolean; - @ValidateEnum({ enum: AssetVisibility, name: 'AssetVisibility', description: 'Asset visibility' }) - visibility!: AssetVisibility; - @ApiProperty({ description: 'Live photo video ID' }) - livePhotoVideoId!: string | null; - @ApiProperty({ description: 'Stack ID' }) - stackId!: string | null; - @ApiProperty({ description: 'Library ID' }) - libraryId!: string | null; - @ApiProperty({ type: 'integer', description: 'Asset width' }) - width!: number | null; - @ApiProperty({ type: 'integer', description: 'Asset height' }) - height!: number | null; - @ApiProperty({ description: 'Is edited' }) - isEdited!: boolean; -} - +class SyncMemoryDeleteV1 extends createZodDto(SyncMemoryDeleteV1Schema) {} @ExtraModel() -export class SyncAssetDeleteV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - +class SyncMemoryAssetV1 extends createZodDto(SyncMemoryAssetV1Schema) {} @ExtraModel() -export class SyncAssetExifV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Description' }) - description!: string | null; - @ApiProperty({ type: 'integer', description: 'Exif image width' }) - exifImageWidth!: number | null; - @ApiProperty({ type: 'integer', description: 'Exif image height' }) - exifImageHeight!: number | null; - @ApiProperty({ type: 'integer', description: 'File size in byte' }) - fileSizeInByte!: number | null; - @ApiProperty({ description: 'Orientation' }) - orientation!: string | null; - @ApiProperty({ description: 'Date time original' }) - dateTimeOriginal!: Date | null; - @ApiProperty({ description: 'Modify date' }) - modifyDate!: Date | null; - @ApiProperty({ description: 'Time zone' }) - timeZone!: string | null; - @ApiProperty({ type: 'number', format: 'double', description: 'Latitude' }) - latitude!: number | null; - @ApiProperty({ type: 'number', format: 'double', description: 'Longitude' }) - longitude!: number | null; - @ApiProperty({ description: 'Projection type' }) - projectionType!: string | null; - @ApiProperty({ description: 'City' }) - city!: string | null; - @ApiProperty({ description: 'State' }) - state!: string | null; - @ApiProperty({ description: 'Country' }) - country!: string | null; - @ApiProperty({ description: 'Make' }) - make!: string | null; - @ApiProperty({ description: 'Model' }) - model!: string | null; - @ApiProperty({ description: 'Lens model' }) - lensModel!: string | null; - @ApiProperty({ type: 'number', format: 'double', description: 'F number' }) - fNumber!: number | null; - @ApiProperty({ type: 'number', format: 'double', description: 'Focal length' }) - focalLength!: number | null; - @ApiProperty({ type: 'integer', description: 'ISO' }) - iso!: number | null; - @ApiProperty({ description: 'Exposure time' }) - exposureTime!: string | null; - @ApiProperty({ description: 'Profile description' }) - profileDescription!: string | null; - @ApiProperty({ type: 'integer', description: 'Rating' }) - rating!: number | null; - @ApiProperty({ type: 'number', format: 'double', description: 'FPS' }) - fps!: number | null; -} - +class SyncMemoryAssetDeleteV1 extends createZodDto(SyncMemoryAssetDeleteV1Schema) {} @ExtraModel() -export class SyncAssetEditV1 { - id!: string; - assetId!: string; - - @ValidateEnum({ enum: AssetEditAction, name: 'AssetEditAction' }) - action!: AssetEditAction; - parameters!: object; - - @ApiProperty({ type: 'integer' }) - sequence!: number; -} - +class SyncStackV1 extends createZodDto(SyncStackV1Schema) {} @ExtraModel() -export class SyncAssetEditDeleteV1 { - editId!: string; -} - +class SyncStackDeleteV1 extends createZodDto(SyncStackDeleteV1Schema) {} @ExtraModel() -export class SyncAssetMetadataV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Key' }) - key!: string; - @ApiProperty({ description: 'Value' }) - value!: object; -} - +class SyncPersonV1 extends createZodDto(SyncPersonV1Schema) {} @ExtraModel() -export class SyncAssetMetadataDeleteV1 { - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Key' }) - key!: string; -} - +class SyncPersonDeleteV1 extends createZodDto(SyncPersonDeleteV1Schema) {} @ExtraModel() -export class SyncAlbumDeleteV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; -} - +class SyncAssetFaceV1 extends createZodDto(SyncAssetFaceV1Schema) {} @ExtraModel() -export class SyncAlbumUserDeleteV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'User ID' }) - userId!: string; -} - -@ExtraModel() -export class SyncAlbumUserV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ValidateEnum({ enum: AlbumUserRole, name: 'AlbumUserRole', description: 'Album user role' }) - role!: AlbumUserRole; -} - -@ExtraModel() -export class SyncAlbumV1 { - @ApiProperty({ description: 'Album ID' }) - id!: string; - @ApiProperty({ description: 'Owner ID' }) - ownerId!: string; - @ApiProperty({ description: 'Album name' }) - name!: string; - @ApiProperty({ description: 'Album description' }) - description!: string; - @ApiProperty({ description: 'Created at' }) - createdAt!: Date; - @ApiProperty({ description: 'Updated at' }) - updatedAt!: Date; - @ApiProperty({ description: 'Thumbnail asset ID' }) - thumbnailAssetId!: string | null; - @ApiProperty({ description: 'Is activity enabled' }) - isActivityEnabled!: boolean; - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder' }) - order!: AssetOrder; -} - -@ExtraModel() -export class SyncAlbumToAssetV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - -@ExtraModel() -export class SyncAlbumToAssetDeleteV1 { - @ApiProperty({ description: 'Album ID' }) - albumId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - -@ExtraModel() -export class SyncMemoryV1 { - @ApiProperty({ description: 'Memory ID' }) - id!: string; - @ApiProperty({ description: 'Created at' }) - createdAt!: Date; - @ApiProperty({ description: 'Updated at' }) - updatedAt!: Date; - @ApiProperty({ description: 'Deleted at' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Owner ID' }) - ownerId!: string; - @ValidateEnum({ enum: MemoryType, name: 'MemoryType', description: 'Memory type' }) - type!: MemoryType; - @ApiProperty({ description: 'Data' }) - data!: object; - @ApiProperty({ description: 'Is saved' }) - isSaved!: boolean; - @ApiProperty({ description: 'Memory at' }) - memoryAt!: Date; - @ApiProperty({ description: 'Seen at' }) - seenAt!: Date | null; - @ApiProperty({ description: 'Show at' }) - showAt!: Date | null; - @ApiProperty({ description: 'Hide at' }) - hideAt!: Date | null; -} - -@ExtraModel() -export class SyncMemoryDeleteV1 { - @ApiProperty({ description: 'Memory ID' }) - memoryId!: string; -} - -@ExtraModel() -export class SyncMemoryAssetV1 { - @ApiProperty({ description: 'Memory ID' }) - memoryId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - -@ExtraModel() -export class SyncMemoryAssetDeleteV1 { - @ApiProperty({ description: 'Memory ID' }) - memoryId!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; -} - -@ExtraModel() -export class SyncStackV1 { - @ApiProperty({ description: 'Stack ID' }) - id!: string; - @ApiProperty({ description: 'Created at' }) - createdAt!: Date; - @ApiProperty({ description: 'Updated at' }) - updatedAt!: Date; - @ApiProperty({ description: 'Primary asset ID' }) - primaryAssetId!: string; - @ApiProperty({ description: 'Owner ID' }) - ownerId!: string; -} - -@ExtraModel() -export class SyncStackDeleteV1 { - @ApiProperty({ description: 'Stack ID' }) - stackId!: string; -} - -@ExtraModel() -export class SyncPersonV1 { - @ApiProperty({ description: 'Person ID' }) - id!: string; - @ApiProperty({ description: 'Created at' }) - createdAt!: Date; - @ApiProperty({ description: 'Updated at' }) - updatedAt!: Date; - @ApiProperty({ description: 'Owner ID' }) - ownerId!: string; - @ApiProperty({ description: 'Person name' }) - name!: string; - @ApiProperty({ description: 'Birth date' }) - birthDate!: Date | null; - @ApiProperty({ description: 'Is hidden' }) - isHidden!: boolean; - @ApiProperty({ description: 'Is favorite' }) - isFavorite!: boolean; - @ApiProperty({ description: 'Color' }) - color!: string | null; - @ApiProperty({ description: 'Face asset ID' }) - faceAssetId!: string | null; -} - -@ExtraModel() -export class SyncPersonDeleteV1 { - @ApiProperty({ description: 'Person ID' }) - personId!: string; -} - -@ExtraModel() -export class SyncAssetFaceV1 { - @ApiProperty({ description: 'Asset face ID' }) - id!: string; - @ApiProperty({ description: 'Asset ID' }) - assetId!: string; - @ApiProperty({ description: 'Person ID' }) - personId!: string | null; - @ApiProperty({ type: 'integer' }) - imageWidth!: number; - @ApiProperty({ type: 'integer' }) - imageHeight!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxX1!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxY1!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxX2!: number; - @ApiProperty({ type: 'integer' }) - boundingBoxY2!: number; - @ApiProperty({ description: 'Source type' }) - sourceType!: string; -} - -@ExtraModel() -export class SyncAssetFaceV2 extends SyncAssetFaceV1 { - @ApiProperty({ description: 'Face deleted at' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Is the face visible in the asset' }) - isVisible!: boolean; -} +class SyncAssetFaceV2 extends createZodDto(SyncAssetFaceV2Schema) {} export function syncAssetFaceV2ToV1(faceV2: SyncAssetFaceV2): SyncAssetFaceV1 { const { deletedAt: _, isVisible: __, ...faceV1 } = faceV2; return faceV1; } - @ExtraModel() -export class SyncAssetFaceDeleteV1 { - @ApiProperty({ description: 'Asset face ID' }) - assetFaceId!: string; -} - +class SyncAssetFaceDeleteV1 extends createZodDto(SyncAssetFaceDeleteV1Schema) {} @ExtraModel() -export class SyncUserMetadataV1 { - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey', description: 'User metadata key' }) - key!: UserMetadataKey; - @ApiProperty({ description: 'User metadata value' }) - value!: UserMetadata[UserMetadataKey]; -} - +class SyncUserMetadataV1 extends createZodDto(SyncUserMetadataV1Schema) {} @ExtraModel() -export class SyncUserMetadataDeleteV1 { - @ApiProperty({ description: 'User ID' }) - userId!: string; - @ValidateEnum({ enum: UserMetadataKey, name: 'UserMetadataKey', description: 'User metadata key' }) - key!: UserMetadataKey; -} - +class SyncUserMetadataDeleteV1 extends createZodDto(SyncUserMetadataDeleteV1Schema) {} @ExtraModel() -export class SyncAckV1 {} - +class SyncAckV1 extends createZodDto(SyncAckV1Schema) {} @ExtraModel() -export class SyncResetV1 {} - +class SyncResetV1 extends createZodDto(SyncResetV1Schema) {} @ExtraModel() -export class SyncCompleteV1 {} +class SyncCompleteV1 extends createZodDto(SyncCompleteV1Schema) {} export type SyncItem = { [SyncEntityType.AuthUserV1]: SyncAuthUserV1; @@ -541,35 +472,33 @@ export type SyncItem = { [SyncEntityType.SyncResetV1]: SyncResetV1; }; -export class SyncStreamDto { - @ValidateEnum({ enum: SyncRequestType, name: 'SyncRequestType', each: true, description: 'Sync request types' }) - types!: SyncRequestType[]; - - @ValidateBoolean({ optional: true, description: 'Reset sync state' }) - reset?: boolean; -} - -export class SyncAckDto { - @ValidateEnum({ enum: SyncEntityType, name: 'SyncEntityType', description: 'Sync entity type' }) - type!: SyncEntityType; - @ApiProperty({ description: 'Acknowledgment ID' }) - ack!: string; -} - -export class SyncAckSetDto { - @ApiProperty({ description: 'Acknowledgment IDs (max 1000)' }) - @ArrayMaxSize(1000) - @IsString({ each: true }) - acks!: string[]; -} - -export class SyncAckDeleteDto { - @ValidateEnum({ - enum: SyncEntityType, - name: 'SyncEntityType', - optional: true, - each: true, - description: 'Sync entity types to delete acks for', +const SyncStreamSchema = z + .object({ + types: z.array(SyncRequestTypeSchema).describe('Sync request types'), + reset: z.boolean().optional().describe('Reset sync state'), }) - types?: SyncEntityType[]; -} + .meta({ id: 'SyncStreamDto' }); + +const SyncAckSchema = z + .object({ + type: SyncEntityTypeSchema, + ack: z.string().describe('Acknowledgment ID'), + }) + .meta({ id: 'SyncAckDto' }); + +const SyncAckSetSchema = z + .object({ + acks: z.array(z.string()).max(1000).describe('Acknowledgment IDs (max 1000)'), + }) + .meta({ id: 'SyncAckSetDto' }); + +const SyncAckDeleteSchema = z + .object({ + types: z.array(SyncEntityTypeSchema).optional().describe('Sync entity types to delete acks for'), + }) + .meta({ id: 'SyncAckDeleteDto' }); + +export class SyncStreamDto extends createZodDto(SyncStreamSchema) {} +export class SyncAckDto extends createZodDto(SyncAckSchema) {} +export class SyncAckSetDto extends createZodDto(SyncAckSetSchema) {} +export class SyncAckDeleteDto extends createZodDto(SyncAckDeleteSchema) {} diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts index a214dbc46715..b5222fd88390 100644 --- a/server/src/dtos/system-config.dto.ts +++ b/server/src/dtos/system-config.dto.ts @@ -1,863 +1,374 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Transform, Type } from 'class-transformer'; -import { - ArrayMinSize, - IsInt, - IsNotEmpty, - IsNumber, - IsObject, - IsPositive, - IsString, - IsUrl, - Max, - Min, - ValidateIf, - ValidateNested, -} from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { SystemConfig } from 'src/config'; -import { CLIPConfig, DuplicateDetectionConfig, FacialRecognitionConfig, OcrConfig } from 'src/dtos/model-config.dto'; +import { + CLIPConfigSchema, + DuplicateDetectionConfigSchema, + FacialRecognitionConfigSchema, + OcrConfigSchema, +} from 'src/dtos/model-config.dto'; import { AudioCodec, - CQMode, - Colorspace, - ImageFormat, - LogLevel, - OAuthTokenEndpointAuthMethod, - QueueName, - ToneMapping, - TranscodeHardwareAcceleration, - TranscodePolicy, - VideoCodec, - VideoContainer, + AudioCodecSchema, + ColorspaceSchema, + CQModeSchema, + ImageFormatSchema, + LogLevelSchema, + OAuthTokenEndpointAuthMethodSchema, + ToneMappingSchema, + TranscodeHardwareAccelerationSchema, + TranscodePolicySchema, + VideoCodecSchema, + VideoContainerSchema, } from 'src/enum'; -import { ConcurrentQueueName } from 'src/types'; -import { IsCronExpression, IsDateStringFormat, Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; +import { isValidTime } from 'src/validation'; +import z from 'zod'; -const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled; -const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled; -const isOAuthOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled; -const isEmailNotificationEnabled = (config: SystemConfigSmtpDto) => config.enabled; -const isDatabaseBackupEnabled = (config: DatabaseBackupConfig) => config.enabled; +/** Coerces 'true'/'false' strings to boolean, but also allows booleans. */ +const configBool = z + .preprocess((val) => { + if (val === 'true') { + return true; + } + if (val === 'false') { + return false; + } + return val; + }, z.boolean()) + .meta({ type: 'boolean' }); -export class DatabaseBackupConfig { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; +const JobSettingsSchema = z + .object({ + concurrency: z.int().min(1).describe('Concurrency'), + }) + .meta({ id: 'JobSettingsDto' }); - @ValidateIf(isDatabaseBackupEnabled) - @IsNotEmpty() - @IsCronExpression() - @IsString() - @ApiProperty({ description: 'Cron expression' }) - cronExpression!: string; +const cronExpressionSchema = z + .string() + .regex(/(((\d+,)+\d+|(\d+(\/|-)\d+)|\d+|\*) ?){5,7}/, 'Invalid cron expression') + .describe('Cron expression'); - @IsInt() - @IsPositive() - @IsNotEmpty() - @ApiProperty({ description: 'Keep last amount' }) - keepLastAmount!: number; -} +const DatabaseBackupSchema = z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + keepLastAmount: z.number().min(1).describe('Keep last amount'), + }) + .meta({ id: 'DatabaseBackupConfig' }); -export class SystemConfigBackupsDto { - @Type(() => DatabaseBackupConfig) - @ValidateNested() - @IsObject() - database!: DatabaseBackupConfig; -} +const SystemConfigBackupsSchema = z.object({ database: DatabaseBackupSchema }).meta({ id: 'SystemConfigBackupsDto' }); -export class SystemConfigFFmpegDto { - @IsInt() - @Min(0) - @Max(51) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'CRF' }) - crf!: number; +const SystemConfigFFmpegSchema = z + .object({ + crf: z.coerce.number().int().min(0).max(51).describe('CRF'), + threads: z.coerce.number().int().min(0).describe('Threads'), + preset: z.string().describe('Preset'), + targetVideoCodec: VideoCodecSchema, + acceptedVideoCodecs: z.array(VideoCodecSchema).describe('Accepted video codecs'), + targetAudioCodec: AudioCodecSchema, + acceptedAudioCodecs: z + .array(AudioCodecSchema) + .transform((value): AudioCodec[] => value.map((v) => (v === AudioCodec.Libopus ? AudioCodec.Opus : v))) + .describe('Accepted audio codecs'), + acceptedContainers: z.array(VideoContainerSchema).describe('Accepted containers'), + targetResolution: z.string().describe('Target resolution'), + maxBitrate: z.string().describe('Max bitrate'), + bframes: z.coerce.number().int().min(-1).max(16).describe('B-frames'), + refs: z.coerce.number().int().min(0).max(6).describe('References'), + gopSize: z.coerce.number().int().min(0).describe('GOP size'), + temporalAQ: configBool.describe('Temporal AQ'), + cqMode: CQModeSchema, + twoPass: configBool.describe('Two pass'), + preferredHwDevice: z.string().describe('Preferred hardware device'), + transcode: TranscodePolicySchema, + accel: TranscodeHardwareAccelerationSchema, + accelDecode: configBool.describe('Accelerated decode'), + tonemap: ToneMappingSchema, + }) + .meta({ id: 'SystemConfigFFmpegDto' }); - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Threads' }) - threads!: number; +const SystemConfigJobSchema = z + .object({ + thumbnailGeneration: JobSettingsSchema, + metadataExtraction: JobSettingsSchema, + videoConversion: JobSettingsSchema, + faceDetection: JobSettingsSchema, + smartSearch: JobSettingsSchema, + backgroundTask: JobSettingsSchema, + migration: JobSettingsSchema, + search: JobSettingsSchema, + sidecar: JobSettingsSchema, + library: JobSettingsSchema, + notifications: JobSettingsSchema, + ocr: JobSettingsSchema, + workflow: JobSettingsSchema, + editor: JobSettingsSchema, + }) + .meta({ id: 'SystemConfigJobDto' }); - @IsString() - @ApiProperty({ description: 'Preset' }) - preset!: string; +const SystemConfigLibraryScanSchema = z + .object({ + enabled: configBool.describe('Enabled'), + cronExpression: cronExpressionSchema, + }) + .meta({ id: 'SystemConfigLibraryScanDto' }); - @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', description: 'Target video codec' }) - targetVideoCodec!: VideoCodec; +const SystemConfigLibraryWatchSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigLibraryWatchDto' }); - @ValidateEnum({ enum: VideoCodec, name: 'VideoCodec', each: true, description: 'Accepted video codecs' }) - acceptedVideoCodecs!: VideoCodec[]; +const SystemConfigLibrarySchema = z + .object({ scan: SystemConfigLibraryScanSchema, watch: SystemConfigLibraryWatchSchema }) + .meta({ id: 'SystemConfigLibraryDto' }); - @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', description: 'Target audio codec' }) - targetAudioCodec!: AudioCodec; +const SystemConfigLoggingSchema = z + .object({ + enabled: configBool.describe('Enabled'), + level: LogLevelSchema, + }) + .meta({ id: 'SystemConfigLoggingDto' }); - @ValidateEnum({ enum: AudioCodec, name: 'AudioCodec', each: true, description: 'Accepted audio codecs' }) - @Transform(({ value }) => { - if (Array.isArray(value)) { - const libopusIndex = value.indexOf('libopus'); - if (libopusIndex !== -1) { - value[libopusIndex] = 'opus'; - } +const MachineLearningAvailabilityChecksSchema = z + .object({ + enabled: configBool.describe('Enabled'), + timeout: z.number(), + interval: z.number(), + }) + .meta({ id: 'MachineLearningAvailabilityChecksDto' }); + +const SystemConfigMachineLearningSchema = z + .object({ + enabled: configBool.describe('Enabled'), + urls: z.array(z.string()).min(1).describe('ML service URLs'), + availabilityChecks: MachineLearningAvailabilityChecksSchema, + clip: CLIPConfigSchema, + duplicateDetection: DuplicateDetectionConfigSchema, + facialRecognition: FacialRecognitionConfigSchema, + ocr: OcrConfigSchema, + }) + .meta({ id: 'SystemConfigMachineLearningDto' }); + +const SystemConfigMapSchema = z + .object({ + enabled: configBool.describe('Enabled'), + lightStyle: z.url().describe('Light map style URL'), + darkStyle: z.url().describe('Dark map style URL'), + }) + .meta({ id: 'SystemConfigMapDto' }); + +const SystemConfigNewVersionCheckSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigNewVersionCheckDto' }); + +const SystemConfigNightlyTasksSchema = z + .object({ + startTime: isValidTime.describe('Start time'), + databaseCleanup: configBool.describe('Database cleanup'), + missingThumbnails: configBool.describe('Missing thumbnails'), + clusterNewFaces: configBool.describe('Cluster new faces'), + generateMemories: configBool.describe('Generate memories'), + syncQuotaUsage: configBool.describe('Sync quota usage'), + }) + .meta({ id: 'SystemConfigNightlyTasksDto' }); + +const SystemConfigOAuthSchema = z + .object({ + autoLaunch: configBool.describe('Auto launch'), + autoRegister: configBool.describe('Auto register'), + buttonText: z.string().describe('Button text'), + clientId: z.string().describe('Client ID'), + clientSecret: z.string().describe('Client secret'), + tokenEndpointAuthMethod: OAuthTokenEndpointAuthMethodSchema, + timeout: z.int().min(1).describe('Timeout'), + defaultStorageQuota: z.number().min(0).nullable().describe('Default storage quota'), + enabled: configBool.describe('Enabled'), + issuerUrl: z.string().describe('Issuer URL'), + scope: z.string().describe('Scope'), + signingAlgorithm: z.string().describe('Signing algorithm'), + profileSigningAlgorithm: z.string().describe('Profile signing algorithm'), + storageLabelClaim: z.string().describe('Storage label claim'), + storageQuotaClaim: z.string().describe('Storage quota claim'), + roleClaim: z.string().describe('Role claim'), + mobileOverrideEnabled: configBool.describe('Mobile override enabled'), + mobileRedirectUri: z.string().describe('Mobile redirect URI (set to empty string to disable)'), + }) + .transform((value, ctx) => { + if (!value.mobileOverrideEnabled || value.mobileRedirectUri === '') { + return value; + } + + if (!z.url().safeParse(value.mobileRedirectUri).success) { + ctx.issues.push({ + code: 'custom', + message: 'Mobile redirect URI must be an empty string or a valid URL', + input: value.mobileRedirectUri, + }); + return z.NEVER; } return value; }) - acceptedAudioCodecs!: AudioCodec[]; + .meta({ + id: 'SystemConfigOAuthDto', + }); - @ValidateEnum({ enum: VideoContainer, name: 'VideoContainer', each: true, description: 'Accepted containers' }) - acceptedContainers!: VideoContainer[]; +const SystemConfigPasswordLoginSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigPasswordLoginDto' }); - @IsString() - @ApiProperty({ description: 'Target resolution' }) - targetResolution!: string; +const SystemConfigReverseGeocodingSchema = z + .object({ enabled: configBool.describe('Enabled') }) + .meta({ id: 'SystemConfigReverseGeocodingDto' }); - @IsString() - @ApiProperty({ description: 'Max bitrate' }) - maxBitrate!: string; +const SystemConfigFacesSchema = z + .object({ import: configBool.describe('Import') }) + .meta({ id: 'SystemConfigFacesDto' }); +const SystemConfigMetadataSchema = z.object({ faces: SystemConfigFacesSchema }).meta({ id: 'SystemConfigMetadataDto' }); - @IsInt() - @Min(-1) - @Max(16) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'B-frames' }) - bframes!: number; - - @IsInt() - @Min(0) - @Max(6) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'References' }) - refs!: number; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'GOP size' }) - gopSize!: number; - - @ValidateBoolean({ description: 'Temporal AQ' }) - temporalAQ!: boolean; - - @ValidateEnum({ enum: CQMode, name: 'CQMode', description: 'CQ mode' }) - cqMode!: CQMode; - - @ValidateBoolean({ description: 'Two pass' }) - twoPass!: boolean; - - @ApiProperty({ description: 'Preferred hardware device' }) - @IsString() - preferredHwDevice!: string; - - @ValidateEnum({ enum: TranscodePolicy, name: 'TranscodePolicy', description: 'Transcode policy' }) - transcode!: TranscodePolicy; - - @ValidateEnum({ - enum: TranscodeHardwareAcceleration, - name: 'TranscodeHWAccel', - description: 'Transcode hardware acceleration', +const SystemConfigServerSchema = z + .object({ + externalDomain: z + .string() + .refine((url) => url.length === 0 || z.url().safeParse(url).success, { + error: 'External domain must be an empty string or a valid URL', + }) + .describe('External domain'), + loginPageMessage: z.string().describe('Login page message'), + publicUsers: configBool.describe('Public users'), }) - accel!: TranscodeHardwareAcceleration; + .meta({ id: 'SystemConfigServerDto' }); - @ValidateBoolean({ description: 'Accelerated decode' }) - accelDecode!: boolean; - - @ValidateEnum({ enum: ToneMapping, name: 'ToneMapping', description: 'Tone mapping' }) - tonemap!: ToneMapping; -} - -class JobSettingsDto { - @IsInt() - @IsPositive() - @ApiProperty({ type: 'integer', description: 'Concurrency' }) - concurrency!: number; -} - -class SystemConfigJobDto implements Record { - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.ThumbnailGeneration]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.MetadataExtraction]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.VideoConversion]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.SmartSearch]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Migration]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.BackgroundTask]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Search]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.FaceDetection]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Ocr]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Sidecar]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Library]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Notification]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Workflow]!: JobSettingsDto; - - @ApiProperty({ type: JobSettingsDto, description: undefined }) - @ValidateNested() - @IsObject() - @Type(() => JobSettingsDto) - [QueueName.Editor]!: JobSettingsDto; -} - -class SystemConfigLibraryScanDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateIf(isLibraryScanEnabled) - @IsNotEmpty() - @IsCronExpression() - @IsString() - cronExpression!: string; -} - -class SystemConfigLibraryWatchDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigLibraryDto { - @Type(() => SystemConfigLibraryScanDto) - @ValidateNested() - @IsObject() - scan!: SystemConfigLibraryScanDto; - - @Type(() => SystemConfigLibraryWatchDto) - @ValidateNested() - @IsObject() - watch!: SystemConfigLibraryWatchDto; -} - -class SystemConfigLoggingDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateEnum({ enum: LogLevel, name: 'LogLevel' }) - level!: LogLevel; -} - -class MachineLearningAvailabilityChecksDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsInt() - timeout!: number; - - @IsInt() - interval!: number; -} - -class SystemConfigMachineLearningDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsUrl({ require_tld: false, allow_underscores: true }, { each: true }) - @ArrayMinSize(1) - @ValidateIf((dto) => dto.enabled) - @ApiProperty({ type: 'array', items: { type: 'string', format: 'uri' }, minItems: 1 }) - urls!: string[]; - - @Type(() => MachineLearningAvailabilityChecksDto) - @ValidateNested() - @IsObject() - availabilityChecks!: MachineLearningAvailabilityChecksDto; - - @Type(() => CLIPConfig) - @ValidateNested() - @IsObject() - clip!: CLIPConfig; - - @Type(() => DuplicateDetectionConfig) - @ValidateNested() - @IsObject() - duplicateDetection!: DuplicateDetectionConfig; - - @Type(() => FacialRecognitionConfig) - @ValidateNested() - @IsObject() - facialRecognition!: FacialRecognitionConfig; - - @Type(() => OcrConfig) - @ValidateNested() - @IsObject() - ocr!: OcrConfig; -} - -enum MapTheme { - LIGHT = 'light', - DARK = 'dark', -} - -export class MapThemeDto { - @ValidateEnum({ enum: MapTheme, name: 'MapTheme' }) - theme!: MapTheme; -} - -class SystemConfigMapDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsNotEmpty() - @IsUrl() - lightStyle!: string; - - @IsNotEmpty() - @IsUrl() - darkStyle!: string; -} - -class SystemConfigNewVersionCheckDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigNightlyTasksDto { - @IsDateStringFormat('HH:mm', { message: 'startTime must be in HH:mm format' }) - startTime!: string; - - @ValidateBoolean({ description: 'Database cleanup' }) - databaseCleanup!: boolean; - - @ValidateBoolean({ description: 'Missing thumbnails' }) - missingThumbnails!: boolean; - - @ValidateBoolean({ description: 'Cluster new faces' }) - clusterNewFaces!: boolean; - - @ValidateBoolean({ description: 'Generate memories' }) - generateMemories!: boolean; - - @ValidateBoolean({ description: 'Sync quota usage' }) - syncQuotaUsage!: boolean; -} - -class SystemConfigOAuthDto { - @ValidateBoolean({ description: 'Auto launch' }) - autoLaunch!: boolean; - - @ValidateBoolean({ description: 'Auto register' }) - autoRegister!: boolean; - - @IsString() - @ApiProperty({ description: 'Button text' }) - buttonText!: string; - - @ValidateIf(isOAuthEnabled) - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Client ID' }) - clientId!: string; - - @ValidateIf(isOAuthEnabled) - @IsString() - @ApiProperty({ description: 'Client secret' }) - clientSecret!: string; - - @ValidateEnum({ - enum: OAuthTokenEndpointAuthMethod, - name: 'OAuthTokenEndpointAuthMethod', - description: 'Token endpoint auth method', +const SystemConfigSmtpTransportSchema = z + .object({ + ignoreCert: configBool.describe('Whether to ignore SSL certificate errors'), + host: z.string().describe('SMTP server hostname'), + port: z.number().min(0).max(65_535).describe('SMTP server port'), + secure: configBool.describe('Whether to use secure connection (TLS/SSL)'), + username: z.string().describe('SMTP username'), + password: z.string().describe('SMTP password'), }) - tokenEndpointAuthMethod!: OAuthTokenEndpointAuthMethod; - - @IsInt() - @IsPositive() - @Optional() - @ApiProperty({ type: 'integer', description: 'Timeout' }) - timeout!: number; - - @IsNumber() - @Min(0) - @Optional({ nullable: true }) - @ApiProperty({ type: 'integer', format: 'int64', description: 'Default storage quota' }) - defaultStorageQuota!: number | null; - - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateIf(isOAuthEnabled) - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Issuer URL' }) - issuerUrl!: string; - - @ValidateBoolean({ description: 'Mobile override enabled' }) - mobileOverrideEnabled!: boolean; - - @ValidateIf(isOAuthOverrideEnabled) - @IsUrl() - @ApiProperty({ description: 'Mobile redirect URI' }) - mobileRedirectUri!: string; - - @IsString() - @ApiProperty({ description: 'Scope' }) - scope!: string; - - @IsString() - @IsNotEmpty() - signingAlgorithm!: string; - - @IsString() - @IsNotEmpty() - @ApiProperty({ description: 'Profile signing algorithm' }) - profileSigningAlgorithm!: string; - - @IsString() - @ApiProperty({ description: 'Storage label claim' }) - storageLabelClaim!: string; - - @IsString() - @ApiProperty({ description: 'Storage quota claim' }) - storageQuotaClaim!: string; - - @IsString() - @ApiProperty({ description: 'Role claim' }) - roleClaim!: string; -} - -class SystemConfigPasswordLoginDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigReverseGeocodingDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; -} - -class SystemConfigFacesDto { - @ValidateBoolean({ description: 'Import' }) - import!: boolean; -} - -class SystemConfigMetadataDto { - @Type(() => SystemConfigFacesDto) - @ValidateNested() - @IsObject() - faces!: SystemConfigFacesDto; -} - -class SystemConfigServerDto { - @ValidateIf((_, value: string) => value !== '') - @IsUrl({ require_tld: false, require_protocol: true, protocols: ['http', 'https'] }) - @ApiProperty({ description: 'External domain' }) - externalDomain!: string; - - @IsString() - @ApiProperty({ description: 'Login page message' }) - loginPageMessage!: string; - - @ValidateBoolean({ description: 'Public users' }) - publicUsers!: boolean; -} - -class SystemConfigSmtpTransportDto { - @ValidateBoolean({ description: 'Whether to ignore SSL certificate errors' }) - ignoreCert!: boolean; - - @ApiProperty({ description: 'SMTP server hostname' }) - @IsNotEmpty() - @IsString() - host!: string; - - @ApiProperty({ description: 'SMTP server port', type: Number, minimum: 0, maximum: 65_535 }) - @IsNumber() - @Min(0) - @Max(65_535) - port!: number; - - @ValidateBoolean({ description: 'Whether to use secure connection (TLS/SSL)' }) - secure!: boolean; - - @ApiProperty({ description: 'SMTP username' }) - @IsString() - username!: string; - - @ApiProperty({ description: 'SMTP password' }) - @IsString() - password!: string; -} - -export class SystemConfigSmtpDto { - @ValidateBoolean({ description: 'Whether SMTP email notifications are enabled' }) - enabled!: boolean; - - @ApiProperty({ description: 'Email address to send from' }) - @ValidateIf(isEmailNotificationEnabled) - @IsNotEmpty() - @IsString() - @IsNotEmpty() - from!: string; - - @ApiProperty({ description: 'Email address for replies' }) - @IsString() - replyTo!: string; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @ValidateIf(isEmailNotificationEnabled) - @Type(() => SystemConfigSmtpTransportDto) - @ValidateNested() - @IsObject() - transport!: SystemConfigSmtpTransportDto; -} - -class SystemConfigNotificationsDto { - @Type(() => SystemConfigSmtpDto) - @ValidateNested() - @IsObject() - smtp!: SystemConfigSmtpDto; -} - -class SystemConfigTemplateEmailsDto { - @IsString() - albumInviteTemplate!: string; - - @IsString() - welcomeTemplate!: string; - - @IsString() - albumUpdateTemplate!: string; -} - -class SystemConfigTemplatesDto { - @Type(() => SystemConfigTemplateEmailsDto) - @ValidateNested() - @IsObject() - email!: SystemConfigTemplateEmailsDto; -} - -class SystemConfigStorageTemplateDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateBoolean({ description: 'Hash verification enabled' }) - hashVerificationEnabled!: boolean; - - @IsNotEmpty() - @IsString() - @ApiProperty({ description: 'Template' }) - template!: string; -} - -export class SystemConfigTemplateStorageOptionDto { - @ApiProperty({ description: 'Available year format options for storage template' }) - yearOptions!: string[]; - @ApiProperty({ description: 'Available month format options for storage template' }) - monthOptions!: string[]; - @ApiProperty({ description: 'Available week format options for storage template' }) - weekOptions!: string[]; - @ApiProperty({ description: 'Available day format options for storage template' }) - dayOptions!: string[]; - @ApiProperty({ description: 'Available hour format options for storage template' }) - hourOptions!: string[]; - @ApiProperty({ description: 'Available minute format options for storage template' }) - minuteOptions!: string[]; - @ApiProperty({ description: 'Available second format options for storage template' }) - secondOptions!: string[]; - @ApiProperty({ description: 'Available preset template options' }) - presetOptions!: string[]; -} - -export class SystemConfigThemeDto { - @ApiProperty({ description: 'Custom CSS for theming' }) - @IsString() - customCss!: string; -} - -class SystemConfigGeneratedImageDto { - @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat', description: 'Image format' }) - format!: ImageFormat; - - @IsInt() - @Min(1) - @Max(100) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Quality' }) - quality!: number; - - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Size' }) - size!: number; - - @ValidateBoolean({ optional: true, default: false }) - progressive?: boolean; -} - -class SystemConfigGeneratedFullsizeImageDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @ValidateEnum({ enum: ImageFormat, name: 'ImageFormat', description: 'Image format' }) - format!: ImageFormat; - - @IsInt() - @Min(1) - @Max(100) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Quality' }) - quality!: number; - - @ValidateBoolean({ optional: true, default: false, description: 'Progressive' }) - progressive?: boolean; -} - -export class SystemConfigImageDto { - @Type(() => SystemConfigGeneratedImageDto) - @ValidateNested() - @IsObject() - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - thumbnail!: SystemConfigGeneratedImageDto; - - @Type(() => SystemConfigGeneratedImageDto) - @ValidateNested() - @IsObject() - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - preview!: SystemConfigGeneratedImageDto; - - @Type(() => SystemConfigGeneratedFullsizeImageDto) - @ValidateNested() - @IsObject() - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - fullsize!: SystemConfigGeneratedFullsizeImageDto; - - @ValidateEnum({ enum: Colorspace, name: 'Colorspace', description: 'Colorspace' }) - colorspace!: Colorspace; - - @ValidateBoolean({ description: 'Extract embedded' }) - extractEmbedded!: boolean; -} - -class SystemConfigTrashDto { - @ValidateBoolean({ description: 'Enabled' }) - enabled!: boolean; - - @IsInt() - @Min(0) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Days' }) - days!: number; -} - -class SystemConfigUserDto { - @IsInt() - @Min(1) - @Type(() => Number) - @ApiProperty({ type: 'integer', description: 'Delete delay' }) - deleteDelay!: number; -} - -export class SystemConfigDto implements SystemConfig { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigBackupsDto) - @ValidateNested() - @IsObject() - backup!: SystemConfigBackupsDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigFFmpegDto) - @ValidateNested() - @IsObject() - ffmpeg!: SystemConfigFFmpegDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigLoggingDto) - @ValidateNested() - @IsObject() - logging!: SystemConfigLoggingDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigMachineLearningDto) - @ValidateNested() - @IsObject() - machineLearning!: SystemConfigMachineLearningDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigMapDto) - @ValidateNested() - @IsObject() - map!: SystemConfigMapDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigNewVersionCheckDto) - @ValidateNested() - @IsObject() - newVersionCheck!: SystemConfigNewVersionCheckDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigNightlyTasksDto) - @ValidateNested() - @IsObject() - nightlyTasks!: SystemConfigNightlyTasksDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigOAuthDto) - @ValidateNested() - @IsObject() - oauth!: SystemConfigOAuthDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigPasswordLoginDto) - @ValidateNested() - @IsObject() - passwordLogin!: SystemConfigPasswordLoginDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigReverseGeocodingDto) - @ValidateNested() - @IsObject() - reverseGeocoding!: SystemConfigReverseGeocodingDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigMetadataDto) - @ValidateNested() - @IsObject() - metadata!: SystemConfigMetadataDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigStorageTemplateDto) - @ValidateNested() - @IsObject() - storageTemplate!: SystemConfigStorageTemplateDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigJobDto) - @ValidateNested() - @IsObject() - job!: SystemConfigJobDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigImageDto) - @ValidateNested() - @IsObject() - image!: SystemConfigImageDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigTrashDto) - @ValidateNested() - @IsObject() - trash!: SystemConfigTrashDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigThemeDto) - @ValidateNested() - @IsObject() - theme!: SystemConfigThemeDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigLibraryDto) - @ValidateNested() - @IsObject() - library!: SystemConfigLibraryDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigNotificationsDto) - @ValidateNested() - @IsObject() - notifications!: SystemConfigNotificationsDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigTemplatesDto) - @ValidateNested() - @IsObject() - templates!: SystemConfigTemplatesDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigServerDto) - @ValidateNested() - @IsObject() - server!: SystemConfigServerDto; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - @Type(() => SystemConfigUserDto) - @ValidateNested() - @IsObject() - user!: SystemConfigUserDto; -} + .meta({ id: 'SystemConfigSmtpTransportDto' }); + +const SystemConfigSmtpSchema = z + .object({ + enabled: configBool.describe('Whether SMTP email notifications are enabled'), + from: z.string().describe('Email address to send from'), + replyTo: z.string().describe('Email address for replies'), + transport: SystemConfigSmtpTransportSchema, + }) + .meta({ id: 'SystemConfigSmtpDto' }); + +const SystemConfigNotificationsSchema = z + .object({ smtp: SystemConfigSmtpSchema }) + .meta({ id: 'SystemConfigNotificationsDto' }); + +const SystemConfigTemplateEmailsSchema = z + .object({ + albumInviteTemplate: z.string().describe('Album invite template'), + welcomeTemplate: z.string().describe('Welcome template'), + albumUpdateTemplate: z.string().describe('Album update template'), + }) + .meta({ id: 'SystemConfigTemplateEmailsDto' }); +const SystemConfigTemplatesSchema = z + .object({ email: SystemConfigTemplateEmailsSchema }) + .meta({ id: 'SystemConfigTemplatesDto' }); + +const SystemConfigStorageTemplateSchema = z + .object({ + enabled: configBool.describe('Enabled'), + hashVerificationEnabled: configBool.describe('Hash verification enabled'), + template: z.string().describe('Template'), + }) + .meta({ id: 'SystemConfigStorageTemplateDto' }); + +const SystemConfigTemplateStorageOptionSchema = z + .object({ + yearOptions: z.array(z.string()).describe('Available year format options for storage template'), + monthOptions: z.array(z.string()).describe('Available month format options for storage template'), + weekOptions: z.array(z.string()).describe('Available week format options for storage template'), + dayOptions: z.array(z.string()).describe('Available day format options for storage template'), + hourOptions: z.array(z.string()).describe('Available hour format options for storage template'), + minuteOptions: z.array(z.string()).describe('Available minute format options for storage template'), + secondOptions: z.array(z.string()).describe('Available second format options for storage template'), + presetOptions: z.array(z.string()).describe('Available preset template options'), + }) + .meta({ id: 'SystemConfigTemplateStorageOptionDto' }); + +const SystemConfigThemeSchema = z + .object({ customCss: z.string().describe('Custom CSS for theming') }) + .meta({ id: 'SystemConfigThemeDto' }); + +const SystemConfigGeneratedImageSchema = z + .object({ + format: ImageFormatSchema, + quality: z.int().min(1).max(100).describe('Quality'), + size: z.int().min(1).describe('Size'), + progressive: configBool.default(false).optional().describe('Progressive'), + }) + .meta({ id: 'SystemConfigGeneratedImageDto' }); + +const SystemConfigGeneratedFullsizeImageSchema = z + .object({ + enabled: configBool.describe('Enabled'), + format: ImageFormatSchema, + quality: z.int().min(1).max(100).describe('Quality'), + progressive: configBool.default(false).optional().describe('Progressive'), + }) + .meta({ id: 'SystemConfigGeneratedFullsizeImageDto' }); + +const SystemConfigImageSchema = z + .object({ + thumbnail: SystemConfigGeneratedImageSchema, + preview: SystemConfigGeneratedImageSchema, + fullsize: SystemConfigGeneratedFullsizeImageSchema, + colorspace: ColorspaceSchema, + extractEmbedded: configBool.describe('Extract embedded'), + }) + .meta({ id: 'SystemConfigImageDto' }); + +const SystemConfigTrashSchema = z + .object({ + enabled: configBool.describe('Enabled'), + days: z.int().min(0).describe('Days'), + }) + .meta({ id: 'SystemConfigTrashDto' }); + +const SystemConfigUserSchema = z + .object({ + deleteDelay: z.int().min(1).describe('Delete delay'), + }) + .meta({ id: 'SystemConfigUserDto' }); + +export const SystemConfigSchema = z + .object({ + backup: SystemConfigBackupsSchema, + ffmpeg: SystemConfigFFmpegSchema, + logging: SystemConfigLoggingSchema, + machineLearning: SystemConfigMachineLearningSchema, + map: SystemConfigMapSchema, + newVersionCheck: SystemConfigNewVersionCheckSchema, + nightlyTasks: SystemConfigNightlyTasksSchema, + oauth: SystemConfigOAuthSchema, + passwordLogin: SystemConfigPasswordLoginSchema, + reverseGeocoding: SystemConfigReverseGeocodingSchema, + metadata: SystemConfigMetadataSchema, + storageTemplate: SystemConfigStorageTemplateSchema, + job: SystemConfigJobSchema, + image: SystemConfigImageSchema, + trash: SystemConfigTrashSchema, + theme: SystemConfigThemeSchema, + library: SystemConfigLibrarySchema, + notifications: SystemConfigNotificationsSchema, + templates: SystemConfigTemplatesSchema, + server: SystemConfigServerSchema, + user: SystemConfigUserSchema, + }) + .describe('System configuration') + .meta({ id: 'SystemConfigDto' }); + +export class SystemConfigFFmpegDto extends createZodDto(SystemConfigFFmpegSchema) {} +export class SystemConfigSmtpDto extends createZodDto(SystemConfigSmtpSchema) {} +export class SystemConfigTemplateStorageOptionDto extends createZodDto(SystemConfigTemplateStorageOptionSchema) {} +export class SystemConfigDto extends createZodDto(SystemConfigSchema) {} export function mapConfig(config: SystemConfig): SystemConfigDto { return config; diff --git a/server/src/dtos/system-metadata.dto.ts b/server/src/dtos/system-metadata.dto.ts index 0a4d55c9708c..676a06f7748c 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 ea85ea71f336..67dbca9914fa 100644 --- a/server/src/dtos/tag.dto.ts +++ b/server/src/dtos/tag.dto.ts @@ -1,68 +1,63 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { IsHexColor, IsNotEmpty, IsString } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { Tag } from 'src/database'; import { MaybeDehydrated } from 'src/types'; import { asDateString } from 'src/utils/date'; -import { Optional, ValidateHexColor, ValidateUUID } from 'src/validation'; +import { emptyStringToNull, hexColor } from 'src/validation'; +import z from 'zod'; -export class TagCreateDto { - @ApiProperty({ description: 'Tag name' }) - @IsString() - @IsNotEmpty() - name!: string; +const TagCreateSchema = z + .object({ + name: z.string().describe('Tag name'), + parentId: z.uuidv4().nullish().describe('Parent tag ID'), + color: emptyStringToNull(hexColor.nullable()).optional().describe('Tag color (hex)'), + }) + .meta({ id: 'TagCreateDto' }); - @ValidateUUID({ nullable: true, optional: true, description: 'Parent tag ID' }) - parentId?: string | null; +const TagUpdateSchema = z + .object({ + color: emptyStringToNull(hexColor.nullable()).optional().describe('Tag color (hex)'), + }) + .meta({ id: 'TagUpdateDto' }); - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - @IsHexColor() - @Optional({ nullable: true, emptyToNull: true }) - color?: string; -} +const TagUpsertSchema = z + .object({ + tags: z.array(z.string()).describe('Tag names to upsert'), + }) + .meta({ id: 'TagUpsertDto' }); -export class TagUpdateDto { - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - @Optional({ nullable: true, emptyToNull: true }) - @ValidateHexColor() - color?: string | null; -} +const TagBulkAssetsSchema = z + .object({ + tagIds: z.array(z.uuidv4()).describe('Tag IDs'), + assetIds: z.array(z.uuidv4()).describe('Asset IDs'), + }) + .meta({ id: 'TagBulkAssetsDto' }); -export class TagUpsertDto { - @ApiProperty({ description: 'Tag names to upsert' }) - @IsString({ each: true }) - @IsNotEmpty({ each: true }) - tags!: string[]; -} +const TagBulkAssetsResponseSchema = z + .object({ + count: z.int().describe('Number of assets tagged'), + }) + .meta({ id: 'TagBulkAssetsResponseDto' }); -export class TagBulkAssetsDto { - @ValidateUUID({ each: true, description: 'Tag IDs' }) - tagIds!: string[]; +export const TagResponseSchema = z + .object({ + id: z.string().describe('Tag ID'), + parentId: z.string().optional().describe('Parent tag ID'), + name: z.string().describe('Tag name'), + value: z.string().describe('Tag value (full path)'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + createdAt: z.string().meta({ format: 'date-time' }).describe('Creation date'), + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + updatedAt: z.string().meta({ format: 'date-time' }).describe('Last update date'), + color: z.string().optional().describe('Tag color (hex)'), + }) + .meta({ id: 'TagResponseDto' }); - @ValidateUUID({ each: true, description: 'Asset IDs' }) - assetIds!: string[]; -} - -export class TagBulkAssetsResponseDto { - @ApiProperty({ type: 'integer', description: 'Number of assets tagged' }) - count!: number; -} - -export class TagResponseDto { - @ApiProperty({ description: 'Tag ID' }) - id!: string; - @ApiPropertyOptional({ description: 'Parent tag ID' }) - parentId?: string; - @ApiProperty({ description: 'Tag name' }) - name!: string; - @ApiProperty({ description: 'Tag value (full path)' }) - value!: string; - @ApiProperty({ description: 'Creation date', format: 'date-time' }) - createdAt!: string; - @ApiProperty({ description: 'Last update date', format: 'date-time' }) - updatedAt!: string; - @ApiPropertyOptional({ description: 'Tag color (hex)' }) - color?: string; -} +export class TagCreateDto extends createZodDto(TagCreateSchema) {} +export class TagUpdateDto extends createZodDto(TagUpdateSchema) {} +export class TagUpsertDto extends createZodDto(TagUpsertSchema) {} +export class TagBulkAssetsDto extends createZodDto(TagBulkAssetsSchema) {} +export class TagBulkAssetsResponseDto extends createZodDto(TagBulkAssetsResponseSchema) {} +export class TagResponseDto extends createZodDto(TagResponseSchema) {} export function mapTag(entity: MaybeDehydrated): TagResponseDto { return { diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index 9ea9dc49ae22..af820e6868c9 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,230 +1,128 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString } from 'class-validator'; -import type { BBoxDto } from 'src/dtos/bbox.dto'; -import { AssetOrder, AssetVisibility } from 'src/enum'; -import { ValidateBBox } from 'src/utils/bbox'; -import { ValidateBoolean, ValidateEnum, ValidateUUID } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import { BBoxSchema } from 'src/dtos/bbox.dto'; +import { AssetOrderSchema, AssetVisibilitySchema } from 'src/enum'; +import { stringToBool } from 'src/validation'; +import z from 'zod'; -export class TimeBucketDto { - @ValidateUUID({ optional: true, description: 'Filter assets by specific user ID' }) - userId?: string; +const TimeBucketQueryBaseSchema = z + .object({ + userId: z.uuidv4().optional().describe('Filter assets by specific user ID'), + albumId: z.uuidv4().optional().describe('Filter assets belonging to a specific album'), + personId: z.uuidv4().optional().describe('Filter assets containing a specific person (face recognition)'), + tagId: z.uuidv4().optional().describe('Filter assets with a specific tag'), + isFavorite: stringToBool + .optional() + .describe('Filter by favorite status (true for favorites only, false for non-favorites only)'), + isTrashed: stringToBool + .optional() + .describe('Filter by trash status (true for trashed assets only, false for non-trashed only)'), + withStacked: stringToBool + .optional() + .describe('Include stacked assets in the response. When true, only primary assets from stacks are returned.'), + withPartners: stringToBool.optional().describe('Include assets shared by partners'), + order: AssetOrderSchema.optional().describe( + 'Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)', + ), + visibility: AssetVisibilitySchema.optional().describe( + 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)', + ), + withCoordinates: stringToBool.optional().describe('Include location data in the response'), + key: z.string().optional(), + slug: z.string().optional(), + bbox: z + .string() + .transform((value, ctx) => { + const parts = value.split(','); + if (parts.length !== 4) { + ctx.issues.push({ + code: 'custom', + message: 'bbox must have 4 comma-separated numbers: west,south,east,north', + input: value, + }); + return z.NEVER; + } - @ValidateUUID({ optional: true, description: 'Filter assets belonging to a specific album' }) - albumId?: string; + const [west, south, east, north] = parts.map(Number); + if ([west, south, east, north].some((part) => Number.isNaN(part))) { + ctx.issues.push({ + code: 'custom', + message: 'bbox parts must be valid numbers', + input: value, + }); + return z.NEVER; + } - @ValidateUUID({ optional: true, description: 'Filter assets containing a specific person (face recognition)' }) - personId?: string; - - @ValidateUUID({ optional: true, description: 'Filter assets with a specific tag' }) - tagId?: string; - - @ValidateBoolean({ - optional: true, - description: 'Filter by favorite status (true for favorites only, false for non-favorites only)', + return { west, south, east, north }; + }) + .pipe(BBoxSchema) + .optional() + .describe('Bounding box coordinates as west,south,east,north (WGS84)') + .meta({ example: '11.075683,49.416711,11.117589,49.454875' }), }) - isFavorite?: boolean; + .meta({ id: 'TimeBucketDto' }); - @ValidateBoolean({ - optional: true, - description: 'Filter by trash status (true for trashed assets only, false for non-trashed only)', +const TimeBucketSchema = TimeBucketQueryBaseSchema; +const TimeBucketAssetSchema = TimeBucketQueryBaseSchema.extend({ + timeBucket: z.string().describe('Time bucket identifier in YYYY-MM-DD format').meta({ example: '2024-01-01' }), +}).meta({ id: 'TimeBucketAssetDto' }); + +const stackTupleSchema = z.array(z.string()).length(2).nullable(); + +const TimeBucketAssetResponseSchema = z + .object({ + id: z.array(z.string()).describe('Array of asset IDs in the time bucket'), + ownerId: z.array(z.string()).describe('Array of owner IDs for each asset'), + ratio: z.array(z.number()).describe('Array of aspect ratios (width/height) for each asset'), + isFavorite: z.array(z.boolean()).describe('Array indicating whether each asset is favorited'), + visibility: z + .array(AssetVisibilitySchema) + .describe('Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)'), + isTrashed: z.array(z.boolean()).describe('Array indicating whether each asset is in the trash'), + isImage: z.array(z.boolean()).describe('Array indicating whether each asset is an image (false for videos)'), + thumbhash: z + .array(z.string().nullable()) + .describe('Array of BlurHash strings for generating asset previews (base64 encoded)'), + fileCreatedAt: z.array(z.string()).describe('Array of file creation timestamps in UTC'), + localOffsetHours: z + .array(z.number()) + .describe( + "Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.", + ), + duration: z.array(z.string().nullable()).describe('Array of video durations in HH:MM:SS format (null for images)'), + stack: z + .array(stackTupleSchema) + .optional() + .describe('Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets)'), + projectionType: z + .array(z.string().nullable()) + .describe('Array of projection types for 360° content (e.g., "EQUIRECTANGULAR", "CUBEFACE", "CYLINDRICAL")'), + livePhotoVideoId: z + .array(z.string().nullable()) + .describe('Array of live photo video asset IDs (null for non-live photos)'), + city: z.array(z.string().nullable()).describe('Array of city names extracted from EXIF GPS data'), + country: z.array(z.string().nullable()).describe('Array of country names extracted from EXIF GPS data'), + latitude: z + .array(z.number().nullable()) + .optional() + .describe('Array of latitude coordinates extracted from EXIF GPS data'), + longitude: z + .array(z.number().nullable()) + .optional() + .describe('Array of longitude coordinates extracted from EXIF GPS data'), }) - isTrashed?: boolean; + .meta({ id: 'TimeBucketAssetResponseDto' }); - @ValidateBoolean({ - optional: true, - description: 'Include stacked assets in the response. When true, only primary assets from stacks are returned.', +const TimeBucketsResponseSchema = z + .object({ + timeBucket: z + .string() + .describe('Time bucket identifier in YYYY-MM-DD format representing the start of the time period') + .meta({ example: '2024-01-01' }), + count: z.int().describe('Number of assets in this time bucket').meta({ example: 42 }), }) - withStacked?: boolean; + .meta({ id: 'TimeBucketsResponseDto' }); - @ValidateBoolean({ optional: true, description: 'Include assets shared by partners' }) - withPartners?: boolean; - - @ValidateEnum({ - enum: AssetOrder, - name: 'AssetOrder', - description: 'Sort order for assets within time buckets (ASC for oldest first, DESC for newest first)', - optional: true, - }) - order?: AssetOrder; - - @ValidateEnum({ - enum: AssetVisibility, - name: 'AssetVisibility', - optional: true, - description: 'Filter by asset visibility status (ARCHIVE, TIMELINE, HIDDEN, LOCKED)', - }) - visibility?: AssetVisibility; - - @ValidateBoolean({ - optional: true, - description: 'Include location data in the response', - }) - withCoordinates?: boolean; - - @ValidateBBox({ optional: true }) - bbox?: BBoxDto; -} - -export class TimeBucketAssetDto extends TimeBucketDto { - @ApiProperty({ - type: 'string', - description: 'Time bucket identifier in YYYY-MM-DD format (e.g., "2024-01-01" for January 2024)', - example: '2024-01-01', - }) - @IsString() - timeBucket!: string; -} - -export class TimeBucketAssetResponseDto { - @ApiProperty({ - type: 'array', - items: { type: 'string' }, - description: 'Array of asset IDs in the time bucket', - }) - id!: string[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string' }, - description: 'Array of owner IDs for each asset', - }) - ownerId!: string[]; - - @ApiProperty({ - type: 'array', - items: { type: 'number' }, - description: 'Array of aspect ratios (width/height) for each asset', - }) - ratio!: number[]; - - @ApiProperty({ - type: 'array', - items: { type: 'boolean' }, - description: 'Array indicating whether each asset is favorited', - }) - isFavorite!: boolean[]; - - @ValidateEnum({ - enum: AssetVisibility, - name: 'AssetVisibility', - each: true, - description: 'Array of visibility statuses for each asset (e.g., ARCHIVE, TIMELINE, HIDDEN, LOCKED)', - }) - visibility!: AssetVisibility[]; - - @ApiProperty({ - type: 'array', - items: { type: 'boolean' }, - description: 'Array indicating whether each asset is in the trash', - }) - isTrashed!: boolean[]; - - @ApiProperty({ - type: 'array', - items: { type: 'boolean' }, - description: 'Array indicating whether each asset is an image (false for videos)', - }) - isImage!: boolean[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of BlurHash strings for generating asset previews (base64 encoded)', - }) - thumbhash!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string' }, - description: 'Array of file creation timestamps in UTC', - }) - fileCreatedAt!: string[]; - - @ApiProperty({ - type: 'array', - items: { type: 'number' }, - description: - "Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.", - }) - localOffsetHours!: number[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of video durations in HH:MM:SS format (null for images)', - }) - duration!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { - type: 'array', - items: { type: 'string' }, - minItems: 2, - maxItems: 2, - nullable: true, - }, - description: 'Array of stack information as [stackId, assetCount] tuples (null for non-stacked assets)', - }) - stack?: ([string, string] | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of projection types for 360° content (e.g., "EQUIRECTANGULAR", "CUBEFACE", "CYLINDRICAL")', - }) - projectionType!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of live photo video asset IDs (null for non-live photos)', - }) - livePhotoVideoId!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of city names extracted from EXIF GPS data', - }) - city!: (string | null)[]; - - @ApiProperty({ - type: 'array', - items: { type: 'string', nullable: true }, - description: 'Array of country names extracted from EXIF GPS data', - }) - country!: (string | null)[]; - - @ApiProperty({ - type: 'array', - required: false, - items: { type: 'number', nullable: true }, - description: 'Array of latitude coordinates extracted from EXIF GPS data', - }) - latitude!: number[]; - - @ApiProperty({ - type: 'array', - required: false, - items: { type: 'number', nullable: true }, - description: 'Array of longitude coordinates extracted from EXIF GPS data', - }) - longitude!: number[]; -} - -export class TimeBucketsResponseDto { - @ApiProperty({ - type: 'string', - description: 'Time bucket identifier in YYYY-MM-DD format representing the start of the time period', - example: '2024-01-01', - }) - timeBucket!: string; - - @ApiProperty({ - type: 'integer', - description: 'Number of assets in this time bucket', - example: 42, - }) - count!: number; -} +export class TimeBucketDto extends createZodDto(TimeBucketSchema) {} +export class TimeBucketAssetDto extends createZodDto(TimeBucketAssetSchema) {} +export class TimeBucketAssetResponseDto extends createZodDto(TimeBucketAssetResponseSchema) {} +export class TimeBucketsResponseDto extends createZodDto(TimeBucketsResponseSchema) {} diff --git a/server/src/dtos/trash.dto.ts b/server/src/dtos/trash.dto.ts index f1d1f109f614..9a725bc6c835 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 cce19940074f..7a7c1d255831 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,302 +1,212 @@ -import { ApiProperty, ApiPropertyOptional, ApiSchema } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsDateString, IsInt, IsPositive, ValidateNested } from 'class-validator'; -import { AssetOrder, UserAvatarColor } from 'src/enum'; +import { createZodDto } from 'nestjs-zod'; +import { AssetOrderSchema, UserAvatarColorSchema } from 'src/enum'; import { UserPreferences } from 'src/types'; -import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; +import z from 'zod'; -class AvatarUpdate { - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', optional: true, description: 'Avatar color' }) - color?: UserAvatarColor; -} +const AlbumsUpdateSchema = z + .object({ + defaultAssetOrder: AssetOrderSchema.optional(), + }) + .optional() + .describe('Album preferences') + .meta({ id: 'AlbumsUpdate' }); -class MemoriesUpdate { - @ValidateBoolean({ optional: true, description: 'Whether memories are enabled' }) - enabled?: boolean; +const AvatarUpdateSchema = z + .object({ + color: UserAvatarColorSchema.optional(), + }) + .optional() + .meta({ id: 'AvatarUpdate' }); - @Optional() - @IsInt() - @IsPositive() - @ApiProperty({ type: 'integer', description: 'Memory duration in seconds' }) - duration?: number; -} +const MemoriesUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether memories are enabled'), + duration: z.int().min(1).optional().describe('Memory duration in seconds'), + }) + .optional() + .meta({ id: 'MemoriesUpdate' }); -class RatingsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether ratings are enabled' }) - enabled?: boolean; -} +const RatingsUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether ratings are enabled'), + }) + .optional() + .meta({ id: 'RatingsUpdate' }); -@ApiSchema({ description: 'Album preferences' }) -class AlbumsUpdate { - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', optional: true, description: 'Default asset order for albums' }) - defaultAssetOrder?: AssetOrder; -} +const FoldersUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether folders are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether folders appear in web sidebar'), + }) + .optional() + .meta({ id: 'FoldersUpdate' }); -class FoldersUpdate { - @ValidateBoolean({ optional: true, description: 'Whether folders are enabled' }) - enabled?: boolean; +const PeopleUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether people are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether people appear in web sidebar'), + }) + .optional() + .meta({ id: 'PeopleUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether folders appear in web sidebar' }) - sidebarWeb?: boolean; -} +const SharedLinksUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether shared links are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether shared links appear in web sidebar'), + }) + .optional() + .meta({ id: 'SharedLinksUpdate' }); -class PeopleUpdate { - @ValidateBoolean({ optional: true, description: 'Whether people are enabled' }) - enabled?: boolean; +const TagsUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether tags are enabled'), + sidebarWeb: z.boolean().optional().describe('Whether tags appear in web sidebar'), + }) + .optional() + .meta({ id: 'TagsUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether people appear in web sidebar' }) - sidebarWeb?: boolean; -} +const EmailNotificationsUpdateSchema = z + .object({ + enabled: z.boolean().optional().describe('Whether email notifications are enabled'), + albumInvite: z.boolean().optional().describe('Whether to receive email notifications for album invites'), + albumUpdate: z.boolean().optional().describe('Whether to receive email notifications for album updates'), + }) + .optional() + .meta({ id: 'EmailNotificationsUpdate' }); -class SharedLinksUpdate { - @ValidateBoolean({ optional: true, description: 'Whether shared links are enabled' }) - enabled?: boolean; +const DownloadUpdateSchema = z + .object({ + archiveSize: z.int().min(1).optional().describe('Maximum archive size in bytes'), + includeEmbeddedVideos: z.boolean().optional().describe('Whether to include embedded videos in downloads'), + }) + .optional() + .meta({ id: 'DownloadUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether shared links appear in web sidebar' }) - sidebarWeb?: boolean; -} +const PurchaseUpdateSchema = z + .object({ + showSupportBadge: z.boolean().optional().describe('Whether to show support badge'), + hideBuyButtonUntil: z.string().optional().describe('Date until which to hide buy button'), + }) + .optional() + .meta({ id: 'PurchaseUpdate' }); -class TagsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether tags are enabled' }) - enabled?: boolean; +const CastUpdateSchema = z + .object({ + gCastEnabled: z.boolean().optional().describe('Whether Google Cast is enabled'), + }) + .optional() + .meta({ id: 'CastUpdate' }); - @ValidateBoolean({ optional: true, description: 'Whether tags appear in web sidebar' }) - sidebarWeb?: boolean; -} +const UserPreferencesUpdateSchema = z + .object({ + albums: AlbumsUpdateSchema, + avatar: AvatarUpdateSchema, + cast: CastUpdateSchema, + download: DownloadUpdateSchema, + emailNotifications: EmailNotificationsUpdateSchema, + folders: FoldersUpdateSchema, + memories: MemoriesUpdateSchema, + people: PeopleUpdateSchema, + purchase: PurchaseUpdateSchema, + ratings: RatingsUpdateSchema, + sharedLinks: SharedLinksUpdateSchema, + tags: TagsUpdateSchema, + }) + .meta({ id: 'UserPreferencesUpdateDto' }); -class EmailNotificationsUpdate { - @ValidateBoolean({ optional: true, description: 'Whether email notifications are enabled' }) - enabled?: boolean; +const AlbumsResponseSchema = z + .object({ + defaultAssetOrder: AssetOrderSchema, + }) + .meta({ id: 'AlbumsResponse' }); - @ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album invites' }) - albumInvite?: boolean; +const FoldersResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether folders are enabled'), + sidebarWeb: z.boolean().describe('Whether folders appear in web sidebar'), + }) + .meta({ id: 'FoldersResponse' }); - @ValidateBoolean({ optional: true, description: 'Whether to receive email notifications for album updates' }) - albumUpdate?: boolean; -} +const MemoriesResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether memories are enabled'), + duration: z.int().describe('Memory duration in seconds'), + }) + .meta({ id: 'MemoriesResponse' }); -class DownloadUpdate implements Partial { - @Optional() - @IsInt() - @IsPositive() - @ApiPropertyOptional({ type: 'integer', description: 'Maximum archive size in bytes' }) - archiveSize?: number; +const PeopleResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether people are enabled'), + sidebarWeb: z.boolean().describe('Whether people appear in web sidebar'), + }) + .meta({ id: 'PeopleResponse' }); - @ValidateBoolean({ optional: true, description: 'Whether to include embedded videos in downloads' }) - includeEmbeddedVideos?: boolean; -} +const RatingsResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether ratings are enabled'), + }) + .meta({ id: 'RatingsResponse' }); -class PurchaseUpdate { - @ValidateBoolean({ optional: true, description: 'Whether to show support badge' }) - showSupportBadge?: boolean; +const SharedLinksResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether shared links are enabled'), + sidebarWeb: z.boolean().describe('Whether shared links appear in web sidebar'), + }) + .meta({ id: 'SharedLinksResponse' }); - @ApiPropertyOptional({ description: 'Date until which to hide buy button' }) - @IsDateString() - @Optional() - hideBuyButtonUntil?: string; -} +const TagsResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether tags are enabled'), + sidebarWeb: z.boolean().describe('Whether tags appear in web sidebar'), + }) + .meta({ id: 'TagsResponse' }); -class CastUpdate { - @ValidateBoolean({ optional: true, description: 'Whether Google Cast is enabled' }) - gCastEnabled?: boolean; -} +const EmailNotificationsResponseSchema = z + .object({ + enabled: z.boolean().describe('Whether email notifications are enabled'), + albumInvite: z.boolean().describe('Whether to receive email notifications for album invites'), + albumUpdate: z.boolean().describe('Whether to receive email notifications for album updates'), + }) + .meta({ id: 'EmailNotificationsResponse' }); -export class UserPreferencesUpdateDto { - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => AlbumsUpdate) - albums?: AlbumsUpdate; +const DownloadResponseSchema = z + .object({ + archiveSize: z.int().describe('Maximum archive size in bytes'), + includeEmbeddedVideos: z.boolean().describe('Whether to include embedded videos in downloads'), + }) + .meta({ id: 'DownloadResponse' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => FoldersUpdate) - folders?: FoldersUpdate; +const PurchaseResponseSchema = z + .object({ + showSupportBadge: z.boolean().describe('Whether to show support badge'), + hideBuyButtonUntil: z.string().describe('Date until which to hide buy button'), + }) + .meta({ id: 'PurchaseResponse' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => MemoriesUpdate) - memories?: MemoriesUpdate; +const CastResponseSchema = z + .object({ + gCastEnabled: z.boolean().describe('Whether Google Cast is enabled'), + }) + .meta({ id: 'CastResponse' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => PeopleUpdate) - people?: PeopleUpdate; +const UserPreferencesResponseSchema = z + .object({ + albums: AlbumsResponseSchema, + folders: FoldersResponseSchema, + memories: MemoriesResponseSchema, + people: PeopleResponseSchema, + ratings: RatingsResponseSchema, + sharedLinks: SharedLinksResponseSchema, + tags: TagsResponseSchema, + emailNotifications: EmailNotificationsResponseSchema, + download: DownloadResponseSchema, + purchase: PurchaseResponseSchema, + cast: CastResponseSchema, + }) + .meta({ id: 'UserPreferencesResponseDto' }); - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => RatingsUpdate) - ratings?: RatingsUpdate; - - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined, required: false }) - @Optional() - @ValidateNested() - @Type(() => SharedLinksUpdate) - sharedLinks?: SharedLinksUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => TagsUpdate) - tags?: TagsUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => AvatarUpdate) - avatar?: AvatarUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => EmailNotificationsUpdate) - emailNotifications?: EmailNotificationsUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => DownloadUpdate) - download?: DownloadUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => PurchaseUpdate) - purchase?: PurchaseUpdate; - - // Description lives on schema to avoid duplication - @ApiPropertyOptional({ description: undefined }) - @Optional() - @ValidateNested() - @Type(() => CastUpdate) - cast?: CastUpdate; -} - -class AlbumsResponse { - @ValidateEnum({ enum: AssetOrder, name: 'AssetOrder', description: 'Default asset order for albums' }) - defaultAssetOrder: AssetOrder = AssetOrder.Desc; -} - -class RatingsResponse { - @ApiProperty({ description: 'Whether ratings are enabled' }) - enabled: boolean = false; -} - -class MemoriesResponse { - @ApiProperty({ description: 'Whether memories are enabled' }) - enabled: boolean = true; - - @ApiProperty({ type: 'integer', description: 'Memory duration in seconds' }) - duration: number = 5; -} - -class FoldersResponse { - @ApiProperty({ description: 'Whether folders are enabled' }) - enabled: boolean = false; - @ApiProperty({ description: 'Whether folders appear in web sidebar' }) - sidebarWeb: boolean = false; -} - -class PeopleResponse { - @ApiProperty({ description: 'Whether people are enabled' }) - enabled: boolean = true; - @ApiProperty({ description: 'Whether people appear in web sidebar' }) - sidebarWeb: boolean = false; -} - -class TagsResponse { - @ApiProperty({ description: 'Whether tags are enabled' }) - enabled: boolean = true; - @ApiProperty({ description: 'Whether tags appear in web sidebar' }) - sidebarWeb: boolean = true; -} - -class SharedLinksResponse { - @ApiProperty({ description: 'Whether shared links are enabled' }) - enabled: boolean = true; - @ApiProperty({ description: 'Whether shared links appear in web sidebar' }) - sidebarWeb: boolean = false; -} - -class EmailNotificationsResponse { - @ApiProperty({ description: 'Whether email notifications are enabled' }) - enabled!: boolean; - @ApiProperty({ description: 'Whether to receive email notifications for album invites' }) - albumInvite!: boolean; - @ApiProperty({ description: 'Whether to receive email notifications for album updates' }) - albumUpdate!: boolean; -} - -class DownloadResponse { - @ApiProperty({ type: 'integer', description: 'Maximum archive size in bytes' }) - archiveSize!: number; - - @ApiProperty({ description: 'Whether to include embedded videos in downloads' }) - includeEmbeddedVideos: boolean = false; -} - -class PurchaseResponse { - @ApiProperty({ description: 'Whether to show support badge' }) - showSupportBadge!: boolean; - @ApiProperty({ description: 'Date until which to hide buy button' }) - hideBuyButtonUntil!: string; -} - -class CastResponse { - @ApiProperty({ description: 'Whether Google Cast is enabled' }) - gCastEnabled: boolean = false; -} - -export class UserPreferencesResponseDto implements UserPreferences { - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - albums!: AlbumsResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - folders!: FoldersResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - memories!: MemoriesResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - people!: PeopleResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - ratings!: RatingsResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - sharedLinks!: SharedLinksResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - tags!: TagsResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - emailNotifications!: EmailNotificationsResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - download!: DownloadResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - purchase!: PurchaseResponse; - // Description lives on schema to avoid duplication - @ApiProperty({ description: undefined }) - cast!: CastResponse; -} +export class UserPreferencesUpdateDto extends createZodDto(UserPreferencesUpdateSchema) {} +export class UserPreferencesResponseDto extends createZodDto(UserPreferencesResponseSchema) {} export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { return preferences; diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts index 6559dd052c03..c3c91d3d954a 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 e6be3b17d117..6acc9554f9a0 100644 --- a/server/src/dtos/user.dto.spec.ts +++ b/server/src/dtos/user.dto.spec.ts @@ -1,69 +1,59 @@ -import { plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; -import { UserAdminCreateDto, UserUpdateMeDto } from 'src/dtos/user.dto'; +import { UserAdminCreateSchema, UserUpdateMeSchema } from 'src/dtos/user.dto'; describe('update user DTO', () => { - it('should allow emails without a tld', async () => { + it('should allow emails without a tld', () => { const someEmail = 'test@test'; - - const dto = plainToInstance(UserUpdateMeDto, { + const result = UserUpdateMeSchema.safeParse({ email: someEmail, id: '3fe388e4-2078-44d7-b36c-39d9dee3a657', }); - const errors = await validate(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); + expect(result.success).toBe(true); + expect(result.data?.email).toEqual(someEmail); }); }); describe('create user DTO', () => { - it('validates the email', async () => { - const params: Partial = { - email: undefined, + it('validates the email', () => { + expect(UserAdminCreateSchema.safeParse({ password: 'password', name: 'name' }).success).toBe(false); + + expect( + UserAdminCreateSchema.safeParse({ email: 'invalid email', password: 'password', name: 'name' }).success, + ).toBe(false); + + const result = UserAdminCreateSchema.safeParse({ + email: 'valid@email.com', password: 'password', name: 'name', - }; - let dto: UserAdminCreateDto = plainToInstance(UserAdminCreateDto, params); - let errors = await validate(dto); - expect(errors).toHaveLength(1); - - params.email = 'invalid email'; - dto = plainToInstance(UserAdminCreateDto, params); - errors = await validate(dto); - expect(errors).toHaveLength(1); - - params.email = 'valid@email.com'; - dto = plainToInstance(UserAdminCreateDto, params); - errors = await validate(dto); - expect(errors).toHaveLength(0); + }); + expect(result.success).toBe(true); }); - it('validates invalid email type', async () => { - let dto = plainToInstance(UserAdminCreateDto, { - email: [], - password: 'some password', - name: 'some name', - }); - expect(await validate(dto)).toHaveLength(1); + it('validates invalid email type', () => { + expect( + UserAdminCreateSchema.safeParse({ + email: [], + password: 'some password', + name: 'some name', + }).success, + ).toBe(false); - dto = plainToInstance(UserAdminCreateDto, { - email: {}, - password: 'some password', - name: 'some name', - }); - expect(await validate(dto)).toHaveLength(1); + expect( + UserAdminCreateSchema.safeParse({ + email: {}, + password: 'some password', + name: 'some name', + }).success, + ).toBe(false); }); - it('should allow emails without a tld', async () => { + it('should allow emails without a tld', () => { const someEmail = 'test@test'; - - const dto = plainToInstance(UserAdminCreateDto, { + const result = UserAdminCreateSchema.safeParse({ email: someEmail, password: 'some password', name: 'some name', }); - const errors = await validate(dto); - expect(errors).toHaveLength(0); - expect(dto.email).toEqual(someEmail); + expect(result.success).toBe(true); + expect(result.data?.email).toEqual(someEmail); }); }); diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index ebd0018bba9f..75256b9e1a3d 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,65 +1,50 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { IsEmail, IsInt, IsNotEmpty, IsString, Min } from 'class-validator'; +import { createZodDto } from 'nestjs-zod'; import { User, UserAdmin } from 'src/database'; -import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; +import { pinCodeRegex } from 'src/dtos/auth.dto'; +import { UserAvatarColor, UserAvatarColorSchema, UserMetadataKey, UserStatusSchema } from 'src/enum'; import { MaybeDehydrated, UserMetadataItem } from 'src/types'; import { asDateString } from 'src/utils/date'; -import { Optional, PinCode, ValidateBoolean, ValidateEnum, ValidateUUID, toEmail, toSanitized } from 'src/validation'; +import { emptyStringToNull, isoDatetimeToDate, sanitizeFilename, stringToBool, toEmail } from 'src/validation'; +import z from 'zod'; -export class UserUpdateMeDto { - @ApiPropertyOptional({ description: 'User email' }) - @Optional() - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email?: string; - - // TODO: migrate to the other change password endpoint - @ApiPropertyOptional({ description: 'User password (deprecated, use change password endpoint)' }) - @Optional() - @IsNotEmpty() - @IsString() - password?: string; - - @ApiPropertyOptional({ description: 'User name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ValidateEnum({ - enum: UserAvatarColor, - name: 'UserAvatarColor', - optional: true, - nullable: true, - description: 'Avatar color', +export const UserUpdateMeSchema = z + .object({ + email: toEmail.optional().describe('User email'), + password: z + .string() + .optional() + .describe('User password (deprecated, use change password endpoint)') + .meta({ deprecated: true }), + name: z.string().optional().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), }) - avatarColor?: UserAvatarColor | null; -} + .meta({ id: 'UserUpdateMeDto' }); -export class UserResponseDto { - @ApiProperty({ description: 'User ID' }) - id!: string; - @ApiProperty({ description: 'User name' }) - name!: string; - @ApiProperty({ description: 'User email' }) - email!: string; - @ApiProperty({ description: 'Profile image path' }) - profileImagePath!: string; - @ValidateEnum({ enum: UserAvatarColor, name: 'UserAvatarColor', description: 'Avatar color' }) - avatarColor!: UserAvatarColor; - @ApiProperty({ description: 'Profile change date', format: 'date-time' }) - profileChangedAt!: string; -} +export class UserUpdateMeDto extends createZodDto(UserUpdateMeSchema) {} -export class UserLicense { - @ApiProperty({ description: 'License key' }) - licenseKey!: string; - @ApiProperty({ description: 'Activation key' }) - activationKey!: string; - @ApiProperty({ description: 'Activation date' }) - activatedAt!: Date; -} +export const UserResponseSchema = z + .object({ + id: z.uuidv4().describe('User ID'), + name: z.string().describe('User name'), + email: toEmail.describe('User email'), + profileImagePath: z.string().describe('Profile image path'), + avatarColor: UserAvatarColorSchema, + // TODO: use `isoDatetimeToDate` when using `ZodSerializerDto` on the controllers. + profileChangedAt: z.string().meta({ format: 'date-time' }).describe('Profile change date'), + }) + .meta({ id: 'UserResponseDto' }); + +export class UserResponseDto extends createZodDto(UserResponseSchema) {} + +const licenseKeyRegex = /^IM(SV|CL)(-[\dA-Za-z]{4}){8}$/; + +export const UserLicenseSchema = z + .object({ + licenseKey: z.string().regex(licenseKeyRegex).describe(`License key (format: ${licenseKeyRegex.toString()})`), + activationKey: z.string().describe('Activation key'), + activatedAt: isoDatetimeToDate.describe('Activation date'), + }) + .meta({ id: 'UserLicense' }); const emailToAvatarColor = (email: string): UserAvatarColor => { const values = Object.values(UserAvatarColor); @@ -80,144 +65,77 @@ export const mapUser = (entity: MaybeDehydrated): UserResponse }; }; -export class UserAdminSearchDto { - @ValidateBoolean({ optional: true, description: 'Include deleted users' }) - withDeleted?: boolean; - - @ValidateUUID({ optional: true, description: 'User ID filter' }) - id?: string; -} - -export class UserAdminCreateDto { - @ApiProperty({ description: 'User email' }) - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email!: string; - - @ApiProperty({ description: 'User password' }) - @IsString() - password!: string; - - @ApiProperty({ description: 'User name' }) - @IsNotEmpty() - @IsString() - name!: string; - - @ValidateEnum({ - enum: UserAvatarColor, - name: 'UserAvatarColor', - optional: true, - nullable: true, - description: 'Avatar color', +const UserAdminSearchSchema = z + .object({ + withDeleted: stringToBool.optional().describe('Include deleted users'), + id: z.uuidv4().optional().describe('User ID filter'), }) - avatarColor?: UserAvatarColor | null; + .meta({ id: 'UserAdminSearchDto' }); - @ApiPropertyOptional({ description: 'PIN code' }) - @PinCode({ optional: true, nullable: true, emptyToNull: true }) - pinCode?: string | null; +export class UserAdminSearchDto extends createZodDto(UserAdminSearchSchema) {} - @ApiPropertyOptional({ description: 'Storage label' }) - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; - - @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - @Optional({ nullable: true }) - @IsInt() - @Min(0) - quotaSizeInBytes?: number | null; - - @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) - shouldChangePassword?: boolean; - - @ValidateBoolean({ optional: true, description: 'Send notification email' }) - notify?: boolean; - - @ValidateBoolean({ optional: true, description: 'Grant admin privileges' }) - isAdmin?: boolean; -} - -export class UserAdminUpdateDto { - @ApiPropertyOptional({ description: 'User email' }) - @Optional() - @IsEmail({ require_tld: false }) - @Transform(toEmail) - email?: string; - - @ApiPropertyOptional({ description: 'User password' }) - @Optional() - @IsNotEmpty() - @IsString() - password?: string; - - @ApiPropertyOptional({ description: 'PIN code' }) - @PinCode({ optional: true, nullable: true, emptyToNull: true }) - pinCode?: string | null; - - @ApiPropertyOptional({ description: 'User name' }) - @Optional() - @IsString() - @IsNotEmpty() - name?: string; - - @ValidateEnum({ - enum: UserAvatarColor, - name: 'UserAvatarColor', - optional: true, - nullable: true, - description: 'Avatar color', +export const UserAdminCreateSchema = z + .object({ + email: toEmail.describe('User email'), + password: z.string().describe('User password'), + name: z.string().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), + pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable()) + .optional() + .describe('PIN code') + .meta({ example: '123456' }), + storageLabel: z.string().pipe(sanitizeFilename).nullish().describe('Storage label'), + quotaSizeInBytes: z.int().min(0).nullish().describe('Storage quota in bytes'), + shouldChangePassword: z.boolean().optional().describe('Require password change on next login'), + notify: z.boolean().optional().describe('Send notification email'), + isAdmin: z.boolean().optional().describe('Grant admin privileges'), }) - avatarColor?: UserAvatarColor | null; + .meta({ id: 'UserAdminCreateDto' }); - @ApiPropertyOptional({ description: 'Storage label' }) - @Optional({ nullable: true }) - @IsString() - @Transform(toSanitized) - storageLabel?: string | null; +export class UserAdminCreateDto extends createZodDto(UserAdminCreateSchema) {} - @ValidateBoolean({ optional: true, description: 'Require password change on next login' }) - shouldChangePassword?: boolean; +const UserAdminUpdateSchema = z + .object({ + email: toEmail.optional().describe('User email'), + password: z.string().optional().describe('User password'), + pinCode: emptyStringToNull(z.string().regex(pinCodeRegex).nullable()) + .optional() + .describe('PIN code') + .meta({ example: '123456' }), + name: z.string().optional().describe('User name'), + avatarColor: UserAvatarColorSchema.nullish(), + storageLabel: z.string().pipe(sanitizeFilename).nullish().describe('Storage label'), + shouldChangePassword: z.boolean().optional().describe('Require password change on next login'), + quotaSizeInBytes: z.int().min(0).nullish().describe('Storage quota in bytes'), + isAdmin: z.boolean().optional().describe('Grant admin privileges'), + }) + .meta({ id: 'UserAdminUpdateDto' }); - @ApiPropertyOptional({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - @Optional({ nullable: true }) - @IsInt() - @Min(0) - quotaSizeInBytes?: number | null; +export class UserAdminUpdateDto extends createZodDto(UserAdminUpdateSchema) {} - @ValidateBoolean({ optional: true, description: 'Grant admin privileges' }) - isAdmin?: boolean; -} +const UserAdminDeleteSchema = z + .object({ + force: z.boolean().optional().describe('Force delete even if user has assets'), + }) + .meta({ id: 'UserAdminDeleteDto' }); -export class UserAdminDeleteDto { - @ValidateBoolean({ optional: true, description: 'Force delete even if user has assets' }) - force?: boolean; -} +export class UserAdminDeleteDto extends createZodDto(UserAdminDeleteSchema) {} -export class UserAdminResponseDto extends UserResponseDto { - @ApiProperty({ description: 'Storage label' }) - storageLabel!: string | null; - @ApiProperty({ description: 'Require password change on next login' }) - shouldChangePassword!: boolean; - @ApiProperty({ description: 'Is admin user' }) - isAdmin!: boolean; - @ApiProperty({ description: 'Creation date' }) - createdAt!: Date; - @ApiProperty({ description: 'Deletion date' }) - deletedAt!: Date | null; - @ApiProperty({ description: 'Last update date' }) - updatedAt!: Date; - @ApiProperty({ description: 'OAuth ID' }) - oauthId!: string; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage quota in bytes' }) - quotaSizeInBytes!: number | null; - @ApiProperty({ type: 'integer', format: 'int64', description: 'Storage usage in bytes' }) - quotaUsageInBytes!: number | null; - @ValidateEnum({ enum: UserStatus, name: 'UserStatus', description: 'User status' }) - status!: string; - @ApiProperty({ description: 'User license' }) - license!: UserLicense | null; -} +const UserAdminResponseSchema = UserResponseSchema.extend({ + storageLabel: z.string().nullable().describe('Storage label'), + shouldChangePassword: z.boolean().describe('Require password change on next login'), + isAdmin: z.boolean().describe('Is admin user'), + createdAt: isoDatetimeToDate.describe('Creation date'), + deletedAt: isoDatetimeToDate.nullable().describe('Deletion date'), + updatedAt: isoDatetimeToDate.describe('Last update date'), + oauthId: z.string().describe('OAuth ID'), + quotaSizeInBytes: z.int().min(0).nullable().describe('Storage quota in bytes'), + quotaUsageInBytes: z.int().min(0).nullable().describe('Storage usage in bytes'), + status: UserStatusSchema, + license: UserLicenseSchema.nullable(), +}).meta({ id: 'UserAdminResponseDto' }); + +export class UserAdminResponseDto extends createZodDto(UserAdminResponseSchema) {} export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { const metadata = entity.metadata || []; @@ -237,6 +155,6 @@ export function mapUserAdmin(entity: UserAdmin): UserAdminResponseDto { quotaSizeInBytes: entity.quotaSizeInBytes, quotaUsageInBytes: entity.quotaUsageInBytes, status: entity.status, - license: license ? { ...license, activatedAt: new Date(license?.activatedAt) } : null, + license: license ? { ...license, activatedAt: new Date(license.activatedAt) } : null, }; } diff --git a/server/src/dtos/workflow.dto.ts b/server/src/dtos/workflow.dto.ts index c4e5ac9c4c51..0307c7f483e9 100644 --- a/server/src/dtos/workflow.dto.ts +++ b/server/src/dtos/workflow.dto.ts @@ -1,143 +1,84 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { Type } from 'class-transformer'; -import { IsNotEmpty, IsObject, IsString, IsUUID, ValidateNested } from 'class-validator'; -import { WorkflowAction, WorkflowFilter } from 'src/database'; -import { PluginTriggerType } from 'src/enum'; -import type { ActionConfig, FilterConfig } from 'src/types/plugin-schema.types'; -import { Optional, ValidateBoolean, ValidateEnum } from 'src/validation'; +import { createZodDto } from 'nestjs-zod'; +import type { WorkflowAction, WorkflowFilter } from 'src/database'; +import { PluginTriggerTypeSchema } from 'src/enum'; +import { ActionConfigSchema, FilterConfigSchema } from 'src/types/plugin-schema.types'; +import z from 'zod'; -export class WorkflowFilterItemDto { - @ApiProperty({ description: 'Plugin filter ID' }) - @IsUUID() - pluginFilterId!: string; - - @ApiPropertyOptional({ description: 'Filter configuration' }) - @IsObject() - @Optional() - filterConfig?: FilterConfig; -} - -export class WorkflowActionItemDto { - @ApiProperty({ description: 'Plugin action ID' }) - @IsUUID() - pluginActionId!: string; - - @ApiPropertyOptional({ description: 'Action configuration' }) - @IsObject() - @Optional() - actionConfig?: ActionConfig; -} - -export class WorkflowCreateDto { - @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Workflow trigger type' }) - triggerType!: PluginTriggerType; - - @ApiProperty({ description: 'Workflow name' }) - @IsString() - @IsNotEmpty() - name!: string; - - @ApiPropertyOptional({ description: 'Workflow description' }) - @IsString() - @Optional() - description?: string; - - @ValidateBoolean({ optional: true, description: 'Workflow enabled' }) - enabled?: boolean; - - @ApiProperty({ description: 'Workflow filters' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowFilterItemDto) - filters!: WorkflowFilterItemDto[]; - - @ApiProperty({ description: 'Workflow actions' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowActionItemDto) - actions!: WorkflowActionItemDto[]; -} - -export class WorkflowUpdateDto { - @ValidateEnum({ - enum: PluginTriggerType, - name: 'PluginTriggerType', - optional: true, - description: 'Workflow trigger type', +const WorkflowFilterItemSchema = z + .object({ + pluginFilterId: z.uuidv4().describe('Plugin filter ID'), + filterConfig: FilterConfigSchema.optional(), }) - triggerType?: PluginTriggerType; + .meta({ id: 'WorkflowFilterItemDto' }); - @ApiPropertyOptional({ description: 'Workflow name' }) - @IsString() - @IsNotEmpty() - @Optional() - name?: string; +const WorkflowActionItemSchema = z + .object({ + pluginActionId: z.uuidv4().describe('Plugin action ID'), + actionConfig: ActionConfigSchema.optional(), + }) + .meta({ id: 'WorkflowActionItemDto' }); - @ApiPropertyOptional({ description: 'Workflow description' }) - @IsString() - @Optional() - description?: string; +const WorkflowCreateSchema = z + .object({ + triggerType: PluginTriggerTypeSchema, + name: z.string().describe('Workflow name'), + description: z.string().optional().describe('Workflow description'), + enabled: z.boolean().optional().describe('Workflow enabled'), + filters: z.array(WorkflowFilterItemSchema).describe('Workflow filters'), + actions: z.array(WorkflowActionItemSchema).describe('Workflow actions'), + }) + .meta({ id: 'WorkflowCreateDto' }); - @ValidateBoolean({ optional: true, description: 'Workflow enabled' }) - enabled?: boolean; +const WorkflowUpdateSchema = z + .object({ + triggerType: PluginTriggerTypeSchema.optional(), + name: z.string().optional().describe('Workflow name'), + description: z.string().optional().describe('Workflow description'), + enabled: z.boolean().optional().describe('Workflow enabled'), + filters: z.array(WorkflowFilterItemSchema).optional().describe('Workflow filters'), + actions: z.array(WorkflowActionItemSchema).optional().describe('Workflow actions'), + }) + .meta({ id: 'WorkflowUpdateDto' }); - @ApiPropertyOptional({ description: 'Workflow filters' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowFilterItemDto) - @Optional() - filters?: WorkflowFilterItemDto[]; +const WorkflowFilterResponseSchema = z + .object({ + id: z.string().describe('Filter ID'), + workflowId: z.string().describe('Workflow ID'), + pluginFilterId: z.string().describe('Plugin filter ID'), + filterConfig: FilterConfigSchema.nullable(), + order: z.number().describe('Filter order'), + }) + .meta({ id: 'WorkflowFilterResponseDto' }); - @ApiPropertyOptional({ description: 'Workflow actions' }) - @ValidateNested({ each: true }) - @Type(() => WorkflowActionItemDto) - @Optional() - actions?: WorkflowActionItemDto[]; -} +const WorkflowActionResponseSchema = z + .object({ + id: z.string().describe('Action ID'), + workflowId: z.string().describe('Workflow ID'), + pluginActionId: z.string().describe('Plugin action ID'), + actionConfig: ActionConfigSchema.nullable(), + order: z.number().describe('Action order'), + }) + .meta({ id: 'WorkflowActionResponseDto' }); -export class WorkflowResponseDto { - @ApiProperty({ description: 'Workflow ID' }) - id!: string; - @ApiProperty({ description: 'Owner user ID' }) - ownerId!: string; - @ValidateEnum({ enum: PluginTriggerType, name: 'PluginTriggerType', description: 'Workflow trigger type' }) - triggerType!: PluginTriggerType; - @ApiProperty({ description: 'Workflow name' }) - name!: string | null; - @ApiProperty({ description: 'Workflow description' }) - description!: string; - @ApiProperty({ description: 'Creation date' }) - createdAt!: string; - @ApiProperty({ description: 'Workflow enabled' }) - enabled!: boolean; - @ApiProperty({ description: 'Workflow filters' }) - filters!: WorkflowFilterResponseDto[]; - @ApiProperty({ description: 'Workflow actions' }) - actions!: WorkflowActionResponseDto[]; -} +const WorkflowResponseSchema = z + .object({ + id: z.string().describe('Workflow ID'), + ownerId: z.string().describe('Owner user ID'), + triggerType: PluginTriggerTypeSchema, + name: z.string().nullable().describe('Workflow name'), + description: z.string().describe('Workflow description'), + createdAt: z.string().describe('Creation date'), + enabled: z.boolean().describe('Workflow enabled'), + filters: z.array(WorkflowFilterResponseSchema).describe('Workflow filters'), + actions: z.array(WorkflowActionResponseSchema).describe('Workflow actions'), + }) + .meta({ id: 'WorkflowResponseDto' }); -export class WorkflowFilterResponseDto { - @ApiProperty({ description: 'Filter ID' }) - id!: string; - @ApiProperty({ description: 'Workflow ID' }) - workflowId!: string; - @ApiProperty({ description: 'Plugin filter ID' }) - pluginFilterId!: string; - @ApiProperty({ description: 'Filter configuration' }) - filterConfig!: FilterConfig | null; - @ApiProperty({ description: 'Filter order', type: 'number' }) - order!: number; -} - -export class WorkflowActionResponseDto { - @ApiProperty({ description: 'Action ID' }) - id!: string; - @ApiProperty({ description: 'Workflow ID' }) - workflowId!: string; - @ApiProperty({ description: 'Plugin action ID' }) - pluginActionId!: string; - @ApiProperty({ description: 'Action configuration' }) - actionConfig!: ActionConfig | null; - @ApiProperty({ description: 'Action order', type: 'number' }) - order!: number; -} +export class WorkflowCreateDto extends createZodDto(WorkflowCreateSchema) {} +export class WorkflowUpdateDto extends createZodDto(WorkflowUpdateSchema) {} +export class WorkflowResponseDto extends createZodDto(WorkflowResponseSchema) {} +class WorkflowFilterResponseDto extends createZodDto(WorkflowFilterResponseSchema) {} +class WorkflowActionResponseDto extends createZodDto(WorkflowActionResponseSchema) {} export function mapWorkflowFilter(filter: WorkflowFilter): WorkflowFilterResponseDto { return { diff --git a/server/src/enum.ts b/server/src/enum.ts index de85d24db331..cb4835020fe0 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 a8afa91cbcad..f91bb2b12292 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 5876b934e544..2ff4d224cf1b 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 a3dc8ba5cbd4..3c579a1a94c9 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 fa4823362e6a..97ec3f1cdcab 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 fbc281ccb323..c505dd3fb39c 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 142d5e325236..a94e5aa9f679 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 5fbbb76cf732..d87c0acf5a74 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 53e3121a415e..dc5b98416016 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 8b9867b4cc4d..c132d42feeaf 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 2c4b31c83a91..94b8acd25e1b 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 5932855a21a7..498c16588865 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 ce3c9ee662e1..81e8c99d49ba 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 c7bea2b4409c..1eaa4f9a2c9b 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 d78b8940d3aa..7209a613fe82 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 cdfa2ad2ed22..662ccbe61846 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 30bc1f1f0def..77636acfd26c 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 b346906fc8ac..bb68f70d13fc 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 ea95b4df24cb..981141b02e15 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 793bb3c1ff8f..da1f6da935d8 100644 --- a/server/src/types/plugin-schema.types.ts +++ b/server/src/types/plugin-schema.types.ts @@ -3,33 +3,54 @@ * Based on JSON Schema Draft 7 */ -export type JSONSchemaType = 'string' | 'number' | 'integer' | 'boolean' | 'object' | 'array' | 'null'; +import z from 'zod'; -export interface JSONSchemaProperty { - type?: JSONSchemaType | JSONSchemaType[]; - description?: string; - default?: any; - enum?: any[]; - items?: JSONSchemaProperty; - properties?: Record; - required?: string[]; - additionalProperties?: boolean | JSONSchemaProperty; -} +const JSONSchemaTypeSchema = z + .enum(['string', 'number', 'integer', 'boolean', 'object', 'array', 'null']) + .meta({ id: 'PluginJsonSchemaType' }); -export interface JSONSchema { - type: 'object'; - properties?: Record; - required?: string[]; - additionalProperties?: boolean; - description?: string; -} +const JSONSchemaPropertySchema = z + .object({ + type: JSONSchemaTypeSchema.optional(), + description: z.string().optional(), + default: z.any().optional(), + enum: z.array(z.string()).optional(), -export type ConfigValue = string | number | boolean | null | ConfigValue[] | { [key: string]: ConfigValue }; + get items() { + return JSONSchemaPropertySchema.optional(); + }, -export interface FilterConfig { - [key: string]: ConfigValue; -} + get properties() { + return z.record(z.string(), JSONSchemaPropertySchema).optional(); + }, -export interface ActionConfig { - [key: string]: ConfigValue; -} + required: z.array(z.string()).optional(), + + get additionalProperties() { + return z.union([z.boolean(), JSONSchemaPropertySchema]).optional(); + }, + }) + .meta({ id: 'PluginJsonSchemaProperty' }); + +export type JSONSchemaProperty = z.infer; + +export const JSONSchemaSchema = z + .object({ + type: JSONSchemaTypeSchema.optional(), + properties: z.record(z.string(), JSONSchemaPropertySchema).optional(), + required: z.array(z.string()).optional(), + additionalProperties: z.boolean().optional(), + description: z.string().optional(), + }) + .meta({ id: 'PluginJsonSchema' }); +export type JSONSchema = z.infer; + +type ConfigValue = string | number | boolean | null | ConfigValue[] | { [key: string]: ConfigValue }; + +const ConfigValueSchema: z.ZodType = z.any().meta({ id: 'PluginConfigValue' }); + +export const FilterConfigSchema = z.record(z.string(), ConfigValueSchema).meta({ id: 'WorkflowFilterConfig' }); +export type FilterConfig = z.infer; + +export const ActionConfigSchema = z.record(z.string(), ConfigValueSchema).meta({ id: 'WorkflowActionConfig' }); +export type ActionConfig = z.infer; diff --git a/server/src/utils/bbox.ts b/server/src/utils/bbox.ts deleted file mode 100644 index ad02e8355ed7..000000000000 --- 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 a669af31cf9e..df7d05978c51 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 092a0e6619d0..d4de1eba86f4 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 4c5d5ddfc43c..9c8822518b39 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 7d2e99a21521..450563cf7e37 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 631ba60a607a..434ac89ceef3 100644 --- a/server/src/validation.spec.ts +++ b/server/src/validation.spec.ts @@ -1,92 +1,45 @@ -import { plainToInstance } from 'class-transformer'; -import { validate } from 'class-validator'; -import { DateTime } from 'luxon'; -import { IsDateStringFormat, IsNotSiblingOf, MaxDateString, Optional } from 'src/validation'; -import { describe } from 'vitest'; +import { IsNotSiblingOf } from 'src/validation'; +import { describe, expect, it } from 'vitest'; +import z from 'zod'; describe('Validation', () => { - describe('MaxDateString', () => { - const maxDate = DateTime.fromISO('2000-01-01', { zone: 'utc' }); - - class MyDto { - @MaxDateString(maxDate) - date!: string; - } - - it('passes when date is before maxDate', async () => { - const dto = plainToInstance(MyDto, { date: '1999-12-31' }); - await expect(validate(dto)).resolves.toHaveLength(0); - }); - - it('passes when date is equal to maxDate', async () => { - const dto = plainToInstance(MyDto, { date: '2000-01-01' }); - await expect(validate(dto)).resolves.toHaveLength(0); - }); - - it('fails when date is after maxDate', async () => { - const dto = plainToInstance(MyDto, { date: '2010-01-01' }); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - }); - - describe('IsDateStringFormat', () => { - class MyDto { - @IsDateStringFormat('yyyy-MM-dd') - date!: string; - } - - it('passes when date is valid', async () => { - const dto = plainToInstance(MyDto, { date: '1999-12-31' }); - await expect(validate(dto)).resolves.toHaveLength(0); - }); - - it('fails when date has invalid format', async () => { - const dto = plainToInstance(MyDto, { date: '2000-01-01T00:00:00Z' }); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - - it('fails when empty string', async () => { - const dto = plainToInstance(MyDto, { date: '' }); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - - it('fails when undefined', async () => { - const dto = plainToInstance(MyDto, {}); - await expect(validate(dto)).resolves.toHaveLength(1); - }); - }); - describe('IsNotSiblingOf', () => { - class MyDto { - @IsNotSiblingOf(['attribute2']) - @Optional() - attribute1?: string; - - @IsNotSiblingOf(['attribute1', 'attribute3']) - @Optional() - attribute2?: string; - - @IsNotSiblingOf(['attribute2']) - @Optional() - attribute3?: string; - - @Optional() - unrelatedAttribute?: string; - } - - it('passes when only one attribute is present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', unrelatedAttribute: 'value2' }); - await expect(validate(dto)).resolves.toHaveLength(0); + const MySchemaBase = z.object({ + attribute1: z.string().optional(), + attribute2: z.string().optional(), + attribute3: z.string().optional(), + unrelatedAttribute: z.string().optional(), }); - it('fails when colliding attributes are present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', attribute2: 'value2' }); - await expect(validate(dto)).resolves.toHaveLength(2); + const MySchema = MySchemaBase.pipe(IsNotSiblingOf(MySchemaBase, 'attribute1', ['attribute2'])) + .pipe(IsNotSiblingOf(MySchemaBase, 'attribute2', ['attribute1', 'attribute3'])) + .pipe(IsNotSiblingOf(MySchemaBase, 'attribute3', ['attribute2'])); + + it('passes when only one attribute is present', () => { + const result = MySchema.safeParse({ + attribute1: 'value1', + unrelatedAttribute: 'value2', + }); + expect(result.success).toBe(true); }); - it('passes when no colliding attributes are present', async () => { - const dto = plainToInstance(MyDto, { attribute1: 'value1', attribute3: 'value2' }); - await expect(validate(dto)).resolves.toHaveLength(0); + it('fails when colliding attributes are present', () => { + const result = MySchema.safeParse({ + attribute1: 'value1', + attribute2: 'value2', + }); + expect(result.success).toBe(false); + if (!result.success) { + expect(result.error.issues[0].message).toBe('attribute1 cannot exist alongside attribute2'); + } + }); + + it('passes when no colliding attributes are present', () => { + const result = MySchema.safeParse({ + attribute1: 'value1', + attribute3: 'value2', + }); + expect(result.success).toBe(true); }); }); }); diff --git a/server/src/validation.ts b/server/src/validation.ts index b959de94b127..54e3b1820ea4 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -1,40 +1,62 @@ -import { - ArgumentMetadata, - BadRequestException, - FileValidator, - Injectable, - ParseUUIDPipe, - applyDecorators, -} from '@nestjs/common'; -import { ApiProperty, ApiPropertyOptions } from '@nestjs/swagger'; -import { Transform } from 'class-transformer'; -import { - IsArray, - IsBoolean, - IsDate, - IsEnum, - IsHexColor, - IsNotEmpty, - IsOptional, - IsString, - IsUUID, - Matches, - Validate, - ValidateBy, - ValidateIf, - ValidationArguments, - ValidationOptions, - ValidatorConstraint, - ValidatorConstraintInterface, - buildMessage, - isDateString, - isDefined, -} from 'class-validator'; -import { CronJob } from 'cron'; -import { DateTime } from 'luxon'; +import { ArgumentMetadata, FileValidator, Injectable, ParseUUIDPipe } from '@nestjs/common'; +import { createZodDto } from 'nestjs-zod'; import sanitize from 'sanitize-filename'; -import { Property, PropertyOptions } from 'src/decorators'; import { isIP, isIPRange } from 'validator'; +import z from 'zod'; + +export type IsIPRangeOptions = { requireCIDR?: boolean }; + +function isIPOrRange(value: string, options?: IsIPRangeOptions): boolean { + const { requireCIDR = true } = options ?? {}; + if (isIPRange(value)) { + return true; + } + if (!requireCIDR && isIP(value)) { + return true; + } + return false; +} + +/** + * Zod schema that validates an array of strings as IP addresses or IP/CIDR ranges. + * When requireCIDR is true (default), plain IPs are rejected; only CIDR ranges are allowed. + * + * @example + * z.string().optional().transform(...).pipe(IsIPRange()) + * @example + * z.string().optional().transform(...).pipe(IsIPRange({ requireCIDR: false })) + */ +export function IsIPRange(options?: IsIPRangeOptions) { + return z + .array(z.string()) + .refine((arr) => arr.every((item) => isIPOrRange(item, options)), 'Must be an ip address or ip address range'); +} + +/** + * Zod schema that validates sibling-exclusion for object schemas. + * Validation passes when the target property is missing, or when none of the sibling properties are present. + * Use with .pipe() like IsIPRange. + * + * @example + * const Schema = z.object({ a: z.string().optional(), b: z.string().optional() }); + * Schema.pipe(IsNotSiblingOf(Schema, 'a', ['b'])); + */ +export function IsNotSiblingOf< + TSchema extends z.ZodObject, + TKey extends z.infer> & keyof z.infer, +>(_schema: TSchema, property: TKey, siblings: TKey[]) { + type T = z.infer; + const message = `${String(property)} cannot exist alongside ${siblings.join(' or ')}`; + return z.custom().refine( + (data) => { + if (data[property] === undefined) { + return true; + } + return siblings.every((sibling) => data[sibling] === undefined); + }, + { message }, + ); +} @Injectable() export class ParseMeUUIDPipe extends ParseUUIDPipe { @@ -66,386 +88,163 @@ export class FileNotEmptyValidator extends FileValidator { } } -type UUIDOptions = { optional?: boolean; each?: boolean; nullable?: boolean }; -export const ValidateUUID = (options?: UUIDOptions & PropertyOptions) => { - const { optional, each, nullable, ...apiPropertyOptions } = { - optional: false, - each: false, - nullable: false, - ...options, - }; - return applyDecorators( - IsUUID('4', { each }), - Property({ format: 'uuid', ...apiPropertyOptions }), - optional ? Optional({ nullable }) : IsNotEmpty(), - each ? IsArray() : IsString(), - ); -}; +const UUIDParamSchema = z.object({ + id: z.uuidv4(), +}); -export function IsAxisAlignedRotation() { - return ValidateBy( - { - name: 'isAxisAlignedRotation', - validator: { - validate(value: any) { - return [0, 90, 180, 270].includes(value); - }, - defaultMessage: buildMessage( - (eachPrefix) => eachPrefix + '$property must be one of the following values: 0, 90, 180, 270', - {}, - ), - }, - }, - {}, - ); -} +export class UUIDParamDto extends createZodDto(UUIDParamSchema) {} -@ValidatorConstraint({ name: 'uniqueEditActions' }) -class UniqueEditActionsValidator implements ValidatorConstraintInterface { - validate(edits: { action: string; parameters?: unknown }[]): boolean { - if (!Array.isArray(edits)) { - return true; - } +const UUIDAssetIDParamSchema = z.object({ + id: z.uuidv4(), + assetId: z.uuidv4(), +}); - const actionSet = new Set(); - for (const edit of edits) { - const key = edit.action === 'mirror' ? `${edit.action}-${JSON.stringify(edit.parameters)}` : edit.action; - if (actionSet.has(key)) { - return false; - } - actionSet.add(key); - } - return true; - } +export class UUIDAssetIDParamDto extends createZodDto(UUIDAssetIDParamSchema) {} - defaultMessage(): string { - return 'Duplicate edit actions are not allowed'; - } -} +const FilenameParamSchema = z.object({ + filename: z.string().regex(/^[a-zA-Z0-9_\-.]+$/, { + error: 'Filename contains invalid characters', + }), +}); -export const IsUniqueEditActions = () => Validate(UniqueEditActionsValidator); - -export class UUIDParamDto { - @IsNotEmpty() - @IsUUID('4') - @ApiProperty({ format: 'uuid' }) - id!: string; -} - -export class UUIDAssetIDParamDto { - @ValidateUUID() - id!: string; - - @ValidateUUID() - assetId!: string; -} - -export class FilenameParamDto { - @IsNotEmpty() - @IsString() - @ApiProperty({ format: 'string' }) - @Matches(/^[a-zA-Z0-9_\-.]+$/, { - message: 'Filename contains invalid characters', - }) - filename!: string; -} - -type PinCodeOptions = { optional?: boolean } & OptionalOptions; -export const PinCode = (options?: PinCodeOptions & ApiPropertyOptions) => { - const { optional, nullable, emptyToNull, ...apiPropertyOptions } = { - optional: false, - nullable: false, - emptyToNull: false, - ...options, - }; - const decorators = [ - IsString(), - IsNotEmpty(), - Matches(/^\d{6}$/, { message: ({ property }) => `${property} must be a 6-digit numeric string` }), - ApiProperty({ example: '123456', ...apiPropertyOptions }), - ]; - - if (optional) { - decorators.push(Optional({ nullable, emptyToNull })); - } - - return applyDecorators(...decorators); -}; - -export interface OptionalOptions { - nullable?: boolean; - /** convert empty strings to null */ - emptyToNull?: boolean; -} - -/** - * Checks if value is missing and if so, ignores all validators. - * - * @param validationOptions {@link OptionalOptions} - * - * @see IsOptional exported from `class-validator. - */ -// https://stackoverflow.com/a/71353929 -export function Optional({ nullable, emptyToNull, ...validationOptions }: OptionalOptions = {}) { - const decorators: PropertyDecorator[] = []; - - if (nullable === true) { - decorators.push(IsOptional(validationOptions)); - } else { - decorators.push(ValidateIf((object: any, v: any) => v !== undefined, validationOptions)); - } - - if (emptyToNull) { - decorators.push(Transform(({ value }) => (value === '' ? null : value))); - } - - return applyDecorators(...decorators); -} - -export function IsNotSiblingOf(siblings: string[], validationOptions?: ValidationOptions) { - return ValidateBy( - { - name: 'isNotSiblingOf', - constraints: siblings, - validator: { - validate(value: any, args: ValidationArguments) { - if (!isDefined(value)) { - return true; - } - return args.constraints.filter((prop) => isDefined((args.object as any)[prop])).length === 0; - }, - defaultMessage: (args: ValidationArguments) => { - return `${args.property} cannot exist alongside any of the following properties: ${args.constraints.join(', ')}`; - }, - }, - }, - validationOptions, - ); -} - -export const ValidateHexColor = () => { - const decorators = [ - IsHexColor(), - Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value)), - ]; - - return applyDecorators(...decorators); -}; - -type DateOptions = OptionalOptions & { optional?: boolean; format?: 'date' | 'date-time' }; -export const ValidateDate = (options?: DateOptions & PropertyOptions) => { - const { - optional, - nullable = false, - emptyToNull = false, - format = 'date-time', - ...apiPropertyOptions - } = options || {}; - - return applyDecorators( - Property({ format, ...apiPropertyOptions }), - IsDate(), - optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), - Transform(({ key, value }) => { - if (value === null || value === undefined) { - return value; - } - - if (!isDateString(value)) { - throw new BadRequestException(`${key} must be a date string`); - } - - return new Date(value as string); - }), - ); -}; - -type StringOptions = OptionalOptions & { optional?: boolean; trim?: boolean }; -export const ValidateString = (options?: StringOptions & ApiPropertyOptions) => { - const { optional, nullable, emptyToNull, trim, ...apiPropertyOptions } = options || {}; - const decorators = [ - ApiProperty(apiPropertyOptions), - IsString(), - optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), - ]; - - if (trim) { - decorators.push(Transform(({ value }: { value: string }) => value?.trim())); - } - - return applyDecorators(...decorators); -}; - -type BooleanOptions = OptionalOptions & { optional?: boolean }; -export const ValidateBoolean = (options?: BooleanOptions & PropertyOptions) => { - const { optional, nullable, emptyToNull, ...apiPropertyOptions } = options || {}; - const decorators = [ - Property(apiPropertyOptions), - IsBoolean(), - Transform(({ value }) => { - if (value == 'true') { - return true; - } else if (value == 'false') { - return false; - } - return value; - }), - optional ? Optional({ nullable, emptyToNull }) : IsNotEmpty(), - ]; - - return applyDecorators(...decorators); -}; - -type EnumOptions = { - enum: T; - name: string; - each?: boolean; - optional?: boolean; - nullable?: boolean; - default?: T[keyof T]; - description?: string; -}; -export const ValidateEnum = ({ - name, - enum: value, - each, - optional, - nullable, - default: defaultValue, - description, -}: EnumOptions) => { - return applyDecorators( - optional ? Optional({ nullable }) : IsNotEmpty(), - IsEnum(value, { each }), - ApiProperty({ enumName: name, enum: value, isArray: each, default: defaultValue, description }), - ); -}; - -@ValidatorConstraint({ name: 'cronValidator' }) -class CronValidator implements ValidatorConstraintInterface { - validate(expression: string): boolean { - try { - new CronJob(expression, () => {}); - return true; - } catch { - return false; - } - } -} - -export const IsCronExpression = () => Validate(CronValidator, { message: 'Invalid cron expression' }); - -type IValue = { value: unknown }; - -export const toEmail = ({ value }: IValue) => (typeof value === 'string' ? value.toLowerCase() : value); - -export const toSanitized = ({ value }: IValue) => { - const input = typeof value === 'string' ? value : ''; - return sanitize(input.replaceAll('.', '')); -}; +export class FilenameParamDto extends createZodDto(FilenameParamSchema) {} export const isValidInteger = (value: number, options: { min?: number; max?: number }): value is number => { const { min = Number.MIN_SAFE_INTEGER, max = Number.MAX_SAFE_INTEGER } = options; return Number.isInteger(value) && value >= min && value <= max; }; -export function isDateStringFormat(value: unknown, format: string) { - if (typeof value !== 'string') { - return false; - } - return DateTime.fromFormat(value, format, { zone: 'utc' }).isValid; -} +/** + * Unified email validation + * Converts email strings to lowercase and validates against HTML5 email regex + * @docs https://zod.dev/api?id=email + */ +export const toEmail = z + .email({ + pattern: z.regexes.html5Email, + error: (iss) => `Invalid input: expected email, received ${typeof iss.input}`, + }) + .transform((val) => val.toLowerCase()); -export function IsDateStringFormat(format: string, validationOptions?: ValidationOptions) { - return ValidateBy( +/** + * Parse ISO 8601 datetime strings to Date objects + * @docs https://zod.dev/api?id=codec + */ +export const isoDatetimeToDate = z + .codec( + z.iso.datetime({ + error: (iss) => `Invalid input: expected ISO 8601 datetime string, received ${typeof iss.input}`, + }), + z.date(), { - name: 'isDateStringFormat', - constraints: [format], - validator: { - validate(value: unknown) { - return isDateStringFormat(value, format); - }, - defaultMessage: () => `$property must be a string in the format ${format}`, - }, + decode: (isoString) => new Date(isoString), + encode: (date) => date.toISOString(), }, - validationOptions, - ); -} + ) + .meta({ example: '2024-01-01T00:00:00.000Z' }); -function maxDate(date: DateTime, maxDate: DateTime | (() => DateTime)) { - return date <= (maxDate instanceof DateTime ? maxDate : maxDate()); -} - -export function MaxDateString( - date: DateTime | (() => DateTime), - validationOptions?: ValidationOptions, -): PropertyDecorator { - return ValidateBy( +/** + * Parse ISO date strings to Date objects + * @docs https://zod.dev/api?id=codec + */ +export const isoDateToDate = z + .codec( + z.iso.date({ + error: (iss) => `Invalid input: expected ISO date string (YYYY-MM-DD), received ${typeof iss.input}`, + }), + z.date(), { - name: 'maxDateString', - constraints: [date], - validator: { - validate: (value, args) => { - const date = DateTime.fromISO(value, { zone: 'utc' }); - return maxDate(date, args?.constraints[0]); - }, - defaultMessage: buildMessage( - (eachPrefix) => 'maximal allowed date for ' + eachPrefix + '$property is $constraint1', - validationOptions, - ), - }, + decode: (isoString) => new Date(isoString), + encode: (date) => date.toISOString().slice(0, 10), }, - validationOptions, - ); -} + ) + .meta({ example: '2024-01-01' }); -type IsIPRangeOptions = { requireCIDR?: boolean }; -export function IsIPRange(options: IsIPRangeOptions, validationOptions?: ValidationOptions): PropertyDecorator { - const { requireCIDR } = { requireCIDR: true, ...options }; +export const isValidTime = z + .string() + .regex(/^([01]\d|2[0-3]):[0-5]\d$/, 'Invalid input: expected string in HH:mm format, received string'); - return ValidateBy( - { - name: 'isIPRange', - validator: { - validate: (value): boolean => { - if (isIPRange(value)) { - return true; - } +/** + * Latitude in range [-90, 90]. Reuse for body or query params. + * + * @example + * // Regular (body): optional coordinates + * latitudeSchema.optional().describe('Latitude coordinate') + * + * @example + * // Pipe (query): coerce string to number then validate range + * z.coerce.number().pipe(latitudeSchema).describe('Latitude (-90 to 90)') + */ +export const latitudeSchema = z.number().min(-90).max(90); - if (!requireCIDR && isIP(value)) { - return true; - } +/** + * Longitude in range [-180, 180]. Reuse for body or query params. + * + * @example + * // Regular (body): optional coordinates + * longitudeSchema.optional().describe('Longitude coordinate') + * + * @example + * // Pipe (query): coerce string to number then validate range + * z.coerce.number().pipe(longitudeSchema).describe('Longitude (-180 to 180)') + */ +export const longitudeSchema = z.number().min(-180).max(180); - return false; - }, - defaultMessage: buildMessage( - (eachPrefix) => eachPrefix + '$property must be an ip address, or ip address range', - validationOptions, - ), - }, - }, - validationOptions, - ); -} +/** + * Parse string to boolean + * This should be used for boolean query parameters and path parameters, but not for boolean request body parameters, as the first are always string. + * We don't use z.coerce.boolean() as any truthy value is considered true + * z.stringbool() is a more robust way to parse strings to booleans as it lets you specify the truthy and falsy values and the case sensitivity. + * @docs https://zod.dev/api?id=coercion + * @docs https://zod.dev/api?id=stringbool + */ +export const stringToBool = z + .stringbool({ truthy: ['true'], falsy: ['false'], case: 'sensitive' }) + .meta({ type: 'boolean' }); -@ValidatorConstraint({ name: 'isGreaterThanOrEqualTo' }) -export class IsGreaterThanOrEqualToConstraint implements ValidatorConstraintInterface { - validate(value: unknown, args: ValidationArguments) { - const relatedPropertyName = args.constraints?.[0] as string; - const relatedValue = (args.object as Record)[relatedPropertyName]; - if (!Number.isFinite(value) || !Number.isFinite(relatedValue)) { - return true; +/** + * Parse JSON strings from multipart/form-data + */ +export const JsonParsed = z.transform((val, ctx) => { + if (typeof val === 'string') { + try { + return JSON.parse(val); + } catch { + ctx.issues.push({ + code: 'custom', + message: `Invalid input: expected JSON string, received ${typeof val}`, + input: val, + }); + return z.NEVER; } - - return Number(value) >= Number(relatedValue); } + return val; +}); - defaultMessage(args: ValidationArguments) { - const relatedPropertyName = args.constraints?.[0] as string; - return `${args.property} must be greater than or equal to ${relatedPropertyName}`; - } -} +/** + * Hex color validation and normalization. + * Accepts formats: #RGB, #RGBA, #RRGGBB, #RRGGBBAA (with or without # prefix). + * Normalizes output to always include the # prefix. + * + * @example + * hexColor.optional() + */ +const hexColorRegex = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{4}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/; +export const hexColor = z + .string() + .regex(hexColorRegex) + .transform((val) => (val.startsWith('#') ? val : `#${val}`)); -export const IsGreaterThanOrEqualTo = (property: string, validationOptions?: ValidationOptions) => { - return Validate(IsGreaterThanOrEqualToConstraint, [property], validationOptions); -}; +/** + * Transform empty strings to null. Inner schema passed to this function must accept null. + * @docs https://zod.dev/api?id=preprocess + * @example emptyStringToNull(z.string().nullable()).optional() // [encouraged] final schema is optional + * @example emptyStringToNull(z.string().nullable()) // [encouraged] same as the one above, but final schema is not optional + * @example emptyStringToNull(z.string().nullish()) // [discouraged] same as the one above, might be confusing + * @example emptyStringToNull(z.string().optional()) // fails: string schema rejects null + * @example emptyStringToNull(z.string().nullable()).nullish() // [discouraged] passes, null is duplicated. use the first example instead + */ +export const emptyStringToNull = (schema: T) => + z.preprocess((val) => (val === '' ? null : val), schema); + +export const sanitizeFilename = z.string().transform((val) => sanitize(val.replaceAll('.', ''))); diff --git a/server/test/utils.ts b/server/test/utils.ts index b3e47b2b7e0a..aa9a9735bfdc 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 627cdded50b4..24bf3739e2a0 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,