feat(server): add /search/statistics resource (#18885)

This commit is contained in:
Jonathan Gilbert 2025-06-07 11:12:53 +10:00 committed by GitHub
parent ecb16d9907
commit fb4be6e231
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 954 additions and 16 deletions

View File

@ -177,6 +177,7 @@ Class | Method | HTTP request | Description
*SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities | *SearchApi* | [**getAssetsByCity**](doc//SearchApi.md#getassetsbycity) | **GET** /search/cities |
*SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore | *SearchApi* | [**getExploreData**](doc//SearchApi.md#getexploredata) | **GET** /search/explore |
*SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions | *SearchApi* | [**getSearchSuggestions**](doc//SearchApi.md#getsearchsuggestions) | **GET** /search/suggestions |
*SearchApi* | [**searchAssetStatistics**](doc//SearchApi.md#searchassetstatistics) | **POST** /search/statistics |
*SearchApi* | [**searchAssets**](doc//SearchApi.md#searchassets) | **POST** /search/metadata | *SearchApi* | [**searchAssets**](doc//SearchApi.md#searchassets) | **POST** /search/metadata |
*SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person | *SearchApi* | [**searchPerson**](doc//SearchApi.md#searchperson) | **GET** /search/person |
*SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places | *SearchApi* | [**searchPlaces**](doc//SearchApi.md#searchplaces) | **GET** /search/places |
@ -425,6 +426,7 @@ Class | Method | HTTP request | Description
- [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md) - [SearchFacetCountResponseDto](doc//SearchFacetCountResponseDto.md)
- [SearchFacetResponseDto](doc//SearchFacetResponseDto.md) - [SearchFacetResponseDto](doc//SearchFacetResponseDto.md)
- [SearchResponseDto](doc//SearchResponseDto.md) - [SearchResponseDto](doc//SearchResponseDto.md)
- [SearchStatisticsResponseDto](doc//SearchStatisticsResponseDto.md)
- [SearchSuggestionType](doc//SearchSuggestionType.md) - [SearchSuggestionType](doc//SearchSuggestionType.md)
- [ServerAboutResponseDto](doc//ServerAboutResponseDto.md) - [ServerAboutResponseDto](doc//ServerAboutResponseDto.md)
- [ServerApkLinksDto](doc//ServerApkLinksDto.md) - [ServerApkLinksDto](doc//ServerApkLinksDto.md)
@ -453,6 +455,7 @@ Class | Method | HTTP request | Description
- [StackCreateDto](doc//StackCreateDto.md) - [StackCreateDto](doc//StackCreateDto.md)
- [StackResponseDto](doc//StackResponseDto.md) - [StackResponseDto](doc//StackResponseDto.md)
- [StackUpdateDto](doc//StackUpdateDto.md) - [StackUpdateDto](doc//StackUpdateDto.md)
- [StatisticsSearchDto](doc//StatisticsSearchDto.md)
- [SyncAckDeleteDto](doc//SyncAckDeleteDto.md) - [SyncAckDeleteDto](doc//SyncAckDeleteDto.md)
- [SyncAckDto](doc//SyncAckDto.md) - [SyncAckDto](doc//SyncAckDto.md)
- [SyncAckSetDto](doc//SyncAckSetDto.md) - [SyncAckSetDto](doc//SyncAckSetDto.md)

View File

@ -214,6 +214,7 @@ part 'model/search_explore_response_dto.dart';
part 'model/search_facet_count_response_dto.dart'; part 'model/search_facet_count_response_dto.dart';
part 'model/search_facet_response_dto.dart'; part 'model/search_facet_response_dto.dart';
part 'model/search_response_dto.dart'; part 'model/search_response_dto.dart';
part 'model/search_statistics_response_dto.dart';
part 'model/search_suggestion_type.dart'; part 'model/search_suggestion_type.dart';
part 'model/server_about_response_dto.dart'; part 'model/server_about_response_dto.dart';
part 'model/server_apk_links_dto.dart'; part 'model/server_apk_links_dto.dart';
@ -242,6 +243,7 @@ part 'model/source_type.dart';
part 'model/stack_create_dto.dart'; part 'model/stack_create_dto.dart';
part 'model/stack_response_dto.dart'; part 'model/stack_response_dto.dart';
part 'model/stack_update_dto.dart'; part 'model/stack_update_dto.dart';
part 'model/statistics_search_dto.dart';
part 'model/sync_ack_delete_dto.dart'; part 'model/sync_ack_delete_dto.dart';
part 'model/sync_ack_dto.dart'; part 'model/sync_ack_dto.dart';
part 'model/sync_ack_set_dto.dart'; part 'model/sync_ack_set_dto.dart';

View File

@ -193,6 +193,53 @@ class SearchApi {
return null; return null;
} }
/// Performs an HTTP 'POST /search/statistics' operation and returns the [Response].
/// Parameters:
///
/// * [StatisticsSearchDto] statisticsSearchDto (required):
Future<Response> searchAssetStatisticsWithHttpInfo(StatisticsSearchDto statisticsSearchDto,) async {
// ignore: prefer_const_declarations
final apiPath = r'/search/statistics';
// ignore: prefer_final_locals
Object? postBody = statisticsSearchDto;
final queryParams = <QueryParam>[];
final headerParams = <String, String>{};
final formParams = <String, String>{};
const contentTypes = <String>['application/json'];
return apiClient.invokeAPI(
apiPath,
'POST',
queryParams,
postBody,
headerParams,
formParams,
contentTypes.isEmpty ? null : contentTypes.first,
);
}
/// Parameters:
///
/// * [StatisticsSearchDto] statisticsSearchDto (required):
Future<SearchStatisticsResponseDto?> searchAssetStatistics(StatisticsSearchDto statisticsSearchDto,) async {
final response = await searchAssetStatisticsWithHttpInfo(statisticsSearchDto,);
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) {
return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'SearchStatisticsResponseDto',) as SearchStatisticsResponseDto;
}
return null;
}
/// Performs an HTTP 'POST /search/metadata' operation and returns the [Response]. /// Performs an HTTP 'POST /search/metadata' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///

View File

@ -484,6 +484,8 @@ class ApiClient {
return SearchFacetResponseDto.fromJson(value); return SearchFacetResponseDto.fromJson(value);
case 'SearchResponseDto': case 'SearchResponseDto':
return SearchResponseDto.fromJson(value); return SearchResponseDto.fromJson(value);
case 'SearchStatisticsResponseDto':
return SearchStatisticsResponseDto.fromJson(value);
case 'SearchSuggestionType': case 'SearchSuggestionType':
return SearchSuggestionTypeTypeTransformer().decode(value); return SearchSuggestionTypeTypeTransformer().decode(value);
case 'ServerAboutResponseDto': case 'ServerAboutResponseDto':
@ -540,6 +542,8 @@ class ApiClient {
return StackResponseDto.fromJson(value); return StackResponseDto.fromJson(value);
case 'StackUpdateDto': case 'StackUpdateDto':
return StackUpdateDto.fromJson(value); return StackUpdateDto.fromJson(value);
case 'StatisticsSearchDto':
return StatisticsSearchDto.fromJson(value);
case 'SyncAckDeleteDto': case 'SyncAckDeleteDto':
return SyncAckDeleteDto.fromJson(value); return SyncAckDeleteDto.fromJson(value);
case 'SyncAckDto': case 'SyncAckDto':

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

View File

@ -0,0 +1,500 @@
//
// 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 StatisticsSearchDto {
/// Returns a new [StatisticsSearchDto] instance.
StatisticsSearchDto({
this.city,
this.country,
this.createdAfter,
this.createdBefore,
this.description,
this.deviceId,
this.isEncoded,
this.isFavorite,
this.isMotion,
this.isNotInAlbum,
this.isOffline,
this.lensModel,
this.libraryId,
this.make,
this.model,
this.personIds = const [],
this.rating,
this.state,
this.tagIds = const [],
this.takenAfter,
this.takenBefore,
this.trashedAfter,
this.trashedBefore,
this.type,
this.updatedAfter,
this.updatedBefore,
this.visibility,
});
String? city;
String? 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.
///
DateTime? createdAfter;
///
/// Please note: 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.
///
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.
///
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.
///
String? deviceId;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? 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.
///
bool? 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.
///
bool? 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.
///
bool? isNotInAlbum;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
bool? isOffline;
String? lensModel;
String? 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.
///
String? make;
String? model;
List<String> personIds;
/// Minimum value: -1
/// Maximum value: 5
///
/// Please note: 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.
///
num? rating;
String? state;
List<String> 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.
///
DateTime? takenAfter;
///
/// Please note: 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.
///
DateTime? takenBefore;
///
/// Please note: 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.
///
DateTime? trashedAfter;
///
/// Please note: 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.
///
DateTime? trashedBefore;
///
/// Please note: 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.
///
AssetTypeEnum? 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.
///
DateTime? updatedAfter;
///
/// Please note: 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.
///
DateTime? updatedBefore;
///
/// Please note: 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.
///
AssetVisibility? visibility;
@override
bool operator ==(Object other) => identical(this, other) || other is StatisticsSearchDto &&
other.city == city &&
other.country == country &&
other.createdAfter == createdAfter &&
other.createdBefore == createdBefore &&
other.description == description &&
other.deviceId == deviceId &&
other.isEncoded == isEncoded &&
other.isFavorite == isFavorite &&
other.isMotion == isMotion &&
other.isNotInAlbum == isNotInAlbum &&
other.isOffline == isOffline &&
other.lensModel == lensModel &&
other.libraryId == libraryId &&
other.make == make &&
other.model == model &&
_deepEquality.equals(other.personIds, personIds) &&
other.rating == rating &&
other.state == state &&
_deepEquality.equals(other.tagIds, tagIds) &&
other.takenAfter == takenAfter &&
other.takenBefore == takenBefore &&
other.trashedAfter == trashedAfter &&
other.trashedBefore == trashedBefore &&
other.type == type &&
other.updatedAfter == updatedAfter &&
other.updatedBefore == updatedBefore &&
other.visibility == visibility;
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(city == null ? 0 : city!.hashCode) +
(country == null ? 0 : country!.hashCode) +
(createdAfter == null ? 0 : createdAfter!.hashCode) +
(createdBefore == null ? 0 : createdBefore!.hashCode) +
(description == null ? 0 : description!.hashCode) +
(deviceId == null ? 0 : deviceId!.hashCode) +
(isEncoded == null ? 0 : isEncoded!.hashCode) +
(isFavorite == null ? 0 : isFavorite!.hashCode) +
(isMotion == null ? 0 : isMotion!.hashCode) +
(isNotInAlbum == null ? 0 : isNotInAlbum!.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) +
(personIds.hashCode) +
(rating == null ? 0 : rating!.hashCode) +
(state == null ? 0 : state!.hashCode) +
(tagIds.hashCode) +
(takenAfter == null ? 0 : takenAfter!.hashCode) +
(takenBefore == null ? 0 : takenBefore!.hashCode) +
(trashedAfter == null ? 0 : trashedAfter!.hashCode) +
(trashedBefore == null ? 0 : trashedBefore!.hashCode) +
(type == null ? 0 : type!.hashCode) +
(updatedAfter == null ? 0 : updatedAfter!.hashCode) +
(updatedBefore == null ? 0 : updatedBefore!.hashCode) +
(visibility == null ? 0 : visibility!.hashCode);
@override
String toString() => 'StatisticsSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, description=$description, deviceId=$deviceId, isEncoded=$isEncoded, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, 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>{};
if (this.city != null) {
json[r'city'] = this.city;
} else {
// json[r'city'] = null;
}
if (this.country != null) {
json[r'country'] = this.country;
} else {
// json[r'country'] = null;
}
if (this.createdAfter != null) {
json[r'createdAfter'] = this.createdAfter!.toUtc().toIso8601String();
} else {
// json[r'createdAfter'] = null;
}
if (this.createdBefore != null) {
json[r'createdBefore'] = this.createdBefore!.toUtc().toIso8601String();
} else {
// json[r'createdBefore'] = null;
}
if (this.description != null) {
json[r'description'] = this.description;
} else {
// json[r'description'] = null;
}
if (this.deviceId != null) {
json[r'deviceId'] = this.deviceId;
} else {
// json[r'deviceId'] = null;
}
if (this.isEncoded != null) {
json[r'isEncoded'] = this.isEncoded;
} else {
// json[r'isEncoded'] = null;
}
if (this.isFavorite != null) {
json[r'isFavorite'] = this.isFavorite;
} else {
// json[r'isFavorite'] = null;
}
if (this.isMotion != null) {
json[r'isMotion'] = this.isMotion;
} else {
// json[r'isMotion'] = null;
}
if (this.isNotInAlbum != null) {
json[r'isNotInAlbum'] = this.isNotInAlbum;
} else {
// json[r'isNotInAlbum'] = null;
}
if (this.isOffline != null) {
json[r'isOffline'] = this.isOffline;
} else {
// json[r'isOffline'] = null;
}
if (this.lensModel != null) {
json[r'lensModel'] = this.lensModel;
} else {
// json[r'lensModel'] = null;
}
if (this.libraryId != null) {
json[r'libraryId'] = this.libraryId;
} else {
// json[r'libraryId'] = null;
}
if (this.make != null) {
json[r'make'] = this.make;
} else {
// json[r'make'] = null;
}
if (this.model != null) {
json[r'model'] = this.model;
} else {
// json[r'model'] = null;
}
json[r'personIds'] = this.personIds;
if (this.rating != null) {
json[r'rating'] = this.rating;
} else {
// json[r'rating'] = null;
}
if (this.state != null) {
json[r'state'] = this.state;
} else {
// json[r'state'] = null;
}
json[r'tagIds'] = this.tagIds;
if (this.takenAfter != null) {
json[r'takenAfter'] = this.takenAfter!.toUtc().toIso8601String();
} else {
// json[r'takenAfter'] = null;
}
if (this.takenBefore != null) {
json[r'takenBefore'] = this.takenBefore!.toUtc().toIso8601String();
} else {
// json[r'takenBefore'] = null;
}
if (this.trashedAfter != null) {
json[r'trashedAfter'] = this.trashedAfter!.toUtc().toIso8601String();
} else {
// json[r'trashedAfter'] = null;
}
if (this.trashedBefore != null) {
json[r'trashedBefore'] = this.trashedBefore!.toUtc().toIso8601String();
} else {
// json[r'trashedBefore'] = null;
}
if (this.type != null) {
json[r'type'] = this.type;
} else {
// json[r'type'] = null;
}
if (this.updatedAfter != null) {
json[r'updatedAfter'] = this.updatedAfter!.toUtc().toIso8601String();
} else {
// json[r'updatedAfter'] = null;
}
if (this.updatedBefore != null) {
json[r'updatedBefore'] = this.updatedBefore!.toUtc().toIso8601String();
} else {
// json[r'updatedBefore'] = null;
}
if (this.visibility != null) {
json[r'visibility'] = this.visibility;
} else {
// json[r'visibility'] = null;
}
return json;
}
/// Returns a new [StatisticsSearchDto] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
static StatisticsSearchDto? fromJson(dynamic value) {
upgradeDto(value, "StatisticsSearchDto");
if (value is Map) {
final json = value.cast<String, dynamic>();
return StatisticsSearchDto(
city: mapValueOfType<String>(json, r'city'),
country: mapValueOfType<String>(json, r'country'),
createdAfter: mapDateTime(json, r'createdAfter', r''),
createdBefore: mapDateTime(json, r'createdBefore', r''),
description: mapValueOfType<String>(json, r'description'),
deviceId: mapValueOfType<String>(json, r'deviceId'),
isEncoded: mapValueOfType<bool>(json, r'isEncoded'),
isFavorite: mapValueOfType<bool>(json, r'isFavorite'),
isMotion: mapValueOfType<bool>(json, r'isMotion'),
isNotInAlbum: mapValueOfType<bool>(json, r'isNotInAlbum'),
isOffline: mapValueOfType<bool>(json, r'isOffline'),
lensModel: mapValueOfType<String>(json, r'lensModel'),
libraryId: mapValueOfType<String>(json, r'libraryId'),
make: mapValueOfType<String>(json, r'make'),
model: mapValueOfType<String>(json, r'model'),
personIds: json[r'personIds'] is Iterable
? (json[r'personIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
rating: num.parse('${json[r'rating']}'),
state: mapValueOfType<String>(json, r'state'),
tagIds: json[r'tagIds'] is Iterable
? (json[r'tagIds'] as Iterable).cast<String>().toList(growable: false)
: const [],
takenAfter: mapDateTime(json, r'takenAfter', r''),
takenBefore: mapDateTime(json, r'takenBefore', r''),
trashedAfter: mapDateTime(json, r'trashedAfter', r''),
trashedBefore: mapDateTime(json, r'trashedBefore', r''),
type: AssetTypeEnum.fromJson(json[r'type']),
updatedAfter: mapDateTime(json, r'updatedAfter', r''),
updatedBefore: mapDateTime(json, r'updatedBefore', r''),
visibility: AssetVisibility.fromJson(json[r'visibility']),
);
}
return null;
}
static List<StatisticsSearchDto> listFromJson(dynamic json, {bool growable = false,}) {
final result = <StatisticsSearchDto>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = StatisticsSearchDto.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
static Map<String, StatisticsSearchDto> mapFromJson(dynamic json) {
final map = <String, StatisticsSearchDto>{};
if (json is Map && json.isNotEmpty) {
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
for (final entry in json.entries) {
final value = StatisticsSearchDto.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
}
}
return map;
}
// maps a json object with a list of StatisticsSearchDto-objects as value to a dart map
static Map<String, List<StatisticsSearchDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
final map = <String, List<StatisticsSearchDto>>{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast<String, dynamic>();
for (final entry in json.entries) {
map[entry.key] = StatisticsSearchDto.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

@ -5158,6 +5158,48 @@
] ]
} }
}, },
"/search/statistics": {
"post": {
"operationId": "searchAssetStatistics",
"parameters": [],
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/StatisticsSearchDto"
}
}
},
"required": true
},
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SearchStatisticsResponseDto"
}
}
},
"description": ""
}
},
"security": [
{
"bearer": []
},
{
"cookie": []
},
{
"api_key": []
}
],
"tags": [
"Search"
]
}
},
"/search/suggestions": { "/search/suggestions": {
"get": { "get": {
"operationId": "getSearchSuggestions", "operationId": "getSearchSuggestions",
@ -12069,6 +12111,17 @@
], ],
"type": "object" "type": "object"
}, },
"SearchStatisticsResponseDto": {
"properties": {
"total": {
"type": "integer"
}
},
"required": [
"total"
],
"type": "object"
},
"SearchSuggestionType": { "SearchSuggestionType": {
"enum": [ "enum": [
"country", "country",
@ -12974,6 +13027,125 @@
}, },
"type": "object" "type": "object"
}, },
"StatisticsSearchDto": {
"properties": {
"city": {
"nullable": true,
"type": "string"
},
"country": {
"nullable": true,
"type": "string"
},
"createdAfter": {
"format": "date-time",
"type": "string"
},
"createdBefore": {
"format": "date-time",
"type": "string"
},
"description": {
"type": "string"
},
"deviceId": {
"type": "string"
},
"isEncoded": {
"type": "boolean"
},
"isFavorite": {
"type": "boolean"
},
"isMotion": {
"type": "boolean"
},
"isNotInAlbum": {
"type": "boolean"
},
"isOffline": {
"type": "boolean"
},
"lensModel": {
"nullable": true,
"type": "string"
},
"libraryId": {
"format": "uuid",
"nullable": true,
"type": "string"
},
"make": {
"type": "string"
},
"model": {
"nullable": true,
"type": "string"
},
"personIds": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"rating": {
"maximum": 5,
"minimum": -1,
"type": "number"
},
"state": {
"nullable": true,
"type": "string"
},
"tagIds": {
"items": {
"format": "uuid",
"type": "string"
},
"type": "array"
},
"takenAfter": {
"format": "date-time",
"type": "string"
},
"takenBefore": {
"format": "date-time",
"type": "string"
},
"trashedAfter": {
"format": "date-time",
"type": "string"
},
"trashedBefore": {
"format": "date-time",
"type": "string"
},
"type": {
"allOf": [
{
"$ref": "#/components/schemas/AssetTypeEnum"
}
]
},
"updatedAfter": {
"format": "date-time",
"type": "string"
},
"updatedBefore": {
"format": "date-time",
"type": "string"
},
"visibility": {
"allOf": [
{
"$ref": "#/components/schemas/AssetVisibility"
}
]
}
},
"type": "object"
},
"SyncAckDeleteDto": { "SyncAckDeleteDto": {
"properties": { "properties": {
"types": { "types": {

View File

@ -995,6 +995,38 @@ export type SmartSearchDto = {
withDeleted?: boolean; withDeleted?: boolean;
withExif?: boolean; withExif?: boolean;
}; };
export type StatisticsSearchDto = {
city?: string | null;
country?: string | null;
createdAfter?: string;
createdBefore?: string;
description?: string;
deviceId?: string;
isEncoded?: boolean;
isFavorite?: boolean;
isMotion?: boolean;
isNotInAlbum?: boolean;
isOffline?: boolean;
lensModel?: string | null;
libraryId?: string | null;
make?: string;
model?: string | null;
personIds?: string[];
rating?: number;
state?: string | null;
tagIds?: string[];
takenAfter?: string;
takenBefore?: string;
trashedAfter?: string;
trashedBefore?: string;
"type"?: AssetTypeEnum;
updatedAfter?: string;
updatedBefore?: string;
visibility?: AssetVisibility;
};
export type SearchStatisticsResponseDto = {
total: number;
};
export type ServerAboutResponseDto = { export type ServerAboutResponseDto = {
build?: string; build?: string;
buildImage?: string; buildImage?: string;
@ -2882,6 +2914,18 @@ export function searchSmart({ smartSearchDto }: {
body: smartSearchDto body: smartSearchDto
}))); })));
} }
export function searchAssetStatistics({ statisticsSearchDto }: {
statisticsSearchDto: StatisticsSearchDto;
}, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{
status: 200;
data: SearchStatisticsResponseDto;
}>("/search/statistics", oazapfts.json({
...opts,
method: "POST",
body: statisticsSearchDto
})));
}
export function getSearchSuggestions({ country, includeNull, make, model, state, $type }: { export function getSearchSuggestions({ country, includeNull, make, model, state, $type }: {
country?: string; country?: string;
includeNull?: boolean; includeNull?: boolean;

View File

@ -11,8 +11,10 @@ import {
SearchPeopleDto, SearchPeopleDto,
SearchPlacesDto, SearchPlacesDto,
SearchResponseDto, SearchResponseDto,
SearchStatisticsResponseDto,
SearchSuggestionRequestDto, SearchSuggestionRequestDto,
SmartSearchDto, SmartSearchDto,
StatisticsSearchDto,
} from 'src/dtos/search.dto'; } from 'src/dtos/search.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Auth, Authenticated } from 'src/middleware/auth.guard';
import { SearchService } from 'src/services/search.service'; import { SearchService } from 'src/services/search.service';
@ -29,6 +31,13 @@ export class SearchController {
return this.service.searchMetadata(auth, dto); return this.service.searchMetadata(auth, dto);
} }
@Post('statistics')
@HttpCode(HttpStatus.OK)
@Authenticated()
searchAssetStatistics(@Auth() auth: AuthDto, @Body() dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
return this.service.searchStatistics(auth, dto);
}
@Post('random') @Post('random')
@HttpCode(HttpStatus.OK) @HttpCode(HttpStatus.OK)
@Authenticated() @Authenticated()

View File

@ -37,12 +37,6 @@ class BaseSearchDto {
@ValidateAssetVisibility({ optional: true }) @ValidateAssetVisibility({ optional: true })
visibility?: AssetVisibility; visibility?: AssetVisibility;
@ValidateBoolean({ optional: true })
withDeleted?: boolean;
@ValidateBoolean({ optional: true })
withExif?: boolean;
@ValidateDate({ optional: true }) @ValidateDate({ optional: true })
createdBefore?: Date; createdBefore?: Date;
@ -92,13 +86,6 @@ class BaseSearchDto {
@Optional({ nullable: true, emptyToNull: true }) @Optional({ nullable: true, emptyToNull: true })
lensModel?: string | null; lensModel?: string | null;
@IsInt()
@Min(1)
@Max(1000)
@Type(() => Number)
@Optional()
size?: number;
@ValidateBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isNotInAlbum?: boolean; isNotInAlbum?: boolean;
@ -115,7 +102,22 @@ class BaseSearchDto {
rating?: number; rating?: number;
} }
export class RandomSearchDto extends BaseSearchDto { class BaseSearchWithResultsDto extends BaseSearchDto {
@ValidateBoolean({ optional: true })
withDeleted?: boolean;
@ValidateBoolean({ optional: true })
withExif?: boolean;
@IsInt()
@Min(1)
@Max(1000)
@Type(() => Number)
@Optional()
size?: number;
}
export class RandomSearchDto extends BaseSearchWithResultsDto {
@ValidateBoolean({ optional: true }) @ValidateBoolean({ optional: true })
withStacked?: boolean; withStacked?: boolean;
@ -179,7 +181,14 @@ export class MetadataSearchDto extends RandomSearchDto {
page?: number; page?: number;
} }
export class SmartSearchDto extends BaseSearchDto { export class StatisticsSearchDto extends BaseSearchDto {
@IsString()
@IsNotEmpty()
@Optional()
description?: string;
}
export class SmartSearchDto extends BaseSearchWithResultsDto {
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
query!: string; query!: string;
@ -299,6 +308,11 @@ export class SearchResponseDto {
assets!: SearchAssetResponseDto; assets!: SearchAssetResponseDto;
} }
export class SearchStatisticsResponseDto {
@ApiProperty({ type: 'integer' })
total!: number;
}
class SearchExploreItem { class SearchExploreItem {
value!: string; value!: string;
data!: AssetResponseDto; data!: AssetResponseDto;

View File

@ -20,6 +20,20 @@ limit
offset offset
$7 $7
-- SearchRepository.searchStatistics
select
count(*) as "total"
from
"assets"
inner join "exif" on "assets"."id" = "exif"."assetId"
where
"assets"."visibility" = $1
and "assets"."fileCreatedAt" >= $2
and "exif"."lensModel" = $3
and "assets"."ownerId" = any ($4::uuid[])
and "assets"."isFavorite" = $5
and "assets"."deletedAt" is null
-- SearchRepository.searchRandom -- SearchRepository.searchRandom
( (
select select

View File

@ -185,6 +185,7 @@ export class SearchRepository {
async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) { async searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions) {
const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection; const orderDirection = (options.orderDirection?.toLowerCase() || 'desc') as OrderByDirection;
const items = await searchAssetBuilder(this.db, options) const items = await searchAssetBuilder(this.db, options)
.selectAll('assets')
.orderBy('assets.fileCreatedAt', orderDirection) .orderBy('assets.fileCreatedAt', orderDirection)
.limit(pagination.size + 1) .limit(pagination.size + 1)
.offset((pagination.page - 1) * pagination.size) .offset((pagination.page - 1) * pagination.size)
@ -193,6 +194,22 @@ export class SearchRepository {
return paginationHelper(items, pagination.size); return paginationHelper(items, pagination.size);
} }
@GenerateSql({
params: [
{
takenAfter: DummyValue.DATE,
lensModel: DummyValue.STRING,
isFavorite: true,
userIds: [DummyValue.UUID],
},
],
})
searchStatistics(options: AssetSearchOptions) {
return searchAssetBuilder(this.db, options)
.select((qb) => qb.fn.countAll<number>().as('total'))
.executeTakeFirstOrThrow();
}
@GenerateSql({ @GenerateSql({
params: [ params: [
100, 100,
@ -209,10 +226,12 @@ export class SearchRepository {
const uuid = randomUUID(); const uuid = randomUUID();
const builder = searchAssetBuilder(this.db, options); const builder = searchAssetBuilder(this.db, options);
const lessThan = builder const lessThan = builder
.selectAll('assets')
.where('assets.id', '<', uuid) .where('assets.id', '<', uuid)
.orderBy(sql`random()`) .orderBy(sql`random()`)
.limit(size); .limit(size);
const greaterThan = builder const greaterThan = builder
.selectAll('assets')
.where('assets.id', '>', uuid) .where('assets.id', '>', uuid)
.orderBy(sql`random()`) .orderBy(sql`random()`)
.limit(size); .limit(size);
@ -241,6 +260,7 @@ export class SearchRepository {
return this.db.transaction().execute(async (trx) => { return this.db.transaction().execute(async (trx) => {
await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx); await sql`set local vchordrq.probes = ${sql.lit(probes[VectorIndex.CLIP])}`.execute(trx);
const items = await searchAssetBuilder(trx, options) const items = await searchAssetBuilder(trx, options)
.selectAll('assets')
.innerJoin('smart_search', 'assets.id', 'smart_search.assetId') .innerJoin('smart_search', 'assets.id', 'smart_search.assetId')
.orderBy(sql`smart_search.embedding <=> ${options.embedding}`) .orderBy(sql`smart_search.embedding <=> ${options.embedding}`)
.limit(pagination.size + 1) .limit(pagination.size + 1)

View File

@ -10,9 +10,11 @@ import {
SearchPeopleDto, SearchPeopleDto,
SearchPlacesDto, SearchPlacesDto,
SearchResponseDto, SearchResponseDto,
SearchStatisticsResponseDto,
SearchSuggestionRequestDto, SearchSuggestionRequestDto,
SearchSuggestionType, SearchSuggestionType,
SmartSearchDto, SmartSearchDto,
StatisticsSearchDto,
} from 'src/dtos/search.dto'; } from 'src/dtos/search.dto';
import { AssetOrder, AssetVisibility } from 'src/enum'; import { AssetOrder, AssetVisibility } from 'src/enum';
import { BaseService } from 'src/services/base.service'; import { BaseService } from 'src/services/base.service';
@ -67,6 +69,15 @@ export class SearchService extends BaseService {
return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth }); return this.mapResponse(items, hasNextPage ? (page + 1).toString() : null, { auth });
} }
async searchStatistics(auth: AuthDto, dto: StatisticsSearchDto): Promise<SearchStatisticsResponseDto> {
const userIds = await this.getUserIdsToSearch(auth);
return await this.searchRepository.searchStatistics({
...dto,
userIds,
});
}
async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> { async searchRandom(auth: AuthDto, dto: RandomSearchDto): Promise<AssetResponseDto[]> {
if (dto.visibility === AssetVisibility.LOCKED) { if (dto.visibility === AssetVisibility.LOCKED) {
requireElevatedPermission(auth); requireElevatedPermission(auth);

View File

@ -291,7 +291,6 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
return kysely return kysely
.withPlugin(joinDeduplicationPlugin) .withPlugin(joinDeduplicationPlugin)
.selectFrom('assets') .selectFrom('assets')
.selectAll('assets')
.where('assets.visibility', '=', visibility) .where('assets.visibility', '=', visibility)
.$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!)) .$if(!!options.tagIds && options.tagIds.length > 0, (qb) => hasTags(qb, options.tagIds!))
.$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!)) .$if(!!options.personIds && options.personIds.length > 0, (qb) => hasPeople(qb, options.personIds!))