Compare commits

...

16 Commits

Author SHA1 Message Date
timonrieger
c2b6e911e8 split search opts from scope 2026-07-24 01:06:09 +02:00
timonrieger
7a5f8d954a enum op exhaustion 2026-07-24 00:38:21 +02:00
timonrieger
43b7c669e4 page defaults 2026-07-24 00:38:18 +02:00
timonrieger
0231169135 tests 2026-07-24 00:20:06 +02:00
timonrieger
1a13dff3c1 regen openapi 2026-07-23 23:37:05 +02:00
timonrieger
3a7bb760d3 deprecate controller 2026-07-23 23:34:06 +02:00
timonrieger
702a14cd76 fmt 2026-07-23 23:29:17 +02:00
timonrieger
ae428c20a8 services and simplify 2026-07-23 23:27:39 +02:00
timonrieger
55aa05b4c9 repos 2026-07-23 18:16:45 +02:00
timonrieger
fc4edf8d06 dtos 2026-07-23 18:07:45 +02:00
timonrieger
4f5e3cc234 enum condition filters 2026-07-23 17:34:02 +02:00
timonrieger
2f83615195 cursor en-/decoder 2026-07-23 16:08:27 +02:00
NOBOIKE
cbb565b7b7 fix(web): mirror asset viewer navigation icons in RTL (#30151) 2026-07-23 12:22:45 +02:00
okxint
3bd580e37d fix(web): restore correct back route when opening person asset via direct URL (#30129) 2026-07-23 09:06:59 +00:00
Timon
f24a128319 feat(server): new search schemas and query builders (#28686) 2026-07-22 22:33:23 +00:00
Santo Shakil
36dfc98527 fix(mobile): birthday picker date order follows locale (#29419)
* fix(mobile): birthday picker date order follows locale

* test(mobile): cover date order locales for the birthday picker
2026-07-23 02:32:17 +05:30
53 changed files with 10930 additions and 275 deletions

View File

@@ -65,6 +65,7 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEdi
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(16.0)),
child: ScrollDatePicker(
viewType: datePickerColumnOrder(DateFormat.yMd(context.locale.toLanguageTag()).pattern),
options: DatePickerOptions(
backgroundColor: context.colorScheme.surfaceContainerHigh,
itemExtent: 50,
@@ -118,3 +119,18 @@ class _DriftPersonNameEditFormState extends ConsumerState<DriftPersonBirthdayEdi
);
}
}
List<DatePickerViewType>? datePickerColumnOrder(String? pattern) {
if (pattern == null) {
return null;
}
final positions = {
DatePickerViewType.year: pattern.indexOf('y'),
DatePickerViewType.month: pattern.indexOf('M'),
DatePickerViewType.day: pattern.indexOf('d'),
};
if (positions.values.any((position) => position < 0)) {
return null;
}
return positions.keys.toList()..sort((a, b) => positions[a]!.compareTo(positions[b]!));
}

View File

@@ -148,6 +148,7 @@ Class | Method | HTTP request | Description
*DeprecatedApi* | [**createPartnerDeprecated**](doc//DeprecatedApi.md#createpartnerdeprecated) | **POST** /partners/{id} | Create a partner
*DeprecatedApi* | [**getQueuesLegacy**](doc//DeprecatedApi.md#getqueueslegacy) | **GET** /jobs | Retrieve queue counts and status
*DeprecatedApi* | [**runQueueCommandLegacy**](doc//DeprecatedApi.md#runqueuecommandlegacy) | **PUT** /jobs/{name} | Run jobs
*DeprecatedApi* | [**searchLargeAssets**](doc//DeprecatedApi.md#searchlargeassets) | **POST** /search/large-assets | Search large assets
*DeprecatedApi* | [**updateApiKey**](doc//DeprecatedApi.md#updateapikey) | **PUT** /api-keys/{id} | Update an API key
*DeprecatedApi* | [**updateAsset**](doc//DeprecatedApi.md#updateasset) | **PUT** /assets/{id} | Update an asset
*DeprecatedApi* | [**updateAssets**](doc//DeprecatedApi.md#updateassets) | **PUT** /assets | Update assets
@@ -414,6 +415,7 @@ Class | Method | HTTP request | Description
- [AudioCodec](doc//AudioCodec.md)
- [AuthStatusResponseDto](doc//AuthStatusResponseDto.md)
- [AvatarUpdate](doc//AvatarUpdate.md)
- [BoolFilter](doc//BoolFilter.md)
- [BulkIdErrorReason](doc//BulkIdErrorReason.md)
- [BulkIdResponseDto](doc//BulkIdResponseDto.md)
- [BulkIdsDto](doc//BulkIdsDto.md)
@@ -435,6 +437,8 @@ Class | Method | HTTP request | Description
- [DatabaseBackupDeleteDto](doc//DatabaseBackupDeleteDto.md)
- [DatabaseBackupDto](doc//DatabaseBackupDto.md)
- [DatabaseBackupListResponseDto](doc//DatabaseBackupListResponseDto.md)
- [DateFilter](doc//DateFilter.md)
- [DateFilterNullable](doc//DateFilterNullable.md)
- [DownloadArchiveDto](doc//DownloadArchiveDto.md)
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
- [DownloadInfoDto](doc//DownloadInfoDto.md)
@@ -447,12 +451,17 @@ Class | Method | HTTP request | Description
- [DuplicateResponseDto](doc//DuplicateResponseDto.md)
- [EmailNotificationsResponse](doc//EmailNotificationsResponse.md)
- [EmailNotificationsUpdate](doc//EmailNotificationsUpdate.md)
- [EnumFilterAssetType](doc//EnumFilterAssetType.md)
- [EnumFilterAssetVisibility](doc//EnumFilterAssetVisibility.md)
- [ExifResponseDto](doc//ExifResponseDto.md)
- [FaceDto](doc//FaceDto.md)
- [FacialRecognitionConfig](doc//FacialRecognitionConfig.md)
- [FoldersResponse](doc//FoldersResponse.md)
- [FoldersUpdate](doc//FoldersUpdate.md)
- [HlsVideoResolution](doc//HlsVideoResolution.md)
- [IdFilter](doc//IdFilter.md)
- [IdFilterNullable](doc//IdFilterNullable.md)
- [IdsFilter](doc//IdsFilter.md)
- [ImageFormat](doc//ImageFormat.md)
- [IntegrityReport](doc//IntegrityReport.md)
- [IntegrityReportResponseDto](doc//IntegrityReportResponseDto.md)
@@ -497,6 +506,8 @@ Class | Method | HTTP request | Description
- [NotificationType](doc//NotificationType.md)
- [NotificationUpdateAllDto](doc//NotificationUpdateAllDto.md)
- [NotificationUpdateDto](doc//NotificationUpdateDto.md)
- [NumberFilter](doc//NumberFilter.md)
- [NumberFilterNullable](doc//NumberFilterNullable.md)
- [OAuthAuthorizeResponseDto](doc//OAuthAuthorizeResponseDto.md)
- [OAuthCallbackDto](doc//OAuthCallbackDto.md)
- [OAuthConfigDto](doc//OAuthConfigDto.md)
@@ -559,6 +570,10 @@ Class | Method | HTTP request | Description
- [SearchExploreResponseDto](doc//SearchExploreResponseDto.md)
- [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
- [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
- [SearchFilter](doc//SearchFilter.md)
- [SearchFilterBranch](doc//SearchFilterBranch.md)
- [SearchOrder](doc//SearchOrder.md)
- [SearchOrderField](doc//SearchOrderField.md)
- [SearchResponseDto](doc//SearchResponseDto.md)
- [SearchStatisticsResponseDto](doc//SearchStatisticsResponseDto.md)
- [SearchSuggestionType](doc//SearchSuggestionType.md)
@@ -593,6 +608,10 @@ Class | Method | HTTP request | Description
- [StackUpdateDto](doc//StackUpdateDto.md)
- [StatisticsSearchDto](doc//StatisticsSearchDto.md)
- [StorageFolder](doc//StorageFolder.md)
- [StringFilter](doc//StringFilter.md)
- [StringFilterNullable](doc//StringFilterNullable.md)
- [StringPatternFilter](doc//StringPatternFilter.md)
- [StringSimilarityFilter](doc//StringSimilarityFilter.md)
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
- [SyncAckDto](doc//SyncAckDto.md)
- [SyncAckSetDto](doc//SyncAckSetDto.md)

View File

@@ -135,6 +135,7 @@ part 'model/asset_visibility.dart';
part 'model/audio_codec.dart';
part 'model/auth_status_response_dto.dart';
part 'model/avatar_update.dart';
part 'model/bool_filter.dart';
part 'model/bulk_id_error_reason.dart';
part 'model/bulk_id_response_dto.dart';
part 'model/bulk_ids_dto.dart';
@@ -156,6 +157,8 @@ part 'model/database_backup_config.dart';
part 'model/database_backup_delete_dto.dart';
part 'model/database_backup_dto.dart';
part 'model/database_backup_list_response_dto.dart';
part 'model/date_filter.dart';
part 'model/date_filter_nullable.dart';
part 'model/download_archive_dto.dart';
part 'model/download_archive_info.dart';
part 'model/download_info_dto.dart';
@@ -168,12 +171,17 @@ part 'model/duplicate_resolve_group_dto.dart';
part 'model/duplicate_response_dto.dart';
part 'model/email_notifications_response.dart';
part 'model/email_notifications_update.dart';
part 'model/enum_filter_asset_type.dart';
part 'model/enum_filter_asset_visibility.dart';
part 'model/exif_response_dto.dart';
part 'model/face_dto.dart';
part 'model/facial_recognition_config.dart';
part 'model/folders_response.dart';
part 'model/folders_update.dart';
part 'model/hls_video_resolution.dart';
part 'model/id_filter.dart';
part 'model/id_filter_nullable.dart';
part 'model/ids_filter.dart';
part 'model/image_format.dart';
part 'model/integrity_report.dart';
part 'model/integrity_report_response_dto.dart';
@@ -218,6 +226,8 @@ part 'model/notification_level.dart';
part 'model/notification_type.dart';
part 'model/notification_update_all_dto.dart';
part 'model/notification_update_dto.dart';
part 'model/number_filter.dart';
part 'model/number_filter_nullable.dart';
part 'model/o_auth_authorize_response_dto.dart';
part 'model/o_auth_callback_dto.dart';
part 'model/o_auth_config_dto.dart';
@@ -280,6 +290,10 @@ part 'model/search_explore_item.dart';
part 'model/search_explore_response_dto.dart';
part 'model/search_facet_count_response_dto.dart';
part 'model/search_facet_response_dto.dart';
part 'model/search_filter.dart';
part 'model/search_filter_branch.dart';
part 'model/search_order.dart';
part 'model/search_order_field.dart';
part 'model/search_response_dto.dart';
part 'model/search_statistics_response_dto.dart';
part 'model/search_suggestion_type.dart';
@@ -314,6 +328,10 @@ part 'model/stack_response_dto.dart';
part 'model/stack_update_dto.dart';
part 'model/statistics_search_dto.dart';
part 'model/storage_folder.dart';
part 'model/string_filter.dart';
part 'model/string_filter_nullable.dart';
part 'model/string_pattern_filter.dart';
part 'model/string_similarity_filter.dart';
part 'model/sync_ack_delete_dto.dart';
part 'model/sync_ack_dto.dart';
part 'model/sync_ack_set_dto.dart';

View File

@@ -185,6 +185,338 @@ class DeprecatedApi {
return null;
}
/// Search large assets
///
/// Search for assets that are considered large based on specified criteria.
///
/// Note: This method returns the HTTP [Response].
///
/// Parameters:
///
/// * [List<String>] albumIds:
/// Filter by album IDs
///
/// * [String] city:
/// Filter by city name
///
/// * [String] country:
/// Filter by country name
///
/// * [DateTime] createdAfter:
/// Filter by creation date (after)
///
/// * [DateTime] createdBefore:
/// Filter by creation date (before)
///
/// * [bool] isEncoded:
/// Filter by encoded status
///
/// * [bool] isFavorite:
/// Filter by favorite status
///
/// * [bool] isMotion:
/// Filter by motion photo status
///
/// * [bool] isNotInAlbum:
/// Filter assets not in any album
///
/// * [bool] isOffline:
/// Filter by offline status
///
/// * [String] lensModel:
/// Filter by lens model
///
/// * [String] libraryId:
/// Library ID to filter by
///
/// * [String] make:
/// Filter by camera make
///
/// * [int] minFileSize:
/// Minimum file size in bytes
///
/// * [String] model:
/// Filter by camera model
///
/// * [String] ocr:
/// Filter by OCR text content
///
/// * [List<String>] personIds:
/// Filter by person IDs
///
/// * [int] rating:
/// Filter by rating [1-5], or null for unrated
///
/// * [int] size:
/// Number of results to return
///
/// * [String] state:
/// Filter by state/province name
///
/// * [List<String>] tagIds:
/// Filter by tag IDs
///
/// * [DateTime] takenAfter:
/// Filter by taken date (after)
///
/// * [DateTime] takenBefore:
/// Filter by taken date (before)
///
/// * [DateTime] trashedAfter:
/// Filter by trash date (after)
///
/// * [DateTime] trashedBefore:
/// Filter by trash date (before)
///
/// * [AssetTypeEnum] type:
///
/// * [DateTime] updatedAfter:
/// Filter by update date (after)
///
/// * [DateTime] updatedBefore:
/// Filter by update date (before)
///
/// * [AssetVisibility] visibility:
///
/// * [bool] withDeleted:
/// Include deleted assets
///
/// * [bool] withExif:
/// Include EXIF data in response
Future<Response> searchLargeAssetsWithHttpInfo({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, Future<void>? abortTrigger, }) async {
// ignore: prefer_const_declarations
final apiPath = r'/search/large-assets';
// ignore: prefer_final_locals
Object? postBody;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
if (albumIds != null) {
queryParams.addAll(_queryParams('multi', 'albumIds', albumIds));
}
if (city != null) {
queryParams.addAll(_queryParams('', 'city', city));
}
if (country != null) {
queryParams.addAll(_queryParams('', 'country', country));
}
if (createdAfter != null) {
queryParams.addAll(_queryParams('', 'createdAfter', createdAfter));
}
if (createdBefore != null) {
queryParams.addAll(_queryParams('', 'createdBefore', createdBefore));
}
if (isEncoded != null) {
queryParams.addAll(_queryParams('', 'isEncoded', isEncoded));
}
if (isFavorite != null) {
queryParams.addAll(_queryParams('', 'isFavorite', isFavorite));
}
if (isMotion != null) {
queryParams.addAll(_queryParams('', 'isMotion', isMotion));
}
if (isNotInAlbum != null) {
queryParams.addAll(_queryParams('', 'isNotInAlbum', isNotInAlbum));
}
if (isOffline != null) {
queryParams.addAll(_queryParams('', 'isOffline', isOffline));
}
if (lensModel != null) {
queryParams.addAll(_queryParams('', 'lensModel', lensModel));
}
if (libraryId != null) {
queryParams.addAll(_queryParams('', 'libraryId', libraryId));
}
if (make != null) {
queryParams.addAll(_queryParams('', 'make', make));
}
if (minFileSize != null) {
queryParams.addAll(_queryParams('', 'minFileSize', minFileSize));
}
if (model != null) {
queryParams.addAll(_queryParams('', 'model', model));
}
if (ocr != null) {
queryParams.addAll(_queryParams('', 'ocr', ocr));
}
if (personIds != null) {
queryParams.addAll(_queryParams('multi', 'personIds', personIds));
}
if (rating != null) {
queryParams.addAll(_queryParams('', 'rating', rating));
}
if (size != null) {
queryParams.addAll(_queryParams('', 'size', size));
}
if (state != null) {
queryParams.addAll(_queryParams('', 'state', state));
}
if (tagIds != null) {
queryParams.addAll(_queryParams('multi', 'tagIds', tagIds));
}
if (takenAfter != null) {
queryParams.addAll(_queryParams('', 'takenAfter', takenAfter));
}
if (takenBefore != null) {
queryParams.addAll(_queryParams('', 'takenBefore', takenBefore));
}
if (trashedAfter != null) {
queryParams.addAll(_queryParams('', 'trashedAfter', trashedAfter));
}
if (trashedBefore != null) {
queryParams.addAll(_queryParams('', 'trashedBefore', trashedBefore));
}
if (type != null) {
queryParams.addAll(_queryParams('', 'type', type));
}
if (updatedAfter != null) {
queryParams.addAll(_queryParams('', 'updatedAfter', updatedAfter));
}
if (updatedBefore != null) {
queryParams.addAll(_queryParams('', 'updatedBefore', updatedBefore));
}
if (visibility != null) {
queryParams.addAll(_queryParams('', 'visibility', visibility));
}
if (withDeleted != null) {
queryParams.addAll(_queryParams('', 'withDeleted', withDeleted));
}
if (withExif != null) {
queryParams.addAll(_queryParams('', 'withExif', withExif));
}
const contentTypes = <String>[];
return apiClient.invokeAPI(
apiPath,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
abortTrigger: abortTrigger,
);
}
/// Search large assets
///
/// Search for assets that are considered large based on specified criteria.
///
/// Parameters:
///
/// * [List<String>] albumIds:
/// Filter by album IDs
///
/// * [String] city:
/// Filter by city name
///
/// * [String] country:
/// Filter by country name
///
/// * [DateTime] createdAfter:
/// Filter by creation date (after)
///
/// * [DateTime] createdBefore:
/// Filter by creation date (before)
///
/// * [bool] isEncoded:
/// Filter by encoded status
///
/// * [bool] isFavorite:
/// Filter by favorite status
///
/// * [bool] isMotion:
/// Filter by motion photo status
///
/// * [bool] isNotInAlbum:
/// Filter assets not in any album
///
/// * [bool] isOffline:
/// Filter by offline status
///
/// * [String] lensModel:
/// Filter by lens model
///
/// * [String] libraryId:
/// Library ID to filter by
///
/// * [String] make:
/// Filter by camera make
///
/// * [int] minFileSize:
/// Minimum file size in bytes
///
/// * [String] model:
/// Filter by camera model
///
/// * [String] ocr:
/// Filter by OCR text content
///
/// * [List<String>] personIds:
/// Filter by person IDs
///
/// * [int] rating:
/// Filter by rating [1-5], or null for unrated
///
/// * [int] size:
/// Number of results to return
///
/// * [String] state:
/// Filter by state/province name
///
/// * [List<String>] tagIds:
/// Filter by tag IDs
///
/// * [DateTime] takenAfter:
/// Filter by taken date (after)
///
/// * [DateTime] takenBefore:
/// Filter by taken date (before)
///
/// * [DateTime] trashedAfter:
/// Filter by trash date (after)
///
/// * [DateTime] trashedBefore:
/// Filter by trash date (before)
///
/// * [AssetTypeEnum] type:
///
/// * [DateTime] updatedAfter:
/// Filter by update date (after)
///
/// * [DateTime] updatedBefore:
/// Filter by update date (before)
///
/// * [AssetVisibility] visibility:
///
/// * [bool] withDeleted:
/// Include deleted assets
///
/// * [bool] withExif:
/// Include EXIF data in response
Future<List<AssetResponseDto>?> searchLargeAssets({ List<String>? albumIds, String? city, String? country, DateTime? createdAfter, DateTime? createdBefore, bool? isEncoded, bool? isFavorite, bool? isMotion, bool? isNotInAlbum, bool? isOffline, String? lensModel, String? libraryId, String? make, int? minFileSize, String? model, String? ocr, List<String>? personIds, int? rating, int? size, String? state, List<String>? tagIds, DateTime? takenAfter, DateTime? takenBefore, DateTime? trashedAfter, DateTime? trashedBefore, AssetTypeEnum? type, DateTime? updatedAfter, DateTime? updatedBefore, AssetVisibility? visibility, bool? withDeleted, bool? withExif, Future<void>? abortTrigger, }) async {
final response = await searchLargeAssetsWithHttpInfo(albumIds: albumIds, city: city, country: country, createdAfter: createdAfter, createdBefore: createdBefore, isEncoded: isEncoded, isFavorite: isFavorite, isMotion: isMotion, isNotInAlbum: isNotInAlbum, isOffline: isOffline, lensModel: lensModel, libraryId: libraryId, make: make, minFileSize: minFileSize, model: model, ocr: ocr, personIds: personIds, rating: rating, size: size, state: state, tagIds: tagIds, takenAfter: takenAfter, takenBefore: takenBefore, trashedAfter: trashedAfter, trashedBefore: trashedBefore, type: type, updatedAfter: updatedAfter, updatedBefore: updatedBefore, visibility: visibility, withDeleted: withDeleted, withExif: withExif, abortTrigger: abortTrigger,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
// When a remote server returns no body with a status of 204, we shall not decode it.
// 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) {
final responseBody = await _decodeBodyBytes(response);
return (await apiClient.deserializeAsync(responseBody, 'List<AssetResponseDto>') as List)
.cast<AssetResponseDto>()
.toList(growable: false);
}
return null;
}
/// Update an API key
///
/// Updates the name and permissions of an API key by its ID. The current user must own this API key.

View File

@@ -315,6 +315,8 @@ class ApiClient {
return AuthStatusResponseDto.fromJson(value);
case 'AvatarUpdate':
return AvatarUpdate.fromJson(value);
case 'BoolFilter':
return BoolFilter.fromJson(value);
case 'BulkIdErrorReason':
return BulkIdErrorReasonTypeTransformer().decode(value);
case 'BulkIdResponseDto':
@@ -357,6 +359,10 @@ class ApiClient {
return DatabaseBackupDto.fromJson(value);
case 'DatabaseBackupListResponseDto':
return DatabaseBackupListResponseDto.fromJson(value);
case 'DateFilter':
return DateFilter.fromJson(value);
case 'DateFilterNullable':
return DateFilterNullable.fromJson(value);
case 'DownloadArchiveDto':
return DownloadArchiveDto.fromJson(value);
case 'DownloadArchiveInfo':
@@ -381,6 +387,10 @@ class ApiClient {
return EmailNotificationsResponse.fromJson(value);
case 'EmailNotificationsUpdate':
return EmailNotificationsUpdate.fromJson(value);
case 'EnumFilterAssetType':
return EnumFilterAssetType.fromJson(value);
case 'EnumFilterAssetVisibility':
return EnumFilterAssetVisibility.fromJson(value);
case 'ExifResponseDto':
return ExifResponseDto.fromJson(value);
case 'FaceDto':
@@ -393,6 +403,12 @@ class ApiClient {
return FoldersUpdate.fromJson(value);
case 'HlsVideoResolution':
return HlsVideoResolutionTypeTransformer().decode(value);
case 'IdFilter':
return IdFilter.fromJson(value);
case 'IdFilterNullable':
return IdFilterNullable.fromJson(value);
case 'IdsFilter':
return IdsFilter.fromJson(value);
case 'ImageFormat':
return ImageFormatTypeTransformer().decode(value);
case 'IntegrityReport':
@@ -481,6 +497,10 @@ class ApiClient {
return NotificationUpdateAllDto.fromJson(value);
case 'NotificationUpdateDto':
return NotificationUpdateDto.fromJson(value);
case 'NumberFilter':
return NumberFilter.fromJson(value);
case 'NumberFilterNullable':
return NumberFilterNullable.fromJson(value);
case 'OAuthAuthorizeResponseDto':
return OAuthAuthorizeResponseDto.fromJson(value);
case 'OAuthCallbackDto':
@@ -605,6 +625,14 @@ class ApiClient {
return SearchFacetCountResponseDto.fromJson(value);
case 'SearchFacetResponseDto':
return SearchFacetResponseDto.fromJson(value);
case 'SearchFilter':
return SearchFilter.fromJson(value);
case 'SearchFilterBranch':
return SearchFilterBranch.fromJson(value);
case 'SearchOrder':
return SearchOrder.fromJson(value);
case 'SearchOrderField':
return SearchOrderFieldTypeTransformer().decode(value);
case 'SearchResponseDto':
return SearchResponseDto.fromJson(value);
case 'SearchStatisticsResponseDto':
@@ -673,6 +701,14 @@ class ApiClient {
return StatisticsSearchDto.fromJson(value);
case 'StorageFolder':
return StorageFolderTypeTransformer().decode(value);
case 'StringFilter':
return StringFilter.fromJson(value);
case 'StringFilterNullable':
return StringFilterNullable.fromJson(value);
case 'StringPatternFilter':
return StringPatternFilter.fromJson(value);
case 'StringSimilarityFilter':
return StringSimilarityFilter.fromJson(value);
case 'SyncAckDeleteDto':
return SyncAckDeleteDto.fromJson(value);
case 'SyncAckDto':

View File

@@ -172,6 +172,9 @@ String parameterToString(dynamic value) {
if (value is ReleaseType) {
return ReleaseTypeTypeTransformer().encode(value).toString();
}
if (value is SearchOrderField) {
return SearchOrderFieldTypeTransformer().encode(value).toString();
}
if (value is SearchSuggestionType) {
return SearchSuggestionTypeTypeTransformer().encode(value).toString();
}

View File

@@ -0,0 +1,99 @@
//
// 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 BoolFilter {
/// Returns a new [BoolFilter] instance.
BoolFilter({
required this.eq,
});
bool eq;
@override
bool operator ==(Object other) => identical(this, other) || other is BoolFilter &&
other.eq == eq;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq.hashCode);
@override
String toString() => 'BoolFilter[eq=$eq]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'eq'] = this.eq;
return json;
}
/// Returns a new [BoolFilter] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static BoolFilter? fromJson(dynamic value) {
upgradeDto(value, "BoolFilter");
if (value is Map) {
final json = value.cast<String, dynamic>();
return BoolFilter(
eq: mapValueOfType<bool>(json, r'eq')!,
);
}
return null;
}
static List<BoolFilter> listFromJson(dynamic json, {bool growable = false,}) {
final result = <BoolFilter>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = BoolFilter.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, BoolFilter> mapFromJson(dynamic json) {
final map = <String, BoolFilter>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = BoolFilter.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of BoolFilter-objects as value to a dart map
static Map<String, List<BoolFilter>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<BoolFilter>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = BoolFilter.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'eq',
};
}

199
mobile/openapi/lib/model/date_filter.dart generated Normal file
View File

@@ -0,0 +1,199 @@
//
// 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 DateFilter {
/// Returns a new [DateFilter] instance.
DateFilter({
this.eq = const Optional.absent(),
this.gt = const Optional.absent(),
this.gte = const Optional.absent(),
this.lt = const Optional.absent(),
this.lte = const Optional.absent(),
this.ne = const Optional.absent(),
});
///
/// 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.
///
Optional<DateTime?> eq;
///
/// 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.
///
Optional<DateTime?> gt;
///
/// 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.
///
Optional<DateTime?> gte;
///
/// 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.
///
Optional<DateTime?> lt;
///
/// 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.
///
Optional<DateTime?> lte;
///
/// 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.
///
Optional<DateTime?> ne;
@override
bool operator ==(Object other) => identical(this, other) || other is DateFilter &&
other.eq == eq &&
other.gt == gt &&
other.gte == gte &&
other.lt == lt &&
other.lte == lte &&
other.ne == ne;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq == null ? 0 : eq!.hashCode) +
(gt == null ? 0 : gt!.hashCode) +
(gte == null ? 0 : gte!.hashCode) +
(lt == null ? 0 : lt!.hashCode) +
(lte == null ? 0 : lte!.hashCode) +
(ne == null ? 0 : ne!.hashCode);
@override
String toString() => 'DateFilter[eq=$eq, gt=$gt, gte=$gte, lt=$lt, lte=$lte, ne=$ne]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.gt.isPresent) {
final value = this.gt.value;
json[r'gt'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.gte.isPresent) {
final value = this.gte.value;
json[r'gte'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.lt.isPresent) {
final value = this.lt.value;
json[r'lt'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.lte.isPresent) {
final value = this.lte.value;
json[r'lte'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
return json;
}
/// Returns a new [DateFilter] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static DateFilter? fromJson(dynamic value) {
upgradeDto(value, "DateFilter");
if (value is Map) {
final json = value.cast<String, dynamic>();
return DateFilter(
eq: json.containsKey(r'eq') ? Optional.present(mapDateTime(json, r'eq', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
gt: json.containsKey(r'gt') ? Optional.present(mapDateTime(json, r'gt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
gte: json.containsKey(r'gte') ? Optional.present(mapDateTime(json, r'gte', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
lt: json.containsKey(r'lt') ? Optional.present(mapDateTime(json, r'lt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
lte: json.containsKey(r'lte') ? Optional.present(mapDateTime(json, r'lte', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(mapDateTime(json, r'ne', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
);
}
return null;
}
static List<DateFilter> listFromJson(dynamic json, {bool growable = false,}) {
final result = <DateFilter>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = DateFilter.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, DateFilter> mapFromJson(dynamic json) {
final map = <String, DateFilter>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = DateFilter.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of DateFilter-objects as value to a dart map
static Map<String, List<DateFilter>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<DateFilter>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = DateFilter.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,187 @@
//
// 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 DateFilterNullable {
/// Returns a new [DateFilterNullable] instance.
DateFilterNullable({
this.eq = const Optional.absent(),
this.gt = const Optional.absent(),
this.gte = const Optional.absent(),
this.lt = const Optional.absent(),
this.lte = const Optional.absent(),
this.ne = const Optional.absent(),
});
Optional<DateTime?> eq;
///
/// 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.
///
Optional<DateTime?> gt;
///
/// 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.
///
Optional<DateTime?> gte;
///
/// 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.
///
Optional<DateTime?> lt;
///
/// 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.
///
Optional<DateTime?> lte;
Optional<DateTime?> ne;
@override
bool operator ==(Object other) => identical(this, other) || other is DateFilterNullable &&
other.eq == eq &&
other.gt == gt &&
other.gte == gte &&
other.lt == lt &&
other.lte == lte &&
other.ne == ne;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq == null ? 0 : eq!.hashCode) +
(gt == null ? 0 : gt!.hashCode) +
(gte == null ? 0 : gte!.hashCode) +
(lt == null ? 0 : lt!.hashCode) +
(lte == null ? 0 : lte!.hashCode) +
(ne == null ? 0 : ne!.hashCode);
@override
String toString() => 'DateFilterNullable[eq=$eq, gt=$gt, gte=$gte, lt=$lt, lte=$lte, ne=$ne]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.gt.isPresent) {
final value = this.gt.value;
json[r'gt'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.gte.isPresent) {
final value = this.gte.value;
json[r'gte'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.lt.isPresent) {
final value = this.lt.value;
json[r'lt'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.lte.isPresent) {
final value = this.lte.value;
json[r'lte'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value == null ? null : (_isEpochMarker(r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
return json;
}
/// Returns a new [DateFilterNullable] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static DateFilterNullable? fromJson(dynamic value) {
upgradeDto(value, "DateFilterNullable");
if (value is Map) {
final json = value.cast<String, dynamic>();
return DateFilterNullable(
eq: json.containsKey(r'eq') ? Optional.present(mapDateTime(json, r'eq', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
gt: json.containsKey(r'gt') ? Optional.present(mapDateTime(json, r'gt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
gte: json.containsKey(r'gte') ? Optional.present(mapDateTime(json, r'gte', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
lt: json.containsKey(r'lt') ? Optional.present(mapDateTime(json, r'lt', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
lte: json.containsKey(r'lte') ? Optional.present(mapDateTime(json, r'lte', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(mapDateTime(json, r'ne', r'/^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
);
}
return null;
}
static List<DateFilterNullable> listFromJson(dynamic json, {bool growable = false,}) {
final result = <DateFilterNullable>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = DateFilterNullable.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, DateFilterNullable> mapFromJson(dynamic json) {
final map = <String, DateFilterNullable>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = DateFilterNullable.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of DateFilterNullable-objects as value to a dart map
static Map<String, List<DateFilterNullable>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<DateFilterNullable>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = DateFilterNullable.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,143 @@
//
// 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 EnumFilterAssetType {
/// Returns a new [EnumFilterAssetType] instance.
EnumFilterAssetType({
this.eq = const Optional.absent(),
this.in_ = const Optional.present(const []),
this.ne = const Optional.absent(),
this.notIn = const Optional.present(const []),
});
///
/// 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.
///
Optional<AssetTypeEnum?> eq;
Optional<List<AssetTypeEnum>?> in_;
///
/// 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.
///
Optional<AssetTypeEnum?> ne;
Optional<List<AssetTypeEnum>?> notIn;
@override
bool operator ==(Object other) => identical(this, other) || other is EnumFilterAssetType &&
other.eq == eq &&
_deepEquality.equals(other.in_, in_) &&
other.ne == ne &&
_deepEquality.equals(other.notIn, notIn);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq == null ? 0 : eq!.hashCode) +
(in_.hashCode) +
(ne == null ? 0 : ne!.hashCode) +
(notIn.hashCode);
@override
String toString() => 'EnumFilterAssetType[eq=$eq, in_=$in_, ne=$ne, notIn=$notIn]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value;
}
if (this.in_.isPresent) {
final value = this.in_.value;
json[r'in'] = value;
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value;
}
if (this.notIn.isPresent) {
final value = this.notIn.value;
json[r'notIn'] = value;
}
return json;
}
/// Returns a new [EnumFilterAssetType] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EnumFilterAssetType? fromJson(dynamic value) {
upgradeDto(value, "EnumFilterAssetType");
if (value is Map) {
final json = value.cast<String, dynamic>();
return EnumFilterAssetType(
eq: json.containsKey(r'eq') ? Optional.present(AssetTypeEnum.fromJson(json[r'eq'])) : const Optional.absent(),
in_: json.containsKey(r'in') ? Optional.present(AssetTypeEnum.listFromJson(json[r'in'])) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(AssetTypeEnum.fromJson(json[r'ne'])) : const Optional.absent(),
notIn: json.containsKey(r'notIn') ? Optional.present(AssetTypeEnum.listFromJson(json[r'notIn'])) : const Optional.absent(),
);
}
return null;
}
static List<EnumFilterAssetType> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EnumFilterAssetType>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EnumFilterAssetType.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EnumFilterAssetType> mapFromJson(dynamic json) {
final map = <String, EnumFilterAssetType>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EnumFilterAssetType.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EnumFilterAssetType-objects as value to a dart map
static Map<String, List<EnumFilterAssetType>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EnumFilterAssetType>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EnumFilterAssetType.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,143 @@
//
// 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 EnumFilterAssetVisibility {
/// Returns a new [EnumFilterAssetVisibility] instance.
EnumFilterAssetVisibility({
this.eq = const Optional.absent(),
this.in_ = const Optional.present(const []),
this.ne = const Optional.absent(),
this.notIn = const Optional.present(const []),
});
///
/// 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.
///
Optional<AssetVisibility?> eq;
Optional<List<AssetVisibility>?> in_;
///
/// 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.
///
Optional<AssetVisibility?> ne;
Optional<List<AssetVisibility>?> notIn;
@override
bool operator ==(Object other) => identical(this, other) || other is EnumFilterAssetVisibility &&
other.eq == eq &&
_deepEquality.equals(other.in_, in_) &&
other.ne == ne &&
_deepEquality.equals(other.notIn, notIn);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq == null ? 0 : eq!.hashCode) +
(in_.hashCode) +
(ne == null ? 0 : ne!.hashCode) +
(notIn.hashCode);
@override
String toString() => 'EnumFilterAssetVisibility[eq=$eq, in_=$in_, ne=$ne, notIn=$notIn]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value;
}
if (this.in_.isPresent) {
final value = this.in_.value;
json[r'in'] = value;
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value;
}
if (this.notIn.isPresent) {
final value = this.notIn.value;
json[r'notIn'] = value;
}
return json;
}
/// Returns a new [EnumFilterAssetVisibility] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static EnumFilterAssetVisibility? fromJson(dynamic value) {
upgradeDto(value, "EnumFilterAssetVisibility");
if (value is Map) {
final json = value.cast<String, dynamic>();
return EnumFilterAssetVisibility(
eq: json.containsKey(r'eq') ? Optional.present(AssetVisibility.fromJson(json[r'eq'])) : const Optional.absent(),
in_: json.containsKey(r'in') ? Optional.present(AssetVisibility.listFromJson(json[r'in'])) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(AssetVisibility.fromJson(json[r'ne'])) : const Optional.absent(),
notIn: json.containsKey(r'notIn') ? Optional.present(AssetVisibility.listFromJson(json[r'notIn'])) : const Optional.absent(),
);
}
return null;
}
static List<EnumFilterAssetVisibility> listFromJson(dynamic json, {bool growable = false,}) {
final result = <EnumFilterAssetVisibility>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = EnumFilterAssetVisibility.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, EnumFilterAssetVisibility> mapFromJson(dynamic json) {
final map = <String, EnumFilterAssetVisibility>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = EnumFilterAssetVisibility.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of EnumFilterAssetVisibility-objects as value to a dart map
static Map<String, List<EnumFilterAssetVisibility>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<EnumFilterAssetVisibility>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = EnumFilterAssetVisibility.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

123
mobile/openapi/lib/model/id_filter.dart generated Normal file
View File

@@ -0,0 +1,123 @@
//
// 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 IdFilter {
/// Returns a new [IdFilter] instance.
IdFilter({
this.eq = const Optional.absent(),
this.ne = const Optional.absent(),
});
///
/// 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.
///
Optional<String?> eq;
///
/// 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.
///
Optional<String?> ne;
@override
bool operator ==(Object other) => identical(this, other) || other is IdFilter &&
other.eq == eq &&
other.ne == ne;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq == null ? 0 : eq!.hashCode) +
(ne == null ? 0 : ne!.hashCode);
@override
String toString() => 'IdFilter[eq=$eq, ne=$ne]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value;
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value;
}
return json;
}
/// Returns a new [IdFilter] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static IdFilter? fromJson(dynamic value) {
upgradeDto(value, "IdFilter");
if (value is Map) {
final json = value.cast<String, dynamic>();
return IdFilter(
eq: json.containsKey(r'eq') ? Optional.present(mapValueOfType<String>(json, r'eq')) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(mapValueOfType<String>(json, r'ne')) : const Optional.absent(),
);
}
return null;
}
static List<IdFilter> listFromJson(dynamic json, {bool growable = false,}) {
final result = <IdFilter>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = IdFilter.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, IdFilter> mapFromJson(dynamic json) {
final map = <String, IdFilter>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = IdFilter.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of IdFilter-objects as value to a dart map
static Map<String, List<IdFilter>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<IdFilter>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = IdFilter.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,111 @@
//
// 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 IdFilterNullable {
/// Returns a new [IdFilterNullable] instance.
IdFilterNullable({
this.eq = const Optional.absent(),
this.ne = const Optional.absent(),
});
Optional<String?> eq;
Optional<String?> ne;
@override
bool operator ==(Object other) => identical(this, other) || other is IdFilterNullable &&
other.eq == eq &&
other.ne == ne;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq == null ? 0 : eq!.hashCode) +
(ne == null ? 0 : ne!.hashCode);
@override
String toString() => 'IdFilterNullable[eq=$eq, ne=$ne]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value;
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value;
}
return json;
}
/// Returns a new [IdFilterNullable] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static IdFilterNullable? fromJson(dynamic value) {
upgradeDto(value, "IdFilterNullable");
if (value is Map) {
final json = value.cast<String, dynamic>();
return IdFilterNullable(
eq: json.containsKey(r'eq') ? Optional.present(mapValueOfType<String>(json, r'eq')) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(mapValueOfType<String>(json, r'ne')) : const Optional.absent(),
);
}
return null;
}
static List<IdFilterNullable> listFromJson(dynamic json, {bool growable = false,}) {
final result = <IdFilterNullable>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = IdFilterNullable.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, IdFilterNullable> mapFromJson(dynamic json) {
final map = <String, IdFilterNullable>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = IdFilterNullable.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of IdFilterNullable-objects as value to a dart map
static Map<String, List<IdFilterNullable>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<IdFilterNullable>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = IdFilterNullable.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

127
mobile/openapi/lib/model/ids_filter.dart generated Normal file
View File

@@ -0,0 +1,127 @@
//
// 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 IdsFilter {
/// Returns a new [IdsFilter] instance.
IdsFilter({
this.all = const Optional.present(const []),
this.any = const Optional.present(const []),
this.none = const Optional.present(const []),
});
Optional<List<String>?> all;
Optional<List<String>?> any;
Optional<List<String>?> none;
@override
bool operator ==(Object other) => identical(this, other) || other is IdsFilter &&
_deepEquality.equals(other.all, all) &&
_deepEquality.equals(other.any, any) &&
_deepEquality.equals(other.none, none);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(all.hashCode) +
(any.hashCode) +
(none.hashCode);
@override
String toString() => 'IdsFilter[all=$all, any=$any, none=$none]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.all.isPresent) {
final value = this.all.value;
json[r'all'] = value;
}
if (this.any.isPresent) {
final value = this.any.value;
json[r'any'] = value;
}
if (this.none.isPresent) {
final value = this.none.value;
json[r'none'] = value;
}
return json;
}
/// Returns a new [IdsFilter] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static IdsFilter? fromJson(dynamic value) {
upgradeDto(value, "IdsFilter");
if (value is Map) {
final json = value.cast<String, dynamic>();
return IdsFilter(
all: json.containsKey(r'all') ? Optional.present(json[r'all'] is Iterable
? (json[r'all'] as Iterable).cast<String>().toList(growable: false)
: const []) : const Optional.absent(),
any: json.containsKey(r'any') ? Optional.present(json[r'any'] is Iterable
? (json[r'any'] as Iterable).cast<String>().toList(growable: false)
: const []) : const Optional.absent(),
none: json.containsKey(r'none') ? Optional.present(json[r'none'] is Iterable
? (json[r'none'] as Iterable).cast<String>().toList(growable: false)
: const []) : const Optional.absent(),
);
}
return null;
}
static List<IdsFilter> listFromJson(dynamic json, {bool growable = false,}) {
final result = <IdsFilter>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = IdsFilter.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, IdsFilter> mapFromJson(dynamic json) {
final map = <String, IdsFilter>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = IdsFilter.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of IdsFilter-objects as value to a dart map
static Map<String, List<IdsFilter>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<IdsFilter>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = IdsFilter.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -19,8 +19,10 @@ class MetadataSearchDto {
this.country = const Optional.absent(),
this.createdAfter = const Optional.absent(),
this.createdBefore = const Optional.absent(),
this.cursor = const Optional.absent(),
this.description = const Optional.absent(),
this.encodedVideoPath = const Optional.absent(),
this.filter = const Optional.absent(),
this.id = const Optional.absent(),
this.isEncoded = const Optional.absent(),
this.isFavorite = const Optional.absent(),
@@ -33,6 +35,7 @@ class MetadataSearchDto {
this.model = const Optional.absent(),
this.ocr = const Optional.absent(),
this.order = const Optional.absent(),
this.orderBy = const Optional.absent(),
this.originalFileName = const Optional.absent(),
this.originalPath = const Optional.absent(),
this.page = const Optional.absent(),
@@ -93,6 +96,15 @@ class MetadataSearchDto {
///
Optional<DateTime?> createdBefore;
/// Cursor for the next page of results
///
/// 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.
///
Optional<String?> cursor;
/// Filter by description text
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -111,6 +123,14 @@ class MetadataSearchDto {
///
Optional<String?> encodedVideoPath;
///
/// 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.
///
Optional<SearchFilter?> filter;
/// Filter by asset ID
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -194,6 +214,14 @@ class MetadataSearchDto {
///
Optional<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.
///
Optional<SearchOrder?> orderBy;
/// Filter by original file name
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -383,8 +411,10 @@ class MetadataSearchDto {
other.country == country &&
other.createdAfter == createdAfter &&
other.createdBefore == createdBefore &&
other.cursor == cursor &&
other.description == description &&
other.encodedVideoPath == encodedVideoPath &&
other.filter == filter &&
other.id == id &&
other.isEncoded == isEncoded &&
other.isFavorite == isFavorite &&
@@ -397,6 +427,7 @@ class MetadataSearchDto {
other.model == model &&
other.ocr == ocr &&
other.order == order &&
other.orderBy == orderBy &&
other.originalFileName == originalFileName &&
other.originalPath == originalPath &&
other.page == page &&
@@ -429,8 +460,10 @@ class MetadataSearchDto {
(country == null ? 0 : country!.hashCode) +
(createdAfter == null ? 0 : createdAfter!.hashCode) +
(createdBefore == null ? 0 : createdBefore!.hashCode) +
(cursor == null ? 0 : cursor!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(filter == null ? 0 : filter!.hashCode) +
(id == null ? 0 : id!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
@@ -443,6 +476,7 @@ class MetadataSearchDto {
(model == null ? 0 : model!.hashCode) +
(ocr == null ? 0 : ocr!.hashCode) +
(order == null ? 0 : order!.hashCode) +
(orderBy == null ? 0 : orderBy!.hashCode) +
(originalFileName == null ? 0 : originalFileName!.hashCode) +
(originalPath == null ? 0 : originalPath!.hashCode) +
(page == null ? 0 : page!.hashCode) +
@@ -467,7 +501,7 @@ class MetadataSearchDto {
(withStacked == null ? 0 : withStacked!.hashCode);
@override
String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, encodedVideoPath=$encodedVideoPath, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
String toString() => 'MetadataSearchDto[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, cursor=$cursor, description=$description, encodedVideoPath=$encodedVideoPath, filter=$filter, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, order=$order, orderBy=$orderBy, originalFileName=$originalFileName, originalPath=$originalPath, page=$page, personIds=$personIds, previewPath=$previewPath, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, thumbnailPath=$thumbnailPath, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -499,6 +533,10 @@ class MetadataSearchDto {
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.cursor.isPresent) {
final value = this.cursor.value;
json[r'cursor'] = value;
}
if (this.description.isPresent) {
final value = this.description.value;
json[r'description'] = value;
@@ -507,6 +545,10 @@ class MetadataSearchDto {
final value = this.encodedVideoPath.value;
json[r'encodedVideoPath'] = value;
}
if (this.filter.isPresent) {
final value = this.filter.value;
json[r'filter'] = value;
}
if (this.id.isPresent) {
final value = this.id.value;
json[r'id'] = value;
@@ -555,6 +597,10 @@ class MetadataSearchDto {
final value = this.order.value;
json[r'order'] = value;
}
if (this.orderBy.isPresent) {
final value = this.orderBy.value;
json[r'orderBy'] = value;
}
if (this.originalFileName.isPresent) {
final value = this.originalFileName.value;
json[r'originalFileName'] = value;
@@ -675,8 +721,10 @@ class MetadataSearchDto {
country: json.containsKey(r'country') ? Optional.present(mapValueOfType<String>(json, r'country')) : const Optional.absent(),
createdAfter: json.containsKey(r'createdAfter') ? Optional.present(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|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
createdBefore: json.containsKey(r'createdBefore') ? Optional.present(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|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
cursor: json.containsKey(r'cursor') ? Optional.present(mapValueOfType<String>(json, r'cursor')) : const Optional.absent(),
description: json.containsKey(r'description') ? Optional.present(mapValueOfType<String>(json, r'description')) : const Optional.absent(),
encodedVideoPath: json.containsKey(r'encodedVideoPath') ? Optional.present(mapValueOfType<String>(json, r'encodedVideoPath')) : const Optional.absent(),
filter: json.containsKey(r'filter') ? Optional.present(SearchFilter.fromJson(json[r'filter'])) : const Optional.absent(),
id: json.containsKey(r'id') ? Optional.present(mapValueOfType<String>(json, r'id')) : const Optional.absent(),
isEncoded: json.containsKey(r'isEncoded') ? Optional.present(mapValueOfType<bool>(json, r'isEncoded')) : const Optional.absent(),
isFavorite: json.containsKey(r'isFavorite') ? Optional.present(mapValueOfType<bool>(json, r'isFavorite')) : const Optional.absent(),
@@ -689,6 +737,7 @@ class MetadataSearchDto {
model: json.containsKey(r'model') ? Optional.present(mapValueOfType<String>(json, r'model')) : const Optional.absent(),
ocr: json.containsKey(r'ocr') ? Optional.present(mapValueOfType<String>(json, r'ocr')) : const Optional.absent(),
order: json.containsKey(r'order') ? Optional.present(AssetOrder.fromJson(json[r'order'])) : const Optional.absent(),
orderBy: json.containsKey(r'orderBy') ? Optional.present(SearchOrder.fromJson(json[r'orderBy'])) : const Optional.absent(),
originalFileName: json.containsKey(r'originalFileName') ? Optional.present(mapValueOfType<String>(json, r'originalFileName')) : const Optional.absent(),
originalPath: json.containsKey(r'originalPath') ? Optional.present(mapValueOfType<String>(json, r'originalPath')) : const Optional.absent(),
page: json.containsKey(r'page') ? Optional.present(json[r'page'] == null ? null : int.parse('${json[r'page']}')) : const Optional.absent(),

View File

@@ -0,0 +1,211 @@
//
// 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 NumberFilter {
/// Returns a new [NumberFilter] instance.
NumberFilter({
this.eq = const Optional.absent(),
this.gt = const Optional.absent(),
this.gte = const Optional.absent(),
this.in_ = const Optional.present(const []),
this.lt = const Optional.absent(),
this.lte = const Optional.absent(),
this.ne = const Optional.absent(),
this.notIn = const Optional.present(const []),
});
///
/// 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.
///
Optional<num?> eq;
///
/// 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.
///
Optional<num?> gt;
///
/// 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.
///
Optional<num?> gte;
Optional<List<num>?> in_;
///
/// 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.
///
Optional<num?> lt;
///
/// 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.
///
Optional<num?> lte;
///
/// 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.
///
Optional<num?> ne;
Optional<List<num>?> notIn;
@override
bool operator ==(Object other) => identical(this, other) || other is NumberFilter &&
other.eq == eq &&
other.gt == gt &&
other.gte == gte &&
_deepEquality.equals(other.in_, in_) &&
other.lt == lt &&
other.lte == lte &&
other.ne == ne &&
_deepEquality.equals(other.notIn, notIn);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq == null ? 0 : eq!.hashCode) +
(gt == null ? 0 : gt!.hashCode) +
(gte == null ? 0 : gte!.hashCode) +
(in_.hashCode) +
(lt == null ? 0 : lt!.hashCode) +
(lte == null ? 0 : lte!.hashCode) +
(ne == null ? 0 : ne!.hashCode) +
(notIn.hashCode);
@override
String toString() => 'NumberFilter[eq=$eq, gt=$gt, gte=$gte, in_=$in_, lt=$lt, lte=$lte, ne=$ne, notIn=$notIn]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value;
}
if (this.gt.isPresent) {
final value = this.gt.value;
json[r'gt'] = value;
}
if (this.gte.isPresent) {
final value = this.gte.value;
json[r'gte'] = value;
}
if (this.in_.isPresent) {
final value = this.in_.value;
json[r'in'] = value;
}
if (this.lt.isPresent) {
final value = this.lt.value;
json[r'lt'] = value;
}
if (this.lte.isPresent) {
final value = this.lte.value;
json[r'lte'] = value;
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value;
}
if (this.notIn.isPresent) {
final value = this.notIn.value;
json[r'notIn'] = value;
}
return json;
}
/// Returns a new [NumberFilter] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static NumberFilter? fromJson(dynamic value) {
upgradeDto(value, "NumberFilter");
if (value is Map) {
final json = value.cast<String, dynamic>();
return NumberFilter(
eq: json.containsKey(r'eq') ? Optional.present(json[r'eq'] == null ? null : num.parse('${json[r'eq']}')) : const Optional.absent(),
gt: json.containsKey(r'gt') ? Optional.present(json[r'gt'] == null ? null : num.parse('${json[r'gt']}')) : const Optional.absent(),
gte: json.containsKey(r'gte') ? Optional.present(json[r'gte'] == null ? null : num.parse('${json[r'gte']}')) : const Optional.absent(),
in_: json.containsKey(r'in') ? Optional.present(json[r'in'] is Iterable
? (json[r'in'] as Iterable).cast<num>().toList(growable: false)
: const []) : const Optional.absent(),
lt: json.containsKey(r'lt') ? Optional.present(json[r'lt'] == null ? null : num.parse('${json[r'lt']}')) : const Optional.absent(),
lte: json.containsKey(r'lte') ? Optional.present(json[r'lte'] == null ? null : num.parse('${json[r'lte']}')) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(json[r'ne'] == null ? null : num.parse('${json[r'ne']}')) : const Optional.absent(),
notIn: json.containsKey(r'notIn') ? Optional.present(json[r'notIn'] is Iterable
? (json[r'notIn'] as Iterable).cast<num>().toList(growable: false)
: const []) : const Optional.absent(),
);
}
return null;
}
static List<NumberFilter> listFromJson(dynamic json, {bool growable = false,}) {
final result = <NumberFilter>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = NumberFilter.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, NumberFilter> mapFromJson(dynamic json) {
final map = <String, NumberFilter>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = NumberFilter.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of NumberFilter-objects as value to a dart map
static Map<String, List<NumberFilter>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<NumberFilter>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = NumberFilter.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,199 @@
//
// 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 NumberFilterNullable {
/// Returns a new [NumberFilterNullable] instance.
NumberFilterNullable({
this.eq = const Optional.absent(),
this.gt = const Optional.absent(),
this.gte = const Optional.absent(),
this.in_ = const Optional.present(const []),
this.lt = const Optional.absent(),
this.lte = const Optional.absent(),
this.ne = const Optional.absent(),
this.notIn = const Optional.present(const []),
});
Optional<num?> eq;
///
/// 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.
///
Optional<num?> gt;
///
/// 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.
///
Optional<num?> gte;
Optional<List<num>?> in_;
///
/// 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.
///
Optional<num?> lt;
///
/// 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.
///
Optional<num?> lte;
Optional<num?> ne;
Optional<List<num>?> notIn;
@override
bool operator ==(Object other) => identical(this, other) || other is NumberFilterNullable &&
other.eq == eq &&
other.gt == gt &&
other.gte == gte &&
_deepEquality.equals(other.in_, in_) &&
other.lt == lt &&
other.lte == lte &&
other.ne == ne &&
_deepEquality.equals(other.notIn, notIn);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq == null ? 0 : eq!.hashCode) +
(gt == null ? 0 : gt!.hashCode) +
(gte == null ? 0 : gte!.hashCode) +
(in_.hashCode) +
(lt == null ? 0 : lt!.hashCode) +
(lte == null ? 0 : lte!.hashCode) +
(ne == null ? 0 : ne!.hashCode) +
(notIn.hashCode);
@override
String toString() => 'NumberFilterNullable[eq=$eq, gt=$gt, gte=$gte, in_=$in_, lt=$lt, lte=$lte, ne=$ne, notIn=$notIn]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value;
}
if (this.gt.isPresent) {
final value = this.gt.value;
json[r'gt'] = value;
}
if (this.gte.isPresent) {
final value = this.gte.value;
json[r'gte'] = value;
}
if (this.in_.isPresent) {
final value = this.in_.value;
json[r'in'] = value;
}
if (this.lt.isPresent) {
final value = this.lt.value;
json[r'lt'] = value;
}
if (this.lte.isPresent) {
final value = this.lte.value;
json[r'lte'] = value;
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value;
}
if (this.notIn.isPresent) {
final value = this.notIn.value;
json[r'notIn'] = value;
}
return json;
}
/// Returns a new [NumberFilterNullable] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static NumberFilterNullable? fromJson(dynamic value) {
upgradeDto(value, "NumberFilterNullable");
if (value is Map) {
final json = value.cast<String, dynamic>();
return NumberFilterNullable(
eq: json.containsKey(r'eq') ? Optional.present(json[r'eq'] == null ? null : num.parse('${json[r'eq']}')) : const Optional.absent(),
gt: json.containsKey(r'gt') ? Optional.present(json[r'gt'] == null ? null : num.parse('${json[r'gt']}')) : const Optional.absent(),
gte: json.containsKey(r'gte') ? Optional.present(json[r'gte'] == null ? null : num.parse('${json[r'gte']}')) : const Optional.absent(),
in_: json.containsKey(r'in') ? Optional.present(json[r'in'] is Iterable
? (json[r'in'] as Iterable).cast<num>().toList(growable: false)
: const []) : const Optional.absent(),
lt: json.containsKey(r'lt') ? Optional.present(json[r'lt'] == null ? null : num.parse('${json[r'lt']}')) : const Optional.absent(),
lte: json.containsKey(r'lte') ? Optional.present(json[r'lte'] == null ? null : num.parse('${json[r'lte']}')) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(json[r'ne'] == null ? null : num.parse('${json[r'ne']}')) : const Optional.absent(),
notIn: json.containsKey(r'notIn') ? Optional.present(json[r'notIn'] is Iterable
? (json[r'notIn'] as Iterable).cast<num>().toList(growable: false)
: const []) : const Optional.absent(),
);
}
return null;
}
static List<NumberFilterNullable> listFromJson(dynamic json, {bool growable = false,}) {
final result = <NumberFilterNullable>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = NumberFilterNullable.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, NumberFilterNullable> mapFromJson(dynamic json) {
final map = <String, NumberFilterNullable>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = NumberFilterNullable.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of NumberFilterNullable-objects as value to a dart map
static Map<String, List<NumberFilterNullable>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<NumberFilterNullable>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = NumberFilterNullable.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -18,6 +18,7 @@ class RandomSearchDto {
this.country = const Optional.absent(),
this.createdAfter = const Optional.absent(),
this.createdBefore = const Optional.absent(),
this.filter = const Optional.absent(),
this.isEncoded = const Optional.absent(),
this.isFavorite = const Optional.absent(),
this.isMotion = const Optional.absent(),
@@ -74,6 +75,14 @@ class RandomSearchDto {
///
Optional<DateTime?> createdBefore;
///
/// 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.
///
Optional<SearchFilter?> filter;
/// Filter by encoded status
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -280,6 +289,7 @@ class RandomSearchDto {
other.country == country &&
other.createdAfter == createdAfter &&
other.createdBefore == createdBefore &&
other.filter == filter &&
other.isEncoded == isEncoded &&
other.isFavorite == isFavorite &&
other.isMotion == isMotion &&
@@ -316,6 +326,7 @@ class RandomSearchDto {
(country == null ? 0 : country!.hashCode) +
(createdAfter == null ? 0 : createdAfter!.hashCode) +
(createdBefore == null ? 0 : createdBefore!.hashCode) +
(filter == null ? 0 : filter!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) +
@@ -345,7 +356,7 @@ class RandomSearchDto {
(withStacked == null ? 0 : withStacked!.hashCode);
@override
String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
String toString() => 'RandomSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, filter=$filter, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif, withPeople=$withPeople, withStacked=$withStacked]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -373,6 +384,10 @@ class RandomSearchDto {
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.filter.isPresent) {
final value = this.filter.value;
json[r'filter'] = value;
}
if (this.isEncoded.isPresent) {
final value = this.isEncoded.value;
json[r'isEncoded'] = value;
@@ -512,6 +527,7 @@ class RandomSearchDto {
country: json.containsKey(r'country') ? Optional.present(mapValueOfType<String>(json, r'country')) : const Optional.absent(),
createdAfter: json.containsKey(r'createdAfter') ? Optional.present(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|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
createdBefore: json.containsKey(r'createdBefore') ? Optional.present(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|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
filter: json.containsKey(r'filter') ? Optional.present(SearchFilter.fromJson(json[r'filter'])) : const Optional.absent(),
isEncoded: json.containsKey(r'isEncoded') ? Optional.present(mapValueOfType<bool>(json, r'isEncoded')) : const Optional.absent(),
isFavorite: json.containsKey(r'isFavorite') ? Optional.present(mapValueOfType<bool>(json, r'isFavorite')) : const Optional.absent(),
isMotion: json.containsKey(r'isMotion') ? Optional.present(mapValueOfType<bool>(json, r'isMotion')) : const Optional.absent(),

View File

@@ -16,6 +16,7 @@ class SearchAssetResponseDto {
required this.count,
this.facets = const [],
this.items = const [],
required this.nextCursor,
required this.nextPage,
required this.total,
});
@@ -30,6 +31,9 @@ class SearchAssetResponseDto {
List<AssetResponseDto> items;
/// Cursor for the next page of results
String? nextCursor;
/// Next page token
String? nextPage;
@@ -44,6 +48,7 @@ class SearchAssetResponseDto {
other.count == count &&
_deepEquality.equals(other.facets, facets) &&
_deepEquality.equals(other.items, items) &&
other.nextCursor == nextCursor &&
other.nextPage == nextPage &&
other.total == total;
@@ -53,17 +58,23 @@ class SearchAssetResponseDto {
(count.hashCode) +
(facets.hashCode) +
(items.hashCode) +
(nextCursor == null ? 0 : nextCursor!.hashCode) +
(nextPage == null ? 0 : nextPage!.hashCode) +
(total.hashCode);
@override
String toString() => 'SearchAssetResponseDto[count=$count, facets=$facets, items=$items, nextPage=$nextPage, total=$total]';
String toString() => 'SearchAssetResponseDto[count=$count, facets=$facets, items=$items, nextCursor=$nextCursor, nextPage=$nextPage, total=$total]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'count'] = this.count;
json[r'facets'] = this.facets;
json[r'items'] = this.items;
if (this.nextCursor != null) {
json[r'nextCursor'] = this.nextCursor;
} else {
json[r'nextCursor'] = null;
}
if (this.nextPage != null) {
json[r'nextPage'] = this.nextPage;
} else {
@@ -85,6 +96,7 @@ class SearchAssetResponseDto {
count: mapValueOfType<int>(json, r'count')!,
facets: SearchFacetResponseDto.listFromJson(json[r'facets']),
items: AssetResponseDto.listFromJson(json[r'items']),
nextCursor: mapValueOfType<String>(json, r'nextCursor'),
nextPage: mapValueOfType<String>(json, r'nextPage'),
total: mapValueOfType<int>(json, r'total')!,
);
@@ -137,6 +149,7 @@ class SearchAssetResponseDto {
'count',
'facets',
'items',
'nextCursor',
'nextPage',
'total',
};

View File

@@ -0,0 +1,613 @@
//
// 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 SearchFilter {
/// Returns a new [SearchFilter] instance.
SearchFilter({
this.albumIds = const Optional.absent(),
this.checksum = const Optional.absent(),
this.city = const Optional.absent(),
this.country = const Optional.absent(),
this.createdAt = const Optional.absent(),
this.description = const Optional.absent(),
this.encodedVideoPath = const Optional.absent(),
this.fileSizeInBytes = const Optional.absent(),
this.hasAlbums = const Optional.absent(),
this.hasPeople = const Optional.absent(),
this.hasTags = const Optional.absent(),
this.id = const Optional.absent(),
this.isEncoded = const Optional.absent(),
this.isFavorite = const Optional.absent(),
this.isMotion = const Optional.absent(),
this.isOffline = const Optional.absent(),
this.lensModel = const Optional.absent(),
this.libraryId = const Optional.absent(),
this.make = const Optional.absent(),
this.model = const Optional.absent(),
this.ocr = const Optional.absent(),
this.or = const Optional.present(const []),
this.originalFileName = const Optional.absent(),
this.originalPath = const Optional.absent(),
this.personIds = const Optional.absent(),
this.rating = const Optional.absent(),
this.state = const Optional.absent(),
this.tagIds = const Optional.absent(),
this.takenAt = const Optional.absent(),
this.trashedAt = const Optional.absent(),
this.type = const Optional.absent(),
this.updatedAt = const Optional.absent(),
this.visibility = const Optional.absent(),
});
///
/// 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.
///
Optional<IdsFilter?> albumIds;
///
/// 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.
///
Optional<StringFilter?> checksum;
///
/// 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.
///
Optional<StringFilterNullable?> city;
///
/// 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.
///
Optional<StringFilterNullable?> country;
///
/// 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.
///
Optional<DateFilter?> createdAt;
///
/// 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.
///
Optional<StringPatternFilter?> description;
///
/// 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.
///
Optional<StringFilter?> encodedVideoPath;
///
/// 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.
///
Optional<NumberFilter?> fileSizeInBytes;
///
/// 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.
///
Optional<BoolFilter?> hasAlbums;
///
/// 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.
///
Optional<BoolFilter?> hasPeople;
///
/// 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.
///
Optional<BoolFilter?> hasTags;
///
/// 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.
///
Optional<IdFilter?> id;
///
/// 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.
///
Optional<BoolFilter?> isEncoded;
///
/// 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.
///
Optional<BoolFilter?> isFavorite;
///
/// 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.
///
Optional<BoolFilter?> isMotion;
///
/// 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.
///
Optional<BoolFilter?> isOffline;
///
/// 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.
///
Optional<StringFilterNullable?> lensModel;
///
/// 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.
///
Optional<IdFilterNullable?> libraryId;
///
/// 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.
///
Optional<StringFilterNullable?> 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.
///
Optional<StringFilterNullable?> model;
///
/// 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.
///
Optional<StringSimilarityFilter?> ocr;
Optional<List<SearchFilterBranch>?> or;
///
/// 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.
///
Optional<StringPatternFilter?> originalFileName;
///
/// 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.
///
Optional<StringPatternFilter?> originalPath;
///
/// 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.
///
Optional<IdsFilter?> personIds;
///
/// 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.
///
Optional<NumberFilterNullable?> rating;
///
/// 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.
///
Optional<StringFilterNullable?> state;
///
/// 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.
///
Optional<IdsFilter?> tagIds;
///
/// 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.
///
Optional<DateFilter?> takenAt;
///
/// 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.
///
Optional<DateFilterNullable?> trashedAt;
///
/// 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.
///
Optional<EnumFilterAssetType?> 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.
///
Optional<DateFilter?> updatedAt;
///
/// 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.
///
Optional<EnumFilterAssetVisibility?> visibility;
@override
bool operator ==(Object other) => identical(this, other) || other is SearchFilter &&
other.albumIds == albumIds &&
other.checksum == checksum &&
other.city == city &&
other.country == country &&
other.createdAt == createdAt &&
other.description == description &&
other.encodedVideoPath == encodedVideoPath &&
other.fileSizeInBytes == fileSizeInBytes &&
other.hasAlbums == hasAlbums &&
other.hasPeople == hasPeople &&
other.hasTags == hasTags &&
other.id == id &&
other.isEncoded == isEncoded &&
other.isFavorite == isFavorite &&
other.isMotion == isMotion &&
other.isOffline == isOffline &&
other.lensModel == lensModel &&
other.libraryId == libraryId &&
other.make == make &&
other.model == model &&
other.ocr == ocr &&
_deepEquality.equals(other.or, or) &&
other.originalFileName == originalFileName &&
other.originalPath == originalPath &&
other.personIds == personIds &&
other.rating == rating &&
other.state == state &&
other.tagIds == tagIds &&
other.takenAt == takenAt &&
other.trashedAt == trashedAt &&
other.type == type &&
other.updatedAt == updatedAt &&
other.visibility == visibility;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(albumIds == null ? 0 : albumIds!.hashCode) +
(checksum == null ? 0 : checksum!.hashCode) +
(city == null ? 0 : city!.hashCode) +
(country == null ? 0 : country!.hashCode) +
(createdAt == null ? 0 : createdAt!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(fileSizeInBytes == null ? 0 : fileSizeInBytes!.hashCode) +
(hasAlbums == null ? 0 : hasAlbums!.hashCode) +
(hasPeople == null ? 0 : hasPeople!.hashCode) +
(hasTags == null ? 0 : hasTags!.hashCode) +
(id == null ? 0 : id!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) +
(isOffline == null ? 0 : isOffline!.hashCode) +
(lensModel == null ? 0 : lensModel!.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) +
(make == null ? 0 : make!.hashCode) +
(model == null ? 0 : model!.hashCode) +
(ocr == null ? 0 : ocr!.hashCode) +
(or.hashCode) +
(originalFileName == null ? 0 : originalFileName!.hashCode) +
(originalPath == null ? 0 : originalPath!.hashCode) +
(personIds == null ? 0 : personIds!.hashCode) +
(rating == null ? 0 : rating!.hashCode) +
(state == null ? 0 : state!.hashCode) +
(tagIds == null ? 0 : tagIds!.hashCode) +
(takenAt == null ? 0 : takenAt!.hashCode) +
(trashedAt == null ? 0 : trashedAt!.hashCode) +
(type == null ? 0 : type!.hashCode) +
(updatedAt == null ? 0 : updatedAt!.hashCode) +
(visibility == null ? 0 : visibility!.hashCode);
@override
String toString() => 'SearchFilter[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAt=$createdAt, description=$description, encodedVideoPath=$encodedVideoPath, fileSizeInBytes=$fileSizeInBytes, hasAlbums=$hasAlbums, hasPeople=$hasPeople, hasTags=$hasTags, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, or=$or, originalFileName=$originalFileName, originalPath=$originalPath, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAt=$takenAt, trashedAt=$trashedAt, type=$type, updatedAt=$updatedAt, visibility=$visibility]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.albumIds.isPresent) {
final value = this.albumIds.value;
json[r'albumIds'] = value;
}
if (this.checksum.isPresent) {
final value = this.checksum.value;
json[r'checksum'] = value;
}
if (this.city.isPresent) {
final value = this.city.value;
json[r'city'] = value;
}
if (this.country.isPresent) {
final value = this.country.value;
json[r'country'] = value;
}
if (this.createdAt.isPresent) {
final value = this.createdAt.value;
json[r'createdAt'] = value;
}
if (this.description.isPresent) {
final value = this.description.value;
json[r'description'] = value;
}
if (this.encodedVideoPath.isPresent) {
final value = this.encodedVideoPath.value;
json[r'encodedVideoPath'] = value;
}
if (this.fileSizeInBytes.isPresent) {
final value = this.fileSizeInBytes.value;
json[r'fileSizeInBytes'] = value;
}
if (this.hasAlbums.isPresent) {
final value = this.hasAlbums.value;
json[r'hasAlbums'] = value;
}
if (this.hasPeople.isPresent) {
final value = this.hasPeople.value;
json[r'hasPeople'] = value;
}
if (this.hasTags.isPresent) {
final value = this.hasTags.value;
json[r'hasTags'] = value;
}
if (this.id.isPresent) {
final value = this.id.value;
json[r'id'] = value;
}
if (this.isEncoded.isPresent) {
final value = this.isEncoded.value;
json[r'isEncoded'] = value;
}
if (this.isFavorite.isPresent) {
final value = this.isFavorite.value;
json[r'isFavorite'] = value;
}
if (this.isMotion.isPresent) {
final value = this.isMotion.value;
json[r'isMotion'] = value;
}
if (this.isOffline.isPresent) {
final value = this.isOffline.value;
json[r'isOffline'] = value;
}
if (this.lensModel.isPresent) {
final value = this.lensModel.value;
json[r'lensModel'] = value;
}
if (this.libraryId.isPresent) {
final value = this.libraryId.value;
json[r'libraryId'] = value;
}
if (this.make.isPresent) {
final value = this.make.value;
json[r'make'] = value;
}
if (this.model.isPresent) {
final value = this.model.value;
json[r'model'] = value;
}
if (this.ocr.isPresent) {
final value = this.ocr.value;
json[r'ocr'] = value;
}
if (this.or.isPresent) {
final value = this.or.value;
json[r'or'] = value;
}
if (this.originalFileName.isPresent) {
final value = this.originalFileName.value;
json[r'originalFileName'] = value;
}
if (this.originalPath.isPresent) {
final value = this.originalPath.value;
json[r'originalPath'] = value;
}
if (this.personIds.isPresent) {
final value = this.personIds.value;
json[r'personIds'] = value;
}
if (this.rating.isPresent) {
final value = this.rating.value;
json[r'rating'] = value;
}
if (this.state.isPresent) {
final value = this.state.value;
json[r'state'] = value;
}
if (this.tagIds.isPresent) {
final value = this.tagIds.value;
json[r'tagIds'] = value;
}
if (this.takenAt.isPresent) {
final value = this.takenAt.value;
json[r'takenAt'] = value;
}
if (this.trashedAt.isPresent) {
final value = this.trashedAt.value;
json[r'trashedAt'] = value;
}
if (this.type.isPresent) {
final value = this.type.value;
json[r'type'] = value;
}
if (this.updatedAt.isPresent) {
final value = this.updatedAt.value;
json[r'updatedAt'] = value;
}
if (this.visibility.isPresent) {
final value = this.visibility.value;
json[r'visibility'] = value;
}
return json;
}
/// Returns a new [SearchFilter] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SearchFilter? fromJson(dynamic value) {
upgradeDto(value, "SearchFilter");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SearchFilter(
albumIds: json.containsKey(r'albumIds') ? Optional.present(IdsFilter.fromJson(json[r'albumIds'])) : const Optional.absent(),
checksum: json.containsKey(r'checksum') ? Optional.present(StringFilter.fromJson(json[r'checksum'])) : const Optional.absent(),
city: json.containsKey(r'city') ? Optional.present(StringFilterNullable.fromJson(json[r'city'])) : const Optional.absent(),
country: json.containsKey(r'country') ? Optional.present(StringFilterNullable.fromJson(json[r'country'])) : const Optional.absent(),
createdAt: json.containsKey(r'createdAt') ? Optional.present(DateFilter.fromJson(json[r'createdAt'])) : const Optional.absent(),
description: json.containsKey(r'description') ? Optional.present(StringPatternFilter.fromJson(json[r'description'])) : const Optional.absent(),
encodedVideoPath: json.containsKey(r'encodedVideoPath') ? Optional.present(StringFilter.fromJson(json[r'encodedVideoPath'])) : const Optional.absent(),
fileSizeInBytes: json.containsKey(r'fileSizeInBytes') ? Optional.present(NumberFilter.fromJson(json[r'fileSizeInBytes'])) : const Optional.absent(),
hasAlbums: json.containsKey(r'hasAlbums') ? Optional.present(BoolFilter.fromJson(json[r'hasAlbums'])) : const Optional.absent(),
hasPeople: json.containsKey(r'hasPeople') ? Optional.present(BoolFilter.fromJson(json[r'hasPeople'])) : const Optional.absent(),
hasTags: json.containsKey(r'hasTags') ? Optional.present(BoolFilter.fromJson(json[r'hasTags'])) : const Optional.absent(),
id: json.containsKey(r'id') ? Optional.present(IdFilter.fromJson(json[r'id'])) : const Optional.absent(),
isEncoded: json.containsKey(r'isEncoded') ? Optional.present(BoolFilter.fromJson(json[r'isEncoded'])) : const Optional.absent(),
isFavorite: json.containsKey(r'isFavorite') ? Optional.present(BoolFilter.fromJson(json[r'isFavorite'])) : const Optional.absent(),
isMotion: json.containsKey(r'isMotion') ? Optional.present(BoolFilter.fromJson(json[r'isMotion'])) : const Optional.absent(),
isOffline: json.containsKey(r'isOffline') ? Optional.present(BoolFilter.fromJson(json[r'isOffline'])) : const Optional.absent(),
lensModel: json.containsKey(r'lensModel') ? Optional.present(StringFilterNullable.fromJson(json[r'lensModel'])) : const Optional.absent(),
libraryId: json.containsKey(r'libraryId') ? Optional.present(IdFilterNullable.fromJson(json[r'libraryId'])) : const Optional.absent(),
make: json.containsKey(r'make') ? Optional.present(StringFilterNullable.fromJson(json[r'make'])) : const Optional.absent(),
model: json.containsKey(r'model') ? Optional.present(StringFilterNullable.fromJson(json[r'model'])) : const Optional.absent(),
ocr: json.containsKey(r'ocr') ? Optional.present(StringSimilarityFilter.fromJson(json[r'ocr'])) : const Optional.absent(),
or: json.containsKey(r'or') ? Optional.present(SearchFilterBranch.listFromJson(json[r'or'])) : const Optional.absent(),
originalFileName: json.containsKey(r'originalFileName') ? Optional.present(StringPatternFilter.fromJson(json[r'originalFileName'])) : const Optional.absent(),
originalPath: json.containsKey(r'originalPath') ? Optional.present(StringPatternFilter.fromJson(json[r'originalPath'])) : const Optional.absent(),
personIds: json.containsKey(r'personIds') ? Optional.present(IdsFilter.fromJson(json[r'personIds'])) : const Optional.absent(),
rating: json.containsKey(r'rating') ? Optional.present(NumberFilterNullable.fromJson(json[r'rating'])) : const Optional.absent(),
state: json.containsKey(r'state') ? Optional.present(StringFilterNullable.fromJson(json[r'state'])) : const Optional.absent(),
tagIds: json.containsKey(r'tagIds') ? Optional.present(IdsFilter.fromJson(json[r'tagIds'])) : const Optional.absent(),
takenAt: json.containsKey(r'takenAt') ? Optional.present(DateFilter.fromJson(json[r'takenAt'])) : const Optional.absent(),
trashedAt: json.containsKey(r'trashedAt') ? Optional.present(DateFilterNullable.fromJson(json[r'trashedAt'])) : const Optional.absent(),
type: json.containsKey(r'type') ? Optional.present(EnumFilterAssetType.fromJson(json[r'type'])) : const Optional.absent(),
updatedAt: json.containsKey(r'updatedAt') ? Optional.present(DateFilter.fromJson(json[r'updatedAt'])) : const Optional.absent(),
visibility: json.containsKey(r'visibility') ? Optional.present(EnumFilterAssetVisibility.fromJson(json[r'visibility'])) : const Optional.absent(),
);
}
return null;
}
static List<SearchFilter> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SearchFilter>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SearchFilter.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SearchFilter> mapFromJson(dynamic json) {
final map = <String, SearchFilter>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SearchFilter.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SearchFilter-objects as value to a dart map
static Map<String, List<SearchFilter>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SearchFilter>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SearchFilter.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,603 @@
//
// 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 SearchFilterBranch {
/// Returns a new [SearchFilterBranch] instance.
SearchFilterBranch({
this.albumIds = const Optional.absent(),
this.checksum = const Optional.absent(),
this.city = const Optional.absent(),
this.country = const Optional.absent(),
this.createdAt = const Optional.absent(),
this.description = const Optional.absent(),
this.encodedVideoPath = const Optional.absent(),
this.fileSizeInBytes = const Optional.absent(),
this.hasAlbums = const Optional.absent(),
this.hasPeople = const Optional.absent(),
this.hasTags = const Optional.absent(),
this.id = const Optional.absent(),
this.isEncoded = const Optional.absent(),
this.isFavorite = const Optional.absent(),
this.isMotion = const Optional.absent(),
this.isOffline = const Optional.absent(),
this.lensModel = const Optional.absent(),
this.libraryId = const Optional.absent(),
this.make = const Optional.absent(),
this.model = const Optional.absent(),
this.ocr = const Optional.absent(),
this.originalFileName = const Optional.absent(),
this.originalPath = const Optional.absent(),
this.personIds = const Optional.absent(),
this.rating = const Optional.absent(),
this.state = const Optional.absent(),
this.tagIds = const Optional.absent(),
this.takenAt = const Optional.absent(),
this.trashedAt = const Optional.absent(),
this.type = const Optional.absent(),
this.updatedAt = const Optional.absent(),
this.visibility = const Optional.absent(),
});
///
/// 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.
///
Optional<IdsFilter?> albumIds;
///
/// 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.
///
Optional<StringFilter?> checksum;
///
/// 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.
///
Optional<StringFilterNullable?> city;
///
/// 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.
///
Optional<StringFilterNullable?> country;
///
/// 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.
///
Optional<DateFilter?> createdAt;
///
/// 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.
///
Optional<StringPatternFilter?> description;
///
/// 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.
///
Optional<StringFilter?> encodedVideoPath;
///
/// 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.
///
Optional<NumberFilter?> fileSizeInBytes;
///
/// 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.
///
Optional<BoolFilter?> hasAlbums;
///
/// 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.
///
Optional<BoolFilter?> hasPeople;
///
/// 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.
///
Optional<BoolFilter?> hasTags;
///
/// 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.
///
Optional<IdFilter?> id;
///
/// 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.
///
Optional<BoolFilter?> isEncoded;
///
/// 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.
///
Optional<BoolFilter?> isFavorite;
///
/// 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.
///
Optional<BoolFilter?> isMotion;
///
/// 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.
///
Optional<BoolFilter?> isOffline;
///
/// 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.
///
Optional<StringFilterNullable?> lensModel;
///
/// 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.
///
Optional<IdFilterNullable?> libraryId;
///
/// 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.
///
Optional<StringFilterNullable?> 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.
///
Optional<StringFilterNullable?> model;
///
/// 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.
///
Optional<StringSimilarityFilter?> ocr;
///
/// 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.
///
Optional<StringPatternFilter?> originalFileName;
///
/// 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.
///
Optional<StringPatternFilter?> originalPath;
///
/// 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.
///
Optional<IdsFilter?> personIds;
///
/// 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.
///
Optional<NumberFilterNullable?> rating;
///
/// 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.
///
Optional<StringFilterNullable?> state;
///
/// 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.
///
Optional<IdsFilter?> tagIds;
///
/// 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.
///
Optional<DateFilter?> takenAt;
///
/// 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.
///
Optional<DateFilterNullable?> trashedAt;
///
/// 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.
///
Optional<EnumFilterAssetType?> 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.
///
Optional<DateFilter?> updatedAt;
///
/// 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.
///
Optional<EnumFilterAssetVisibility?> visibility;
@override
bool operator ==(Object other) => identical(this, other) || other is SearchFilterBranch &&
other.albumIds == albumIds &&
other.checksum == checksum &&
other.city == city &&
other.country == country &&
other.createdAt == createdAt &&
other.description == description &&
other.encodedVideoPath == encodedVideoPath &&
other.fileSizeInBytes == fileSizeInBytes &&
other.hasAlbums == hasAlbums &&
other.hasPeople == hasPeople &&
other.hasTags == hasTags &&
other.id == id &&
other.isEncoded == isEncoded &&
other.isFavorite == isFavorite &&
other.isMotion == isMotion &&
other.isOffline == isOffline &&
other.lensModel == lensModel &&
other.libraryId == libraryId &&
other.make == make &&
other.model == model &&
other.ocr == ocr &&
other.originalFileName == originalFileName &&
other.originalPath == originalPath &&
other.personIds == personIds &&
other.rating == rating &&
other.state == state &&
other.tagIds == tagIds &&
other.takenAt == takenAt &&
other.trashedAt == trashedAt &&
other.type == type &&
other.updatedAt == updatedAt &&
other.visibility == visibility;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(albumIds == null ? 0 : albumIds!.hashCode) +
(checksum == null ? 0 : checksum!.hashCode) +
(city == null ? 0 : city!.hashCode) +
(country == null ? 0 : country!.hashCode) +
(createdAt == null ? 0 : createdAt!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(encodedVideoPath == null ? 0 : encodedVideoPath!.hashCode) +
(fileSizeInBytes == null ? 0 : fileSizeInBytes!.hashCode) +
(hasAlbums == null ? 0 : hasAlbums!.hashCode) +
(hasPeople == null ? 0 : hasPeople!.hashCode) +
(hasTags == null ? 0 : hasTags!.hashCode) +
(id == null ? 0 : id!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) +
(isOffline == null ? 0 : isOffline!.hashCode) +
(lensModel == null ? 0 : lensModel!.hashCode) +
(libraryId == null ? 0 : libraryId!.hashCode) +
(make == null ? 0 : make!.hashCode) +
(model == null ? 0 : model!.hashCode) +
(ocr == null ? 0 : ocr!.hashCode) +
(originalFileName == null ? 0 : originalFileName!.hashCode) +
(originalPath == null ? 0 : originalPath!.hashCode) +
(personIds == null ? 0 : personIds!.hashCode) +
(rating == null ? 0 : rating!.hashCode) +
(state == null ? 0 : state!.hashCode) +
(tagIds == null ? 0 : tagIds!.hashCode) +
(takenAt == null ? 0 : takenAt!.hashCode) +
(trashedAt == null ? 0 : trashedAt!.hashCode) +
(type == null ? 0 : type!.hashCode) +
(updatedAt == null ? 0 : updatedAt!.hashCode) +
(visibility == null ? 0 : visibility!.hashCode);
@override
String toString() => 'SearchFilterBranch[albumIds=$albumIds, checksum=$checksum, city=$city, country=$country, createdAt=$createdAt, description=$description, encodedVideoPath=$encodedVideoPath, fileSizeInBytes=$fileSizeInBytes, hasAlbums=$hasAlbums, hasPeople=$hasPeople, hasTags=$hasTags, id=$id, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, originalFileName=$originalFileName, originalPath=$originalPath, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAt=$takenAt, trashedAt=$trashedAt, type=$type, updatedAt=$updatedAt, visibility=$visibility]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.albumIds.isPresent) {
final value = this.albumIds.value;
json[r'albumIds'] = value;
}
if (this.checksum.isPresent) {
final value = this.checksum.value;
json[r'checksum'] = value;
}
if (this.city.isPresent) {
final value = this.city.value;
json[r'city'] = value;
}
if (this.country.isPresent) {
final value = this.country.value;
json[r'country'] = value;
}
if (this.createdAt.isPresent) {
final value = this.createdAt.value;
json[r'createdAt'] = value;
}
if (this.description.isPresent) {
final value = this.description.value;
json[r'description'] = value;
}
if (this.encodedVideoPath.isPresent) {
final value = this.encodedVideoPath.value;
json[r'encodedVideoPath'] = value;
}
if (this.fileSizeInBytes.isPresent) {
final value = this.fileSizeInBytes.value;
json[r'fileSizeInBytes'] = value;
}
if (this.hasAlbums.isPresent) {
final value = this.hasAlbums.value;
json[r'hasAlbums'] = value;
}
if (this.hasPeople.isPresent) {
final value = this.hasPeople.value;
json[r'hasPeople'] = value;
}
if (this.hasTags.isPresent) {
final value = this.hasTags.value;
json[r'hasTags'] = value;
}
if (this.id.isPresent) {
final value = this.id.value;
json[r'id'] = value;
}
if (this.isEncoded.isPresent) {
final value = this.isEncoded.value;
json[r'isEncoded'] = value;
}
if (this.isFavorite.isPresent) {
final value = this.isFavorite.value;
json[r'isFavorite'] = value;
}
if (this.isMotion.isPresent) {
final value = this.isMotion.value;
json[r'isMotion'] = value;
}
if (this.isOffline.isPresent) {
final value = this.isOffline.value;
json[r'isOffline'] = value;
}
if (this.lensModel.isPresent) {
final value = this.lensModel.value;
json[r'lensModel'] = value;
}
if (this.libraryId.isPresent) {
final value = this.libraryId.value;
json[r'libraryId'] = value;
}
if (this.make.isPresent) {
final value = this.make.value;
json[r'make'] = value;
}
if (this.model.isPresent) {
final value = this.model.value;
json[r'model'] = value;
}
if (this.ocr.isPresent) {
final value = this.ocr.value;
json[r'ocr'] = value;
}
if (this.originalFileName.isPresent) {
final value = this.originalFileName.value;
json[r'originalFileName'] = value;
}
if (this.originalPath.isPresent) {
final value = this.originalPath.value;
json[r'originalPath'] = value;
}
if (this.personIds.isPresent) {
final value = this.personIds.value;
json[r'personIds'] = value;
}
if (this.rating.isPresent) {
final value = this.rating.value;
json[r'rating'] = value;
}
if (this.state.isPresent) {
final value = this.state.value;
json[r'state'] = value;
}
if (this.tagIds.isPresent) {
final value = this.tagIds.value;
json[r'tagIds'] = value;
}
if (this.takenAt.isPresent) {
final value = this.takenAt.value;
json[r'takenAt'] = value;
}
if (this.trashedAt.isPresent) {
final value = this.trashedAt.value;
json[r'trashedAt'] = value;
}
if (this.type.isPresent) {
final value = this.type.value;
json[r'type'] = value;
}
if (this.updatedAt.isPresent) {
final value = this.updatedAt.value;
json[r'updatedAt'] = value;
}
if (this.visibility.isPresent) {
final value = this.visibility.value;
json[r'visibility'] = value;
}
return json;
}
/// Returns a new [SearchFilterBranch] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SearchFilterBranch? fromJson(dynamic value) {
upgradeDto(value, "SearchFilterBranch");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SearchFilterBranch(
albumIds: json.containsKey(r'albumIds') ? Optional.present(IdsFilter.fromJson(json[r'albumIds'])) : const Optional.absent(),
checksum: json.containsKey(r'checksum') ? Optional.present(StringFilter.fromJson(json[r'checksum'])) : const Optional.absent(),
city: json.containsKey(r'city') ? Optional.present(StringFilterNullable.fromJson(json[r'city'])) : const Optional.absent(),
country: json.containsKey(r'country') ? Optional.present(StringFilterNullable.fromJson(json[r'country'])) : const Optional.absent(),
createdAt: json.containsKey(r'createdAt') ? Optional.present(DateFilter.fromJson(json[r'createdAt'])) : const Optional.absent(),
description: json.containsKey(r'description') ? Optional.present(StringPatternFilter.fromJson(json[r'description'])) : const Optional.absent(),
encodedVideoPath: json.containsKey(r'encodedVideoPath') ? Optional.present(StringFilter.fromJson(json[r'encodedVideoPath'])) : const Optional.absent(),
fileSizeInBytes: json.containsKey(r'fileSizeInBytes') ? Optional.present(NumberFilter.fromJson(json[r'fileSizeInBytes'])) : const Optional.absent(),
hasAlbums: json.containsKey(r'hasAlbums') ? Optional.present(BoolFilter.fromJson(json[r'hasAlbums'])) : const Optional.absent(),
hasPeople: json.containsKey(r'hasPeople') ? Optional.present(BoolFilter.fromJson(json[r'hasPeople'])) : const Optional.absent(),
hasTags: json.containsKey(r'hasTags') ? Optional.present(BoolFilter.fromJson(json[r'hasTags'])) : const Optional.absent(),
id: json.containsKey(r'id') ? Optional.present(IdFilter.fromJson(json[r'id'])) : const Optional.absent(),
isEncoded: json.containsKey(r'isEncoded') ? Optional.present(BoolFilter.fromJson(json[r'isEncoded'])) : const Optional.absent(),
isFavorite: json.containsKey(r'isFavorite') ? Optional.present(BoolFilter.fromJson(json[r'isFavorite'])) : const Optional.absent(),
isMotion: json.containsKey(r'isMotion') ? Optional.present(BoolFilter.fromJson(json[r'isMotion'])) : const Optional.absent(),
isOffline: json.containsKey(r'isOffline') ? Optional.present(BoolFilter.fromJson(json[r'isOffline'])) : const Optional.absent(),
lensModel: json.containsKey(r'lensModel') ? Optional.present(StringFilterNullable.fromJson(json[r'lensModel'])) : const Optional.absent(),
libraryId: json.containsKey(r'libraryId') ? Optional.present(IdFilterNullable.fromJson(json[r'libraryId'])) : const Optional.absent(),
make: json.containsKey(r'make') ? Optional.present(StringFilterNullable.fromJson(json[r'make'])) : const Optional.absent(),
model: json.containsKey(r'model') ? Optional.present(StringFilterNullable.fromJson(json[r'model'])) : const Optional.absent(),
ocr: json.containsKey(r'ocr') ? Optional.present(StringSimilarityFilter.fromJson(json[r'ocr'])) : const Optional.absent(),
originalFileName: json.containsKey(r'originalFileName') ? Optional.present(StringPatternFilter.fromJson(json[r'originalFileName'])) : const Optional.absent(),
originalPath: json.containsKey(r'originalPath') ? Optional.present(StringPatternFilter.fromJson(json[r'originalPath'])) : const Optional.absent(),
personIds: json.containsKey(r'personIds') ? Optional.present(IdsFilter.fromJson(json[r'personIds'])) : const Optional.absent(),
rating: json.containsKey(r'rating') ? Optional.present(NumberFilterNullable.fromJson(json[r'rating'])) : const Optional.absent(),
state: json.containsKey(r'state') ? Optional.present(StringFilterNullable.fromJson(json[r'state'])) : const Optional.absent(),
tagIds: json.containsKey(r'tagIds') ? Optional.present(IdsFilter.fromJson(json[r'tagIds'])) : const Optional.absent(),
takenAt: json.containsKey(r'takenAt') ? Optional.present(DateFilter.fromJson(json[r'takenAt'])) : const Optional.absent(),
trashedAt: json.containsKey(r'trashedAt') ? Optional.present(DateFilterNullable.fromJson(json[r'trashedAt'])) : const Optional.absent(),
type: json.containsKey(r'type') ? Optional.present(EnumFilterAssetType.fromJson(json[r'type'])) : const Optional.absent(),
updatedAt: json.containsKey(r'updatedAt') ? Optional.present(DateFilter.fromJson(json[r'updatedAt'])) : const Optional.absent(),
visibility: json.containsKey(r'visibility') ? Optional.present(EnumFilterAssetVisibility.fromJson(json[r'visibility'])) : const Optional.absent(),
);
}
return null;
}
static List<SearchFilterBranch> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SearchFilterBranch>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SearchFilterBranch.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SearchFilterBranch> mapFromJson(dynamic json) {
final map = <String, SearchFilterBranch>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SearchFilterBranch.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SearchFilterBranch-objects as value to a dart map
static Map<String, List<SearchFilterBranch>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SearchFilterBranch>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SearchFilterBranch.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

123
mobile/openapi/lib/model/search_order.dart generated Normal file
View File

@@ -0,0 +1,123 @@
//
// 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 SearchOrder {
/// Returns a new [SearchOrder] instance.
SearchOrder({
this.direction = const Optional.absent(),
this.field = const Optional.absent(),
});
///
/// 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.
///
Optional<AssetOrder?> direction;
///
/// 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.
///
Optional<SearchOrderField?> field;
@override
bool operator ==(Object other) => identical(this, other) || other is SearchOrder &&
other.direction == direction &&
other.field == field;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(direction == null ? 0 : direction!.hashCode) +
(field == null ? 0 : field!.hashCode);
@override
String toString() => 'SearchOrder[direction=$direction, field=$field]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.direction.isPresent) {
final value = this.direction.value;
json[r'direction'] = value;
}
if (this.field.isPresent) {
final value = this.field.value;
json[r'field'] = value;
}
return json;
}
/// Returns a new [SearchOrder] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static SearchOrder? fromJson(dynamic value) {
upgradeDto(value, "SearchOrder");
if (value is Map) {
final json = value.cast<String, dynamic>();
return SearchOrder(
direction: json.containsKey(r'direction') ? Optional.present(AssetOrder.fromJson(json[r'direction'])) : const Optional.absent(),
field: json.containsKey(r'field') ? Optional.present(SearchOrderField.fromJson(json[r'field'])) : const Optional.absent(),
);
}
return null;
}
static List<SearchOrder> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SearchOrder>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SearchOrder.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, SearchOrder> mapFromJson(dynamic json) {
final map = <String, SearchOrder>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = SearchOrder.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of SearchOrder-objects as value to a dart map
static Map<String, List<SearchOrder>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<SearchOrder>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = SearchOrder.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,94 @@
//
// 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;
enum SearchOrderField {
fileCreatedAt._(r'fileCreatedAt'),
localDateTime._(r'localDateTime'),
fileSizeInBytes._(r'fileSizeInBytes'),
rating._(r'rating'),
;
/// Instantiate a new enum with the provided value.
const SearchOrderField._(this._value);
/// The underlying value of this enum member.
final String _value;
@override
String toString() => _value;
/// Encodes this enum as a value suitable for JSON.
String toJson() => _value;
/// Returns the instance of [SearchOrderField] that was successfully decoded
/// from the passed [value] on success, null otherwise.
static SearchOrderField? fromJson(dynamic value) => SearchOrderFieldTypeTransformer().decode(value);
/// Returns a [List] containing instances of [SearchOrderField]
/// that were successfully decoded from the passed [JSON][json].
static List<SearchOrderField> listFromJson(dynamic json, {bool growable = false,}) {
final result = <SearchOrderField>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = SearchOrderField.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [SearchOrderField] to String,
/// and [decode] dynamic data back to [SearchOrderField].
class SearchOrderFieldTypeTransformer {
factory SearchOrderFieldTypeTransformer() => _instance ??= const SearchOrderFieldTypeTransformer._();
const SearchOrderFieldTypeTransformer._();
/// Encodes this enum as a value suitable for JSON.
String encode(SearchOrderField data) => data._value;
/// Returns the instance of [SearchOrderField] that was successfully decoded
/// from the passed [data] value on success, null otherwise.
///
/// 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.
SearchOrderField? decode(dynamic data, {bool allowNull = true}) {
if (data is SearchOrderField) {
return data;
}
if (data != null) {
switch (data) {
case r'fileCreatedAt': return SearchOrderField.fileCreatedAt;
case r'localDateTime': return SearchOrderField.localDateTime;
case r'fileSizeInBytes': return SearchOrderField.fileSizeInBytes;
case r'rating': return SearchOrderField.rating;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// The singleton instance of this transformer.
static SearchOrderFieldTypeTransformer? _instance;
}

View File

@@ -18,6 +18,8 @@ class SmartSearchDto {
this.country = const Optional.absent(),
this.createdAfter = const Optional.absent(),
this.createdBefore = const Optional.absent(),
this.cursor = const Optional.absent(),
this.filter = const Optional.absent(),
this.isEncoded = const Optional.absent(),
this.isFavorite = const Optional.absent(),
this.isMotion = const Optional.absent(),
@@ -76,6 +78,23 @@ class SmartSearchDto {
///
Optional<DateTime?> createdBefore;
/// Cursor for the next page of results
///
/// 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.
///
Optional<String?> cursor;
///
/// 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.
///
Optional<SearchFilter?> filter;
/// Filter by encoded status
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -303,6 +322,8 @@ class SmartSearchDto {
other.country == country &&
other.createdAfter == createdAfter &&
other.createdBefore == createdBefore &&
other.cursor == cursor &&
other.filter == filter &&
other.isEncoded == isEncoded &&
other.isFavorite == isFavorite &&
other.isMotion == isMotion &&
@@ -341,6 +362,8 @@ class SmartSearchDto {
(country == null ? 0 : country!.hashCode) +
(createdAfter == null ? 0 : createdAfter!.hashCode) +
(createdBefore == null ? 0 : createdBefore!.hashCode) +
(cursor == null ? 0 : cursor!.hashCode) +
(filter == null ? 0 : filter!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) +
@@ -372,7 +395,7 @@ class SmartSearchDto {
(withExif == null ? 0 : withExif!.hashCode);
@override
String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]';
String toString() => 'SmartSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, cursor=$cursor, filter=$filter, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, language=$language, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, page=$page, personIds=$personIds, query=$query, queryAssetId=$queryAssetId, rating=$rating, size=$size, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility, withDeleted=$withDeleted, withExif=$withExif]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -400,6 +423,14 @@ class SmartSearchDto {
? value.millisecondsSinceEpoch
: value.toUtc().toIso8601String());
}
if (this.cursor.isPresent) {
final value = this.cursor.value;
json[r'cursor'] = value;
}
if (this.filter.isPresent) {
final value = this.filter.value;
json[r'filter'] = value;
}
if (this.isEncoded.isPresent) {
final value = this.isEncoded.value;
json[r'isEncoded'] = value;
@@ -547,6 +578,8 @@ class SmartSearchDto {
country: json.containsKey(r'country') ? Optional.present(mapValueOfType<String>(json, r'country')) : const Optional.absent(),
createdAfter: json.containsKey(r'createdAfter') ? Optional.present(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|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
createdBefore: json.containsKey(r'createdBefore') ? Optional.present(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|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
cursor: json.containsKey(r'cursor') ? Optional.present(mapValueOfType<String>(json, r'cursor')) : const Optional.absent(),
filter: json.containsKey(r'filter') ? Optional.present(SearchFilter.fromJson(json[r'filter'])) : const Optional.absent(),
isEncoded: json.containsKey(r'isEncoded') ? Optional.present(mapValueOfType<bool>(json, r'isEncoded')) : const Optional.absent(),
isFavorite: json.containsKey(r'isFavorite') ? Optional.present(mapValueOfType<bool>(json, r'isFavorite')) : const Optional.absent(),
isMotion: json.containsKey(r'isMotion') ? Optional.present(mapValueOfType<bool>(json, r'isMotion')) : const Optional.absent(),

View File

@@ -19,6 +19,7 @@ class StatisticsSearchDto {
this.createdAfter = const Optional.absent(),
this.createdBefore = const Optional.absent(),
this.description = const Optional.absent(),
this.filter = const Optional.absent(),
this.isEncoded = const Optional.absent(),
this.isFavorite = const Optional.absent(),
this.isMotion = const Optional.absent(),
@@ -79,6 +80,14 @@ class StatisticsSearchDto {
///
Optional<String?> description;
///
/// 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.
///
Optional<SearchFilter?> filter;
/// Filter by encoded status
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -238,6 +247,7 @@ class StatisticsSearchDto {
other.createdAfter == createdAfter &&
other.createdBefore == createdBefore &&
other.description == description &&
other.filter == filter &&
other.isEncoded == isEncoded &&
other.isFavorite == isFavorite &&
other.isMotion == isMotion &&
@@ -270,6 +280,7 @@ class StatisticsSearchDto {
(createdAfter == null ? 0 : createdAfter!.hashCode) +
(createdBefore == null ? 0 : createdBefore!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(filter == null ? 0 : filter!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) +
@@ -294,7 +305,7 @@ class StatisticsSearchDto {
(visibility == null ? 0 : visibility!.hashCode);
@override
String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]';
String toString() => 'StatisticsSearchDto[albumIds=$albumIds, city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, filter=$filter, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, ocr=$ocr, personIds=$personIds, rating=$rating, state=$state, tagIds=$tagIds, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, visibility=$visibility]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -326,6 +337,10 @@ class StatisticsSearchDto {
final value = this.description.value;
json[r'description'] = value;
}
if (this.filter.isPresent) {
final value = this.filter.value;
json[r'filter'] = value;
}
if (this.isEncoded.isPresent) {
final value = this.isEncoded.value;
json[r'isEncoded'] = value;
@@ -446,6 +461,7 @@ class StatisticsSearchDto {
createdAfter: json.containsKey(r'createdAfter') ? Optional.present(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|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
createdBefore: json.containsKey(r'createdBefore') ? Optional.present(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|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$/')) : const Optional.absent(),
description: json.containsKey(r'description') ? Optional.present(mapValueOfType<String>(json, r'description')) : const Optional.absent(),
filter: json.containsKey(r'filter') ? Optional.present(SearchFilter.fromJson(json[r'filter'])) : const Optional.absent(),
isEncoded: json.containsKey(r'isEncoded') ? Optional.present(mapValueOfType<bool>(json, r'isEncoded')) : const Optional.absent(),
isFavorite: json.containsKey(r'isFavorite') ? Optional.present(mapValueOfType<bool>(json, r'isFavorite')) : const Optional.absent(),
isMotion: json.containsKey(r'isMotion') ? Optional.present(mapValueOfType<bool>(json, r'isMotion')) : const Optional.absent(),

View File

@@ -0,0 +1,147 @@
//
// 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 StringFilter {
/// Returns a new [StringFilter] instance.
StringFilter({
this.eq = const Optional.absent(),
this.in_ = const Optional.present(const []),
this.ne = const Optional.absent(),
this.notIn = const Optional.present(const []),
});
///
/// 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.
///
Optional<String?> eq;
Optional<List<String>?> in_;
///
/// 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.
///
Optional<String?> ne;
Optional<List<String>?> notIn;
@override
bool operator ==(Object other) => identical(this, other) || other is StringFilter &&
other.eq == eq &&
_deepEquality.equals(other.in_, in_) &&
other.ne == ne &&
_deepEquality.equals(other.notIn, notIn);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq == null ? 0 : eq!.hashCode) +
(in_.hashCode) +
(ne == null ? 0 : ne!.hashCode) +
(notIn.hashCode);
@override
String toString() => 'StringFilter[eq=$eq, in_=$in_, ne=$ne, notIn=$notIn]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value;
}
if (this.in_.isPresent) {
final value = this.in_.value;
json[r'in'] = value;
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value;
}
if (this.notIn.isPresent) {
final value = this.notIn.value;
json[r'notIn'] = value;
}
return json;
}
/// Returns a new [StringFilter] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static StringFilter? fromJson(dynamic value) {
upgradeDto(value, "StringFilter");
if (value is Map) {
final json = value.cast<String, dynamic>();
return StringFilter(
eq: json.containsKey(r'eq') ? Optional.present(mapValueOfType<String>(json, r'eq')) : const Optional.absent(),
in_: json.containsKey(r'in') ? Optional.present(json[r'in'] is Iterable
? (json[r'in'] as Iterable).cast<String>().toList(growable: false)
: const []) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(mapValueOfType<String>(json, r'ne')) : const Optional.absent(),
notIn: json.containsKey(r'notIn') ? Optional.present(json[r'notIn'] is Iterable
? (json[r'notIn'] as Iterable).cast<String>().toList(growable: false)
: const []) : const Optional.absent(),
);
}
return null;
}
static List<StringFilter> listFromJson(dynamic json, {bool growable = false,}) {
final result = <StringFilter>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = StringFilter.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, StringFilter> mapFromJson(dynamic json) {
final map = <String, StringFilter>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = StringFilter.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of StringFilter-objects as value to a dart map
static Map<String, List<StringFilter>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<StringFilter>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = StringFilter.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,135 @@
//
// 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 StringFilterNullable {
/// Returns a new [StringFilterNullable] instance.
StringFilterNullable({
this.eq = const Optional.absent(),
this.in_ = const Optional.present(const []),
this.ne = const Optional.absent(),
this.notIn = const Optional.present(const []),
});
Optional<String?> eq;
Optional<List<String>?> in_;
Optional<String?> ne;
Optional<List<String>?> notIn;
@override
bool operator ==(Object other) => identical(this, other) || other is StringFilterNullable &&
other.eq == eq &&
_deepEquality.equals(other.in_, in_) &&
other.ne == ne &&
_deepEquality.equals(other.notIn, notIn);
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(eq == null ? 0 : eq!.hashCode) +
(in_.hashCode) +
(ne == null ? 0 : ne!.hashCode) +
(notIn.hashCode);
@override
String toString() => 'StringFilterNullable[eq=$eq, in_=$in_, ne=$ne, notIn=$notIn]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value;
}
if (this.in_.isPresent) {
final value = this.in_.value;
json[r'in'] = value;
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value;
}
if (this.notIn.isPresent) {
final value = this.notIn.value;
json[r'notIn'] = value;
}
return json;
}
/// Returns a new [StringFilterNullable] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static StringFilterNullable? fromJson(dynamic value) {
upgradeDto(value, "StringFilterNullable");
if (value is Map) {
final json = value.cast<String, dynamic>();
return StringFilterNullable(
eq: json.containsKey(r'eq') ? Optional.present(mapValueOfType<String>(json, r'eq')) : const Optional.absent(),
in_: json.containsKey(r'in') ? Optional.present(json[r'in'] is Iterable
? (json[r'in'] as Iterable).cast<String>().toList(growable: false)
: const []) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(mapValueOfType<String>(json, r'ne')) : const Optional.absent(),
notIn: json.containsKey(r'notIn') ? Optional.present(json[r'notIn'] is Iterable
? (json[r'notIn'] as Iterable).cast<String>().toList(growable: false)
: const []) : const Optional.absent(),
);
}
return null;
}
static List<StringFilterNullable> listFromJson(dynamic json, {bool growable = false,}) {
final result = <StringFilterNullable>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = StringFilterNullable.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, StringFilterNullable> mapFromJson(dynamic json) {
final map = <String, StringFilterNullable>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = StringFilterNullable.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of StringFilterNullable-objects as value to a dart map
static Map<String, List<StringFilterNullable>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<StringFilterNullable>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = StringFilterNullable.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,199 @@
//
// 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 StringPatternFilter {
/// Returns a new [StringPatternFilter] instance.
StringPatternFilter({
this.endsWith = const Optional.absent(),
this.eq = const Optional.absent(),
this.in_ = const Optional.present(const []),
this.like = const Optional.absent(),
this.ne = const Optional.absent(),
this.notIn = const Optional.present(const []),
this.notLike = const Optional.absent(),
this.startsWith = const Optional.absent(),
});
///
/// 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.
///
Optional<String?> endsWith;
Optional<String?> eq;
Optional<List<String>?> in_;
///
/// 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.
///
Optional<String?> like;
Optional<String?> ne;
Optional<List<String>?> notIn;
///
/// 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.
///
Optional<String?> notLike;
///
/// 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.
///
Optional<String?> startsWith;
@override
bool operator ==(Object other) => identical(this, other) || other is StringPatternFilter &&
other.endsWith == endsWith &&
other.eq == eq &&
_deepEquality.equals(other.in_, in_) &&
other.like == like &&
other.ne == ne &&
_deepEquality.equals(other.notIn, notIn) &&
other.notLike == notLike &&
other.startsWith == startsWith;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(endsWith == null ? 0 : endsWith!.hashCode) +
(eq == null ? 0 : eq!.hashCode) +
(in_.hashCode) +
(like == null ? 0 : like!.hashCode) +
(ne == null ? 0 : ne!.hashCode) +
(notIn.hashCode) +
(notLike == null ? 0 : notLike!.hashCode) +
(startsWith == null ? 0 : startsWith!.hashCode);
@override
String toString() => 'StringPatternFilter[endsWith=$endsWith, eq=$eq, in_=$in_, like=$like, ne=$ne, notIn=$notIn, notLike=$notLike, startsWith=$startsWith]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.endsWith.isPresent) {
final value = this.endsWith.value;
json[r'endsWith'] = value;
}
if (this.eq.isPresent) {
final value = this.eq.value;
json[r'eq'] = value;
}
if (this.in_.isPresent) {
final value = this.in_.value;
json[r'in'] = value;
}
if (this.like.isPresent) {
final value = this.like.value;
json[r'like'] = value;
}
if (this.ne.isPresent) {
final value = this.ne.value;
json[r'ne'] = value;
}
if (this.notIn.isPresent) {
final value = this.notIn.value;
json[r'notIn'] = value;
}
if (this.notLike.isPresent) {
final value = this.notLike.value;
json[r'notLike'] = value;
}
if (this.startsWith.isPresent) {
final value = this.startsWith.value;
json[r'startsWith'] = value;
}
return json;
}
/// Returns a new [StringPatternFilter] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static StringPatternFilter? fromJson(dynamic value) {
upgradeDto(value, "StringPatternFilter");
if (value is Map) {
final json = value.cast<String, dynamic>();
return StringPatternFilter(
endsWith: json.containsKey(r'endsWith') ? Optional.present(mapValueOfType<String>(json, r'endsWith')) : const Optional.absent(),
eq: json.containsKey(r'eq') ? Optional.present(mapValueOfType<String>(json, r'eq')) : const Optional.absent(),
in_: json.containsKey(r'in') ? Optional.present(json[r'in'] is Iterable
? (json[r'in'] as Iterable).cast<String>().toList(growable: false)
: const []) : const Optional.absent(),
like: json.containsKey(r'like') ? Optional.present(mapValueOfType<String>(json, r'like')) : const Optional.absent(),
ne: json.containsKey(r'ne') ? Optional.present(mapValueOfType<String>(json, r'ne')) : const Optional.absent(),
notIn: json.containsKey(r'notIn') ? Optional.present(json[r'notIn'] is Iterable
? (json[r'notIn'] as Iterable).cast<String>().toList(growable: false)
: const []) : const Optional.absent(),
notLike: json.containsKey(r'notLike') ? Optional.present(mapValueOfType<String>(json, r'notLike')) : const Optional.absent(),
startsWith: json.containsKey(r'startsWith') ? Optional.present(mapValueOfType<String>(json, r'startsWith')) : const Optional.absent(),
);
}
return null;
}
static List<StringPatternFilter> listFromJson(dynamic json, {bool growable = false,}) {
final result = <StringPatternFilter>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = StringPatternFilter.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, StringPatternFilter> mapFromJson(dynamic json) {
final map = <String, StringPatternFilter>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = StringPatternFilter.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of StringPatternFilter-objects as value to a dart map
static Map<String, List<StringPatternFilter>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<StringPatternFilter>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = StringPatternFilter.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
};
}

View File

@@ -0,0 +1,99 @@
//
// 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 StringSimilarityFilter {
/// Returns a new [StringSimilarityFilter] instance.
StringSimilarityFilter({
required this.matches,
});
String matches;
@override
bool operator ==(Object other) => identical(this, other) || other is StringSimilarityFilter &&
other.matches == matches;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(matches.hashCode);
@override
String toString() => 'StringSimilarityFilter[matches=$matches]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'matches'] = this.matches;
return json;
}
/// Returns a new [StringSimilarityFilter] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static StringSimilarityFilter? fromJson(dynamic value) {
upgradeDto(value, "StringSimilarityFilter");
if (value is Map) {
final json = value.cast<String, dynamic>();
return StringSimilarityFilter(
matches: mapValueOfType<String>(json, r'matches')!,
);
}
return null;
}
static List<StringSimilarityFilter> listFromJson(dynamic json, {bool growable = false,}) {
final result = <StringSimilarityFilter>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = StringSimilarityFilter.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, StringSimilarityFilter> mapFromJson(dynamic json) {
final map = <String, StringSimilarityFilter>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = StringSimilarityFilter.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of StringSimilarityFilter-objects as value to a dart map
static Map<String, List<StringSimilarityFilter>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<StringSimilarityFilter>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = StringSimilarityFilter.listFromJson(entry.value, growable: growable,);
}
}
return map;
}
/// The list of required keys that must be present in a JSON.
static const requiredKeys = <String>{
'matches',
};
}

View File

@@ -0,0 +1,69 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/presentation/widgets/people/person_edit_birthday_modal.widget.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl.dart';
import 'package:scroll_date_picker/scroll_date_picker.dart';
void main() {
group('datePickerColumnOrder', () {
test('month first (en_US)', () {
expect(
datePickerColumnOrder('M/d/y'),
orderedEquals([DatePickerViewType.month, DatePickerViewType.day, DatePickerViewType.year]),
);
});
test('day first (pl)', () {
expect(
datePickerColumnOrder('dd.MM.y'),
orderedEquals([DatePickerViewType.day, DatePickerViewType.month, DatePickerViewType.year]),
);
});
test('year first (ko)', () {
expect(
datePickerColumnOrder('y. M. d.'),
orderedEquals([DatePickerViewType.year, DatePickerViewType.month, DatePickerViewType.day]),
);
});
test('null pattern falls back to package default', () {
expect(datePickerColumnOrder(null), isNull);
});
test('missing field falls back to package default', () {
expect(datePickerColumnOrder('M/y'), isNull);
});
});
group('datePickerColumnOrder with real locale patterns', () {
setUpAll(() async {
await initializeDateFormatting();
});
for (final (locales, order, name) in const [
(
['en', 'en-US', 'en-PH'],
[DatePickerViewType.month, DatePickerViewType.day, DatePickerViewType.year],
'month/day/year',
),
(
['en-GB', 'fr', 'fr-FR', 'de', 'de-DE', 'pl'],
[DatePickerViewType.day, DatePickerViewType.month, DatePickerViewType.year],
'day/month/year',
),
(
['ja', 'ja-JP', 'zh', 'zh-CN', 'ko', 'ko-KR'],
[DatePickerViewType.year, DatePickerViewType.month, DatePickerViewType.day],
'year/month/day',
),
(['ky'], [DatePickerViewType.year, DatePickerViewType.day, DatePickerViewType.month], 'year/day/month'),
]) {
for (final locale in locales) {
test('$locale uses $name', () {
expect(datePickerColumnOrder(DateFormat.yMd(locale).pattern), orderedEquals(order));
});
}
}
});
}

File diff suppressed because it is too large Load Diff

View File

@@ -1617,6 +1617,168 @@ export type SearchExploreResponseDto = {
fieldName: string;
items: SearchExploreItem[];
};
export type IdsFilter = {
all?: string[];
"any"?: string[];
none?: string[];
};
export type StringFilter = {
eq?: string;
"in"?: string[];
ne?: string;
notIn?: string[];
};
export type StringFilterNullable = {
eq?: string | null;
"in"?: string[];
ne?: string | null;
notIn?: string[];
};
export type DateFilter = {
eq?: string;
gt?: string;
gte?: string;
lt?: string;
lte?: string;
ne?: string;
};
export type StringPatternFilter = {
endsWith?: string;
eq?: string | null;
"in"?: string[];
like?: string;
ne?: string | null;
notIn?: string[];
notLike?: string;
startsWith?: string;
};
export type NumberFilter = {
eq?: number;
gt?: number;
gte?: number;
"in"?: number[];
lt?: number;
lte?: number;
ne?: number;
notIn?: number[];
};
export type BoolFilter = {
eq: boolean;
};
export type IdFilter = {
eq?: string;
ne?: string;
};
export type IdFilterNullable = {
eq?: string | null;
ne?: string | null;
};
export type StringSimilarityFilter = {
matches: string;
};
export type NumberFilterNullable = {
eq?: number | null;
gt?: number;
gte?: number;
"in"?: number[];
lt?: number;
lte?: number;
ne?: number | null;
notIn?: number[];
};
export type DateFilterNullable = {
eq?: string | null;
gt?: string;
gte?: string;
lt?: string;
lte?: string;
ne?: string | null;
};
export type EnumFilterAssetType = {
eq?: AssetTypeEnum;
"in"?: AssetTypeEnum[];
ne?: AssetTypeEnum;
notIn?: AssetTypeEnum[];
};
export type EnumFilterAssetVisibility = {
eq?: AssetVisibility;
"in"?: AssetVisibility[];
ne?: AssetVisibility;
notIn?: AssetVisibility[];
};
export type SearchFilterBranch = {
albumIds?: IdsFilter;
checksum?: StringFilter;
city?: StringFilterNullable;
country?: StringFilterNullable;
createdAt?: DateFilter;
description?: StringPatternFilter;
encodedVideoPath?: StringFilter;
fileSizeInBytes?: NumberFilter;
hasAlbums?: BoolFilter;
hasPeople?: BoolFilter;
hasTags?: BoolFilter;
id?: IdFilter;
isEncoded?: BoolFilter;
isFavorite?: BoolFilter;
isMotion?: BoolFilter;
isOffline?: BoolFilter;
lensModel?: StringFilterNullable;
libraryId?: IdFilterNullable;
make?: StringFilterNullable;
model?: StringFilterNullable;
ocr?: StringSimilarityFilter;
originalFileName?: StringPatternFilter;
originalPath?: StringPatternFilter;
personIds?: IdsFilter;
rating?: NumberFilterNullable;
state?: StringFilterNullable;
tagIds?: IdsFilter;
takenAt?: DateFilter;
trashedAt?: DateFilterNullable;
"type"?: EnumFilterAssetType;
updatedAt?: DateFilter;
visibility?: EnumFilterAssetVisibility;
};
export type SearchFilter = {
albumIds?: IdsFilter;
checksum?: StringFilter;
city?: StringFilterNullable;
country?: StringFilterNullable;
createdAt?: DateFilter;
description?: StringPatternFilter;
encodedVideoPath?: StringFilter;
fileSizeInBytes?: NumberFilter;
hasAlbums?: BoolFilter;
hasPeople?: BoolFilter;
hasTags?: BoolFilter;
id?: IdFilter;
isEncoded?: BoolFilter;
isFavorite?: BoolFilter;
isMotion?: BoolFilter;
isOffline?: BoolFilter;
lensModel?: StringFilterNullable;
libraryId?: IdFilterNullable;
make?: StringFilterNullable;
model?: StringFilterNullable;
ocr?: StringSimilarityFilter;
or?: SearchFilterBranch[];
originalFileName?: StringPatternFilter;
originalPath?: StringPatternFilter;
personIds?: IdsFilter;
rating?: NumberFilterNullable;
state?: StringFilterNullable;
tagIds?: IdsFilter;
takenAt?: DateFilter;
trashedAt?: DateFilterNullable;
"type"?: EnumFilterAssetType;
updatedAt?: DateFilter;
visibility?: EnumFilterAssetVisibility;
};
export type SearchOrder = {
direction?: AssetOrder;
field?: SearchOrderField;
};
export type MetadataSearchDto = {
/** Filter by album IDs */
albumIds?: string[];
@@ -1630,10 +1792,13 @@ export type MetadataSearchDto = {
createdAfter?: string;
/** Filter by creation date (before) */
createdBefore?: string;
/** Cursor for the next page of results */
cursor?: string;
/** Filter by description text */
description?: string;
/** Filter by encoded video file path */
encodedVideoPath?: string;
filter?: SearchFilter;
/** Filter by asset ID */
id?: string;
/** Filter by encoded status */
@@ -1658,6 +1823,7 @@ export type MetadataSearchDto = {
ocr?: string;
/** Sort order */
order?: AssetOrder;
orderBy?: SearchOrder;
/** Filter by original file name */
originalFileName?: string;
/** Filter by original file path */
@@ -1725,6 +1891,8 @@ export type SearchAssetResponseDto = {
count: number;
facets: SearchFacetResponseDto[];
items: AssetResponseDto[];
/** Cursor for the next page of results */
nextCursor: string | null;
/** Next page token */
nextPage: string | null;
/** Total number of matching assets */
@@ -1757,6 +1925,7 @@ export type RandomSearchDto = {
createdAfter?: string;
/** Filter by creation date (before) */
createdBefore?: string;
filter?: SearchFilter;
/** Filter by encoded status */
isEncoded?: boolean;
/** Filter by favorite status */
@@ -1821,6 +1990,9 @@ export type SmartSearchDto = {
createdAfter?: string;
/** Filter by creation date (before) */
createdBefore?: string;
/** Cursor for the next page of results */
cursor?: string;
filter?: SearchFilter;
/** Filter by encoded status */
isEncoded?: boolean;
/** Filter by favorite status */
@@ -1891,6 +2063,7 @@ export type StatisticsSearchDto = {
createdBefore?: string;
/** Filter by description text */
description?: string;
filter?: SearchFilter;
/** Filter by encoded status */
isEncoded?: boolean;
/** Filter by favorite status */
@@ -7501,6 +7674,12 @@ export enum JobName {
IntegrityDeleteReportType = "IntegrityDeleteReportType",
IntegrityDeleteReports = "IntegrityDeleteReports"
}
export enum SearchOrderField {
FileCreatedAt = "fileCreatedAt",
LocalDateTime = "localDateTime",
FileSizeInBytes = "fileSizeInBytes",
Rating = "rating"
}
export enum SearchSuggestionType {
Country = "country",
State = "state",

View File

@@ -106,7 +106,11 @@ describe(MemoryController.name, () => {
it('should require at least one field', async () => {
const { status, body } = await request(ctx.getHttpServer()).put(`/memories/${factory.uuid()}`).send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.validationError([{ path: [], message: 'At least one field must be provided' }]));
expect(body).toEqual(
errorDto.validationError([
{ path: [], message: 'At least one of the following fields is required: isSaved, seenAt, memoryAt' },
]),
);
});
});

View File

@@ -120,6 +120,16 @@ describe(SearchController.name, () => {
);
});
it('should reject a deprecated field combined with a new structure field', async () => {
const { status, body } = await request(ctx.getHttpServer())
.post('/search/metadata')
.send({ filter: {}, city: 'Oslo' });
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([{ path: ['city'], message: 'Deprecated field city cannot be combined with filter' }]),
);
});
describe('POST /search/random', () => {
it('should be an authenticated route', async () => {
await request(ctx.getHttpServer()).post('/search/random');

View File

@@ -69,7 +69,11 @@ export class SearchController {
@Endpoint({
summary: 'Search large assets',
description: 'Search for assets that are considered large based on specified criteria.',
history: new HistoryBuilder().added('v1').beta('v1').stable('v2'),
history: new HistoryBuilder()
.added('v1')
.beta('v1')
.stable('v2')
.deprecated('v3.1.0', { replacementId: 'searchAssets' }),
})
searchLargeAssets(@Auth() auth: AuthDto, @Query() dto: LargeAssetSearchDto): Promise<AssetResponseDto[]> {
return this.service.searchLargeAssets(auth, dto);

View File

@@ -304,6 +304,36 @@ export const columns = {
'asset.height',
'asset.isEdited',
],
searchAsset: [
'asset.id',
'asset.updateId',
'asset.createdAt',
'asset.updatedAt',
'asset.deletedAt',
'asset.status',
'asset.checksum',
'asset.checksumAlgorithm',
'asset.duplicateId',
'asset.duration',
'asset.fileCreatedAt',
'asset.fileModifiedAt',
'asset.isExternal',
'asset.isFavorite',
'asset.isOffline',
'asset.isEdited',
'asset.visibility',
'asset.libraryId',
'asset.livePhotoVideoId',
'asset.localDateTime',
'asset.originalFileName',
'asset.originalPath',
'asset.ownerId',
'asset.stackId',
'asset.thumbhash',
'asset.type',
'asset.width',
'asset.height',
],
workflowAssetV1: [
'asset.id',
'asset.ownerId',

View File

@@ -108,9 +108,11 @@ export function ChunkedSet(options?: { paramIndex?: number; chunkSize?: number }
}
const UUID = '00000000-0000-4000-a000-000000000000';
const UUID_1 = '00000000-0000-4000-a000-000000000001';
export const DummyValue = {
UUID,
UUID_1,
UUID_SET: new Set([UUID]),
PAGINATION: { take: 10, skip: 0 },
EMAIL: 'user@immich.app',

View File

@@ -3,36 +3,51 @@ import { Place } from 'src/database';
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 { isoDatetimeToDate, stringToBool } from 'src/validation';
import {
AssetOrder,
AssetOrderSchema,
AssetTypeSchema,
AssetVisibilitySchema,
SearchOrderField,
SearchOrderFieldSchema,
} from 'src/enum';
import { isoDatetimeToDate, nonEmptyPartial, stringToBool } from 'src/validation';
import z from 'zod';
const ADDED_V3_1 = new HistoryBuilder().added('v3.1.0').getExtensions();
// fields deprecated in favor of the structured filter tree
const DEPRECATED_FLAT_FIELD = {
...new HistoryBuilder().added('v1').stable('v2').deprecated('v3.1.0').getExtensions(),
deprecated: true,
};
const BaseSearchSchema = z.object({
libraryId: z.uuidv4().nullish().describe('Library 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: z.string().nullable().optional().describe('Filter by city name'),
state: z.string().nullable().optional().describe('Filter by state/province name'),
country: z.string().nullable().optional().describe('Filter by country name'),
make: z.string().nullable().optional().describe('Filter by camera make'),
model: z.string().nullable().optional().describe('Filter by camera model'),
lensModel: 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'),
libraryId: z.uuidv4().nullish().describe('Library ID to filter by').meta(DEPRECATED_FLAT_FIELD),
type: AssetTypeSchema.optional().meta(DEPRECATED_FLAT_FIELD),
isEncoded: z.boolean().optional().describe('Filter by encoded status').meta(DEPRECATED_FLAT_FIELD),
isFavorite: z.boolean().optional().describe('Filter by favorite status').meta(DEPRECATED_FLAT_FIELD),
isMotion: z.boolean().optional().describe('Filter by motion photo status').meta(DEPRECATED_FLAT_FIELD),
isOffline: z.boolean().optional().describe('Filter by offline status').meta(DEPRECATED_FLAT_FIELD),
visibility: AssetVisibilitySchema.optional().meta(DEPRECATED_FLAT_FIELD),
createdBefore: isoDatetimeToDate.optional().describe('Filter by creation date (before)').meta(DEPRECATED_FLAT_FIELD),
createdAfter: isoDatetimeToDate.optional().describe('Filter by creation date (after)').meta(DEPRECATED_FLAT_FIELD),
updatedBefore: isoDatetimeToDate.optional().describe('Filter by update date (before)').meta(DEPRECATED_FLAT_FIELD),
updatedAfter: isoDatetimeToDate.optional().describe('Filter by update date (after)').meta(DEPRECATED_FLAT_FIELD),
trashedBefore: isoDatetimeToDate.optional().describe('Filter by trash date (before)').meta(DEPRECATED_FLAT_FIELD),
trashedAfter: isoDatetimeToDate.optional().describe('Filter by trash date (after)').meta(DEPRECATED_FLAT_FIELD),
takenBefore: isoDatetimeToDate.optional().describe('Filter by taken date (before)').meta(DEPRECATED_FLAT_FIELD),
takenAfter: isoDatetimeToDate.optional().describe('Filter by taken date (after)').meta(DEPRECATED_FLAT_FIELD),
city: z.string().nullable().optional().describe('Filter by city name').meta(DEPRECATED_FLAT_FIELD),
state: z.string().nullable().optional().describe('Filter by state/province name').meta(DEPRECATED_FLAT_FIELD),
country: z.string().nullable().optional().describe('Filter by country name').meta(DEPRECATED_FLAT_FIELD),
make: z.string().nullable().optional().describe('Filter by camera make').meta(DEPRECATED_FLAT_FIELD),
model: z.string().nullable().optional().describe('Filter by camera model').meta(DEPRECATED_FLAT_FIELD),
lensModel: z.string().nullable().optional().describe('Filter by lens model').meta(DEPRECATED_FLAT_FIELD),
isNotInAlbum: z.boolean().optional().describe('Filter assets not in any album').meta(DEPRECATED_FLAT_FIELD),
personIds: z.array(z.uuidv4()).optional().describe('Filter by person IDs').meta(DEPRECATED_FLAT_FIELD),
tagIds: z.array(z.uuidv4()).nullish().describe('Filter by tag IDs').meta(DEPRECATED_FLAT_FIELD),
albumIds: z.array(z.uuidv4()).optional().describe('Filter by album IDs').meta(DEPRECATED_FLAT_FIELD),
rating: z
.int()
.min(1)
@@ -45,51 +60,24 @@ const BaseSearchSchema = z.object({
.stable('v2')
.updated('v2.6.0', 'Using -1 as a rating is deprecated and will be removed in the next major version.')
.updated('v3', 'Using -1 as a rating is no longer valid.')
.deprecated('v3.1.0')
.getExtensions(),
deprecated: true,
}),
ocr: z.string().optional().describe('Filter by OCR text content'),
ocr: z.string().optional().describe('Filter by OCR text content').meta(DEPRECATED_FLAT_FIELD),
});
const BaseSearchWithResultsSchema = BaseSearchSchema.extend({
withDeleted: z.boolean().optional().describe('Include deleted assets'),
withDeleted: z.boolean().optional().describe('Include deleted assets').meta(DEPRECATED_FLAT_FIELD),
withExif: z.boolean().optional().describe('Include EXIF data in response'),
size: z.int().min(1).max(1000).optional().describe('Number of results to return'),
size: z.int().min(1).max(1000).default(250).describe('Number of results to return'),
});
const RandomSearchSchema = BaseSearchWithResultsSchema.extend({
withStacked: z.boolean().optional().describe('Include stacked assets'),
withPeople: z.boolean().optional().describe('Include people data in response'),
}).meta({ id: 'RandomSearchDto' });
const LargeAssetSearchSchema = BaseSearchWithResultsSchema.extend({
minFileSize: z.coerce.number().int().min(0).optional().describe('Minimum file size in bytes'),
size: z.coerce.number().int().min(1).max(1000).optional().describe('Number of results to return'),
size: z.coerce.number().int().min(1).max(1000).default(250).describe('Number of results to return'),
}).meta({ id: 'LargeAssetSearchDto' });
const MetadataSearchSchema = RandomSearchSchema.extend({
id: z.uuidv4().optional().describe('Filter by 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.int().min(1).optional().describe('Page number'),
}).meta({ id: 'MetadataSearchDto' });
const StatisticsSearchSchema = BaseSearchSchema.extend({
description: z.string().trim().optional().describe('Filter by description text'),
}).meta({ id: 'StatisticsSearchDto' });
const SmartSearchSchema = BaseSearchWithResultsSchema.extend({
query: z.string().trim().optional().describe('Natural language search query'),
queryAssetId: z.uuidv4().optional().describe('Asset ID to use as search reference'),
language: z.string().optional().describe('Search language code'),
page: z.int().min(1).optional().describe('Page number'),
}).meta({ id: 'SmartSearchDto' });
const SearchPlacesSchema = z
.object({
name: z.string().describe('Place name to search for'),
@@ -142,6 +130,258 @@ const SearchSuggestionRequestSchema = z
})
.meta({ id: 'SearchSuggestionRequestDto' });
const IdFilterSchema = nonEmptyPartial({
eq: z.uuidv4(),
ne: z.uuidv4(),
}).meta({ id: 'IdFilter' });
const IdFilterNullableSchema = nonEmptyPartial({
eq: z.uuidv4().nullable(),
ne: z.uuidv4().nullable(),
}).meta({ id: 'IdFilterNullable' });
const IdsFilterSchema = nonEmptyPartial({
any: z.array(z.uuidv4()).min(1),
all: z.array(z.uuidv4()).min(1),
none: z.array(z.uuidv4()).min(1),
}).meta({ id: 'IdsFilter' });
const stringListShape = {
in: z.array(z.string()).min(1),
notIn: z.array(z.string()).min(1),
};
const StringFilterSchema = nonEmptyPartial({
eq: z.string(),
ne: z.string(),
...stringListShape,
}).meta({ id: 'StringFilter' });
const stringNullableShape = {
eq: z.string().nullable(),
ne: z.string().nullable(),
...stringListShape,
};
const StringFilterNullableSchema = nonEmptyPartial(stringNullableShape).meta({ id: 'StringFilterNullable' });
const StringPatternFilterSchema = nonEmptyPartial({
...stringNullableShape,
like: z.string().min(1),
notLike: z.string().min(1),
startsWith: z.string().min(1),
endsWith: z.string().min(1),
}).meta({ id: 'StringPatternFilter' });
const numberRangeShape = {
lt: z.number(),
lte: z.number(),
gt: z.number(),
gte: z.number(),
in: z.array(z.number()).min(1),
notIn: z.array(z.number()).min(1),
};
const NumberFilterSchema = nonEmptyPartial({
eq: z.number(),
ne: z.number(),
...numberRangeShape,
}).meta({ id: 'NumberFilter' });
const NumberFilterNullableSchema = nonEmptyPartial({
eq: z.number().nullable(),
ne: z.number().nullable(),
...numberRangeShape,
}).meta({ id: 'NumberFilterNullable' });
const dateRangeShape = {
gt: isoDatetimeToDate,
gte: isoDatetimeToDate,
lt: isoDatetimeToDate,
lte: isoDatetimeToDate,
};
const DateFilterSchema = nonEmptyPartial({
eq: isoDatetimeToDate,
ne: isoDatetimeToDate,
...dateRangeShape,
}).meta({ id: 'DateFilter' });
const DateFilterNullableSchema = nonEmptyPartial({
eq: isoDatetimeToDate.nullable(),
ne: isoDatetimeToDate.nullable(),
...dateRangeShape,
}).meta({ id: 'DateFilterNullable' });
const BoolFilterSchema = z.object({ eq: z.boolean() }).meta({ id: 'BoolFilter' });
const enumFilterSchema = <T extends z.core.util.EnumLike>(values: z.ZodEnum<T>, id: string) =>
nonEmptyPartial({
eq: values,
ne: values,
in: z.array(values).min(1),
notIn: z.array(values).min(1),
}).meta({ id });
const EnumFilterAssetTypeSchema = enumFilterSchema(AssetTypeSchema, 'EnumFilterAssetType');
const EnumFilterAssetVisibilitySchema = enumFilterSchema(AssetVisibilitySchema, 'EnumFilterAssetVisibility');
const StringSimilarityFilterSchema = z
.object({
matches: z.string().min(1),
})
.meta({ id: 'StringSimilarityFilter' });
export const DEFAULT_SEARCH_ORDER = {
field: SearchOrderField.FileCreatedAt,
direction: AssetOrder.Desc,
};
export const SearchOrderSchema = z
.object({
field: SearchOrderFieldSchema.default(DEFAULT_SEARCH_ORDER.field),
direction: AssetOrderSchema.default(DEFAULT_SEARCH_ORDER.direction),
})
.meta({ id: 'SearchOrder' });
const SearchFilterBranchSchema = z
.object({
id: IdFilterSchema,
libraryId: IdFilterNullableSchema,
type: EnumFilterAssetTypeSchema,
visibility: EnumFilterAssetVisibilitySchema,
isFavorite: BoolFilterSchema,
isMotion: BoolFilterSchema,
isOffline: BoolFilterSchema,
isEncoded: BoolFilterSchema,
hasAlbums: BoolFilterSchema,
hasPeople: BoolFilterSchema,
hasTags: BoolFilterSchema,
city: StringFilterNullableSchema,
state: StringFilterNullableSchema,
country: StringFilterNullableSchema,
make: StringFilterNullableSchema,
model: StringFilterNullableSchema,
lensModel: StringFilterNullableSchema,
description: StringPatternFilterSchema,
originalFileName: StringPatternFilterSchema,
originalPath: StringPatternFilterSchema,
ocr: StringSimilarityFilterSchema,
rating: NumberFilterNullableSchema,
fileSizeInBytes: NumberFilterSchema,
takenAt: DateFilterSchema,
createdAt: DateFilterSchema,
updatedAt: DateFilterSchema,
trashedAt: DateFilterNullableSchema,
personIds: IdsFilterSchema,
tagIds: IdsFilterSchema,
albumIds: IdsFilterSchema,
checksum: StringFilterSchema,
encodedVideoPath: StringFilterSchema,
})
.partial()
.meta({ id: 'SearchFilterBranch' });
export const SearchFilterSchema = SearchFilterBranchSchema.extend({
or: z.array(SearchFilterBranchSchema).min(1).optional(),
}).meta({ id: 'SearchFilter' });
export type IdFilter = z.infer<typeof IdFilterSchema>;
export type IdFilterNullable = z.infer<typeof IdFilterNullableSchema>;
export type IdsFilter = z.infer<typeof IdsFilterSchema>;
export type StringFilter = z.infer<typeof StringFilterSchema>;
export type StringFilterNullable = z.infer<typeof StringFilterNullableSchema>;
export type StringPatternFilter = z.infer<typeof StringPatternFilterSchema>;
export type NumberFilter = z.infer<typeof NumberFilterSchema>;
export type NumberFilterNullable = z.infer<typeof NumberFilterNullableSchema>;
export type DateFilter = z.infer<typeof DateFilterSchema>;
export type DateFilterNullable = z.infer<typeof DateFilterNullableSchema>;
export type SearchOrder = z.infer<typeof SearchOrderSchema>;
export type SearchFilter = z.infer<typeof SearchFilterSchema>;
export type SearchFilterBranch = z.infer<typeof SearchFilterBranchSchema>;
const NEW_SHAPE_FIELDS = ['filter', 'orderBy', 'cursor'] as const;
export const isNewShapeRequest = (dto: Partial<Record<(typeof NEW_SHAPE_FIELDS)[number], unknown>>): boolean =>
NEW_SHAPE_FIELDS.some((field) => dto[field] !== undefined);
/**
* The structured shape and the deprecated flat search fields are mutually exclusive
* TODO(v4): remove together with the deprecated flat fields.
*/
const withShapeExclusivity = <T extends z.ZodObject<z.ZodRawShape>>(schema: T) => {
const deprecatedFields = Object.keys(schema.shape).filter(
(field) => (schema.shape[field] as z.ZodType).meta()?.deprecated,
);
return schema.superRefine((dto, ctx) => {
const values = dto as Record<string, unknown>;
const newShapeFields = NEW_SHAPE_FIELDS.filter((field) => values[field] !== undefined);
if (newShapeFields.length === 0) {
return;
}
for (const field of deprecatedFields) {
if (values[field] === undefined) {
continue;
}
ctx.addIssue({
code: 'custom',
path: [field],
message: `Deprecated field ${field} cannot be combined with ${newShapeFields.join('/')}`,
});
}
});
};
const filterField = SearchFilterSchema.optional().meta(ADDED_V3_1);
const cursorField = z.string().min(1).optional().describe('Cursor for the next page of results').meta(ADDED_V3_1);
const RandomSearchBaseSchema = BaseSearchWithResultsSchema.extend({
withStacked: z.boolean().optional().describe('Include stacked assets'),
withPeople: z.boolean().optional().describe('Include people data in response'),
filter: filterField,
});
const RandomSearchSchema = withShapeExclusivity(RandomSearchBaseSchema).meta({ id: 'RandomSearchDto' });
const MetadataSearchSchema = withShapeExclusivity(
RandomSearchBaseSchema.extend({
id: z.uuidv4().optional().describe('Filter by asset ID').meta(DEPRECATED_FLAT_FIELD),
description: z.string().trim().optional().describe('Filter by description text').meta(DEPRECATED_FLAT_FIELD),
checksum: z.string().optional().describe('Filter by file checksum').meta(DEPRECATED_FLAT_FIELD),
originalFileName: z.string().trim().optional().describe('Filter by original file name').meta(DEPRECATED_FLAT_FIELD),
originalPath: z.string().optional().describe('Filter by original file path').meta(DEPRECATED_FLAT_FIELD),
previewPath: z.string().optional().describe('Filter by preview file path').meta(DEPRECATED_FLAT_FIELD),
thumbnailPath: z.string().optional().describe('Filter by thumbnail file path').meta(DEPRECATED_FLAT_FIELD),
encodedVideoPath: z.string().optional().describe('Filter by encoded video file path').meta(DEPRECATED_FLAT_FIELD),
order: AssetOrderSchema.optional().describe('Sort order').meta(DEPRECATED_FLAT_FIELD),
page: z.int().min(1).optional().describe('Page number').meta(DEPRECATED_FLAT_FIELD),
orderBy: SearchOrderSchema.optional().meta(ADDED_V3_1),
cursor: cursorField,
}),
).meta({ id: 'MetadataSearchDto' });
const StatisticsSearchSchema = withShapeExclusivity(
BaseSearchSchema.extend({
description: z.string().trim().optional().describe('Filter by description text').meta(DEPRECATED_FLAT_FIELD),
filter: filterField,
}),
).meta({ id: 'StatisticsSearchDto' });
const SmartSearchSchema = withShapeExclusivity(
BaseSearchWithResultsSchema.extend({
size: z.int().min(1).max(1000).default(100).describe('Number of results to return'),
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.int().min(1).optional().describe('Page number').meta(DEPRECATED_FLAT_FIELD),
filter: filterField,
cursor: cursorField,
}),
).meta({ id: 'SmartSearchDto' });
export class RandomSearchDto extends createZodDto(RandomSearchSchema) {}
export class LargeAssetSearchDto extends createZodDto(LargeAssetSearchSchema) {}
export class MetadataSearchDto extends createZodDto(MetadataSearchSchema) {}
@@ -195,7 +435,8 @@ const SearchAssetResponseSchema = z
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'),
nextPage: z.string().nullable().describe('Next page token').meta(DEPRECATED_FLAT_FIELD),
nextCursor: z.string().nullable().describe('Cursor for the next page of results').meta(ADDED_V3_1),
})
.meta({ id: 'SearchAssetResponseDto' });

View File

@@ -1234,3 +1234,12 @@ export enum CalendarHeatmapType {
Upload = 'Upload',
Taken = 'Taken',
}
export enum SearchOrderField {
FileCreatedAt = 'fileCreatedAt',
LocalDateTime = 'localDateTime',
FileSizeInBytes = 'fileSizeInBytes',
Rating = 'rating',
}
export const SearchOrderFieldSchema = z.enum(SearchOrderField).meta({ id: 'SearchOrderField' });

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,25 @@
import { Injectable } from '@nestjs/common';
import { Kysely, OrderByDirection, Selectable, ShallowDehydrateObject, sql } from 'kysely';
import { InjectKysely } from 'nestjs-kysely';
import { columns } from 'src/database';
import { DummyValue, GenerateSql } from 'src/decorators';
import { MapAsset } from 'src/dtos/asset-response.dto';
import { SearchFilter, SearchOrder } from 'src/dtos/search.dto';
import { AssetStatus, AssetType, AssetVisibility, VectorIndex } from 'src/enum';
import { probes } from 'src/repositories/database.repository';
import { DB } from 'src/schema';
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
import { anyUuid, searchAssetBuilder, withExifInner } from 'src/utils/database';
import {
anyUuid,
searchAssetBuilder,
searchAssetBuilderLegacy,
searchMetadataV3Examples,
searchRandomV3Examples,
searchSmartV3Examples,
searchStatisticsV3Examples,
withExifInner,
withSearchOrder,
} from 'src/utils/database';
import { paginationHelper } from 'src/utils/pagination';
import z from 'zod';
@@ -122,6 +135,25 @@ export type AssetSearchOptions = Omit<BaseAssetSearchOptions, 'visibility'> &
export type AssetSearchBuilderOptions = Omit<AssetSearchOptions, 'orderDirection'>;
export interface AssetSearchScope {
userIds?: string[];
lockedOwnerId: string;
}
export interface AssetSearchBuilderV3Options {
filter?: SearchFilter;
withExif?: boolean;
withFaces?: boolean;
withPeople?: boolean;
withStacked?: boolean;
order?: SearchOrder;
}
export interface AssetSearchPaginationV3Options {
size: number;
offset?: number;
}
export type SmartSearchOptions = SearchDateOptions &
SearchEmbeddingOptions &
SearchExifOptions &
@@ -182,6 +214,7 @@ export interface GetCameraLensModelsOptions {
export class SearchRepository {
constructor(@InjectKysely() private db: Kysely<DB>) {}
// TODO(v4): remove with the deprecated flat-field search API
@GenerateSql({
params: [
{ page: 1, size: 100 },
@@ -196,9 +229,10 @@ export class SearchRepository {
})
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) {
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection;
const items = await searchAssetBuilder(this.db, options)
.selectAll('asset')
const items = await searchAssetBuilderLegacy(this.db, options)
.select(columns.searchAsset)
.orderBy('asset.fileCreatedAt', orderDirection)
.orderBy('asset.id', orderDirection)
.limit(pagination.size + 1)
.offset((pagination.page - 1) * pagination.size)
.execute();
@@ -206,6 +240,7 @@ export class SearchRepository {
return paginationHelper(items, pagination.size);
}
// TODO(v4): remove with the deprecated flat-field search API
@GenerateSql({
params: [
{
@@ -217,11 +252,12 @@ export class SearchRepository {
],
})
searchStatistics(options: AssetSearchOptions) {
return searchAssetBuilder(this.db, options)
return searchAssetBuilderLegacy(this.db, options)
.select((qb) => qb.fn.countAll<number>().as('total'))
.executeTakeFirstOrThrow();
}
// TODO(v4): remove with the deprecated flat-field search API
@GenerateSql({
params: [
100,
@@ -235,13 +271,14 @@ export class SearchRepository {
],
})
async searchRandom(size: number, options: AssetSearchOptions) {
return searchAssetBuilder(this.db, options)
.selectAll('asset')
return searchAssetBuilderLegacy(this.db, options)
.select(columns.searchAsset)
.orderBy(sql`random()`)
.limit(size)
.execute();
}
// TODO(v4): remove with the deprecated flat-field search API
@GenerateSql({
params: [
100,
@@ -256,8 +293,8 @@ export class SearchRepository {
})
searchLargeAssets(size: number, options: LargeAssetSearchOptions) {
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection;
return searchAssetBuilder(this.db, options)
.selectAll('asset')
return searchAssetBuilderLegacy(this.db, options)
.select(columns.searchAsset)
.$call(withExifInner)
.where('asset_exif.fileSizeInByte', '>', options.minFileSize || 0)
.orderBy('asset_exif.fileSizeInByte', orderDirection)
@@ -265,6 +302,7 @@ export class SearchRepository {
.execute();
}
// TODO(v4): remove with the deprecated flat-field search API
@GenerateSql({
params: [
{ page: 1, size: 200 },
@@ -285,10 +323,11 @@ export class SearchRepository {
return this.db.transaction().execute(async (trx) => {
await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.Clip])}`.execute(trx);
const items = await searchAssetBuilder(trx, options)
.selectAll('asset')
const items = await searchAssetBuilderLegacy(trx, options)
.select(columns.searchAsset)
.innerJoin('smart_search', 'asset.id', 'smart_search.assetId')
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
.orderBy('asset.id', 'asc')
.limit(pagination.size + 1)
.offset((pagination.page - 1) * pagination.size)
.execute();
@@ -417,7 +456,7 @@ export class SearchRepository {
.selectFrom('asset')
.innerJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
.innerJoin('cte', 'asset.id', 'cte.assetId')
.selectAll('asset')
.select(columns.searchAsset)
.select((eb) =>
eb
.fn('to_jsonb', [eb.table('asset_exif')])
@@ -490,6 +529,64 @@ export class SearchRepository {
return res.map((row) => row.lensModel!);
}
// TODO(v4): drop the V3 suffix once the legacy methods are removed
@GenerateSql(...searchMetadataV3Examples)
async searchMetadataV3(
pagination: AssetSearchPaginationV3Options,
options: AssetSearchBuilderV3Options,
scope: AssetSearchScope,
) {
const items = await withSearchOrder(searchAssetBuilder(this.db, options, scope), options.order)
.select(columns.searchAsset)
.limit(pagination.size + 1)
.offset(pagination.offset ?? 0)
.execute();
return paginationHelper(items, pagination.size);
}
// TODO(v4): drop the V3 suffix once the legacy methods are removed
@GenerateSql(...searchRandomV3Examples)
searchRandomV3(
size: number,
options: Omit<AssetSearchBuilderV3Options, 'order'>,
scope: AssetSearchScope,
): Promise<MapAsset[]> {
return searchAssetBuilder(this.db, options, scope)
.select(columns.searchAsset)
.orderBy(sql`random()`)
.limit(size)
.execute();
}
// TODO(v4): drop the V3 suffix once the legacy methods are removed
@GenerateSql(...searchSmartV3Examples)
searchSmartV3(
pagination: AssetSearchPaginationV3Options,
options: Omit<AssetSearchBuilderV3Options, 'order'> & { embedding: string },
scope: AssetSearchScope,
) {
return this.db.transaction().execute(async (trx) => {
await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.Clip])}`.execute(trx);
const items = await searchAssetBuilder(trx, options, scope)
.select(columns.searchAsset)
.innerJoin('smart_search', 'asset.id', 'smart_search.assetId')
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
.orderBy('asset.id', 'asc')
.limit(pagination.size + 1)
.offset(pagination.offset ?? 0)
.execute();
return paginationHelper(items, pagination.size);
});
}
// TODO(v4): drop the V3 suffix once the legacy methods are removed
@GenerateSql(...searchStatisticsV3Examples)
searchStatisticsV3(options: AssetSearchBuilderV3Options, scope: AssetSearchScope) {
return searchAssetBuilder(this.db, options, scope)
.select((qb) => qb.fn.countAll<number>().as('total'))
.executeTakeFirstOrThrow();
}
private getExifField(field: 'city' | 'state' | 'country' | 'make' | 'model' | 'lensModel', userIds: string[]) {
return this.db
.selectFrom('asset_exif')

View File

@@ -1,11 +1,13 @@
import { BadRequestException } from '@nestjs/common';
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { SearchSuggestionType } from 'src/dtos/search.dto';
import { AssetVisibility } from 'src/enum';
import { SearchService } from 'src/services/search.service';
import { AssetFactory } from 'test/factories/asset.factory';
import { AuthFactory } from 'test/factories/auth.factory';
import { authStub } from 'test/fixtures/auth.stub';
import { getForAsset } from 'test/mappers';
import { newUuid } from 'test/small.factory';
import { newTestService, ServiceMocks } from 'test/utils';
import { beforeEach, vitest } from 'vitest';
@@ -215,6 +217,69 @@ describe(SearchService.name, () => {
});
});
describe('new shape routing', () => {
it('should route a filter request to the V3 search and a flat request to the legacy search', async () => {
const auth = AuthFactory.create();
mocks.search.searchMetadataV3.mockResolvedValue({ hasNextPage: false, items: [] });
await sut.searchMetadata(auth, { size: 250, filter: {} });
expect(mocks.search.searchMetadataV3).toHaveBeenCalled();
expect(mocks.search.searchMetadata).not.toHaveBeenCalled();
mocks.search.searchMetadata.mockResolvedValue({ hasNextPage: false, items: [] });
await sut.searchMetadata(auth, { size: 250, city: 'Oslo' });
expect(mocks.search.searchMetadata).toHaveBeenCalled();
});
it('should route statistics, random, and smart filter requests to their V3 search', async () => {
const auth = AuthFactory.create();
mocks.search.searchStatisticsV3.mockResolvedValue({ total: 0 });
await expect(sut.searchStatistics(auth, { filter: {} })).resolves.toEqual({ total: 0 });
mocks.search.searchRandomV3.mockResolvedValue([]);
await expect(sut.searchRandom(auth, { size: 250, filter: {} })).resolves.toEqual([]);
mocks.search.searchSmartV3.mockResolvedValue({ hasNextPage: false, items: [] });
mocks.machineLearning.encodeText.mockResolvedValue('[1, 2, 3]');
await sut.searchSmart(auth, { size: 100, filter: {}, query: 'test' });
expect(mocks.search.searchSmartV3).toHaveBeenCalledWith(
{ size: 100, offset: 0 },
expect.objectContaining({ embedding: '[1, 2, 3]' }),
expect.objectContaining({ lockedOwnerId: expect.any(String) }),
);
});
it('should reject an invalid cursor', async () => {
await expect(sut.searchMetadata(AuthFactory.create(), { size: 250, cursor: '???' })).rejects.toThrowError(
new BadRequestException('Invalid cursor'),
);
});
it('should reject an unelevated session whose filter could match locked assets', async () => {
const filter = { visibility: { in: [AssetVisibility.Locked, AssetVisibility.Timeline] } };
await expect(sut.searchMetadata(AuthFactory.create(), { size: 250, filter })).rejects.toBeInstanceOf(
UnauthorizedException,
);
});
it('should reject a shared link without a top-level album constraint', async () => {
const auth = AuthFactory.from().sharedLink().build();
await expect(sut.searchMetadata(auth, { size: 250, filter: {} })).rejects.toThrowError(
new BadRequestException('Shared link access is only allowed in combination with an albumIds filter'),
);
const albumId = newUuid();
mocks.access.album.checkSharedLinkAccess.mockResolvedValue(new Set([albumId]));
await expect(
sut.searchMetadata(auth, { size: 250, filter: { or: [{ albumIds: { any: [albumId] } }] } }),
).rejects.toThrowError(
new BadRequestException('Shared link access is only allowed in combination with an albumIds filter'),
);
});
});
describe('searchSmart', () => {
beforeEach(() => {
mocks.search.searchSmart.mockResolvedValue({ hasNextPage: false, items: [] });
@@ -226,7 +291,7 @@ describe(SearchService.name, () => {
machineLearning: { enabled: false },
});
await expect(sut.searchSmart(authStub.user1, { query: 'test' })).rejects.toThrowError(
await expect(sut.searchSmart(authStub.user1, { size: 100, query: 'test' })).rejects.toThrowError(
new BadRequestException('Smart search is not enabled'),
);
});
@@ -236,13 +301,13 @@ describe(SearchService.name, () => {
machineLearning: { clip: { enabled: false } },
});
await expect(sut.searchSmart(authStub.user1, { query: 'test' })).rejects.toThrowError(
await expect(sut.searchSmart(authStub.user1, { size: 100, query: 'test' })).rejects.toThrowError(
new BadRequestException('Smart search is not enabled'),
);
});
it('should work', async () => {
await sut.searchSmart(authStub.user1, { query: 'test' });
await sut.searchSmart(authStub.user1, { size: 100, query: 'test' });
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
'test',
@@ -250,7 +315,13 @@ describe(SearchService.name, () => {
);
expect(mocks.search.searchSmart).toHaveBeenCalledWith(
{ page: 1, size: 100 },
{ query: 'test', embedding: '[1, 2, 3]', userIds: [authStub.user1.user.id], visibility: 'not-locked' },
{
query: 'test',
size: 100,
embedding: '[1, 2, 3]',
userIds: [authStub.user1.user.id],
visibility: 'not-locked',
},
);
});
@@ -272,7 +343,7 @@ describe(SearchService.name, () => {
machineLearning: { clip: { modelName: 'ViT-B-16-SigLIP__webli' } },
});
await sut.searchSmart(authStub.user1, { query: 'test' });
await sut.searchSmart(authStub.user1, { size: 100, query: 'test' });
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
'test',
@@ -281,7 +352,7 @@ describe(SearchService.name, () => {
});
it('should use language specified in request', async () => {
await sut.searchSmart(authStub.user1, { query: 'test', language: 'de' });
await sut.searchSmart(authStub.user1, { size: 100, query: 'test', language: 'de' });
expect(mocks.machineLearning.encodeText).toHaveBeenCalledWith(
'test',

View File

@@ -1,14 +1,17 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { LRUMap } from 'mnemonist';
import { SystemConfig } from 'src/config';
import { AssetMapOptions, AssetResponseDto, MapAsset, mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { mapPerson, PersonResponseDto } from 'src/dtos/person.dto';
import {
isNewShapeRequest,
LargeAssetSearchDto,
mapPlaces,
MetadataSearchDto,
PlacesResponseDto,
RandomSearchDto,
SearchFilter,
SearchPeopleDto,
SearchPlacesDto,
SearchResponseDto,
@@ -19,10 +22,17 @@ import {
StatisticsSearchDto,
} from 'src/dtos/search.dto';
import { AssetOrder, AssetVisibility, Permission } from 'src/enum';
import { AssetSearchScope } from 'src/repositories/search.repository';
import { BaseService } from 'src/services/base.service';
import { requireElevatedPermission } from 'src/utils/access';
import { getMyPartnerIds } from 'src/utils/asset.util';
import { isSmartSearchEnabled } from 'src/utils/misc';
import { decodeSearchCursor, encodeSearchCursor } from 'src/utils/search-cursor';
import {
applyLockedVisibilityPolicy,
collectFilterIds,
hasTopLevelPositiveIdsConstraint,
} from 'src/utils/search-filter';
@Injectable()
export class SearchService extends BaseService {
@@ -63,6 +73,10 @@ export class SearchService extends BaseService {
}
async searchMetadata(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
if (isNewShapeRequest(dto)) {
return this.searchMetadataV3(auth, dto);
}
if (dto.visibility === AssetVisibility.Locked) {
requireElevatedPermission(auth);
}
@@ -84,7 +98,7 @@ export class SearchService extends BaseService {
}
const page = dto.page ?? 1;
const size = dto.size || 250;
const size = dto.size;
const { hasNextPage, items } = await this.searchRepository.searchMetadata(
{ page, size },
{
@@ -96,10 +110,14 @@ export class SearchService extends BaseService {
},
);
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
return this.mapResponse(items, { auth }, { nextPage: hasNextPage ? (page + 1).toString() : null });
}
async searchStatistics(auth: AuthDto, dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
if (isNewShapeRequest(dto)) {
return this.searchStatisticsV3(auth, dto);
}
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
if (dto.visibility === AssetVisibility.Locked) {
requireElevatedPermission(auth);
@@ -113,12 +131,16 @@ export class SearchService extends BaseService {
}
async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
if (isNewShapeRequest(dto)) {
return this.searchRandomV3(auth, dto);
}
if (dto.visibility === AssetVisibility.Locked) {
requireElevatedPermission(auth);
}
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
const items = await this.searchRepository.searchRandom(dto.size || 250, {
const items = await this.searchRepository.searchRandom(dto.size, {
...dto,
visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
userIds,
@@ -132,7 +154,7 @@ export class SearchService extends BaseService {
}
const userIds = await this.getUserIdsToSearch(auth, dto.visibility);
const items = await this.searchRepository.searchLargeAssets(dto.size || 250, {
const items = await this.searchRepository.searchLargeAssets(dto.size, {
...dto,
visibility: dto.visibility ?? (auth.session?.hasElevatedPermission ? undefined : 'not-locked'),
userIds,
@@ -141,6 +163,10 @@ export class SearchService extends BaseService {
}
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
if (isNewShapeRequest(dto)) {
return this.searchSmartV3(auth, dto);
}
if (dto.visibility === AssetVisibility.Locked) {
requireElevatedPermission(auth);
}
@@ -151,30 +177,9 @@ export class SearchService extends BaseService {
}
const userIds = this.getUserIdsToSearch(auth, dto.visibility);
let embedding;
if (dto.query) {
const key = machineLearning.clip.modelName + dto.query + dto.language;
embedding = this.embeddingCache.get(key);
if (!embedding) {
embedding = await this.machineLearningRepository.encodeText(dto.query, {
modelName: machineLearning.clip.modelName,
language: dto.language,
});
this.embeddingCache.set(key, embedding);
}
} else if (dto.queryAssetId) {
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.queryAssetId] });
const getEmbeddingResponse = await this.searchRepository.getEmbedding(dto.queryAssetId);
const assetEmbedding = getEmbeddingResponse?.embedding;
if (!assetEmbedding) {
throw new BadRequestException(`Asset ${dto.queryAssetId} has no embedding`);
}
embedding = assetEmbedding;
} else {
throw new BadRequestException('Either `query` or `queryAssetId` must be set');
}
const embedding = await this.resolveEmbedding(auth, dto, machineLearning);
const page = dto.page ?? 1;
const size = dto.size || 100;
const size = dto.size;
const { hasNextPage, items } = await this.searchRepository.searchSmart(
{ page, size },
{
@@ -185,7 +190,7 @@ export class SearchService extends BaseService {
},
);
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
return this.mapResponse(items, { auth }, { nextPage: hasNextPage ? (page + 1).toString() : null });
}
async getAssetsByCity(auth: AuthDto): Promise<AssetResponseDto[]> {
@@ -229,6 +234,122 @@ export class SearchService extends BaseService {
}
}
private async searchMetadataV3(auth: AuthDto, dto: MetadataSearchDto): Promise<SearchResponseDto> {
const { filter, scope } = await this.resolveSearchScopeV3(auth, dto);
const { offset } = decodeSearchCursor(dto.cursor);
const size = dto.size;
const { hasNextPage, items } = await this.searchRepository.searchMetadataV3(
{ size, offset },
{
filter,
withExif: dto.withExif,
withPeople: dto.withPeople,
withStacked: dto.withStacked,
order: dto.orderBy,
},
scope,
);
return this.mapResponse(items, { auth }, { nextCursor: hasNextPage ? encodeSearchCursor(offset + size) : null });
}
private async searchStatisticsV3(auth: AuthDto, dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
const { filter, scope } = await this.resolveSearchScopeV3(auth, dto);
return this.searchRepository.searchStatisticsV3({ filter }, scope);
}
private async searchRandomV3(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
const { filter, scope } = await this.resolveSearchScopeV3(auth, dto);
const items = await this.searchRepository.searchRandomV3(
dto.size,
{
filter,
withExif: dto.withExif,
withPeople: dto.withPeople,
withStacked: dto.withStacked,
},
scope,
);
return items.map((item) => mapAsset(item, { auth }));
}
private async searchSmartV3(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isSmartSearchEnabled(machineLearning)) {
throw new BadRequestException('Smart search is not enabled');
}
const [{ filter, scope }, embedding] = await Promise.all([
this.resolveSearchScopeV3(auth, dto),
this.resolveEmbedding(auth, dto, machineLearning),
]);
const { offset } = decodeSearchCursor(dto.cursor);
const size = dto.size;
const { hasNextPage, items } = await this.searchRepository.searchSmartV3(
{ size, offset },
{ filter, withExif: dto.withExif, embedding },
scope,
);
return this.mapResponse(items, { auth }, { nextCursor: hasNextPage ? encodeSearchCursor(offset + size) : null });
}
private async resolveSearchScopeV3(
auth: AuthDto,
dto: { filter?: SearchFilter },
): Promise<{ filter: SearchFilter; scope: AssetSearchScope }> {
const filter = dto.filter ?? {};
const effectiveFilter = applyLockedVisibilityPolicy(auth, filter);
// only a top-level any/all constrains every result to accessible albums; anything else keeps
// the ownership scope so an OR branch cannot widen the search to other users' assets
const hasTopLevelAlbums = hasTopLevelPositiveIdsConstraint(filter, 'albumIds');
if (auth.sharedLink && !hasTopLevelAlbums) {
throw new BadRequestException('Shared link access is only allowed in combination with an albumIds filter');
}
const albumIds = collectFilterIds(filter, 'albumIds');
const [userIds] = await Promise.all([
hasTopLevelAlbums ? undefined : this.getUserIdsToSearch(auth),
albumIds.length > 0 ? this.requireAccess({ auth, ids: albumIds, permission: Permission.AlbumRead }) : undefined,
]);
return { filter: effectiveFilter, scope: { userIds, lockedOwnerId: auth.user.id } };
}
private async resolveEmbedding(
auth: AuthDto,
dto: SmartSearchDto,
machineLearning: SystemConfig['machineLearning'],
): Promise<string> {
if (dto.query) {
const key = machineLearning.clip.modelName + dto.query + dto.language;
let embedding = this.embeddingCache.get(key);
if (!embedding) {
embedding = await this.machineLearningRepository.encodeText(dto.query, {
modelName: machineLearning.clip.modelName,
language: dto.language,
});
this.embeddingCache.set(key, embedding);
}
return embedding;
}
if (dto.queryAssetId) {
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [dto.queryAssetId] });
const getEmbeddingResponse = await this.searchRepository.getEmbedding(dto.queryAssetId);
const assetEmbedding = getEmbeddingResponse?.embedding;
if (!assetEmbedding) {
throw new BadRequestException(`Asset ${dto.queryAssetId} has no embedding`);
}
return assetEmbedding;
}
throw new BadRequestException('Either `query` or `queryAssetId` must be set');
}
private async getUserIdsToSearch(auth: AuthDto, visibility?: AssetVisibility): Promise<string[]> {
// Locked assets are personal. Never include partner IDs, regardless of A's elevated session.
if (visibility === AssetVisibility.Locked) {
@@ -242,7 +363,11 @@ export class SearchService extends BaseService {
return [auth.user.id, ...partnerIds];
}
private mapResponse(assets: MapAsset[], nextPage: string | null, options: AssetMapOptions): SearchResponseDto {
private mapResponse(
assets: MapAsset[],
options: AssetMapOptions,
page: { nextPage?: string | null; nextCursor?: string | null } = {},
): SearchResponseDto {
return {
albums: { total: 0, count: 0, items: [], facets: [] },
assets: {
@@ -250,7 +375,8 @@ export class SearchService extends BaseService {
count: assets.length,
items: assets.map((asset) => mapAsset(asset, options)),
facets: [],
nextPage,
nextPage: page.nextPage ?? null,
nextCursor: page.nextCursor ?? null,
},
};
}

View File

@@ -7,21 +7,46 @@ import {
Kysely,
KyselyConfig,
NotNull,
OperandValueExpression,
ReferenceExpression,
Selectable,
SelectQueryBuilder,
ShallowDehydrateObject,
sql,
SqlBool,
} from 'kysely';
import { PostgresJSDialect } from 'kysely-postgres-js';
import { jsonArrayFrom, jsonObjectFrom } from 'kysely/helpers/postgres';
import { Notice, PostgresError } from 'postgres';
import { columns, lockableProperties, LockableProperty, Person } from 'src/database';
import { DummyValue, GenerateSqlQueries } from 'src/decorators';
import { AssetEditActionItem } from 'src/dtos/editing.dto';
import { AssetFileType, AssetOrderBy, AssetVisibility, DatabaseExtension, ExifOrientation } from 'src/enum';
import { AssetSearchBuilderOptions } from 'src/repositories/search.repository';
import {
DEFAULT_SEARCH_ORDER,
IdsFilter,
SearchFilterBranch,
SearchOrder,
StringFilter,
StringPatternFilter,
} from 'src/dtos/search.dto';
import {
AssetFileType,
AssetOrder,
AssetOrderBy,
AssetVisibility,
DatabaseExtension,
ExifOrientation,
SearchOrderField,
} from 'src/enum';
import {
AssetSearchBuilderOptions,
AssetSearchBuilderV3Options,
AssetSearchScope,
} from 'src/repositories/search.repository';
import { DB } from 'src/schema';
import { AssetExifTable } from 'src/schema/tables/asset-exif.table';
import { AudioStreamInfo, VectorExtension, VideoFormat, VideoPacketInfo, VideoStreamInfo } from 'src/types';
import { fromChecksum } from 'src/utils/request';
export const getKyselyConfig = (connection: DatabaseConnectionParams): KyselyConfig => {
return {
@@ -54,6 +79,8 @@ export const getKyselyConfig = (connection: DatabaseConnectionParams): KyselyCon
};
};
const uniqueIds = (ids: string[]) => [...new Set(ids)];
export const asUuid = (id: string | Expression<string>) => sql<string>`${id}::uuid`;
export const anyUuid = (ids: string[]) => sql<string>`any(${`{${ids}}`}::uuid[])`;
@@ -85,16 +112,15 @@ export function withDefaultVisibility<O>(qb: SelectQueryBuilder<DB, 'asset', O>)
return qb.where('asset.visibility', 'in', [sql.lit(AssetVisibility.Archive), sql.lit(AssetVisibility.Timeline)]);
}
const selectExifInfo = (eb: AssetExpressionBuilder) =>
eb.fn
.toJson(eb.table('asset_exif'))
.$castTo<ShallowDehydrateObject<Selectable<AssetExifTable>> | null>()
.as('exifInfo');
// TODO come up with a better query that only selects the fields we need
export function withExif<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
return qb
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
.select((eb) =>
eb.fn
.toJson(eb.table('asset_exif'))
.$castTo<ShallowDehydrateObject<Selectable<AssetExifTable>> | null>()
.as('exifInfo'),
);
return qb.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId').select(selectExifInfo);
}
export function withExifInner<O>(qb: SelectQueryBuilder<DB, 'asset', O>) {
@@ -373,7 +399,7 @@ export function withEdits(eb: ExpressionBuilder<DB, 'asset'>): AliasedEditAction
const joinDeduplicationPlugin = new DeduplicateJoinsPlugin();
/** TODO: This should only be used for search-related queries, not as a general purpose query builder */
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
export function searchAssetBuilderLegacy(kysely: Kysely<DB>, options: AssetSearchBuilderOptions) {
options.withDeleted ||= !!(options.trashedAfter || options.trashedBefore || options.isOffline);
return kysely
@@ -493,6 +519,528 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
.$if(!options.withDeleted, (qb) => qb.where('asset.deletedAt', 'is', null));
}
type AssetExpressionBuilder = ExpressionBuilder<DB, 'asset' | 'asset_exif'>;
const albumAssets = (eb: AssetExpressionBuilder) =>
eb.selectFrom('album_asset').whereRef('album_asset.assetId', '=', 'asset.id');
const visibleFaces = (eb: AssetExpressionBuilder) =>
eb
.selectFrom('asset_face')
.whereRef('asset_face.assetId', '=', 'asset.id')
.where('asset_face.deletedAt', 'is', null)
.where('asset_face.isVisible', '=', true);
const tagAssets = (eb: AssetExpressionBuilder) =>
eb.selectFrom('tag_asset').whereRef('tag_asset.assetId', '=', 'asset.id');
// shared any/all/none mechanics; `matchesAll` only receives deduplicated multi-id lists,
// so its `count(distinct id) = ids.length` check stays satisfiable
function idsPredicates(
eb: AssetExpressionBuilder,
{ any, all, none }: IdsFilter = {},
ops: {
matchesAny: (ids: string[]) => Expression<SqlBool>;
matchesAll: (ids: string[]) => Expression<SqlBool>;
},
) {
const predicates: Expression<SqlBool>[] = [];
if (any) {
predicates.push(ops.matchesAny(any));
}
if (all) {
const ids = uniqueIds(all);
predicates.push(ids.length === 1 ? ops.matchesAny(ids) : ops.matchesAll(ids));
}
if (none) {
predicates.push(eb.not(ops.matchesAny(none)));
}
return predicates;
}
function albumIdsPredicates(eb: AssetExpressionBuilder, filter?: IdsFilter) {
const matching = (ids: string[]) => albumAssets(eb).where('album_asset.albumId', '=', anyUuid(ids));
return idsPredicates(eb, filter, {
matchesAny: (ids) => eb.exists(matching(ids)),
matchesAll: (ids) =>
eb.exists(
matching(ids)
.select('album_asset.assetId')
.groupBy('album_asset.assetId')
.having((eb) => eb.fn.count('album_asset.albumId').distinct(), '=', ids.length),
),
});
}
function personIdsPredicates(eb: AssetExpressionBuilder, filter?: IdsFilter) {
const matching = (ids: string[]) => visibleFaces(eb).where('asset_face.personId', '=', anyUuid(ids));
return idsPredicates(eb, filter, {
matchesAny: (ids) => eb.exists(matching(ids)),
matchesAll: (ids) =>
eb.exists(
matching(ids)
.select('asset_face.assetId')
.groupBy('asset_face.assetId')
.having((eb) => eb.fn.count('asset_face.personId').distinct(), '=', ids.length),
),
});
}
function tagIdsPredicates(eb: AssetExpressionBuilder, filter?: IdsFilter) {
const matching = (ids: string[]) =>
tagAssets(eb)
.innerJoin('tag_closure', 'tag_asset.tagId', 'tag_closure.id_descendant')
.where('tag_closure.id_ancestor', '=', anyUuid(ids));
return idsPredicates(eb, filter, {
matchesAny: (ids) => eb.exists(matching(ids)),
matchesAll: (ids) =>
eb.exists(
matching(ids)
.select('tag_asset.assetId')
.groupBy('tag_asset.assetId')
.having((eb) => eb.fn.count('tag_closure.id_ancestor').distinct(), '=', ids.length),
),
});
}
type ComparisonFilter<T> = {
eq?: T | null;
ne?: T | null;
lt?: T;
lte?: T;
gt?: T;
gte?: T;
in?: T[];
notIn?: T[];
};
// one operator dispatch for every filter shape; the DTO schemas constrain which
// operators (and null literals) each filter can actually carry
function comparisonPredicates<TB extends keyof DB, RE extends ReferenceExpression<DB, TB>>(
eb: ExpressionBuilder<DB, TB>,
column: RE,
filter: ComparisonFilter<OperandValueExpression<DB, TB, RE>> = {},
) {
const predicates: Expression<SqlBool>[] = [];
if (filter.eq !== undefined) {
predicates.push(filter.eq === null ? eb(column, 'is', null) : eb(column, '=', filter.eq));
}
if (filter.ne !== undefined) {
predicates.push(filter.ne === null ? eb(column, 'is not', null) : eb(column, '!=', filter.ne));
}
if (filter.lt !== undefined) {
predicates.push(eb(column, '<', filter.lt));
}
if (filter.lte !== undefined) {
predicates.push(eb(column, '<=', filter.lte));
}
if (filter.gt !== undefined) {
predicates.push(eb(column, '>', filter.gt));
}
if (filter.gte !== undefined) {
predicates.push(eb(column, '>=', filter.gte));
}
if (filter.in !== undefined) {
predicates.push(eb(column, 'in', filter.in));
}
if (filter.notIn !== undefined) {
predicates.push(eb(column, 'not in', filter.notIn));
}
return predicates;
}
type StringColumn =
| 'asset_exif.city'
| 'asset_exif.state'
| 'asset_exif.country'
| 'asset_exif.make'
| 'asset_exif.model'
| 'asset_exif.lensModel'
| 'asset_exif.description'
| 'asset.originalFileName'
| 'asset.originalPath';
function stringPatternPredicates(eb: AssetExpressionBuilder, column: StringColumn, filter: StringPatternFilter = {}) {
const ref = sql.ref(column);
const predicates = comparisonPredicates(eb, column, filter);
if (filter.like !== undefined) {
predicates.push(sql<SqlBool>`f_unaccent(${ref}) ilike ('%' || f_unaccent(${filter.like}) || '%')`);
}
if (filter.notLike !== undefined) {
predicates.push(sql<SqlBool>`f_unaccent(${ref}) not ilike ('%' || f_unaccent(${filter.notLike}) || '%')`);
}
if (filter.startsWith !== undefined) {
predicates.push(sql<SqlBool>`f_unaccent(${ref}) ilike (f_unaccent(${filter.startsWith}) || '%')`);
}
if (filter.endsWith !== undefined) {
predicates.push(sql<SqlBool>`f_unaccent(${ref}) ilike ('%' || f_unaccent(${filter.endsWith}))`);
}
return predicates;
}
function checksumPredicates(eb: AssetExpressionBuilder, filter: StringFilter = {}) {
return comparisonPredicates(eb, 'asset.checksum', {
eq: filter.eq === undefined ? undefined : fromChecksum(filter.eq),
ne: filter.ne === undefined ? undefined : fromChecksum(filter.ne),
in: filter.in?.map((checksum) => fromChecksum(checksum)),
notIn: filter.notIn?.map((checksum) => fromChecksum(checksum)),
});
}
const encodedVideoFiles = (eb: AssetExpressionBuilder) =>
eb
.selectFrom('asset_file')
.whereRef('asset_file.assetId', '=', 'asset.id')
.where('asset_file.type', '=', AssetFileType.EncodedVideo);
function existsPredicates(
eb: AssetExpressionBuilder,
filter: { eq: boolean } | undefined,
subquery: () => Expression<unknown>,
): Expression<SqlBool>[] {
if (!filter) {
return [];
}
const exists = eb.exists(subquery());
return [filter.eq ? exists : eb.not(exists)];
}
// predicates are collected as expressions rather than chained `where` calls so the same
// helpers can build each `or` branch, which must compose into eb.and/eb.or
function branchPredicates(eb: AssetExpressionBuilder, branch: SearchFilterBranch) {
const { encodedVideoPath } = branch;
return [
...comparisonPredicates(eb, 'asset.id', branch.id),
...comparisonPredicates(eb, 'asset.libraryId', branch.libraryId),
...comparisonPredicates(eb, 'asset.type', branch.type),
...comparisonPredicates(eb, 'asset.visibility', branch.visibility),
...(branch.isFavorite ? [eb('asset.isFavorite', '=', branch.isFavorite.eq)] : []),
...(branch.isOffline ? [eb('asset.isOffline', '=', branch.isOffline.eq)] : []),
...(branch.isMotion ? [eb('asset.livePhotoVideoId', branch.isMotion.eq ? 'is not' : 'is', null)] : []),
...existsPredicates(eb, branch.isEncoded, () => encodedVideoFiles(eb)),
...existsPredicates(eb, branch.hasAlbums, () => albumAssets(eb)),
...existsPredicates(eb, branch.hasPeople, () => visibleFaces(eb)),
...existsPredicates(eb, branch.hasTags, () => tagAssets(eb)),
...comparisonPredicates(eb, 'asset_exif.city', branch.city),
...comparisonPredicates(eb, 'asset_exif.state', branch.state),
...comparisonPredicates(eb, 'asset_exif.country', branch.country),
...comparisonPredicates(eb, 'asset_exif.make', branch.make),
...comparisonPredicates(eb, 'asset_exif.model', branch.model),
...comparisonPredicates(eb, 'asset_exif.lensModel', branch.lensModel),
...stringPatternPredicates(eb, 'asset_exif.description', branch.description),
...stringPatternPredicates(eb, 'asset.originalFileName', branch.originalFileName),
...stringPatternPredicates(eb, 'asset.originalPath', branch.originalPath),
...(branch.ocr
? [
eb.exists(
eb
.selectFrom('ocr_search')
.whereRef('ocr_search.assetId', '=', 'asset.id')
.where(
sql<SqlBool>`f_unaccent(ocr_search.text) %>> f_unaccent(${tokenizeForSearch(branch.ocr.matches).join(' ')})`,
),
),
]
: []),
...comparisonPredicates(eb, 'asset_exif.rating', branch.rating),
...comparisonPredicates(eb, 'asset_exif.fileSizeInByte', branch.fileSizeInBytes),
...comparisonPredicates(eb, 'asset.fileCreatedAt', branch.takenAt),
...comparisonPredicates(eb, 'asset.createdAt', branch.createdAt),
...comparisonPredicates(eb, 'asset.updatedAt', branch.updatedAt),
...comparisonPredicates(eb, 'asset.deletedAt', branch.trashedAt),
...albumIdsPredicates(eb, branch.albumIds),
...personIdsPredicates(eb, branch.personIds),
...tagIdsPredicates(eb, branch.tagIds),
...checksumPredicates(eb, branch.checksum),
...(encodedVideoPath
? [
eb.exists(
encodedVideoFiles(eb)
.where('asset_file.isEdited', '=', false)
.where((eb) => eb.and(comparisonPredicates(eb, 'asset_file.path', encodedVideoPath))),
),
]
: []),
];
}
// ordering is deliberately left to the caller so aggregate-only consumers (counts, stats)
// can compose the same filters without stripping an order by
export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuilderV3Options, scope: AssetSearchScope) {
const filter = options.filter ?? {};
return (
kysely
.withPlugin(joinDeduplicationPlugin)
.selectFrom('asset')
// postgres eliminates the left join when no exif column is referenced, so unused joins are free
.leftJoin('asset_exif', 'asset.id', 'asset_exif.assetId')
.$if(!!options.withExif, (qb) => qb.select(selectExifInfo))
.$if(!!scope.userIds && scope.userIds.length > 0, (qb) => qb.where('asset.ownerId', '=', anyUuid(scope.userIds!)))
.where((eb) =>
eb.or([eb('asset.visibility', '!=', AssetVisibility.Locked), eb('asset.ownerId', '=', scope.lockedOwnerId)]),
)
.$if(!!(options.withFaces || options.withPeople), (qb) => qb.select(withFacesAndPeople))
.$if(options.withStacked === false, (qb) => qb.where('asset.stackId', 'is', null))
.where((eb) => {
const predicates = branchPredicates(eb, filter);
if (filter.or && filter.or.length > 0) {
predicates.push(eb.or(filter.or.map((branch) => eb.and(branchPredicates(eb, branch)))));
}
return predicates.length > 0 ? eb.and(predicates) : eb.lit(true);
})
);
}
const searchOrderColumns = {
[SearchOrderField.FileCreatedAt]: { column: 'asset.fileCreatedAt', nullable: false },
[SearchOrderField.LocalDateTime]: { column: 'asset.localDateTime', nullable: false },
[SearchOrderField.FileSizeInBytes]: { column: 'asset_exif.fileSizeInByte', nullable: true },
[SearchOrderField.Rating]: { column: 'asset_exif.rating', nullable: true },
} as const;
export function withSearchOrder(qb: ReturnType<typeof searchAssetBuilder>, order?: SearchOrder) {
const { field, direction } = order ?? DEFAULT_SEARCH_ORDER;
const { column, nullable } = searchOrderColumns[field];
return (
qb
.orderBy(column, (ob) => {
const ordered = direction === AssetOrder.Asc ? ob.asc() : ob.desc();
// nulls last: assets without an asset_exif row would otherwise lead descending results
return nullable ? ordered.nullsLast() : ordered;
})
// id tie-break for deterministic pagination
.orderBy('asset.id', direction)
);
}
const scopeExample: AssetSearchScope = { userIds: [DummyValue.UUID], lockedOwnerId: DummyValue.UUID };
export const searchMetadataV3Examples: GenerateSqlQueries[] = [
{ name: 'baseline', params: [{ size: 100 }, {}, scopeExample] },
{ name: 'empty', params: [{ size: 100 }, {}, { lockedOwnerId: DummyValue.UUID }] },
{
name: 'or-exif-only',
params: [
{ size: 100 },
{
filter: { or: [{ city: { eq: DummyValue.STRING } }] },
},
scopeExample,
],
},
{
name: 'string-eq-null',
params: [{ size: 100 }, { filter: { city: { eq: null } } }, scopeExample],
},
{
name: 'string-pattern-like',
params: [
{ size: 100 },
{
filter: { description: { like: DummyValue.STRING } },
},
scopeExample,
],
},
{
name: 'string-pattern-notLike',
params: [
{ size: 100 },
{
filter: { description: { notLike: DummyValue.STRING } },
},
scopeExample,
],
},
{
name: 'string-pattern-startsWith',
params: [
{ size: 100 },
{
filter: { originalFileName: { startsWith: DummyValue.STRING } },
},
scopeExample,
],
},
{
name: 'string-similarity-ocr',
params: [{ size: 100 }, { filter: { ocr: { matches: DummyValue.STRING } } }, scopeExample],
},
{
name: 'ids-any',
params: [{ size: 100 }, { filter: { albumIds: { any: [DummyValue.UUID] } } }, scopeExample],
},
{
name: 'ids-all',
params: [
{ size: 100 },
{
filter: { personIds: { all: [DummyValue.UUID, DummyValue.UUID_1] } },
},
scopeExample,
],
},
{
name: 'ids-all-single',
params: [{ size: 100 }, { filter: { albumIds: { all: [DummyValue.UUID] } } }, scopeExample],
},
{
name: 'ids-none',
params: [{ size: 100 }, { filter: { tagIds: { none: [DummyValue.UUID] } } }, scopeExample],
},
{
name: 'ids-tags-all',
params: [
{ size: 100 },
{
filter: { tagIds: { all: [DummyValue.UUID, DummyValue.UUID_1] } },
},
scopeExample,
],
},
{
name: 'has-albums-false',
params: [{ size: 100 }, { filter: { hasAlbums: { eq: false } } }, scopeExample],
},
{
name: 'is-encoded',
params: [{ size: 100 }, { filter: { isEncoded: { eq: true } } }, scopeExample],
},
{
name: 'number-range',
params: [
{ size: 100 },
{
filter: { fileSizeInBytes: { gte: 100, lte: 1000 } },
},
scopeExample,
],
},
{
name: 'date-eq',
params: [{ size: 100 }, { filter: { takenAt: { eq: DummyValue.DATE } } }, scopeExample],
},
{
name: 'date-range',
params: [
{ size: 100 },
{
filter: { takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE } },
},
scopeExample,
],
},
{
name: 'order-fileSize-noExif',
params: [
{ size: 100 },
{
order: { field: SearchOrderField.FileSizeInBytes, direction: AssetOrder.Desc },
withExif: false,
},
scopeExample,
],
},
{
name: 'order-rating-withExif',
params: [
{ size: 100 },
{
order: { field: SearchOrderField.Rating, direction: AssetOrder.Asc },
withExif: true,
},
scopeExample,
],
},
{
name: 'or-branches',
params: [
{ size: 100 },
{
filter: {
or: [{ isFavorite: { eq: true } }, { personIds: { any: [DummyValue.UUID] } }],
},
},
scopeExample,
],
},
{
name: 'or-with-top-level',
params: [
{ size: 100 },
{
filter: {
takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE },
or: [{ isFavorite: { eq: true } }, { albumIds: { any: [DummyValue.UUID] } }],
},
},
scopeExample,
],
},
{
name: 'cursor-offset',
params: [{ size: 100, offset: 100 }, { filter: { isFavorite: { eq: true } } }, scopeExample],
},
];
export const searchRandomV3Examples: GenerateSqlQueries[] = [
{ name: 'baseline', params: [100, {}, scopeExample] },
{
name: 'with-filter',
params: [100, { filter: { isFavorite: { eq: true } } }, scopeExample],
},
];
export const searchSmartV3Examples: GenerateSqlQueries[] = [
{
name: 'baseline',
params: [{ size: 100 }, { embedding: DummyValue.VECTOR }, scopeExample],
},
{
name: 'with-filter',
params: [
{ size: 100 },
{
embedding: DummyValue.VECTOR,
filter: { takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE } },
},
scopeExample,
],
},
{
name: 'cursor-offset',
params: [{ size: 100, offset: 100 }, { embedding: DummyValue.VECTOR }, scopeExample],
},
];
export const searchStatisticsV3Examples: GenerateSqlQueries[] = [
{ name: 'baseline', params: [{}, scopeExample] },
{
name: 'with-filter',
params: [
{
filter: {
takenAt: { gte: DummyValue.DATE, lt: DummyValue.DATE },
fileSizeInBytes: { gte: 100 },
},
},
scopeExample,
],
},
{
name: 'with-or',
params: [
{
filter: {
or: [{ isFavorite: { eq: true } }, { hasAlbums: { eq: false } }],
},
},
scopeExample,
],
},
];
export type ReindexVectorIndexOptions = { indexName: string; lists?: number };
type VectorIndexQueryOptions = { table: string; vectorExtension: VectorExtension } & ReindexVectorIndexOptions;

View File

@@ -0,0 +1,38 @@
import { BadRequestException } from '@nestjs/common';
import { decodeSearchCursor, encodeSearchCursor } from 'src/utils/search-cursor';
import { describe, expect, it } from 'vitest';
describe('encodeSearchCursor', () => {
it('should produce an opaque base64url string', () => {
const cursor = encodeSearchCursor(250);
expect(cursor).toMatch(/^[\w-]+$/);
});
it('should round-trip an offset', () => {
for (const offset of [0, 1, 250, 1_000_000]) {
expect(decodeSearchCursor(encodeSearchCursor(offset))).toEqual({ offset });
}
});
});
describe('decodeSearchCursor', () => {
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString('base64url');
it('should treat a missing cursor as the first page', () => {
expect(decodeSearchCursor(undefined)).toEqual({ offset: 0 });
});
it.each([
['empty string', ''],
['garbage that is not base64url', '!!!not-base64!!!'],
['base64 of non-JSON', Buffer.from('not json').toString('base64url')],
['JSON without an offset', encode({})],
['JSON with a non-object payload', encode(42)],
['null payload', encode(null)],
['negative offset', encode({ o: -1 })],
['fractional offset', encode({ o: 1.5 })],
['string offset', encode({ o: '5' })],
])('should reject %s', (_, cursor) => {
expect(() => decodeSearchCursor(cursor)).toThrowError(new BadRequestException('Invalid cursor'));
});
});

View File

@@ -0,0 +1,22 @@
import { BadRequestException } from '@nestjs/common';
import z from 'zod';
const SearchCursorPayloadSchema = z.object({
o: z.int().min(0),
});
export const encodeSearchCursor = (offset: number): string =>
Buffer.from(JSON.stringify({ o: offset } satisfies z.infer<typeof SearchCursorPayloadSchema>)).toString('base64url');
export const decodeSearchCursor = (cursor?: string): { offset: number } => {
if (cursor === undefined) {
return { offset: 0 };
}
try {
const payload = SearchCursorPayloadSchema.parse(JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8')));
return { offset: payload.o };
} catch {
throw new BadRequestException('Invalid cursor');
}
};

View File

@@ -0,0 +1,102 @@
import { UnauthorizedException } from '@nestjs/common';
import { SearchFilter } from 'src/dtos/search.dto';
import { AssetVisibility } from 'src/enum';
import {
applyLockedVisibilityPolicy,
collectFilterIds,
hasTopLevelPositiveIdsConstraint,
} from 'src/utils/search-filter';
import { AuthFactory } from 'test/factories/auth.factory';
import { describe, expect, it } from 'vitest';
const elevatedAuth = () => AuthFactory.from().session({ hasElevatedPermission: true }).build();
const unelevatedAuth = () => AuthFactory.from().session().build();
describe(applyLockedVisibilityPolicy.name, () => {
it('should let an elevated session query locked assets', () => {
const filter = { visibility: { eq: AssetVisibility.Locked } };
expect(applyLockedVisibilityPolicy(elevatedAuth(), filter)).toBe(filter);
});
it('should reject an unelevated session when any operator permits locked', () => {
for (const visibility of [
{ eq: AssetVisibility.Locked },
{ ne: AssetVisibility.Timeline },
{ in: [AssetVisibility.Locked, AssetVisibility.Timeline] },
{ notIn: [AssetVisibility.Timeline] },
]) {
expect(() => applyLockedVisibilityPolicy(unelevatedAuth(), { visibility })).toThrow(UnauthorizedException);
}
});
it('should keep a filter whose top-level visibility excludes locked', () => {
for (const visibility of [
{ eq: AssetVisibility.Timeline },
{ ne: AssetVisibility.Locked },
{ in: [AssetVisibility.Timeline, AssetVisibility.Archive] },
{ notIn: [AssetVisibility.Locked] },
]) {
const filter = { visibility };
expect(applyLockedVisibilityPolicy(unelevatedAuth(), filter)).toBe(filter);
}
});
it('should let a safe top-level visibility decide over could-match branches', () => {
const filter = { visibility: { ne: AssetVisibility.Locked }, or: [{ visibility: { eq: AssetVisibility.Locked } }] };
expect(applyLockedVisibilityPolicy(unelevatedAuth(), filter)).toBe(filter);
});
it('should reject a branch that permits locked when the top level has no visibility', () => {
const filter = {
or: [{ city: { eq: 'Oslo' } }, { visibility: { in: [AssetVisibility.Locked, AssetVisibility.Timeline] } }],
};
expect(() => applyLockedVisibilityPolicy(unelevatedAuth(), filter)).toThrow(UnauthorizedException);
});
it('should otherwise inject visibility != locked without mutating the input', () => {
const filter = { city: { eq: 'Oslo' }, visibility: undefined };
expect(applyLockedVisibilityPolicy(unelevatedAuth(), filter)).toEqual({
city: { eq: 'Oslo' },
visibility: { ne: AssetVisibility.Locked },
});
expect(filter.visibility).toBeUndefined();
const branched = { or: [{ isFavorite: { eq: true } }, { visibility: { eq: AssetVisibility.Timeline } }] };
expect(applyLockedVisibilityPolicy(unelevatedAuth(), branched).visibility).toEqual({ ne: AssetVisibility.Locked });
});
});
describe(collectFilterIds.name, () => {
it('should union and dedupe ids across operators and branches', () => {
const [albumA, albumB, albumC] = [
'00000000-0000-4000-8000-00000000000a',
'00000000-0000-4000-8000-00000000000b',
'00000000-0000-4000-8000-00000000000c',
];
const filter: SearchFilter = {
albumIds: { any: [albumA], none: [albumB] },
or: [{ albumIds: { all: [albumC, albumA] } }, { city: { eq: 'Oslo' } }],
};
expect(collectFilterIds(filter, 'albumIds').toSorted()).toEqual([albumA, albumB, albumC]);
expect(collectFilterIds({}, 'albumIds')).toEqual([]);
});
it('should only collect ids of the requested field', () => {
const albumId = '00000000-0000-4000-8000-00000000000a';
const personId = '00000000-0000-4000-8000-00000000000b';
const filter = { albumIds: { any: [albumId] }, personIds: { any: [personId] } };
expect(collectFilterIds(filter, 'personIds')).toEqual([personId]);
});
});
describe(hasTopLevelPositiveIdsConstraint.name, () => {
it('should detect only top-level any/all constraints', () => {
const albumId = '00000000-0000-4000-8000-00000000000a';
expect(hasTopLevelPositiveIdsConstraint({ albumIds: { any: [albumId] } }, 'albumIds')).toBe(true);
expect(hasTopLevelPositiveIdsConstraint({ albumIds: { all: [albumId] } }, 'albumIds')).toBe(true);
expect(hasTopLevelPositiveIdsConstraint({ albumIds: { none: [albumId] } }, 'albumIds')).toBe(false);
expect(hasTopLevelPositiveIdsConstraint({ or: [{ albumIds: { any: [albumId] } }] }, 'albumIds')).toBe(false);
expect(hasTopLevelPositiveIdsConstraint({ albumIds: { any: [albumId] } }, 'tagIds')).toBe(false);
expect(hasTopLevelPositiveIdsConstraint({}, 'albumIds')).toBe(false);
});
});

View File

@@ -0,0 +1,70 @@
import { AuthDto } from 'src/dtos/auth.dto';
import { SearchFilter, SearchFilterBranch } from 'src/dtos/search.dto';
import { AssetVisibility } from 'src/enum';
import { requireElevatedPermission } from 'src/utils/access';
type EnumField = 'type' | 'visibility';
type EnumOperator = keyof NonNullable<SearchFilterBranch[EnumField]>;
type EnumOperandMap<T> = { eq: T; ne: T; in: T[]; notIn: T[] };
type EnumCondition<T> = { [K in EnumOperator]?: EnumOperandMap<T>[K] };
type IdsFilterField = 'albumIds' | 'personIds' | 'tagIds';
const filterBranches = (filter: SearchFilter): SearchFilterBranch[] => [filter, ...(filter.or ?? [])];
/** Whether a row with `value` can satisfy the condition. A missing operator allows any value. */
const canMatch = <T>(condition: EnumCondition<T>, value: T): boolean =>
(condition.eq === undefined || condition.eq === value) &&
(condition.ne === undefined || condition.ne !== value) &&
(condition.in === undefined || condition.in.includes(value)) &&
(condition.notIn === undefined || !condition.notIn.includes(value));
/**
* The conditions that decide which `field` values the filter can return. A top-level condition decides alone, otherwise each branch itself.
*/
const decidingConditions = <F extends EnumField>(filter: SearchFilter, field: F) => {
if (filter[field] !== undefined) {
return [filter[field]];
}
return filterBranches(filter)
.map((branch) => branch[field])
.filter((condition) => condition !== undefined);
};
/**
* Keeps locked assets out of search results unless the session is elevated: a filter that asks for
* them is rejected with 401, and any other filter gets `visibility != locked` ANDed in.
*/
export const applyLockedVisibilityPolicy = (auth: AuthDto, filter: SearchFilter): SearchFilter => {
if (auth.session?.hasElevatedPermission) {
return filter;
}
if (decidingConditions(filter, 'visibility').some((condition) => canMatch(condition, AssetVisibility.Locked))) {
requireElevatedPermission(auth);
}
if (filter.visibility !== undefined) {
return filter;
}
return { ...filter, visibility: { ne: AssetVisibility.Locked } };
};
export const collectFilterIds = (filter: SearchFilter, field: IdsFilterField): string[] => {
const ids = new Set<string>();
for (const branch of filterBranches(filter)) {
for (const operator of ['any', 'all', 'none'] as const) {
for (const id of branch[field]?.[operator] ?? []) {
ids.add(id);
}
}
}
return [...ids];
};
/** Whether the top level constrains results to specific `field` ids (`any`/`all`; `none` only excludes). */
export const hasTopLevelPositiveIdsConstraint = (filter: SearchFilter, field: IdsFilterField): boolean =>
filter[field]?.any !== undefined || filter[field]?.all !== undefined;

View File

@@ -42,7 +42,7 @@ export function nonEmptyPartial<T extends z.ZodRawShape>(shape: T) {
.object(shape)
.partial()
.refine((data) => Object.values(data as Record<string, unknown>).some((value) => value !== undefined), {
message: 'At least one field must be provided',
message: `At least one of the following fields is required: ${Object.keys(shape).join(', ')}`,
});
}

View File

@@ -1,6 +1,6 @@
import { Kysely } from 'kysely';
import { SearchSuggestionType } from 'src/dtos/search.dto';
import { AlbumUserRole, AssetVisibility } from 'src/enum';
import { AlbumUserRole, AssetOrder, AssetVisibility, SearchOrderField } from 'src/enum';
import { AccessRepository } from 'src/repositories/access.repository';
import { AssetRepository } from 'src/repositories/asset.repository';
import { DatabaseRepository } from 'src/repositories/database.repository';
@@ -16,6 +16,8 @@ import { getKyselyDB } from 'test/utils';
let defaultDatabase: Kysely<DB>;
const unitVector = (index: number) => JSON.stringify(Array.from({ length: 512 }, (_, i) => (i === index ? 1 : 0)));
const setup = (db?: Kysely<DB>) => {
return newMediumService(SearchService, {
database: db || defaultDatabase,
@@ -56,7 +58,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
await expect(sut.searchLargeAssets(auth, {})).resolves.toEqual([
await expect(sut.searchLargeAssets(auth, { size: 250 })).resolves.toEqual([
expect.objectContaining({ id: assets[2].id }),
expect.objectContaining({ id: assets[0].id }),
expect.objectContaining({ id: assets[1].id }),
@@ -120,7 +122,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, { withStacked: false });
const response = await sut.searchMetadata(auth, { size: 250, withStacked: false });
expect(response.assets.items.length).toBe(1);
expect(response.assets.items[0].id).toBe(unstackedAsset.id);
@@ -135,7 +137,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, { withStacked: false });
const response = await sut.searchMetadata(auth, { size: 250, withStacked: false });
expect(response.assets.items.length).toBe(0);
});
@@ -148,7 +150,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } });
const response = await sut.searchMetadata(auth, { withStacked: false });
const response = await sut.searchMetadata(auth, { size: 250, withStacked: false });
expect(response.assets.items.length).toBe(1);
});
@@ -168,7 +170,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, { albumIds: [album.id] });
const response = await sut.searchMetadata(auth, { size: 250, albumIds: [album.id] });
expect(response.assets.items.length).toBe(1);
});
@@ -186,7 +188,7 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
await expect(sut.searchMetadata(auth, { albumIds: [album.id] })).rejects.toThrow(
await expect(sut.searchMetadata(auth, { size: 250, albumIds: [album.id] })).rejects.toThrow(
'Not found or no album.read access',
);
});
@@ -222,9 +224,196 @@ describe(SearchService.name, () => {
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchRandom(auth, {});
const response = await sut.searchRandom(auth, { size: 250 });
expect(response.length).toBe(0);
});
});
describe('new search shape', () => {
it('should filter by an exif field and return a cursor-less single page', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: asset.id, city: 'Oslo' });
const { asset: other } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: other.id, city: 'Bergen' });
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, { size: 250, filter: { city: { eq: 'Oslo' } } });
expect(response.assets.items).toEqual([expect.objectContaining({ id: asset.id })]);
expect(response.assets.nextPage).toBeNull();
expect(response.assets.nextCursor).toBeNull();
});
it('should combine OR branches', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { asset: oslo } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: oslo.id, city: 'Oslo' });
const { asset: favorite } = await ctx.newAsset({ ownerId: user.id, isFavorite: true });
await ctx.newAsset({ ownerId: user.id });
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, {
size: 250,
filter: { or: [{ city: { eq: 'Oslo' } }, { isFavorite: { eq: true } }] },
});
expect(response.assets.items.map(({ id }) => id).toSorted()).toEqual([oslo.id, favorite.id].toSorted());
});
it('should widen to album assets only for a top-level album constraint', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { user: otherUser } = await ctx.newUser();
const { asset } = await ctx.newAsset({ ownerId: otherUser.id });
const { album } = await ctx.newAlbum({ ownerId: user.id });
await ctx.newAlbumAsset({ albumId: album.id, assetId: asset.id });
const auth = factory.auth({ user: { id: user.id } });
const topLevel = await sut.searchMetadata(auth, { size: 250, filter: { albumIds: { any: [album.id] } } });
expect(topLevel.assets.items).toEqual([expect.objectContaining({ id: asset.id })]);
const branchOnly = await sut.searchMetadata(auth, {
size: 250,
filter: { or: [{ albumIds: { any: [album.id] } }] },
});
expect(branchOnly.assets.items).toEqual([]);
});
it('should reject an inaccessible album anywhere in the filter', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { user: otherUser } = await ctx.newUser();
const { album } = await ctx.newAlbum({ ownerId: otherUser.id });
const auth = factory.auth({ user: { id: user.id } });
await expect(
sut.searchMetadata(auth, { size: 250, filter: { or: [{ albumIds: { none: [album.id] } }] } }),
).rejects.toThrow('Not found or no album.read access');
});
it('should return locked assets only to an elevated session that asks for them', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { asset: timeline } = await ctx.newAsset({ ownerId: user.id });
const { asset: locked } = await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked });
const unelevated = await sut.searchMetadata(factory.auth({ user: { id: user.id } }), { size: 250, filter: {} });
expect(unelevated.assets.items).toEqual([expect.objectContaining({ id: timeline.id })]);
const elevatedAuth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } });
const elevated = await sut.searchMetadata(elevatedAuth, {
size: 250,
filter: { visibility: { eq: AssetVisibility.Locked } },
});
expect(elevated.assets.items).toEqual([expect.objectContaining({ id: locked.id })]);
});
it('should exclude partner assets from a locked-only search', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { user: partner } = await ctx.newUser();
await ctx.newPartner({ sharedById: partner.id, sharedWithId: user.id });
const { asset: ownLocked } = await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked });
await ctx.newAsset({ ownerId: partner.id, visibility: AssetVisibility.Locked });
const auth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } });
const response = await sut.searchMetadata(auth, {
size: 250,
filter: { visibility: { eq: AssetVisibility.Locked } },
});
expect(response.assets.items).toEqual([expect.objectContaining({ id: ownLocked.id })]);
});
it('should never return partner locked assets, even for locked-matching mixed filters', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const { user: partner } = await ctx.newUser();
await ctx.newPartner({ sharedById: partner.id, sharedWithId: user.id });
const { asset: ownLocked } = await ctx.newAsset({ ownerId: user.id, visibility: AssetVisibility.Locked });
const { asset: partnerTimeline } = await ctx.newAsset({ ownerId: partner.id });
await ctx.newAsset({ ownerId: partner.id, visibility: AssetVisibility.Locked });
const auth = factory.auth({ user: { id: user.id }, session: { hasElevatedPermission: true } });
const response = await sut.searchMetadata(auth, {
size: 250,
filter: { visibility: { in: [AssetVisibility.Locked, AssetVisibility.Timeline] } },
});
const ids = response.assets.items.map(({ id }) => id);
expect(ids.toSorted()).toEqual([ownLocked.id, partnerTimeline.id].toSorted());
});
it('should paginate with an opaque cursor', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
for (let i = 0; i < 3; i++) {
await ctx.newAsset({ ownerId: user.id });
}
const auth = factory.auth({ user: { id: user.id } });
const firstPage = await sut.searchMetadata(auth, { filter: {}, size: 2 });
expect(firstPage.assets.items.length).toBe(2);
expect(firstPage.assets.nextPage).toBeNull();
expect(firstPage.assets.nextCursor).toEqual(expect.any(String));
const secondPage = await sut.searchMetadata(auth, { cursor: firstPage.assets.nextCursor!, size: 2 });
expect(secondPage.assets.items.length).toBe(1);
expect(secondPage.assets.nextCursor).toBeNull();
const ids = [...firstPage.assets.items, ...secondPage.assets.items].map(({ id }) => id);
expect(new Set(ids).size).toBe(3);
});
it('should order by fileSizeInBytes', async () => {
const { sut, ctx } = setup();
const { user } = await ctx.newUser();
const sizes = [12_334, 599, 123_456];
const assetIds: string[] = [];
for (const fileSizeInByte of sizes) {
const { asset } = await ctx.newAsset({ ownerId: user.id });
await ctx.newExif({ assetId: asset.id, fileSizeInByte });
assetIds.push(asset.id);
}
const auth = factory.auth({ user: { id: user.id } });
const response = await sut.searchMetadata(auth, {
size: 250,
orderBy: { field: SearchOrderField.FileSizeInBytes, direction: AssetOrder.Asc },
});
expect(response.assets.items.map(({ id }) => id)).toEqual([assetIds[1], assetIds[0], assetIds[2]]);
});
it('should order smart search results by embedding distance with cursor offsets', async () => {
const { ctx } = setup();
const { user } = await ctx.newUser();
const searchRepository = ctx.get(SearchRepository);
const assetIds: string[] = [];
for (let i = 0; i < 3; i++) {
const { asset } = await ctx.newAsset({ ownerId: user.id });
await searchRepository.upsert(asset.id, unitVector(i));
assetIds.push(asset.id);
}
const options = { filter: {}, embedding: unitVector(0) };
const scope = { userIds: [user.id], lockedOwnerId: user.id };
const firstPage = await searchRepository.searchSmartV3({ size: 2 }, options, scope);
expect(firstPage.items.length).toBe(2);
expect(firstPage.items[0].id).toBe(assetIds[0]);
expect(firstPage.hasNextPage).toBe(true);
const secondPage = await searchRepository.searchSmartV3({ size: 2, offset: 2 }, options, scope);
expect(secondPage.items.length).toBe(1);
expect(secondPage.hasNextPage).toBe(false);
});
});
});

View File

@@ -1,9 +1,10 @@
<script lang="ts">
import { shortcuts } from '$lib/actions/shortcut';
import { Icon } from '@immich/ui';
import { mdiChevronRight } from '@mdi/js';
import { mdiChevronRight, mdiChevronLeft } from '@mdi/js';
import { t } from 'svelte-i18n';
import NavigationArea from '../NavigationArea.svelte';
import { languageManager } from '$lib/managers/language-manager.svelte';
interface Props {
onNextAsset: () => void;
@@ -20,5 +21,5 @@
/>
<NavigationArea onClick={onNextAsset} label={$t('view_next_asset')}>
<Icon icon={mdiChevronRight} size="36" aria-hidden />
<Icon icon={languageManager.rtl ? mdiChevronLeft : mdiChevronRight} size="36" aria-hidden />
</NavigationArea>

View File

@@ -1,9 +1,10 @@
<script lang="ts">
import { shortcuts } from '$lib/actions/shortcut';
import { Icon } from '@immich/ui';
import { mdiChevronLeft } from '@mdi/js';
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
import { t } from 'svelte-i18n';
import NavigationArea from '../NavigationArea.svelte';
import { languageManager } from '$lib/managers/language-manager.svelte';
interface Props {
onPreviousAsset: () => void;
@@ -20,5 +21,5 @@
/>
<NavigationArea onClick={onPreviousAsset} label={$t('view_previous_asset')}>
<Icon icon={mdiChevronLeft} size="36" aria-hidden />
<Icon icon={languageManager.rtl ? mdiChevronRight : mdiChevronLeft} size="36" aria-hidden />
</NavigationArea>

View File

@@ -95,6 +95,8 @@
const getPreviousRoute = $page.url.searchParams.get(QueryParameter.PREVIOUS_ROUTE);
if (getPreviousRoute && !isExternalUrl(getPreviousRoute)) {
previousRoute = getPreviousRoute;
} else if ($page.params.assetId) {
previousRoute = Route.viewPerson(data.person);
}
if (action == 'merge') {
viewMode = PersonPageViewMode.MERGE_PEOPLE;